diff --git a/README.md b/README.md index 5915103..4ea9d9b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,23 @@ Draft mode: https://imgur.com/a/BOvMeQL __Most of the development effort now happens in the ng branch, which introduces a few breaking changes but a ton of new features/enhancements.__ +## Machi-ng + +Machi-ng is a refactoring effort of machi with new features and enhancements. + +### Breaking changes + +1. Added a max split (before merging) of 1,000 for all commands and a global cap of 10,000 areas. +2. `t` command now applies to the current area and its further splits, instead of globally. +3. `s` command now shifts inside the last group of pending areas that have the same parent, instead of all pending areas. + +### New features & enhancements + +1. Minimum area size. +2. More tolerating "safer" error handling. +3. Dynamic size adjustment with propagation. +4. Editing non-global areas. + ## Why? TL;DR --- To bring back the control of the window layout. @@ -46,6 +63,7 @@ Use `local layout = machi.layout.create(args)` to instantiate the layout with an - `name`: the constant name of the layout. - `name_func`: a `function(t)` closure that returns a string for tag `t`. `name_func` overrides `name`. + - `icon_name`: the "system" name used by Awesome to find the icon. The default value is `machi`. - `persistent`: whether to keep a history of the command for the layout. The default is `true`. - `default_cmd`: the command to use if there is no persistent history for this layout. - `editor`: the editor used for the layout. The default is `machi.default_editor` (or `machi.editor.default_editor`). @@ -59,19 +77,19 @@ The function is compatible with the previous `machi.layout.create(name, editor, ### Starting editor in lua Call `local editor = machi.editor.create()` to create an editor. -To edit the layout `l` on screen `s`, call `editor.start_interactive(s, l)`. -Calling it with no arguments would be the same as `editor.start_interactive(awful.screen.focused(), awful.layout.get(awful.screen.focused()))`. +To edit the current machi layout on screen `s`, call `editor.start_interactive(s)`. +Calling it with no arguments would be the same as `editor.start_interactive(awful.screen.focused())`. ### Basic usage -The editing command starts with the open region of the entire workarea, perform "operations" to split the current region into multiple sub-regions, then recursively edits each of them (by default, the maximum split depth is 2). +The editing command starts with the open area of the entire workarea, perform "operations" to split the current area into multiple sub-areas, then recursively edits each of them (by default, the maximum split depth is 2). The layout is defined by a sequence of operations as a layout command. The layout editor allows users to interactively input their commands and shows the resulting layouts on screen, with the following auxiliary functions: 1. `Up`/`Down`: restore to the history command -2. `Backspace`: undo the last command. +2. `Backspace`: undo the last command. If the command is already empty, restores to the current (maybe transcoded) command of the layout. 3. `Escape`: exit the editor without saving the layout. -4. `Enter`: when all regions are defined, hit enter will save the layout. +4. `Enter`: when all areas are defined, hit enter will save the layout. If shift is hold, only applies the command without saving it to the history. ### Layout command @@ -80,15 +98,24 @@ There are three kinds of operations: 1. Operations taking argument string and parsed as multiple numbers. - `h` horizontally split, `v` vertically split, `w` grid split, `d` draft split + - `h`: horizontally split. Splits to two areas evenly without args. + - `v`: vertically split. Splits to two areas evenly without args. + - `w`: grid split. No splits without args. + - `d`: draft split. No splits without args. -2. Operations taking argument string as a single number. +2. Operations taking argument string as a single number or string. - `s` shift active region, `t` set the maximum split depth, `x` set the nested layout of the current region. + - `s`: shift open areas within the same parent. Shifts one area without args. + - `c`: finish the open areas within the same parent. Finishes all areas with the same parent without args. + - `t`: set the number of further split of the curret area. Sets to the default (2) splits without args. + - `x`: set the nested layout of the current area. Behaves like `-` without args. 3. Operation not taking argument. - `.` finish all regions, `-` finish the current region, `/` remove the current region, `;` no-op + - `.`: finish all areas. + - `-`: finish the current area + - `/`: remove the current area + - `;`: no-op Argument strings are composed of numbers and `,`. If the string contains `,`, it will be used to split argument into multiple numbers. Otherwise, each digit in the string will be treated as a separated number in type 1 ops. @@ -124,9 +151,9 @@ For examples: Details: - - `131h`: horizontally split the initial region (entire desktop) to the ratio of 1:3:1 + - `131h`: horizontally split the initial area (entire desktop) to the ratio of 1:3:1 - For the first `1` part: - - `2v`: vertically split the region to the ratio of 2:1 + - `2v`: vertically split the area to the ratio of 2:1 - `-`: skip the editing of the middle `3` part - For the right `1` part: - `12v`: split the right part vertically to the ratio of 1:2 @@ -229,15 +256,17 @@ Final merge, size 3x1, `w443113132231`: 2 4-4-4 ``` +`d` command works similarly after the inital grid is defined, such as `d1221012210221212121222`. + ### Draft mode __This mode is somewhat usable, yet it may change in the future.__ -Unlike the original machi layout, where a window fits in a single region, draft mode allows window to span across multiple regions. -Each tiled window is associated with a upper-left region (ULR) and a bottom-right region (BRR). -The geometry of the window is from the upper-left corner of the ULR to the bottom-right corner of the BRR. +Unlike the original machi layout, where a window fits in a single area, draft mode allows window to span across multiple areas. +Each tiled window is associated with a upper-left area (UL) and a bottom-right area (BR). +The geometry of the window is from the upper-left corner of the UL to the bottom-right corner of the BR. -This is suppose to work with regions produced with `d` or `w` operation. +This is suppose to work with areas produced with `d` or `w` operation. To enable draft mode in a layout, configure the layout with a command with a leading `d`, for example, `d12210121`, or `dw66`. ### Nested layouts @@ -253,7 +282,7 @@ Known caveats include: __This feature is not available in draft mode.__ To set up nested layouts, you first need to check/modify `machi.editor.nested_layouts` array, which maps an argument string (`[0-9,]+`) to a layout object. -In machi command, use the argument string with command `x` will set up the nested layout of the region to the mapped one. +In machi command, use the argument string with command `x` will set up the nested layout of the area to the mapped one. For example, since by default `machi.editor.nested_layouts["0"]` is `awful.layout.suit.tile` and `machi.editor.nested_layouts["1"]` is `awful.layout.suit.spiral`, the command `11h0x1x` will split the screen horizontally and apply the layouts accordingly - see the figure below. @@ -269,18 +298,19 @@ To change that, please refer to `editor.lua`. (XXX more documents) Calling `machi.switcher.start()` will create a switcher supporting the following keys: - - Arrow keys: move focus into other regions by the direction. - - `Shift` + arrow keys: move the focused window to other regions by the direction. In draft mode, move the window while preserving its size. - - `Control`[ + `Shift`] + arrow keys: move the bottom-right (or top-left window if `Shift` is pressed) region of the focused window by direction. Only works in draft mode. - - `Tab`: switch beteen windows covering the current regions. + - Arrow keys: move focus into other areas by the direction. + - `Shift` + arrow keys: move the focused window to other areas by the direction. In draft mode, move the window while preserving its size. + - `Control`[ + `Shift`] + arrow keys: move the bottom-right (or top-left window if `Shift` is pressed) area of the focused window by direction. Only works in draft mode. + - `Tab`: switch beteen windows covering the current areas. + - `u` or `PageUp` (`Prior`): In non-draft mode, you can select the parent of the current area. + - `/`: In non-draft mode, this opens the editor to edit the selected area using the same command interpretation. + Note the final command may be transcoded to be embeddable, but the areas shall be the same. So far, the key binding is not configurable. One has to modify the source code to change it. ## Caveats -1. layout-machi handles `beautiful.useless_gap` slightly differently. - -2. A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients. +A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients. ## License diff --git a/editor.lua b/editor.lua index bab862f..ec9b717 100644 --- a/editor.lua +++ b/editor.lua @@ -1,15 +1,13 @@ -local api = { - beautiful = require("beautiful"), - wibox = require("wibox"), - awful = require("awful"), - screen = require("awful.screen"), - layout = require("awful.layout"), - naughty = require("naughty"), - gears = require("gears"), - gfs = require("gears.filesystem"), - lgi = require("lgi"), - dpi = require("beautiful.xresources").apply_dpi, -} +local this_package = ... and (...):match("(.-)[^%.]+$") or "" +local machi_engine = require(this_package.."engine") +local beautiful = require("beautiful") +local awful = require("awful") +local wibox = require("wibox") +local naughty = require("naughty") +local gears = require("gears") +local gfs = require("gears.filesystem") +local lgi = require("lgi") +local dpi = require("beautiful.xresources").apply_dpi local ERROR = 2 local WARNING = 1 @@ -19,10 +17,10 @@ local DEBUG = -1 local module = { log_level = WARNING, nested_layouts = { - ["0"] = api.layout.suit.tile, - ["1"] = api.layout.suit.spiral, - ["2"] = api.layout.suit.fair, - ["3"] = api.layout.suit.fair.horizontal, + ["0"] = awful.layout.suit.tile, + ["1"] = awful.layout.suit.spiral, + ["2"] = awful.layout.suit.fair, + ["3"] = awful.layout.suit.fair.horizontal, }, } @@ -35,7 +33,7 @@ end local function with_alpha(col, alpha) local r, g, b _, r, g, b, _ = col:get_rgba() - return api.lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha) + return lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha) end local function max(a, b) @@ -55,90 +53,17 @@ local function set_tiling(c) c.fullscreen = false end -local function parse_arg_string(s, default) - local ret = {} - if #s == 0 then return ret end - local index = 1 - local comma_mode = s:find(",") ~= nil - - local p = index - while index <= #s do - if comma_mode then - if s:sub(index, index) == "," then - local r = tonumber(s:sub(p, index - 1)) - if r == nil then - ret[#ret + 1] = default - else - ret[#ret + 1] = r - end - p = index + 1 - end - else - local r = tonumber(s:sub(index, index)) - if r == nil then - ret[#ret + 1] = default - else - ret[#ret + 1] = r - end - end - index = index + 1 - end - - if comma_mode then - local r = tonumber(s:sub(p, index - 1)) - if r == nil then - ret[#ret + 1] = default - else - ret[#ret + 1] = r - end - p = index + 1 - end - - return ret -end - -local function test_parse_arg_string() - local x = parse_arg_string("12a3", "aha") - assert(#x == 4 and x[1] == 1 and x[2] == 2 and x[3] == "aha" and x[4] == 3) - local x = parse_arg_string("12,a3,4", "aha") - assert(#x == 3 and x[1] == 12 and x[2] == "aha" and x[3] == 4) -end - --- test_parse_arg_string() - -local function fair_split(total, shares, shares_sum) - local ret = {} - local acc = 0 - local acc_ret = 0 - if shares_sum == nil then - shares_sum = 0 - for i = 1, #shares do shares_sum = shares_sum + shares[i] end - end - for i = 1, #shares do - acc = acc + shares[i] - ret[i] = i < #shares and math.floor(total / shares_sum * acc - acc_ret + 0.5) or total - acc_ret - acc_ret = acc_ret + ret[i] - end - return ret -end - -local function min(a, b) - if a < b then return a else return b end -end - -local function max(a, b) - if a < b then return b else return a end -end - local function _area_tostring(wa) return "{x:" .. tostring(wa.x) .. ",y:" .. tostring(wa.y) .. ",w:" .. tostring(wa.width) .. ",h:" .. tostring(wa.height) .. "}" end -local function shrink_area_with_gap(a, inner_gap, outer_gap) - return { x = a.x + (a.bl and outer_gap or inner_gap / 2), y = a.y + (a.bu and outer_gap or inner_gap / 2), - width = a.width - (a.bl and outer_gap or inner_gap / 2) - (a.br and outer_gap or inner_gap / 2), - height = a.height - (a.bu and outer_gap or inner_gap / 2) - (a.bd and outer_gap or inner_gap / 2), - layout = a.layout } +local function shrink_area_with_gap(a, gap) + return { + x = a.x + gap, + y = a.y + gap, + width = a.width - gap * 2, + height = a.height - gap * 2, + } end function module.restore_data(data) @@ -174,512 +99,80 @@ end function module.create(data) if data == nil then data = module.restore_data({ - history_file = api.gfs.get_cache_dir() .. "/history_machi", + history_file = gfs.get_cache_dir() .. "/history_machi", history_save_max = 100, }) end - if data.cmds == nil then - data.cmds = {} - end + data.cmds = data.cmds or {} + data.last_cmd = data.last_cmd or {} + data.minimum_size = data.minimum_size or 100 - if data.last_cmd == nil then - data.last_cmd = {} - end - local init_max_depth = 2 - - local closed_areas - local open_areas - local history - local arg_str - local max_depth - local current_info - local current_cmd - local pending_op - local to_exit - local to_apply - - local function init(init_area, extend) - closed_areas = {} - open_areas = { - { - x = init_area.x - extend, - y = init_area.y - extend, - width = init_area.width + extend * 2, - height = init_area.height + extend * 2, - depth = 0, - group_id = 0, - bl = true, br = true, bu = true, bd = true, - } - } - history = {} - arg_str = "" - max_depth = init_max_depth - current_info = "" - current_cmd = "" - pending_op = nil - to_exit = false - to_apply = false - end - - local function push_history() - if history == nil then return end - history[#history + 1] = {#closed_areas, #open_areas, {}, current_info, current_cmd, pending_op, max_depth, arg_str} - end - - local function discard_history() - if history == nil then return end - table.remove(history, #history) - end - - local function pop_history() - if history == nil or #history == 0 then return end - for i = history[#history][1] + 1, #closed_areas do - table.remove(closed_areas, #closed_areas) - end - - for i = history[#history][2] + 1, #open_areas do - table.remove(open_areas, #open_areas) - end - - for i = 1, #history[#history][3] do - open_areas[history[#history][2] - i + 1] = history[#history][3][i] - end - - current_info = history[#history][4] - current_cmd = history[#history][5] - pending_op = history[#history][6] - max_depth = history[#history][7] - arg_str = history[#history][8] - - table.remove(history, #history) - end - - local function pop_open_area() - local a = open_areas[#open_areas] - table.remove(open_areas, #open_areas) - if history == nil or #history == 0 then return a end - - local idx = history[#history][2] - #open_areas - -- only save when the position has been firstly poped - if idx > #history[#history][3] then - history[#history][3][#history[#history][3] + 1] = a - end - return a - end - - local function push_area() - closed_areas[#closed_areas + 1] = pop_open_area() - end - - local function push_children(c) - for i = #c, 1, -1 do - if c[i].x ~= math.floor(c[i].x) - or c[i].y ~= math.floor(c[i].y) - or c[i].width ~= math.floor(c[i].width) - or c[i].height ~= math.floor(c[i].height) - then - log(WARNING, "splitting yields floating area " .. _area_tostring(c[i])) - end - open_areas[#open_areas + 1] = c[i] - end - end - - local op_count = 0 - - local function handle_op(method) - op_count = op_count + 1 - - local l = method:lower() - local alt = method ~= l - method = l - - log(DEBUG, "op " .. method .. " " .. tostring(alt) .. " " .. arg_str) - if method == "h" or method == "v" then - - local a = pop_open_area() - local args = parse_arg_string(arg_str, 0) - if #args == 0 then - args = {1, 1} - elseif #args == 1 then - args[2] = 1 - end - - local total = 0 - local shares = { } - for i = 1, #args do - local arg - if not alt then - arg = args[i] - else - arg = args[#args - i + 1] - end - if arg < 1 then arg = 1 end - total = total + arg - shares[i] = arg - end - local children = {} - - if method == "h" then - shares = fair_split(a.width, shares, total) - for i = 1, #shares do - local child = { - x = i == 1 and a.x or children[#children].x + children[#children].width, - y = a.y, - width = shares[i], - height = a.height, - depth = a.depth + 1, - group_id = op_count, - bl = i == 1 and a.bl or false, - br = i == #shares and a.br or false, - bu = a.bu, - bd = a.bd, - } - children[#children + 1] = child - end - else - shares = fair_split(a.height, shares, total) - for i = 1, #shares do - local child = { - x = a.x, - y = i == 1 and a.y or children[#children].y + children[#children].height, - width = a.width, - height = shares[i], - depth = a.depth + 1, - group_id = op_count, - bl = a.bl, - br = a.br, - bu = i == 1 and a.bu or false, - bd = i == #shares and a.bd or false, - } - children[#children + 1] = child - end - end - - push_children(children) - - elseif method == "w" then - - local a = pop_open_area() - local args = parse_arg_string(arg_str, 0) - if #args == 0 then - args = {1, 1} - elseif #args == 1 then - args[2] = 1 - end - - local h_split, v_split - if alt then - h_split = args[2] - v_split = args[1] - else - h_split = args[1] - v_split = args[2] - end - if h_split < 1 then h_split = 1 end - if v_split < 1 then v_split = 1 end - - local x_shares = {} - local y_shares = {} - for i = 1, h_split do x_shares[i] = 1 end - for i = 1, v_split do y_shares[i] = 1 end - - x_shares = fair_split(a.width, x_shares, h_split) - y_shares = fair_split(a.height, y_shares, v_split) - - local children = {} - for y_index = 1, v_split do - for x_index = 1, h_split do - local r = { - x = x_index == 1 and a.x or children[#children].x + children[#children].width, - y = y_index == 1 and a.y or (x_index == 1 and children[#children].y + children[#children].height or children[#children].y), - width = x_shares[x_index], - height = y_shares[y_index], - depth = a.depth + 1, - group_id = op_count, - } - if x_index == 1 then r.bl = a.bl else r.bl = false end - if x_index == h_split then r.br = a.br else r.br = false end - if y_index == 1 then r.bu = a.bu else r.bu = false end - if y_index == v_split then r.bd = a.bd else r.bd = false end - children[#children + 1] = r - end - end - - local merged_children = {} - local start_index = 1 - for i = 3, #args - 1, 2 do - -- find the first index that is not merged - while start_index <= #children and children[start_index] == false do - start_index = start_index + 1 - end - if start_index > #children or children[start_index] == false then - break - end - local x = (start_index - 1) % h_split - local y = math.floor((start_index - 1) / h_split) - local w = args[i] - local h = args[i + 1] - if w < 1 then w = 1 end - if h == nil or h < 1 then h = 1 end - if alt then - local tmp = w - w = h - h = tmp - end - if x + w > h_split then w = h_split - x end - if y + h > v_split then h = v_split - y end - local end_index = start_index - for ty = y, y + h - 1 do - local succ = true - for tx = x, x + w - 1 do - if children[ty * h_split + tx + 1] == false then - succ = false - break - elseif ty == y then - end_index = ty * h_split + tx + 1 - end - end - - if not succ then - break - elseif ty > y then - end_index = ty * h_split + x + w - end - end - - local r = { - x = children[start_index].x, y = children[start_index].y, - width = children[end_index].x + children[end_index].width - children[start_index].x, - height = children[end_index].y + children[end_index].height - children[start_index].y, - bu = children[start_index].bu, bl = children[start_index].bl, - bd = children[end_index].bd, br = children[end_index].br, - depth = a.depth + 1, - group_id = op_count - } - merged_children[#merged_children + 1] = r - - for ty = y, y + h - 1 do - local succ = true - for tx = x, x + w - 1 do - local index = ty * h_split + tx + 1 - if index <= end_index then - children[index] = false - else - break - end - end - end - end - - -- clean up children, remove all `false' - local j = 1 - for i = 1, #children do - if children[i] ~= false then - children[j] = children[i] + local function add_cmd(instance_name, cmd) + -- remove duplicated entries + local j = 1 + for i = 1, #data.cmds do + if data.cmds[i] ~= cmd then + data.cmds[j] = data.cmds[i] j = j + 1 - end - end - for i = #children, j, -1 do - table.remove(children, i) - end + end + end + for i = #data.cmds, j, -1 do + table.remove(data.cmds, i) + end - push_children(merged_children) - push_children(children) + data.cmds[#data.cmds + 1] = cmd + data.last_cmd[instance_name] = cmd + if data.history_file then + local file, err = io.open(data.history_file, "w") + if err then + log(ERROR, "cannot save history to " .. data.history_file) + else + for i = max(1, #data.cmds - data.history_save_max + 1), #data.cmds do + log(DEBUG, "save cmd " .. data.cmds[i]) + file:write(data.cmds[i] .. "\n") + end + for name, cmd in pairs(data.last_cmd) do + log(DEBUG, "save last cmd " .. cmd .. " for " .. name) + file:write("+" .. name .. "\n" .. cmd .. "\n") + end + end + file:close() + end - elseif method == "d" then - - local a = pop_open_area() - local shares = parse_arg_string(arg_str, 0) - local x_shares = {} - local y_shares = {} - - local current = x_shares - for i = 1, #shares do - if not alt then - arg = shares[i] - else - arg = shares[#shares - i + 1] - end - if arg < 1 then - if current == x_shares then current = y_shares else break end - else - current[#current + 1] = arg - end - end - - if #x_shares == 0 then - open_areas[#open_areas + 1] = a - return - elseif #y_shares == 0 then - y_shares = {1} - end - - x_shares = fair_split(a.width, x_shares) - y_shares = fair_split(a.height, y_shares) - - local children = {} - for y_index = 1, #y_shares do - for x_index = 1, #x_shares do - local r = { - x = x_index == 1 and a.x or children[#children].x + children[#children].width, - y = y_index == 1 and a.y or (x_index == 1 and children[#children].y + children[#children].height or children[#children].y), - width = x_shares[x_index], - height = y_shares[y_index], - depth = a.depth + 1, - group_id = op_count, - } - if x_index == 1 then r.bl = a.bl else r.bl = false end - if x_index == #x_shares then r.br = a.br else r.br = false end - if y_index == 1 then r.bu = a.bu else r.bu = false end - if y_index == #y_shares then r.bd = a.bd else r.bd = false end - children[#children + 1] = r - end - end - - push_children(children) - - elseif method == "s" then - - if #open_areas > 0 then - local times = arg_str == "" and 1 or tonumber(arg_str) - local t = {} - while #open_areas > 0 do - t[#t + 1] = pop_open_area() - end - for i = #t, 1, -1 do - open_areas[#open_areas + 1] = t[(i + times - 1) % #t + 1] - end - end - - elseif method == "t" then - - local n = tonumber(arg_str) - if n ~= nil then - max_depth = n - end - - elseif method == "x" then - - push_area() - closed_areas[#closed_areas].layout = module.nested_layouts[arg_str] - - elseif method == "-" then - - push_area() - - elseif method == "." then - - while #open_areas > 0 do - push_area() - end - - elseif method == "/" then - - pop_open_area() - - elseif method == ";" then - - -- nothing - - end - - while #open_areas > 0 and open_areas[#open_areas].depth >= max_depth do - push_area() - end - - arg_str = "" + return true end - local key_translate_tab = { - ["Return"] = ".", - [" "] = "-", - } - -- 3 for taking the arg string and an open area - -- 2 for taking an open area - -- 1 for taking nothing - -- 0 for args - local ch_info = { - ["h"] = 3, ["H"] = 3, - ["v"] = 3, ["V"] = 3, - ["w"] = 3, ["W"] = 3, - ["d"] = 3, ["D"] = 3, - ["s"] = 3, ["S"] = 3, - ["t"] = 3, ["T"] = 3, - ["x"] = 3, - ["-"] = 2, - ["/"] = 2, - ["."] = 1, - [";"] = 1, - ["0"] = 0, ["1"] = 0, ["2"] = 0, ["3"] = 0, ["4"] = 0, - ["5"] = 0, ["6"] = 0, ["7"] = 0, ["8"] = 0, ["9"] = 0, - [","] = 0, - } + local function start_interactive(screen, embed_args) + local label_font_family = beautiful.get_font( + beautiful.font):get_family() + local label_size = dpi(30) + local info_size = dpi(60) + -- colors are in rgba + local border_color = with_alpha(api.gears.color( + api.beautiful.machi_editor_border_color or api.beautiful.border_focus), + api.beautiful.machi_editor_border_opacity or 0.75) + local active_color = with_alpha(api.gears.color( + api.beautiful.machi_editor_active_color or api.beautiful.bg_focus), + api.beautiful.machi_editor_active_opacity or 0.5) + local open_color = with_alpha(api.gears.color( + api.beautiful.machi_editor_open_color or api.beautiful.bg_normal), + api.beautiful.machi_editor_open_opacity or 0.5) + local closed_color = open_color + + if to_save == nil then + to_save = true + end - local function handle_ch(key) - if key_translate_tab[key] ~= nil then - key = key_translate_tab[key] - end - local t = ch_info[key] - if t == nil then - return nil - elseif t == 3 then - if pending_op ~= nil then - handle_op(pending_op) - pending_op = nil - end - if #open_areas == 0 then return nil end - if arg_str == "" then - pending_op = key - else - handle_op(key) - end - elseif t == 2 or t == 1 then - if pending_op ~= nil then - handle_op(pending_op) - pending_op = nil - end - if #open_areas == 0 and t == 2 then return nil end - handle_op(key) - elseif t == 0 then - arg_str = arg_str .. key - end - - return key - end - - local function set_gap(inner_gap, outer_gap) - data.inner_gap = inner_gap - data.outer_gap = outer_gap - end - - local function start_interactive(screen, layout) - local outer_gap = data.outer_gap or data.gap or api.beautiful.useless_gap * 2 or 0 - local inner_gap = data.inner_gap or data.gap or api.beautiful.useless_gap * 2 or 0 - local label_font_family = api.beautiful.get_font( - api.beautiful.font):get_family() - local label_size = api.dpi(30) - local info_size = api.dpi(60) - -- colors are in rgba - local border_color = with_alpha(api.gears.color( - api.beautiful.machi_editor_border_color or api.beautiful.border_focus), - api.beautiful.machi_editor_border_opacity or 0.75) - local active_color = with_alpha(api.gears.color( - api.beautiful.machi_editor_active_color or api.beautiful.bg_focus), - api.beautiful.machi_editor_active_opacity or 0.5) - local open_color = with_alpha(api.gears.color( - api.beautiful.machi_editor_open_color or api.beautiful.bg_normal), - api.beautiful.machi_editor_open_opacity or 0.5) - local closed_color = open_color - - screen = screen or api.screen.focused() - layout = layout or api.layout.get(screen) - local tag = screen.selected_tag + screen = screen or awful.screen.focused() + local tag = screen.selected_tag + local gap = tag.gap or 0 + local layout = tag.layout if layout.machi_set_cmd == nil then - api.naughty.notify({ + naughty.notify({ text = "The layout to edit is not machi", timeout = 3, }) @@ -693,7 +186,7 @@ function module.create(data) local start_y = screen.workarea.y local kg - local infobox = api.wibox({ + local infobox = wibox({ screen = screen, x = screen.workarea.x, y = screen.workarea.y, @@ -706,6 +199,56 @@ function module.create(data) }) infobox.visible = true + workarea = embed_args and embed_args.workarea or screen.workarea + + local closed_areas + local open_areas + local pending_op + local current_cmd + local to_exit + local to_apply + + local key_translate_tab = { + ["Return"] = ".", + [" "] = "-", + } + + local function set_cmd(cmd) + local new_closed_areas, new_open_areas, new_pending_op = machi_engine.areas_from_command( + cmd, + { + x = workarea.x + gap, + y = workarea.y + gap, + width = workarea.width - gap * 2, + height = workarea.height - gap * 2 + }, + gap * 2 + data.minimum_size) + if new_closed_areas then + closed_areas, open_areas, pending_op = + new_closed_areas, new_open_areas, new_pending_op + current_cmd = cmd + + if embed_args then + current_info = + embed_args.cmd_prefix.."["..current_cmd.."]"..embed_args.cmd_suffix + else + current_info = cmd + end + return true + else + return false + end + end + + local function handle_key(key) + if key_translate_tab[key] ~= nil then + key = key_translate_tab[key] + end + + return set_cmd(current_cmd..key) + end + + local function cleanup() infobox.visible = false end @@ -718,56 +261,60 @@ function module.create(data) local msg, ext for i, a in ipairs(closed_areas) do - local sa = shrink_area_with_gap(a, inner_gap, inner_gap / 2) - local to_highlight = false - if pending_op ~= nil then - to_highlight = a.group_id == op_count + if not a.inhabitable then + local sa = shrink_area_with_gap(a, gap) + local to_highlight = false + if pending_op ~= nil then + to_highlight = a.group_id == op_count + end + cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) + cr:clip() + if to_highlight then + cr:set_source(active_color) + else + cr:set_source(closed_color) + end + cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) + cr:fill() + cr:set_source(border_color) + cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) + cr:set_line_width(10.0) + cr:stroke() + cr:reset_clip() end - cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) - cr:clip() - if to_highlight then - cr:set_source(active_color) - else - cr:set_source(closed_color) - end - cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) - cr:fill() - cr:set_source(border_color) - cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) - cr:set_line_width(10.0) - cr:stroke() - cr:reset_clip() end for i, a in ipairs(open_areas) do - local sa = shrink_area_with_gap(a, inner_gap, inner_gap / 2) - local to_highlight = false - if pending_op == nil then - to_highlight = i == #open_areas - else - to_highlight = a.group_id == op_count - end - cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) - cr:clip() - if i == #open_areas then - cr:set_source(active_color) - else - cr:set_source(open_color) - end - cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) - cr:fill() + if not a.inhabitable then + local sa = shrink_area_with_gap(a, gap) + local to_highlight = false + if not pending_op then + to_highlight = i == #open_areas + else + to_highlight = a.group_id == op_count + end + cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) + cr:clip() + if i == #open_areas then + cr:set_source(active_color) + else + cr:set_source(open_color) + end + cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) + cr:fill() - cr:set_source(border_color) - cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) - cr:set_line_width(10.0) - if to_highlight then - cr:stroke() - else - cr:set_dash({5, 5}, 0) - cr:stroke() - cr:set_dash({}, 0) + cr:set_source(border_color) + cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) + cr:set_line_width(10.0) + if to_highlight then + cr:stroke() + else + cr:set_dash({5, 5}, 0) + cr:stroke() + cr:set_dash({}, 0) + end + cr:reset_clip() end - cr:reset_clip() end cr:select_font_face(label_font_family, "normal", "normal") @@ -798,12 +345,22 @@ function module.create(data) infobox.bgimage = draw_info end + local function get_final_cmd() + local final_cmd = current_cmd + if embed_args then + final_cmd = embed_args.cmd_prefix .. + machi_engine.areas_to_command(closed_areas, true) .. + embed_args.cmd_suffix + end + return final_cmd + end + log(DEBUG, "interactive layout editing starts") - init(screen.workarea, inner_gap / 2 - outer_gap) + set_cmd("") refresh() - kg = api.awful.keygrabber.run( + kg = awful.keygrabber.run( function (mod, key, event) if event == "release" then return @@ -811,12 +368,13 @@ function module.create(data) local ok, err = pcall( function () - if pending_op ~= nil then - pop_history() - end - if key == "BackSpace" then - pop_history() + if #current_cmd > 0 then + set_cmd(current_cmd:sub(1, #current_cmd - 1)) + elseif embed_args == nil then + local areas = layout.machi_get_areas(screen, tag) + set_cmd(machi_engine.areas_to_command(areas)) + end elseif key == "Escape" then table.remove(data.cmds, #data.cmds) to_exit = true @@ -832,73 +390,27 @@ function module.create(data) end log(DEBUG, "restore history #" .. tostring(cmd_index) .. ":" .. data.cmds[cmd_index]) - init(screen.workarea, inner_gap / 2 - outer_gap) - for i = 1, #data.cmds[cmd_index] do - local cmd = data.cmds[cmd_index]:sub(i, i) - - push_history() - local ret = handle_ch(cmd) - - if ret == nil then - log(WARNING, "ret is nil") - else - current_info = current_info .. ret - current_cmd = current_cmd .. ret - end - end - - if #open_areas == 0 then + if set_cmd(data.cmds[cmd_index]) and #open_areas == 0 then current_info = current_info .. " (enter to save)" end - elseif #open_areas > 0 then - push_history() - local ret = handle_ch(key) - if ret ~= nil then - current_info = current_info .. ret - current_cmd = current_cmd .. ret - else - pop_history() - end - - if #open_areas == 0 then + elseif #open_areas > 0 or pending_op then + if handle_key(key) and #open_areas == 0 and not pending_op then current_info = current_info .. " (enter to apply)" end else if key == "Return" then - table.remove(data.cmds, #data.cmds) - -- remove duplicated entries - local j = 1 - for i = 1, #data.cmds do - if data.cmds[i] ~= current_cmd then - data.cmds[j] = data.cmds[i] - j = j + 1 - end + local alt = false + for _, m in ipairs(mod) do + if m == "Shift" then + alt = true + break + end end - for i = #data.cmds, j, -1 do - table.remove(data.cmds, i) - end - -- bring the current cmd to the front - data.cmds[#data.cmds + 1] = current_cmd local instance_name, persistent = layout.machi_get_instance_info(tag) - if persistent then - data.last_cmd[instance_name] = current_cmd - if data.history_file then - local file, err = io.open(data.history_file, "w") - if err then - log(ERROR, "cannot save history to " .. data.history_file) - else - for i = max(1, #data.cmds - data.history_save_max + 1), #data.cmds do - log(DEBUG, "save cmd " .. data.cmds[i]) - file:write(data.cmds[i] .. "\n") - end - for name, cmd in pairs(data.last_cmd) do - log(DEBUG, "save last cmd " .. cmd .. " for " .. name) - file:write("+" .. name .. "\n" .. cmd .. "\n") - end - end - file:close() - end + if not alt and persistent then + table.remove(data.cmds, #data.cmds) + add_cmd(instance_name, get_final_cmd()) current_info = "Saved!" else current_info = "Applied!" @@ -909,19 +421,14 @@ function module.create(data) end end - if not to_exit and pending_op ~= nil then - push_history() - handle_op(pending_op) - end - refresh() if to_exit then log(DEBUG, "interactive layout editing ends") if to_apply then - layout.machi_set_cmd(current_cmd, tag) - api.layout.arrange(screen) - api.gears.timer{ + layout.machi_set_cmd(get_final_cmd(), tag) + awful.layout.arrange(screen) + gears.timer{ timeout = 1, autostart = true, singleshot = true, @@ -940,50 +447,108 @@ function module.create(data) end if to_exit then - api.awful.keygrabber.stop(kg) + awful.keygrabber.stop(kg) end end ) end - local function run_cmd(init_area, cmd) - local outer_gap = data.outer_gap or data.gap or api.beautiful.useless_gap * 2 or 0 - local inner_gap = data.inner_gap or data.gap or api.beautiful.useless_gap * 2 or 0 - init(init_area, inner_gap / 2 - outer_gap) - - for i = 1, #cmd do - handle_ch(cmd:sub(i, i)) - end - - local areas_with_gap = {} - for _, a in ipairs(closed_areas) do - areas_with_gap[#areas_with_gap + 1] = shrink_area_with_gap(a, inner_gap, inner_gap / 2) - end - table.sort( - areas_with_gap, - function (a1, a2) - local s1 = a1.width * a1.height - local s2 = a2.width * a2.height - if math.abs(s1 - s2) < 0.01 then - return (a1.x + a1.y) < (a2.x + a2.y) - else - return s1 > s2 - end - end - ) - - return areas_with_gap + local function run_cmd(cmd, screen, tag) + local gap = tag.gap + local areas = machi_engine.areas_from_command( + cmd, + { + x = screen.workarea.x + gap, + y = screen.workarea.y + gap, + width = screen.workarea.width - gap * 2, + height = screen.workarea.height - gap * 2 + }, + gap * 2 + data.minimum_size) + if not areas then + return nil + end + for _, a in ipairs(areas) do + a.x = a.x + gap + a.y = a.y + gap + a.width = a.width - gap * 2 + a.height = a.height - gap * 2 + end + return areas end local function get_last_cmd(name) return data.last_cmd[name] end + function adjust_shares(c, axis, adj) + if not c:isvisible() or c.floating or c.immobilized or + not c.machi or not c.machi.area then + return + end + local screen = c.screen + local tag = screen.selected_tag + local layout = tag.layout + if not layout.machi_get_areas then return end + local areas = layout.machi_get_areas(screen, tag) + local key_shares = axis.."_shares" + local key_spare = axis.."_spare" + local key_parent_shares = "parent_"..axis.."_shares" + + if adj < 0 then + if axis == "x" and c.width + adj < data.minimum_size then + adj = data.minimum_size - c.width + elseif axis == "y" and c.height + adj < data.minimum_size then + adj = data.minimum_size - c.height + end + end + + local function adjust(parent_id, shares, adj) + -- The propagation part is questionable. But it is not critical anyway.. + if type(shares) ~= "table" then + local old = areas[parent_id].split[key_shares][shares][2] or 0 + areas[parent_id].split[key_shares][shares][2] = old + adj + else + local acc = 0 + for i = 1, #shares do + local old = areas[parent_id].split[key_shares][shares[i]][2] or 0 + local adj_split = i == #shares and adj - acc or math.floor(adj * i / #shares - acc + 0.5) + areas[parent_id].split[key_shares][shares[i]][2] = old + adj_split + acc = acc + adj_split + end + end + if adj <= 0 then + return #areas[parent_id].split[key_shares] > 1 + else + return areas[parent_id].split[key_spare] >= adj + end + end + + local area = c.machi.area + while areas[area].parent_id do + if adjust(areas[area].parent_id, areas[area][key_parent_shares], adj) then + break + end + area = areas[area].parent_id + end + + layout.machi_set_cmd(machi_engine.areas_to_command(areas), tag) + awful.layout.arrange(screen) + end + + function adjust_x_shares(c, adj) + adjust_shares(c, "x", adj) + end + + function adjust_y_shares(c, adj) + adjust_shares(c, "y", adj) + end + return { start_interactive = start_interactive, run_cmd = run_cmd, get_last_cmd = get_last_cmd, - set_gap = set_gap, + adjust_x_shares = adjust_x_shares, + adjust_y_shares = adjust_y_shares, } end diff --git a/engine.lua b/engine.lua new file mode 100644 index 0000000..d07919a --- /dev/null +++ b/engine.lua @@ -0,0 +1,937 @@ +-- area { +-- x, y, width, height +-- parent_id +-- parent_cid +-- parent_x_shares +-- parent_y_shares +-- inhabitable +-- hole (unique) +-- draft_mode (root only) +-- } +-- +-- split { +-- method +-- x_shares +-- y_shares +-- children +-- } +-- +-- share {weight, adjustment, dynamic, minimum} +local in_module = ... + +-- Split a length by `measures`, such that each split respect the +-- weight [1], adjustment (user [2] + engine [3]) without breaking the minimum size [4]. +-- +-- The split algorithm has a worst case of O(n^2) where n = #shares, +-- which should be fine for practical usage of screen partitions. +-- Using geometric algorithm this can be optimized to O(n log n), but +-- I don't think it is worth. + +-- Returns two values: +-- 1. the (accumulative) result if it is possible to give every share its minimum size, otherwise nil. +-- 2. any spare space to adjust without capping any share. +local function fair_split(length, shares) + local ret = {} + local normalized_adj = nil + local sum_weight + local sum_adj + local remaining = #shares + local spare = nil + repeat + local need_recompute = false + + sum_weight = 0 + sum_adj = 0 + for i = 1, #shares do + if ret[i] == nil then + sum_weight = sum_weight + shares[i][1] + if normalized_adj then + sum_adj = sum_adj + normalized_adj[i] + end + end + end + + if normalized_adj == nil then + normalized_adj = {} + for i = 1, #shares do + if sum_weight > shares[i][1] then + normalized_adj[i] = ((shares[i][2] or 0) + (shares[i][3] or 0)) * sum_weight / (sum_weight - shares[i][1]) + else + normalized_adj[i] = 0 + end + sum_adj = sum_adj + normalized_adj[i] + end + + for i = 1, #shares do + local required = (shares[i][4] - normalized_adj[i]) * sum_weight / shares[i][1] + sum_adj + if spare == nil or spare > length - required then + spare = length - required + end + end + end + + local capped_length = 0 + for i = 1, #shares do + if ret[i] == nil then + local split = (length - sum_adj) * shares[i][1] / sum_weight + normalized_adj[i] + if split < shares[i][4] then + ret[i] = shares[i][4] + capped_length = capped_length + shares[i][4] + need_recompute = true + end + end + end + + length = length - capped_length + until not need_recompute + + if #shares == 1 or spare < 0 then + spare = 0 + end + + if remaining == 0 then + return nil, spare + end + + local acc_weight = 0 + local acc_adj = 0 + local acc_ret = 0 + for i = 1, #shares do + if ret[i] == nil then + acc_weight = acc_weight + shares[i][1] + acc_adj = acc_adj + normalized_adj[i] + ret[i] = remaining == 1 and length - acc_ret or math.floor((length - sum_adj) / sum_weight * acc_weight + acc_adj - acc_ret + 0.5) + acc_ret = acc_ret + ret[i] + remaining = remaining - 1 + end + end + + ret[0] = 0 + for i = 1, #shares do + ret[i] = ret[i - 1] + ret[i] + end + + return ret, spare +end + +-- Static data + +-- Command character info +-- 3 for taking the arg string and an open area +-- 2 for taking an open area +-- 1 for taking nothing +-- 0 for args +local ch_info = { + ["h"] = 3, ["H"] = 3, + ["v"] = 3, ["V"] = 3, + ["w"] = 3, ["W"] = 3, + ["d"] = 3, ["D"] = 3, + ["s"] = 3, + ["t"] = 3, + ["c"] = 3, + ["x"] = 3, + ["-"] = 2, + ["/"] = 2, + ["."] = 1, + [";"] = 1, + ["0"] = 0, ["1"] = 0, ["2"] = 0, ["3"] = 0, ["4"] = 0, + ["5"] = 0, ["6"] = 0, ["7"] = 0, ["8"] = 0, ["9"] = 0, + ["_"] = 0, [","] = 0, +} + +local function parse_arg_str(arg_str, default) + local ret = {} + local current = {} + if #arg_str == 0 then return ret end + local index = 1 + local split_mode = arg_str:find("[,_]") ~= nil + + local p = index + while index <= #arg_str do + local ch = arg_str:sub(index, index) + if split_mode then + if ch == "_" then + local r = tonumber(arg_str:sub(p, index - 1)) + if r == nil then + current[#current + 1] = default + else + current[#current + 1] = r + end + p = index + 1 + elseif ch == "," then + local r = tonumber(arg_str:sub(p, index - 1)) + if r == nil then + current[#current + 1] = default + else + current[#current + 1] = r + end + ret[#ret + 1] = current + current = {} + p = index + 1 + end + else + local r = tonumber(ch) + if r == nil then + ret[#ret + 1] = {default} + else + ret[#ret + 1] = {r} + end + end + index = index + 1 + end + + if split_mode then + local r = tonumber(arg_str:sub(p, index - 1)) + if r == nil then + current[#current + 1] = default + else + current[#current + 1] = r + end + ret[#ret + 1] = current + end + + return ret +end + +if not in_module then + print("Testing parse_arg_str") + local x = parse_arg_str("1234", 0) + assert(#x == 4) + assert(#x[1] == 1 and x[1][1] == 1) + assert(#x[2] == 1 and x[2][1] == 2) + assert(#x[3] == 1 and x[3][1] == 3) + assert(#x[4] == 1 and x[4][1] == 4) + local x = parse_arg_str("12_34_,", -1) + assert(#x == 2) + assert(#x[1] == 3 and x[1][1] == 12 and x[1][2] == 34 and x[1][3] == -1) + assert(#x[2] == 1 and x[2][1] == -1) + local x = parse_arg_str("12_34,56_,78_90_", -1) + assert(#x == 3) + assert(#x[1] == 2 and x[1][1] == 12 and x[1][2] == 34) + assert(#x[2] == 2 and x[2][1] == 56 and x[2][2] == -1) + assert(#x[3] == 3 and x[3][1] == 78 and x[3][2] == 90 and x[3][3] == -1) + print("Passed.") +end + +local max_split = 1000 +local max_areas = 10000 +local default_expansion = 2 + +-- Execute a (partial) command, returns: +-- 1. Closed areas: areas that will not be further partitioned by further input. +-- 2. Open areas: areas that can be further partitioned. +-- 3. Pending: if the command can take more argument into the last command. +local function areas_from_command(command, workarea, minimum) + local pending_op = nil + local arg_str = "" + local closed_areas = {} + local open_areas + local root = { + expansion = default_expansion, + x = workarea.x, + y = workarea.y, + width = workarea.width, + height = workarea.height, + bl = true, + br = true, + bu = true, + bd = true, + } + + local function close_area() + local a = open_areas[#open_areas] + table.remove(open_areas, #open_areas) + local i = #closed_areas + 1 + closed_areas[i] = a + a.id = i + return a, i + end + + local function push_open_areas(areas) + for i = #areas, 1, -1 do + open_areas[#open_areas + 1] = areas[i] + end + end + + local function handle_op(method) + local l = method:lower() + local alt = method ~= l + method = l + + if method == "h" or method == "v" then + + local args = parse_arg_str(arg_str, 0) + if #args == 0 then + args = {{1}, {1}} + elseif #args == 1 then + args[2] = {1} + end + + local total = 0 + local shares = { } + for i = 1, #args do + local arg + if not alt then + arg = args[i] + else + arg = args[#args - i + 1] + end + if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end + shares[i] = arg + end + + if #shares > max_split then + return nil + end + + local a, area_index = close_area() + a.inhabitable = true + a.split = { + method = method, + x_shares = method == "h" and shares or {{1}}, + y_shares = method == "v" and shares or {{1}}, + children = {} + } + local children = a.split.children + + if method == "h" then + for i = 1, #a.split.x_shares do + local child = { + parent_id = area_index, + parent_cid = #children + 1, + parent_x_shares = #children + 1, + parent_y_shares = 1, + expansion = a.expansion - 1, + + bl = i == 1 and a.bl or false, + br = i == #a.split.x_shares and a.br or false, + bu = a.bu, + bd = a.bd, + } + children[#children + 1] = child + end + else + for i = 1, #a.split.y_shares do + local child = { + parent_id = area_index, + parent_cid = #children + 1, + parent_x_shares = 1, + parent_y_shares = #children + 1, + expansion = a.expansion - 1, + + bl = a.bl, + br = a.br, + bu = i == 1 and a.bu or false, + bd = i == #a.split.y_shares and a.bd or false, + } + children[#children + 1] = child + end + end + + push_open_areas(children) + + elseif method == "w" or method == "d" then + + local args = parse_arg_str(arg_str, 0) + + local x_shares = {} + local y_shares = {} + local m_start = #args + 1 + + if method == "w" then + if #args == 0 then + args = {{1}, {1}} + elseif #args == 1 then + args[2] = {1} + end + + local x_shares_count, y_shares_count + if alt then + x_shares_count = args[2][1] + y_shares_count = args[1][1] + else + x_shares_count = args[1][1] + y_shares_count = args[2][1] + end + if x_shares_count < 1 then x_shares_count = 1 end + if y_shares_count < 1 then y_shares_count = 1 end + + if x_shares_count * y_shares_count > max_split then + return nil + end + + for i = 1, x_shares_count do x_shares[i] = {1} end + for i = 1, y_shares_count do y_shares[i] = {1} end + + m_start = 3 + else + local current = x_shares + for i = 1, #args do + if not alt then + arg = args[i] + else + arg = args[#args - i + 1] + end + if arg[1] == 0 then + if current == x_shares then current = y_shares else + m_start = i + 1 + break + end + else + if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end + current[#current + 1] = arg + end + end + + if #x_shares == 0 then + x_shares = {{1}} + end + + if #y_shares == 0 then + y_shares = {{1}} + end + + if #x_shares * #y_shares > max_split then + return nil + end + end + + local a, area_index = close_area() + a.inhabitable = true + a.split = { + method = method, + x_shares = x_shares, + y_shares = y_shares, + children = {}, + } + local children = {} + + for y_index = 1, #a.split.y_shares do + for x_index = 1, #a.split.x_shares do + local r = { + parent_id = area_index, + -- parent_cid will be filled later. + parent_x_shares = x_index, + parent_y_shares = y_index, + expansion = a.expansion - 1 + } + if x_index == 1 then r.bl = a.bl else r.bl = false end + if x_index == #a.split.x_shares then r.br = a.br else r.br = false end + if y_index == 1 then r.bu = a.bu else r.bu = false end + if y_index == #a.split.y_shares then r.bd = a.bd else r.bd = false end + children[#children + 1] = r + end + end + + local merged_children = {} + local start_index = 1 + for i = m_start, #args - 1, 2 do + -- find the first index that is not merged + while start_index <= #children and children[start_index] == false do + start_index = start_index + 1 + end + if start_index > #children or children[start_index] == false then + break + end + local x = (start_index - 1) % #x_shares + local y = math.floor((start_index - 1) / #x_shares) + local w = args[i][1] + local h = args[i + 1][1] + if w < 1 then w = 1 end + if h == nil or h < 1 then h = 1 end + if alt then + local tmp = w + w = h + h = tmp + end + if x + w > #x_shares then w = #x_shares - x end + if y + h > #y_shares then h = #y_shares - y end + local end_index = start_index + for ty = y, y + h - 1 do + local succ = true + for tx = x, x + w - 1 do + if children[ty * #x_shares + tx + 1] == false then + succ = false + break + elseif ty == y then + end_index = ty * #x_shares + tx + 1 + end + end + + if not succ then + break + elseif ty > y then + end_index = ty * #x_shares + x + w + end + end + + local function generate_range(s, e) + local r = {} for i = s, e do r[#r+1] = i end return r + end + + local r = { + bu = children[start_index].bu, bl = children[start_index].bl, + bd = children[end_index].bd, br = children[end_index].br, + + parent_id = area_index, + -- parent_cid will be filled later. + parent_x_shares = generate_range(children[start_index].parent_x_shares, children[end_index].parent_x_shares), + parent_y_shares = generate_range(children[start_index].parent_y_shares, children[end_index].parent_y_shares), + expansion = a.expansion - 1 + } + merged_children[#merged_children + 1] = r + + for ty = y, y + h - 1 do + local succ = true + for tx = x, x + w - 1 do + local index = ty * #x_shares + tx + 1 + if index <= end_index then + children[index] = false + else + break + end + end + end + end + + for i = 1, #merged_children do + a.split.children[#a.split.children + 1] = merged_children[i] + a.split.children[#a.split.children].parent_cid = #a.split.children + end + + -- clean up children, remove all `false' + for i = 1, #children do + if children[i] ~= false then + a.split.children[#a.split.children + 1] = children[i] + a.split.children[#a.split.children].parent_cid = #a.split.children + end + end + + push_open_areas(a.split.children) + + elseif method == "s" then + + if #open_areas > 0 then + local times = arg_str == "" and 1 or tonumber(arg_str) + local t = {} + local c = #open_areas + local p = open_areas[c].parent_id + while c > 0 and open_areas[c].parent_id == p do + t[#t + 1] = open_areas[c] + open_areas[c] = nil + c = c - 1 + end + for i = #t, 1, -1 do + open_areas[c + 1] = t[(i + times - 1) % #t + 1] + c = c + 1 + end + end + + elseif method == "t" then + + if #open_areas > 0 then + open_areas[#open_areas].expansion = tonumber(arg_str) or default_expansion + end + + elseif method == "x" then + + local a = close_area() + a.layout = arg_str + + elseif method == "-" then + + close_area() + + elseif method == "." then + + while #open_areas > 0 do + close_area() + end + + elseif method == "c" then + + local limit = tonumber(arg_str) + if limit == nil or limit > #open_areas then + limit = #open_areas + end + local p = open_areas[#open_areas].parent_id + while limit > 0 and open_areas[#open_areas].parent_id == p do + close_area() + limit = limit - 1 + end + + elseif method == "/" then + + close_area().inhabitable = true + + elseif method == ";" then + + -- nothing + + end + + if #open_areas + #closed_areas > max_areas then + return nil + end + + while #open_areas > 0 and open_areas[#open_areas].expansion <= 0 do + close_area() + end + + arg_str = "" + return true + end + + open_areas = {root} + + for i = 1, #command do + local ch = command:sub(i, i) + local t = ch_info[ch] + local r = true + if t == nil then + return nil + elseif t == 3 then + if pending_op ~= nil then + r = handle_op(pending_op) + pending_op = nil + end + if #open_areas == 0 then return nil end + if arg_str == "" then + pending_op = ch + else + r = handle_op(ch) + end + elseif t == 2 or t == 1 then + if pending_op ~= nil then + handle_op(pending_op) + pending_op = nil + end + if #open_areas == 0 and t == 2 then return nil end + r = handle_op(ch) + elseif t == 0 then + arg_str = arg_str..ch + end + + if not r then return nil end + end + + if pending_op ~= nil then + if not handle_op(pending_op) then + return nil + end + end + + if #closed_areas == 0 then + return closed_areas, open_areas, pending_op ~= nil + end + + local old_closed_areas = closed_areas + closed_areas = {} + local function reorder_and_fill_adj_min(old_id) + local a = old_closed_areas[old_id] + closed_areas[#closed_areas + 1] = a + a.id = #closed_areas + + if a.split then + for i = 1, #a.split.x_shares do + a.split.x_shares[i][3] = 0 + a.split.x_shares[i][4] = minimum + end + + for i = 1, #a.split.y_shares do + a.split.y_shares[i][3] = 0 + a.split.y_shares[i][4] = minimum + end + + for _, c in ipairs(a.split.children) do + if c.id then + reorder_and_fill_adj_min(c.id) + end + + local x_minimum, y_minimum + if c.split then + x_minimum, y_minimum = c.x_minimum, c.y_minimum + else + x_minimum, y_minimum = + minimum, minimum + end + + if type(c.parent_x_shares) == "table" then + local x_minimum_split = math.ceil(x_minimum / #c.parent_x_shares) + for i = 1, #c.parent_x_shares do + if a.split.x_shares[c.parent_x_shares[i]][4] < x_minimum_split then + a.split.x_shares[c.parent_x_shares[i]][4] = x_minimum_split + end + end + else + if a.split.x_shares[c.parent_x_shares][4] < x_minimum then + a.split.x_shares[c.parent_x_shares][4] = x_minimum + end + end + + if type(c.parent_y_shares) == "table" then + local y_minimum_split = math.ceil(y_minimum / #c.parent_y_shares) + for i = 1, #c.parent_y_shares do + if a.split.y_shares[c.parent_y_shares[i]][4] < y_minimum_split then + a.split.y_shares[c.parent_y_shares[i]][4] = y_minimum_split + end + end + else + if a.split.y_shares[c.parent_y_shares][4] < y_minimum then + a.split.y_shares[c.parent_y_shares][4] = y_minimum + end + end + end + + a.x_minimum = 0 + a.x_total_weight = 0 + for i = 1, #a.split.x_shares do + a.x_minimum = a.x_minimum + a.split.x_shares[i][4] + a.x_total_weight = a.x_total_weight + (a.split.x_shares[i][2] or 0) + end + a.y_minimum = 0 + a.y_total_weight = 0 + for i = 1, #a.split.y_shares do + a.y_minimum = a.y_minimum + a.split.y_shares[i][4] + a.y_total_weight = a.y_total_weight + (a.split.y_shares[i][2] or 0) + end + end + end + reorder_and_fill_adj_min(1) + + -- For debugging + -- for i = 1, #closed_areas do + -- print(i, closed_areas[i].parent_id, closed_areas[i].parent_x_shares, closed_areas[i].parent_y_shares) + -- if closed_areas[i].split then + -- print("/", closed_areas[i].split.method, #closed_areas[i].split.x_shares, #closed_areas[i].split.y_shares) + -- for j = 1, #closed_areas[i].split.children do + -- print("->", closed_areas[i].split.children[j].id) + -- end + -- end + -- end + + local orig_width = root.width + if root.x_minimum and root.width < root.x_minimum then + root.width = root.x_minimum + end + local orig_height = root.height + if root.y_minimum and root.height < root.y_minimum then + root.height = root.y_minimum + end + + function split(id) + local a = closed_areas[id] + if a.split then + local x_shares, y_shares + x_shares, a.split.x_spare = fair_split(a.width, a.split.x_shares) + y_shares, a.split.y_spare = fair_split(a.height, a.split.y_shares) + + for _, c in ipairs(a.split.children) do + + if type(c.parent_x_shares) == "table" then + c.x = a.x + x_shares[c.parent_x_shares[1] - 1] + c.width = 0 + for i = 1, #c.parent_x_shares do + c.width = c.width + x_shares[c.parent_x_shares[i]] - x_shares[c.parent_x_shares[i] - 1] + end + else + c.x = a.x + x_shares[c.parent_x_shares - 1] + c.width = x_shares[c.parent_x_shares] - x_shares[c.parent_x_shares - 1] + end + + if type(c.parent_y_shares) == "table" then + c.y = a.y + y_shares[c.parent_y_shares[1] - 1] + c.height = 0 + for i = 1, #c.parent_y_shares do + c.height = c.height + y_shares[c.parent_y_shares[i]] - y_shares[c.parent_y_shares[i] - 1] + end + else + c.y = a.y + y_shares[c.parent_y_shares - 1] + c.height = y_shares[c.parent_y_shares] - y_shares[c.parent_y_shares - 1] + end + + if c.id then + split(c.id) + end + end + end + end + split(1) + + -- Compatibility workaround. + if command:sub(1, 1) == "d" then + root.draft_mode = true + end + + for i = 1, #closed_areas do + if closed_areas[i].x + closed_areas[i].width > root.x + orig_width or + closed_areas[i].y + closed_areas[i].height > root.y + orig_height + then + closed_areas[i].inhabitable = true + end + end + + for i = 1, #open_areas do + if open_areas[i].x + open_areas[i].width > root.x + orig_width or + open_areas[i].y + open_areas[i].height > root.y + orig_height + then + open_areas[i].inhabitable = true + end + end + + return closed_areas, open_areas, pending_op ~= nil +end + +local function areas_to_command(areas, to_embed) + if #areas == 0 then return nil end + + local function shares_to_arg_str(shares) + local arg_str = "" + for _, share in ipairs(shares) do + if #arg_str > 0 then arg_str = arg_str.."," end + arg_str = arg_str..tostring(share[1]) + if not share[2] then + -- nothing + elseif share[2] > 0 then + arg_str = arg_str.."_"..tostring(share[2]) + else + arg_str = arg_str.."__"..tostring(-share[2]) + end + end + return arg_str + end + + local function get_command(area_id) + local r + local handled_options = {} + local a = areas[area_id] + + if a.hole then + return "|" + end + + if a.split then + for i = 1, #a.split.children do + if a.split.children[i].hole then + a.expansion = default_expansion + 1 + break + end + end + + local method = a.split.method + if method == "h" then + r = shares_to_arg_str(a.split.x_shares) + r = "h"..r + elseif method == "v" then + r = shares_to_arg_str(a.split.y_shares) + r = "v"..r + elseif method == "d" or method == "w" then + local simple = true + for _, s in ipairs(a.split.x_shares) do + if s[1] ~= 1 or s[2] then simple = false break end + end + if simple then + for _, s in ipairs(a.split.y_shares) do + if s[1] ~= 1 or s[2] then simple = false break end + end + end + if method == "w" and simple then + r = tostring(#a.split.x_shares)..","..tostring(#a.split.y_shares) + else + r = shares_to_arg_str(a.split.x_shares)..",,"..shares_to_arg_str(a.split.y_shares) + method = "d" + end + local m = "" + for _, c in ipairs(a.split.children) do + if type(c.parent_x_shares) == "table" then + if #m > 0 then m = m.."," end + m = m..tostring(c.parent_x_shares[#c.parent_x_shares] - c.parent_x_shares[1] + 1)..",".. + tostring(c.parent_y_shares[#c.parent_y_shares] - c.parent_y_shares[1] + 1) + end + end + if method == "d" and r == "1,,1" then + r = "" + end + if method == "d" and area_id == 1 then + r = (areas[1].draft_mode and "d" or ";d")..r + else + r = method..r..(#m == 0 and m or (method == "w" and "," or ",,"))..m + end + end + local acc_dashes = 0 + for _, c in ipairs(a.split.children) do + local cr = get_command(c.id) + if cr == "-" then + acc_dashes = acc_dashes + 1 + else + if acc_dashes == 0 then + elseif acc_dashes == 1 then + r = r.."-" + else + r = r.."c"..tonumber(acc_dashes) + end + acc_dashes = 0 + r = r..cr + end + end + if acc_dashes > 0 then + r = r.."c" + end + elseif a.disabled then + r = "/" + elseif a.layout then + r = "x"..a.layout + else + r = "-" + end + if a.split then + if a.parent_id then + if a.expansion ~= areas[a.parent_id].expansion - 1 then + r = "t"..tostring(a.expansion)..r + end + else + if a.expansion ~= default_expansion then + r = "t"..tostring(a.expansion)..r + end + end + end + return r + end + + local r = get_command(1) + if not to_embed then + r = r:gsub("[\\c]+$", ".") + end + return r +end + +if not in_module then + print("Testing areas/command processing") + local function check_transcoded_command(command, expectation) + local areas, open_areas = areas_from_command(command, {x = 0, y = 0, width = 100, height = 100}, 0) + if #open_areas > 0 then + print("Found open areas after command "..command) + assert(false) + end + local transcoded = areas_to_command(areas) + if transcoded ~= expectation then + print("Mismatched transcoding for "..command..": got "..transcoded..", expected "..expectation) + assert(false) + end + end + check_transcoded_command("t.", "-") + check_transcoded_command("121h.", "h1,2,1.") + check_transcoded_command("1_10,2,1h1s131v.", "h1_10,2,1-v1,3,1.") + check_transcoded_command("332111w.", "w3,3,2,1,1,1.") + check_transcoded_command("1310111d.", ";d1,3,1,,1,1,1.") + check_transcoded_command("dw66.", "dw6,6.") + check_transcoded_command(";dw66.", ";dw6,6.") + check_transcoded_command("101dw66.", ";dw6,6.") + check_transcoded_command("3tdw66.", "t3;dw6,6.") + print("Passed.") +end + +return { + areas_from_command = areas_from_command, + areas_to_command = areas_to_command, +} diff --git a/init.lua b/init.lua index eca07e6..341db39 100644 --- a/init.lua +++ b/init.lua @@ -1,3 +1,4 @@ +local engine = require(... .. ".engine") local layout = require(... .. ".layout") local editor = require(... .. ".editor") local switcher = require(... .. ".switcher") @@ -29,9 +30,11 @@ local function get_icon() end return { + engine = engine, layout = layout, editor = editor, switcher = switcher, + default_name = default_name, default_editor = default_editor, default_layout = default_layout, icon_raw = icon_raw, diff --git a/layout.lua b/layout.lua index 9be05f7..cd15ca5 100644 --- a/layout.lua +++ b/layout.lua @@ -1,10 +1,8 @@ -local machi = { - editor = require((...):match("(.-)[^%.]+$") .. "editor"), -} - -local api = { - screen = screen, - awful = require("awful"), +local this_package = ... and (...):match("(.-)[^%.]+$") or "" +local machi_editor = require(this_package.."editor") +local awful = require("awful") +local capi = { + screen = screen } local ERROR = 2 @@ -15,106 +13,112 @@ local DEBUG = -1 local module = { log_level = WARNING, global_default_cmd = "dw66.", - allowing_shrinking_by_mouse_moving = false, + allow_shrinking_by_mouse_moving = false, } local function log(level, msg) - if level > module.log_level then - print(msg) - end + if level > module.log_level then + print(msg) + end end local function min(a, b) - if a < b then return a else return b end + if a < b then return a else return b end end local function max(a, b) - if a < b then return b else return a end + if a < b then return b else return a end end local function get_screen(s) - return s and api.screen[s] + return s and capi.screen[s] end -api.awful.mouse.resize.add_enter_callback( - function (c) - c.full_width_before_move = c.width + c.border_width * 2 - c.full_height_before_move = c.height + c.border_width * 2 - end, 'mouse.move') +awful.mouse.resize.add_enter_callback( + function (c) + c.full_width_before_move = c.width + c.border_width * 2 + c.full_height_before_move = c.height + c.border_width * 2 + end, 'mouse.move') ---- find the best region for the area-like object +--- find the best area for the area-like object -- @param c area-like object - table with properties x, y, width, and height --- @param regions array of area-like objects --- @return the index of the best region -local function find_region(c, regions) - local choice = 1 - local choice_value = nil - local c_area = c.width * c.height - for i, a in ipairs(regions) do - local x_cap = max(0, min(c.x + c.width, a.x + a.width) - max(c.x, a.x)) - local y_cap = max(0, min(c.y + c.height, a.y + a.height) - max(c.y, a.y)) - local cap = x_cap * y_cap - -- -- a cap b / a cup b - -- local cup = c_area + a.width * a.height - cap - -- if cup > 0 then - -- local itx_ratio = cap / cup - -- if choice_value == nil or choice_value < itx_ratio then - -- choice_value = itx_ratio - -- choice = i - -- end - -- end - -- a cap b - if choice_value == nil or choice_value < cap then - choice = i - choice_value = cap - end - end - return choice +-- @param areas array of area objects +-- @return the index of the best area +local function find_area(c, areas) + local choice = 1 + local choice_value = nil + local c_area = c.width * c.height + for i, a in ipairs(areas) do + if not a.inhabitable then + local x_cap = max(0, min(c.x + c.width, a.x + a.width) - max(c.x, a.x)) + local y_cap = max(0, min(c.y + c.height, a.y + a.height) - max(c.y, a.y)) + local cap = x_cap * y_cap + -- -- a cap b / a cup b + -- local cup = c_area + a.width * a.height - cap + -- if cup > 0 then + -- local itx_ratio = cap / cup + -- if choice_value == nil or choice_value < itx_ratio then + -- choice_value = itx_ratio + -- choice = i + -- end + -- end + -- a cap b + if choice_value == nil or choice_value < cap then + choice = i + choice_value = cap + end + end + end + return choice end local function distance(x1, y1, x2, y2) - -- use d1 - return math.abs(x1 - x2) + math.abs(y1 - y2) + -- use d1 + return math.abs(x1 - x2) + math.abs(y1 - y2) end -local function find_lu(c, regions, rd) - local lu = nil - for i, a in ipairs(regions) do - if rd == nil or (a.x < regions[rd].x + regions[rd].width and a.y < regions[rd].y + regions[rd].height) then - if lu == nil or distance(c.x, c.y, a.x, a.y) < distance(c.x, c.y, regions[lu].x, regions[lu].y) then - lu = i - end - end - end - return lu +local function find_lu(c, areas, rd) + local lu = nil + for i, a in ipairs(areas) do + if not a.inhabitable then + if rd == nil or (a.x < areas[rd].x + areas[rd].width and a.y < areas[rd].y + areas[rd].height) then + if lu == nil or distance(c.x, c.y, a.x, a.y) < distance(c.x, c.y, areas[lu].x, areas[lu].y) then + lu = i + end + end + end + end + return lu end -local function find_rd(c, regions, lu) - local x, y - x = c.x + c.width + (c.border_width or 0) - y = c.y + c.height + (c.border_width or 0) - local rd = nil - for i, a in ipairs(regions) do - if lu == nil or (a.x + a.width > regions[lu].x and a.y + a.height > regions[lu].y) then - if rd == nil or distance(x, y, a.x + a.width, a.y + a.height) < distance(x, y, regions[rd].x + regions[rd].width, regions[rd].y + regions[rd].height) then - rd = i - end - end - end - return rd +local function find_rd(c, areas, lu) + local x, y + x = c.x + c.width + (c.border_width or 0) + y = c.y + c.height + (c.border_width or 0) + local rd = nil + for i, a in ipairs(areas) do + if not a.inhabitable then + if lu == nil or (a.x + a.width > areas[lu].x and a.y + a.height > areas[lu].y) then + if rd == nil or distance(x, y, a.x + a.width, a.y + a.height) < distance(x, y, areas[rd].x + areas[rd].width, areas[rd].y + areas[rd].height) then + rd = i + end + end + end + end + return rd end -function module.set_geometry(c, region_lu, region_rd, useless_gap, border_width) - -- We try to negate the gap of outer layer - if region_lu ~= nil then - c.x = region_lu.x - useless_gap - c.y = region_lu.y - useless_gap - end +function module.set_geometry(c, area_lu, area_rd, useless_gap, border_width) + -- We try to negate the gap of outer layer + if area_lu ~= nil then + c.x = area_lu.x - useless_gap + c.y = area_lu.y - useless_gap + end - if region_rd ~= nil then - c.width = region_rd.x + region_rd.width - c.x + useless_gap - border_width * 2 - c.height = region_rd.y + region_rd.height - c.y + useless_gap - border_width * 2 - end + if area_rd ~= nil then + c.width = area_rd.x + area_rd.width - c.x + useless_gap - border_width * 2 + c.height = area_rd.y + area_rd.height - c.y + useless_gap - border_width * 2 + end end function module.create(args_or_name, editor, default_cmd) @@ -132,7 +136,15 @@ function module.create(args_or_name, editor, default_cmd) else return nil end - args.editor = args.editor or editor or machi.editor.default_editor + args.name = args.name or function (tag) + if tag.machi_name_cache == nil then + tag.machi_name_cache = + tostring(tag.screen.geometry.width) .. "x" .. tostring(tag.screen.geometry.height) .. "+" .. + tostring(tag.screen.geometry.x) .. "+" .. tostring(tag.screen.geometry.y) .. '+' .. tag.name + end + return tag.machi_name_cache + end + args.editor = args.editor or editor or machi_editor.default_editor args.default_cmd = args.default_cmd or default_cmd or global_default_cmd args.persistent = args.persistent == nil or args.persistent @@ -143,253 +155,259 @@ function module.create(args_or_name, editor, default_cmd) return (args.name_func and args.name_func(tag) or args.name), args.persistent end - local function get_instance_(tag) - local name, persistent = get_instance_info(tag) - if instances[name] == nil then - instances[name] = { - layout = layout, - cmd = persistent and args.editor.get_last_cmd(name) or nil, - regions_cache = {}, - tag_data = {}, - } - if instances[name].cmd == nil then - instances[name].cmd = args.default_cmd - end - end - return instances[name] - end - - local function get_regions(workarea, tag) - local instance = get_instance_(tag) - local cmd = instance.cmd or module.global_default_cmd - if cmd == nil then return {}, false end - - local key = tostring(workarea.width) .. "x" .. tostring(workarea.height) .. "+" .. tostring(workarea.x) .. "+" .. tostring(workarea.y) - if instance.regions_cache[key] == nil then - instance.regions_cache[key] = args.editor.run_cmd(workarea, cmd) - end - return instance.regions_cache[key], cmd:sub(1,1) == "d" - end - - local function set_cmd(cmd, tag) - local instance = get_instance_(tag) - if instance.cmd ~= cmd then - instance.cmd = cmd - instance.regions_cache = {} - instance.tag_data = {} - end - end - - local function arrange(p) - local useless_gap = p.useless_gap - local wa = get_screen(p.screen).workarea -- get the real workarea without the gap (instead of p.workarea) - local cls = p.clients - local tag = get_screen(p.screen).selected_tag - local instance = get_instance_(tag) - local regions, draft_mode = get_regions(wa, tag) - - if #regions == 0 then return end - local nested_clients = {} - - for _, c in ipairs(cls) do - if c.machi == nil then - c.machi = setmetatable({}, {__mode = "v"}) - end - end - - if draft_mode then - for i, c in ipairs(cls) do - if c.floating or c.immobilized then - log(DEBUG, "Ignore client " .. tostring(c)) - else - local skip = false - if c.machi.lu ~= nil and c.machi.rd ~= nil and - c.machi.lu <= #regions and c.machi.rd <= #regions - then - if regions[c.machi.lu].x == c.x and - regions[c.machi.lu].y == c.y and - regions[c.machi.rd].x + regions[c.machi.rd].width - c.border_width * 2 == c.x + c.width and - regions[c.machi.rd].y + regions[c.machi.rd].height - c.border_width * 2 == c.y + c.height - then - skip = true - end - end - - local lu = nil - local rd = nil - if not skip then - log(DEBUG, "Compute regions for " .. (c.name or (""))) - lu = find_lu(c, regions) - if lu ~= nil then - c.x = regions[lu].x - c.y = regions[lu].y - rd = find_rd(c, regions, lu) - end - end - - if lu ~= nil and rd ~= nil then - c.machi.instance = instance - c.machi.region, c.machi.lu, c.machi.rd = nil, lu, rd - p.geometries[c] = {} - module.set_geometry(p.geometries[c], regions[lu], regions[rd], useless_gap, 0) - end + local function get_instance_(tag) + local name, persistent = get_instance_info(tag) + if instances[name] == nil then + instances[name] = { + layout = layout, + cmd = persistent and args.editor.get_last_cmd(name) or nil, + areas_cache = {}, + tag_data = {}, + } + if instances[name].cmd == nil then + instances[name].cmd = args.default_cmd end - end - else - for i, c in ipairs(cls) do - if c.floating or c.immobilized then - log(DEBUG, "Ignore client " .. tostring(c)) - else - if c.machi.region ~= nil and - regions[c.machi.region].layout == nil and - regions[c.machi.region].x == c.x and - regions[c.machi.region].y == c.y and - regions[c.machi.region].width - c.border_width * 2 == c.width and - regions[c.machi.region].height - c.border_width * 2 == c.height - then - else - log(DEBUG, "Compute regions for " .. (c.name or (""))) - local region = find_region(c, regions) - c.machi.instance = instance - c.machi.region, c.machi.lu, c.machi.rd = region, nil, nil - p.geometries[c] = {} - if regions[region].layout ~= nil then - local clients = nested_clients[region] - if clients == nil then clients = {}; nested_clients[region] = clients end - clients[#clients + 1] = c - else - module.set_geometry(p.geometries[c], regions[region], regions[region], useless_gap, 0) - end - end + end + return instances[name] + end + + local function get_areas(screen, tag) + local workarea = screen.workarea + local instance = get_instance_(tag) + local cmd = instance.cmd or module.global_default_cmd + if cmd == nil then return {}, false end + + local key = tostring(workarea.width) .. "x" .. tostring(workarea.height) .. "+" .. tostring(workarea.x) .. "+" .. tostring(workarea.y) + if instance.areas_cache[key] == nil then + instance.areas_cache[key] = args.editor.run_cmd(cmd, screen, tag) + end + local draft_mode = instance.areas_cache[key] and instance.areas_cache[key][1].draft_mode + return instance.areas_cache[key], draft_mode + end + + local function set_cmd(cmd, tag) + local instance = get_instance_(tag) + if instance.cmd ~= cmd then + instance.cmd = cmd + instance.areas_cache = {} + instance.tag_data = {} + end + end + + local function arrange(p) + local useless_gap = p.useless_gap + local screen = get_screen(p.screen) + local wa = screen.workarea -- get the real workarea without the gap (instead of p.workarea) + local cls = p.clients + local tag = screen.selected_tag + local instance = get_instance_(tag) + local areas, draft_mode = get_areas(screen, tag) + + if #areas == 0 then return end + local nested_clients = {} + + for _, c in ipairs(cls) do + if c.machi == nil then + c.machi = setmetatable({}, {__mode = "v"}) end - end + end - for region, clients in pairs(nested_clients) do - if instance.tag_data[region] == nil then - -- TODO: Make the default more flexible. - instance.tag_data[region] = { - column_count = 1, - master_count = 1, - master_fill_policy = "expand", - useless_gap = 0, - master_width_factor = 0.5, - _private = { - awful_tag_properties = { - }, - }, - } - end - local nested_params = { - tag = instance.tag_data[region], - screen = p.screen, - clients = clients, - padding = 0, - geometry = { - x = regions[region].x, - y = regions[region].y, - width = regions[region].width, - height = regions[region].height, - }, - -- Not sure how useless_gap adjustment works here. It seems to work anyway. - workarea = { - x = regions[region].x - useless_gap, - y = regions[region].y - useless_gap, - width = regions[region].width + useless_gap * 2, - height = regions[region].height + useless_gap * 2, - }, - useless_gap = useless_gap, - geometries = {}, - } - regions[region].layout.arrange(nested_params) - for _, c in ipairs(clients) do - p.geometries[c] = { - x = nested_params.geometries[c].x, - y = nested_params.geometries[c].y, - width = nested_params.geometries[c].width, - height = nested_params.geometries[c].height, - } - end - end - end - end + if draft_mode then + for i, c in ipairs(cls) do + if c.floating or c.immobilized then + log(DEBUG, "Ignore client " .. tostring(c)) + else + local skip = false + if c.machi.lu ~= nil and c.machi.rd ~= nil and + c.machi.lu <= #areas and c.machi.rd <= #areas and + not areas[c.machi.lu].inhabitable and not areas[c.machi.rd].inhabitable + then + if areas[c.machi.lu].x == c.x and + areas[c.machi.lu].y == c.y and + areas[c.machi.rd].x + areas[c.machi.rd].width - c.border_width * 2 == c.x + c.width and + areas[c.machi.rd].y + areas[c.machi.rd].height - c.border_width * 2 == c.y + c.height + then + skip = true + end + end - local function resize_handler (c, context, h) - local workarea = c.screen.workarea - local regions, draft_mode = get_regions(workarea, c.screen.selected_tag) + local lu = nil + local rd = nil + if not skip then + log(DEBUG, "Compute areas for " .. (c.name or (""))) + lu = find_lu(c, areas) + if lu ~= nil then + c.x = areas[lu].x + c.y = areas[lu].y + rd = find_rd(c, areas, lu) + end + end - if #regions == 0 then return end - - if draft_mode then - local lu = find_lu(h, regions) - local rd = nil - if lu ~= nil then - if context == "mouse.move" then - -- Use the initial width and height since it may change in undesired way. - local hh = {} - hh.x = regions[lu].x - hh.y = regions[lu].y - hh.width = c.full_width_before_move - hh.height = c.full_height_before_move - rd = find_rd(hh, regions, lu) - - if rd ~= nil and not module.allowing_shrinking_by_mouse_moving and - (regions[rd].x + regions[rd].width - regions[lu].x < c.full_width_before_move or - regions[rd].y + regions[rd].height - regions[lu].y < c.full_height_before_move) then - hh.x = regions[rd].x + regions[rd].width - c.full_width_before_move - hh.y = regions[rd].y + regions[rd].height - c.full_height_before_move - lu = find_lu(hh, regions, rd) - end - else - local hh = {} - hh.x = h.x - hh.y = h.y - hh.width = h.width - hh.height = h.height - hh.border_width = c.border_width - rd = find_rd(hh, regions, lu) + if lu ~= nil and rd ~= nil then + c.machi.instance = instance + c.machi.area, c.machi.lu, c.machi.rd = nil, lu, rd + p.geometries[c] = {} + module.set_geometry(p.geometries[c], areas[lu], areas[rd], useless_gap, 0) + end + end + end + else + for i, c in ipairs(cls) do + if c.floating or c.immobilized then + log(DEBUG, "Ignore client " .. tostring(c)) + else + if c.machi.area ~= nil and + c.machi.area < #areas and + not areas[c.machi.area].inhabitable and + areas[c.machi.area].layout == nil and + areas[c.machi.area].x == c.x and + areas[c.machi.area].y == c.y and + areas[c.machi.area].width - c.border_width * 2 == c.width and + areas[c.machi.area].height - c.border_width * 2 == c.height + then + else + log(DEBUG, "Compute areas for " .. (c.name or (""))) + local area = find_area(c, areas) + c.machi.instance = instance + c.machi.area, c.machi.lu, c.machi.rd = area, nil, nil + p.geometries[c] = {} + if machi_editor.nested_layouts[areas[area].layout] ~= nil then + local clients = nested_clients[area] + if clients == nil then clients = {}; nested_clients[area] = clients end + clients[#clients + 1] = c + else + module.set_geometry(p.geometries[c], areas[area], areas[area], useless_gap, 0) + end + end + end end - if lu ~= nil and rd ~= nil then - c.machi.lu = lu - c.machi.rd = rd - module.set_geometry(c, regions[lu], regions[rd], 0, c.border_width) + for area, clients in pairs(nested_clients) do + if instance.tag_data[area] == nil then + -- TODO: Make the default more flexible. + instance.tag_data[area] = { + column_count = 1, + master_count = 1, + master_fill_policy = "expand", + gap = 0, + master_width_factor = 0.5, + _private = { + awful_tag_properties = { + }, + }, + } + end + local nested_params = { + tag = instance.tag_data[area], + screen = p.screen, + clients = clients, + padding = 0, + geometry = { + x = areas[area].x, + y = areas[area].y, + width = areas[area].width, + height = areas[area].height, + }, + -- Not sure how useless_gap adjustment works here. It seems to work anyway. + workarea = { + x = areas[area].x - useless_gap, + y = areas[area].y - useless_gap, + width = areas[area].width + useless_gap * 2, + height = areas[area].height + useless_gap * 2, + }, + useless_gap = useless_gap, + geometries = {}, + } + machi_editor.nested_layouts[areas[area].layout].arrange(nested_params) + for _, c in ipairs(clients) do + p.geometries[c] = { + x = nested_params.geometries[c].x, + y = nested_params.geometries[c].y, + width = nested_params.geometries[c].width, + height = nested_params.geometries[c].height, + } + end end - end - else - if context ~= "mouse.move" then return end + end + end - if #regions == 0 then return end + local function resize_handler (c, context, h) + local areas, draft_mode = get_areas(c.screen, c.screen.selected_tag) - local center_x = h.x + h.width / 2 - local center_y = h.y + h.height / 2 + if #areas == 0 then return end - local choice = 1 - local choice_value = nil + if draft_mode then + local lu = find_lu(h, areas) + local rd = nil + if lu ~= nil then + if context == "mouse.move" then + -- Use the initial width and height since it may change in undesired way. + local hh = {} + hh.x = areas[lu].x + hh.y = areas[lu].y + hh.width = c.full_width_before_move + hh.height = c.full_height_before_move + rd = find_rd(hh, areas, lu) - for i, r in ipairs(regions) do - local r_x = r.x + r.width / 2 - local r_y = r.y + r.height / 2 - local dis = (r_x - center_x) * (r_x - center_x) + (r_y - center_y) * (r_y - center_y) - if choice_value == nil or choice_value > dis then - choice = i - choice_value = dis + if rd ~= nil and not module.allowing_shrinking_by_mouse_moving and + (areas[rd].x + areas[rd].width - areas[lu].x < c.full_width_before_move or + areas[rd].y + areas[rd].height - areas[lu].y < c.full_height_before_move) then + hh.x = areas[rd].x + areas[rd].width - c.full_width_before_move + hh.y = areas[rd].y + areas[rd].height - c.full_height_before_move + lu = find_lu(hh, areas, rd) + end + else + local hh = {} + hh.x = h.x + hh.y = h.y + hh.width = h.width + hh.height = h.height + hh.border_width = c.border_width + rd = find_rd(hh, areas, lu) + end + + if lu ~= nil and rd ~= nil then + c.machi.lu = lu + c.machi.rd = rd + module.set_geometry(c, areas[lu], areas[rd], 0, c.border_width) + end end - end + else + if context ~= "mouse.move" then return end - if c.machi.region ~= choice then - c.machi.region = choice - module.set_geometry(c, regions[choice], regions[choice], 0, c.border_width) - end - end - end + if #areas == 0 then return end - layout.name = "machi" - layout.arrange = arrange - layout.resize_handler = resize_handler - layout.machi_get_instance_info = get_instance_info - layout.machi_set_cmd = set_cmd - layout.machi_get_regions = get_regions - return layout + local center_x = h.x + h.width / 2 + local center_y = h.y + h.height / 2 + + local choice = 1 + local choice_value = nil + + for i, r in ipairs(areas) do + local r_x = r.x + r.width / 2 + local r_y = r.y + r.height / 2 + local dis = (r_x - center_x) * (r_x - center_x) + (r_y - center_y) * (r_y - center_y) + if choice_value == nil or choice_value > dis then + choice = i + choice_value = dis + end + end + + if c.machi.area ~= choice then + c.machi.area = choice + module.set_geometry(c, areas[choice], areas[choice], 0, c.border_width) + end + end + end + + layout.name = args.icon_name or "machi" + layout.editor = args.editor + layout.arrange = arrange + layout.resize_handler = resize_handler + layout.machi_get_instance_info = get_instance_info + layout.machi_set_cmd = set_cmd + layout.machi_get_areas = get_areas + return layout end return module diff --git a/switcher.lua b/switcher.lua index ee41679..b8825e2 100644 --- a/switcher.lua +++ b/switcher.lua @@ -1,5 +1,6 @@ local machi = { - layout = require((...):match("(.-)[^%.]+$") .. "layout"), + layout = require((...):match("(.-)[^%.]+$") .. "layout"), + engine = require((...):match("(.-)[^%.]+$") .. "engine"), } local api = { @@ -15,6 +16,8 @@ local api = { dpi = require("beautiful.xresources").apply_dpi, } +local gtimer = require("gears.timer") + local ERROR = 2 local WARNING = 1 local INFO = 0 @@ -53,6 +56,9 @@ function module.start(c, exit_keys) local border_color = with_alpha(api.gears.color( api.beautiful.machi_switcher_border_color or api.beautiful.border_focus), api.beautiful.machi_switcher_border_opacity or 0.25) + local border_color_hl = with_alpha(api.gears.color( + api.beautiful.machi_switcher_border_hl_color or api.beautiful.border_focus), + api.beautiful.machi_switcher_border_hl_opacity or 0.75) local fill_color = with_alpha(api.gears.color( api.beautiful.machi_switcher_fill_color or api.beautiful.bg_normal), api.beautiful.machi_switcher_fill_opacity or 0.25) @@ -67,14 +73,16 @@ function module.start(c, exit_keys) local traverse_radius = api.dpi(5) local screen = c and c.screen or api.screen.focused() + local tag = screen.selected_tag + local layout = tag.layout + local gap = tag.gap local start_x = screen.workarea.x local start_y = screen.workarea.y - local layout = api.layout.get(screen) - if (c ~= nil and c.floating) or layout.machi_get_regions == nil then return end + if (c ~= nil and c.floating) or layout.machi_get_areas == nil then return end - local regions, draft_mode = layout.machi_get_regions(screen.workarea, screen.selected_tag) - if regions == nil or #regions == 0 then + local areas, draft_mode = layout.machi_get_areas(screen, screen.selected_tag) + if areas == nil or #areas == 0 then return end @@ -91,7 +99,6 @@ function module.start(c, exit_keys) }) infobox.visible = true - local tablist_region = nil local tablist = nil local tablist_index = nil @@ -104,23 +111,35 @@ function module.start(c, exit_keys) traverse_y = screen.workarea.y + screen.workarea.height / 2 end + local selected_area_ = nil + local function selected_area() + if selected_area_ == nil then + for i, a in ipairs(areas) do + if not a.inhabitable and + a.x <= traverse_x and traverse_x < a.x + a.width and + a.y <= traverse_y and traverse_y < a.y + a.height + then + selected_area_ = i + end + end + end + return selected_area_ + end + + local function set_selected_area(a) + selected_area_ = a + end + local function maintain_tablist() if tablist == nil then tablist = {} - for i, a in ipairs(regions) do - if a.x <= traverse_x and traverse_x < a.x + a.width and - a.y <= traverse_y and traverse_y < a.y + a.height - then - active_region = i - end - end - + local active_area = selected_area() for _, tc in ipairs(screen.tiled_clients) do if not (tc.floating or tc.immobilized) then - if regions[active_region].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= regions[active_region].x + regions[active_region].width and - regions[active_region].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= regions[active_region].y + regions[active_region].height + if areas[active_area].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= areas[active_area].x + areas[active_area].width and + areas[active_area].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= areas[active_area].y + areas[active_area].height then tablist[#tablist + 1] = tc end @@ -159,30 +178,25 @@ function module.start(c, exit_keys) cr:rectangle(0, 0, width, height) cr:fill() - local msg, ext, active_region - for i, a in ipairs(regions) do - - cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) - cr:clip() - cr:set_source(fill_color) - cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) - cr:fill() - cr:set_source(border_color) - cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) - cr:set_line_width(10.0) - cr:stroke() - cr:reset_clip() - - -- TODO deduplicate this with code in maintain_tablist() - if a.x <= traverse_x and traverse_x < a.x + a.width and - a.y <= traverse_y and traverse_y < a.y + a.height - then - active_region = i + local msg, ext + local active_area = selected_area() + for i, a in ipairs(areas) do + if not a.inhabitable or i == active_area then + cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) + cr:clip() + cr:set_source(fill_color) + cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) + cr:fill() + cr:set_source(i == active_area and border_color_hl or border_color) + cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) + cr:set_line_width(10.0) + cr:stroke() + cr:reset_clip() end end if #tablist > 0 then - local a = regions[active_region] + local a = areas[active_area] local pl = api.lgi.Pango.Layout.create(cr) pl:set_font_description(tablist_font_desc) @@ -192,7 +206,7 @@ function module.start(c, exit_keys) local exts = {} for index, tc in ipairs(tablist) do - local label = tc.name + local label = tc.name or "" pl:set_text(label) local w, h w, h = pl:get_size() @@ -208,7 +222,7 @@ function module.start(c, exit_keys) local y_offset = a.y + a.height / 2 - list_height / 2 + vpadding - start_y -- cr:rectangle(a.x - start_x, y_offset - vpadding - start_y, a.width, list_height) - -- cover the entire region + -- cover the entire area cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) cr:set_source(fill_color) cr:fill() @@ -218,7 +232,7 @@ function module.start(c, exit_keys) cr:fill() for index, tc in ipairs(tablist) do - local label = tc.name + local label = tc.name or "" local ext = exts[index] if index == tablist_index then cr:rectangle(x_offset - ext.width / 2 - vpadding / 2, y_offset - vpadding / 2, ext.width + vpadding, ext.height + vpadding) @@ -298,37 +312,28 @@ function module.start(c, exit_keys) end end - local current_region = nil + local current_area = selected_area() if c and (shift or ctrl) then - for i, a in ipairs(regions) do - if a.x <= traverse_x and traverse_x < a.x + a.width and - a.y <= traverse_y and traverse_y < a.y + a.height - then - current_region = i - break - end - end - if shift then - if current_region == nil or - regions[current_region].x ~= c.x or - regions[current_region].y ~= c.y + if current_area == nil or + areas[current_area].x ~= c.x or + areas[current_area].y ~= c.y then traverse_x = c.x + traverse_radius traverse_y = c.y + traverse_radius - current_region = nil + set_selected_area(nil) end elseif ctrl then local ex = c.x + c.width + c.border_width * 2 local ey = c.y + c.height + c.border_width * 2 - if current_region == nil or - regions[current_region].x + regions[current_region].width ~= ex or - regions[current_region].y + regions[current_region].height ~= ey + if current_area == nil or + areas[current_area].x + areas[current_area].width ~= ex or + areas[current_area].y + areas[current_area].height ~= ey then traverse_x = ex - traverse_radius traverse_y = ey - traverse_radius - current_region = nil + set_selected_area(nil) end end end @@ -336,12 +341,10 @@ function module.start(c, exit_keys) local choice = nil local choice_value - for i, a in ipairs(regions) do - if a.x <= traverse_x and traverse_x < a.x + a.width and - a.y <= traverse_y and traverse_y < a.y + a.height - then - current_region = i - end + current_area = selected_area() + + for i, a in ipairs(areas) do + if a.inhabitable then goto continue end local v if key == "Up" then @@ -378,10 +381,11 @@ function module.start(c, exit_keys) choice = i choice_value = v end + ::continue:: end if choice == nil then - choice = current_region + choice = current_area if key == "Up" then traverse_y = screen.workarea.y elseif key == "Down" then @@ -394,9 +398,10 @@ function module.start(c, exit_keys) end if choice ~= nil then - traverse_x = max(regions[choice].x + traverse_radius, min(regions[choice].x + regions[choice].width - traverse_radius, traverse_x)) - traverse_y = max(regions[choice].y + traverse_radius, min(regions[choice].y + regions[choice].height - traverse_radius, traverse_y)) + traverse_x = max(areas[choice].x + traverse_radius, min(areas[choice].x + areas[choice].width - traverse_radius, traverse_x)) + traverse_y = max(areas[choice].y + traverse_radius, min(areas[choice].y + areas[choice].height - traverse_radius, traverse_y)) tablist = nil + set_selected_area(nil) if c and ctrl and draft_mode then local lu = c.machi.lu @@ -404,28 +409,28 @@ function module.start(c, exit_keys) if shift then lu = choice - if regions[rd].x + regions[rd].width <= regions[lu].x or - regions[rd].y + regions[rd].height <= regions[lu].y + if areas[rd].x + areas[rd].width <= areas[lu].x or + areas[rd].y + areas[rd].height <= areas[lu].y then rd = nil end else rd = choice - if regions[rd].x + regions[rd].width <= regions[lu].x or - regions[rd].y + regions[rd].height <= regions[lu].y + if areas[rd].x + areas[rd].width <= areas[lu].x or + areas[rd].y + areas[rd].height <= areas[lu].y then lu = nil end end if lu ~= nil and rd ~= nil then - machi.layout.set_geometry(c, regions[lu], regions[rd], 0, c.border_width) + machi.layout.set_geometry(c, areas[lu], areas[rd], 0, c.border_width) elseif lu ~= nil then - machi.layout.set_geometry(c, regions[lu], nil, 0, c.border_width) + machi.layout.set_geometry(c, areas[lu], nil, 0, c.border_width) elseif rd ~= nil then - c.x = min(c.x, regions[rd].x) - c.y = min(c.y, regions[rd].y) - machi.layout.set_geometry(c, nil, regions[rd], 0, c.border_width) + c.x = min(c.x, areas[rd].x) + c.y = min(c.y, areas[rd].y) + machi.layout.set_geometry(c, nil, areas[rd], 0, c.border_width) end c.machi.lu = lu c.machi.rd = rd @@ -436,11 +441,11 @@ function module.start(c, exit_keys) elseif c and shift then -- move the window if draft_mode then - c.x = regions[choice].x - c.y = regions[choice].y + c.x = areas[choice].x + c.y = areas[choice].y else - machi.layout.set_geometry(c, regions[choice], regions[choice], 0, c.border_width) - c.machi.region = choice + machi.layout.set_geometry(c, areas[choice], areas[choice], 0, c.border_width) + c.machi.area = choice end c:emit_signal("request::activate", "mouse.move", {raise=false}) c:raise() @@ -458,6 +463,42 @@ function module.start(c, exit_keys) infobox.bgimage = draw_info end + elseif (key == "u" or key == "Prior") and not draft_mode then + local current_area = selected_area() + if areas[current_area].parent_id then + tablist = nil + set_selected_area(areas[current_area].parent_id) + infobox.bgimage = draw_info + end + elseif key == "/" and not draft_mode then + local current_area = selected_area() + areas[current_area].hole = true + local prefix, suffix = machi.engine.areas_to_command( + areas, true):match("(.*)|(.*)") + print(prefix, suffix) + + areas[current_area].hole = nil + + workarea = { + x = areas[current_area].x - gap * 2, + y = areas[current_area].y - gap * 2, + width = areas[current_area].width + gap * 4, + height = areas[current_area].height + gap * 4, + } + gtimer.delayed_call( + function () + print(layout.editor) + layout.editor.start_interactive( + screen, + { + workarea = workarea, + cmd_prefix = prefix, + cmd_suffix = suffix, + } + ) + end + ) + exit() elseif key == "Escape" or key == "Return" then exit() else