diff options
| author | Opportunity <[email protected]> | 2020-01-16 17:03:20 +0800 |
|---|---|---|
| committer | Opportunity <[email protected]> | 2020-01-16 17:03:20 +0800 |
| commit | d5f108398a0b638f60aaab8089831bb9e67023c4 (patch) | |
| tree | 4756301fbfd20abfb6373f913a060d6290a21a6e | |
| parent | d72f0495c5a8b860e7abed729f0d874542653066 (diff) | |
add table
| -rw-r--r-- | xmake/core/base/cli.lua | 72 | ||||
| -rw-r--r-- | xmake/core/base/colors.lua | 77 | ||||
| -rw-r--r-- | xmake/core/base/option.lua | 204 | ||||
| -rw-r--r-- | xmake/core/base/text.lua | 370 | ||||
| -rw-r--r-- | xmake/core/sandbox/modules/import/core/base/text.lua | 38 |
5 files changed, 462 insertions, 299 deletions
diff --git a/xmake/core/base/cli.lua b/xmake/core/base/cli.lua index 9ce43500b..310335fd4 100644 --- a/xmake/core/base/cli.lua +++ b/xmake/core/base/cli.lua @@ -117,78 +117,6 @@ function cli.parsev(argv, flags) return parsed end --- @see https://unicode.org/reports/tr14/ -function cli._lastwbr(str, width, wordbreak) - - -- check - assert(#str >= width) - - if wordbreak == "breakall" then - -- To prevent overflow, word may be broken at any character - return width - else - - if str:sub(width + 1, width + 1):find("[%s]") then - -- exact break - return width - end - - local range = str:sub(1, width) - local poss = range:reverse():find("[%s-]") - if poss then - return #range - poss + 1 - end - - -- not found in range, try afterwards - poss = str:find("[%s-]", width + 1) - if poss then - return poss - end - - -- not found in all str - return #str - end -end - --- break lines -function cli.wordwrap(str, width, opt) - - opt = opt or {} - - -- split to lines - if type(str) == 'table' then - str = table.concat(str, '\n') - end - local lines = tostring(str):split('\n', {plain = true, strict = true}) - - local result = {} - -- handle lines - for _, v in ipairs(lines) do - - -- remove tailing spaces, include '\r', which will be produced by `('l1\r\nl2'):split(...)` - v = v:rtrim() - while #v > width do - - -- find word break chance - local wbr = cli._lastwbr(v, width, opt.wordbreak) - - -- break line - local line = v:sub(1, wbr):rtrim() - table.insert(result, line) - v = v:sub(wbr + 1):ltrim() - - -- prevent empty line - if #v == 0 then v = nil end - end - - -- put remaining parts - table.insert(result, v) - end - - -- ok - return result -end - cli._segment = segment -- return module return cli diff --git a/xmake/core/base/colors.lua b/xmake/core/base/colors.lua index de66a9414..c49191cd1 100644 --- a/xmake/core/base/colors.lua +++ b/xmake/core/base/colors.lua @@ -442,83 +442,6 @@ function colors.ignore(str) return colors.translate(str, {plain = true}) end --- make a table with colors --- --- @param data table data, array of array of strings with colors, eg: {{"1", "2", "3"}, {"4", "5", "6"}} --- @param opt options --- plain: false --- sep: table colunm sepertor, default is ' | ' --- colunms: colunm attributes, eg: {{align = 'left', min_width = 80}, {align = 'center'}, {align = 'right', min_width = 120}} -function colors.table(data, opt) - - assert(data) - - -- init options - opt = opt or {} - if opt.sep == nil then opt.sep = ' | ' end - opt.patch_reset = true - opt.ignore_unknown = true - local sep = colors.translate(opt.sep, opt) - - -- formartted cells - local tab = {} - local col_width = {} - -- 'l' 'r' 'c' - local col_align = {} - - -- load column options - if opt.colunms then - for i = 1, table.maxn(opt.colunms or {}) do - local v = opt.colunms[i] or {} - col_width[i] = v.min_width or 0 - col_align[i] = (v.align or 'l'):sub(1, 1):lower() - end - end - - -- format cells - for i, row in ipairs(data) do - tab[i] = {} - for j, cell in ipairs(row) do - local str = tostring(cell) - local value = colors.translate(str, opt) - local len = opt.plain and #value or #colors.ignore(str) - tab[i][j] = { str = value, len = len } - col_width[j] = math.max(len, col_width[j] or 0) - end - end - - -- render cells - local empty_cell = { str = "", len = 0 } - local rows = {} - for i, row in ipairs(tab) do - local cells = {} - for j = 1, #col_width do - - -- use empty cell if not defined - local cell = row[j] or empty_cell - - if col_align[j] == 'r' then - -- right align - cells[j] = string.rep(' ', col_width[j] - cell.len) .. cell.str - elseif col_align[j] == 'c' then - -- centered - local padding = col_width[j] - cell.len - local lp = math.floor(padding / 2) - local rp = math.ceil(padding / 2) - cells[j] = string.rep(' ', lp) .. cell.str .. string.rep(' ', rp) - else - --left align, emit tailing spaces for last colunm - cells[j] = cell.str .. ((j == #col_width) and "" or string.rep(' ', col_width[j] - cell.len)) - end - end - rows[i] = table.concat(cells, sep) - end - - -- concat rendered rows - rows[#rows + 1] = "" - return table.concat(rows, '\n') -end - -- get theme function colors.theme() return colors._THEME diff --git a/xmake/core/base/option.lua b/xmake/core/base/option.lua index a862a2d40..b9721edec 100644 --- a/xmake/core/base/option.lua +++ b/xmake/core/base/option.lua @@ -25,6 +25,7 @@ local option = option or {} local cli = require("base/cli") local table = require("base/table") local colors = require("base/colors") +local text = require("base/text") -- ifelse, a? b : c function option._ifelse(a, b, c) @@ -69,51 +70,6 @@ function option._context() end end --- get line length -function option._get_linelen(st) - local poss = st:reverse():find("\n") - if not poss then return (#st) end - local start_pos, _ = poss - return start_pos - 1 -end - --- get last space -function option._get_lastspace(st) - local poss = st:reverse():find("[%s-]") - if not poss then return (#st) end - local start_pos, _ = poss - return (#st) - start_pos + 1 -end - --- append spaces in width -function option._inwidth_append(dst, st, padding, width, remain_width) - - if padding >= width then - return dst .. st - end - - local white_padding = string.rep(" ", padding) - if remain_width == nil then - -- TODO because of colored string, it's wrong sometimes - remain_width = width - option._get_linelen(dst) - end - - if remain_width <= 0 then - return option._inwidth_append(dst .. "\n" .. white_padding, st, padding, width, width - padding) - end - - if (#st) <= remain_width then - return dst .. st - end - - local lastspace = option._get_lastspace(st:sub(1, remain_width)) - if lastspace + 1 > (#st) then - return dst .. st - else - return option._inwidth_append(dst .. st:sub(1, lastspace) .. "\n" .. white_padding, st:sub(lastspace + 1):ltrim(), padding, width, width - padding) - end -end - -- save context function option.save(taskname) @@ -633,7 +589,7 @@ function option.show_menu(task) if taskmenu.options then option.show_options(taskmenu.options, task) end -end +end -- show the main menu function option.show_main() @@ -710,30 +666,26 @@ function option.show_main() -- print category name table.insert(tablecontent, {}) - table.insert(tablecontent, {string.format("${bright}%s%ss: ", string.sub(category.name, 1, 1):upper(), string.sub(category.name, 2))}) - - -- the padding spaces - local padding = 42 - - -- get width of right colunm - local right_width = math.max(os.getwinsize().width, 60) - 41 + table.insert(tablecontent, {{string.format("%s%ss: ", string.sub(category.name, 1, 1):upper(), string.sub(category.name, 2)), style="${reset bright}"}}) -- print tasks for taskname, taskinfo in pairs(category.tasks) do -- init the task line - local taskline = string.format("${color.menu.main.task.name} %s%s", + local taskline = string.format(" %s%s", taskinfo.shortname and (taskinfo.shortname .. ", ") or " ", taskname) - - local taskdesc = cli.wordwrap(taskinfo.description or "", right_width) - table.insert(tablecontent, {taskline, taskdesc[1]}) - for i = 2, #taskdesc do - table.insert(tablecontent, {"", taskdesc[i]}) - end + table.insert(tablecontent, {taskline, taskinfo.description or ""}) end end - io.write(colors.table(tablecontent, {sep = " ", colunms = {{min_width=40}, {}}})) + + -- set table styles + tablecontent.style = {"${color.menu.main.task.name}"} + tablecontent.width = {nil, "auto"} + tablecontent.sep = " " + + -- print table + io.write(text.table(tablecontent)) end -- print options @@ -748,9 +700,6 @@ function option.show_options(options, taskname) -- check assert(options) - -- the padding spaces - local padding = 42 - -- remove repeat empty lines local is_action = false local emptyline_count = 0 @@ -769,27 +718,28 @@ function option.show_options(options, taskname) end end + local tablecontent = {} + -- print header - io.print("") + table.insert(tablecontent, {}) if is_action then - io.print(colors.translate("${bright}Common options: ")) + table.insert(tablecontent, {{"Common options:", style="${reset bright}"}}) else - io.print(colors.translate("${bright}Options: ")) + table.insert(tablecontent, {{"Options:", style="${reset bright}"}}) end -- print options - options = printed_options - for _, opt in ipairs(options) do + for _, opt in ipairs(printed_options) do -- the following options are belong action? show command section -- -- @see core/base/task.lua: translate menu -- if opt.category and opt.category == "action" then - io.print("") - io.print(colors.translate("${bright}Command options (" .. taskname .. "): ")) + table.insert(tablecontent, {}) + table.insert(tablecontent, {{"Command options(" .. taskname .. "):", style="${reset bright}"}}) end - + -- init the option info local option_info = "" @@ -821,104 +771,58 @@ function option.show_options(options, taskname) option_info = option_info .. " ..." end - -- append spaces - for i = (#option_info), padding do - option_info = option_info .. " " + -- get description + local description = table.move(opt, 5, table.maxn(opt), 1, {}) + if #description == 0 then + description[1] = "" end - -- append color - option_info = colors.translate("${color.menu.option.name}" .. option_info .. "${clear}") - - -- get width of console - local console_width = math.max(os.getwinsize().width, 80) - - -- append the option description - local description = opt[5] - if description then - option_info = option._inwidth_append(option_info, description, padding + 1, console_width, console_width - padding - 1) + -- transform description + local desp_strs = {} + for _, v in ipairs(description) do + if type(v) == "function" then + v = v() + end + if type(v) == "string" then + table.insert(desp_strs, v) + elseif type(v) == "table" then + table.move(v, 1, #v, #desp_strs + 1, desp_strs) + end end -- append the default value if default then local defaultval = tostring(default) if type(default) == "boolean" then - defaultval = option._ifelse(default, "y", "n") + defaultval = default and "y" or "n" end - option_info = option._inwidth_append(option_info, " (default: ", padding + 1, console_width) - local origin_width = option._get_linelen(option_info) - option_info = option_info .. "${bright}" - option_info = option._inwidth_append(option_info, defaultval, padding + 1, console_width, console_width - origin_width) - origin_width = option._ifelse(origin_width + #defaultval > console_width, option._get_linelen(option_info), origin_width + (#(tostring(default)))) - option_info = option_info .. "${clear}" - option_info = option._inwidth_append(option_info, ")", padding + 1, console_width, console_width - origin_width) + local def_desp = colors.translate(string.format(" (default: ${bright}%s${clear})", defaultval)) + desp_strs[1] = desp_strs[1] .. def_desp end - -- print option info - io.print(colors.translate(option_info)) - - -- print more description if exists - for i = 6, 64 do - - -- the description, @note some option may be nil - local description = opt[i] - if not description then break end - - -- is function? get results - if type(description) == "function" then - description = description() - end - - -- the description is string? - if type(description) == "string" then - - -- make spaces - local spaces = "" - for i = 0, padding do - spaces = spaces .. " " - end - - -- print this description - io.print(option._inwidth_append(spaces, description, padding + 1, console_width)) - - -- the description is table? - elseif type(description) == "table" then - - -- print all descriptions - for _, v in pairs(description) do - - -- make spaces - local spaces = "" - for i = 0, padding do - spaces = spaces .. " " - end - - -- print this description - io.print(option._inwidth_append(spaces, v, padding + 1, console_width)) - end - end - end - - -- print values + -- append values local values = opt.values if type(values) == "function" then values = values() end if values then - for _, value in ipairs(table.wrap(values)) do - - -- make spaces - local spaces = "" - for i = 0, padding do - spaces = spaces .. " " - end - - -- print this value - io.print(option._inwidth_append(spaces, " - " .. tostring(value), padding + 1, console_width)) + table.insert(desp_strs, " - " .. tostring(value)) end end + + -- insert row + table.insert(tablecontent, {option_info, desp_strs}) end -end + + -- set table styles + tablecontent.style = {"${color.menu.option.name}"} + tablecontent.width = {nil, "auto"} + tablecontent.sep = " " + + -- print table + io.write(text.table(tablecontent)) +end -- return module: option return option diff --git a/xmake/core/base/text.lua b/xmake/core/base/text.lua new file mode 100644 index 000000000..462c757a1 --- /dev/null +++ b/xmake/core/base/text.lua @@ -0,0 +1,370 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed 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-2020, TBOOX Open Source Group. +-- +-- @author OpportunityLiu +-- @file text.lua +-- + +-- define module +local text = text or {} + +-- load modules +local string = require("base/string") +local colors = require("base/colors") +local math = require("base/math") +local dump = require("base/dump") + +-- @see https://unicode.org/reports/tr14/ +function text._lastwbr(str, width, wordbreak) + + -- check + assert(#str >= width) + + if wordbreak == "breakall" then + -- To prevent overflow, word may be broken at any character + return width + else + + if str:sub(width + 1, width + 1):find("[%s]") then + -- exact break + return width + end + + local range = str:sub(1, width) + local poss = range:reverse():find("[%s-]") + if poss then + return #range - poss + 1 + end + + -- not found in range, try afterwards + poss = str:find("[%s-]", width + 1) + if poss then + return poss + end + + -- not found in all str + return #str + end +end + +-- break lines +function text.wordwrap(str, width, opt) + + opt = opt or {} + + -- split to lines + if type(str) == 'table' then + str = table.concat(str, '\n') + end + local lines = tostring(str):split('\n', {plain = true, strict = true}) + + local result = {} + local actual_width = 0 + + -- handle lines + for _, v in ipairs(lines) do + + -- remove tailing spaces, include '\r', which will be produced by `('l1\r\nl2'):split(...)` + v = v:rtrim() + + while #v > width do + + -- find word break chance + local wbr = text._lastwbr(v, width, opt.wordbreak) + + -- break line + local line = v:sub(1, wbr):rtrim() + actual_width = math.max(#line, actual_width) + table.insert(result, line) + v = v:sub(wbr + 1):ltrim() + + -- prevent empty line + if #v == 0 then + v = nil + break + end + end + + -- put remaining parts + if v then + actual_width = math.max(#v, actual_width) + table.insert(result, v) + end + end + + -- ok + return result, actual_width +end + +function text._format_cell(cell, width, opt) + local result = {} + local max_width = 0 + for _, v in ipairs(cell) do + local lines, aw = text.wordwrap(tostring(v), width[2], opt) + table.move(lines, 1, #lines, #result + 1, result) + max_width = math.max(max_width, aw) + end + cell.formatted = result + cell.width = max_width +end + +function text._format_col(col, width, opt) + local max_width = 0 + for i = 1, table.maxn(col) do + local v = col[i] + -- skip span cells + if v and not v.span then + text._format_cell(v, width, opt) + max_width = math.max(max_width, v.width) + end + end + col.width = max_width +end + + +-- make a table with colors +-- +-- @param data table data, array of array of cells with optional styles +-- eg: { +-- {"1", nil, "3"}, -- use nil to make previous cell to span next column +-- {"4", "5", {"line1", "line2", style="${yellow}", align = 'r'}}, -- multi-line content & set style or align for cell +-- {"7", "8", {"9", style="${reset}${red}"}, style="${bright}", align = 'c'}, -- set style or align for row +-- style = {"${underline}"}, -- set style for columns +-- -- or use "${underline}" for all columns +-- width = { 20, {10, 50}, "auto"}, +-- -- 2 numbers - min and max width (nil for not set, eg: {nil, 50}); +-- -- a number - width, num is equivalent to {num, num}; +-- -- nil - no limit, equivalent to {nil, nil} +-- -- "auto" - use remain space of console, only one 'auto' colunm is allowed +-- align = {'l', 'r', 'c'} -- align mode for each column, 'left', 'center' or 'right' +-- sep = "${dim} | ", -- table colunm sepertor, default is ' | ', use '' to hide +-- } +-- priority of style and align: cell > row > col +-- @param opt options for color rendering and word warpping +function text.table(data, opt) + + assert(data) + + -- init options + opt = opt or { patch_reset = false, ignore_unknown = true } + opt.patch_reset = false + local sep = colors.translate(data.sep or ' | ', opt) + local sep_len = #colors.ignore(data.sep or ' | ', opt) + + -- col ordered cells + local cols = {} + local n_row = table.maxn(data) + local n_col = 1 + + -- count cols + for i = 1, n_row do + local row = data[i] + if row == nil then + data[i] = {{""}} + else + n_col = math.max(n_col, table.maxn(row)) + end + end + + -- reorder + for i = 1, n_row do + local row = data[i] + local p_cell = nil + for j = 1, n_col do + local cell = row[j] + if cell ~= nil and type(cell) ~= "table" then + -- wrap cells if needed + cell = {tostring(cell)} + elseif cell == nil and j == 1 then + cell = {""} + end + local col = cols[j] + if not col then + col = {} + cols[j] = col + end + if cell then + col[i] = cell + p_cell = cell + else + p_cell.span = (p_cell.span or 1) + 1 + end + end + end + + -- load column options + data.width = data.width or {} + data.align = data.align or {} + data.style = data.style or {} + + local style = "" + if type(data.style) == "string" then + style = data.style + data.style = {} + end + + -- index of auto col + local auto_col = nil + for i = 1, n_col do + + -- load width + local w = data.width[i] + if w ~= "auto" then + local wl, wu + if w == nil then + wl, wu = 0, math.huge + elseif type(w) == 'number' then + if math.isnan(w) or math.isinf(w) then + wl, wu = 0, math.huge + else + wl, wu = w, w + end + else + wl, wu = w[1], w[2] + end + wl = wl or 0 + wu = wu or math.huge + data.width[i] = {wl, wu} + else + assert(not auto_col, 'Only one "auto" colunm is allowed.') + auto_col = i + end + + -- load align + cols[i].align = (data.align[i] or 'l'):sub(1, 1):lower() + -- load style + cols[i].style = data.style[i] or style + end + + -- format table + + -- 1. format non-auto cols + for i, col in ipairs(cols) do + if i ~= auto_col then + text._format_col(col, data.width[i], opt) + end + end + + if auto_col then + + -- 2. caculate auto col width + local auto_width = os.getwinsize().width + for i = 1, n_col do + if i ~= auto_col then + auto_width = auto_width - cols[i].width + end + end + auto_width = math.max(0, auto_width - sep_len * (n_col - 1)) + data.width[auto_col] = {0,auto_width} + + -- 3. format auto col + text._format_col(cols[auto_col], data.width[auto_col], opt) + end + + -- 4. format span cell + for i, col in ipairs(cols) do + + for j = 1, n_row do + local cell = col[j] + if cell and cell.span then + local w, wl = 0, 0 + for ci = 0, (cell.span - 1) do + -- actual width of spanned cols + w = w + cols[i + ci].width + -- min width of spanned cols + wl = wl + data.width[i + ci][1] + end + text._format_cell(cell, {0, math.max(w, wl) + sep_len * (cell.span - 1)}, opt) + end + end + end + + -- render cells + + -- row ordered cells + local rows = {} + + -- reorder + for i = 1, n_row do + local row = {} + local line = 1 + for j = 1, n_col do + local cell = cols[j][i] + if cell then + assert(cell.formatted) + line = math.max(#cell.formatted, line) + end + row[j] = cell + end + row.line = line + rows[i] = row + end + + local results = {} + local reset = colors.translate("${reset}", opt) + for i, row in ipairs(rows) do + for l = 1, row.line do + local cells = {} + local j = 1 + while j <= n_col do + + local cell = row[j] + assert(cell) + local col = cols[j] + + if l == 1 then + cell.align = cell.align or row.align or col.align + cell.style = colors.translate((col.style or "") .. (row.style or "") .. (cell.style or ""), opt) + end + + local str = cell.formatted[l] or "" + local width = col.width + local span = cell.span or 1 + if cell.span then + for ci = (j + 1), (j + span - 1) do + width = width + cols[ci].width + end + width = width + sep_len * (span - 1) + end + + local padded + if cell.align == 'r' then + -- right align + padded = string.rep(' ', width - #str) .. str + elseif cell.align == 'c' then + -- centered + local padding = width - #str + local lp = math.floor(padding / 2) + local rp = math.ceil(padding / 2) + padded = string.rep(' ', lp) .. str .. string.rep(' ', rp) + else + --left align, emit tailing spaces for last colunm + padded = str .. ((j + span == n_col + 1) and "" or string.rep(' ', width - #str)) + end + table.insert(cells, cell.style .. padded .. reset) + j = j + span + end + table.insert(results, table.concat(cells, sep)) + end + end + + -- concat rendered rows + results[#results + 1] = "" + return table.concat(results, '\n') +end + +-- return module +return text diff --git a/xmake/core/sandbox/modules/import/core/base/text.lua b/xmake/core/sandbox/modules/import/core/base/text.lua new file mode 100644 index 000000000..dbbc9eccc --- /dev/null +++ b/xmake/core/sandbox/modules/import/core/base/text.lua @@ -0,0 +1,38 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed 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-2020, TBOOX Open Source Group. +-- +-- @author OpportunityLiu +-- @file text.lua +-- + +-- load modules +local text = require("base/text") + + +-- define module +local sandbox_text = sandbox_text or {} + +-- inherit some builtin interfaces +for key, value in pairs(text) do + if not key:startswith("_") then + sandbox_text[key] = value + end +end + +-- return module +return sandbox_text + + |
