--------------------------------------------------------------------------- --- Useful functions for tag manipulation. -- -- @author Julien Danjou <julien@danjou.info> -- @copyright 2008 Julien Danjou -- @release @AWESOME_VERSION@ -- @module awful.tag --------------------------------------------------------------------------- -- Grab environment we need local util = require("awful.util") local ascreen = require("awful.screen") local timer = require("gears.timer") local beautiful = require("beautiful") local tostring = tostring local pairs = pairs local ipairs = ipairs local table = table local setmetatable = setmetatable local capi = { tag = tag, screen = screen, mouse = mouse, client = client, root = root } -- we use require("awful.client") inside functions to prevent circular dependencies. local client local tag = { mt = {} } -- Private data local data = {} data.history = {} data.tags = setmetatable({}, { __mode = 'k' }) -- History functions tag.history = {} tag.history.limit = 20 --- Move a tag to an absolute position in the screen[]:tags() table. -- @param new_index Integer absolute position in the table to insert. -- @param target_tag The tag that should be moved. If null, the currently -- selected tag is used. function tag.move(new_index, target_tag) local target_tag = target_tag or tag.selected() local scr = tag.getscreen(target_tag) local tmp_tags = tag.gettags(scr) if (not new_index) or (new_index < 1) or (new_index > #tmp_tags) then return end local rm_index = nil for i, t in ipairs(tmp_tags) do if t == target_tag then table.remove(tmp_tags, i) rm_index = i break end end table.insert(tmp_tags, new_index, target_tag) for i=new_index < rm_index and new_index or rm_index, #tmp_tags do local tmp_tag = tmp_tags[i] tag.setscreen(tmp_tag, scr) tag.setproperty(tmp_tag, "index", i) end end --- Swap 2 tags -- @param tag1 The first tag -- @param tag2 The second tag function tag.swap(tag1, tag2) local idx1, idx2 = tag.getidx(tag1), tag.getidx(tag2) local src2, src1 = tag.getscreen(tag2), tag.getscreen(tag1) tag.setscreen(tag2, src1) tag.move(idx1, tag2) tag.setscreen(tag1, scr2) tag.move(idx2, tag1) end --- Add a tag. -- @param name The tag name, a string -- @param props The tags properties, a table -- @return The created tag function tag.add(name, props) local properties = props or {} -- Be sure to set the screen before the tag is activated to avoid function -- connected to property::activated to be called without a valid tag. -- set properies cannot be used as this has to be set before the first signal -- is sent properties.screen = properties.screen or ascreen.focused() -- Index is also required properties.index = (#tag.gettags(properties.screen))+1 local newtag = capi.tag{ name = name } -- Start with a fresh property table to avoid collisions with unsupported data data.tags[newtag] = {screen=properties.screen, index=properties.index} newtag.activated = true for k, v in pairs(properties) do tag.setproperty(newtag, k, v) end return newtag end --- Create a set of tags and attach it to a screen. -- @param names The tag name, in a table -- @param screen The tag screen, or 1 if not set. -- @param layout The layout or layout table to set for this tags by default. -- @return A table with all created tags. function tag.new(names, screen, layout) local screen = screen or 1 local tags = {} for id, name in ipairs(names) do table.insert(tags, id, tag.add(name, {screen = screen, layout = (layout and layout[id]) or layout})) -- Select the first tag. if id == 1 then tags[id].selected = true end end return tags end --- Find a suitable fallback tag. -- @param screen The screen number to look for a tag on. [awful.screen.focused()] -- @param invalids A table of tags we consider unacceptable. [selectedlist(scr)] function tag.find_fallback(screen, invalids) local scr = screen or ascreen.focused() local t = invalids or tag.selectedlist(scr) for _, v in pairs(tag.gettags(scr)) do if not util.table.hasitem(t, v) then return v end end end --- Delete a tag. -- @param target_tag Optional tag object to delete. [selected()] -- @param fallback_tag Tag to assign stickied tags to. [~selected()] -- @return Returns true if the tag is successfully deleted, nil otherwise. -- If there are no clients exclusively on this tag then delete it. Any -- stickied clients are assigned to the optional 'fallback_tag'. -- If after deleting the tag there is no selected tag, try and restore from -- history or select the first tag on the screen. function tag.delete(target_tag, fallback_tag) -- abort if no tag is passed or currently selected local target_tag = target_tag or tag.selected() if target_tag == nil or target_tag.activated == false then return end local target_scr = tag.getscreen(target_tag) local tags = tag.gettags(target_scr) local idx = tag.getidx(target_tag) local ntags = #tags -- We can't use the target tag as a fallback. local fallback_tag = fallback_tag if fallback_tag == target_tag then return end -- No fallback_tag provided, try and get one. if fallback_tag == nil then fallback_tag = tag.find_fallback(target_scr, {target_tag}) end -- Abort if we would have un-tagged clients. local clients = target_tag:clients() if ( #clients > 0 and ntags <= 1 ) or fallback_tag == nil then return end -- Move the clients we can off of this tag. for _, c in pairs(clients) do local nb_tags = #c:tags() -- If a client has only this tag, or stickied clients with -- nowhere to go, abort. if (not c.sticky and nb_tags == 1) or (c.sticky and fallback_tag == nil) then return -- If a client has multiple tags, then do not move it to fallback elseif nb_tags < 2 then c:tags({fallback_tag}) end end -- delete the tag data.tags[target_tag].screen = nil target_tag.activated = false -- Update all indexes for i=idx+1, #tags do tag.setproperty(tags[i], "index", i-1) end -- If no tags are visible, try and view one. if tag.selected(target_scr) == nil and ntags > 0 then tag.history.restore(nil, 1) if tag.selected(target_scr) == nil then tags[tags[1] == target_tag and 2 or 1].selected = true end end return true end --- Update the tag history. -- @param obj Screen object. function tag.history.update(obj) local s = obj.index local curtags = tag.selectedlist(s) -- create history table if not data.history[s] then data.history[s] = {} else if data.history[s].current then -- Check that the list is not identical local identical = #data.history[s].current == #curtags if identical then for idx, _tag in ipairs(data.history[s].current) do if curtags[idx] ~= _tag then identical = false break end end end -- Do not update history the table are identical if identical then return end end -- Limit history if #data.history[s] >= tag.history.limit then for i = tag.history.limit, #data.history[s] do data.history[s][i] = nil end end end -- store previously selected tags in the history table table.insert(data.history[s], 1, data.history[s].current) data.history[s].previous = data.history[s][1] -- store currently selected tags data.history[s].current = setmetatable(curtags, { __mode = 'v' }) end --- Revert tag history. -- @param screen The screen number. -- @param idx Index in history. Defaults to "previous" which is a special index -- toggling between last two selected sets of tags. Number (eg 1) will go back -- to the given index in history. function tag.history.restore(screen, idx) local s = screen or ascreen.focused() local i = idx or "previous" local sel = tag.selectedlist(s) -- do nothing if history empty if not data.history[s] or not data.history[s][i] then return end -- if all tags been deleted, try next entry if #data.history[s][i] == 0 then if i == "previous" then i = 0 end tag.history.restore(s, i + 1) return end -- deselect all tags tag.viewnone(s) -- select tags from the history entry for _, t in ipairs(data.history[s][i]) do t.selected = true end -- update currently selected tags table data.history[s].current = data.history[s][i] -- store previously selected tags data.history[s].previous = setmetatable(sel, { __mode = 'v' }) -- remove the reverted history entry if i ~= "previous" then table.remove(data.history[s], i) end capi.screen[s]:emit_signal("tag::history::update") end --- Get a list of all tags on a screen -- @param s Screen number -- @return A table with all available tags function tag.gettags(s) local tags = {} for i, t in ipairs(root.tags()) do if tag.getscreen(t) == s then table.insert(tags, t) end end table.sort(tags, function(a, b) return (tag.getproperty(a, "index") or 9999) < (tag.getproperty(b, "index") or 9999) end) return tags end --- Set a tag's screen -- @param t tag object -- @param s Screen number function tag.setscreen(t, s) local s = s or ascreen.focused() local sel = tag.selected local old_screen = tag.getproperty(t, "screen") if s == old_screen then return end -- Keeping the old index make very little sense when changing screen tag.setproperty(t, "index", nil) -- Change the screen tag.setproperty(t, "screen", s) -- Make sure the client's screen matches its tags for k,c in ipairs(t:clients()) do c.screen = s --Move all clients c:tags({t}) end -- Update all indexes for _,screen in ipairs {old_screen, s} do for i,t in ipairs(tag.gettags(screen)) do tag.setproperty(t, "index", i) end end -- Restore the old screen history if the tag was selected if sel then tag.history.restore(old_screen, 1) end end --- Get a tag's screen -- @param[opt] t tag object -- @return Screen number function tag.getscreen(t) local t = t or tag.selected() return tag.getproperty(t, "screen") end --- Return a table with all visible tags -- @param s Screen number. -- @return A table with all selected tags. function tag.selectedlist(s) local screen = s or ascreen.focused() local tags = tag.gettags(screen) local vtags = {} for i, t in pairs(tags) do if t.selected then vtags[#vtags + 1] = t end end return vtags end --- Return only the first visible tag. -- @param s Screen number. function tag.selected(s) return tag.selectedlist(s)[1] end --- Set master width factor. -- @param mwfact Master width factor. -- @param t The tag to modify, if null tag.selected() is used. function tag.setmwfact(mwfact, t) local t = t or tag.selected() if mwfact >= 0 and mwfact <= 1 then tag.setproperty(t, "mwfact", mwfact) end end --- Increase master width factor. -- @param add Value to add to master width factor. -- @param t The tag to modify, if null tag.selected() is used. function tag.incmwfact(add, t) tag.setmwfact(tag.getmwfact(t) + add, t) end --- Get master width factor. -- @param[opt] t The tag. function tag.getmwfact(t) local t = t or tag.selected() return tag.getproperty(t, "mwfact") or 0.5 end --- Set the spacing between clients -- @param useless_gap The spacing between clients -- @param t The tag to modify, if null tag.selected() is used. function tag.setgap(useless_gap, t) local t = t or tag.selected() if useless_gap >= 0 then tag.setproperty(t, "useless_gap", useless_gap) end end --- Increase the spacing between clients -- @param add Value to add to the spacing between clients -- @param t The tag to modify, if null tag.selected() is used. function tag.incgap(add, t) tag.setgap(tag.getgap(t) + add, t) end --- Get the spacing between clients -- @param t Optional tag. function tag.getgap(t) local t = t or tag.selected() return tag.getproperty(t, "useless_gap") or beautiful.useless_gap or 0 end --- Set size fill policy for the master client(s) -- @tparam string policy Can be set to -- "expand" (fill all the available workarea) or -- "mwfact" (fill only an area inside the master width factor) -- @tparam[opt=tag.selected()] tag t The tag to modify function tag.setmfpol(policy, t) local t = t or tag.selected() tag.setproperty(t, "master_fill_policy", policy) end --- Toggle size fill policy for the master client(s) -- between "expand" and "mwfact" -- @tparam tag t The tag to modify, if null tag.selected() is used. function tag.togglemfpol(t) if tag.getmfpol(t) == "expand" then tag.setmfpol("mwfact", t) else tag.setmfpol("expand", t) end end --- Get size fill policy for the master client(s) -- @tparam[opt=tag.selected()] tag t The tag -- @treturn string Possible values are -- "expand" (fill all the available workarea, default one) or -- "mwfact" (fill only an area inside the master width factor) function tag.getmfpol(t) local t = t or tag.selected() return tag.getproperty(t, "master_fill_policy") or "expand" end --- Set the number of master windows. -- @param nmaster The number of master windows. -- @param[opt] t The tag. function tag.setnmaster(nmaster, t) local t = t or tag.selected() if nmaster >= 0 then tag.setproperty(t, "nmaster", nmaster) end end --- Get the number of master windows. -- @param[opt] t The tag. function tag.getnmaster(t) local t = t or tag.selected() return tag.getproperty(t, "nmaster") or 1 end --- Increase the number of master windows. -- @param add Value to add to number of master windows. -- @param[opt] t The tag to modify, if null tag.selected() is used. -- @tparam[opt=false] boolean sensible Limit nmaster based on the number of -- visible tiled windows? function tag.incnmaster(add, t, sensible) if sensible then client = client or require("awful.client") local ntiled = #client.tiled(tag.getscreen(t)) local nmaster = tag.getnmaster(t) if nmaster > ntiled then nmaster = ntiled end local newnmaster = nmaster + add if newnmaster > ntiled then newnmaster = ntiled end tag.setnmaster(newnmaster, t) else tag.setnmaster(tag.getnmaster(t) + add, t) end end --- Set the tag icon -- @param icon the icon to set, either path or image object -- @param _tag the tag function tag.seticon(icon, _tag) local _tag = _tag or tag.selected() tag.setproperty(_tag, "icon", icon) end --- Get the tag icon -- @param _tag the tag function tag.geticon(_tag) local _tag = _tag or tag.selected() return tag.getproperty(_tag, "icon") end --- Set number of column windows. -- @param ncol The number of column. -- @param t The tag to modify, if null tag.selected() is used. function tag.setncol(ncol, t) local t = t or tag.selected() if ncol >= 1 then tag.setproperty(t, "ncol", ncol) end end --- Get number of column windows. -- @param[opt] t The tag. function tag.getncol(t) local t = t or tag.selected() return tag.getproperty(t, "ncol") or 1 end --- Increase number of column windows. -- @param add Value to add to number of column windows. -- @param[opt] t The tag to modify, if null tag.selected() is used. -- @tparam[opt=false] boolean sensible Limit ncol based on the number of visible -- tiled windows? function tag.incncol(add, t, sensible) if sensible then client = client or require("awful.client") local ntiled = #client.tiled(tag.getscreen(t)) local nmaster = tag.getnmaster(t) local nsecondary = ntiled - nmaster local ncol = tag.getncol(t) if ncol > nsecondary then ncol = nsecondary end local newncol = ncol + add if newncol > nsecondary then newncol = nsecondary end tag.setncol(newncol, t) else tag.setncol(tag.getncol(t) + add, t) end end --- View no tag. -- @tparam[opt] int screen The screen number. function tag.viewnone(screen) local tags = tag.gettags(screen or ascreen.focused()) for i, t in pairs(tags) do t.selected = false end end --- View a tag by its taglist index. -- @param i The relative index to see. -- @param[opt] screen The screen number. function tag.viewidx(i, screen) local screen = screen or ascreen.focused() local tags = tag.gettags(screen) local showntags = {} for k, t in ipairs(tags) do if not tag.getproperty(t, "hide") then table.insert(showntags, t) end end local sel = tag.selected(screen) tag.viewnone(screen) for k, t in ipairs(showntags) do if t == sel then showntags[util.cycle(#showntags, k + i)].selected = true end end capi.screen[screen]:emit_signal("tag::history::update") end --- Get a tag's index in the gettags() table. -- @param query_tag The tag object to find. [selected()] -- @return The index of the tag, nil if the tag is not found. function tag.getidx(query_tag) local query_tag = query_tag or tag.selected() if query_tag == nil then return end for i, t in ipairs(tag.gettags(tag.getscreen(query_tag))) do if t == query_tag then return i end end end --- View next tag. This is the same as tag.viewidx(1). -- @param screen The screen number. function tag.viewnext(screen) return tag.viewidx(1, screen) end --- View previous tag. This is the same a tag.viewidx(-1). -- @param screen The screen number. function tag.viewprev(screen) return tag.viewidx(-1, screen) end --- View only a tag. -- @param t The tag object. function tag.viewonly(t) local tags = tag.gettags(tag.getscreen(t)) -- First, untag everyone except the viewed tag. for _, _tag in pairs(tags) do if _tag ~= t then _tag.selected = false end end -- Then, set this one to selected. -- We need to do that in 2 operations so we avoid flickering and several tag -- selected at the same time. t.selected = true capi.screen[tag.getscreen(t)]:emit_signal("tag::history::update") end --- View only a set of tags. -- @param tags A table with tags to view only. -- @param[opt] screen The screen number of the tags. function tag.viewmore(tags, screen) local screen = screen or ascreen.focused() local screen_tags = tag.gettags(screen) for _, _tag in ipairs(screen_tags) do if not util.table.hasitem(tags, _tag) then _tag.selected = false end end for _, _tag in ipairs(tags) do _tag.selected = true end capi.screen[screen]:emit_signal("tag::history::update") end --- Toggle selection of a tag -- @tparam tag t Tag to be toggled function tag.viewtoggle(t) t.selected = not t.selected capi.screen[tag.getscreen(t)]:emit_signal("tag::history::update") end --- Get tag data table. -- @tparam tag _tag The tag. -- @return The data table. function tag.getdata(_tag) return data.tags[_tag] end --- Get a tag property. -- @tparam tag _tag The tag. -- @tparam string prop The property name. -- @return The property. function tag.getproperty(_tag, prop) if data.tags[_tag] then return data.tags[_tag][prop] end end --- Set a tag property. -- This properties are internal to awful. Some are used to draw taglist, or to -- handle layout, etc. -- @param _tag The tag. -- @param prop The property name. -- @param value The value. function tag.setproperty(_tag, prop, value) if not data.tags[_tag] then data.tags[_tag] = {} end if data.tags[_tag][prop] ~= value then data.tags[_tag][prop] = value _tag:emit_signal("property::" .. prop) end end --- Tag a client with the set of current tags. -- @param c The client to tag. function tag.withcurrent(c) local tags = {} for k, t in ipairs(c:tags()) do if tag.getscreen(t) == c.screen then table.insert(tags, t) end end if #tags == 0 then tags = tag.selectedlist(c.screen) end if #tags == 0 then tags = tag.gettags(c.screen) end if #tags ~= 0 then c:tags(tags) end end local function attached_connect_signal_screen(screen, sig, func) capi.tag.connect_signal(sig, function(_tag, ...) if tag.getscreen(_tag) == screen then func(_tag) end end) end --- Add a signal to all attached tags and all tags that will be attached in the -- future. When a tag is detached from the screen, its signal is removed. -- @param screen The screen concerned, or all if nil. function tag.attached_connect_signal(screen, ...) if screen then attached_connect_signal_screen(screen, ...) else capi.tag.connect_signal(...) end end -- Register standard signals. capi.client.connect_signal("manage", function(c) -- If we are not managing this application at startup, -- move it to the screen where the mouse is. -- We only do it for "normal" windows (i.e. no dock, etc). if not awesome.startup and c.type ~= "desktop" and c.type ~= "dock" then if c.transient_for then c.screen = c.transient_for.screen if not c.sticky then c:tags(c.transient_for:tags()) end else c.screen = ascreen.focused() end end c:connect_signal("property::screen", tag.withcurrent) end) -- Keep track of the number of urgent clients. local function update_urgent(c, t, modif) local count = tag.getproperty(t, "urgent_count") or 0 count = (count + modif) >= 0 and (count + modif) or 0 tag.setproperty(t, "urgent" , count > 0) tag.setproperty(t, "urgent_count", count ) end -- Update the urgent counter when a client is tagged. local function client_tagged(c, t) if c.urgent then update_urgent(c, t, 1) end end -- Update the urgent counter when a client is untagged. local function client_untagged(c, t) if c.urgent then update_urgent(c, t, -1) end end -- Count the urgent clients. local function urgent_callback(c) for k,t in ipairs(c:tags()) do update_urgent(c, t, c.urgent and 1 or -1) end end capi.client.connect_signal("property::urgent", urgent_callback) capi.client.connect_signal("manage", tag.withcurrent) capi.client.connect_signal("untagged", client_untagged) capi.client.connect_signal("tagged", client_tagged) capi.tag.connect_signal("request::select", tag.viewonly) capi.tag.add_signal("property::hide") capi.tag.add_signal("property::icon") capi.tag.add_signal("property::icon_only") capi.tag.add_signal("property::layout") capi.tag.add_signal("property::mwfact") capi.tag.add_signal("property::useless_gap") capi.tag.add_signal("property::master_fill_policy") capi.tag.add_signal("property::ncol") capi.tag.add_signal("property::nmaster") capi.tag.add_signal("property::windowfact") capi.tag.add_signal("property::screen") capi.tag.add_signal("property::index") capi.tag.add_signal("property::urgent") capi.tag.add_signal("property::urgent_count") capi.screen.add_signal("tag::history::update") for s = 1, capi.screen.count() do capi.screen[s]:connect_signal("tag::history::update", tag.history.update) end function tag.mt:__call(...) return tag.new(...) end return setmetatable(tag, tag.mt) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80