Added/Implemented machi-editor

This commit is contained in:
Mutzi 2025-03-18 16:58:19 +01:00
parent 9d91ceabcc
commit f2094af979
Signed by: root
GPG Key ID: 2437494E09F13876
13 changed files with 3737 additions and 9 deletions

13
lib/layout-machi/LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright 2019 Xinhao Yuan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

534
lib/layout-machi/README.md Normal file
View File

@ -0,0 +1,534 @@
# Fork
Personal fork, not planning to get this merged upstream as some changes
are too personalised. Just patch what you like.
Will try to make atomic commits you can easily rebase on upstream.
## Changes
### switcher
- hjkl shifts focus, super+hjkl moves (previously wasd/arrow keys)
- prevent overlapping windows as much as possible (called tabs in source)
- don't require switching with the switching UI (super+/ by default) for simple window operations like swap/tab-cycle/... (see examples)
- ui tweaks
- no transparency => no picom required
### editor
- cursor => editing of command
- ctrl-c => clear command
- ui tweaks
- contrasting bg
- highlight active area properly
### engine
- nothing
### layout
- prevent overlapping when changing layouts if new layout has less areas
- ui tweaks
- overlapping client callback (so you can theme them how you want) (see examples)
## Planned
- editor: perhaps do same changes as done in switcher but seems hardly worth it. I don't need to seem my windows while updating the layout
- rewrite/refactor: too much duplicate code, no api layer, functions with side effects, ...
## Demo
https://user-images.githubusercontent.com/823696/152007355-9a87b606-fff9-4adf-b4b2-125075358d20.mp4
## Examples
```lua
---
--- Style overlapping clients
--- I personal add a titlebar to the bottom of each client and change that color
--- since it's more robust than borders and allows setting focus/normal color.
--- Which you can't really do with borders (cleanly)
---
local theme = {
machi_style_tabbed = function (client, tabbed)
if tabbed then
client.border_color = "#ff0000"
return
end
client.border_color = "#000000"
end,
...
}
beautiful.init(theme)
---
--- Start switcher ui
---
awful.key({modkey}, "/", function () machi.switcher.start(client.focus).ui() end),
---
--- An awful.client.setmaster alternative for machi.
---
awful.key({modkey, "Control"}, "Return", function() machi.switcher.start(client.focus).master_swap(true) end),
---
--- Tab through overlapping clients (no switcher ui needed)
---
awful.key({modkey}, "Tab", function () machi.switcher.start(client.focus).tab() end),
---
--- Move by direction (no switcher ui needed)
---
awful.key({modkey, "Shift"}, "h", function() machi.switcher.start(client.focus).move("left") end),
awful.key({modkey, "Shift"}, "j", function() machi.switcher.start(client.focus).move("down") end),
awful.key({modkey, "Shift"}, "k", function() machi.switcher.start(client.focus).move("up") end),
awful.key({modkey, "Shift"}, "l", function() machi.switcher.start(client.focus).move("right") end),
---
--- Focus by direction (no switcher ui needed)
---
awful.key({modkey}, "h", function() machi.switcher.start(client.focus).focus("left") end),
awful.key({modkey}, "j", function() machi.switcher.start(client.focus).focus("down") end),
awful.key({modkey}, "k", function() machi.switcher.start(client.focus).focus("up") end),
awful.key({modkey}, "l", function() machi.switcher.start(client.focus).focus("right") end),
---
--- Focus by direction (what I use)
---
function focus(dir)
-- focus on the current screen (floating windows are ignored)
if machi.switcher.start(client.focus).focus(dir) then
return
end
-- we're at the edge of the screen
-- might focus a floating window
-- or switch to a screen in the given direction
awful.client.focus.global_bydirection(dir)
local c = client.focus
local screen = awful.screen.focused()
if c ~= nil and c.screen ~= screen then
awful.screen.focus_bydirection(dir)
end
end
awful.key({modkey}, "h", function() focus("left") end),
awful.key({modkey}, "j", function() focus("down") end),
awful.key({modkey}, "k", function() focus("up") end),
awful.key({modkey}, "l", function() focus("right") end),
--
-- Cycle predefined layouts
--
awful.key({ modkey, }, "z", function() nextMachiLayout() end),
function setMachiLayout(cmd)
local screen = awful.screen.focused()
local tag = screen.selected_tags[1]
machi_layout.machi_set_cmd(cmd, tag, true)
end
local machi_layouts = {
"v1,4t3h1,4,1-h1,1cct3h1,4,1-t3v2.",
"v13h1221h1331.",
}
local machi_layout_n = 0
function nextMachiLayout()
machi_layout_n = machi_layout_n + 1
if machi_layout_n > #machi_layouts then
machi_layout_n = 1
end
local l = machi_layouts[machi_layout_n]
setMachiLayout(l)
end
--
-- Swap by direction (credits: @basaran https://github.com/xinhaoyuan/layout-machi/issues/13)
-- Inferior to moving imo but prevents overlapping.
--
function swap(dir)
local cltbl = awful.client.visible(client.focus.screen, true)
local grect = require("gears.geometry").rectangle
local geomtbl = {}
for i,cl in ipairs(cltbl) do
geomtbl[i] = cl:geometry()
end
local target = grect.get_in_direction(dir, geomtbl, client.focus:geometry())
tobe = cltbl[target]:geometry()
is = client.focus:geometry()
client.focus:geometry(tobe)
cltbl[target]:geometry(is)
end
awful.key({ modkey, "Shift" }, "h", function() swap("left") end),
awful.key({ modkey, "Shift" }, "j", function() swap("down") end),
awful.key({ modkey, "Shift" }, "k", function() swap("up") end),
awful.key({ modkey, "Shift" }, "l", function() swap("right") end),
```
Original readme:
# ![machi icon](icon.png) layout-machi
A manual layout for Awesome with a rapid interactive editor.
Demos: https://imgur.com/a/OlM60iw
Draft mode: https://imgur.com/a/BOvMeQL
__`ng` is merged into `master`. Checkout `legacy` tag for the previous master checkpoint.__
## 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.
4. There is no more per-layout setting of "draft mode". Every window has its own setting.
### New features & enhancements
1. Areas are protected by a minimum size (not configurable for now).
2. More tolerating "safer" error handling. If the screen cannot fit the minimum size of the layout, areas out of the screen will be hidden, but it will not crash the layout logic.
3. Dynamic size adjustment with propagation.
4. Editor can be used on areas instead of entire screens.
## Why?
TL;DR --- To bring back the control of the window layout.
1. Dynamic tiling can be an overkill, since tiling is only useful for persistent windows, and people extensively use hibernate/sleep these days.
2. Having window moving around can be annoying whenever a new window shows up.
3. I want a flexible layout such that I can quickly adjust to whatever I need.
## Compatibilities
I developed it with Awesome git version. Hopefully it works with 4.3 stable.
Please let me know if it does not work in 4.3 or older versions.
## Really quick usage
See `rc.patch` for adding layout-machi to the default 4.3 config.
## Quick usage
Suppose this git is checked out at `~/.config/awesome/layout-machi`
Use `local machi = require("layout-machi")` to load the module.
The package provide a default layout `machi.default_layout` and editor `machi.default_editor`, which can be added into the layout list.
The package comes with the icon for `layoutbox`, which can be set with the following statement (after a theme has been loaded):
`require("beautiful").layout_machi = machi.get_icon()`
By default, any machi layout will use the layout command from `machi.layout.default_cmd`, which is initialized as `w66.` (see interpretation below).
You can change it after loading the module.
## Use the layout
Use `local layout = machi.layout.create(args)` to instantiate the layout with an editor object. `args` is a table of arguments, where the followings can be used:
- `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`).
- `new_placement_cb`: a callback `function(c, instance, areas, geometry)` that fits new client `c` into the areas.
This is a new and experimental feature. The interface is subject to changes.
If `name` and `name_func` are both nil, a default name function will be used, which depends on the tag names, screen geometries, and `icon_name`.
The function is compatible with the previous `machi.layout.create(name, editor, default_cmd)` calls.
For `new_placement_cb` the arguments are:
- `c`: the new client to be placed.
- `instance`: a layout and tag depedent table with the following fields available:
- `cmd`: the current layout command.
- `client_data`: a mapping from previously managed clients to their layout related settings and assigned areas.
Note that it may contain some clients that are no longer in the layout. You can filter them using `screen.tiled_clients`.
Each entry is a table with fields:
- `.placement`: If true, the client has been placed by the layout, otherwise `new_placement_cb` will be called on the client in the further.
- `.area`: If it is non-nil, the window is fit in the area.
- `.lu`, `.rd`: If those are non-nil, the window is in draft mode and the fields are for the areas of its corners.
- `.draft`: if non-nil, this is the overriding perference of draft mode for the window.
- `tag_data`: a mapping from area ids to their fake tag data. This is for nested layouts.
- `areas`: the current array of areas produced by `instance.cmd`. Each area is a table with the following fields available:
- `id`: self index of the array.
- `habitable`: if true, the area is for placing windows. It could be false for a parent area, or an area disabled by command `/`.
- `x`, `y`, `width`, `height`: area geometry.
- `layout`: the string used to index the nested layout, if any.
- `geometry`: the output table the client geometry. Note that the geometry _includes_ the borders.
The callback places the new client by changing its geometry and client data.
Note that after the callback machi will validate the geometry and fit into the areas.
So no need to set the `.area`, `.lu`, or `.rd` field of the client data in the callback.
For example, to place new client in the largest area among empty areas, create the layout with
```
machi.layout.create{ new_placement_cb = machi.layout.placement.empty }
```
## The layout editor and commands
### Starting editor in lua
Call `local editor = machi.editor.create()` to create an editor.
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 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. If `Shift` is hold, restores to the current (maybe transcoded) command of the layout.
3. `Escape`: exit the editor without saving 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
As aforementioned, command a sequence of operations.
There are three kinds of operations:
1. Operations taking argument string and parsed as multiple numbers.
- `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 or string.
- `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 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.
Each operation may take argument string either from before (such as `22w`) or after (such as `w22`).
When any ambiguity arises, operation before always take the argument after. So `h11v` is interpreted as `h11` and `v`.
For examples:
`h-v`
```
11 22
11 22
11
11 33
11 33
```
`hvv` (or `22w`)
```
11 33
11 33
22 44
22 44
```
`131h2v-12v`
Details:
- `131h`: horizontally split the initial area (entire desktop) to the ratio of 1:3:1
- For the first `1` part:
- `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
Tada!
```
11 3333 44
11 3333 44
11 3333
11 3333 55
3333 55
22 3333 55
22 3333 55
```
`12210121d`
```
11 2222 3333 44
11 2222 3333 44
55 6666 7777 88
55 6666 7777 88
55 6666 7777 88
55 6666 7777 88
99 AAAA BBBB CC
99 AAAA BBBB CC
```
### Advanced grid layout
__More document coming soon. For now there is only a running example.__
Simple grid, `w44`:
```
0 1 2 3
4 5 6 7
8 9 A B
C D E F
```
Merge grid from the top-left corner, size 3x1, `w4431`:
```
0-0-0 1
2 3 4 5
6 7 8 9
A B C D
```
Another merge, size 1x3, `w443113`:
```
0-0-0 1
|
2 3 4 1
|
5 6 7 1
8 9 A B
```
Another merge, size 1x3, `w44311313`:
```
0-0-0 1
|
2 3 4 1
| |
2 5 6 1
|
2 7 8 9
```
Another merge, size 2x2, `w4431131322`:
```
0-0-0 1
|
2 3-3 1
| | | |
2 3-3 1
|
2 4 5 6
```
Final merge, size 3x1, `w443113132231`:
```
0-0-0 1
|
2 3-3 1
| | | |
2 3-3 1
|
2 4-4-4
```
`d` command works similarly after the inital grid is defined, such as `d1221012210221212121222`.
### Draft mode
Unlike the regular placement, where a window fits in a single area, windows in draft mode can span across multiple areas.
Each drafting 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.
Draft mode is suppose to work well with grid areas (produced by `d` or `w` operations), but it is not limited to those.
Draft mode is enabled for a newly placed window if
(a) `new_placement_cb` returns so, or
(b) `new_placement_cb` is unspecified and the window's UL and BR corners fit different areas.
Resizing a window to a single area disables drafting, otherwise resizing across areas enables drafting.
You can also use `f` or `.` key in switcher UI to manually cycle through modes despit how the window previously spans areas.
### Nested layouts
__This feature is a toy. It may come with performance and usability issues - you have been warned.__
Known caveats include:
1. `arrange()` of the nested layouts are always called when the machi `arrange()` is called. This could be optimized with caching.
2. `client.*wfact` and other layout related operations don't work as machi fakes tag data to the nested layout engine.
But it hopefully works if one changes the fields in the faked tag data.
__This feature is not available for windows 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 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.
![nested layout screenshot](nested_layout_screenshot.png)
### Persistent history
By default, the last 100 command sequences are stored in `.cache/awesome/history_machi`.
To change that, please refer to `editor.lua`. (XXX more documents)
## Switcher
Calling `machi.switcher.start()` will create a switcher supporting the following keys:
- 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.
- `q` or `PageUp` (`Prior`): select the parent of the current area. Hold `Control` to resize the current window accordingly.
- `e` or `PageDown` (`Next`): select the previous child of the current area, if `q` or `PageUp` was used. Hold `Control` to resize the current window accordingly.
- `f` or `.`: toggle the per-window setting of draft mode.
- `/`: open 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
A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients.
## License
Apache 2.0 --- See LICENSE

672
lib/layout-machi/editor.lua Normal file
View File

@ -0,0 +1,672 @@
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
local INFO = 0
local DEBUG = -1
local module = {
log_level = WARNING,
nested_layouts = {
["0"] = awful.layout.suit.tile,
["1"] = awful.layout.suit.spiral,
["2"] = awful.layout.suit.fair,
["3"] = awful.layout.suit.fair.horizontal,
},
}
local function log(level, msg)
if level > module.log_level then
print(msg)
end
end
local function with_alpha(col, alpha)
local r, g, b
_, r, g, b, _ = col:get_rgba()
return lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
end
local function max(a, b)
if a < b then return b else return a end
end
local function is_tiling(c)
return
not (c.tomb_floating or c.floating or c.maximized_horizontal or c.maximized_vertical or c.maximized or c.fullscreen)
end
local function set_tiling(c)
c.floating = false
c.maximized = false
c.maximized_vertical = false
c.maximized_horizontal = false
c.fullscreen = false
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, 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)
if data.history_file then
local file, err = io.open(data.history_file, "r")
if err then
log(INFO, "cannot read history from " .. data.history_file)
else
data.cmds = {}
data.last_cmd = {}
local last_layout_name
for line in file:lines() do
if line:sub(1, 1) == "+" then
last_layout_name = line:sub(2, #line)
else
if last_layout_name ~= nil then
log(DEBUG, "restore last cmd " .. line .. " for " .. last_layout_name)
data.last_cmd[last_layout_name] = line
last_layout_name = nil
else
log(DEBUG, "restore cmd " .. line)
data.cmds[#data.cmds + 1] = line
end
end
end
file:close()
end
end
return data
end
function module.create(data)
if data == nil then
data = module.restore_data({
history_file = gfs.get_cache_dir() .. "/history_machi",
history_save_max = 100,
})
end
data.cmds = data.cmds or {}
data.last_cmd = data.last_cmd or {}
data.minimum_size = data.minimum_size or 100
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 = #data.cmds, j, -1 do
table.remove(data.cmds, i)
end
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
if data.cmds[i] ~= "." then
log(DEBUG, "save cmd " .. data.cmds[i])
file:write(data.cmds[i] .. "\n")
end
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
return true
end
local function start_interactive(screen, embed_args)
local info_size = dpi(60)
-- colors are in rgba
local border_color = with_alpha(
gears.color(beautiful.machi_editor_border_color or beautiful.border_focus),
beautiful.machi_editor_border_opacity or 0.75)
local active_color = with_alpha(
gears.color(beautiful.machi_editor_active_color or beautiful.bg_focus),
beautiful.machi_editor_active_opacity or 0.5)
local open_color = with_alpha(
gears.color(beautiful.machi_editor_open_color or beautiful.bg_normal),
beautiful.machi_editor_open_opacity or 0.5)
local done_color = with_alpha(
gears.color(beautiful.machi_editor_done_color or beautiful.bg_focus),
beautiful.machi_editor_done_opacity or 0.5)
local closed_color = open_color
if to_save == nil then
to_save = true
end
screen = screen or awful.screen.focused()
local tag = screen.selected_tag
local prev_layout = nil
if tag.layout.machi_set_cmd == nil then
prev_layout = tag.layout
for _, l in ipairs(awful.layout.layouts) do
if l.machi_set_cmd ~= nil then
awful.layout.set(l, tag)
naughty.notify {
text = 'Switched layout to machi',
timeout = 2
}
end
end
end
local gap = tag.gap or 0
local layout = tag.layout
if layout.machi_set_cmd == nil then
naughty.notify {
text = "The layout to edit is not machi",
timeout = 3
}
return
end
local cmd_index = #data.cmds + 1
data.cmds[cmd_index] = ""
local start_x = screen.workarea.x
local start_y = screen.workarea.y
local kg
local infobox = wibox({
screen = screen,
x = screen.workarea.x,
y = screen.workarea.y,
width = screen.workarea.width,
height = screen.workarea.height,
bg = "#ffffff00",
opacity = 1,
ontop = true,
type = "dock",
})
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 curpos = 0
local current_info_pre = ""
local current_info_post = ""
local current_msg
local function set_cmd(cmd, arbitrary_input)
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 or not arbitrary_input then
if new_closed_areas then
closed_areas, open_areas, pending_op =
new_closed_areas, new_open_areas, new_pending_op
end
current_cmd = cmd
current_info_pre = current_cmd:sub(0, curpos)
current_info_post = current_cmd:sub(curpos+1, #current_cmd)
if embed_args then
current_info_pre = embed_args.cmd_prefix.."["..current_info_pre
current_info_post = current_info_post.."]"..embed_args.cmd_suffix
end
current_msg = ""
if new_closed_areas and #open_areas == 0 and not pending_op then
current_msg = "(enter to apply)"
end
return true
else
return false
end
end
local move_cursor = function(n)
curpos = curpos + n
if curpos > #current_cmd then
curpos = #current_cmd
elseif curpos < 0 then
curpos = 0
end
-- trigger refresh
set_cmd(current_cmd)
end
local function handle_key(key)
if key_translate_tab[key] ~= nil then
key = key_translate_tab[key]
end
return set_cmd(current_cmd:sub(0, curpos)..key..current_cmd:sub(curpos+1, #current_cmd), true)
end
local function cleanup()
infobox.visible = false
if prev_layout ~= nil then
awful.layout.set(prev_layout, tag)
end
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
if a.habitable 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(done_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
end
for i, a in ipairs(open_areas) do
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)
end
cr:reset_clip()
end
local pl = lgi.Pango.Layout.create(cr)
pl:set_font_description(beautiful.get_merged_font(beautiful.font, info_size))
pl:set_alignment("CENTER")
pl:set_text(current_info_pre)
local w0, _ = pl:get_pixel_size()
pl:set_text(current_info_pre..current_info_post)
local w, h = pl:get_pixel_size()
local pl_msg = lgi.Pango.Layout.create(cr)
pl_msg:set_font_description(beautiful.get_merged_font(beautiful.font, info_size))
pl_msg:set_alignment("CENTER")
pl_msg:set_text(current_msg)
local w_msg, h_msg = pl_msg:get_pixel_size()
local lh = pl_msg:get_line_spacing()
local draw = function(pl, w, h, y_offset, color)
local ext = { width = w, height = h, x_bearing = 0, y_bearing = 0 }
cr:move_to(
width / 2 - ext.width / 2 - ext.x_bearing,
y_offset + height / 2 - ext.height / 2 - ext.y_bearing
)
if color then
cr:set_source(color)
else
cr:set_source_rgba(1, 1, 1, 1)
end
cr:show_layout(pl)
cr:fill()
end
local wpad, hpad = dpi(50), dpi(5)
local mw, mh = max(w, w_msg) + wpad, h + hpad
if current_msg ~= "" then
mh = mh + h_msg + lh
end
if mw < dpi(120) then
mw = dpi(120)
end
cr:rectangle(width / 2 - mw / 2, height / 2 - (h + hpad) / 2, mw, mh)
cr:set_source_rgba(0, 0, 0, 1)
cr:fill()
local cursor_border = 0
local cursor_width = 1
if cursor_border >= 0 then
cr:rectangle(
width / 2 - w / 2 + w0,
height / 2 - h / 2 + lh,
2 * cursor_border + cursor_width,
h
)
cr:set_source_rgba(0, 0, 0, 0.8)
cr:fill()
end
cr:rectangle(
cursor_border + width / 2 - w / 2 + w0,
cursor_border + height / 2 - h / 2 + lh,
cursor_width,
h - 2 * cursor_border
)
cr:set_source_rgba(1, 1, 1, 1)
cr:fill()
draw(pl, w, h, lh)
draw(pl_msg, w_msg, h_msg, h + lh)
end
local function refresh()
log(DEBUG, "closed areas:")
for i, a in ipairs(closed_areas) do
log(DEBUG, " " .. _area_tostring(a))
end
log(DEBUG, "open areas:")
for i, a in ipairs(open_areas) do
log(DEBUG, " " .. _area_tostring(a))
end
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")
set_cmd("")
refresh()
kg = awful.keygrabber.run(
function (mod, key, event)
if event == "release" then
return
end
local ok, err = pcall(
function ()
if key == "Left" then
move_cursor(-1)
elseif key == "Right" then
move_cursor(1)
elseif key == "BackSpace" then
local alt = false
for _, m in ipairs(mod) do
if m == "Shift" then
alt = true
break
end
end
if alt then
if embed_args then
set_cmd(embed_args.original_cmd or "")
else
local _cd, _td, areas = layout.machi_get_instance_data(screen, tag)
set_cmd(machi_engine.areas_to_command(areas))
end
else
local s = curpos - 1
if s < 0 then
s = 0
end
set_cmd(current_cmd:sub(0, s)..current_cmd:sub(curpos+1, #current_cmd))
move_cursor(-1)
end
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
log(DEBUG, "restore history #" .. tostring(cmd_index) .. ":" .. data.cmds[cmd_index])
set_cmd(data.cmds[cmd_index])
move_cursor(#data.cmds[cmd_index])
elseif #open_areas > 0 or pending_op or curpos < #current_cmd then
if key == "." or key == "Return" then
move_cursor(#current_cmd)
end
if handle_key(key) then
move_cursor(1)
end
else
if key == "Return" then
local alt = false
for _, m in ipairs(mod) do
if m == "Shift" then
alt = true
break
end
end
local instance_name, persistent = layout.machi_get_instance_info(tag)
if not alt and persistent then
table.remove(data.cmds, #data.cmds)
add_cmd(instance_name, get_final_cmd())
current_msg = "Saved!"
else
current_msg = "Applied"
end
to_exit = true
to_apply = true
end
end
refresh()
if to_exit then
log(DEBUG, "interactive layout editing ends")
if to_apply then
layout.machi_set_cmd(get_final_cmd(), tag)
awful.layout.arrange(screen)
gears.timer{
timeout = 1,
autostart = true,
singleshot = true,
callback = cleanup,
}
else
cleanup()
end
end
end)
if not ok then
log(ERROR, "Getting error in keygrabber: " .. err)
to_exit = true
cleanup()
end
if to_exit then
awful.keygrabber.stop(kg)
end
end
)
end
local function run_cmd(cmd, screen, tag)
local gap = tag.gap
local areas, closed = 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 or #closed > 0 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 then
return
end
local screen = c.screen
local tag = screen.selected_tag
local layout = tag.layout
if not layout.machi_get_instance_data then return end
local cd, _td, areas = layout.machi_get_instance_data(screen, tag)
local key_shares = axis.."_shares"
local key_spare = axis.."_spare"
local key_parent_shares = "parent_"..axis.."_shares"
if not cd[c] or not cd[c].area then
return
end
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 = cd[c].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, true)
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,
adjust_x_shares = adjust_x_shares,
adjust_y_shares = adjust_y_shares,
}
end
module.default_editor = module.create()
return module

942
lib/layout-machi/engine.lua Normal file
View File

@ -0,0 +1,942 @@
-- area {
-- x, y, width, height
-- parent_id
-- parent_cid
-- parent_x_shares
-- parent_y_shares
-- habitable
-- hole (unique)
-- }
--
-- 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
local need_recompute
repeat
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 b = require("beautiful")
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
a.habitable = true
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.habitable = false
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.habitable = false
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 j = s, e do r[#r+1] = j 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().habitable = false
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
local 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)
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].habitable = false
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].habitable = false
end
end
return closed_areas, open_areas, pending_op ~= nil
end
local function areas_to_command(areas, to_embed, root_area)
root_area = root_area or 1
if #areas < root_area 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] or share[2] == 0 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 == nil then
return ""
end
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
r = method..r..(#m == 0 and m or (method == "w" and "," or ",,"))..m
end
local acc_dashes = 0
if a.expansion > 1 then
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
end
if area_id ~= root_area 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
elseif a.disabled then
r = "/"
elseif a.layout then
r = "x"..a.layout
else
r = "-"
end
return r
end
local r = get_command(root_area)
if not to_embed then
if r == "-" then
r = "."
else
-- The last . may be redundant, but it makes sure no pending op.
r = r:gsub("[\\c]+$", "").."."
end
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(".", ".")
check_transcoded_command("3t.", ".")
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.", "t3dw6,6.")
print("Passed.")
end
return {
areas_from_command = areas_from_command,
areas_to_command = areas_to_command,
}

BIN
lib/layout-machi/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

33
lib/layout-machi/init.lua Normal file
View File

@ -0,0 +1,33 @@
local engine = require(... .. ".engine")
local layout = require(... .. ".layout")
local editor = require(... .. ".editor")
local switcher = require(... .. ".switcher")
local default_editor = editor.default_editor
local default_layout = layout.create{ name_func = default_name }
local gcolor = require("gears.color")
local beautiful = require("beautiful")
local icon_raw
local source = debug.getinfo(1, "S").source
if source:sub(1, 1) == "@" then
icon_raw = source:match("^@(.-)[^/]+$") .. "icon.png"
end
local function get_icon()
if icon_raw ~= nil then
return gcolor.recolor_image(icon_raw, beautiful.fg_normal)
else
return nil
end
end
return {
engine = engine,
layout = layout,
editor = editor,
switcher = switcher,
default_editor = default_editor,
default_layout = default_layout,
icon_raw = icon_raw,
get_icon = get_icon,
}

647
lib/layout-machi/layout.lua Normal file
View File

@ -0,0 +1,647 @@
local this_package = ... and (...):match("(.-)[^%.]+$") or ""
local machi_editor = require(this_package.."editor")
local awful = require("awful")
local gobject = require("gears.object")
local capi = {
screen = screen
}
local ERROR = 2
local WARNING = 1
local INFO = 0
local DEBUG = -1
local module = {
log_level = WARNING,
global_default_cmd = "w66.",
allow_shrinking_by_mouse_moving = false,
}
local function log(level, msg)
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
end
local function max(a, b)
if a < b then return b else return a end
end
local function get_screen(s)
return s and capi.screen[s]
end
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 unique (read empty) area for the area-like object
--- unless all spots are taken.
-- @param c area-like object - table with properties x, y, width, and height
-- @param areas array of area objects
-- @param uniq array that will keep track of matched (read: non empty) areas
-- @return the index of the best area
local function find_uniq_area(c, areas, uniq)
local choice = 1
local choice_value = nil
local room = false
for i, a in ipairs(areas) do
if not uniq[i] then
room = true
break
end
end
for i, a in ipairs(areas) do
if a.habitable and (not room or (room and not uniq[i])) 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
if choice_value == nil or choice_value < cap then
choice = i
choice_value = cap
end
end
end
uniq[choice] = true
return choice
end
--- find the best area for the area-like object
-- @param c area-like object - table with properties x, y, width, and height
-- @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 a.habitable 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)
end
local function find_lu(c, areas, rd)
local lu = nil
for i, a in ipairs(areas) do
if a.habitable 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, border_width, areas, lu)
local x, y
x = c.x + c.width + (border_width or 0) * 2
y = c.y + c.height + (border_width or 0) * 2
local rd = nil
for i, a in ipairs(areas) do
if a.habitable 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, 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 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
-- TODO: the string need to be updated when its screen geometry changed.
local function get_machi_tag_string(tag)
return tostring(tag.screen.geometry.width) .. "x" .. tostring(tag.screen.geometry.height) .. "+" ..
tostring(tag.screen.geometry.x) .. "+" .. tostring(tag.screen.geometry.y) .. '+' .. tag.name
end
function module.create(args_or_name, editor, default_cmd)
local args
if type(args_or_name) == "string" then
args = {
name = args_or_name
}
elseif type(args_or_name) == "function" then
args = {
name_func = args_or_name
}
elseif type(args_or_name) == "table" then
args = args_or_name
else
return nil
end
if args.name == nil and args.name_func == nil then
local prefix = args.icon_name and (args.icon_name.."-") or ""
args.name_func = function (tag)
return prefix..get_machi_tag_string(tag)
end
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
local layout = {}
local instances = {}
local function get_instance_info(tag)
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,
areas_cache = {},
tag_data = {},
client_data = setmetatable({}, {__mode="k"}),
}
if instances[name].cmd == nil then
instances[name].cmd = args.default_cmd
end
end
return instances[name]
end
local function get_instance_data(screen, tag)
if screen == nil then return end
local workarea = screen.workarea
local instance = get_instance_(tag)
local cmd = instance.cmd or module.global_default_cmd
if cmd == nil then return 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)
if instance.areas_cache[key] == nil then
return
end
end
return instance.client_data, instance.tag_data, instance.areas_cache[key], instance, args.new_placement_cb
end
local function set_cmd(cmd, tag, keep_instance_data)
local instance = get_instance_(tag)
if instance.cmd ~= cmd then
instance.cmd = cmd
instance.areas_cache = {}
tag:emit_signal("property::layout")
if not keep_instance_data then
instance.tag_data = {}
instance.client_data = setmetatable({}, {__mode="k"})
end
end
end
local clean_up
local tag_data = setmetatable({}, {__mode = "k"})
clean_up = function (tag)
local screen = tag.screen
if not screen then return end
local _cd, _td, _areas, instance, _new_placement_cb = get_instance_data(screen, tag)
if tag_data[tag].regsitered then
tag_data[tag].regsitered = false
tag:disconnect_signal("property::layout", clean_up)
tag:disconnect_signal("property::selected", clean_up)
for _, tag in pairs(instance.tag_data) do
tag:emit_signal("property::layout")
end
end
end
clean_up_on_selected_change = function (tag)
if not tag.selected then clean_up(tag) end
end
local function arrange(p)
local useless_gap = p.useless_gap
local screen = get_screen(p.screen)
local cls = p.clients
local tag = p.tag or screen.selected_tag
local cd, td, areas, instance, new_placement_cb = get_instance_data(screen, tag)
if not tag_data[tag] then tag_data[tag] = {} end
if not tag_data[tag].registered then
tag_data[tag].regsitered = true
tag:connect_signal("property::layout", clean_up)
tag:connect_signal("property::selected", clean_up)
end
if areas == nil then return end
local nested_clients = {}
local function place_client_in_area(c, area)
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
p.geometries[c] = {}
module.set_geometry(p.geometries[c], areas[area], areas[area], useless_gap, 0)
end
end
-- Make clients calling new_placement_cb appear in the end.
local j = 0
for i = 1, #cls do
cd[cls[i]] = cd[cls[i]] or {}
if cd[cls[i]].placement then
j = j + 1
cls[j], cls[i] = cls[i], cls[j]
end
end
local empty_areas = {}
for i, c in ipairs(cls) do
if c.floating or c.immobilized then
log(DEBUG, "Ignore client " .. tostring(c))
else
local geo = {
x = c.x,
y = c.y,
width = c.width + c.border_width * 2,
height = c.height + c.border_width * 2,
}
if not cd[c].placement and new_placement_cb then
cd[c].placement = true
new_placement_cb(c, instance, areas, geo)
end
local in_draft = cd[c].draft
if cd[c].draft ~= nil then
in_draft = cd[c].draft
elseif cd[c].lu then
in_draft = true
elseif cd[c].area then
in_draft = false
else
in_draft = nil
end
local skip = false
if in_draft ~= false then
if cd[c].lu ~= nil and cd[c].rd ~= nil and
cd[c].lu <= #areas and cd[c].rd <= #areas and
areas[cd[c].lu].habitable and areas[cd[c].rd].habitable
then
if areas[cd[c].lu].x == geo.x and
areas[cd[c].lu].y == geo.y and
areas[cd[c].rd].x + areas[cd[c].rd].width == geo.x + geo.width and
areas[cd[c].rd].y + areas[cd[c].rd].height == geo.y + geo.height
then
skip = true
end
end
local lu = nil
local rd = nil
if not skip then
log(DEBUG, "Compute areas for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
lu = find_lu(geo, areas)
if lu ~= nil then
geo.x = areas[lu].x
geo.y = areas[lu].y
rd = find_rd(geo, 0, areas, lu)
end
end
if lu ~= nil and rd ~= nil then
if lu == rd and cd[c].lu == nil then
cd[c].area = lu
place_client_in_area(c, lu)
else
cd[c].lu = lu
cd[c].rd = rd
cd[c].area = nil
p.geometries[c] = {}
module.set_geometry(p.geometries[c], areas[lu], areas[rd], useless_gap, 0)
end
end
else
if cd[c].area ~= nil and
cd[c].area <= #areas and
areas[cd[c].area].habitable and
areas[cd[c].area].layout == nil and
areas[cd[c].area].x == geo.x and
areas[cd[c].area].y == geo.y and
areas[cd[c].area].width == geo.width and
areas[cd[c].area].height == geo.height
then
skip = true
else
log(DEBUG, "Compute areas for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
local area = find_uniq_area(geo, areas, empty_areas)
cd[c].area, cd[c].lu, cd[c].rd = area, nil, nil
place_client_in_area(c, area)
end
end
if skip then
if geo.x ~= c.x or geo.y ~= c.y or
geo.width ~= c.width + c.border_width * 2 or
geo.height ~= c.height + c.border_width * 2 then
p.geometries[c] = {}
module.set_geometry(p.geometries[c], geo, geo, useless_gap, 0)
end
end
end
end
local arranged_area = {}
local function arrange_nested_layout(area, clients)
local nested_layout = machi_editor.nested_layouts[areas[area].layout]
if not nested_layout then return end
if td[area] == nil then
local tag = gobject{}
td[area] = tag
-- TODO: Make the default more flexible.
tag.layout = nested_layout
tag.column_count = 1
tag.master_count = 1
tag.master_fill_policy = "expand"
tag.gap = 0
tag.master_width_factor = 0.5
tag._private = {
awful_tag_properties = {
},
}
end
local nested_params = {
tag = td[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 = {},
}
nested_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
for area, clients in pairs(nested_clients) do
arranged_area[area] = true
arrange_nested_layout(area, clients)
end
-- Also rearrange empty nested layouts.
-- TODO Iterate through only if the area has a nested layout
for area, data in pairs(areas) do
if not arranged_area[area] and areas[area].layout then
arrange_nested_layout(area, {})
end
end
local b = require("beautiful")
local style_tabbed = b.machi_style_tabbed
if style_tabbed == nil then
return
end
local area_client_count = {}
for _, oc in ipairs(screen.tiled_clients) do
local cd = instance.client_data[oc]
if cd and cd.placement and cd.area then
if area_client_count[cd.area] == nil then
area_client_count[cd.area] = {}
end
table.insert(area_client_count[cd.area], oc)
end
end
for i, v in pairs(area_client_count) do
local tabbed = #v > 1
for _, c in pairs(v) do
style_tabbed(c, tabbed)
end
end
end
local function resize_handler (c, context, h)
local tag = c.screen.selected_tag
local instance = get_instance_(tag)
local cd = instance.client_data
local cd, td, areas, _placement_cb = get_instance_data(c.screen, tag)
if areas == nil then return end
if context == "mouse.move" then
local in_draft = cd[c].draft
if cd[c].draft ~= nil then
in_draft = cd[c].draft
elseif cd[c].lu then
in_draft = true
elseif cd[c].area then
in_draft = false
else
log(ERROR, "Assuming in_draft for unhandled client "..tostring(c))
in_draft = true
end
if in_draft then
local lu = find_lu(h, areas)
local rd = nil
if lu ~= nil 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, 0, areas, lu)
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
if lu ~= nil and rd ~= nil then
cd[c].lu = lu
cd[c].rd = rd
cd[c].area = nil
module.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
end
end
else
local center_x = h.x + h.width / 2
local center_y = h.y + h.height / 2
local choice = nil
local choice_value = nil
for i, a in ipairs(areas) do
if a.habitable then
local ac_x = a.x + a.width / 2
local ac_y = a.y + a.height / 2
local dis = (ac_x - center_x) * (ac_x - center_x) + (ac_y - center_y) * (ac_y - center_y)
if choice_value == nil or choice_value > dis then
choice = i
choice_value = dis
end
end
end
if choice and cd[c].area ~= choice then
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = choice
module.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
end
end
elseif cd[c].draft ~= false then
local lu = find_lu(h, areas)
local rd = nil
if lu ~= nil then
local hh = {}
hh.x = h.x
hh.y = h.y
hh.width = h.width
hh.height = h.height
rd = find_rd(hh, c.border_width, areas, lu)
end
if lu ~= nil and rd ~= nil then
if lu == rd and cd[c].draft ~= true then
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = lu
awful.layout.arrange(c.screen)
else
cd[c].lu = lu
cd[c].rd = rd
cd[c].area = nil
module.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
end
end
end
end
layout.name = args.icon_name or "machi"
layout.arrange = arrange
layout.resize_handler = resize_handler
layout.machi_editor = args.editor
layout.machi_get_instance_info = get_instance_info
layout.machi_get_instance_data = get_instance_data
layout.machi_set_cmd = set_cmd
return layout
end
module.placement = {}
local function empty_then_maybe_fair(c, instance, areas, geometry, do_fair)
local area_client_count = {}
for _, oc in ipairs(c.screen.tiled_clients) do
local cd = instance.client_data[oc]
if cd and cd.placement and cd.area then
area_client_count[cd.area] = (area_client_count[cd.area] or 0) + 1
end
end
local choice_client_count = nil
local choice_spare_score = nil
local choice = nil
for i = 1, #areas do
local a = areas[i]
if a.habitable then
-- +1 for the new client
local client_count = (area_client_count[i] or 0) + 1
local spare_score = a.width * a.height / client_count
if choice == nil or (choice_client_count > 1 and client_count == 1) then
choice_client_count = client_count
choice_spare_score = spare_score
choice = i
elseif (choice_client_count > 1) == (client_count > 1) and choice_spare_score < spare_score then
choice_client_count = client_count
choice_spare_score = spare_score
choice = i
end
end
end
if choice_client_count > 1 and not do_fair then
return
end
instance.client_data[c].lu = nil
instance.client_data[c].rd = nil
instance.client_data[c].area = choice
geometry.x = areas[choice].x
geometry.y = areas[choice].y
geometry.width = areas[choice].width
geometry.height = areas[choice].height
end
function module.placement.empty(c, instance, areas, geometry)
empty_then_maybe_fair(c, instance, areas, geometry, false)
end
function module.placement.empty_then_fair(c, instance, areas, geometry)
empty_then_maybe_fair(c, instance, areas, geometry, true)
end
return module

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

38
lib/layout-machi/rc.patch Normal file
View File

@ -0,0 +1,38 @@
--- /usr/etc/xdg/awesome/rc.lua 2019-10-02 22:20:36.000000000 -0400
+++ rc.lua 2019-10-06 12:13:41.090197230 -0400
@@ -17,6 +17,7 @@
-- Enable hotkeys help widget for VIM and other apps
-- when client with a matching name is opened:
require("awful.hotkeys_popup.keys")
+local machi = require("layout-machi")
-- {{{ Error handling
-- Check if awesome encountered an error during startup and fell back to
@@ -34,6 +35,8 @@
-- Themes define colours, icons, font and wallpapers.
beautiful.init(gears.filesystem.get_themes_dir() .. "default/theme.lua")
+beautiful.layout_machi = machi.get_icon()
+
-- This is used later as the default terminal and editor to run.
terminal = "xterm"
editor = os.getenv("EDITOR") or "nano"
@@ -48,6 +51,7 @@
-- Table of layouts to cover with awful.layout.inc, order matters.
awful.layout.layouts = {
+ machi.default_layout,
awful.layout.suit.floating,
awful.layout.suit.tile,
awful.layout.suit.tile.left,
@@ -262,6 +266,10 @@
awful.key({ modkey, "Shift" }, "q", awesome.quit,
{description = "quit awesome", group = "awesome"}),
+ awful.key({ modkey, }, ".", function () machi.default_editor.start_interactive() end,
+ {description = "edit the current layout if it is a machi layout", group = "layout"}),
+ awful.key({ modkey, }, "/", function () machi.switcher.start(client.focus) end,
+ {description = "switch between windows for a machi layout", group = "layout"}),
awful.key({ modkey, }, "l", function () awful.tag.incmwfact( 0.05) end,
{description = "increase master width factor", group = "layout"}),
awful.key({ modkey, }, "h", function () awful.tag.incmwfact(-0.05) end,

View File

@ -0,0 +1,841 @@
local machi = {
layout = require((...):match("(.-)[^%.]+$") .. "layout"),
engine = require((...):match("(.-)[^%.]+$") .. "engine"),
}
local capi = {
client = client,
screen = screen,
}
local beautiful = require("beautiful")
local wibox = require("wibox")
local awful = require("awful")
local gears = require("gears")
local lgi = require("lgi")
local dpi = require("beautiful.xresources").apply_dpi
local gtimer = require("gears.timer")
local ERROR = 2
local WARNING = 1
local INFO = 0
local DEBUG = -1
local module = {
log_level = WARNING,
}
local function log(level, msg)
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
end
local function max(a, b)
if a < b then return b else return a end
end
local function with_alpha(col, alpha)
local r, g, b
_, r, g, b, _ = col:get_rgba()
return lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
end
function module.start(c, exit_keys)
local tablist_font_desc = beautiful.get_merged_font(
beautiful.font, dpi(10))
local font_color = with_alpha(gears.color(beautiful.fg_normal), 1)
local font_color_hl = with_alpha(gears.color(beautiful.fg_focus), 1)
local label_size = dpi(30)
local border_color = with_alpha(
gears.color(beautiful.machi_switcher_border_color or beautiful.border_focus),
beautiful.machi_switcher_border_opacity or 0.25)
local border_color_hl = with_alpha(
gears.color(beautiful.machi_switcher_border_hl_color or beautiful.border_focus),
beautiful.machi_switcher_border_hl_opacity or 0.75)
local fill_color = with_alpha(
gears.color(beautiful.machi_switcher_fill_color or beautiful.bg_normal),
beautiful.machi_switcher_fill_opacity or 0.25)
local box_bg = with_alpha(
gears.color(beautiful.machi_switcher_box_bg or beautiful.bg_normal),
beautiful.machi_switcher_box_opacity or 0.85)
local fill_color_hl = with_alpha(
gears.color(beautiful.machi_switcher_fill_color_hl or beautiful.bg_focus),
beautiful.machi_switcher_fill_hl_opacity or 1)
-- for comparing floats
local threshold = 0.1
local traverse_radius = dpi(5)
local screen = c and c.screen or awful.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
if layout.machi_get_instance_data == nil then return end
local cd, td, areas, _new_placement_cb = layout.machi_get_instance_data(screen, screen.selected_tag)
if areas == nil or #areas == 0 then
return
end
local infobox, infotabbox
local tablist = nil
local traverse_x, traverse_y
if c then
traverse_x = c.x + traverse_radius
traverse_y = c.y + traverse_radius
else
traverse_x = screen.workarea.x + screen.workarea.width / 2
traverse_y = screen.workarea.y + screen.workarea.height / 2
end
local selected_area_ = nil
local function set_selected_area(area)
selected_area_ = area
if area then
traverse_x = max(areas[area].x + traverse_radius, min(areas[area].x + areas[area].width - traverse_radius, traverse_x))
traverse_y = max(areas[area].y + traverse_radius, min(areas[area].y + areas[area].height - traverse_radius, traverse_y))
end
end
local function selected_area()
if selected_area_ == nil then
local min_dis = nil
for i, a in ipairs(areas) do
if a.habitable then
local dis =
math.abs(a.x + traverse_radius - traverse_x) + math.abs(a.x + a.width - traverse_radius - traverse_x) - a.width +
math.abs(a.y + traverse_radius - traverse_y) + math.abs(a.y + a.height - traverse_radius - traverse_y) - a.height +
traverse_radius * 4
if min_dis == nil or min_dis > dis then
min_dis = dis
selected_area_ = i
end
end
end
set_selected_area(selected_area_)
end
return selected_area_
end
local parent_stack = {}
local function get_tablist(area)
local list = {}
for _, tc in ipairs(screen.tiled_clients) do
if not (tc.floating or tc.immobilized)
then
if areas[area].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= areas[area].x + areas[area].width and
areas[area].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= areas[area].y + areas[area].height
then
list[#list + 1] = tc
end
end
end
return list
end
local function maintain_tablist()
tablist = get_tablist(selected_area())
if #tablist ~= 0 then
c = tablist[1]
end
end
local function draw_tab_info(context, cr, width, height)
maintain_tablist()
local wi = { x = 0, y = 0, w = 1, h = 1 }
local ext
local active_area = selected_area()
if #tablist > 0 then
local a = areas[active_area]
local pl = lgi.Pango.Layout.create(cr)
pl:set_font_description(tablist_font_desc)
local vpadding = dpi(10)
local hpadding = dpi(15)
local vborder = dpi(5)
local hborder = dpi(5)
local list_height = 2 * vborder
local list_width = 2 * hpadding + 2 * hborder
local exts = {}
local tl = {}
local hw = math.floor((#tablist-1)/2)
for i = hw+1,2,-1 do
table.insert(tl, tablist[i])
end
table.insert(tl, tablist[1])
local active = #tl
for i = #tablist,hw+2,-1 do
table.insert(tl, tablist[i])
end
for index, tc in ipairs(tl) do
local label = tc.name or "<unnamed>"
pl:set_text(label)
local w, h
w, h = pl:get_size()
w = w / lgi.Pango.SCALE
h = h / lgi.Pango.SCALE
local ext = { width = w, height = h }
exts[#exts + 1] = ext
list_height = list_height + ext.height + vpadding
list_width = max(list_width, w + 2 * hpadding + 2 * hborder)
end
local y_offset = vborder
wi.x = a.x + (a.width - list_width) / 2
wi.y = a.y + (a.height - list_height) / 2
wi.w = list_width
wi.h = list_height
cr:rectangle(0, 0, list_width, list_height)
cr:set_source(border_color_hl)
cr:fill()
for index, tc in ipairs(tl) do
local label = tc.name or "<unnamed>"
local ext = exts[index]
if index == active then
cr:rectangle(hborder, y_offset, list_width - 2 * hborder, ext.height + vpadding)
cr:set_source(fill_color_hl)
cr:fill()
pl:set_text(label)
cr:move_to(hborder + hpadding, vpadding / 2 + y_offset)
cr:set_source(font_color_hl)
cr:show_layout(pl)
else
pl:set_text(label)
cr:move_to(hborder + hpadding, vpadding / 2 + y_offset)
cr:set_source(font_color)
cr:show_layout(pl)
end
y_offset = y_offset + ext.height + vpadding
end
end
if infotabbox.x ~= wi.x or infotabbox.y ~= wi.y or
infotabbox.width ~= wi.w or infotabbox.height ~= wi.h
then
infotabbox.x = wi.x
infotabbox.y = wi.y
infotabbox.width = wi.w
infotabbox.height = wi.h
end
end
local function draw_info(context, cr, width, height)
maintain_tablist()
cr:set_source_rgba(0, 0, 0, 0)
cr:rectangle(0, 0, width, height)
cr:fill()
local padx = 5
local pady = 5
local msg
local active_area = selected_area()
for i, a in ipairs(areas) do
if a.habitable or i == active_area then
cr:rectangle(a.x - padx - start_x, a.y - pady - start_y, a.width + 2*padx, a.height + 2*pady)
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 infobox.width ~= screen.workarea.width or infobox.height ~= screen.workarea.height then
gtimer.delayed_call(function ()
infobox.width = screen.workarea.width
infobox.height = screen.workarea.height
infobox.visible = true
end)
end
-- show the traverse point
-- cr:rectangle(traverse_x - start_x - traverse_radius, traverse_y - start_y - traverse_radius, traverse_radius * 2, traverse_radius * 2)
-- cr:set_source_rgba(1, 1, 1, 1)
-- cr:fill()
end
local draw = function()
if infobox ~= nil then
infobox.bgimage = draw_info
end
if infotabbox ~= nil then
infotabbox.bgimage = draw_tab_info
end
end
local largest_area = function(areas)
local largest = nil
local size = 0
for i, a in ipairs(areas) do
local s = a.width * a.height
if a.habitable and (largest == nil or s > size) then
largest = i
size = s
end
end
return largest
end
local snap_area = function(x, y, width, height)
local bestD, d
local best
local dist = function(x0, y0, x1, y1)
local a, b = x0-x1, y0-y1
return math.sqrt(a*a+b*b)
end
for i, a in ipairs(areas) do
d = dist(a.x, a.y, x, y) +
dist(a.x+a.width, a.y+a.height, x+width, y+height)
if bestD == nil or d < bestD then
bestD = d
best = i
end
end
return best
end
-- area is the index in areas, should be made consistent after el big refactor
-- draft is reset
local move_client
move_client = function(c, area)
if area == nil or areas[area] == nil or c == nil or not c.valid or c.floating then
return
end
machi.layout.set_geometry(c, areas[area], areas[area], 0, c.border_width)
if cd[c] == nil then
return
end
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = area
end
local snap_client = function()
if c == nil then
return
end
a = snap_area(c.x, c.y, c.width, c.height)
if a == nil then
return false
end
move_client(c, a)
return true
end
local master_add = function()
if c == nil or c.floating then
return
end
local dst = largest_area(areas)
if dst == nil then
return
end
move_client(c, dst)
awful.layout.arrange(screen)
end
local master_swap = function(all)
if c == nil or c.floating then
return
end
local src = selected_area()
local dst = largest_area(areas)
if dst == nil or dst == src then
return
end
local srclist = get_tablist(src)
local dstlist = get_tablist(dst)
for i, c in ipairs(dstlist) do
if all or i == 1 then
move_client(c, src)
end
end
for i, c in ipairs(srclist) do
if all or i == 1 then
move_client(c, dst)
end
end
awful.layout.arrange(screen)
end
local traverse_clients = function(dir)
dir = string.lower(dir)
local current_area = selected_area()
local current = areas[current_area]
if current == nil then
return false
end
local candidates = {}
for i, a in ipairs(areas) do
local data = {i = i, a = a}
if not a.habitable or i == current_area then
elseif dir == "up" and
a.y + a.height < current.y and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
elseif dir == "down" and
a.y > current.y + current.height and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
elseif dir == "left" and
a.x + a.width < current.x and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
elseif dir == "right" and
a.x > current.x + current.width and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
end
end
local best_area
local score
for i, d in ipairs(candidates) do
local a = d.a
local dist, overlap
if dir == "up" or dir == "down" then
overlap = min(current.x+current.width, a.x+a.width) - max(current.x, a.x)
elseif dir == "left" or dir == "right" then
overlap = min(current.y+current.height, a.y+a.height) - max(current.y, a.y)
end
if dir == "up" then
dist = current.y - a.y - a.height
elseif dir == "down" then
dist = a.y - current. y - current.height
elseif dir == "left" then
dist = current.x - a.x - a.width
elseif dir == "right" then
dist = a.x - current.x - current.width
end
-- TODO this is probably not optimal
local s = overlap - dist
if score == nil or s > score then
best_area = d.i
score = s
end
end
if best_area == nil then
if #candidates == 0 then
return false
end
best_area = candidates[1].i
end
set_selected_area(best_area)
local tablist = get_tablist(best_area)
if #tablist == 0 or tablist[1] == c then
return false
end
c = tablist[1]
capi.client.focus = c
return true
end
local traverse_areas = function(dir, move, draft)
dir = string.lower(dir)
local current_area = selected_area()
if c and move then
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
set_selected_area(nil)
end
elseif c and draft then
local ex = c.x + c.width + c.border_width * 2
local ey = c.y + c.height + c.border_width * 2
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
set_selected_area(nil)
end
end
local choice = nil
local choice_value
current_area = selected_area()
for i, a in ipairs(areas) do
if not a.habitable then goto continue end
local v
if dir == "up" then
if a.x < traverse_x + threshold
and traverse_x < a.x + a.width + threshold then
v = traverse_y - a.y - a.height
else
v = -1
end
elseif dir == "down" then
if a.x < traverse_x + threshold
and traverse_x < a.x + a.width + threshold then
v = a.y - traverse_y
else
v = -1
end
elseif dir == "left" then
if a.y < traverse_y + threshold
and traverse_y < a.y + a.height + threshold then
v = traverse_x - a.x - a.width
else
v = -1
end
elseif dir == "right" then
if a.y < traverse_y + threshold
and traverse_y < a.y + a.height + threshold then
v = a.x - traverse_x
else
v = -1
end
end
if (v > threshold) and (choice_value == nil or choice_value > v) then
choice = i
choice_value = v
end
::continue::
end
if choice == nil then
choice = current_area
if dir == "up" then
traverse_y = screen.workarea.y
elseif dir == "down" then
traverse_y = screen.workarea.y + screen.workarea.height
elseif dir == "left" then
traverse_x = screen.workarea.x
else
traverse_x = screen.workarea.x + screen.workarea.width
end
end
if choice ~= nil then
tablist = nil
set_selected_area(choice)
if c and draft and cd[c].draft ~= false then
local lu = cd[c].lu or cd[c].area
local rd = cd[c].rd or cd[c].area
if draft and move then
lu = choice
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 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, areas[lu], areas[rd], 0, c.border_width)
elseif lu ~= nil then
machi.layout.set_geometry(c, areas[lu], nil, 0, c.border_width)
elseif rd ~= nil then
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
if lu == rd and cd[c].draft ~= true then
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = lu
else
cd[c].lu = lu
cd[c].rd = rd
cd[c].area = nil
end
c:emit_signal("request::activate", "mouse.move", {raise=false})
c:raise()
awful.layout.arrange(screen)
elseif c and move then
-- move the window
local in_draft = cd[c].draft
if cd[c].draft ~= nil then
in_draft = cd[c].draft
elseif cd[c].lu then
in_draft = true
elseif cd[c].area then
in_draft = false
else
log(ERROR, "Assuming in_draft for unhandled client "..tostring(c))
in_draft = true
end
if in_draft then
c.x = areas[choice].x
c.y = areas[choice].y
else
machi.layout.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = choice
end
c:emit_signal("request::activate", "mouse.move", {raise=false})
c:raise()
awful.layout.arrange(screen)
tablist = nil
else
maintain_tablist()
-- move the focus
if #tablist > 0 and tablist[1] ~= c then
c = tablist[1]
capi.client.focus = c
end
end
draw()
end
end
local tab = function()
maintain_tablist()
if #tablist > 1 then
c = tablist[#tablist]
c:emit_signal("request::activate", "mouse.move", {raise=false})
c:raise()
draw()
end
end
local function handle_key(mod, key, event)
local key_translate_tab = {
["k"] = "Up",
["h"] = "Left",
["j"] = "Down",
["l"] = "Right",
["K"] = "Up",
["H"] = "Left",
["J"] = "Down",
["L"] = "Right",
}
if event == "release" then
if exit_keys and exit_keys[key] then
exit()
end
return
end
if key_translate_tab[key] ~= nil then
key = key_translate_tab[key]
end
maintain_tablist()
assert(tablist ~= nil)
local super = false
local shift = false
local ctrl = false
for i, m in ipairs(mod) do
if m == "Mod4" then super = true
elseif m == "Shift" then shift = true
elseif m == "Control" then ctrl = true
end
end
if key == "Tab" then
tab()
elseif key == "Up" or key == "Down" or key == "Left" or key == "Right" then
traverse_areas(key, shift or super, ctrl)
elseif (key == "q" or key == "Prior") then
local current_area = selected_area()
if areas[current_area].parent_id == nil then
return
end
set_selected_area(areas[current_area].parent_id)
if #parent_stack == 0 or
parent_stack[#parent_stack] ~= current_area then
parent_stack = {current_area}
end
parent_stack[#parent_stack + 1] = areas[current_area].parent_id
current_area = parent_stack[#parent_stack]
if c and ctrl and cd[c].draft ~= false then
if cd[c].area then
cd[c].lu, cd[c].rd, cd[c].area = cd[c].area, cd[c].area, nil
end
machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
awful.layout.arrange(screen)
end
draw()
elseif (key =="e" or key == "Next") then
local current_area = selected_area()
if #parent_stack <= 1 or parent_stack[#parent_stack] ~= current_area then
return
end
set_selected_area(parent_stack[#parent_stack - 1])
table.remove(parent_stack, #parent_stack)
current_area = parent_stack[#parent_stack]
if c and ctrl then
if areas[current_area].habitable and cd[c].draft ~= true then
cd[c].lu, cd[c].rd, cd[c].area = nil, nil, current_area
end
machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
awful.layout.arrange(screen)
end
draw()
elseif key == "/" then
local current_area = selected_area()
local original_cmd = machi.engine.areas_to_command(areas, true, current_area)
areas[current_area].hole = true
local prefix, suffix = machi.engine.areas_to_command(
areas, false):match("(.*)|(.*)")
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 ()
layout.machi_editor.start_interactive(
screen,
{
workarea = workarea,
original_cmd = original_cmd,
cmd_prefix = prefix,
cmd_suffix = suffix,
}
)
end
)
exit()
elseif (key == "f" or key == ".") and c then
if cd[c].draft == nil then
cd[c].draft = true
elseif cd[c].draft == true then
cd[c].draft = false
else
cd[c].draft = nil
end
awful.layout.arrange(screen)
elseif key == "Escape" or key == "Return" then
exit()
else
log(DEBUG, "Unhandled key " .. key)
end
end
local ui = function()
infobox = wibox({
screen = screen,
x = screen.workarea.x,
y = screen.workarea.y,
width = 1, -- screen.workarea.width,
height = 1, --screen.workarea.height,
bg = "#ffffff00",
opacity = 1,
ontop = false,
type = "normal",
})
infotabbox = wibox({
screen = screen,
x = screen.workarea.x,
y = screen.workarea.y,
width = 1,
height = 1,
bg = "#ffffff00",
ontop = true,
type = "normal",
visible = true,
})
draw()
awful.client.focus.history.disable_tracking()
local kg
local function exit()
awful.client.focus.history.enable_tracking()
if capi.client.focus then
capi.client.emit_signal("focus", capi.client.focus)
end
infobox.visible = false
infotabbox.visible = false
awful.keygrabber.stop(kg)
end
kg = awful.keygrabber.run(
function (...)
ok, _ = pcall(handle_key, ...)
if not ok then exit() end
end
)
end
return {
ui = ui,
tab = tab,
snap_client = snap_client,
master_add = master_add,
--- swap current window with 'master' or moves it to the master position if it's unoccupied (i.e.: largest area/client)
-- @param all swap/move all clients in both areas, not just the top ones.
master_swap = master_swap,
--- focus client by direction
-- @param dir direction (up|right|down|left)
-- @returns true if a client was found
traverse_clients = traverse_clients,
traverse_areas = traverse_areas,
move = function(dir) traverse_areas(dir, true, false) end,
-- like traverse_clients, returns true if a client was found
focus = function(dir) return traverse_clients(dir) end,
}
end
return module

View File

@ -1,14 +1,15 @@
local gears = require('gears')
local awful = require('awful')
local lain = require('lain')
local hotkeys_popup = require('awful.hotkeys_popup')
local machi = require('layout-machi')
local lain = require('lain')
local globals = require('src.globals')
local mpris = require('src.util.mpris')
local volume = require('src.util.volume')
local modkey = globals.modkey
local altkey = globals.altkey
local mpris = require('src.util.mpris')
local volume = require('src.util.volume')
local quake_terminal_name = 'QuakeTerminal'
local quake = lain.util.quake({
@ -102,6 +103,8 @@ local globalkeys = gears.table.join(
awful.key({ modkey, 'Control' }, 'l', function() awful.tag.incncol(-1, nil, true) end, { description = 'decrease the number of columns', group = 'layout' }),
awful.key({ modkey }, 'space', function() awful.layout.inc(1) end, { description = 'select next', group = 'layout' }),
awful.key({ modkey, 'Shift' }, 'space', function() awful.layout.inc(-1) end, { description = 'select previous', group = 'layout' }),
awful.key({ modkey }, '.', function () machi.default_editor.start_interactive() end, { description = 'start machi editor', group = 'layout' }),
awful.key({ modkey }, '+', function () machi.default_editor.start_interactive() end, { description = 'switch between windows for a machi', group = 'layout' }),
---- AUDIO KEYS ----
awful.key({}, "XF86AudioMute", volume.toggle_mute),

View File

@ -1,9 +1,7 @@
local awful = require('awful')
local fair_col = require('src.layouts.fair_col')
local tile_col = require('src.layouts.tile_col')
tag.connect_signal("request::default_layouts", function()
awful.layout.append_default_layouts({
awful.layout.append_default_layouts {
awful.layout.suit.fair,
awful.layout.suit.fair.horizontal,
awful.layout.suit.tile,
@ -11,7 +9,8 @@ tag.connect_signal("request::default_layouts", function()
awful.layout.suit.tile.bottom,
awful.layout.suit.tile.top,
awful.layout.suit.floating,
fair_col,
--tile_col
})
require('src.layouts.fair_col'),
--require('src.layouts.tile_col'),
require('layout-machi.layout').create {}
}
end)

View File

@ -40,6 +40,11 @@ return {
fg_focus = '#ff8c00',
fg_urgent = '#af1d18',
fg_minimize = '#ffffff',
machi_editor_border_color = '#606060',
machi_editor_active_color = '#002200',
machi_editor_open_color = '#000000',
machi_editor_done_color = '#000022',
machi_editor_closed_color = '#000022',
border_width = 1,
border_normal = '#1c2022',
border_focus = '#606060',
@ -86,6 +91,7 @@ return {
layout_magnifier = confdir .. '/icons/magnifier.png',
layout_floating = confdir .. '/icons/floating.png',
layout_fairc = confdir .. '/icons/fairc.png',
layout_machi = confdir .. '/lib/layout-machi/icon.png',
titlebar_close_button_normal = confdir .. '/icons/titlebar/close_normal.png',
titlebar_close_button_focus = confdir .. '/icons/titlebar/close_focus.png',
titlebar_minimize_button_normal = confdir .. '/icons/titlebar/minimize_normal.png',