1 --- Shifty: Dynamic tagging library for awesome3-git
2 -- @author koniu <gkusnierz@gmail.com>
3 -- @author bioe007 <perry.hargrave@gmail.com>
5 -- http://awesome.naquadah.org/wiki/index.php?title=Shifty
18 local beautiful = require("beautiful")
19 local awful = require("awful")
22 local tonumber = tonumber
37 config.guess_name = true
38 config.guess_position = true
39 config.remember_index = true
41 config.default_name = "new"
42 config.clientkeys = {}
43 config.globalkeys = nil
45 config.prompt_sources = {"config_tags", "config_apps", "existing", "history"}
46 config.prompt_matchers = {"^", ":", ""}
49 local index_cache = {}
50 for i = 1, screen.count() do index_cache[i] = {} end
53 --{{{name2tags: matches string 'name' to tag objects
54 -- @param name : tag name to find
55 -- @param scr : screen to look for tags on
56 -- @return table of tag objects or nil
57 function name2tags(name, scr)
59 local a, b = scr or 1, scr or screen.count()
61 for i, t in ipairs(screen[s]:tags()) do
62 if name == t.name then
67 if #ret > 0 then return ret end
70 function name2tag(name, scr, idx)
71 local ts = name2tags(name, scr)
72 if ts then return ts[idx or 1] end
76 --{{{tag2index: finds index of a tag object
77 -- @param scr : screen number to look for tag on
78 -- @param tag : the tag object to find
79 -- @return the index [or zero] or end of the list
80 function tag2index(scr, tag)
81 for i, t in ipairs(screen[scr]:tags()) do
82 if t == tag then return i end
88 --@param tag: tag object to be renamed
89 --@param prefix: if any prefix is to be added
90 --@param no_selectall:
91 function rename(tag, prefix, no_selectall)
92 local theme = beautiful.get()
93 local t = tag or awful.tag.selected(mouse.screen)
97 local text = prefix or t.name
100 if t == awful.tag.selected(scr) then
101 bg = theme.bg_focus or '#535d6c'
102 fg = theme.fg_urgent or '#ffffff'
104 bg = theme.bg_normal or '#222222'
105 fg = theme.fg_urgent or '#ffffff'
108 awful.prompt.run({ fg_cursor = fg, bg_cursor = bg, ul_cursor = "single",
109 text = text, selectall = not no_selectall
111 -- taglist[scr][tag2index(scr, t)][2],
112 promptbox[scr].widget,
113 function (name) if name:len() > 0 then t.name = name; end end,
115 awful.util.getdir("cache") .. "/history_tags",
118 if t.name == before then
119 if awful.tag.getproperty(t, "initial") then del(t) end
121 awful.tag.setproperty(t, "initial", true)
125 t:emit_signal("property::name")
131 --{{{send: moves client to tag[idx]
132 -- maybe this isn't needed here in shifty?
133 -- @param idx the tag number to send a client to
135 local scr = client.focus.screen or mouse.screen
136 local sel = awful.tag.selected(scr)
137 local sel_idx = tag2index(scr, sel)
138 local tags = screen[scr]:tags()
139 local target = awful.util.cycle(#tags, sel_idx + idx)
140 awful.client.movetotag(tags[target], client.focus)
141 awful.tag.viewonly(tags[target])
144 function send_next() send(1) end
145 function send_prev() send(-1) end
148 --{{{pos2idx: translate shifty position to tag index
149 --@param pos: position (an integer)
150 --@param scr: screen number
151 function pos2idx(pos, scr)
154 for i = #screen[scr]:tags() , 1, -1 do
155 local t = screen[scr]:tags()[i]
156 if awful.tag.getproperty(t, "position") and
157 awful.tag.getproperty(t, "position") <= pos then
167 --{{{select : helper function chooses the first non-nil argument
168 --@param args - table of arguments
169 function select(args)
170 for i, a in pairs(args) do
178 --{{{tagtoscr : move an entire tag to another screen
180 --@param scr : the screen to move tag to
181 --@param t : the tag to be moved [awful.tag.selected()]
183 function tagtoscr(scr, t)
184 -- break if called with an invalid screen number
185 if not scr or scr < 1 or scr > screen.count() then return end
187 local otag = t or awful.tag.selected()
189 -- set screen and then reset tag to order properly
190 if #otag:clients() > 0 then
191 for _ , c in ipairs(otag:clients()) do
196 awful.client.toggletag(otag, c)
204 --{{{set : set a tags properties
206 --@param args : a table of optional (?) tag properties
207 --@return t - the tag object
208 function set(t, args)
209 if not t then return end
210 if not args then args = {} end
213 t.name = args.name or t.name
215 -- attempt to load preset on initial run
216 local preset = (awful.tag.getproperty(t, "initial") and
217 config.tags[t.name]) or {}
219 -- pick screen and get its tag table
220 local scr = args.screen or
221 (not t.screen and preset.screen) or
225 local clientstomove = nil
226 if scr > screen.count() then scr = screen.count() end
227 if t.screen and scr ~= t.screen then
231 local tags = screen[scr]:tags()
233 -- try to guess position from the name
234 local guessed_position = nil
235 if not (args.position or preset.position) and config.guess_position then
236 local num = t.name:find('^[1-9]')
237 if num then guessed_position = tonumber(t.name:sub(1, 1)) end
240 -- allow preset.layout to be a table to provide a different layout per
241 -- screen for a given tag
242 local preset_layout = preset.layout
243 if preset_layout and preset_layout[scr] then
244 preset_layout = preset.layout[scr]
247 -- select from args, preset, getproperty,
248 -- config.defaults.configs or defaults
250 layout = select{args.layout, preset_layout,
251 awful.tag.getproperty(t, "layout"),
252 config.defaults.layout, awful.layout.suit.tile},
253 mwfact = select{args.mwfact, preset.mwfact,
254 awful.tag.getproperty(t, "mwfact"),
255 config.defaults.mwfact, 0.55},
256 nmaster = select{args.nmaster, preset.nmaster,
257 awful.tag.getproperty(t, "nmaster"),
258 config.defaults.nmaster, 1},
259 ncol = select{args.ncol, preset.ncol,
260 awful.tag.getproperty(t, "ncol"),
261 config.defaults.ncol, 1},
262 matched = select{args.matched, awful.tag.getproperty(t, "matched")},
263 exclusive = select{args.exclusive, preset.exclusive,
264 awful.tag.getproperty(t, "exclusive"),
265 config.defaults.exclusive},
266 persist = select{args.persist, preset.persist,
267 awful.tag.getproperty(t, "persist"),
268 config.defaults.persist},
269 nopopup = select{args.nopopup, preset.nopopup,
270 awful.tag.getproperty(t, "nopopup"),
271 config.defaults.nopopup},
272 leave_kills = select{args.leave_kills, preset.leave_kills,
273 awful.tag.getproperty(t, "leave_kills"),
274 config.defaults.leave_kills},
275 max_clients = select{args.max_clients, preset.max_clients,
276 awful.tag.getproperty(t, "max_clients"),
277 config.defaults.max_clients},
278 position = select{args.position, preset.position, guessed_position,
279 awful.tag.getproperty(t, "position")},
280 icon = select{args.icon and image(args.icon),
281 preset.icon and image(preset.icon),
282 awful.tag.getproperty(t, "icon"),
283 config.defaults.icon and image(config.defaults.icon)},
284 icon_only = select{args.icon_only, preset.icon_only,
285 awful.tag.getproperty(t, "icon_only"),
286 config.defaults.icon_only},
287 sweep_delay = select{args.sweep_delay, preset.sweep_delay,
288 awful.tag.getproperty(t, "sweep_delay"),
289 config.defaults.sweep_delay},
290 overload_keys = select{args.overload_keys, preset.overload_keys,
291 awful.tag.getproperty(t, "overload_keys"),
292 config.defaults.overload_keys},
295 -- get layout by name if given as string
296 if type(props.layout) == "string" then
297 props.layout = getlayout(props.layout)
301 if args.keys or preset.keys then
302 local keys = awful.util.table.join(config.globalkeys,
303 args.keys or preset.keys)
304 if props.overload_keys then
307 props.keys = squash_keys(keys)
311 -- calculate desired taglist index
312 local index = args.index or preset.index or config.defaults.index
313 local rel_index = args.rel_index or
315 config.defaults.rel_index
316 local sel = awful.tag.selected(scr)
317 --TODO: what happens with rel_idx if no tags selected
318 local sel_idx = (sel and tag2index(scr, sel)) or 0
319 local t_idx = tag2index(scr, t)
320 local limit = (not t_idx and #tags + 1) or #tags
324 idx = awful.util.cycle(limit, (t_idx or sel_idx) + rel_index)
326 idx = awful.util.cycle(limit, index)
327 elseif props.position then
328 idx = pos2idx(props.position, scr)
329 if t_idx and t_idx < idx then idx = idx - 1 end
330 elseif config.remember_index and index_cache[scr][t.name] then
331 idx = index_cache[scr][t.name]
332 elseif not t_idx then
336 -- if we have a new index, remove from old index and insert
338 if t_idx then table.remove(tags, t_idx) end
339 table.insert(tags, idx, t)
340 index_cache[scr][t.name] = idx
343 -- set tag properties and push the new tag table
344 screen[scr]:tags(tags)
345 for prop, val in pairs(props) do awful.tag.setproperty(t, prop, val) end
348 if awful.tag.getproperty(t, "initial") then
349 local spawn = args.spawn or preset.spawn or config.defaults.spawn
350 local run = args.run or preset.run or config.defaults.run
351 if spawn and args.matched ~= true then
352 awful.util.spawn_with_shell(spawn, scr)
354 if run then run(t) end
355 awful.tag.setproperty(t, "initial", nil)
362 function shift_next() set(awful.tag.selected(), {rel_index = 1}) end
363 function shift_prev() set(awful.tag.selected(), {rel_index = -1}) end
366 --{{{add : adds a tag
367 --@param args: table of optional arguments
370 if not args then args = {} end
371 local name = args.name or " "
373 -- initialize a new tag object and its data structure
374 local t = tag{name = name}
376 -- tell set() that this is the first time
377 awful.tag.setproperty(t, "initial", true)
379 -- apply tag settings
382 -- unless forbidden or if first tag on the screen, show the tag
383 if not (awful.tag.getproperty(t, "nopopup") or args.noswitch) or
384 #screen[t.screen]:tags() == 1 then
385 awful.tag.viewonly(t)
388 -- get the name or rename
392 -- FIXME: hack to delay rename for un-named tags for
393 -- tackling taglist refresh which disabled prompt
394 -- from being rendered until input
395 awful.tag.setproperty(t, "initial", true)
397 if args.position then
398 f = function() rename(t, args.rename, true); tmr:stop() end
400 f = function() rename(t); tmr:stop() end
402 tmr = timer({timeout = 0.01})
403 tmr:add_signal("timeout", f)
411 --{{{del : delete a tag
412 --@param tag : the tag to be deleted [current tag]
414 local scr = (tag and tag.screen) or mouse.screen or 1
415 local tags = screen[scr]:tags()
416 local sel = awful.tag.selected(scr)
418 local idx = tag2index(scr, t)
420 -- return if tag not empty (except sticky)
421 local clients = t:clients()
423 for i, c in ipairs(clients) do
424 if c.sticky then sticky = sticky + 1 end
426 if #clients > sticky then return end
428 -- store index for later
429 index_cache[scr][t.name] = idx
434 -- if the current tag is being deleted, restore from history
435 if t == sel and #tags > 1 then
436 awful.tag.history.restore(scr, 1)
437 -- this is supposed to cycle if history is invalid?
438 -- e.g. if many tags are deleted in a row
439 if not awful.tag.selected(scr) then
440 awful.tag.viewonly(tags[awful.util.cycle(#tags, idx - 1)])
444 -- FIXME: what is this for??
445 if client.focus then client.focus:raise() end
449 --{{{is_client_tagged : replicate behavior in tag.c - returns true if the
450 --given client is tagged with the given tag
451 function is_client_tagged(tag, client)
452 for i, c in ipairs(tag:clients()) do
461 --{{{match : handles app->tag matching, a replacement for the manage hook in
463 --@param c : client to be matched
464 function match(c, startup)
465 local nopopup, intrusive, nofocus, run, slave
466 local wfact, struts, geom, float
467 local target_tag_names, target_tags = {}, {}
470 local inst = c.instance
473 local keys = config.clientkeys or c:keys() or {}
474 local target_screen = mouse.screen
476 c.border_color = beautiful.border_normal
477 c.border_width = beautiful.border_width
479 -- try matching client to config.apps
480 for i, a in ipairs(config.apps) do
483 for k, w in ipairs(a.match) do
485 (cls and cls:find(w)) or
486 (inst and inst:find(w)) or
487 (name and name:find(w)) or
488 (role and role:find(w)) or
489 (typ and typ:find(w)) then
490 if a.screen then target_screen = a.screen end
492 if type(a.tag) == "string" then
493 target_tag_names = {a.tag}
495 target_tag_names = a.tag
498 if a.startup and startup then
499 a = awful.util.table.join(a, a.startup)
501 if a.geometry ~=nil then
502 geom = {x = a.geometry[1],
504 width = a.geometry[3],
505 height = a.geometry[4]}
507 if a.float ~= nil then float = a.float end
508 if a.slave ~=nil then slave = a.slave end
509 if a.border_width ~= nil then
510 c.border_width = a.border_width
512 if a.nopopup ~=nil then nopopup = a.nopopup end
513 if a.intrusive ~=nil then
514 intrusive = a.intrusive
516 if a.fullscreen ~=nil then
517 c.fullscreen = a.fullscreen
519 if a.honorsizehints ~=nil then
520 c.size_hints_honor = a.honorsizehints
522 if a.kill ~=nil then c:kill(); return end
523 if a.ontop ~= nil then c.ontop = a.ontop end
524 if a.above ~= nil then c.above = a.above end
525 if a.below ~= nil then c.below = a.below end
526 if a.buttons ~= nil then
529 if a.nofocus ~= nil then nofocus = a.nofocus end
530 if a.keys ~= nil then
531 keys = awful.util.table.join(keys, a.keys)
533 if a.hidden ~= nil then c.hidden = a.hidden end
534 if a.minimized ~= nil then
535 c.minimized = a.minimized
537 if a.dockable ~= nil then
538 awful.client.dockable.set(c, a.dockable)
540 if a.urgent ~= nil then
543 if a.opacity ~= nil then
544 c.opacity = a.opacity
546 if a.run ~= nil then run = a.run end
547 if a.sticky ~= nil then c.sticky = a.sticky end
548 if a.wfact ~= nil then wfact = a.wfact end
549 if a.struts then struts = a.struts end
550 if a.skip_taskbar ~= nil then
551 c.skip_taskbar = a.skip_taskbar
554 for kk, vv in pairs(a.props) do
555 awful.client.property.set(c, kk, vv)
567 -- set properties of floating clients
570 awful.client.floating.set(c, float)
571 -- if config.float_bars then
572 -- awful.titlebar.add(c, modkey)
573 awful.placement.centered(c, c.transient_for)
574 awful.placement.no_offscreen(c)
578 local sel = awful.tag.selectedlist(target_screen)
579 if not target_tag_names or #target_tag_names == 0 then
580 -- {{{if not matched to some names try putting
581 -- client in c.transient_for or current tags
582 if c.transient_for then
583 target_tags = c.transient_for:tags()
585 for i, t in ipairs(sel) do
586 local mc = awful.tag.getproperty(t, "max_clients")
588 not (awful.tag.getproperty(t, "exclusive") or
589 (mc and mc >= #t:clients())) then
590 table.insert(target_tags, t)
597 if (not target_tag_names or #target_tag_names == 0) and
598 (not target_tags or #target_tags == 0) then
599 -- {{{if we still don't know any target names/tags guess
600 -- name from class or use default
601 if config.guess_name and cls then
602 target_tag_names = {cls:lower()}
604 target_tag_names = {config.default_name}
609 if #target_tag_names > 0 and #target_tags == 0 then
610 -- {{{translate target names to tag objects, creating
612 for i, tn in ipairs(target_tag_names) do
614 for j, t in ipairs(name2tags(tn, target_screen) or
615 name2tags(tn) or {}) do
616 local mc = awful.tag.getproperty(t, "max_clients")
617 local tagged = is_client_tagged(t, c)
619 not (mc and (((#t:clients() >= mc) and not
621 (#t:clients() > mc))) or
627 table.insert(target_tags,
632 target_tags = awful.util.table.join(target_tags, res)
638 -- set client's screen/tag if needed
639 target_screen = target_tags[1].screen or target_screen
640 if c.screen ~= target_screen then c.screen = target_screen end
641 if slave then awful.client.setslave(c) end
644 if wfact then awful.client.setwfact(wfact, c) end
645 if geom then c:geometry(geom) end
646 if struts then c:struts(struts) end
650 if #target_tags > 0 and not startup then
651 -- {{{switch or highlight
652 for i, t in ipairs(target_tags) do
653 if not (nopopup or awful.tag.getproperty(t, "nopopup")) then
654 table.insert(showtags, t)
655 elseif not startup then
659 if #showtags > 0 then
661 -- iterate selected tags and and see if any targets
662 -- currently selected
663 for kk, vv in pairs(showtags) do
664 for _, tag in pairs(sel) do
671 awful.tag.viewmore(showtags, c.screen)
676 if not (nofocus or c.hidden or c.minimized) then
677 --{{{focus and raise accordingly or lower if supressed
678 if (target and target ~= sel) and
679 (awful.tag.getproperty(target, "nopopup") or nopopup) then
680 awful.client.focus.history.add(c)
690 if config.sloppy then
691 -- {{{Enable sloppy focus
692 c:add_signal("mouse::enter", function(c)
693 if awful.client.focus.filter(c) and
694 awful.layout.get(c.screen) ~= awful.layout.suit.magnifier then
701 -- execute run function if specified
702 if run then run(c, target) end
707 --{{{sweep : hook function that marks tags as used, visited,
708 --deserted also handles deleting used and empty tags
710 for s = 1, screen.count() do
711 for i, t in ipairs(screen[s]:tags()) do
712 local clients = t:clients()
714 for i, c in ipairs(clients) do
715 if c.sticky then sticky = sticky + 1 end
717 if #clients == sticky then
718 if awful.tag.getproperty(t, "used") and
719 not awful.tag.getproperty(t, "persist") then
720 if awful.tag.getproperty(t, "deserted") or
721 not awful.tag.getproperty(t, "leave_kills") then
722 local delay = awful.tag.getproperty(t, "sweep_delay")
727 tmr = timer({timeout = delay})
728 tmr:add_signal("timeout", f)
734 if awful.tag.getproperty(t, "visited") and
736 awful.tag.setproperty(t, "deserted", true)
741 awful.tag.setproperty(t, "used", true)
744 awful.tag.setproperty(t, "visited", true)
751 --{{{getpos : returns a tag to match position
752 -- @param pos : the index to find
753 -- @return v : the tag (found or created) at position == 'pos'
754 function getpos(pos, scr_arg)
758 local scr = scr_arg or mouse.screen or 1
760 -- search for existing tag assigned to pos
761 for i = 1, screen.count() do
762 for j, t in ipairs(screen[i]:tags()) do
763 if awful.tag.getproperty(t, "position") == pos then
764 table.insert(existing, t)
765 if t.selected and i == scr then
772 if #existing > 0 then
773 -- if making another of an existing tag, return the end of
774 -- the list the optional 2nd argument decides if we return
776 if scr_arg ~= nil then
777 for _, tag in pairs(existing) do
778 if tag.screen == scr_arg then return tag end
780 -- no tag with a position and scr_arg match found, clear
781 -- v and allow the subseqeunt conditions to be evaluated
785 existing[awful.util.cycle(#existing, selected + 1)]) or
791 -- search for preconf with 'pos' and create it
792 for i, j in pairs(config.tags) do
793 if j.position == pos then
796 noswitch = not switch})
801 -- not existing, not preconfigured
802 v = add({position = pos,
805 noswitch = not switch})
811 --{{{init : search shifty.config.tags for initial set of
814 local numscr = screen.count()
816 for i, j in pairs(config.tags) do
817 local scr = j.screen or {1}
818 if type(scr) ~= 'table' then
821 for _, s in pairs(scr) do
822 if j.init and (s <= numscr) then
834 --{{{count : utility function returns the index of a table element
835 --FIXME: this is currently used only in remove_dup, so is it really
837 function count(table, element)
839 for i, e in pairs(table) do
840 if element == e then v = v + 1 end
846 --{{{remove_dup : used by shifty.completion when more than one
847 --tag at a position exists
848 function remove_dup(table)
850 for i, entry in ipairs(table) do
851 if count(v, entry) == 0 then v[#v+ 1] = entry end
857 --{{{completion : prompt completion
859 function completion(cmd, cur_pos, ncomp, sources, matchers)
861 -- get sources and matches tables
862 sources = sources or config.prompt_sources
863 matchers = matchers or config.prompt_matchers
866 -- gather names from config.tags
867 config_tags = function()
869 for n, p in pairs(config.tags) do
874 -- gather names from config.apps
875 config_apps = function()
877 for i, p in pairs(config.apps) do
879 if type(p.tag) == "string" then
880 table.insert(ret, p.tag)
882 ret = awful.util.table.join(ret, p.tag)
888 -- gather names from existing tags, starting with the
890 existing = function()
892 for i = 1, screen.count() do
893 local s = awful.util.cycle(screen.count(),
894 mouse.screen + i - 1)
895 local tags = screen[s]:tags()
896 for j, t in pairs(tags) do
897 table.insert(ret, t.name)
902 -- gather names from history
905 local f = io.open(awful.util.getdir("cache") ..
907 for name in f:lines() do table.insert(ret, name) end
913 -- if empty, match all
914 if #cmd == 0 or cmd == " " then cmd = "" end
916 -- match all up to the cursor if moved or no matchphrase
918 cmd:sub(cur_pos, cur_pos+#matchp) ~= matchp then
919 matchp = cmd:sub(1, cur_pos)
922 -- find matching commands
924 for i, src in ipairs(sources) do
925 local source = get_source[src]()
926 for j, matcher in ipairs(matchers) do
927 for k, name in ipairs(source) do
928 if name:find(matcher .. matchp) then
929 table.insert(matches, name)
936 if #matches == 0 then return cmd, cur_pos end
939 matches = remove_dup(matches)
942 while ncomp > #matches do ncomp = ncomp - #matches end
944 -- put cursor at the end of the matched phrase
945 if #matches == 1 then
946 cur_pos = #matches[ncomp] + 1
948 cur_pos = matches[ncomp]:find(matchp) + #matchp
951 -- return match and position
952 return matches[ncomp], cur_pos
956 -- {{{tagkeys : hook function that sets keybindings per tag
958 local sel = awful.tag.selected(s.index)
959 local keys = awful.tag.getproperty(sel, "keys") or
961 if keys and sel.selected then root.keys(keys) end
965 -- {{{squash_keys: helper function which removes duplicate
966 -- keybindings by picking only the last one to be listed in keys
968 function squash_keys(keys)
971 for i, k in ipairs(keys) do
972 squashed[table.concat(k.modifiers) .. k.key] = k
974 for i, k in pairs(squashed) do
981 -- {{{getlayout: returns a layout by name
982 function getlayout(name)
983 for _, layout in ipairs(config.layouts) do
984 if awful.layout.getname(layout) == name then
992 client.add_signal("manage", match)
993 client.add_signal("unmanage", sweep)
994 client.remove_signal("manage", awful.tag.withcurrent)
996 for s = 1, screen.count() do
997 awful.tag.attached_add_signal(s, "property::selected", sweep)
998 awful.tag.attached_add_signal(s, "tagged", sweep)
999 screen[s]:add_signal("tag::history::update", tagkeys)
1003 -- vim:set ft=lua fdm=marker tw=80 ts=4 sw=4 et sta ai si: --