add ui views

This commit is contained in:
ruki
2018-11-19 22:48:45 +08:00
parent 4a0792c7b0
commit dcc76e8f52
39 changed files with 5570 additions and 11 deletions

View File

@@ -2352,7 +2352,6 @@ __export int luaopen_ltui_curses (lua_State *L)
return 1;
}
/* initialize the character map table with the known values after
** curses initialization (for ACS_xxx values) */
static void init_ascii_map()

View File

@@ -1,8 +1,8 @@
-- add target
target("lcurses")
-- make as a static library
set_kind("static")
-- only make objects
set_kind("object")
-- add deps
if is_plat("windows") then

View File

@@ -1,8 +1,8 @@
-- add target
target("pdcurses")
-- make as a static library
set_kind("static")
-- only make objects
set_kind("object")
-- add the common source files
add_files("**.c")

View File

@@ -31,12 +31,6 @@ end
-- add requires
add_requires("luajit")
-- add projects
includes("lcurses")
if is_plat("windows") then
includes("pdcurses")
end
-- add target
target("ltui")
@@ -45,3 +39,12 @@ target("ltui")
-- add deps
add_deps("lcurses")
-- set target directory
set_targetdir("$(buildir)")
-- add projects
includes("lcurses")
if is_plat("windows") then
includes("pdcurses")
end

53
src/ltui/action.lua Normal file
View File

@@ -0,0 +1,53 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file action.lua
--
-- load modules
local log = require("ltui/base/log")
local object = require("ltui/object")
-- define module
local action = action or object { }
-- register action types
function action:register(tag, ...)
local base = self[tag] or 0
local enums = {...}
local n = #enums
for i = 1, n do
self[enums[i]] = i + base
end
self[tag] = base + n
end
-- register action enums
action:register("ac_max",
"ac_on_text_changed",
"ac_on_selected",
"ac_on_enter",
"ac_on_load",
"ac_on_save",
"ac_on_exit")
-- return module
return action

124
src/ltui/application.lua Normal file
View File

@@ -0,0 +1,124 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file application.lua
--
-- load modules
local os = require("ltui/base/os")
local log = require("ltui/base/log")
local rect = require("ltui/rect")
local event = require("ltui/event")
local curses = require("ltui/curses")
local program = require("ltui/program")
local desktop = require("ltui/desktop")
local menubar = require("ltui/menubar")
local statusbar = require("ltui/statusbar")
-- define module
local application = application or program()
-- init application
function application:init(name, argv)
-- init log
log:clear()
-- log:enable(false)
-- trace
log:print("<application: %s>: init ..", name)
-- init program
program.init(self, name, argv)
-- trace
log:print("<application: %s>: init ok", name)
end
-- exit application
function application:exit()
-- exit program
program.exit(self)
-- flush log
log:flush()
end
-- get menubar
function application:menubar()
if not self._MENUBAR then
self._MENUBAR = menubar:new("menubar", rect{0, 0, self:width(), 1})
end
return self._MENUBAR
end
-- get desktop
function application:desktop()
if not self._DESKTOP then
self._DESKTOP = desktop:new("desktop", rect{0, 1, self:width(), self:height() - 1})
end
return self._DESKTOP
end
-- get statusbar
function application:statusbar()
if not self._STATUSBAR then
self._STATUSBAR = statusbar:new("statusbar", rect{0, self:height() - 1, self:width(), self:height()})
end
return self._STATUSBAR
end
-- on event
function application:event_on(e)
program.event_on(self, e)
end
-- run application
function application:run(...)
-- init runner
local argv = {...}
local runner = function ()
-- new an application
local app = self:new(argv)
if app then
app:loop()
app:exit()
end
end
-- run application
local ok, errors = xpcall(runner, debug.traceback)
-- exit curses
if not ok then
if not curses.isdone() then
curses.done()
end
log:flush()
os.raise(errors)
end
end
-- return module
return application

213
src/ltui/base/dlist.lua Normal file
View File

@@ -0,0 +1,213 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file dlist.lua
--
-- load modules
local object = require("ltui/object")
-- define module
local dlist = dlist or object { _init = {"_length"} } {0}
-- clear list
function dlist:clear()
self._length = 0
self._first = nil
self._last = nil
end
-- push item to tail
function dlist:push(t)
assert(t)
if self._last then
self._last._next = t
t._prev = self._last
self._last = t
else
self._first = t
self._last = t
end
self._length = self._length + 1
end
-- insert item after the given item
function dlist:insert(t, after)
assert(t)
if not after then
return self:push(t)
end
assert(t ~= after)
if after._next then
after._next._prev = t
t._next = after._next
else
self._last = t
end
t._prev = after
after._next = t
self._length = self._length + 1
end
-- pop item from tail
function dlist:pop()
if not self._last then return end
local t = self._last
if t._prev then
t._prev._next = nil
self._last = t._prev
t._prev = nil
else
self._first = nil
self._last = nil
end
self._length = self._length - 1
return t
end
-- shift item: 1 2 3 <- 2 3
function dlist:shift()
if not self._first then return end
local t = self._first
if t._next then
t._next._prev = nil
self._first = t._next
t._next = nil
else
self._first = nil
self._last = nil
end
self._length = self._length - 1
return t
end
-- unshift item: 1 2 -> t 1 2
function dlist:unshift(t)
assert(t)
if self._first then
self._first._prev = t
t._next = self._first
self._first = t
else
self._first = t
self._last = t
end
self._length = self._length + 1
end
-- remove item
function dlist:remove(t)
assert(t)
if t._next then
if t._prev then
t._next._prev = t._prev
t._prev._next = t._next
else
assert(t == self._first)
t._next._prev = nil
self._first = t._next
end
elseif t._prev then
assert(t == self._last)
t._prev._next = nil
self._last = t._prev
else
assert(t == self._first and t == self._last)
self._first = nil
self._last = nil
end
t._next = nil
t._prev = nil
self._length = self._length - 1
return t
end
-- get first item
function dlist:first()
return self._first
end
-- get last item
function dlist:last()
return self._last
end
-- get next item
function dlist:next(last)
if last then
return last._next
else
return self._first
end
end
-- get the previous item
function dlist:prev(last)
if last then
return last._prev
else
return self._last
end
end
-- get list size
function dlist:size()
return self._length
end
-- is empty?
function dlist:empty()
return self:size() == 0
end
-- get items
--
-- .e.g
--
-- for item in dlist:items() do
-- print(item)
-- end
--
function dlist:items()
-- init iterator
local iter = function (list, item)
return list:next(item)
end
-- return iterator and initialized state
return iter, self, nil
end
-- get reverse items
function dlist:ritems()
-- init iterator
local iter = function (list, item)
return list:prev(item)
end
-- return iterator and initialized state
return iter, self, nil
end
-- return module: dlist
return dlist

144
src/ltui/base/log.lua Normal file
View File

@@ -0,0 +1,144 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--o
-- @author ruki
-- @file log.lua
--
-- define module: log
local log = log or {}
-- get the log file
function log:file()
-- disable?
if self._ENABLE ~= nil and not self._ENABLE then
return
end
-- get the output file
if self._FILE == nil then
local outputfile = self:outputfile()
if outputfile then
-- get directory
local i = outputfile:find_last("[/\\]")
if i then
if i > 1 then i = i - 1 end
dir = outputfile:sub(1, i)
else
dir = "."
end
-- ensure the directory
if not os.isdir(dir) then
os.mkdir(dir)
end
-- open the log file
self._FILE = io.open(outputfile, 'w+')
end
self._FILE = self._FILE or false
end
return self._FILE
end
-- get the output file
function log:outputfile()
if self._LOGFILE == nil then
self._LOGFILE = os.getenv("LTUI_LOGFILE") or false
end
return self._LOGFILE
end
-- clear log
function log:clear(state)
if os.isfile(self:outputfile()) then
io.writefile(self:outputfile(), "")
end
end
-- enable log
function log:enable(state)
self._ENABLE = state
end
-- flush log to file
function log:flush()
local file = self:file()
if file then
io.flush(file)
end
end
-- close the log file
function log:close()
local file = self:file()
if file then
file:close()
end
end
-- print log to the log file
function log:print(...)
local file = self:file()
if file then
file:write(string.format(...) .. "\n")
end
end
-- print variables to the log file
function log:printv(...)
local file = self:file()
if file then
local values = {...}
for i, v in ipairs(values) do
-- dump basic type
if type(v) == "string" or type(v) == "boolean" or type(v) == "number" then
file:write(tostring(v))
else
file:write("<" .. tostring(v) .. ">")
end
if i ~= #values then
file:write(" ")
end
end
file:write('\n')
end
end
-- printf log to the log file
function log:printf(...)
local file = self:file()
if file then
file:write(string.format(...))
end
end
-- write log the log file
function log:write(...)
local file = self:file()
if file then
file:write(...)
end
end
-- return module: log
return log

109
src/ltui/base/os.lua Normal file
View File

@@ -0,0 +1,109 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file os.lua
--
-- define module
local os = os or {}
-- load modules
local string = require("ltui/base/string")
-- raise an exception and abort the current script
--
-- the parent function will capture it if we uses pcall or xpcall
--
function os.raise(msg, ...)
-- raise it
if msg then
error(string.tryformat(msg, ...))
else
error()
end
end
-- run program
function os.run(cmd, ...)
return os.execute(string.tryformat(cmd, ...))
end
-- run program and get io output
function os.iorun(cmd, ...)
local ok = false
local outs = nil
local file = io.popen(string.tryformat(cmd, ...), "r")
if file then
outs = file:read("*a"):trim()
file:close()
ok = true
end
return ok, outs
end
-- get host name
function os.host()
if os._HOST == nil then
local ok, result = os.iorun("uname")
if ok then
if result:lower():find("linux", 1, true) then
os._HOST = "linux"
elseif result:lower():find("darwin", 1, true) then
os._HOST = "macosx"
end
elseif os.run("cmd /c ver") == 0 then
os._HOST = "windows"
end
end
return os._HOST
end
-- read string data from pasteboard
function os.pbpaste()
if os.host() == "macosx" then
local ok, result = os.iorun("pbpaste")
if ok then
return result
end
elseif os.host() == "linux" then
local ok, result = os.iorun("xsel --clipboard --output")
if ok then
return result
end
else
-- TODO
end
end
-- copy string data to pasteboard
function os.pbcopy(data)
if os.host() == "macosx" then
os.run("bash -c \"echo '" .. data .. "' | pbcopy\"")
elseif os.host() == "linux" then
os.run("bash -c \"echo '" .. data .. "' | xsel --clipboard --input\"")
else
-- TODO
end
end
-- return module
return os

127
src/ltui/base/path.lua Normal file
View File

@@ -0,0 +1,127 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file path.lua
--
-- define module: path
local path = path or {}
-- load modules
local string = require("ltui/base/string")
-- get the directory of the path
function path.directory(p)
local i = p:find_last("[/\\]")
if i then
if i > 1 then i = i - 1 end
return p:sub(1, i)
else
return "."
end
end
-- get the filename of the path
function path.filename(p)
local i = p:find_last("[/\\]")
if i then
return p:sub(i + 1)
else
return p
end
end
-- get the basename of the path
function path.basename(p)
local name = path.filename(p)
local i = name:find_last(".", true)
if i then
return name:sub(1, i - 1)
else
return name
end
end
-- get the file extension of the path: .xxx
function path.extension(p)
-- check
assert(p)
-- get extension
local i = p:find_last(".", true)
if i then
return p:sub(i)
else
return ""
end
end
-- join path
function path.join(p, ...)
-- check
assert(p)
-- join them
for _, name in ipairs({...}) do
p = p .. "/" .. name
end
-- translate path
return path.translate(p)
end
-- split path by the separator
function path.split(p)
return p:split("/\\")
end
-- get the path seperator
function path.seperator()
return xmake._HOST == "windows" and '\\' or '/'
end
-- the last character is the path seperator?
function path.islastsep(p)
local sep = p:sub(#p, #p)
return xmake._HOST == "windows" and (sep == '\\' or sep == '/') or (sep == '/')
end
-- convert path pattern to a lua pattern
function path.pattern(pattern)
-- translate wildcards, .e.g *, **
pattern = pattern:gsub("([%+%.%-%^%$%(%)%%])", "%%%1")
pattern = pattern:gsub("%*%*", "\001")
pattern = pattern:gsub("%*", "\002")
pattern = pattern:gsub("\001", ".*")
pattern = pattern:gsub("\002", "[^/]*")
-- case-insensitive filesystem?
if not os.fscase() then
pattern = string.ipattern(pattern, true)
end
return pattern
end
-- return module: path
return path

215
src/ltui/base/string.lua Normal file
View File

@@ -0,0 +1,215 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file string.lua
--
-- define module: string
local string = string or {}
-- find the last substring with the given pattern
function string:find_last(pattern, plain)
-- find the last substring
local curr = 0
repeat
local next = self:find(pattern, curr + 1, plain)
if next then
curr = next
end
until (not next)
-- found?
if curr > 0 then
return curr
end
end
-- split string with the given characters
--
-- ("1\n\n2\n3"):split('\n') => 1, 2, 3
-- ("1\n\n2\n3"):split('\n', true) => 1, , 2, 3
--
function string:split(delimiter, strict)
local result = {}
if strict then
for match in (self .. delimiter):gmatch("(.-)" .. delimiter) do
table.insert(result, match)
end
else
self:gsub("[^" .. delimiter .."]+", function(v) table.insert(result, v) end)
end
return result
end
-- trim the spaces
function string:trim()
return (self:gsub("^%s*(.-)%s*$", "%1"))
end
-- trim the left spaces
function string:ltrim()
return (self:gsub("^%s*", ""))
end
-- trim the right spaces
function string:rtrim()
local n = #self
while n > 0 and s:find("^%s", n) do n = n - 1 end
return self:sub(1, n)
end
-- append a substring with a given separator
function string:append(substr, separator)
-- check
assert(self)
-- not substr? return self
if not substr then
return self
end
-- append it
local s = self
if #s == 0 then
s = substr
else
s = string.format("%s%s%s", s, separator or "", substr)
end
-- ok
return s
end
-- encode: ' ', '=', '\"', '<'
function string:encode()
-- null?
if self == nil then return end
-- done
return (self:gsub("[%s=\"<]", function (w) return string.format("%%%x", w:byte()) end))
end
-- decode: ' ', '=', '\"'
function string:decode()
-- null?
if self == nil then return end
-- done
return (self:gsub("%%(%x%x)", function (w) return string.char(tonumber(w, 16)) end))
end
-- join array to string with the given separator
function string.join(items, sep)
-- join them
local str = ""
local index = 1
local count = #items
for _, item in ipairs(items) do
str = str .. item
if index ~= count and sep ~= nil then
str = str .. sep
end
index = index + 1
end
-- ok?
return str
end
-- try to format
function string.tryformat(format, ...)
-- attempt to format it
local ok, str = pcall(string.format, format, ...)
if ok then
return str
else
return format
end
end
-- case-insensitive pattern-matching
--
-- print(("src/dadasd.C"):match(string.ipattern("sR[cd]/.*%.c", true)))
-- print(("src/dadasd.C"):match(string.ipattern("src/.*%.c", true)))
--
-- print(string.ipattern("sR[cd]/.*%.c"))
-- [sS][rR][cd]/.*%.[cC]
--
-- print(string.ipattern("sR[cd]/.*%.c", true))
-- [sS][rR][cCdD]/.*%.[cC]
--
function string.ipattern(pattern, brackets)
local tmp = {}
local i = 1
while i <= #pattern do
-- get current charactor
local char = pattern:sub(i, i)
-- escape?
if char == '%' then
tmp[#tmp + 1] = char
i = i + 1
char = pattern:sub(i,i)
tmp[#tmp + 1] = char
-- '%bxy'? add next 2 chars
if char == 'b' then
tmp[#tmp + 1] = pattern:sub(i + 1, i + 2)
i = i + 2
end
-- brackets?
elseif char == '[' then
tmp[#tmp + 1] = char
i = i + 1
while i <= #pattern do
char = pattern:sub(i, i)
if char == '%' then
tmp[#tmp + 1] = char
tmp[#tmp + 1] = pattern:sub(i + 1, i + 1)
i = i + 1
elseif char:match("%a") then
tmp[#tmp + 1] = not brackets and char or char:lower() .. char:upper()
else
tmp[#tmp + 1] = char
end
if char == ']' then break end
i = i + 1
end
-- letter, [aA]
elseif char:match("%a") then
tmp[#tmp + 1] = '[' .. char:lower() .. char:upper() .. ']'
else
tmp[#tmp + 1] = char
end
i = i + 1
end
return table.concat(tmp)
end
-- return module: string
return string

344
src/ltui/base/table.lua Normal file
View File

@@ -0,0 +1,344 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file table.lua
--
-- define module: table
local table = table or {}
-- clear the table
function table.clear(self)
for k in next, self do
rawset(self, k, nil)
end
end
-- join all objects and tables
function table.join(...)
local result = {}
for _, t in ipairs({...}) do
if type(t) == "table" then
for k, v in pairs(t) do
if type(k) == "number" then table.insert(result, v)
else result[k] = v end
end
else
table.insert(result, t)
end
end
return result
end
-- join all objects and tables to self
function table.join2(self, ...)
for _, t in ipairs({...}) do
if type(t) == "table" then
for k, v in pairs(t) do
if type(k) == "number" then table.insert(self, v)
else self[k] = v end
end
else
table.insert(self, t)
end
end
return self
end
-- append all objects to array
function table.append(array, ...)
for _, value in ipairs({...}) do
table.insert(array, value)
end
return array
end
-- copy the table to self
function table.copy(copied)
-- init it
copied = copied or {}
-- copy it
local result = {}
for k, v in pairs(table.wrap(copied)) do
result[k] = v
end
-- ok
return result
end
-- copy the table to self
function table.copy2(self, copied)
-- check
assert(self)
-- init it
copied = copied or {}
-- clear self first
table.clear(self)
-- copy it
for k, v in pairs(table.wrap(copied)) do
self[k] = v
end
end
-- inherit interfaces and create a new instance
function table.inherit(...)
-- init instance
local classes = {...}
local instance = {}
local metainfo = {}
for _, clasz in ipairs(classes) do
for k, v in pairs(clasz) do
if type(v) == "function" then
if k:startswith("__") then
if metainfo[k] == nil then
metainfo[k] = v
end
else
if instance[k] == nil then
instance[k] = v
else
instance["_super_" .. k] = v
end
end
end
end
end
setmetatable(instance, metainfo)
-- ok?
return instance
end
-- inherit interfaces from the given class
function table.inherit2(self, ...)
-- check
assert(self)
-- init instance
local classes = {...}
local metainfo = getmetatable(self) or {}
for _, clasz in ipairs(classes) do
for k, v in pairs(clasz) do
if type(v) == "function" then
if k:startswith("__") then
if metainfo[k] == nil then
metainfo[k] = v
end
else
if self[k] == nil then
self[k] = v
else
self["_super_" .. k] = v
end
end
end
end
end
-- ok?
return self
end
-- slice table array
function table.slice(self, first, last, step)
-- slice it
local sliced = {}
for i = first or 1, last or #self, step or 1 do
sliced[#sliced + 1] = self[i]
end
return sliced
end
-- is array?
function table.is_array(array)
return type(array) == "table" and array[1] ~= nil
end
-- is dictionary?
function table.is_dictionary(dict)
return type(dict) == "table" and dict[1] == nil
end
-- dump it with the level
function table._dump(self, exclude, level)
-- dump basic type
if type(self) == "string" or type(self) == "boolean" or type(self) == "number" then
io.write(tostring(self))
elseif type(self) == "table" and (getmetatable(self) or {}).__tostring then
io.write(tostring(self))
-- dump table
elseif type(self) == "table" then
-- dump head
io.write("\n")
for l = 1, level do
io.write(" ")
end
io.write("{\n")
-- dump body
local i = 0
for k, v in pairs(self) do
-- exclude some keys
if not exclude or type(k) ~= "string" or not k:find(exclude) then
-- dump spaces and separator
for l = 1, level do
io.write(" ")
end
if i == 0 then
io.write(" ")
else
io.write(", ")
end
-- dump key
if type(k) == "string" then
io.write(k, " = ")
end
-- dump value
table._dump(v, exclude, level + 1)
-- dump newline
io.write("\n")
i = i + 1
end
end
-- dump tail
for l = 1, level do
io.write(" ")
end
io.write("}\n")
elseif self ~= nil then
io.write("<" .. tostring(self) .. ">")
else
io.write("nil")
end
end
-- dump it
function table.dump(self, exclude, prefix)
-- dump prefix
if prefix then
io.write(prefix)
end
-- dump it
table._dump(self, exclude, 0)
-- end
print("")
-- return it
return self
end
-- unwrap object if be only one
function table.unwrap(object)
if type(object) == "table" then
if #object == 1 then
return object[1]
end
end
return object
end
-- wrap object to table
function table.wrap(object)
-- no object?
if nil == object then
return {}
end
-- wrap it if not table
if type(object) ~= "table" then
return {object}
end
-- ok
return object
end
-- remove repeat from the given array
function table.unique(array, barrier)
-- remove repeat
if type(array) == "table" then
-- not only one?
if table.getn(array) ~= 1 then
-- done
local exists = {}
local unique = {}
for _, v in ipairs(array) do
-- exists barrier? clear the current existed items
if barrier and barrier(v) then
exists = {}
end
-- add unique item
if type(v) == "string" then
if not exists[v] then
exists[v] = true
table.insert(unique, v)
end
else
local key = "\"" .. tostring(v) .. "\""
if not exists[key] then
exists[key] = true
table.insert(unique, v)
end
end
end
-- update it
array = unique
end
end
-- ok
return array
end
-- return module: table
return table

109
src/ltui/border.lua Normal file
View File

@@ -0,0 +1,109 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file border.lua
--
-- load modules
local log = require("ltui/base/log")
local rect = require("ltui/rect")
local view = require("ltui/view")
local label = require("ltui/label")
local curses = require("ltui/curses")
-- define module
local border = border or view()
-- init border
function border:init(name, bounds)
-- init view
view.init(self, name, bounds)
-- check bounds
assert(self:width() > 2 and self:height() > 2, string.format("%s: too small!", self))
end
-- draw border
function border:draw(transparent)
-- draw background (transparent)
view.draw(self, true)
-- get corner attribute
local cornerattr = self:cornerattr()
-- the left-upper attribute
local attr_ul = curses.color_pair(cornerattr[1], self:background())
if self:background() == cornerattr[1] then
attr_ul = {attr_ul, "standout"}
end
-- the right-lower attribute
local attr_rl = curses.color_pair(cornerattr[2], self:background())
if self:background() == cornerattr[2] then
attr_rl = {attr_rl, "standout"}
end
-- the border characters
-- @note acs character will use 2 width on borders (pdcurses), so we use acsii characters instead of them.
local iswin = os.host() == "windows"
local hline = iswin and '-' or "hline"
local vline = iswin and '|' or "vline"
local ulcorner = iswin and ' ' or "ulcorner"
local llcorner = iswin and ' ' or "llcorner"
local urcorner = iswin and ' ' or "urcorner"
local lrcorner = iswin and ' ' or "lrcorner"
-- draw left and top border
self:canvas():attr(attr_ul)
self:canvas():move(0, 0):putchar(hline, self:width())
self:canvas():move(0, 0):putchar(ulcorner)
self:canvas():move(0, 1):putchar(vline, self:height() - 1, true)
self:canvas():move(0, self:height() - 1):putchar(llcorner)
-- draw bottom and right border
self:canvas():attr(attr_rl)
self:canvas():move(1, self:height() - 1):putchar(hline, self:width() - 1)
self:canvas():move(self:width() - 1, 0):putchar(urcorner)
self:canvas():move(self:width() - 1, 1):putchar(vline, self:height() - 1, true)
self:canvas():move(self:width() - 1, self:height() - 1):putchar(lrcorner)
end
-- get border corner attribute
function border:cornerattr()
return self._CORNERATTR or {"white", "black"}
end
-- set border corner attribute
function border:cornerattr_set(attr_ul, attr_rl)
self._CORNERATTR = {attr_ul or "white", attr_rl or attr_ul or "black"}
self:invalidate()
end
-- swap border corner attribute
function border:cornerattr_swap()
local cornerattr = self:cornerattr()
self:cornerattr_set(cornerattr[2], cornerattr[1])
end
-- return module
return border

78
src/ltui/boxdialog.lua Normal file
View File

@@ -0,0 +1,78 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file boxdialog.lua
--
-- load modules
local log = require("ui/log")
local rect = require("ui/rect")
local action = require("ui/action")
local curses = require("ui/curses")
local window = require("ui/window")
local textdialog = require("ui/textdialog")
-- define module
local boxdialog = boxdialog or textdialog()
-- init dialog
function boxdialog:init(name, bounds, title)
-- init window
textdialog.init(self, name, bounds, title)
-- insert box
self:panel():insert(self:box())
-- resize text
self:text():bounds().ey = 3
self:text():invalidate(true)
self:text():option_set("selectable", false)
self:text():option_set("progress", false)
-- text changed
self:text():action_set(action.ac_on_text_changed, function (v)
if v:text() then
local lines = #self:text():splitext(v:text())
if lines > 0 and lines < self:height() then
self:box():bounds().sy = lines
self:text():bounds().ey = lines
self:box():invalidate(true)
self:text():invalidate(true)
end
end
end)
-- select buttons by default
self:panel():select(self:buttons())
end
-- get box
function boxdialog:box()
if not self._BOX then
self._BOX = window:new("boxdialog.box", rect{0, 3, self:panel():width(), self:panel():height() - 1})
self._BOX:border():cornerattr_set("black", "white")
end
return self._BOX
end
-- return module
return boxdialog

105
src/ltui/button.lua Normal file
View File

@@ -0,0 +1,105 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file button.lua
--
-- load modules
local log = require("ui/log")
local view = require("ui/view")
local event = require("ui/event")
local label = require("ui/label")
local action = require("ui/action")
local curses = require("ui/curses")
-- define module
local button = button or label()
-- init button
function button:init(name, bounds, text, on_action)
-- init label
label.init(self, name, bounds, text)
-- mark as selectable
self:option_set("selectable", true)
-- show cursor
self:cursor_show(true)
-- init action
self:action_set(action.ac_on_enter, on_action)
end
-- draw button
function button:draw(transparent)
-- draw background
view.draw(self, transparent)
-- strip text string
local str = self:text()
if str and #str > 0 then
str = string.sub(str, 1, self:width())
end
if not str or #str == 0 then
return
end
-- get the text attribute value
local textattr = self:textattr_val()
-- selected?
if self:state("selected") and self:state("focused") then
textattr = {textattr, "reverse"}
end
-- draw text
self:canvas():attr(textattr):move(0, 0):putstr(str)
end
-- on event
function button:event_on(e)
-- selected?
if not self:state("selected") then
return
end
-- enter this button?
if e.type == event.ev_keyboard then
if e.key_name == "Enter" then
self:action_on(action.ac_on_enter)
return true
end
end
end
-- set state
function button:state_set(name, enable)
if name == "focused" and self:state(name) ~= enable then
self:invalidate()
end
return view.state_set(self, name, enable)
end
-- return module
return button

161
src/ltui/canvas.lua Normal file
View File

@@ -0,0 +1,161 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file canvas.lua
--
-- load modules
local log = require("ltui/base/log")
local point = require("ltui/point")
local curses = require("ltui/curses")
local object = require("ltui/object")
-- define module
local line = line or object()
local canvas = canvas or object()
-- new canvas instance
function canvas:new(view, window)
-- create instance
self = self()
-- save view and window
self._view = view
self._window = window
assert(view and window, "cannot new canvas instance without view and window!")
-- set the default attributes
self:attr()
-- done
return self
end
-- clear canvas
function canvas:clear()
self._window:clear()
return self
end
-- move canvas to the given position
function canvas:move(x, y)
self._window:move(y, x)
return self
end
-- get the current position
function canvas:pos()
local y, x = self._window:getyx()
return x, y
end
-- get the canvas size
function canvas:size()
local y, x = self._window:getmaxyx()
return point {x + 1, y + 1}
end
-- get the canvas width
function canvas:width()
local _, x = self._window:getmaxyx()
return x + 1
end
-- get the canvas height
function canvas:height()
local y, _ = self._window:getmaxyx()
return y + 1
end
-- put character to canvas
function canvas:putchar(ch, n, vertical)
-- acs character?
if type(ch) == "string" and #ch > 1 then
ch = curses.acs(ch)
end
-- draw characters
n = n or 1
if vertical then
local x, y = self:pos()
while n > 0 do
self:move(x, y)
self._window:addch(ch)
n = n - 1
y = y + 1
end
else
while n > 0 do
self._window:addch(ch)
n = n - 1
end
end
return self
end
-- put a string to canvas
function canvas:putstr(str)
self._window:addstr(str)
return self
end
-- put strings to canvas
function canvas:putstrs(strs, startline)
-- draw strings
local sy, sx = self._window:getyx()
local ey, _ = self._window:getmaxyx()
for idx = startline or 1, #strs do
local _, y = self:pos()
self._window:addstr(strs[idx])
if y + 1 < ey and idx < #strs then
self:move(sx, y + 1)
else
break
end
end
return self
end
-- set canvas attributes
--
-- set attr: canvas:attr("bold")
-- add attr: canvas:attr("bold", true)
-- remove attr: canvas:attr("bold", false)
--
function canvas:attr(attrs, modify)
-- calculate the attributes
local attr = curses.calc_attr(attrs)
if modify == nil then
self._window:attrset(attr)
elseif modify == false then
self._window:attroff(attr)
else
self._window:attron(attr)
end
return self
end
-- return module
return canvas

123
src/ltui/choicebox.lua Normal file
View File

@@ -0,0 +1,123 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file choicebox.lua
--
-- load modules
local log = require("ui/log")
local view = require("ui/view")
local rect = require("ui/rect")
local panel = require("ui/panel")
local event = require("ui/event")
local action = require("ui/action")
local curses = require("ui/curses")
local button = require("ui/button")
local object = require("ui/object")
-- define module
local choicebox = choicebox or panel()
-- init choicebox
function choicebox:init(name, bounds)
-- init panel
panel.init(self, name, bounds)
-- init values
self._VALUES = {}
end
-- on event
function choicebox:event_on(e)
-- select config
if e.type == event.ev_keyboard then
if e.key_name == "Down" then
return self:select_next()
elseif e.key_name == "Up" then
return self:select_prev()
elseif e.key_name == "Enter" or e.key_name == " " then
self:_do_select()
return true
end
elseif e.type == event.ev_command and e.command == "cm_enter" then
self:_do_select()
return true
end
end
-- load values
function choicebox:load(values, selected)
-- clear the views first
self:clear()
-- insert values
self._VALUES = values
for idx, value in ipairs(values) do
self:_do_insert(value, idx, idx == selected)
end
-- select the first item
self:select(self:first())
-- invalidate
self:invalidate()
end
-- do insert a value item
function choicebox:_do_insert(value, index, selected)
-- init text
local text = (selected and "(X) " or "( ) ") .. tostring(value)
-- init a value item view
local item = button:new("choicebox.value." .. self:count(), rect:new(0, self:count(), self:width(), 1), text)
-- attach this index
item:extra_set("index", index)
-- insert this config item
self:insert(item)
end
-- do select the current config
function choicebox:_do_select()
-- get the current item
local item = self:current()
-- get the current index
local index = item:extra("index")
-- get the current value
local value = self._VALUES[index]
-- do action: on selected
self:action_on(action.ac_on_selected, index, value)
-- update text
item:text_set("(X) " .. tostring(value))
end
-- return module
return choicebox

93
src/ltui/choicedialog.lua Normal file
View File

@@ -0,0 +1,93 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file choicedialog.lua
--
-- load modules
local log = require("ui/log")
local rect = require("ui/rect")
local event = require("ui/event")
local action = require("ui/action")
local curses = require("ui/curses")
local window = require("ui/window")
local choicebox = require("ui/choicebox")
local boxdialog = require("ui/boxdialog")
-- define module
local choicedialog = choicedialog or boxdialog()
-- init dialog
function choicedialog:init(name, bounds, title)
-- init window
boxdialog.init(self, name, bounds, title)
-- init text
self:text():text_set("Use the arrow keys to navigate this window or press the hotkey of the item you wish to select followed by the <SPACEBAR>. Press <?> for additional information about this")
-- init buttons
self:button_add("select", "< Select >", function (v, e)
self:choicebox():event_on(event.command {"cm_enter"})
self:quit()
end)
self:button_add("cancel", "< Cancel >", function (v, e)
self:quit()
end)
self:buttons():select(self:button("select"))
-- insert choice box
self:box():panel():insert(self:choicebox())
-- disable to select to box (disable Tab switch and only response to buttons)
self:box():option_set("selectable", false)
end
-- get choice box
function choicedialog:choicebox()
if not self._CHOICEBOX then
local bounds = self:box():panel():bounds()
self._CHOICEBOX = choicebox:new("choicedialog.choicebox", rect:new(0, 0, bounds:width(), bounds:height()))
self._CHOICEBOX:state_set("focused", true) -- we can select and highlight selected item
end
return self._CHOICEBOX
end
-- on event
function choicedialog:event_on(e)
-- load values first
if e.type == event.ev_idle then
if not self._LOADED then
self:action_on(action.ac_on_load)
self._LOADED = true
end
-- select value
elseif e.type == event.ev_keyboard then
if e.key_name == "Down" or e.key_name == "Up" or e.key_name == " " then
return self:choicebox():event_on(e)
end
end
return boxdialog.event_on(self, e)
end
-- return module
return choicedialog

219
src/ltui/curses.lua Normal file
View File

@@ -0,0 +1,219 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file curses.lua
--
--[[ Console User Interface (cui) ]-----------------------------------------
Author: Tiago Dionizio (tngd@mega.ist.utl.pt)
$Id: core.lua 18 2007-06-21 20:43:52Z tngd $
--------------------------------------------------------------------------]]
-- load modules
local curses = require("ltui.curses")
local os = require("ltui/base/os")
local log = require("ltui/base/log")
-- get color from the given name
function curses.color(name)
if name == 'black' then return curses.COLOR_BLACK
elseif name == 'red' then return curses.COLOR_RED
elseif name == 'green' then return curses.COLOR_GREEN
elseif name == 'yellow' then return curses.COLOR_YELLOW
elseif name == 'blue' then return curses.COLOR_BLUE
elseif name == 'magenta' then return curses.COLOR_MAGENTA
elseif name == 'cyan' then return curses.COLOR_CYAN
elseif name == 'white' then return curses.COLOR_WHITE
else return curses.COLOR_BLACK
end
end
-- is color?
local colors = {black = true, red = true, green = true, yellow = true, blue = true, magenta = true, cyan = true, white = true}
function curses.iscolor(name)
return colors[name] or colors[name:sub(3) or ""]
end
-- get attr from the given name
function curses.attr(name)
if name == 'normal' then return curses.A_NORMAL
elseif name == 'standout' then return curses.A_STANDOUT
elseif name == 'underline' then return curses.A_UNDERLINE
elseif name == 'reverse' then return curses.A_REVERSE
elseif name == 'blink' then return curses.A_BLINK
elseif name == 'dim' then return curses.A_DIM
elseif name == 'bold' then return curses.A_BOLD
elseif name == 'protect' then return curses.A_PROTECT
elseif name == 'invis' then return curses.A_INVIS
elseif name == 'alt' then return curses.A_ALTCHARSET
else return curses.A_NORMAL
end
end
-- get acs character from the given name
function curses.acs(name)
if name == 'block' then return curses.ACS_BLOCK
elseif name == 'board' then return curses.ACS_BOARD
elseif name == 'btee' then return curses.ACS_BTEE
elseif name == 'bullet' then return curses.ACS_BULLET
elseif name == 'ckboard' then return curses.ACS_CKBOARD
elseif name == 'darrow' then return curses.ACS_DARROW
elseif name == 'degree' then return curses.ACS_DEGREE
elseif name == 'diamond' then return curses.ACS_DIAMOND
elseif name == 'gequal' then return curses.ACS_GEQUAL
elseif name == 'hline' then return curses.ACS_HLINE
elseif name == 'lantern' then return curses.ACS_LANTERN
elseif name == 'larrow' then return curses.ACS_LARROW
elseif name == 'lequal' then return curses.ACS_LEQUAL
elseif name == 'llcorner' then return curses.ACS_LLCORNER
elseif name == 'lrcorner' then return curses.ACS_LRCORNER
elseif name == 'ltee' then return curses.ACS_LTEE
elseif name == 'nequal' then return curses.ACS_NEQUAL
elseif name == 'pi' then return curses.ACS_PI
elseif name == 'plminus' then return curses.ACS_PLMINUS
elseif name == 'plus' then return curses.ACS_PLUS
elseif name == 'rarrow' then return curses.ACS_RARROW
elseif name == 'rtee' then return curses.ACS_RTEE
elseif name == 's1' then return curses.ACS_S1
elseif name == 's3' then return curses.ACS_S3
elseif name == 's7' then return curses.ACS_S7
elseif name == 's9' then return curses.ACS_S9
elseif name == 'sterling' then return curses.ACS_STERLING
elseif name == 'ttee' then return curses.ACS_TTEE
elseif name == 'uarrow' then return curses.ACS_UARROW
elseif name == 'ulcorner' then return curses.ACS_ULCORNER
elseif name == 'urcorner' then return curses.ACS_URCORNER
elseif name == 'vline' then return curses.ACS_VLINE
elseif type(name) == 'string' and #name == 1 then
return name
else
return ' '
end
end
-- calculate attr from the attributes list
--
-- local attr = curses.calc_attr("bold")
-- local attr = curses.calc_attr("yellow")
-- local attr = curses.calc_attr{ "yellow", "ongreen" }
-- local attr = curses.calc_attr{ "yellow", "ongreen", "bold" }
-- local attr = curses.calc_attr{ curses.color_pair("yellow", "green"), "bold" }
--
function curses.calc_attr(attrs)
-- curses.calc_attr(curses.A_BOLD)
-- curses.calc_attr(curses.color_pair("yellow", "green"))
local atype = type(attrs)
if atype == "number" then
return attrs
-- curses.calc_attr("bold")
-- curses.calc_attr("yellow")
elseif atype == "string" then
if curses.iscolor(attrs) then
local color = attrs
if color:startswith("on") then
color = color:sub(3)
end
return curses.color_pair(color, color)
end
return curses.attr(attrs)
-- curses.calc_attr{ "yellow", "ongreen", "bold" }
-- curses.calc_attr{ curses.color_pair("yellow", "green"), "bold" }
elseif atype == "table" then
local v = 0
local set = {}
local fg = nil
local bg = nil
for _, a in ipairs(attrs) do
if not set[a] and a then
set[a] = true
if type(a) == "number" then
v = v + a
elseif curses.iscolor(a) then
if a:startswith("on") then
bg = a:sub(3)
else
fg = a
end
else
v = v + curses.attr(a)
end
end
end
if fg or bg then
v = v + curses.color_pair(fg or bg, bg or fg)
end
return v
else
return 0
end
end
-- get attr from the color pair
curses._color_pair = curses._color_pair or curses.color_pair
function curses.color_pair(fg, bg)
-- get foreground and backround color
fg = curses.color(fg)
bg = curses.color(bg)
-- attempt to get color from the cache first
local key = fg .. ':' .. bg
local colors = curses._COLORS or {}
if colors[key] then
return colors[key]
end
-- no colors?
if not curses.has_colors() then
return 0
end
-- update the colors count
curses._NCOLORS = (curses._NCOLORS or 0) + 1
-- init the color pair
if not curses.init_pair(curses._NCOLORS, fg, bg) then
os.raise("failed to initialize color pair (%d, %s, %s)", curses._NCOLORS, fg, bg)
end
-- get the color attr
local attr = curses._color_pair(curses._NCOLORS)
-- save to cache
colors[key] = attr
curses._COLORS = colors
-- ok
return attr
end
-- set cursor state
curses._cursor_set = curses._cursor_set or curses.cursor_set
function curses.cursor_set(state)
if curses._CURSOR_STATE ~= state then
curses._CURSOR_STATE = state
curses._cursor_set(state)
end
end
-- return module: curses
return curses

46
src/ltui/desktop.lua Normal file
View File

@@ -0,0 +1,46 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file desktop.lua
--
-- load modules
local log = require("ltui/base/log")
local rect = require("ltui/rect")
local view = require("ltui/view")
local panel = require("ltui/panel")
local curses = require("ltui/curses")
-- define module
local desktop = desktop or panel()
-- init desktop
function desktop:init(name, bounds)
-- init panel
panel.init(self, name, bounds)
-- init background
self:background_set("blue")
end
-- return module
return desktop

118
src/ltui/dialog.lua Normal file
View File

@@ -0,0 +1,118 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file dialog.lua
--
-- load modules
local log = require("ui/log")
local rect = require("ui/rect")
local event = require("ui/event")
local label = require("ui/label")
local panel = require("ui/panel")
local action = require("ui/action")
local button = require("ui/button")
local window = require("ui/window")
local curses = require("ui/curses")
-- define module
local dialog = dialog or window()
-- init dialog
function dialog:init(name, bounds, title)
-- init window
window.init(self, name, bounds, title, true)
-- insert buttons
self:panel():insert(self:buttons())
end
-- get buttons
function dialog:buttons()
if not self._BUTTONS then
self._BUTTONS = panel:new("dialog.buttons", rect:new(0, self:panel():height() - 1, self:panel():width(), 1))
end
return self._BUTTONS
end
-- get button from the given button name
function dialog:button(name)
return self:buttons():view(name)
end
-- add button
function dialog:button_add(name, text, command)
-- init button
local btn = button:new(name, rect:new(0, 0, #text, 1), text, command)
-- insert button
self:buttons():insert(btn)
-- update the position of all buttons
local index = 1
local width = self:buttons():width()
local count = self:buttons():count()
local padding = math.floor(width / 8)
for v in self:buttons():views() do
local x = padding + index * math.floor((width - padding * 2) / (count + 1)) - math.floor(v:width() / 2)
if x + v:width() > width then
x = math.max(0, width - v:width())
end
v:bounds():move2(x, 0)
v:invalidate(true)
index = index + 1
end
-- invalidate
self:invalidate()
-- ok
return btn
end
-- select button from the given button name
function dialog:button_select(name)
self:buttons():select(self:button(name))
return self
end
-- quit dialog
function dialog:quit()
local parent = self:parent()
if parent then
self:action_on(action.ac_on_exit)
parent:remove(self)
end
end
-- on event
function dialog:event_on(e)
if e.type == event.ev_keyboard and e.key_name == "Esc" then
self:quit()
return true
end
return window.event_on(self, e)
end
-- return module
return dialog

90
src/ltui/event.lua Normal file
View File

@@ -0,0 +1,90 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file event.lua
--
--[[ Console User Interface (cui) ]-----------------------------------------
Author: Tiago Dionizio (tiago.dionizio AT gmail.com)
$Id: event.lua 18 2007-06-21 20:43:52Z tngd $
--------------------------------------------------------------------------]]
-- load modules
local log = require("ltui/base/log")
local object = require("ltui/object")
-- define module
local event = event or object { _init = {"type", "command", "extra"} }
-- register event types
function event:register(tag, ...)
local base = self[tag] or 0
local enums = {...}
local n = #enums
for i = 1, n do
self[enums[i]] = i + base
end
self[tag] = base + n
end
-- is key?
function event:is_key(key_name)
return self.type == event.ev_keyboard and self.key_name == key_name
end
-- is command event: cm_xxx?
function event:is_command(command)
return self.type == event.ev_command and self.command == command
end
-- dump event
function event:dump()
if self.type == event.ev_keyboard then
log:print("event(key): %s %s ..", self.key_name, self.key_code)
elseif self.type == event.ev_command then
log:print("event(cmd): %s ..", self.command)
else
log:print("event(%s): ..", self.type)
end
end
-- register event types, event.ev_keyboard = 1, event.ev_mouse = 2, ... , event.ev_idle = 5, event.ev_max = 5
event:register("ev_max", "ev_keyboard", "ev_mouse", "ev_command", "ev_text", "ev_idle")
-- register command event types (ev_command)
event:register("cm_max", "cm_quit", "cm_exit", "cm_enter")
-- define keyboard event
--
-- keyname = key name
-- keycode = key code
-- keymeta = ALT key was pressed
--
event.keyboard = object {_init = { "key_code", "key_name", "key_meta" }, type = event.ev_keyboard}
-- define command event
event.command = object {_init = { "command", "extra" }, type = event.ev_command}
-- define idle event
event.idle = object {_init = {}, type = event.ev_idle}
-- return module
return event

77
src/ltui/inputdialog.lua Normal file
View File

@@ -0,0 +1,77 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file inputdialog.lua
--
-- load modules
local log = require("ui/log")
local rect = require("ui/rect")
local view = require("ui/view")
local event = require("ui/event")
local action = require("ui/action")
local curses = require("ui/curses")
local window = require("ui/window")
local textedit = require("ui/textedit")
local textdialog = require("ui/textdialog")
-- define module
local inputdialog = inputdialog or textdialog()
-- init dialog
function inputdialog:init(name, bounds, title)
-- init window
textdialog.init(self, name, bounds, title)
-- insert textedit
self:panel():insert(self:textedit())
-- resize text
self:text():bounds().ey = 1
self:text():invalidate(true)
self:text():option_set("selectable", false)
self:text():option_set("progress", false)
-- text changed
self:text():action_set(action.ac_on_text_changed, function (v)
if v:text() then
local lines = #self:text():splitext(v:text()) + 1
if lines > 0 and lines < self:height() then
self:text():bounds().ey = lines
self:textedit():bounds().sy = lines
self:text():invalidate(true)
self:textedit():invalidate(true)
end
end
end)
end
-- get textedit
function inputdialog:textedit()
if not self._TEXTEDIT then
self._TEXTEDIT = textedit:new("inputdialog.textedit", rect{0, 1, self:panel():width(), self:panel():height() - 1})
end
return self._TEXTEDIT
end
-- return module
return inputdialog

143
src/ltui/label.lua Normal file
View File

@@ -0,0 +1,143 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file label.lua
--
-- load modules
local log = require("ltui/base/log")
local view = require("ltui/view")
local event = require("ltui/event")
local action = require("ltui/action")
local curses = require("ltui/curses")
-- define module
local label = label or view()
-- init label
function label:init(name, bounds, text)
-- init view
view.init(self, name, bounds)
-- init text
self:text_set(text)
-- init text attribute
self:textattr_set("black")
end
-- draw view
function label:draw(transparent)
-- draw background
view.draw(self, transparent)
-- get the text attribute value
local textattr = self:textattr_val()
-- draw text string
local str = self:text()
if str and #str > 0 and textattr then
self:canvas():attr(textattr):move(0, 0):putstrs(self:splitext(str))
end
end
-- get text
function label:text()
return self._TEXT
end
-- set text
function label:text_set(text)
-- set text
text = text or ""
local changed = self._TEXT ~= text
self._TEXT = text
-- do action
if changed then
self:action_on(action.ac_on_text_changed)
end
self:invalidate()
return self
end
-- get text attribute
function label:textattr()
return self:attr("textattr")
end
-- set text attribute, .e.g textattr_set("yellow onblue bold")
function label:textattr_set(attr)
return self:attr_set("textattr", attr)
end
-- get the current text attribute value
function label:textattr_val()
-- get text attribute
local textattr = self:textattr()
if not textattr then
return
end
-- no text background? use view's background
if self:background() and not textattr:find("on") then
textattr = textattr .. " on" .. self:background()
end
-- attempt to get the attribute value from the cache first
self._TEXTATTR = self._TEXTATTR or {}
local value = self._TEXTATTR[textattr]
if value then
return value
end
-- update the cache
value = curses.calc_attr(textattr:split("%s+"))
self._TEXTATTR[textattr] = value
return value
end
-- split text by width
function label:splitext(text, width)
-- get width
width = width or self:width()
-- split text first
local result = {}
local lines = text:split('\n', true)
for idx = 1, #lines do
local line = lines[idx]
while #line > width do
table.insert(result, line:sub(1, width))
line = line:sub(width + 1)
end
table.insert(result, line)
end
return result
end
-- return module
return label

313
src/ltui/mconfdialog.lua Normal file
View File

@@ -0,0 +1,313 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file mconfdialog.lua
--
-- load modules
local log = require("ui/log")
local rect = require("ui/rect")
local event = require("ui/event")
local action = require("ui/action")
local curses = require("ui/curses")
local window = require("ui/window")
local menuconf = require("ui/menuconf")
local boxdialog = require("ui/boxdialog")
local textdialog = require("ui/textdialog")
local inputdialog = require("ui/inputdialog")
local choicedialog = require("ui/choicedialog")
-- define module
local mconfdialog = mconfdialog or boxdialog()
-- init dialog
function mconfdialog:init(name, bounds, title)
-- init window
boxdialog.init(self, name, bounds, title)
-- init text
self:text():text_set([[Arrow keys navigate the menu. <Enter> selects submenus ---> (or empty submenus ----).
Pressing <Y> includes, <N> excludes. Enter <Esc> or <Back> to go back, <?> for Help, </> for Search. Legend: [*] built-in [ ] excluded
]])
-- init buttons
self:button_add("select", "< Select >", function (v, e) self:menuconf():event_on(event.command {"cm_enter"}) end)
self:button_add("back", "< Back >", function (v, e) self:menuconf():event_on(event.command {"cm_back"}) end)
self:button_add("exit", "< Exit >", function (v, e) self:quit() end)
self:button_add("help", "< Help >", function (v, e) self:show_help() end)
self:button_add("save", "< Save >", function (v, e) self:action_on(action.ac_on_save) end)
self:buttons():select(self:button("select"))
-- insert menu config
self:box():panel():insert(self:menuconf())
-- disable to select to box (disable Tab switch and only response to buttons)
self:box():option_set("selectable", false)
-- on selected
self:menuconf():action_set(action.ac_on_selected, function (v, config)
-- show input dialog
if config.kind == "string" or config.kind == "number" then
local dialog_input = self:inputdialog()
dialog_input:extra_set("config", config)
dialog_input:title():text_set(config:prompt())
dialog_input:textedit():text_set(tostring(config.value))
dialog_input:panel():select(dialog_input:textedit())
if config.kind == "string" then
dialog_input:text():text_set("Please enter a string value. Use the <TAB> key to move from the input fields to buttons below it.")
else
dialog_input:text():text_set("Please enter a decimal value. Fractions will not be accepted. Use the <TAB> key to move from the input field to the buttons below it.")
end
self:insert(dialog_input, {centerx = true, centery = true})
return true
-- show choice dialog
elseif config.kind == "choice" and config.values and #config.values > 0 then
local dialog_choice = self:choicedialog()
dialog_choice:title():text_set(config:prompt())
dialog_choice:choicebox():load(config.values, config.value)
dialog_choice:choicebox():action_set(action.ac_on_selected, function (v, index, value)
config.value = index
end)
self:insert(dialog_choice, {centerx = true, centery = true})
return true
end
end)
end
-- load configs
function mconfdialog:load(configs)
self._CONFIGS = configs
return self:menuconf():load(configs)
end
-- get configs
function mconfdialog:configs()
return self._CONFIGS
end
-- get menu config
function mconfdialog:menuconf()
if not self._MENUCONF then
local bounds = self:box():panel():bounds()
self._MENUCONF = menuconf:new("mconfdialog.menuconf", rect:new(0, 0, bounds:width(), bounds:height()))
self._MENUCONF:state_set("focused", true) -- we can select and highlight selected item
end
return self._MENUCONF
end
-- get help dialog
function mconfdialog:helpdialog()
if not self._HELPDIALOG then
local helpdialog = textdialog:new("mconfdialog.help", self:bounds(), "help")
helpdialog:button_add("exit", "< Exit >", function (v) helpdialog:quit() end)
self._HELPDIALOG = helpdialog
end
return self._HELPDIALOG
end
-- get result dialog
function mconfdialog:resultdialog()
if not self._RESULTDIALOG then
local resultdialog = textdialog:new("mconfdialog.result", self:bounds(), "result")
resultdialog:button_add("exit", "< Exit >", function (v) resultdialog:quit() end)
self._RESULTDIALOG = resultdialog
end
return self._RESULTDIALOG
end
-- get input dialog
function mconfdialog:inputdialog()
if not self._INPUTDIALOG then
local dialog_input = inputdialog:new("mconfdialog.input", rect {0, 0, math.min(80, self:width() - 8), math.min(8, self:height())}, "input dialog")
dialog_input:background_set(self:frame():background())
dialog_input:frame():background_set("cyan")
dialog_input:textedit():option_set("multiline", false)
dialog_input:button_add("ok", "< Ok >", function (v)
local config = dialog_input:extra("config")
if config.kind == "string" then
config.value = dialog_input:textedit():text()
elseif config.kind == "number" then
local value = tonumber(dialog_input:textedit():text())
if value ~= nil then
config.value = value
end
end
dialog_input:quit()
end)
dialog_input:button_add("cancel", "< Cancel >", function (v)
dialog_input:quit()
end)
dialog_input:button_select("ok")
self._INPUTDIALOG = dialog_input
end
return self._INPUTDIALOG
end
-- get choice dialog
function mconfdialog:choicedialog()
if not self._CHOICEDIALOG then
local dialog_choice = choicedialog:new("mconfdialog.choice", rect {0, 0, math.min(80, self:width() - 8), math.min(20, self:height())}, "input dialog")
dialog_choice:background_set(self:frame():background())
dialog_choice:frame():background_set("cyan")
dialog_choice:box():frame():background_set("cyan")
self._CHOICEDIALOG = dialog_choice
end
return self._CHOICEDIALOG
end
-- get search dialog
function mconfdialog:searchdialog()
if not self._SEARCHDIALOG then
local dialog_search = inputdialog:new("mconfdialog.input", rect {0, 0, math.min(80, self:width() - 8), math.min(8, self:height())}, "Search Configuration Parameter")
dialog_search:background_set(self:frame():background())
dialog_search:frame():background_set("cyan")
dialog_search:textedit():option_set("multiline", false)
dialog_search:text():text_set("Enter (sub)string or lua pattern string to search for configuration")
dialog_search:button_add("ok", "< Ok >", function (v)
local configs = self:search(self:configs(), dialog_search:textedit():text())
local results = "Search('" .. dialog_search:textedit():text() .. "') results:"
for _, config in ipairs(configs) do
results = results .. "\n" .. config:prompt()
if config.kind then
results = results .. "\nkind: " .. config.kind
end
if config.default then
results = results .. "\ndefault: " .. config.default
end
if config.path then
results = results .. "\npath: " .. config.path
end
if config.sourceinfo then
results = results .. "\nposition: " .. (config.sourceinfo.file or "") .. ":" .. (config.sourceinfo.line or "-1")
end
results = results .. "\n"
end
self:show_result(results)
dialog_search:quit()
end)
dialog_search:button_add("cancel", "< Cancel >", function (v)
dialog_search:quit()
end)
dialog_search:button_select("ok")
self._SEARCHDIALOG = dialog_search
end
return self._SEARCHDIALOG
end
-- search configs via the given text
function mconfdialog:search(configs, text)
local results = {}
for _, config in ipairs(configs) do
local prompt = config:prompt()
if prompt and prompt:find(text) then
table.insert(results, config)
end
if config.kind == "menu" then
table.join2(results, self:search(config.configs, text))
end
end
return results
end
-- show help dialog
function mconfdialog:show_help()
if self:parent() then
-- get the current config item
local item = self:menuconf():current()
-- get the current config
local config = item:extra("config")
-- set help title
self:helpdialog():title():text_set(config:prompt())
-- set help text
local text = config.description
if type(text) == "table" then
text = table.concat(text, '\n')
end
if config.kind then
text = text .. "\ntype: " .. config.kind
end
if config.default then
text = text .. "\ndefault: " .. tostring(config.default)
end
if config.kind == "choice" then
text = text .. "\nvalues: "
for _, value in ipairs(config.values) do
text = text .. "\n - " .. value
end
end
if config.path then
text = text .. "\npath: " .. config.path
end
if config.sourceinfo then
text = text .. "\nposition: " .. (config.sourceinfo.file or "") .. ":" .. (config.sourceinfo.line or "-1")
end
self:helpdialog():text():text_set(text)
-- show help
self:parent():insert(self:helpdialog())
end
end
-- show search dialog
function mconfdialog:show_search()
local dialog_search = self:searchdialog()
dialog_search:panel():select(dialog_search:textedit())
self:insert(dialog_search, {centerx = true, centery = true})
end
-- show result dialog
function mconfdialog:show_result(text)
local dialog_result = self:resultdialog()
dialog_result:text():text_set(text)
if not self:view("mconfdialog.result") then
self:insert(dialog_result, {centerx = true, centery = true})
else
self:select(dialog_result)
end
end
-- on event
function mconfdialog:event_on(e)
-- select config
if e.type == event.ev_keyboard then
if e.key_name == "Down" or e.key_name == "Up" or e.key_name == " " or e.key_name == "Esc" or e.key_name:lower() == "y" or e.key_name:lower() == "n" then
return self:menuconf():event_on(e)
elseif e.key_name == "?" then
self:show_help()
return true
elseif e.key_name == "/" then
self:show_search()
return true
end
end
return boxdialog.event_on(self, e)
end
-- return module
return mconfdialog

56
src/ltui/menubar.lua Normal file
View File

@@ -0,0 +1,56 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file menubar.lua
--
-- load modules
local log = require("ltui/base/log")
local rect = require("ltui/rect")
local label = require("ltui/label")
local panel = require("ltui/panel")
local curses = require("ltui/curses")
-- define module
local menubar = menubar or panel()
-- init menubar
function menubar:init(name, bounds)
-- init panel
panel.init(self, name, bounds)
-- init title
self._TITLE = label:new("menubar.title", rect{0, 0, self:width(), self:height()}, "Menu Bar")
self:insert(self:title())
self:title():textattr_set("red")
-- init background
self:background_set("white")
end
-- get title
function menubar:title()
return self._TITLE
end
-- return module
return menubar

286
src/ltui/menuconf.lua Normal file
View File

@@ -0,0 +1,286 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file menuconf.lua
--
-- load modules
local log = require("ui/log")
local view = require("ui/view")
local rect = require("ui/rect")
local panel = require("ui/panel")
local event = require("ui/event")
local action = require("ui/action")
local curses = require("ui/curses")
local button = require("ui/button")
local object = require("ui/object")
-- define module
local menuconf = menuconf or panel()
-- init menuconf
function menuconf:init(name, bounds)
-- init panel
panel.init(self, name, bounds)
-- init configs
self._CONFIGS = {}
end
-- on event
function menuconf:event_on(e)
-- select config
local back = false
if e.type == event.ev_keyboard then
if e.key_name == "Down" then
return self:select_next()
elseif e.key_name == "Up" then
return self:select_prev()
elseif e.key_name == "Enter" or e.key_name == " " then
self:_do_select()
return true
elseif e.key_name:lower() == "y" then
self:_do_include(true)
return true
elseif e.key_name:lower() == "n" then
self:_do_include(false)
return true
elseif e.key_name == "Esc" then
back = true
end
elseif e.type == event.ev_command then
if e.command == "cm_enter" then
self:_do_select()
return true
elseif e.command == "cm_back" then
back = true
end
end
-- back?
if back then
-- load the previous menu configs
local configs_prev = self._CONFIGS._PREV
if configs_prev then
self._CONFIGS._PREV = configs_prev._PREV
self:load(configs_prev)
return true
end
end
end
-- load configs
function menuconf:load(configs)
-- clear the views first
self:clear()
-- detach the previous config and view
local configs_prev = self._CONFIGS._PREV
if configs_prev then
for _, config in ipairs(configs_prev) do
config._view = nil
end
end
-- insert configs
self._CONFIGS = configs
for _, config in ipairs(configs) do
if self:count() < self:height() then
self:_do_insert(config)
end
end
-- select the first item
self:select(self:first())
-- invalidate
self:invalidate()
end
-- do insert a config item
function menuconf:_do_insert(config)
-- init a config item view
local item = button:new("menuconf.config." .. self:count(), rect:new(0, self:count(), self:width(), 1), tostring(config))
-- attach this config
item:extra_set("config", config)
-- attach this view
config._view = item
-- insert this config item
self:insert(item)
end
-- do select the current config
function menuconf:_do_select()
-- get the current item
local item = self:current()
-- get the current config
local config = item:extra("config")
-- clear new state
config.new = false
-- do action: on selected
if self:action_on(action.ac_on_selected, config) then
return
end
-- select the boolean config
if config.kind == "boolean" then
config.value = not config.value
-- show sub-menu configs
elseif config.kind == "menu" and config.configs and #config.configs > 0 then
local configs_prev = self._CONFIGS
self:load(config.configs)
self._CONFIGS._PREV = configs_prev
end
end
-- do include
function menuconf:_do_include(enabled)
-- get the current item
local item = self:current()
-- get the current config
local config = item:extra("config")
-- clear new state
config.new = false
-- select the boolean config
if config.kind == "boolean" then
config.value = enabled
end
end
-- init config object
--
-- kind
-- - {kind = "number/boolean/string/choice/menu"}
--
-- description
-- - {description = "config item description"}
-- - {description = {"config item description", "line2", "line3", "more description ..."}}
--
-- boolean config
-- - {name = "...", kind = "boolean", value = true, default = true, description = "boolean config item", new = true/false}
--
-- number config
-- - {name = "...", kind = "number", value = 10, default = 0, description = "number config item", new = true/false}
--
-- string config
-- - {name = "...", kind = "string", value = "xmake", default = "", description = "string config item", new = true/false}
--
-- choice config (value is index)
-- - {name = "...", kind = "choice", value = 1, default = 1, description = "choice config item", values = {2, 2, 3, 4, 5}}
--
-- menu config
-- - {name = "...", kind = "menu", description = "menu config item", configs = {...}}
--
local config = config or object{new = true,
__index = function (tbl, key)
if key == "value" then
local val = rawget(tbl, "_value")
if val == nil then
val = rawget(tbl, "default")
end
return val
end
return rawget(tbl, key)
end,
__newindex = function (tbl, key, val)
if key == "value" then
key = "_value"
end
rawset(tbl, key, val)
if key == "_value" then
local v = rawget(tbl, "_view") -- update the config item text in view
if v then
v:text_set(tostring(tbl))
end
end
end}
-- the prompt info
function config:prompt()
-- get text (first line in description)
local text = self.description or ""
if type(text) == "table" then
text = text[1] or ""
end
return text
end
-- to string
function config:__tostring()
-- get text (first line in description)
local text = self:prompt()
-- get value
local value = self.value
-- update text
if self.kind == "boolean" or (not self.kind and type(value) == "boolean") then -- boolean config?
text = (value and "[*] " or "[ ] ") .. text
elseif self.kind == "number" or (not self.kind and type(value) == "number") then -- number config?
text = " " .. text .. " (" .. tostring(value or 0) .. ")"
elseif self.kind == "string" or (not self.kind and type(value) == "string") then -- string config?
text = " " .. text .. " (" .. tostring(value or "") .. ")"
elseif self.kind == "choice" then -- choice config?
if self.values and #self.values > 0 then
text = " " .. text .. " (" .. tostring(self.values[value or 1]) .. ")" .. " --->"
else
text = " " .. text .. " () ----"
end
elseif self.kind == "menu" then -- menu config?
text = " " .. text .. (self.configs and #self.configs > 0 and " --->" or " ----")
end
-- new config?
if self.new and self.kind ~= "choice" and self.kind ~= "menu" then
text = text .. " (NEW)"
end
-- ok
return text
end
-- save config objects
menuconf.config = menuconf.config or config
menuconf.menu = menuconf.menu or config { kind = "menu", configs = {} }
menuconf.number = menuconf.number or config { kind = "number", default = 0 }
menuconf.string = menuconf.string or config { kind = "string", default = "" }
menuconf.choice = menuconf.choice or config { kind = "choice", default = 1, values = {} }
menuconf.boolean = menuconf.boolean or config { kind = "boolean", default = false }
-- return module
return menuconf

99
src/ltui/object.lua Normal file
View File

@@ -0,0 +1,99 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file object.lua
--
-- define module: object
local object = object or {}
-- taken from 'std' library: http://luaforge.net/projects/stdlib/
-- and http://lua-cui.sourceforge.net/
--
-- local point = object { _init = {"x", "y"} }
--
-- local p1 = point {1, 2}
-- > p1 {x = 1, y = 2}
--
-- permute some indices of a table
local function permute (p, t)
local u = {}
for i, v in pairs (t) do
if p[i] ~= nil then
u[p[i]] = v
else
u[i] = v
end
end
return u
end
-- make a shallow copy of a table, including any
local function clone (t)
local u = setmetatable ({}, getmetatable (t))
for i, v in pairs (t) do
u[i] = v
end
return u
end
-- merge two tables
--
-- If there are duplicate fields, u's will be used. The metatable of
-- the returned table is that of t
--
local function merge (t, u)
local r = clone (t)
for i, v in pairs (u) do
r[i] = v
end
return r
end
-- root object
--
-- List of fields to be initialised by the
-- constructor: assuming the default _clone, the
-- numbered values in an object constructor are
-- assigned to the fields given in _init
--
local object = { _init = {} }
setmetatable (object, object)
-- object constructor
--
-- @param initial values for fields in
--
-- @return new object
--
function object:_clone (values)
local object = merge(self, permute(self._init, values or {}))
return setmetatable (object, object)
end
-- local x = object {}
function object.__call (...)
return (...)._clone (...)
end
-- return module: object
return object

394
src/ltui/panel.lua Normal file
View File

@@ -0,0 +1,394 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file panel.lua
--
-- load modules
local log = require("ltui/base/log")
local view = require("ltui/view")
local rect = require("ltui/rect")
local event = require("ltui/event")
local point = require("ltui/point")
local curses = require("ltui/curses")
local dlist = require("ltui/base/dlist")
-- define module
local panel = panel or view()
-- init panel
function panel:init(name, bounds)
-- init view
view.init(self, name, bounds)
-- mark as panel
self:type_set("panel")
-- mark as selectable
self:option_set("selectable", true)
-- init child views
self._VIEWS = dlist()
-- init views cache
self._VIEWS_CACHE = {}
end
-- get all child views
function panel:views()
return self._VIEWS:items()
end
-- get views count
function panel:count()
return self._VIEWS:size()
end
-- is empty?
function panel:empty()
return self._VIEWS:empty()
end
-- get the first view
function panel:first()
return self._VIEWS:first()
end
-- get the next view
function panel:next(v)
return self._VIEWS:next(v)
end
-- get the previous view
function panel:prev(v)
return self._VIEWS:prev(v)
end
-- get the current selected child view
function panel:current()
return self._CURRENT
end
-- get view from the given name
function panel:view(name)
return self._VIEWS_CACHE[name]
end
-- insert view
function panel:insert(v, opt)
-- check
assert(not v:parent() or v:parent() == self)
assert(not self:view(v:name()), v:name() .. " has been in this panel!")
-- this view has been inserted into this panel? remove it first
if v:parent() == self then
self:remove(v)
end
-- center this view if centerx or centery are set
local bounds = v:bounds()
local center = false
local org = point {bounds.sx, bounds.sy}
if opt and opt.centerx then
org.x = math.floor((self:width() - v:width()) / 2)
center = true
end
if opt and opt.centery then
org.y = math.floor((self:height() - v:height()) / 2)
center = true
end
if center then
bounds:move(org.x - bounds.sx, org.y - bounds.sy)
v:invalidate(true)
end
-- insert this view
self._VIEWS:push(v)
-- cache this view
self._VIEWS_CACHE[v:name()] = v
-- set it's parent view
v:parent_set(self)
-- select this view
if v:option("selectable") then
self:select(v)
end
-- invalidate it
self:invalidate()
end
-- remove view
function panel:remove(v)
-- check
assert(v:parent() == self)
-- remove view
self._VIEWS:remove(v)
self._VIEWS_CACHE[v:name()] = nil
-- clear parent
v:parent_set(nil)
-- select next view
if self:current() == v then
self:select_next(nil, true)
end
-- invalidate it
self:invalidate()
end
-- clear views
function panel:clear()
-- clear parents
for v in self:views() do
v:parent_set(nil)
end
-- clear views and cache
self._VIEWS:clear()
self._VIEWS_CACHE = {}
-- reset the current view
self._CURRENT = nil
-- invalidate
self:invalidate()
end
-- select the child view
function panel:select(v)
-- check
assert(v == nil or (v:parent() == self and v:option("selectable")))
-- get the current selected view
local current = self:current()
if v == current then
return
end
-- undo the previous selected view
if current then
-- undo the current view first
if self:state("focused") then
current:state_set("focused", false)
end
current:state_set("selected", false)
end
-- update the current selected view
self._CURRENT = v
-- update the new selected view
if v then
-- select and focus this view
v:state_set("selected", true)
if self:state("focused") then
v:state_set("focused", true)
end
end
-- ok
return v
end
-- select the next view
function panel:select_next(start, reset)
-- is empty?
if self:empty() then
return
end
-- reset?
if reset then
self._CURRENT = nil
end
-- get current view
local current = start or self:current()
-- select the next view
local next = self:next(current)
while next ~= current do
if next and next:option("selectable") and next:state("visible") then
return self:select(next)
end
next = self:next(next)
end
end
-- select the previous view
function panel:select_prev(start)
-- is empty?
if self:empty() then
return
end
-- reset?
if reset then
self._CURRENT = nil
end
-- get current view
local current = start or self:current()
-- select the previous view
local prev = self:prev(current)
while prev ~= current do
if prev and prev:option("selectable") and prev:state("visible") then
return self:select(prev)
end
prev = self:prev(prev)
end
end
-- on event
function panel:event_on(e)
-- select view?
if e.type == event.ev_keyboard then
if e.key_name == "Right" then
return self:select_next()
elseif e.key_name == "Left" then
return self:select_prev()
end
end
end
-- set state
function panel:state_set(name, enable)
view.state_set(self, name, enable)
if name == "focused" and self:current() then
self:current():state_set(name, enable)
end
return self
end
-- draw panel
function panel:draw(transparent)
-- redraw panel?
local redraw = self:state("redraw")
-- draw panel background first
if redraw then
view.draw(self, transparent)
end
-- draw all child views
for v in self:views() do
if redraw then
v:state_set("redraw", true)
end
if v:state("visible") and (v:state("redraw") or v:type() == "panel") then
v:draw(transparent)
end
end
end
-- resize panel
function panel:resize()
-- resize panel?
local resize = self:state("resize")
if resize then
view.resize(self)
end
-- resize all child views
for v in self:views() do
if resize then
v:state_set("resize", true)
end
if v:state("visible") and (v:state("resize") or v:type() == "panel") then
v:resize()
end
end
end
-- refresh panel
function panel:refresh()
-- need not refresh? do not refresh it
if not self:state("refresh") or not self:state("visible") then
return
end
-- refresh all child views
for v in self:views() do
if v:state("refresh") then
v:refresh()
v:state_set("refresh", false)
end
end
-- refresh it
view.refresh(self)
-- clear mark
self:state_set("refresh", false)
end
-- dump all views
function panel:dump()
log:print("%s", self:_tostring(1))
end
-- tostring(panel, level)
function panel:_tostring(level)
local str = ""
if self.views then
str = str .. string.format("<%s %s>", self:name(), tostring(self:bounds()))
if not self:empty() then
str = str .. "\n"
end
for v in self:views() do
for l = 1, level do
str = str .. " "
end
str = str .. panel._tostring(v, level + 1) .. "\n"
end
else
str = tostring(self)
end
return str
end
-- tostring(panel)
function panel:__tostring()
return string.format("<panel(%s) %s>", self:name(), tostring(self:bounds()))
end
-- return module
return panel

104
src/ltui/point.lua Normal file
View File

@@ -0,0 +1,104 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file point.lua
--
--[[ Console User Interface (cui) ]-----------------------------------------
Author: Tiago Dionizio (tiago.dionizio AT gmail.com)
$Id: point.lua 18 2007-06-21 20:43:52Z tngd $
--------------------------------------------------------------------------]]
-- load modules
local object = require("ltui/object")
-- define module
local point = point or object { _init = {"x", "y"} }
-- add delta x and y
function point:addxy(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
return self
end
-- add point
function point:add(p)
return self:addxy(p.x, p.y)
end
-- sub delta x and y
function point:subxy(dx, dy)
return self:addxy(-dx, -dy)
end
-- sub point
function point:sub(p)
return self:addxy(-p.x, -p.y)
end
-- p1 + p2
function point:__add(p)
local np = self()
np.x = np.x + p.x
np.y = np.y + p.y
return np
end
-- p1 - p2
function point:__sub(p)
local np = self()
np.x = np.x - p.x
np.y = np.y - p.y
return np
end
-- -p
function point:__unm()
local p = self()
p.x = -p.x
p.y = -p.y
return p
end
-- p1 == p2?
function point:__eq(p)
return self.x == p.x and self.y == p.y
end
-- tostring(p)
function point:__tostring()
return '(' .. self.x .. ', ' .. self.y .. ')'
end
-- p1 .. p2
function point.__concat(op1, op2)
if type(op1) == 'string' then
return op1 .. op2:__tostring()
elseif type(op2) == 'string' then
return op1:__tostring() .. op2
else
return op1:__tostring() .. op2:__tostring()
end
end
-- return module
return point

456
src/ltui/program.lua Normal file
View File

@@ -0,0 +1,456 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file program.lua
--
--[[ Console User Interface (cui) ]-----------------------------------------
Author: Tiago Dionizio (tiago.dionizio AT gmail.com)
$Id: program.lua 18 2007-06-21 20:43:52Z tngd $
--------------------------------------------------------------------------]]
-- load modules
local log = require("ltui/base/log")
local rect = require("ltui/rect")
local point = require("ltui/point")
local panel = require("ltui/panel")
local event = require("ltui/event")
local curses = require("ltui/curses")
-- define module
local program = program or panel()
-- init program
function program:init(name, argv)
-- init main window
local main_window = self:main_window()
-- disable echo
curses.echo(false)
-- disable input cache
curses.cbreak(true)
-- disable newline
curses.nl(false)
-- to filter characters being output to the screen
-- this will filter all characters where a chtype or chstr is used
curses.map_output(true)
-- on WIN32 ALT keys need to be mapped, so to make sure you get the wanted keys,
-- only makes sense when using keypad(true) and echo(false)
curses.map_keyboard(true)
-- init colors
if (curses.has_colors()) then
curses.start_color()
end
-- disable main window cursor
main_window:leaveok(false)
-- enable special key map
main_window:keypad(true)
-- non-block for getch()
main_window:nodelay(true)
-- get 8-bits character for getch()
main_window:meta(true)
-- save the current arguments
self._ARGV = argv
-- init panel
panel.init(self, name, rect {0, 0, curses.columns(), curses.lines()})
-- init state
self:state_set("focused", true)
self:state_set("selected", true)
end
-- exit program
function program:exit()
-- exit panel
panel.exit(self)
-- (attempt to) make sure the screen will be cleared
-- if not restored by the curses driver
self:main_window():clear()
self:main_window():noutrefresh()
curses.doupdate()
-- exit curses
assert(not curses.isdone())
curses.done()
end
-- get the main window
function program:main_window()
-- init main window if not exists
local main_window = self._MAIN_WINDOW
if not main_window then
-- init main window
main_window = curses.init()
assert(main_window, "cannot init main window!")
-- save main window
self._MAIN_WINDOW = main_window
end
return main_window
end
-- get the command arguments
function program:argv()
return self._ARGV
end
-- get the current event
function program:event()
-- get event from the event queue first
local event_queue = self._EVENT_QUEUE
if event_queue then
local e = event_queue[1]
if e then
table.remove(event_queue, 1)
return e
end
end
-- get input key
local key_code, key_name, key_meta = self:_input_key()
if key_code then
return event.keyboard{key_code, key_name, key_meta}
end
end
-- on event
function program:event_on(e)
-- get the top focused view
local focused_view = self
while focused_view:type() == "panel" and focused_view:current() do
focused_view = focused_view:current()
end
-- do event for focused views
while focused_view and focused_view ~= self do
local parent = focused_view:parent()
if focused_view:event_on(e) then
return true
end
focused_view = parent
end
-- quit program?
if e.type == event.ev_keyboard and e.key_name == "CtrlC" then
self:send("cm_exit")
return true
elseif event.is_command(e, "cm_exit") then
self:quit()
return true
end
end
-- put an event to view
function program:event_put(e)
-- init event queue
self._EVENT_QUEUE = self._EVENT_QUEUE or {}
-- put event to queue
table.insert(self._EVENT_QUEUE, e)
end
-- send command
function program:send(command, extra)
self:event_put(event.command {command, extra})
end
-- quit program
function program:quit()
self:send("cm_quit")
end
-- run program loop
function program:loop(argv)
-- do message loop
local e = nil
local sleep = true
while true do
-- get the current event
e = self:event()
-- do event
if e then
event.dump(e)
self:event_on(e)
sleep = false
else
-- do idle event
self:event_on(event.idle())
sleep = true
end
-- quit?
if e and event.is_command(e, "cm_quit") then
break
end
-- resize views
self:resize()
-- draw views
self:draw()
-- refresh views
self:refresh()
-- wait some time, 50ms
if sleep then
curses.napms(50)
end
end
end
-- refresh program
function program:refresh()
-- need not refresh? do not refresh it
if not self:state("refresh") then
return
end
-- refresh views
panel.refresh(self)
-- trace
log:print("%s: refresh ..", self)
-- get main window
local main_window = curses.main_window()
-- refresh main window
self:window():copy(main_window, 0, 0, 0, 0, self:height() - 1, self:width() - 1)
-- refresh cursor
self:_refresh_cursor()
-- mark as refresh
main_window:noutrefresh()
-- do update
curses.doupdate()
end
-- get key map
function program:_key_map()
if not self._KEYMAP then
self._KEYMAP =
{
[ 1] = "CtrlA", [ 2] = "CtrlB", [ 3] = "CtrlC",
[ 4] = "CtrlD", [ 5] = "CtrlE", [ 6] = "CtrlF",
[ 7] = "CtrlG", [ 8] = "CtrlH", [ 9] = "CtrlI",
[10] = "CtrlJ", [11] = "CtrlK", [12] = "CtrlL",
[13] = "CtrlM", [14] = "CtrlN", [15] = "CtrlO",
[16] = "CtrlP", [17] = "CtrlQ", [18] = "CtrlR",
[19] = "CtrlS", [20] = "CtrlT", [21] = "CtrlU",
[22] = "CtrlV", [23] = "CtrlW", [24] = "CtrlX",
[25] = "CtrlY", [26] = "CtrlZ",
[ 8] = "Backspace",
[ 9] = "Tab",
[ 10] = "Enter",
[ 13] = "Enter",
[ 27] = "Esc",
[ 31] = "CtrlBackspace",
[127] = "Backspace",
[curses.KEY_DOWN ] = "Down",
[curses.KEY_UP ] = "Up",
[curses.KEY_LEFT ] = "Left",
[curses.KEY_RIGHT ] = "Right",
[curses.KEY_HOME ] = "Home",
[curses.KEY_END ] = "End",
[curses.KEY_NPAGE ] = "PageDown",
[curses.KEY_PPAGE ] = "PageUp",
[curses.KEY_IC ] = "Insert",
[curses.KEY_DC ] = "Delete",
[curses.KEY_BACKSPACE ] = "Backspace",
[curses.KEY_F1 ] = "F1",
[curses.KEY_F2 ] = "F2",
[curses.KEY_F3 ] = "F3",
[curses.KEY_F4 ] = "F4",
[curses.KEY_F5 ] = "F5",
[curses.KEY_F6 ] = "F6",
[curses.KEY_F7 ] = "F7",
[curses.KEY_F8 ] = "F8",
[curses.KEY_F9 ] = "F9",
[curses.KEY_F10 ] = "F10",
[curses.KEY_F11 ] = "F11",
[curses.KEY_F12 ] = "F12",
[curses.KEY_RESIZE ] = "Resize",
[curses.KEY_REFRESH ] = "Refresh",
[curses.KEY_BTAB ] = "ShiftTab",
[curses.KEY_SDC ] = "ShiftDelete",
[curses.KEY_SIC ] = "ShiftInsert",
[curses.KEY_SEND ] = "ShiftEnd",
[curses.KEY_SHOME ] = "ShiftHome",
[curses.KEY_SLEFT ] = "ShiftLeft",
[curses.KEY_SRIGHT ] = "ShiftRight",
}
end
return self._KEYMAP
end
-- get input key
function program:_input_key()
-- get main window
local main_window = self:main_window()
-- get input character
local ch = main_window:getch()
if not ch then
return
end
-- this is the time limit in ms within Esc-key sequences are detected as
-- Alt-letter sequences. useful when we can't generate Alt-letter sequences
-- directly. sometimes this pause may be longer than expected since the
-- curses driver may also pause waiting for another key (ncurses-5.3)
local esc_delay = 400
-- get key map
local key_map = self:_key_map()
-- is alt?
local alt = ch == 27
if alt then
-- get the next input character
ch = main_window:getch()
if not ch then
-- since there is no way to know the time with millisecond precision
-- we pause the the program until we get a key or the time limit
-- is reached
local t = 0
while true do
ch = main_window:getch()
if ch or t >= esc_delay then
break
end
-- wait some time, 50ms
curses.napms(50)
t = t + 50
end
-- nothing was typed... return Esc
if not ch then
return 27, "Esc", false
end
end
if ch > 96 and ch < 123 then
ch = ch - 32
end
end
-- map character to key
local key = key_map[ch]
local key_name = nil
if key then
key_name = alt and "Alt".. key or key
elseif (ch < 256) then
key_name = alt and "Alt".. string.char(ch) or string.char(ch)
else
return ch, '(noname)', alt
end
-- return key info
return ch, key_name, alt
end
-- refresh cursor
function program:_refresh_cursor()
-- get the top focused view
local focused_view = self
while focused_view:type() == "panel" and focused_view:current() do
focused_view = focused_view:current()
end
-- get the cursor state of the top focused view
local cursor_state = 0
if focused_view and focused_view:state("cursor_visible") then
cursor_state = focused_view:state("block_cursor") and 2 or 1
end
-- get the cursor position
local cursor = focused_view and focused_view:cursor()() or point{0, 0}
if cursor_state ~= 0 then
local v = focused_view
while v:parent() do
-- update the cursor position
cursor:addxy(v:bounds().sx, v:bounds().sy)
-- is cursor visible?
if cursor.x < 0 or cursor.y < 0 or cursor.x >= v:parent():width() or cursor.y >= v:parent():height() then
cursor_state = 0
break
end
-- get the parent view
v = v:parent()
end
end
-- update the cursor state
curses.cursor_set(cursor_state)
-- get main window
local main_window = curses.main_window()
-- trace
log:print("cursor(%s): %s, %d", focused_view, cursor, cursor_state)
-- move cursor position
if cursor_state ~= 0 then
main_window:move(cursor.y, cursor.x)
else
main_window:move(self:height() - 1, self:width() - 1)
end
end
-- return module
return program

179
src/ltui/rect.lua Normal file
View File

@@ -0,0 +1,179 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file rect.lua
--
--[[ Console User Interface (cui) ]-----------------------------------------
Author: Tiago Dionizio (tiago.dionizio AT gmail.com)
$Id: rect.lua 18 2007-06-21 20:43:52Z tngd $
--------------------------------------------------------------------------]]
-- load modules
local point = require("ltui/point")
local object = require("ltui/object")
-- define module
local rect = rect or object { _init = {"sx", "sy", "ex", "ey"} }
-- make rect
function rect:new(x, y, w, h)
return rect { x, y, x + w, y + h }
end
-- get rect size
function rect:size()
return point { self.ex - self.sx, self.ey - self.sy }
end
-- get width
function rect:width()
return self.ex - self.sx
end
-- get height
function rect:height()
return self.ey - self.sy
end
-- resize rect
function rect:resize(w, h)
self.ex = self.sx + w
self.ey = self.sy + h
end
-- move rect
function rect:move(dx, dy)
self.sx = self.sx + dx
self.sy = self.sy + dy
self.ex = self.ex + dx
self.ey = self.ey + dy
return self
end
-- move rect to the given position
function rect:move2(x, y)
local w = self.ex - self.sx
local h = self.ey - self.sy
self.sx = x
self.sy = y
self.ex = x + w
self.ey = y + h
return self
end
-- move top right corner of the rect
function rect:moves(dx, dy)
self.sx = self.sx + dx
self.sy = self.sy + dy
return self
end
-- move bottom left corner of the rect
function rect:movee(dx, dy)
self.ex = self.ex + dx
self.ey = self.ey + dy
return self
end
-- expand rect area
function rect:grow(dx, dy)
self.sx = self.sx - dx
self.sy = self.sy - dy
self.ex = self.ex + dx
self.ey = self.ey + dy
return self
end
-- is intersect?
function rect:is_intersect(r)
return not self():intersect(r):empty()
end
-- set rect with shared area between this rect and a given rect
function rect:intersect(r)
self.sx = math.max(self.sx, r.sx)
self.sy = math.max(self.sy, r.sy)
self.ex = math.min(self.ex, r.ex)
self.ey = math.min(self.ey, r.ey)
return self
end
-- get rect with shared area between two rects: local rect_new = r1 / r2
function rect:__div(r)
return self():intersect(r)
end
-- set union rect
function rect:union(r)
self.sx = math.min(self.sx, r.sx)
self.sy = math.min(self.sy, r.sy)
self.ex = math.max(self.ex, r.ex)
self.ey = math.max(self.ey, r.ey)
return self
end
-- get union rect: local rect_new = r1 + r2
function rect:__add(r)
return self():union(r)
end
-- r1 == r1?
function rect:__eq(r)
return
self.sx == r.sx and
self.sy == r.sy and
self.ex == r.ex and
self.ey == r.ey
end
-- contains the given point in rect?
function rect:contains(x, y)
return x >= self.sx and x < self.ex and y >= self.sy and y < self.ey
end
-- empty rect?
function rect:empty()
return self.sx >= self.ex or self.sy >= self.ey
end
-- tostring(r)
function rect:__tostring()
if self:empty() then
return '[]'
end
return string.format("[%d, %d, %d, %d]", self.sx, self.sy, self.ex, self.ey)
end
-- r1 .. r2
function rect.__concat(op1, op2)
if type(op1) == 'string' then
return op1 .. op2:__tostring()
elseif type(op2) == 'string' then
return op1:__tostring() .. op2
else
return op1:__tostring() .. op2:__tostring()
end
end
-- return module
return rect

58
src/ltui/statusbar.lua Normal file
View File

@@ -0,0 +1,58 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file statusbar.lua
--
-- load modules
local log = require("ltui/base/log")
local rect = require("ltui/rect")
local panel = require("ltui/panel")
local label = require("ltui/label")
local event = require("ltui/event")
local curses = require("ltui/curses")
-- define module
local statusbar = statusbar or panel()
-- init statusbar
function statusbar:init(name, bounds)
-- init panel
panel.init(self, name, bounds)
-- init info
self._INFO = label:new("statusbar.info", rect{0, 0, self:width(), self:height()})
self:insert(self:info())
self:info():text_set("Status Bar")
self:info():textattr_set("blue")
-- init background
self:background_set("white")
end
-- get status info
function statusbar:info()
return self._INFO
end
-- return module
return statusbar

120
src/ltui/textarea.lua Normal file
View File

@@ -0,0 +1,120 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file textarea.lua
--
-- load modules
local log = require("ui/log")
local view = require("ui/view")
local label = require("ui/label")
local event = require("ui/event")
local curses = require("ui/curses")
-- define module
local textarea = textarea or label()
-- init textarea
function textarea:init(name, bounds, text)
-- init label
label.init(self, name, bounds, text)
-- mark as selectable
self:option_set("selectable", true)
-- enable progress
self:option_set("progress", true)
-- init start line
self._STARTLINE = 0
self._LINECOUNT = 0
end
-- draw textarea
function textarea:draw(transparent)
-- draw background
view.draw(self, transparent)
-- get the text attribute value
local textattr = self:textattr_val()
-- draw text string
local strs = self._SPLITTEXT
if strs and #strs > 0 and textattr then
self:canvas():attr(textattr):move(0, 0):putstrs(strs, self._STARTLINE + 1)
end
-- draw progress
if self:option("progress") then
local progress = (self._STARTLINE + math.min(self:height(), self._LINECOUNT)) * 100 / self._LINECOUNT
if (self._STARTLINE > 0 or progress < 100) and self:width() > 20 then
self:canvas():move(self:width() - 10, self:height() - 1):putstr(string.format("(%%%d)", progress))
end
end
end
-- set text
function textarea:text_set(text)
self._STARTLINE = 0
self._SPLITTEXT = text and self:splitext(text) or {}
self._LINECOUNT = #self._SPLITTEXT
return label.text_set(self, text)
end
-- scroll
function textarea:scroll(lines)
if self._LINECOUNT > self:height() then
self._STARTLINE = self._STARTLINE + lines
if self._STARTLINE < 0 then
self._STARTLINE = 0
end
if self._STARTLINE > self._LINECOUNT - self:height() then
self._STARTLINE = self._LINECOUNT - self:height()
end
self:invalidate()
end
end
-- scroll to end
function textarea:scroll_to_end()
if self._LINECOUNT > self:height() then
self._STARTLINE = self._LINECOUNT - self:height()
self:invalidate()
end
end
-- on event
function textarea:event_on(e)
if e.type == event.ev_keyboard then
if e.key_name == "Up" then
self:scroll(-5)
return true
elseif e.key_name == "Down" then
self:scroll(5)
return true
end
end
end
-- return module
return textarea

72
src/ltui/textdialog.lua Normal file
View File

@@ -0,0 +1,72 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file textdialog.lua
--
-- load modules
local log = require("ui/log")
local rect = require("ui/rect")
local event = require("ui/event")
local dialog = require("ui/dialog")
local curses = require("ui/curses")
local textarea = require("ui/textarea")
-- define module
local textdialog = textdialog or dialog()
-- init dialog
function textdialog:init(name, bounds, title)
-- init window
dialog.init(self, name, bounds, title)
-- insert text
self:panel():insert(self:text())
-- select buttons by default
self:panel():select(self:buttons())
end
-- get text
function textdialog:text()
if not self._TEXT then
self._TEXT = textarea:new("textdialog.text", rect:new(0, 0, self:panel():width(), self:panel():height() - 1))
end
return self._TEXT
end
-- on event
function textdialog:event_on(e)
-- pass event to dialog
if dialog.event_on(self, e) then
return true
end
-- pass keyboard event to text area to scroll
if e.type == event.ev_keyboard then
return self:text():event_on(e)
end
end
-- return module
return textdialog

108
src/ltui/textedit.lua Normal file
View File

@@ -0,0 +1,108 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file textedit.lua
--
-- load modules
local log = require("ui/log")
local view = require("ui/view")
local label = require("ui/label")
local event = require("ui/event")
local border = require("ui/border")
local curses = require("ui/curses")
local textarea = require("ui/textarea")
-- define module
local textedit = textedit or textarea()
-- init textedit
function textedit:init(name, bounds, text)
-- init label
textarea.init(self, name, bounds, text)
-- show cursor
self:cursor_show(true)
-- mark as selectable
self:option_set("selectable", true)
-- disable progress
self:option_set("progress", false)
-- enable multiple line
self:option_set("multiline", true)
end
-- draw textedit
function textedit:draw(transparent)
-- draw label
textarea.draw(self, transparent)
-- move cursor
if not self:text() or #self:text() == 0 then
self:cursor_move(0, 0)
else
self:cursor_move(self:canvas():pos())
end
end
-- set text
function textedit:text_set(text)
textarea.text_set(self, text)
self:scroll_to_end()
return self
end
-- on event
function textedit:event_on(e)
-- update text
if e.type == event.ev_keyboard then
if e.key_code > 0x1f and e.key_code < 0x7f then
self:text_set(self:text() .. e.key_name)
return true
elseif e.key_name == "Enter" and self:option("multiline") then
self:text_set(self:text() .. '\n')
return true
elseif e.key_name == "Backspace" then
local text = self:text()
if #text > 0 then
self:text_set(text:sub(1, #text - 1))
end
return true
elseif e.key_name == "CtrlV" then
local pastetext = os.pbpaste()
if pastetext then
self:text_set(self:text() .. pastetext)
end
return true
end
end
-- do textarea event
return textarea.event_on(self, e)
end
-- return module
return textedit

481
src/ltui/view.lua Normal file
View File

@@ -0,0 +1,481 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file view.lua
--
-- load modules
local log = require("ltui/base/log")
local rect = require("ltui/rect")
local point = require("ltui/point")
local object = require("ltui/object")
local canvas = require("ltui/canvas")
local curses = require("ltui/curses")
-- define module
local view = view or object()
-- new view instance
function view:new(name, bounds, ...)
-- create instance
self = self()
-- init view
self:init(name, bounds, ...)
-- done
return self
end
-- init view
function view:init(name, bounds)
-- check
assert(name and type(bounds) == 'table')
-- init type
self._TYPE = "view"
-- init state
local state = object()
state.visible = true -- view visibility
state.cursor_visible = false -- cursor visibility
state.block_cursor = false -- block cursor
state.selected = false -- is selected?
state.focused = false -- is focused?
state.redraw = true -- need redraw
state.refresh = true -- need refresh
state.resize = true -- need resize
self._STATE = state
-- init options
local options = object()
options.selectable = false -- true if window can be selected
self._OPTIONS = options
-- init attributes
self._ATTRS = object()
-- init actions
self._ACTIONS = object()
-- init extras
self._EXTRAS = object()
-- init name
self._NAME = name
-- init cursor
self._CURSOR = point{0, 0}
-- init bounds and window
self:bounds_set(bounds)
end
-- exit view
function view:exit()
-- close window
if self:window() then
self:window():close()
self._WINDOW = nil
end
end
-- get view name
function view:name()
return self._NAME
end
-- get view bounds
function view:bounds()
return self._BOUNDS
end
-- set window bounds
function view:bounds_set(bounds)
if bounds and self:bounds() ~= bounds then
self._BOUNDS = bounds()
self:invalidate(true)
end
end
-- get view width
function view:width()
return self:bounds():width()
end
-- get view height
function view:height()
return self:bounds():height()
end
-- get view size
function view:size()
return self:bounds():size()
end
-- get the parent view
function view:parent()
return self._PARENT
end
-- set the parent view
function view:parent_set(parent)
self._PARENT = parent
end
-- get the application
function view:application()
if not self._APPLICATION then
local app = self
while app:parent() do
app = app:parent()
end
self._APPLICATION = app
end
return self._APPLICATION
end
-- get the view window
function view:window()
return self._WINDOW
end
-- get the view canvas
function view:canvas()
if not self._CANVAS then
self._CANVAS = canvas:new(self, self:window())
end
return self._CANVAS
end
-- draw view
function view:draw(transparent)
-- trace
log:print("%s: draw ..", self)
-- draw background
if not transparent then
local background = self:background()
if background then
background = curses.color_pair(background, background)
self:canvas():attr(background):move(0, 0):putchar(' ', self:width() * self:height())
else
self:canvas():clear()
end
end
-- clear mark
self:state_set("redraw", false)
self:_mark_refresh()
end
-- refresh view
function view:refresh()
-- refresh to the parent view
local parent = self:parent()
if parent and self:state("visible") then
-- clip bounds with the parent view
local bounds = self:bounds()
local r = bounds():intersect(rect{0, 0, parent:width(), parent:height()})
if not r:empty() then
-- trace
log:print("%s: refresh to %s(%d, %d, %d, %d) ..", self, parent:name(), r.sx, r.sy, r.ex, r.ey)
-- copy this view to parent view
self:window():copy(parent:window(), 0, 0, r.sy, r.sx, r.ey - 1, r.ex - 1)
end
end
end
-- resize bounds of inner child views (abstract)
function view:resize()
-- trace
log:print("%s: resize ..", self)
-- close the previous windows first
if self:window() then
self:window():close()
self._WINDOW = nil
end
-- need renew canvas
self._CANVAS = nil
-- create a new window
self._WINDOW = curses.new_pad(self:height() > 0 and self:height() or 1, self:width() > 0 and self:width() or 1)
assert(self._WINDOW, "cannot create window!")
-- disable cursor
self:window():leaveok(true)
-- clear mark
self:state_set("resize", false)
end
-- show view?
--
-- .e.g
-- v:show(false)
-- v:show(true, {focused = true})
--
function view:show(visible, opt)
if self:state("visible") ~= visible then
local parent = self:parent()
if parent and parent:current() == self and not visible then
parent:select_next(nil, true)
elseif parent and visible and opt and opt.focused then
parent:select(self)
end
self:state_set("visible", visible)
self:invalidate()
end
end
-- invalidate view to redraw it
function view:invalidate(bounds)
if bounds then
self:_mark_resize()
end
self:_mark_redraw()
end
-- on event (abstract)
--
-- @return true: done and break dispatching, false/nil: continous to dispatch to other views
--
function view:event_on(e)
end
-- get the current event
function view:event()
return self:parent() and self:parent():event()
end
-- put an event to view
function view:event_put(e)
return self:parent() and self:parent():event_put(e)
end
-- get type
function view:type()
return self._TYPE
end
-- set type
function view:type_set(t)
self._TYPE = t or "view"
return self
end
-- get state
function view:state(name)
return self._STATE[name]
end
-- set state
function view:state_set(name, enable)
-- state is not changed?
enable = enable or false
if self:state(name) == enable then
return self
end
-- change state
self._STATE[name] = enable
return self
end
-- get option
function view:option(name)
return self._OPTIONS[name]
end
-- set option
function view:option_set(name, enable)
-- state is not changed?
enable = enable or false
if self:option(name) == enable then
return
end
-- set option
self._OPTIONS[name] = enable
end
-- get attribute
function view:attr(name)
return self._ATTRS[name]
end
-- set attribute
function view:attr_set(name, value)
self._ATTRS[name] = value
self:invalidate()
return self
end
-- get extra data
function view:extra(name)
return self._EXTRAS[name]
end
-- set extra data
function view:extra_set(name, value)
self._EXTRAS[name] = value
return self
end
-- get action
function view:action(name)
return self._ACTIONS[name]
end
-- set action
function view:action_set(name, on_action)
self._ACTIONS[name] = on_action
return self
end
-- do action
function view:action_on(name, ...)
local on_action = self:action(name)
if on_action then
if type(on_action) == "string" then
-- send command
if self:application() then
self:application():send(on_action)
end
elseif type(on_action) == "function" then
-- do action script
return on_action(self, ...)
end
end
end
-- get cursor position
function view:cursor()
return self._CURSOR
end
-- move cursor to the given position
function view:cursor_move(x, y)
self._CURSOR = point{ self:_limit(x, 0, self:width() - 1), self:_limit(y, 0, self:height() - 1) }
return self
end
-- show cursor?
function view:cursor_show(visible)
if self:state("cursor_visible") ~= visible then
self:state_set("cursor_visible", visible)
end
return self
end
-- get background
function view:background()
local background = self:attr("background")
if not background and self:parent() then
background = self:parent():background()
end
return background
end
-- set background, .e.g background_set("blue")
function view:background_set(color)
return self:attr_set("background", color)
end
-- limit value range
function view:_limit(value, minval, maxval)
return math.min(maxval, math.max(value, minval))
end
-- need resize view
function view:_mark_resize()
-- have been marked?
if self:state("resize") then
return
end
-- trace
log:print("%s: mark as resize", self)
-- need resize it
self:state_set("resize", true)
end
-- need redraw view
function view:_mark_redraw()
-- have been marked?
if self:state("redraw") then
return
end
-- trace
log:print("%s: mark as redraw", self)
-- need redraw it
self:state_set("redraw", true)
-- need redraw it's parent view if this view is invisible
if not self:state("visible") and self:parent() then
self:parent():_mark_redraw()
end
end
-- need refresh view
function view:_mark_refresh()
-- have been marked?
if self:state("refresh") then
return
end
-- need refresh it
if self:state("visible") then
self:state_set("refresh", true)
end
-- need refresh it's parent view
if self:parent() then
self:parent():_mark_refresh()
end
end
-- tostring(view)
function view:__tostring()
return string.format("<view(%s) %s>", self:name(), tostring(self:bounds()))
end
-- return module
return view

131
src/ltui/window.lua Normal file
View File

@@ -0,0 +1,131 @@
--!A cross-platform build utility based on Lua
--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015 - 2018, TBOOX Open Source Group.
--
-- @author ruki
-- @file window.lua
--
-- load modules
local log = require("ui/log")
local rect = require("ui/rect")
local view = require("ui/view")
local label = require("ui/label")
local panel = require("ui/panel")
local event = require("ui/event")
local border = require("ui/border")
local curses = require("ui/curses")
local action = require("ui/action")
-- define module
local window = window or panel()
-- init window
function window:init(name, bounds, title, shadow)
-- init panel
panel.init(self, name, bounds)
-- check bounds
assert(self:width() > 4 and self:height() > 3, string.format("%s: too small!", self))
-- insert shadow
if shadow then
self:insert(self:shadow())
self:frame():bounds():movee(-2, -1)
self:frame():invalidate(true)
end
-- insert border
self:frame():insert(self:border())
-- insert title
if title then
self._TITLE = label:new("window.title", rect{0, 0, #title, 1}, title)
self:title():textattr_set("blue bold")
self:title():action_set(action.ac_on_text_changed, function (v)
if v:text() then
local bounds = v:bounds()
v:bounds():resize(#v:text(), v:height())
bounds:move2(math.max(0, math.floor((self:frame():width() - v:width()) / 2)), bounds.sy)
v:invalidate(true)
end
end)
self:frame():insert(self:title(), {centerx = true})
end
-- insert panel
self:frame():insert(self:panel())
-- insert frame
self:insert(self:frame())
end
-- get frame
function window:frame()
if not self._FRAME then
self._FRAME = panel:new("window.frame", rect{0, 0, self:width(), self:height()}):background_set("white")
end
return self._FRAME
end
-- get panel
function window:panel()
if not self._PANEL then
self._PANEL = panel:new("window.panel", self:frame():bounds())
self._PANEL:bounds():grow(-1, -1)
self._PANEL:invalidate(true)
end
return self._PANEL
end
-- get title
function window:title()
return self._TITLE
end
-- get shadow
function window:shadow()
if not self._SHADOW then
self._SHADOW = view:new("window.shadow", rect{2, 1, self:width(), self:height()}):background_set("black")
end
return self._SHADOW
end
-- get border
function window:border()
if not self._BORDER then
self._BORDER = border:new("window.border", self:frame():bounds())
end
return self._BORDER
end
-- on event
function window:event_on(e)
-- select panel?
if e.type == event.ev_keyboard then
if e.key_name == "Tab" then
return self:panel():select_next()
end
end
end
-- return module
return window

9
tests/load.lua Executable file
View File

@@ -0,0 +1,9 @@
-- init load directories
package.path = package.path .. ';./src/?.lua'
package.cpath = package.cpath .. ';./build/ltui.dll;./build/libltui.so;./build/libltui.dylib'
local os = require("ltui/base/os")
local ltui = require("ltui.curses")
print(ltui)
print(os.host())