set layout directly by command. restore the last layout

This commit is contained in:
Xinhao Yuan 2019-07-05 21:57:27 -04:00
parent 358c2cee18
commit 8891f371a8
3 changed files with 294 additions and 265 deletions

View File

@ -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

View File

@ -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,
}

View File

@ -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,
}