--------------------------------------------------------------------------- --- Useful client manipulation functions. -- -- @author Julien Danjou <julien@danjou.info> -- @copyright 2008 Julien Danjou -- @release @AWESOME_VERSION@ -- @module awful.client --------------------------------------------------------------------------- -- Grab environment we need local util = require("awful.util") local tag = require("awful.tag") local pairs = pairs local type = type local ipairs = ipairs local table = table local math = math local setmetatable = setmetatable local capi = { client = client, mouse = mouse, screen = screen, awesome = awesome, } -- we use require("awful.screen") inside functions to prevent circular dependencies. local screen local client = {} -- Private data client.data = {} client.data.focus = {} client.data.urgent = {} client.data.marked = {} client.data.properties = setmetatable({}, { __mode = 'k' }) client.data.persistent_properties_registered = {} -- keys are names of persistent properties, value always true client.data.persistent_properties_loaded = setmetatable({}, { __mode = 'k' }) -- keys are clients, value always true -- Functions client.urgent = {} client.focus = {} client.focus.history = {} client.swap = {} client.floating = {} client.dockable = {} client.property = {} client.shape = require("awful.client.shape") --- Jump to the given client. -- Takes care of focussing the screen, the right tag, etc. -- -- @client c the client to jump to -- @tparam bool merge If true then merge tags when clients are not visible. function client.jumpto(c, merge) local s = capi.client.focus and capi.client.focus.screen or capi.mouse.screen -- focus the screen if s ~= c.screen then capi.mouse.screen = c.screen end -- Try to make client visible, this also covers e.g. sticky local t = c:tags()[1] if t and not c:isvisible() then if merge then t.selected = true else tag.viewonly(t) end end -- focus the client capi.client.focus = c c:raise() end --- Get the first client that got the urgent hint. -- -- @treturn client The first urgent client. function client.urgent.get() if #client.data.urgent > 0 then return client.data.urgent[1] else -- fallback behaviour: iterate through clients and get the first urgent local clients = capi.client.get() for k, cl in pairs(clients) do if cl.urgent then return cl end end end end --- Jump to the client that received the urgent hint first. -- -- @tparam bool merge If true then merge tags when clients are not visible. function client.urgent.jumpto(merge) local c = client.urgent.get() if c then client.jumpto(c, merge) end end --- Adds client to urgent stack. -- -- @client c The client object. -- @param prop The property which is updated. function client.urgent.add(c, prop) if type(c) == "client" and prop == "urgent" and c.urgent then table.insert(client.data.urgent, c) end end --- Remove client from urgent stack. -- -- @client c The client object. function client.urgent.delete(c) for k, cl in ipairs(client.data.urgent) do if c == cl then table.remove(client.data.urgent, k) break end end end --- Remove a client from the focus history -- -- @client c The client that must be removed. function client.focus.history.delete(c) for k, v in ipairs(client.data.focus) do if v == c then table.remove(client.data.focus, k) break end end end --- Filter out window that we do not want handled by focus. -- This usually means that desktop, dock and splash windows are -- not registered and cannot get focus. -- -- @client c A client. -- @return The same client if it's ok, nil otherwise. function client.focus.filter(c) if c.type == "desktop" or c.type == "dock" or c.type == "splash" or not c.focusable then return nil end return c end --- Update client focus history. -- -- @client c The client that has been focused. function client.focus.history.add(c) -- Remove the client if its in stack client.focus.history.delete(c) -- Record the client has latest focused table.insert(client.data.focus, 1, c) end --- Get the latest focused client for a screen in history. -- -- @tparam int screen The screen number to look for. -- @tparam int idx The index: 0 will return first candidate, -- 1 will return second, etc. -- @tparam function An optional filter. If no client is found in the first -- iteration, client.focus.filter is used by default to get -- any client. -- @treturn client A client. function client.focus.history.get(screen, idx, filter) -- When this counter is equal to idx, we return the client local counter = 0 local vc = client.visible(screen) for k, c in ipairs(client.data.focus) do if c.screen == screen then if not filter or filter(c) then for j, vcc in ipairs(vc) do if vcc == c then if counter == idx then return c end -- We found one, increment the counter only. counter = counter + 1 break end end end end end -- Argh nobody found in history, give the first one visible if there is one -- that passes the filter. local filter = filter or client.focus.filter if counter == 0 then for k, v in ipairs(vc) do if filter(v) then return v end end end end --- Focus the previous client in history. function client.focus.history.previous() local sel = capi.client.focus local s if sel then s = sel.screen else s = capi.mouse.screen end local c = client.focus.history.get(s, 1) if c then capi.client.focus = c end end --- Get visible clients from a screen. -- -- @param screen The screen number, or nil for all screens. -- @return A table with all visible clients. function client.visible(screen) local cls = capi.client.get(screen) local vcls = {} for k, c in pairs(cls) do if c:isvisible() then table.insert(vcls, c) end end return vcls end --- Get visible and tiled clients -- -- @param screen The screen number, or nil for all screens. -- @return A table with all visible and tiled clients. function client.tiled(screen) local clients = client.visible(screen) local tclients = {} -- Remove floating clients for k, c in pairs(clients) do if not client.floating.get(c) and not c.fullscreen and not c.maximized_vertical and not c.maximized_horizontal then table.insert(tclients, c) end end return tclients end --- Get a client by its relative index to another client. -- If no client is passed, the focused client will be used. -- -- @tparam int i The index. Use 1 to get the next, -1 to get the previous. -- @client[opt] c The client. -- @return A client, or nil if no client is available. -- -- @usage -- focus the next window in the index -- awful.client.next(1) -- -- focus the previous -- awful.client.next(-1) function client.next(i, c) -- Get currently focused client local sel = c or capi.client.focus if sel then -- Get all visible clients local cls = client.visible(sel.screen) local fcls = {} -- Remove all non-normal clients for idx, c in ipairs(cls) do if client.focus.filter(c) or c == sel then table.insert(fcls, c) end end cls = fcls -- Loop upon each client for idx, c in ipairs(cls) do if c == sel then -- Cycle return cls[util.cycle(#cls, idx + i)] end end end end --- Focus a client by the given direction. -- -- @tparam string dir The direction, can be either -- `"up"`, `"down"`, `"left"` or `"right"`. -- @client[opt] c The client. function client.focus.bydirection(dir, c) local sel = c or capi.client.focus if sel then local cltbl = client.visible(sel.screen) local geomtbl = {} for i,cl in ipairs(cltbl) do geomtbl[i] = cl:geometry() end local target = util.get_rectangle_in_direction(dir, geomtbl, sel:geometry()) -- If we found a client to focus, then do it. if target then capi.client.focus = cltbl[target] end end end --- Focus a client by the given direction. Moves across screens. -- -- @param dir The direction, can be either "up", "down", "left" or "right". -- @client[opt] c The client. function client.focus.global_bydirection(dir, c) screen = screen or require("awful.screen") local sel = c or capi.client.focus local scr = capi.mouse.screen if sel then scr = sel.screen end -- change focus inside the screen client.focus.bydirection(dir, sel) -- if focus not changed, we must change screen if sel == capi.client.focus then screen.focus_bydirection(dir, scr) if scr ~= capi.mouse.screen then local cltbl = client.visible(capi.mouse.screen) local geomtbl = {} for i,cl in ipairs(cltbl) do geomtbl[i] = cl:geometry() end local target = util.get_rectangle_in_direction(dir, geomtbl, capi.screen[scr].geometry) if target then capi.client.focus = cltbl[target] end end end end --- Focus a client by its relative index. -- -- @param i The index. -- @client[opt] c The client. function client.focus.byidx(i, c) local target = client.next(i, c) if target then capi.client.focus = target end end --- Swap a client with another client in the given direction -- @param dir The direction, can be either "up", "down", "left" or "right". -- @client[opt] c The client. function client.swap.bydirection(dir, c) local sel = c or capi.client.focus if sel then local cltbl = client.visible(sel.screen) local geomtbl = {} for i,cl in ipairs(cltbl) do geomtbl[i] = cl:geometry() end local target = util.get_rectangle_in_direction(dir, geomtbl, sel:geometry()) -- If we found a client to swap with, then go for it if target then cltbl[target]:swap(sel) end end end --- Swap a client with another client in the given direction. Swaps across screens. -- @param dir The direction, can be either "up", "down", "left" or "right". -- @client[opt] c The client. function client.swap.global_bydirection(dir, c) screen = screen or require("awful.screen") local sel = c or capi.client.focus local scr = capi.mouse.screen if sel then scr = sel.screen end if sel then -- move focus client.focus.global_bydirection(dir, sel) local c = capi.client.focus -- swapping inside a screen if sel.screen == c.screen and sel ~= c then c:swap(sel) -- swapping to an empty screen elseif sel.screen ~= c.screen and sel == c then client.movetoscreen(sel, capi.mouse.screen) --swapping to a nonempty screen elseif sel.screen ~= c.screen and sel ~= c then client.movetoscreen(sel, c.screen) client.movetoscreen(c, scr) end screen.focus(sel.screen) capi.client.focus = sel end end --- Swap a client by its relative index. -- @param i The index. -- @client[opt] c The client, otherwise focused one is used. function client.swap.byidx(i, c) local sel = c or capi.client.focus local target = client.next(i, sel) if target then target:swap(sel) end end --- Cycle clients. -- -- @param clockwise True to cycle clients clockwise. -- @param[opt] screen The screen where to cycle clients. function client.cycle(clockwise, screen) local screen = screen or capi.mouse.screen local cls = client.visible(screen) -- We can't rotate without at least 2 clients, buddy. if #cls >= 2 then local c = table.remove(cls, 1) if clockwise then for i = #cls, 1, -1 do c:swap(cls[i]) end else for _, rc in pairs(cls) do c:swap(rc) end end end end --- Get the master window. -- -- @param[opt] screen The screen number, otherwise screen mouse is used. -- @return The master window. function client.getmaster(screen) local s = screen or capi.mouse.screen return client.visible(s)[1] end --- Set the client as master: put it at the beginning of other windows. -- -- @client c The window to set as master. function client.setmaster(c) local cls = util.table.reverse(capi.client.get(c.screen)) for k, v in pairs(cls) do c:swap(v) end end --- Set the client as slave: put it at the end of other windows. -- @client c The window to set as slave. function client.setslave(c) local cls = capi.client.get(c.screen) for k, v in pairs(cls) do c:swap(v) end end --- Move/resize a client relative to current coordinates. -- @param x The relative x coordinate. -- @param y The relative y coordinate. -- @param w The relative width. -- @param h The relative height. -- @client[opt] c The client, otherwise focused one is used. function client.moveresize(x, y, w, h, c) local sel = c or capi.client.focus local geometry = sel:geometry() geometry['x'] = geometry['x'] + x geometry['y'] = geometry['y'] + y geometry['width'] = geometry['width'] + w geometry['height'] = geometry['height'] + h sel:geometry(geometry) end --- Move a client to a tag. -- @param target The tag to move the client to. -- @client[opt] c The client to move, otherwise the focused one is used. function client.movetotag(target, c) local sel = c or capi.client.focus local s = tag.getscreen(target) if sel and s then -- Set client on the same screen as the tag. sel.screen = s sel:tags({ target }) end end --- Toggle a tag on a client. -- @param target The tag to toggle. -- @client[opt] c The client to toggle, otherwise the focused one is used. function client.toggletag(target, c) local sel = c or capi.client.focus -- Check that tag and client screen are identical if sel and sel.screen == tag.getscreen(target) then local tags = sel:tags() local index = nil; for i, v in ipairs(tags) do if v == target then index = i break end end if index then -- If it's the only tag for the window, stop. if #tags == 1 then return end tags[index] = nil else tags[#tags + 1] = target end sel:tags(tags) end end --- Move a client to a screen. Default is next screen, cycling. -- @client c The client to move. -- @param s The screen number, default to current + 1. function client.movetoscreen(c, s) screen = screen or require("awful.screen") local sel = c or capi.client.focus if sel then local sc = capi.screen.count() if not s then s = sel.screen + 1 end if s > sc then s = 1 elseif s < 1 then s = sc end sel.screen = s screen.focus(s) end end --- Mark a client, and then call 'marked' hook. -- @client c The client to mark, the focused one if not specified. -- @return True if the client has been marked. False if the client was already marked. function client.mark(c) local cl = c or capi.client.focus if cl then for k, v in pairs(client.data.marked) do if cl == v then return false end end table.insert(client.data.marked, cl) -- Call callback cl:emit_signal("marked") return true end end --- Unmark a client and then call 'unmarked' hook. -- @client c The client to unmark, or the focused one if not specified. -- @return True if the client has been unmarked. False if the client was not marked. function client.unmark(c) local cl = c or capi.client.focus for k, v in pairs(client.data.marked) do if cl == v then table.remove(client.data.marked, k) cl:emit_signal("unmarked") return true end end return false end --- Check if a client is marked. -- @client c The client to check, or the focused one otherwise. function client.ismarked(c) local cl = c or capi.client.focus if cl then for k, v in pairs(client.data.marked) do if cl == v then return true end end end return false end --- Toggle a client as marked. -- @client c The client to toggle mark. function client.togglemarked(c) local cl = c or capi.client.focus if not client.mark(c) then client.unmark(c) end end --- Return the marked clients and empty the marked table. -- @return A table with all marked clients. function client.getmarked() for k, v in pairs(client.data.marked) do v:emit_signal("unmarked") end t = client.data.marked client.data.marked = {} return t end --- Set a client floating state, overriding auto-detection. -- Floating client are not handled by tiling layouts. -- @client c A client. -- @param s True or false. function client.floating.set(c, s) local c = c or capi.client.focus if c and client.property.get(c, "floating") ~= s then client.property.set(c, "floating", s) local scr = c.screen if s == true then c:geometry(client.property.get(c, "floating_geometry")) end c.screen = scr end end local function store_floating_geometry(c) if client.floating.get(c) then client.property.set(c, "floating_geometry", c:geometry()) end end -- Store the initial client geometry. capi.client.connect_signal("new", function(c) local function store_init_geometry(c) client.property.set(c, "floating_geometry", c:geometry()) c:disconnect_signal("property::border_width", store_init_geometry) end c:connect_signal("property::border_width", store_init_geometry) end) capi.client.connect_signal("property::geometry", store_floating_geometry) --- Return if a client has a fixe size or not. -- @client c The client. function client.isfixed(c) local c = c or capi.client.focus if not c then return end local h = c.size_hints if h.min_width and h.max_width and h.max_height and h.min_height and h.min_width > 0 and h.max_width > 0 and h.max_height > 0 and h.min_height > 0 and h.min_width == h.max_width and h.min_height == h.max_height then return true end return false end --- Get a client floating state. -- @client c A client. -- @return True or false. Note that some windows might be floating even if you -- did not set them manually. For example, windows with a type different than -- normal. function client.floating.get(c) local c = c or capi.client.focus if c then local value = client.property.get(c, "floating") if value ~= nil then return value end if c.type ~= "normal" or c.fullscreen or c.maximized_vertical or c.maximized_horizontal or client.isfixed(c) then return true end return false end end --- Toggle the floating state of a client between 'auto' and 'true'. -- @client c A client. function client.floating.toggle(c) local c = c or capi.client.focus -- If it has been set to floating if client.floating.get(c) then client.floating.set(c, false) else client.floating.set(c, true) end end --- Remove the floating information on a client. -- @client c The client. function client.floating.delete(c) client.floating.set(c, nil) end --- Restore (=unminimize) a random client. -- @param s The screen to use. -- @return The restored client if some client was restored, otherwise nil. function client.restore(s) local s = s or (capi.client.focus and capi.client.focus.screen) or capi.mouse.screen local cls = capi.client.get(s) local tags = tag.selectedlist(s) local mcls = {} for k, c in pairs(cls) do local ctags = c:tags() if c.minimized then for k, t in ipairs(tags) do if util.table.hasitem(ctags, t) then c.minimized = false return c end end end end return nil end --- Normalize a set of numbers to 1 -- @param set the set of numbers to normalize -- @param num the number of numbers to normalize local function normalize(set, num) local num = num or #set local total = 0 if num then for i = 1,num do total = total + set[i] end for i = 1,num do set[i] = set[i] / total end else for i,v in ipairs(set) do total = total + v end for i,v in ipairs(set) do set[i] = v / total end end end --- Calculate a client's column number, index in that column, and -- number of visible clients in this column. -- -- @client c the client -- @return col the column number -- @return idx index of the client in the column -- @return num the number of visible clients in the column function client.idx(c) local c = c or capi.client.focus if not c then return end local clients = client.tiled(c.screen) local idx = nil for k, cl in ipairs(clients) do if cl == c then idx = k break end end local t = tag.selected(c.screen) local nmaster = tag.getnmaster(t) if idx <= nmaster then return {idx = idx, col=0, num=nmaster} end local nother = #clients - nmaster idx = idx - nmaster -- rather than regenerate the column number we can calculate it -- based on the how the tiling algorithm places clients we calculate -- the column, we could easily use the for loop in the program but we can -- calculate it. local ncol = tag.getncol(t) -- minimum number of clients per column local percol = math.floor(nother / ncol) -- number of columns with an extra client local overcol = math.fmod(nother, ncol) -- number of columns filled with [percol] clients local regcol = ncol - overcol local col = math.floor( (idx - 1) / percol) + 1 if col > regcol then -- col = math.floor( (idx - (percol*regcol) - 1) / (percol + 1) ) + regcol + 1 -- simplified col = math.floor( (idx + regcol + percol) / (percol+1) ) -- calculate the index in the column idx = idx - percol*regcol - (col - regcol - 1) * (percol+1) percol = percol+1 else idx = idx - percol*(col-1) end return {idx = idx, col=col, num=percol} end --- Set the window factor of a client -- -- @param wfact the window factor value -- @client c the client function client.setwfact(wfact, c) -- get the currently selected window local c = c or capi.client.focus if not c or not c:isvisible() then return end local t = tag.selected(c.screen) local w = client.idx(c) local cls = client.tiled(tag.getscreen(t)) local nmaster = tag.getnmaster(t) -- n is the number of windows currently visible for which we have to be concerned with the properties local data = tag.getproperty(t, "windowfact") or {} local colfact = data[w.col] colfact[w.idx] = wfact local rest = 1-wfact -- calculate the current denominator local total = 0 for i = 1,w.num do if i ~= w.idx then total = total + colfact[i] end end -- normalize the windows for i = 1,w.num do if i ~= w.idx then colfact[i] = (colfact[i] * rest) / total end end t:emit_signal("property::windowfact") end --- Increment a client's window factor -- -- @param add amount to increase the client's window -- @client c the client function client.incwfact(add, c) local c = c or capi.client.focus if not c then return end local t = tag.selected(c.screen) local w = client.idx(c) local nmaster = tag.getnmaster(t) local data = tag.getproperty(t, "windowfact") or {} local colfact = data[w.col] curr = colfact[w.idx] or 1 colfact[w.idx] = curr + add -- keep our ratios normalized normalize(colfact, w.num) t:emit_signal("property::windowfact") end --- Get a client dockable state. -- -- @client c A client. -- @return True or false. Note that some windows might be dockable even if you -- did not set them manually. For example, windows with a type "utility", -- "toolbar" or "dock" function client.dockable.get(c) local value = client.property.get(c, "dockable") -- Some sane defaults if value == nil then if (c.type == "utility" or c.type == "toolbar" or c.type == "dock") then value = true else value = false end end return value end --- Set a client dockable state, overriding auto-detection. -- With this enabled you can dock windows by moving them from the center -- to the edge of the workarea. -- -- @client c A client. -- @param value True or false. function client.dockable.set(c, value) client.property.set(c, "dockable", value) end --- Get a client property. -- -- @client c The client. -- @param prop The property name. -- @return The property. function client.property.get(c, prop) if not client.data.persistent_properties_loaded[c] then client.data.persistent_properties_loaded[c] = true for p in pairs(client.data.persistent_properties_registered) do local value = c:get_xproperty("awful.client.property." .. p) if value ~= nil then client.property.set(c, p, value) end end end if client.data.properties[c] then return client.data.properties[c][prop] end end --- Set a client property. -- These properties are internal to awful. Some are used to move clients, etc. -- -- @client c The client. -- @param prop The property name. -- @param value The value. function client.property.set(c, prop, value) if not client.data.properties[c] then client.data.properties[c] = {} end if client.data.persistent_properties_registered[prop] then c:set_xproperty("awful.client.property." .. prop, value) end client.data.properties[c][prop] = value c:emit_signal("property::" .. prop) end --- Set a client property to be persistent across restarts (via X properties). -- -- @param prop The property name. -- @param type The type (used for register_xproperty). -- One of "string", "number" or "boolean". function client.property.persist(prop, type) local xprop = "awful.client.property." .. prop capi.awesome.register_xproperty(xprop, type) client.data.persistent_properties_registered[prop] = true -- Make already-set properties persistent for c, tab in pairs(client.data.properties) do if client.data.properties[c] and client.data.properties[c][prop] ~= nil then c:set_xproperty(xprop, client.data.properties[c][prop]) end end end --- -- Returns an iterator to cycle through, starting from the client in focus or -- the given index, all clients that match a given criteria. -- -- @param filter a function that returns true to indicate a positive match -- @param start what index to start iterating from. Defaults to using the -- index of the currently focused client. -- @param s which screen to use. nil means all screens. -- -- @usage -- un-minimize all urxvt instances -- local urxvt = function (c) -- return awful.rules.match(c, {class = "URxvt"}) -- end -- -- for c in awful.client.iterate(urxvt) do -- c.minimized = false -- end function client.iterate(filter, start, s) local clients = capi.client.get(s) local focused = capi.client.focus local start = start or util.table.hasitem(clients, focused) return util.table.iterate(clients, filter, start) end --- Switch to a client matching the given condition if running, else spawn it. -- If multiple clients match the given condition then the next one is -- focussed. -- -- @param cmd the command to execute -- @param matcher a function that returns true to indicate a matching client -- @param merge if true then merge tags when clients are not visible -- -- @usage -- run or raise urxvt (perhaps, with tabs) on modkey + semicolon -- awful.key({ modkey, }, 'semicolon', function () -- local matcher = function (c) -- return awful.rules.match(c, {class = 'URxvt'}) -- end -- awful.client.run_or_raise('urxvt', matcher) -- end); function client.run_or_raise(cmd, matcher, merge) local clients = capi.client.get() local findex = util.table.hasitem(clients, capi.client.focus) or 1 local start = util.cycle(#clients, findex + 1) for c in client.iterate(matcher, start) do client.jumpto(c, merge) return end -- client not found, spawn it util.spawn(cmd) end -- Register standards signals capi.client.add_signal("property::floating_geometry") capi.client.add_signal("property::floating") capi.client.add_signal("property::dockable") capi.client.add_signal("marked") capi.client.add_signal("unmarked") capi.client.connect_signal("focus", client.focus.history.add) capi.client.connect_signal("unmanage", client.focus.history.delete) capi.client.connect_signal("property::urgent", client.urgent.add) capi.client.connect_signal("focus", client.urgent.delete) capi.client.connect_signal("unmanage", client.urgent.delete) capi.client.connect_signal("unmanage", client.floating.delete) -- Register persistent properties client.property.persist("floating", "boolean") return client -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80