Skip to content

Instantly share code, notes, and snippets.

@corpix
Forked from gitaarik/restore_floating_clients.lua
Last active December 17, 2025 02:34
Show Gist options
  • Select an option

  • Save corpix/1487f5a5aa03b9e890f47ad9d01edb8e to your computer and use it in GitHub Desktop.

Select an option

Save corpix/1487f5a5aa03b9e890f47ad9d01edb8e to your computer and use it in GitHub Desktop.
Awesome WM script that restores floating clients geometry (width / height / position) when switching layouts or unmaximizing
local capi = {
client = client,
tag = tag,
awesome = awesome,
screen = screen
}
local awful = require("awful")
local gears = require("gears")
local json = require("cjson")
local floating_geometry = {}
function floating_geometry:identify(c)
local id = c.class or c.name or "undefined"
if c.type then
id = id .. "_" .. c.type
end
return id
end
function floating_geometry:get_screen_fingerprint()
local screens = {}
for s in capi.screen do
local geom = s.geometry
table.insert(screens, {
index = s.index,
width = geom.width,
height = geom.height,
x = geom.x,
y = geom.y
})
end
table.sort(screens, function(a, b) return a.index < b.index end)
return screens
end
function floating_geometry:verify_screen_fingerprint(saved_fingerprint)
if not saved_fingerprint then
return false
end
local current = self:get_screen_fingerprint()
if #current ~= #saved_fingerprint then
return false
end
for i, screen_data in ipairs(current) do
local saved = saved_fingerprint[i]
if not saved or
saved.width ~= screen_data.width or
saved.height ~= screen_data.height or
saved.x ~= screen_data.x or
saved.y ~= screen_data.y then
return false
end
end
return true
end
function floating_geometry:is_geometry_valid(geom, screen_geom)
local visible_width = math.min(geom.x + geom.width, screen_geom.x + screen_geom.width) -
math.max(geom.x, screen_geom.x)
local visible_height = math.min(geom.y + geom.height, screen_geom.y + screen_geom.height) -
math.max(geom.y, screen_geom.y)
return visible_width >= geom.width * 0.5 and visible_height >= geom.height * 0.5
end
function floating_geometry:adapt_geometry(geom, old_screen, new_screen)
if not old_screen or not new_screen then
return geom
end
local scale_x = new_screen.width / old_screen.width
local scale_y = new_screen.height / old_screen.height
return {
x = math.floor((geom.x - old_screen.x) * scale_x + new_screen.x),
y = math.floor((geom.y - old_screen.y) * scale_y + new_screen.y),
width = math.floor(geom.width * scale_x),
height = math.floor(geom.height * scale_y)
}
end
function floating_geometry:save_geometries()
if not self.geometries_dirty then
return
end
local data = {
version = 2,
screen_fingerprint = self:get_screen_fingerprint(),
geometries = self.geometries
}
local file = io.open(self.opts.file, "w")
if file then
file:write(json.encode(data))
file:close()
self.geometries_dirty = false
end
end
function floating_geometry:load_geometries()
local file = io.open(self.opts.file, "r")
if not file then
return
end
local content = file:read("*all")
file:close()
if not content or content == "" then
return
end
local ok, data = pcall(json.decode, content)
if not ok or not data then
return
end
if data.version ~= 2 then
return
end
if self:verify_screen_fingerprint(data.screen_fingerprint) then
self.geometries = data.geometries or {}
else
self:adapt_saved_geometries(data)
end
end
function floating_geometry:adapt_saved_geometries(data)
if not data.geometries or not data.screen_fingerprint then
return
end
self.geometries = {}
for identifier, screen_geometries in pairs(data.geometries) do
self.geometries[identifier] = {}
for screen_idx, geom_list in pairs(screen_geometries) do
local old_screen_idx = tonumber(screen_idx)
if old_screen_idx and data.screen_fingerprint[old_screen_idx] then
local old_screen_data = data.screen_fingerprint[old_screen_idx]
local new_screen = capi.screen[old_screen_idx]
if new_screen then
local new_screen_geom = new_screen.geometry
self.geometries[identifier][old_screen_idx] = {}
for _, geom in ipairs(geom_list) do
local adapted = self:adapt_geometry(geom, old_screen_data, new_screen_geom)
if self:is_geometry_valid(adapted, new_screen_geom) then
table.insert(self.geometries[identifier][old_screen_idx], adapted)
end
end
end
end
end
end
end
function floating_geometry:sync_geometries()
for _, c in ipairs(capi.client.get()) do
local identifier = self:identify(c)
local screen_idx = c.screen.index
if identifier and self.floating_client_geometries[c.window] then
if not self.geometries[identifier] then
self.geometries[identifier] = {}
end
if not self.geometries[identifier][screen_idx] then
self.geometries[identifier][screen_idx] = {}
end
local geom_list = self.geometries[identifier][screen_idx]
local geom = self.floating_client_geometries[c.window]
local is_duplicate = false
for _, saved_geom in ipairs(geom_list) do
if saved_geom.x == geom.x and saved_geom.y == geom.y and
saved_geom.width == geom.width and saved_geom.height == geom.height then
is_duplicate = true
break
end
end
if not is_duplicate then
table.insert(geom_list, 1, geom)
while #geom_list > self.opts.max_history do
table.remove(geom_list)
end
self.geometries_dirty = true
end
end
end
end
function floating_geometry:on_property_geometry(c)
self.clients_screens[c.window] = c.screen.index
if capi.awesome.startup or
awful.layout.get(awful.screen.focused()) ~= awful.layout.suit.floating then
return
end
self.unmaximized_state[c.window] = not c.maximized
if c.maximized then
return
end
if self.floating_client_geometries[c.window] then
self.prev_floating_client_geometries[c.window] = self.floating_client_geometries[c.window]
end
local geometry = c:geometry()
self.floating_client_geometries[c.window] = geometry
end
function floating_geometry:on_property_layout(t)
if t.layout == awful.layout.suit.floating then
for k, c in ipairs(t:clients()) do
if self.floating_client_geometries[c.window] and self.unmaximized_state[c.window] then
c:geometry(self.floating_client_geometries[c.window])
else
c.maximized = true
end
end
else
for k, c in ipairs(t:clients()) do
c.maximized = false
end
end
end
function floating_geometry:on_property_maximized(c)
if c.maximized or
awful.layout.get(awful.screen.focused()) ~= awful.layout.suit.floating then
return
end
if self.prev_floating_client_geometries[c.window] then
c:geometry(self.prev_floating_client_geometries[c.window])
else
local g = c.screen.geometry
c:geometry({
x = g.x + g.width / 6,
y = g.y + g.height / 6,
width = g.width / 1.5,
height = g.height / 1.5
})
local saved_screen = self.clients_screens[c.window]
if saved_screen then
c.screen = capi.screen[saved_screen]
end
end
end
function floating_geometry:on_manage(c)
if capi.awesome.startup or
awful.layout.get(awful.screen.focused()) ~= awful.layout.suit.floating then
return
end
self.floating_client_geometries[c.window] = nil
self.prev_floating_client_geometries[c.window] = nil
local identifier = self:identify(c)
if self.opts.exclude and self.opts.exclude(c) then
return
end
if identifier and self.geometries[identifier] then
local screen_idx = c.screen.index
local geom_list = self.geometries[identifier][screen_idx]
if geom_list and #geom_list > 0 then
local geom = geom_list[1]
local screen_geom = c.screen.geometry
if self:is_geometry_valid(geom, screen_geom) then
c:geometry(geom)
end
end
end
end
function floating_geometry:on_startup()
self:load_geometries()
end
function floating_geometry:on_exit()
self:sync_geometries()
self:save_geometries()
end
function floating_geometry.new(opts)
opts = opts or {}
opts.file = opts.file or (os.getenv("HOME") .. "/.config/awesome/floating-geometry.json")
opts.max_history = opts.max_history or 5
local obj = gears.object { class = floating_geometry }
obj.opts = opts
obj.floating_client_geometries = {}
obj.geometries = {}
obj.geometries_dirty = false
obj.prev_floating_client_geometries = {}
obj.unmaximized_state = {}
obj.clients_screens = {}
obj.sync_geometries_timer = gears.timer {
timeout = 2,
call_now = false,
autostart = true,
callback = function()
obj:sync_geometries()
end
}
obj.save_geometries_timer = gears.timer {
timeout = 5,
call_now = false,
autostart = true,
callback = function()
if obj.geometries_dirty then
obj:save_geometries()
end
end
}
capi.client.connect_signal("manage", function(c) obj:on_manage(c) end)
capi.client.connect_signal("property::maximized", function(c) obj:on_property_maximized(c) end)
capi.client.connect_signal("property::geometry", function(c) obj:on_property_geometry(c) end)
capi.tag.connect_signal("property::layout", function(t) obj:on_property_layout(t) end)
capi.awesome.connect_signal("startup", function() obj:on_startup() end)
capi.awesome.connect_signal("exit", function() obj:on_exit() end)
return obj
end
function floating_geometry.init(opts)
return floating_geometry.new(opts)
end
return floating_geometry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment