Forked from gitaarik/restore_floating_clients.lua
Last active
December 17, 2025 02:34
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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