diff --git a/README.md b/README.md index 885c634..db4e46a 100644 --- a/README.md +++ b/README.md @@ -7,55 +7,50 @@ A manual layout for Awesome with a rapid interactive editor. TL;DR --- I want the control of my layout. 1. Dynamic tiling is an overkill, since tiling is only useful for persistent windows, and people extensively use hibernate/sleep these days. -2. I don't want to have all windows moving around whenever a new window shows up. +2. I don't want to have all windows moving around whenever a new window shows up. 3. I want to have a flexible layout such that I can quickly adjust to whatever I need. ## Use the layout -Use `layout_machi.layout.create_layout([LAYOUT_NAME}, [DEFAULT_REGIONS])` to instantiate the layout. -For example: - -``` -layout_machi.layout.create_layout("default", {}) -``` - -Creates a layout with no regions +Use `layotu = layout_machi.layout.create()` to instantiate the layout. ## Use the editor -Call `layout_machi.editor.start_editor(data)` to enter the editor for the current layout (given it is a machi instance). -`data` is am object for storing the history of the editing, initially `{}`. -The editor starts with the open area of the entire workarea, taking command to split the current area into multiple sub-areas, then editing each of them. +Call `editor = layout_machi.editor.create(data)` to create an editor that can either + + - Interactively edit layout by calling `editor.start_interactive()` + - Set the layout with batched commands by calling `editor.set_by_cmd(cmd)`, where cmd is a string + +`data` is an object for storing the history of the editing, initially `{}`. + +### The layout editing command + +The editor starts with the open area of the entire workarea, taking command to split the current area into multiple sub-areas, then editing each of them. The editor is keyboard driven, accepting a number of command keys. Before each command, you can optionally provide at most 2 digits for parameters (A, B) of the command. Undefined parameters are (mostly) treated as 1. -1. `Up`/`Down`: restore to the history command sequence -2. `h`/`v`: split the current region horizontally/vertically into 2 regions. The split will respect the ratio A:B. +1. `Up`/`Down`: restore to the history command sequence +2. `h`/`v`: split the current region horizontally/vertically into 2 regions. The split will respect the ratio A:B. 3. `w`: Take two parameters (A, B), and split the current region equally into A columns and B rows. If no parameter is defined, behave the same as `Space` without parameters. 4. `s`: shift the current editing region with other open regions. If A is defined, shift for A times. 5. `Space` or `-`: Without parameters, close the current region and move to the next open region. With parameters, set the maximum depth of splitting (default is 2). -6. `Enter`/`.`: close all open regions. When all regions are closed, press `Enter` will save the layout and exit the editor. +6. `Enter`/`.`: close all open regions. When all regions are closed, press `Enter` will save the layout and exit the editor. 7. `Backspace`: undo the last command. 8. `Escape`: exit the editor without saving the layout. -### Demos: - -I used `Super + /` for the editor and `Super + Tab` for fitting the windows. - +For examples: h-v ``` 11 22 11 22 -11 +11 11 33 11 33 ``` -![](https://i.imgur.com/QbvMRTW.gif) - hvv (or 22w) @@ -67,8 +62,6 @@ hvv (or 22w) 22 44 ``` -![](https://i.imgur.com/xJebxcF.gif) - 3-13h2v--2h-12v @@ -97,24 +90,17 @@ Tada! ``` -history - -![](https://i.imgur.com/gzFr48V.gif) - ### Persistent history -You need to specify the path of the history file in the editor data, then restore the persistent history by `layout_machi.editor.restore_data`. For example, +If you want all command persisted, you need to specify the path of the history file in the editor data. +The persisted history can be restored by `layout_machi.editor.restore_data(...)`. For example, ``` -machi_layout_data = layout_machi.editor.restore_data({ history_file = ".machi-layout", history_save_max = 10 }) +machi_editor_data = layout_machi.editor.restore_data({ history_file = ".machi-layout", history_save_max = 10 }) ``` -Then start the editor with the restored data. The last `history_save_max` commands are persisted. -## Other goodies - - - Moving a window using the mouse will move it across regions ## Other functions diff --git a/editor.lua b/editor.lua index 50f4cc7..7afc4f3 100644 --- a/editor.lua +++ b/editor.lua @@ -86,7 +86,7 @@ function cycle_region(c) end current_region = c.machi_region or 1 if not is_tiling(c) then - -- find out which region has the most intersection, calculated by a cap b / a cup b + -- find out which region has the most intersection, calculated by a cap b / a cup b c.machi_region = fit_region(c, regions) set_tiling(c) elseif current_region >= #regions then @@ -107,40 +107,9 @@ function shrink_area_with_gap(a, gap) height = a.height - (a.bu and 0 or gap / 2) - (a.bd and 0 or gap / 2) } end -function start_editor(data) +function create(data) local gap = data.gap or 0 - if data.cmds == nil then - data.cmds = {} - end - - local cmd_index = #data.cmds + 1 - data.cmds[cmd_index] = "" - - local screen = api.screen.focused() - local init_area = { - x = screen.workarea.x, - y = screen.workarea.y, - width = screen.workarea.width, - height = screen.workarea.height, - border = 15, - depth = 0, - group_id = 0, - -- we do not want to rely on BitOp - bl = true, br = true, bu = true, bd = true, - } - local kg - local infobox = api.wibox({ - x = screen.workarea.x, - y = screen.workarea.y, - width = screen.workarea.width, - height = screen.workarea.height, - bg = "#ffffff00", - opacity = 1, - ontop = true - }) - infobox.visible = true - local closed_areas local open_areas local history @@ -152,9 +121,21 @@ function start_editor(data) local to_exit local to_apply - local function init() + local function init(init_area) closed_areas = {} - open_areas = {init_area} + open_areas = { + { + x = init_area.x, + y = init_area.y, + width = init_area.width, + height = init_area.height, + border = 15, + depth = 0, + group_id = 0, + -- we do not want to rely on BitOp + bl = true, br = true, bu = true, bd = true, + } + } history = {} num_1 = nil num_2 = nil @@ -165,68 +146,6 @@ function start_editor(data) to_apply = false end - local function draw_info(context, cr, width, height) - cr:set_source_rgba(0, 0, 0, 0) - cr:rectangle(0, 0, width, height) - cr:fill() - - local msg, ext - - for i, a in ipairs(closed_areas) do - local sa = shrink_area_with_gap(a, gap) - cr:rectangle(sa.x, sa.y, sa.width, sa.height) - cr:clip() - cr:set_source(api.gears.color(closed_color)) - cr:rectangle(sa.x, sa.y, sa.width, sa.height) - cr:fill() - cr:set_source(api.gears.color(border_color)) - cr:rectangle(sa.x, sa.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, gap) - cr:rectangle(sa.x, sa.y, sa.width, sa.height) - cr:clip() - if i == #open_areas then - cr:set_source(api.gears.color(active_color)) - else - cr:set_source(api.gears.color(open_color)) - end - cr:rectangle(sa.x, sa.y, sa.width, sa.height) - cr:fill() - - cr:set_source(api.gears.color(border_color)) - cr:rectangle(sa.x, sa.y, sa.width, sa.height) - cr:set_line_width(10.0) - if i ~= #open_areas then - cr:set_dash({5, 5}, 0) - cr:stroke() - cr:set_dash({}, 0) - else - cr:stroke() - end - cr:reset_clip() - end - - cr:select_font_face(label_font_family, "normal", "normal") - cr:set_font_size(info_size) - cr:set_font_face(cr:get_font_face()) - msg = current_info - ext = cr:text_extents(msg) - cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing) - cr:text_path(msg) - cr:set_source_rgba(1, 1, 1, 1) - cr:fill() - cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing) - cr:text_path(msg) - cr:set_source_rgba(0, 0, 0, 1) - cr:set_line_width(2.0) - cr:stroke() - end - local function push_history() history[#history + 1] = {#closed_areas, #open_areas, {}, current_info, current_cmd, max_depth, num_1, num_2} end @@ -269,18 +188,6 @@ function start_editor(data) return a end - local function refresh() - print("closed areas:") - for i, a in ipairs(closed_areas) do - print(" " .. _area_tostring(a)) - end - print("open areas:") - for i, a in ipairs(open_areas) do - print(" " .. _area_tostring(a)) - end - infobox.bgimage = draw_info - end - local split_count = 0 local function handle_split(method, alt) @@ -362,13 +269,8 @@ function start_editor(data) num_2 = nil end - local function cleanup() - infobox.visible = false - end - local function push_area() closed_areas[#closed_areas + 1] = pop_open_area() - infobox.bgimage = draw_info end local function handle_command(key) @@ -434,141 +336,285 @@ function start_editor(data) return key end - print("interactive layout editing starts") + local function start_interactive() + if data.cmds == nil then + data.cmds = {} + end - init() - refresh() + local cmd_index = #data.cmds + 1 + data.cmds[cmd_index] = "" - kg = keygrabber.run(function (mod, key, event) - if event == "release" then - return + local screen = api.screen.focused() + local kg + local infobox = api.wibox({ + x = screen.workarea.x, + y = screen.workarea.y, + width = screen.workarea.width, + height = screen.workarea.height, + bg = "#ffffff00", + opacity = 1, + ontop = true + }) + infobox.visible = true + + local function cleanup() + infobox.visible = false + end + + local function draw_info(context, cr, width, height) + cr:set_source_rgba(0, 0, 0, 0) + cr:rectangle(0, 0, width, height) + cr:fill() + + local msg, ext + + for i, a in ipairs(closed_areas) do + local sa = shrink_area_with_gap(a, gap) + cr:rectangle(sa.x, sa.y, sa.width, sa.height) + cr:clip() + cr:set_source(api.gears.color(closed_color)) + cr:rectangle(sa.x, sa.y, sa.width, sa.height) + cr:fill() + cr:set_source(api.gears.color(border_color)) + cr:rectangle(sa.x, sa.y, sa.width, sa.height) + cr:set_line_width(10.0) + cr:stroke() + cr:reset_clip() end - if key == "BackSpace" then - pop_history() - elseif key == "Escape" then - table.remove(data.cmds, #data.cmds) - to_exit = true - elseif key == "Up" or key == "Down" then - if current_cmd ~= data.cmds[cmd_index] then - data.cmds[#data.cmds] = current_cmd - end - - if key == "Up" and cmd_index > 1 then - cmd_index = cmd_index - 1 - elseif key == "Down" and cmd_index < #data.cmds then - cmd_index = cmd_index + 1 - end - - print("restore history #" .. tostring(cmd_index) .. ":" .. data.cmds[cmd_index]) - init() - for i = 1, #data.cmds[cmd_index] do - cmd = data.cmds[cmd_index]:sub(i, i) - - push_history() - local ret = handle_command(cmd) - - current_info = current_info .. ret - current_cmd = current_cmd .. ret - end - - if #open_areas == 0 then - current_info = current_info .. " (enter to save)" - end - elseif #open_areas > 0 then - push_history() - local ret = handle_command(key) - if ret ~= nil then - current_info = current_info .. ret - current_cmd = current_cmd .. ret + for i, a in ipairs(open_areas) do + local sa = shrink_area_with_gap(a, gap) + cr:rectangle(sa.x, sa.y, sa.width, sa.height) + cr:clip() + if i == #open_areas then + cr:set_source(api.gears.color(active_color)) else - discard_history() + cr:set_source(api.gears.color(open_color)) + end + cr:rectangle(sa.x, sa.y, sa.width, sa.height) + cr:fill() + + cr:set_source(api.gears.color(border_color)) + cr:rectangle(sa.x, sa.y, sa.width, sa.height) + cr:set_line_width(10.0) + if i ~= #open_areas then + cr:set_dash({5, 5}, 0) + cr:stroke() + cr:set_dash({}, 0) + else + cr:stroke() + end + cr:reset_clip() + end + + cr:select_font_face(label_font_family, "normal", "normal") + cr:set_font_size(info_size) + cr:set_font_face(cr:get_font_face()) + msg = current_info + ext = cr:text_extents(msg) + cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing) + cr:text_path(msg) + cr:set_source_rgba(1, 1, 1, 1) + cr:fill() + cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing) + cr:text_path(msg) + cr:set_source_rgba(0, 0, 0, 1) + cr:set_line_width(2.0) + cr:stroke() + end + + local function refresh() + print("closed areas:") + for i, a in ipairs(closed_areas) do + print(" " .. _area_tostring(a)) + end + print("open areas:") + for i, a in ipairs(open_areas) do + print(" " .. _area_tostring(a)) + end + infobox.bgimage = draw_info + end + + + print("interactive layout editing starts") + + init(screen.workarea) + refresh() + + kg = keygrabber.run(function (mod, key, event) + if event == "release" then + return end - if #open_areas == 0 then - current_info = current_info .. " (enter to save)" - end - else - if key == "Return" then + if key == "BackSpace" then + pop_history() + elseif key == "Escape" 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 + to_exit = true + elseif key == "Up" or key == "Down" then + if current_cmd ~= data.cmds[cmd_index] then + data.cmds[#data.cmds] = current_cmd 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 - if data.history_file then - local file, err = io.open(data.history_file, "w") - if err then - print("cannot save history to " .. data.history_file) - else - for i = max(1, #data.cmds - data.history_save_max + 1), #data.cmds do - print("save cmd " .. data.cmds[i]) - file:write(data.cmds[i] .. "\n") + if key == "Up" and cmd_index > 1 then + cmd_index = cmd_index - 1 + elseif key == "Down" and cmd_index < #data.cmds then + cmd_index = cmd_index + 1 + end + + print("restore history #" .. tostring(cmd_index) .. ":" .. data.cmds[cmd_index]) + init(screen.workarea) + for i = 1, #data.cmds[cmd_index] do + cmd = data.cmds[cmd_index]:sub(i, i) + + push_history() + local ret = handle_command(cmd) + + current_info = current_info .. ret + current_cmd = current_cmd .. ret + end + + if #open_areas == 0 then + current_info = current_info .. " (enter to save)" + end + elseif #open_areas > 0 then + push_history() + local ret = handle_command(key) + if ret ~= nil then + current_info = current_info .. ret + current_cmd = current_cmd .. ret + else + discard_history() + end + + if #open_areas == 0 then + current_info = current_info .. " (enter to save)" + 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 end - end - - current_info = "Saved!" - to_exit = true - to_apply = true - end - end - - refresh() - - if to_exit then - print("interactive layout editing ends") - if to_apply then - layout = api.layout.get(screen) - if layout.set_regions then - local areas_with_gap = {} - for _, a in ipairs(closed_areas) do - areas_with_gap[#areas_with_gap + 1] = shrink_area_with_gap(a, gap) + for i = #data.cmds, j, -1 do + table.remove(data.cmds, i) 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 + -- bring the current cmd to the front + data.cmds[#data.cmds + 1] = current_cmd + + if data.history_file then + local file, err = io.open(data.history_file, "w") + if err then + print("cannot save history to " .. data.history_file) + else + for i = max(1, #data.cmds - data.history_save_max + 1), #data.cmds do + print("save cmd " .. data.cmds[i]) + file:write(data.cmds[i] .. "\n") end end - ) - layout.set_regions(areas_with_gap) - api.layout.arrange(screen) + end + + current_info = "Saved!" + to_exit = true + to_apply = true end - api.gears.timer{ - timeout = 1, - autostart = true, - singleshot = true, - callback = cleanup - } - else - cleanup() end - keygrabber.stop(kg) - return + + refresh() + + if to_exit then + print("interactive layout editing ends") + if to_apply then + local layout = api.layout.get(screen) + if layout.set_regions then + local areas_with_gap = {} + for _, a in ipairs(closed_areas) do + areas_with_gap[#areas_with_gap + 1] = shrink_area_with_gap(a, gap) + 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 + ) + layout.cmd = current_cmd + layout.set_regions(areas_with_gap) + api.layout.arrange(screen) + end + api.gears.timer{ + timeout = 1, + autostart = true, + singleshot = true, + callback = cleanup + } + else + cleanup() + end + keygrabber.stop(kg) + return + end + end) + end + + local function set_by_cmd(layout, screen, cmd) + init(screen.workarea) + push_history() + + for i = 1, #cmd do + local key = handle_command(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, gap) + 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 - end) + ) + layout.cmd = cmd + layout.set_regions(areas_with_gap) + api.layout.arrange(screen) + end + + local function try_restore_last(layout, screen) + local index = #data.cmds + if index == 0 then return end + + set_by_cmd(layout, screen, data.cmds[#data.cmds]) + end + + return { + start_interactive = start_interactive, + set_by_cmd = set_by_cmd, + try_restore_last = try_restore_last, + } end function restore_data(data) if data.history_file then local file, err = io.open(data.history_file, "r") if err then - print("cannot read history from " .. data.history_file) + print("cannot read history from " .. data.history_file) else data.cmds = {} for line in file:lines() do @@ -585,6 +631,6 @@ return { set_region = set_region, cycle_region = cycle_region, - start_editor = start_editor, + create = create, restore_data = restore_data, } diff --git a/layout.lua b/layout.lua index 7ed56d1..a765b95 100644 --- a/layout.lua +++ b/layout.lua @@ -32,7 +32,7 @@ function do_arrange(p, priv) end end -function create_layout(name, regions) +function create() local priv = { regions = {} } local function set_regions(regions) @@ -72,10 +72,7 @@ function create_layout(name, regions) end end - set_regions(regions) - return { - name = "machi[" .. name .. "]", arrange = function (p) do_arrange(p, priv) end, get_region_count = function () return #priv.regions end, set_regions = set_regions, @@ -85,5 +82,5 @@ function create_layout(name, regions) end return { - create_layout = create_layout, + create = create, }