bug fix: remove tags from history after they vanish
[wmiirc-lua.git] / core / wmii.lua
blobec053e1366f95acbec7a6d64b195e2e3cd341c94
1 --
2 -- Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
3 --
4 -- WMII event loop, in lua
5 --
6 -- http://www.jukie.net/~bart/blog/tag/wmiirc-lua
7 -- git://www.jukie.net/wmiirc-lua.git/
8 --
10 -- ========================================================================
11 -- DOCUMENTATION
12 -- ========================================================================
13 --[[
14 =pod
16 =head1 NAME
18 wmii.lua - WMII event-loop methods in lua
20 =head1 SYNOPSIS
22 require "wmii"
24 -- Write something to the wmii filesystem, in this case a key event.
25 wmii.write ("/event", "Key Mod1-j")
27 -- Set your wmii /ctl parameters
28 wmii.set_ctl({
29 font = '....'
32 -- Configure wmii.lua parameters
33 wmii.set_conf ({
34 xterm = 'x-terminal-emulator'
37 -- Now start the event loop
38 wmii.run_event_loop()
40 =head1 DESCRIPTION
42 wmii.lua provides methods for replacing the stock sh-based wmiirc shipped with
43 wmii 3.6 and newer with a lua-based event loop.
45 It should be used by your wmiirc
47 =head1 METHODS
49 =over 4
51 =cut
52 --]]
54 -- ========================================================================
55 -- MODULE SETUP
56 -- ========================================================================
58 local wmiidir = os.getenv("HOME") .. "/.wmii-3.5"
59 local wmiirc = wmiidir .. "/wmiirc"
61 package.path = wmiidir .. "/core/?.lua;" ..
62 wmiidir .. "/plugins/?.lua;" ..
63 package.path
64 package.cpath = wmiidir .. "/core/?.so;" ..
65 wmiidir .. "/plugins/?.so;" ..
66 package.cpath
68 local ixp = require "ixp"
69 local eventloop = require "eventloop"
70 local history = require "history"
72 local io = require("io")
73 local os = require("os")
74 local string = require("string")
75 local table = require("table")
76 local math = require("math")
77 local type = type
78 local error = error
79 local print = print
80 local pcall = pcall
81 local pairs = pairs
82 local package = package
83 local require = require
84 local tostring = tostring
85 local tonumber = tonumber
86 local setmetatable = setmetatable
88 -- kinda silly, but there is no working liblua5.1-posix0 in ubuntu
89 -- so we make it optional
90 local have_posix, posix = pcall(require,"posix")
92 module("wmii")
94 -- get the process id
95 local myid
96 if have_posix then
97 myid = posix.getprocessid("pid")
98 else
99 local now = tonumber(os.date("%s"))
100 math.randomseed(now)
101 myid = math.random(10000)
104 -- ========================================================================
105 -- MODULE VARIABLES
106 -- ========================================================================
108 -- wmiir points to the wmiir executable
109 -- TODO: need to make sure that wmiir is in path, and if not find it
110 local wmiir = "wmiir"
112 -- wmii_adr is the address we use when connecting using ixp
113 local wmii_adr = os.getenv("WMII_ADDRESS")
114 or ("unix!/tmp/ns." .. os.getenv("USER") .. "."
115 .. os.getenv("DISPLAY"):match("(:%d+)") .. "/wmii")
117 -- wmixp is the ixp context we use to talk to wmii
118 local wmixp = ixp.new(wmii_adr)
120 -- history of previous views, view_hist[#view_hist] is the last one
121 local view_hist = {} -- sorted with 1 being the oldest
122 local view_hist_max = 50 -- max number to keep track of
124 -- allow for a client to be forced to a tag
125 local next_client_goes_to_tag = nil
127 -- program and action histories
128 local prog_hist = history.new (20)
129 local action_hist = history.new(10)
131 -- where to find plugins
132 plugin_path = os.getenv("HOME") .. "/.wmii-3.5/plugins/?.so;"
133 .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.lua;"
134 .. "/usr/local/lib/lua/5.1/wmii/?.so;"
135 .. "/usr/local/share/lua/5.1/wmii/?.lua;"
136 .. "/usr/lib/lua/5.1/wmii/?.so;"
137 .. "/usr/share/lua/5.1/wmii/?.lua"
139 -- where to find wmiirc (see find_wmiirc())
140 wmiirc_path = os.getenv("HOME") .. "/.wmii-3.5/wmiirc.lua;"
141 .. os.getenv("HOME") .. "/.wmii-3.5/wmiirc;"
142 .. "/etc/X11/wmii-3.5/wmiirc.lua;"
143 .. "/etc/X11/wmii-3.5/wmiirc"
145 -- ========================================================================
146 -- LOCAL HELPERS
147 -- ========================================================================
149 --[[
150 =pod
152 =item log ( str )
154 Log the message provided in C<str>
156 Currently just writes to io.stderr
158 =cut
159 --]]
160 function log (str)
161 if get_conf("debug") then
162 io.stderr:write (str .. "\n")
166 --[[
167 =pod
169 =item find_wmiirc ( )
171 Locates the wmiirc script. It looks in ~/.wmii-3.5 and /etc/X11/wmii-3.5
172 for the first lua script bearing the name wmiirc.lua or wmiirc. Returns
173 first match.
175 =cut
176 --]]
177 function find_wmiirc()
178 local fn
179 for fn in string.gmatch(wmiirc_path, "[^;]+") do
180 -- try to locate the files locally
181 local file = io.open(fn, "r")
182 if file then
183 local txt = file:read("*line")
184 file:close()
185 if type(txt) == 'string' and txt:match("lua") then
186 return fn
190 return nil
194 -- ========================================================================
195 -- MAIN ACCESS FUNCTIONS
196 -- ========================================================================
198 --[[
199 =pod
201 =item ls ( dir, fmt )
203 List the wmii filesystem directory provided in C<dir>, in the format specified
204 by C<fmt>.
206 Returns an iterator of TODO
208 =cut
209 --]]
210 function ls (dir, fmt)
211 local verbose = fmt and fmt:match("l")
213 local s = wmixp:stat(dir)
214 if not s then
215 return function () return nil end
217 if s.modestr:match("^[^d]") then
218 return function ()
219 return stat2str(verbose, s)
223 local itr = wmixp:idir (dir)
224 if not itr then
225 --return function ()
226 return nil
227 --end
231 return function ()
232 local s = itr()
233 if s then
234 return stat2str(verbose, s)
236 return nil
240 local function stat2str(verbose, stat)
241 if verbose then
242 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
243 else
244 if stat.modestr:match("^d") then
245 return stat.name .. "/"
246 else
247 return stat.name
252 -- ------------------------------------------------------------------------
253 -- read all contents of a wmii virtual file
254 function read (file)
255 return wmixp:read (file)
258 -- ------------------------------------------------------------------------
259 -- return an iterator which walks all the lines in the file
261 -- example:
262 -- for event in wmii.iread("/ctl")
263 -- ...
264 -- end
266 -- NOTE: don't use iread for files that could block, as this will interfere
267 -- with timer processing and event delivery. Instead fork off a process to
268 -- execute wmiir and read back the responses via callback.
269 function iread (file)
270 return wmixp:iread(file)
273 -- ------------------------------------------------------------------------
274 -- create a wmii file, optionally write data to it
275 function create (file, data)
276 wmixp:create(file, data)
279 -- ------------------------------------------------------------------------
280 -- remove a wmii file
281 function remove (file)
282 wmixp:remove(file)
285 -- ------------------------------------------------------------------------
286 -- write a value to a wmii virtual file system
287 function write (file, value)
288 wmixp:write (file, value)
291 -- ------------------------------------------------------------------------
292 -- setup a table describing dmenu command
293 local function dmenu_cmd (prompt, iterator)
294 local cmdt = { "dmenu", "-b" }
295 local fn = get_ctl("font")
296 if fn then
297 cmdt[#cmdt+1] = "-fn"
298 cmdt[#cmdt+1] = fn
300 local normcolors = get_ctl("normcolors")
301 if normcolors then
302 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
303 if nf then
304 cmdt[#cmdt+1] = "-nf"
305 cmdt[#cmdt+1] = "'" .. nf .. "'"
307 if nb then
308 cmdt[#cmdt+1] = "-nb"
309 cmdt[#cmdt+1] = "'" .. nb .. "'"
312 local focuscolors = get_ctl("focuscolors")
313 if focuscolors then
314 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
315 if sf then
316 cmdt[#cmdt+1] = "-sf"
317 cmdt[#cmdt+1] = "'" .. sf .. "'"
319 if sb then
320 cmdt[#cmdt+1] = "-sb"
321 cmdt[#cmdt+1] = "'" .. sb .. "'"
324 if prompt then
325 cmdt[#cmdt+1] = "-p"
326 cmdt[#cmdt+1] = "'" .. prompt .. "'"
329 return cmdt
332 -- ------------------------------------------------------------------------
333 -- displays the menu given an table of entires, returns selected text
334 function menu (tbl, prompt)
335 local dmenu = dmenu_cmd(prompt)
337 local infile = os.tmpname()
338 local fh = io.open (infile, "w+")
340 local i,v
341 for i,v in pairs(tbl) do
342 if type(i) == 'number' and type(v) == 'string' then
343 fh:write (v)
344 else
345 fh:write (i)
347 fh:write ("\n")
349 fh:close()
351 local outfile = os.tmpname()
353 dmenu[#dmenu+1] = "<"
354 dmenu[#dmenu+1] = infile
355 dmenu[#dmenu+1] = ">"
356 dmenu[#dmenu+1] = outfile
358 local cmd = table.concat(dmenu," ")
359 os.execute (cmd)
361 fh = io.open (outfile, "r")
362 os.remove (outfile)
364 local sel = fh:read("*l")
365 fh:close()
367 return sel
370 -- ------------------------------------------------------------------------
371 -- displays the a tag selection menu, returns selected tag
372 function tag_menu ()
373 local tags = get_tags()
375 return menu(tags, "tag:")
378 -- ------------------------------------------------------------------------
379 -- displays the a program menu, returns selected program
380 function prog_menu ()
381 local dmenu = dmenu_cmd("cmd:")
383 local outfile = os.tmpname()
385 dmenu[#dmenu+1] = ">"
386 dmenu[#dmenu+1] = outfile
388 local hstt = { }
389 for n in prog_hist:walk_reverse_unique() do
390 hstt[#hstt+1] = "echo '" .. n .. "' ; "
393 local cmd = "(" .. table.concat(hstt)
394 .. "dmenu_path ) |"
395 .. table.concat(dmenu," ")
396 os.execute (cmd)
398 local fh = io.open (outfile, "rb")
399 os.remove (outfile)
401 local prog = fh:read("*l")
402 io.close (fh)
404 return prog
407 -- ------------------------------------------------------------------------
408 -- displays the a program menu, returns selected program
409 function get_tags()
410 local t = {}
411 local s
412 for s in wmixp:idir ("/tag") do
413 if s.name and not (s.name == "sel") then
414 t[#t + 1] = s.name
417 table.sort(t)
418 return t
421 -- ------------------------------------------------------------------------
422 -- displays the a program menu, returns selected program
423 function get_view()
424 local v = wmixp:read("/ctl") or ""
425 return v:match("view%s+(%S+)")
428 -- ------------------------------------------------------------------------
429 -- changes the current view to the name given
430 function set_view(sel)
431 local cur = get_view()
432 local all = get_tags()
434 if #all < 2 or sel == cur then
435 -- nothing to do if we have less then 2 tags
436 return
439 if not (type(sel) == "string") then
440 error ("string argument expected")
443 -- set new view
444 write ("/ctl", "view " .. sel)
447 -- ------------------------------------------------------------------------
448 -- changes the current view to the index given
449 function set_view_index(sel)
450 local cur = get_view()
451 local all = get_tags()
453 if #all < 2 then
454 -- nothing to do if we have less then 2 tags
455 return
458 local num = tonumber (sel)
459 if not num then
460 error ("number argument expected")
463 local name = all[sel]
464 if not name or name == cur then
465 return
468 -- set new view
469 write ("/ctl", "view " .. name)
472 -- ------------------------------------------------------------------------
473 -- chnages to current view by offset given
474 function set_view_ofs(jump)
475 local cur = get_view()
476 local all = get_tags()
478 if #all < 2 then
479 -- nothing to do if we have less then 2 tags
480 return
483 -- range check
484 if (jump < - #all) or (jump > #all) then
485 error ("view selector is out of range")
488 -- find the one that's selected index
489 local curi = nil
490 local i,v
491 for i,v in pairs (all) do
492 if v == cur then curi = i end
495 -- adjust by index
496 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
497 if (newi < - #all) or (newi > #all) then
498 error ("error computng new view")
501 write ("/ctl", "view " .. all[newi])
504 -- ------------------------------------------------------------------------
505 -- toggle between last view and current view
506 function toggle_view()
507 local last = view_hist[#view_hist]
508 if last then
509 set_view(last)
513 -- ========================================================================
514 -- ACTION HANDLERS
515 -- ========================================================================
517 local action_handlers = {
518 man = function (act, args)
519 local xterm = get_conf("xterm") or "xterm"
520 local page = args
521 if (not page) or (not page:match("%S")) then
522 page = wmiidir .. "/wmii.3lua"
524 local cmd = xterm .. " -e man " .. page .. " &"
525 log (" executing: " .. cmd)
526 os.execute (cmd)
527 end,
529 quit = function ()
530 write ("/ctl", "quit")
531 end,
533 exec = function (act, args)
534 local what = args or "wmii"
535 log (" asking wmii to exec " .. tostring(what))
536 cleanup()
537 write ("/ctl", "exec " .. what)
538 end,
540 xlock = function (act)
541 local cmd = get_conf("xlock") or "xscreensaver-command --lock"
542 os.execute (cmd)
543 end,
545 wmiirc = function ()
546 if have_posix then
547 local wmiirc = find_wmiirc()
548 if wmiirc then
549 log (" executing: lua " .. wmiirc)
550 cleanup()
551 posix.exec ("lua", wmiirc)
553 else
554 log("sorry cannot restart; you don't have lua's posix library.")
556 end,
558 urgent = function ()
559 wmixp:write ("/client/sel/ctl", "Urgent toggle")
560 end,
562 --[[
563 rehash = function ()
564 -- TODO: consider storing list of executables around, and
565 -- this will then reinitialize that list
566 log (" TODO: rehash")
567 end,
569 status = function ()
570 -- TODO: this should eventually update something on the /rbar
571 log (" TODO: status")
572 end,
573 --]]
576 --[[
577 =pod
579 =item add_action_handler (action, fn)
581 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
583 =cut
584 --]]
585 function add_action_handler (action, fn)
587 if type(action) ~= "string" or type(fn) ~= "function" then
588 error ("expecting a string and a function")
591 if action_handlers[action] then
592 error ("action handler already exists for '" .. action .. "'")
595 action_handlers[action] = fn
598 --[[
599 =pod
601 =item remove_action_handler (action)
603 Remove an action handler callback function for the given action string I<action>.
605 =cut
606 --]]
607 function remove_action_handler (action)
609 action_handlers[action] = nil
612 -- ========================================================================
613 -- KEY HANDLERS
614 -- ========================================================================
616 function ke_fullscreen_toggle()
617 wmixp:write ("/client/sel/ctl", "Fullscreen toggle")
620 function ke_view_starting_with_letter (letter)
621 local i,v
623 -- find the view name in history in reverse order
624 for i=#view_hist,1,-1 do
625 v = view_hist[i]
626 if letter == v:sub(1,1) then
627 set_view(v)
628 return true
632 -- otherwise just pick the first view that matches
633 local all = get_tags()
634 for i,v in pairs(all) do
635 if letter == v:sub(1,1) then
636 set_view_index (i)
637 return true
641 return false
644 function ke_handle_action()
645 local actions = { }
646 local seen = {}
648 local n
649 for n in action_hist:walk_reverse() do
650 if not seen[n] then
651 actions[#actions+1] = n
652 seen[n] = 1
656 local v
657 for n,v in pairs(action_handlers) do
658 if not seen[n] then
659 actions[#actions+1] = n
660 seen[n] = 1
664 local text = menu(actions, "action:")
665 if text then
666 log ("Action: " .. text)
667 local act = text
668 local args = nil
669 local si = text:find("%s")
670 if si then
671 act,args = string.match(text .. " ", "(%w+)%s(.+)")
673 if act then
674 local fn = action_handlers[act]
675 if fn then
676 action_hist:add (act)
677 local r, err = pcall (fn, act, args)
678 if not r then
679 log ("WARNING: " .. tostring(err))
687 local key_handlers = {
688 ["*"] = function (key)
689 log ("*: " .. key)
690 end,
692 -- execution and actions
693 ["Mod1-Return"] = function (key)
694 local xterm = get_conf("xterm") or "xterm"
695 log (" executing: " .. xterm)
696 os.execute (xterm .. " &")
697 end,
698 ["Mod1-Shift-Return"] = function (key)
699 local tag = tag_menu()
700 if tag then
701 local xterm = get_conf("xterm") or "xterm"
702 log (" executing: " .. xterm .. " on: " .. tag)
703 next_client_goes_to_tag = tag
704 os.execute (xterm .. " &")
706 end,
707 ["Mod1-a"] = function (key)
708 ke_handle_action()
709 end,
710 ["Mod1-p"] = function (key)
711 local prog = prog_menu()
712 if prog then
713 prog_hist:add(prog:match("(%w+)"))
714 log (" executing: " .. prog)
715 os.execute (prog .. " &")
717 end,
718 ["Mod1-Shift-p"] = function (key)
719 local tag = tag_menu()
720 if tag then
721 local prog = prog_menu()
722 if prog then
723 log (" executing: " .. prog .. " on: " .. tag)
724 next_client_goes_to_tag = tag
725 os.execute (prog .. " &")
728 end,
729 ["Mod1-Shift-c"] = function (key)
730 write ("/client/sel/ctl", "kill")
731 end,
733 -- HJKL active selection
734 ["Mod1-h"] = function (key)
735 write ("/tag/sel/ctl", "select left")
736 end,
737 ["Mod1-l"] = function (key)
738 write ("/tag/sel/ctl", "select right")
739 end,
740 ["Mod1-j"] = function (key)
741 write ("/tag/sel/ctl", "select down")
742 end,
743 ["Mod1-k"] = function (key)
744 write ("/tag/sel/ctl", "select up")
745 end,
747 -- HJKL movement
748 ["Mod1-Shift-h"] = function (key)
749 write ("/tag/sel/ctl", "send sel left")
750 end,
751 ["Mod1-Shift-l"] = function (key)
752 write ("/tag/sel/ctl", "send sel right")
753 end,
754 ["Mod1-Shift-j"] = function (key)
755 write ("/tag/sel/ctl", "send sel down")
756 end,
757 ["Mod1-Shift-k"] = function (key)
758 write ("/tag/sel/ctl", "send sel up")
759 end,
761 -- floating vs tiled
762 ["Mod1-space"] = function (key)
763 write ("/tag/sel/ctl", "select toggle")
764 end,
765 ["Mod1-Shift-space"] = function (key)
766 write ("/tag/sel/ctl", "send sel toggle")
767 end,
769 -- work spaces (# and @ are wildcards for numbers and letters)
770 ["Mod4-#"] = function (key, num)
771 -- first attempt to find a view that starts with the number requested
772 local num_str = tostring(num)
773 if not ke_view_starting_with_letter (num_str) then
774 -- if we fail, then set it to the index requested
775 set_view_index (num)
777 end,
778 ["Mod4-Shift-#"] = function (key, num)
779 write ("/client/sel/tags", tostring(num))
780 end,
781 ["Mod4-@"] = function (key, letter)
782 ke_view_starting_with_letter (letter)
783 end,
784 ["Mod4-Shift-@"] = function (key, letter)
785 local all = get_tags()
786 local i,v
787 for i,v in pairs(all) do
788 if letter == v:sub(1,1) then
789 write ("/client/sel/tags", v)
790 break
793 end,
794 ["Mod1-comma"] = function (key)
795 set_view_ofs (-1)
796 end,
797 ["Mod1-period"] = function (key)
798 set_view_ofs (1)
799 end,
800 ["Mod1-r"] = function (key)
801 -- got to the last view
802 toggle_view()
803 end,
805 -- switching views and retagging
806 ["Mod1-t"] = function (key)
807 -- got to a view
808 local tag = tag_menu()
809 if tag then
810 set_view (tag)
812 end,
813 ["Mod1-Shift-t"] = function (key)
814 -- move selected client to a tag
815 local tag = tag_menu()
816 if tag then
817 write ("/client/sel/tags", tag)
819 end,
820 ["Mod1-Shift-r"] = function (key)
821 -- move selected client to a tag, and follow
822 local tag = tag_menu()
823 if tag then
824 write ("/client/sel/tags", tag)
825 set_view(tag)
827 end,
828 ["Mod1-Control-t"] = function (key)
829 log (" TODO: Mod1-Control-t: " .. key)
830 end,
832 -- column modes
833 ["Mod1-d"] = function (key)
834 write("/tag/sel/ctl", "colmode sel default")
835 end,
836 ["Mod1-s"] = function (key)
837 write("/tag/sel/ctl", "colmode sel stack")
838 end,
839 ["Mod1-m"] = function (key)
840 write("/tag/sel/ctl", "colmode sel max")
841 end,
842 ["Mod1-f"] = function (key)
843 ke_fullscreen_toggle()
844 end,
846 -- changing client flags
847 ["Shift-Mod1-f"] = function (key)
848 log ("setting flags")
850 local cli = get_client ()
852 local flags = { "suspend", "raw" }
853 local current_flags = cli:flags_string()
855 local what = menu(flags, "current flags: " .. current_flags .. " toggle:")
857 cli:toggle(what)
858 end,
859 ["Mod4-space"] = function (key)
860 local cli = get_client ()
861 cli:toggle("raw")
862 end,
865 --[[
866 =pod
868 =item add_key_handler (key, fn)
870 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
872 =cut
873 --]]
874 function add_key_handler (key, fn)
876 if type(key) ~= "string" or type(fn) ~= "function" then
877 error ("expecting a string and a function")
880 if key_handlers[key] then
881 -- TODO: we may wish to allow multiple handlers for one keypress
882 error ("key handler already exists for '" .. key .. "'")
885 key_handlers[key] = fn
888 --[[
889 =pod
891 =item remove_key_handler (key)
893 Remove an key handler callback function for the given key I<key>.
895 Returns the handler callback function.
897 =cut
898 --]]
899 function remove_key_handler (key)
901 local fn = key_handlers[key]
902 key_handlers[key] = nil
903 return fn
906 --[[
907 =pod
909 =item remap_key_handler (old_key, new_key)
911 Remove a key handler callback function from the given key I<old_key>,
912 and assign it to a new key I<new_key>.
914 =cut
915 --]]
916 function remap_key_handler (old_key, new_key)
918 local fn = remove_key_handler(old_key)
920 return add_key_handler (new_key, fn)
924 -- ------------------------------------------------------------------------
925 -- update the /keys wmii file with the list of all handlers
926 local alphabet="abcdefghijklmnopqrstuvwxyz"
927 function update_active_keys ()
928 local t = {}
929 local x, y
930 for x,y in pairs(key_handlers) do
931 if x:find("%w") then
932 local i = x:find("#$")
933 if i then
934 local j
935 for j=0,9 do
936 t[#t + 1] = x:sub(1,i-1) .. j
938 else
939 i = x:find("@$")
940 if i then
941 local j
942 for j=1,alphabet:len() do
943 local a = alphabet:sub(j,j)
944 t[#t + 1] = x:sub(1,i-1) .. a
946 else
947 t[#t + 1] = tostring(x)
952 local all_keys = table.concat(t, "\n")
953 --log ("setting /keys to...\n" .. all_keys .. "\n");
954 write ("/keys", all_keys)
957 -- ------------------------------------------------------------------------
958 -- update the /lbar wmii file with the current tags
959 function update_displayed_tags ()
960 -- colours for /lbar
961 local fc = get_ctl("focuscolors") or ""
962 local nc = get_ctl("normcolors") or ""
964 -- build up a table of existing tags in the /lbar
965 local old = {}
966 local s
967 for s in wmixp:idir ("/lbar") do
968 old[s.name] = 1
971 -- for all actual tags in use create any entries in /lbar we don't have
972 -- clear the old table entries if we have them
973 local cur = get_view()
974 local all = get_tags()
975 local i,v
976 for i,v in pairs(all) do
977 local color = nc
978 if cur == v then
979 color = fc
981 if not old[v] then
982 create ("/lbar/" .. v, color .. " " .. v)
984 write ("/lbar/" .. v, color .. " " .. v)
985 old[v] = nil
988 -- anything left in the old table should be removed now
989 for i,v in pairs(old) do
990 if v then
991 remove("/lbar/"..i)
996 -- ========================================================================
997 -- EVENT HANDLERS
998 -- ========================================================================
1000 local widget_ev_handlers = {
1003 --[[
1004 =pod
1006 =item _handle_widget_event (ev, arg)
1008 Top-level event handler for redispatching events to widgets. This event
1009 handler is added for any widget event that currently has a widget registered
1010 for it.
1012 Valid widget events are currently
1014 RightBarMouseDown <buttonnumber> <widgetname>
1015 RightBarClick <buttonnumber> <widgetname>
1017 the "Click" event is sent on mouseup.
1019 The callbacks are given only the button number as their argument, to avoid the
1020 need to reparse.
1022 =cut
1023 --]]
1025 local function _handle_widget_event (ev, arg)
1027 log("_handle_widget_event: " .. tostring(ev) .. " - " .. tostring(arg))
1029 -- parse arg to strip out our widget name
1030 local number,wname = string.match(arg, "(%d+)%s+(.+)")
1032 -- check our dispatch table for that widget
1033 if not wname then
1034 log("Didn't find wname")
1035 return
1038 local wtable = widget_ev_handlers[wname]
1039 if not wtable then
1040 log("No widget cares about" .. wname)
1041 return
1044 local fn = wtable[ev] or wtable["*"]
1045 if fn then
1046 success, err = pcall( fn, ev, tonumber(number) )
1047 if not success then
1048 log("Callback had an error in _handle_widget_event: " .. tostring(err) )
1049 return nil
1051 else
1052 log("no function found for " .. ev)
1056 local ev_handlers = {
1057 ["*"] = function (ev, arg)
1058 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
1059 end,
1061 RightBarClick = _handle_widget_event,
1063 -- process timer events
1064 ProcessTimerEvents = function (ev, arg)
1065 process_timers()
1066 end,
1068 -- exit if another wmiirc started up
1069 Start = function (ev, arg)
1070 if arg then
1071 if arg == "wmiirc" then
1072 -- backwards compatibility with bash version
1073 log (" exiting; pid=" .. tostring(myid))
1074 cleanup()
1075 os.exit (0)
1076 else
1077 -- ignore if it came from us
1078 local pid = string.match(arg, "wmiirc (%d+)")
1079 if pid then
1080 local pid = tonumber (pid)
1081 if not (pid == myid) then
1082 log (" exiting; pid=" .. tostring(myid))
1083 cleanup()
1084 os.exit (0)
1089 end,
1091 -- tag management
1092 CreateTag = function (ev, arg)
1093 local nc = get_ctl("normcolors") or ""
1094 create ("/lbar/" .. arg, nc .. " " .. arg)
1095 end,
1096 DestroyTag = function (ev, arg)
1097 remove ("/lbar/" .. arg)
1099 -- remove the tag from history
1100 local i,v
1101 for i=#view_hist,1,-1 do
1102 v = view_hist[i]
1103 if arg == v then
1104 table.remove(view_hist,i)
1107 end,
1109 FocusTag = function (ev, arg)
1110 local fc = get_ctl("focuscolors") or ""
1111 create ("/lbar/" .. arg, fc .. " " .. arg)
1112 write ("/lbar/" .. arg, fc .. " " .. arg)
1113 end,
1114 UnfocusTag = function (ev, arg)
1115 local nc = get_ctl("normcolors") or ""
1116 create ("/lbar/" .. arg, nc .. " " .. arg)
1117 write ("/lbar/" .. arg, nc .. " " .. arg)
1119 -- don't duplicate the last entry
1120 if not (arg == view_hist[#view_hist]) then
1121 view_hist[#view_hist+1] = arg
1123 -- limit to view_hist_max
1124 if #view_hist > view_hist_max then
1125 table.remove(view_hist, 1)
1128 end,
1130 -- key event handling
1131 Key = function (ev, arg)
1132 log ("Key: " .. arg)
1133 local magic = nil
1134 -- can we find an exact match?
1135 local fn = key_handlers[arg]
1136 if not fn then
1137 local key = arg:gsub("-%d$", "-#")
1138 -- can we find a match with a # wild card for the number
1139 fn = key_handlers[key]
1140 if fn then
1141 -- convert the trailing number to a number
1142 magic = tonumber(arg:match("-(%d)$"))
1145 if not fn then
1146 local key = arg:gsub("-%a$", "-@")
1147 -- can we find a match with a @ wild card for a letter
1148 fn = key_handlers[key]
1149 if fn then
1150 -- split off the trailing letter
1151 magic = arg:match("-(%a)$")
1154 if not fn then
1155 -- everything else failed, try default match
1156 fn = key_handlers["*"]
1158 if fn then
1159 local r, err = pcall (fn, arg, magic)
1160 if not r then
1161 log ("WARNING: " .. tostring(err))
1164 end,
1166 -- mouse handling on the lbar
1167 LeftBarClick = function (ev, arg)
1168 local button,tag = string.match(arg, "(%w+)%s+(%S+)")
1169 set_view (tag)
1170 end,
1172 -- focus updates
1173 ClientFocus = function (ev, arg)
1174 log ("ClientFocus: " .. arg)
1175 client_focused (arg)
1176 end,
1177 ColumnFocus = function (ev, arg)
1178 log ("ColumnFocus: " .. arg)
1179 end,
1181 -- client handling
1182 CreateClient = function (ev, arg)
1183 if next_client_goes_to_tag then
1184 local tag = next_client_goes_to_tag
1185 local cli = arg
1186 next_client_goes_to_tag = nil
1187 write ("/client/" .. cli .. "/tags", tag)
1188 set_view(tag)
1190 client_created (arg)
1191 end,
1192 DestroyClient = function (ev, arg)
1193 client_destoryed (arg)
1194 end,
1196 -- urgent tag?
1197 UrgentTag = function (ev, arg)
1198 log ("UrgentTag: " .. arg)
1199 -- wmiir xwrite "/lbar/$@" "*$@"
1200 end,
1201 NotUrgentTag = function (ev, arg)
1202 log ("NotUrgentTag: " .. arg)
1203 -- wmiir xwrite "/lbar/$@" "$@"
1208 --[[
1209 =pod
1211 =item add_widget_event_handler (wname, ev, fn)
1213 Add an event handler callback for the I<ev> event on the widget named I<wname>
1215 =cut
1216 --]]
1218 function add_widget_event_handler (wname, ev, fn)
1219 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
1220 error ("expecting string for widget name, string for event name and a function callback")
1223 -- Make sure the widget event handler is present
1224 if not ev_handlers[ev] then
1225 ev_handlers[ev] = _handle_widget_event
1228 if not widget_ev_handlers[wname] then
1229 widget_ev_handlers[wname] = { }
1232 if widget_ev_handlers[wname][ev] then
1233 -- TODO: we may wish to allow multiple handlers for one event
1234 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
1237 widget_ev_handlers[wname][ev] = fn
1240 --[[
1241 =pod
1243 =item remove_widget_event_handler (wname, ev)
1245 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1247 =cut
1248 --]]
1249 function remove_event_handler (wname, ev)
1251 if not widget_ev_handlers[wname] then
1252 return
1255 widget_ev_handlers[wname][ev] = nil
1258 --[[
1259 =pod
1261 =item add_event_handler (ev, fn)
1263 Add an event handler callback function, I<fn>, for the given event I<ev>.
1265 =cut
1266 --]]
1267 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1268 function add_event_handler (ev, fn)
1269 if type(ev) ~= "string" or type(fn) ~= "function" then
1270 error ("expecting a string and a function")
1273 if ev_handlers[ev] then
1274 -- TODO: we may wish to allow multiple handlers for one event
1275 error ("event handler already exists for '" .. ev .. "'")
1279 ev_handlers[ev] = fn
1282 --[[
1283 =pod
1285 =item remove_event_handler (ev)
1287 Remove an event handler callback function for the given event I<ev>.
1289 =cut
1290 --]]
1291 function remove_event_handler (ev)
1293 ev_handlers[ev] = nil
1297 -- ========================================================================
1298 -- MAIN INTERFACE FUNCTIONS
1299 -- ========================================================================
1301 local config = {
1302 xterm = 'x-terminal-emulator',
1303 xlock = "xscreensaver-command --lock",
1304 debug = false,
1307 -- ------------------------------------------------------------------------
1308 -- write configuration to /ctl wmii file
1309 -- wmii.set_ctl({ "var" = "val", ...})
1310 -- wmii.set_ctl("var, "val")
1311 function set_ctl (first,second)
1312 if type(first) == "table" and second == nil then
1313 local x, y
1314 for x, y in pairs(first) do
1315 write ("/ctl", x .. " " .. y)
1318 elseif type(first) == "string" and type(second) == "string" then
1319 write ("/ctl", first .. " " .. second)
1321 else
1322 error ("expecting a table or two string arguments")
1326 -- ------------------------------------------------------------------------
1327 -- read a value from /ctl wmii file
1328 -- table = wmii.get_ctl()
1329 -- value = wmii.get_ctl("variable"
1330 function get_ctl (name)
1331 local s
1332 local t = {}
1333 for s in iread("/ctl") do
1334 local var,val = s:match("(%w+)%s+(.+)")
1335 if var == name then
1336 return val
1338 t[var] = val
1340 if not name then
1341 return t
1343 return nil
1346 -- ------------------------------------------------------------------------
1347 -- set an internal wmiirc.lua variable
1348 -- wmii.set_conf({ "var" = "val", ...})
1349 -- wmii.set_conf("var, "val")
1350 function set_conf (first,second)
1351 if type(first) == "table" and second == nil then
1352 local x, y
1353 for x, y in pairs(first) do
1354 config[x] = y
1357 elseif type(first) == "string"
1358 and (type(second) == "string"
1359 or type(second) == "number") then
1360 config[first] = second
1362 else
1363 error ("expecting a table, or string and string/number as arguments")
1367 -- ------------------------------------------------------------------------
1368 -- read an internal wmiirc.lua variable
1369 function get_conf (name)
1370 if name then
1371 return config[name]
1373 return config
1376 -- ========================================================================
1377 -- THE EVENT LOOP
1378 -- ========================================================================
1380 -- the event loop instance
1381 local el = eventloop.new()
1383 -- add the core event handler for events
1384 el:add_exec (wmiir .. " read /event",
1385 function (line)
1386 local line = line or "nil"
1388 -- try to split off the argument(s)
1389 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1390 if not ev then
1391 ev = line
1394 -- now locate the handler function and call it
1395 local fn = ev_handlers[ev] or ev_handlers["*"]
1396 if fn then
1397 local r, err = pcall (fn, ev, arg)
1398 if not r then
1399 log ("WARNING: " .. tostring(err))
1402 end)
1404 -- ------------------------------------------------------------------------
1405 -- run the event loop and process events, this function does not exit
1406 function run_event_loop ()
1407 -- stop any other instance of wmiirc
1408 wmixp:write ("/event", "Start wmiirc " .. tostring(myid))
1410 log("wmii: updating lbar")
1412 update_displayed_tags ()
1414 log("wmii: updating rbar")
1416 update_displayed_widgets ()
1418 log("wmii: updating active keys")
1420 update_active_keys ()
1422 log("wmii: starting event loop")
1423 while true do
1424 local sleep_for = process_timers()
1425 el:run_loop(sleep_for)
1429 -- ========================================================================
1430 -- PLUGINS API
1431 -- ========================================================================
1433 api_version = 0.1 -- the API version we export
1435 plugins = {} -- all plugins that were loaded
1437 -- ------------------------------------------------------------------------
1438 -- plugin loader which also verifies the version of the api the plugin needs
1440 -- here is what it does
1441 -- - does a manual locate on the file using package.path
1442 -- - reads in the file w/o using the lua interpreter
1443 -- - locates api_version=X.Y string
1444 -- - makes sure that api_version requested can be satisfied
1445 -- - if the plugins is available it will set variables passed in
1446 -- - it then loads the plugin
1448 -- TODO: currently the api_version must be in an X.Y format, but we may want
1449 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1451 function load_plugin(name, vars)
1452 local backup_path = package.path or "./?.lua"
1454 log ("loading " .. name)
1456 -- this is the version we want to find
1457 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1458 if (not api_major) or (not api_minor) then
1459 log ("WARNING: could not parse api_version in core/wmii.lua")
1460 return nil
1463 -- first find the plugin file
1464 local s, path_match, full_name, file
1465 for s in string.gmatch(plugin_path, "[^;]+") do
1466 -- try to locate the files locally
1467 local fn = s:gsub("%?", name)
1468 file = io.open(fn, "r")
1469 if file then
1470 path_match = s
1471 full_name = fn
1472 break
1476 -- read it in
1477 local txt
1478 if file then
1479 txt = file:read("*all")
1480 file:close()
1483 if not txt then
1484 log ("WARNING: could not load plugin '" .. name .. "'")
1485 return nil
1488 -- find the api_version line
1489 local line, plugin_version
1490 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1491 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1492 if plugin_version then
1493 break
1497 if not plugin_version then
1498 log ("WARNING: could not find api_version string in plugin '" .. name .. "'")
1499 return nil
1502 -- decompose the version string
1503 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1504 if (not plugin_major) or (not plugin_minor) then
1505 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1506 return nil
1509 -- make a version test
1510 if plugin_major ~= api_major then
1511 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1512 .. " (api " .. tonumber(api_version) .. ")")
1513 return nil
1516 if plugin_minor > api_minor then
1517 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1518 .. " (api " .. tonumber(api_version) .. ")")
1519 return nil
1522 -- the configuration parameters before loading
1523 if type(vars) == "table" then
1524 local var, val
1525 for var,val in pairs(vars) do
1526 local success = pcall (set_conf, name .. "." .. var, val)
1527 if not success then
1528 log ("WARNING: bad variable {" .. tostring(var) .. ", " .. tostring(val) .. "} "
1529 .. "given; loading '" .. name .. "' plugin failed.")
1530 return nil
1535 -- actually load the module, but use only the path where we though it should be
1536 package.path = path_match
1537 local success,what = pcall (require, name)
1538 package.path = backup_path
1539 if not success then
1540 log ("WARNING: failed to load '" .. name .. "' plugin")
1541 log (" - path: " .. tostring(path_match))
1542 log (" - file: " .. tostring(full_name))
1543 log (" - plugin's api_version: " .. tostring(plugin_version))
1544 log (" - reason: " .. tostring(what))
1545 return nil
1548 -- success
1549 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1550 plugins[name] = what
1551 return what
1554 -- ------------------------------------------------------------------------
1555 -- widget template
1556 widget = {}
1557 widgets = {}
1559 -- ------------------------------------------------------------------------
1560 -- create a widget object and add it to the wmii /rbar
1562 -- examples:
1563 -- widget = wmii.widget:new ("999_clock")
1564 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1565 function widget:new (name, fn)
1566 local o = {}
1568 if type(name) == "string" then
1569 o.name = name
1570 if type(fn) == "function" then
1571 o.fn = fn
1573 else
1574 error ("expected name followed by an optional function as arguments")
1577 setmetatable (o,self)
1578 self.__index = self
1579 self.__gc = function (o) o:hide() end
1581 widgets[name] = o
1583 o:show()
1584 return o
1587 -- ------------------------------------------------------------------------
1588 -- stop and destroy the timer
1589 function widget:delete ()
1590 widgets[self.name] = nil
1591 self:hide()
1594 -- ------------------------------------------------------------------------
1595 -- displays or updates the widget text
1597 -- examples:
1598 -- w:show("foo")
1599 -- w:show("foo", "#888888 #222222 #333333")
1600 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1602 function widget:show (txt, colors)
1603 local colors = colors or get_ctl("normcolors") or ""
1604 local txt = txt or self.txt or ""
1605 local towrite = txt
1606 if colors then
1607 towrite = colors .. " " .. towrite
1609 if not self.txt then
1610 create ("/rbar/" .. self.name, towrite)
1611 else
1612 write ("/rbar/" .. self.name, towrite)
1614 self.txt = txt
1617 -- ------------------------------------------------------------------------
1618 -- hides a widget and removes it from the bar
1619 function widget:hide ()
1620 if self.txt then
1621 remove ("/lbar/" .. self.name)
1622 self.txt = nil
1626 --[[
1627 =pod
1629 =item widget:add_event_handler (ev, fn)
1631 Add an event handler callback for this widget, using I<fn> for event I<ev>
1633 =cut
1634 --]]
1636 function widget:add_event_handler (ev, fn)
1637 add_widget_event_handler( self.name, ev, fn)
1641 -- ------------------------------------------------------------------------
1642 -- remove all /rbar entries that we don't have widget objects for
1643 function update_displayed_widgets ()
1644 -- colours for /rbar
1645 local nc = get_ctl("normcolors") or ""
1647 -- build up a table of existing tags in the /lbar
1648 local old = {}
1649 local s
1650 for s in wmixp:idir ("/rbar") do
1651 old[s.name] = 1
1654 -- for all actual widgets in use we want to remove them from the old list
1655 local i,v
1656 for i,v in pairs(widgets) do
1657 old[v.name] = nil
1660 -- anything left in the old table should be removed now
1661 for i,v in pairs(old) do
1662 if v then
1663 remove("/rbar/"..i)
1668 -- ------------------------------------------------------------------------
1669 -- create a new program and for each line it generates call the callback function
1670 -- returns fd which can be passed to kill_exec()
1671 function add_exec (command, callback)
1672 return el:add_exec (command, callback)
1675 -- ------------------------------------------------------------------------
1676 -- terminates a program spawned off by add_exec()
1677 function kill_exec (fd)
1678 return el:kill_exec (fd)
1681 -- ------------------------------------------------------------------------
1682 -- timer template
1683 timer = {}
1684 local timers = {}
1686 -- ------------------------------------------------------------------------
1687 -- create a timer object and add it to the event loop
1689 -- examples:
1690 -- timer:new (my_timer_fn)
1691 -- timer:new (my_timer_fn, 15)
1692 function timer:new (fn, seconds)
1693 local o = {}
1695 if type(fn) == "function" then
1696 o.fn = fn
1697 else
1698 error ("expected function followed by an optional number as arguments")
1701 setmetatable (o,self)
1702 self.__index = self
1703 self.__gc = function (o) o:stop() end
1705 -- add the timer
1706 timers[#timers+1] = o
1708 if seconds then
1709 o:resched(seconds)
1711 return o
1714 -- ------------------------------------------------------------------------
1715 -- stop and destroy the timer
1716 function timer:delete ()
1717 self:stop()
1718 local i,t
1719 for i,t in pairs(timers) do
1720 if t == self then
1721 table.remove (timers,i)
1722 return
1727 -- ------------------------------------------------------------------------
1728 -- run the timer given new interval
1729 function timer:resched (seconds)
1730 local seconds = seconds or self.interval
1731 if not (type(seconds) == "number") then
1732 error ("timer:resched expected number as argument")
1735 local now = tonumber(os.date("%s"))
1737 self.interval = seconds
1738 self.next_time = now + seconds
1740 -- resort the timer list
1741 table.sort (timers, timer.is_less_then)
1744 -- helper for sorting timers
1745 function timer:is_less_then(another)
1746 if not self.next_time then
1747 return false -- another is smaller, nil means infinity
1749 elseif not another.next_time then
1750 return true -- self is smaller, nil means infinity
1752 elseif self.next_time < another.next_time then
1753 return true -- self is smaller than another
1756 return false -- another is smaller then self
1759 -- ------------------------------------------------------------------------
1760 -- stop the timer
1761 function timer:stop ()
1762 self.next_time = nil
1764 -- resort the timer list
1765 table.sort (timers, timer.is_less_then)
1768 -- ------------------------------------------------------------------------
1769 -- figure out how long before the next event
1770 function time_before_next_timer_event()
1771 local tmr = timers[1]
1772 if tmr and tmr.next_time then
1773 local now = tonumber(os.date("%s"))
1774 local seconds = tmr.next_time - now
1775 if seconds > 0 then
1776 return seconds
1779 return 0 -- sleep for ever
1782 -- ------------------------------------------------------------------------
1783 -- handle outstanding events
1784 function process_timers ()
1785 local now = tonumber(os.date("%s"))
1786 local torun = {}
1787 local i,tmr
1789 for i,tmr in pairs (timers) do
1790 if not tmr then
1791 -- prune out removed timers
1792 table.remove(timers,i)
1793 break
1795 elseif not tmr.next_time then
1796 -- break out once we find a timer that is stopped
1797 break
1799 elseif tmr.next_time > now then
1800 -- break out once we get to the future
1801 break
1804 -- this one is good to go
1805 torun[#torun+1] = tmr
1808 for i,tmr in pairs (torun) do
1809 tmr:stop()
1810 local status,new_interval = pcall (tmr.fn, tmr)
1811 if status then
1812 new_interval = new_interval or self.interval
1813 if new_interval and (new_interval ~= -1) then
1814 tmr:resched(new_interval)
1816 else
1817 log ("ERROR: " .. tostring(new_interval))
1821 local sleep_for = time_before_next_timer_event()
1822 return sleep_for
1825 -- ------------------------------------------------------------------------
1826 -- cleanup everything in preparation for exit() or exec()
1827 function cleanup ()
1829 local i,v,tmr,p
1831 log ("wmii: stopping timer events")
1833 for i,tmr in pairs (timers) do
1834 pcall (tmr.delete, tmr)
1836 timers = {}
1838 log ("wmii: terminating eventloop")
1840 pcall(el.kill_all,el)
1842 log ("wmii: disposing of widgets")
1844 -- dispose of all widgets
1845 for i,v in pairs(widgets) do
1846 pcall(v.delete,v)
1848 timers = {}
1850 -- FIXME: it doesn't seem to do what I want
1851 --[[
1852 log ("wmii: releasing plugins")
1854 for i,p in pairs(plugins) do
1855 if p.cleanup then
1856 pcall (p.cleanup, p)
1859 plugins = {}
1860 --]]
1862 log ("wmii: dormant")
1865 -- ========================================================================
1866 -- CLIENT HANDLING
1867 -- ========================================================================
1869 --[[
1870 -- Notes on client tracking
1872 -- When a client is created wmii sends us a CreateClient message, and
1873 -- we in turn create a 'client' object and store it in the 'clients'
1874 -- table indexed by the client's ID.
1876 -- Each client object stores the following:
1877 -- .xid - the X client ID
1878 -- .pid - the process ID
1879 -- .prog - program object representing the process
1881 -- The client and program objects track the following modes for each program:
1883 -- raw mode:
1884 -- - for each client window
1885 -- - Mod4-space toggles the state between normal and raw
1886 -- - Mod1-f raw also toggles the state
1887 -- - in raw mode all input goes to the client, except for Mod4-space
1888 -- - a focused client with raw mode enabled is put into raw mode
1890 -- suspend mode:
1891 -- - for each program
1892 -- - Mod1-f suspend toggles the state for current client's program
1893 -- - a focused client, whose program was previous suspended is resumed
1894 -- - an unfocused client, with suspend enabled, will be suspended
1895 -- - suspend/resume is done by sending the STOP/CONT signals to the PID
1896 --]]
1898 function xid_to_pid (xid)
1899 local cmd = "xprop -id " .. tostring(xid) .. " _NET_WM_PID"
1900 local file = io.popen (cmd)
1901 local out = file:read("*a")
1902 file:close()
1903 local pid = out:match("^_NET_WM_PID.*%s+=%s+(%d+)%s+$")
1904 return tonumber(pid)
1907 local focused_xid = nil
1908 local clients = {} -- table of client objects indexed by xid
1909 local programs = {} -- table of program objects indexed by pid
1910 local mode_widget = widget:new ("999_client_mode")
1912 -- make programs table have weak values
1913 -- programs go away as soon as no clients point to it
1914 local programs_mt = {}
1915 setmetatable(programs, programs_mt)
1916 programs_mt.__mode = 'v'
1918 -- program class
1919 program = {}
1920 function program:new (pid)
1921 -- make an object
1922 local o = {}
1923 setmetatable (o,self)
1924 self.__index = self
1925 self.__gc = function (old) old:cont() end
1926 -- initialize the new object
1927 o.pid = pid
1928 -- suspend mode
1929 o.suspend = {}
1930 o.suspend.toggle = function (prog)
1931 prog.suspend.enabled = not prog.suspend.enabled
1933 o.suspend.enabled = false -- if true, defocusing suspends (SIGSTOP)
1934 o.suspend.active = true -- if true, focusing resumes (SIGCONT)
1935 return o
1938 function program:stop ()
1939 if not self.suspend.active then
1940 local cmd = "kill -STOP " .. tostring(self.pid)
1941 log (" executing: " .. cmd)
1942 os.execute (cmd)
1943 self.suspend.active = true
1947 function program:cont ()
1948 if self.suspend.active then
1949 local cmd = "kill -CONT " .. tostring(self.pid)
1950 log (" executing: " .. cmd)
1951 os.execute (cmd)
1952 self.suspend.active = false
1956 function get_program (pid)
1957 local prog = programs[pid]
1958 if not prog then
1959 prog = program:new (pid)
1960 programs[pid] = prog
1962 return prog
1965 -- client class
1966 client = {}
1967 function client:new (xid)
1968 -- make an object
1969 local o = {}
1970 setmetatable (o,self)
1971 self.__index = function (t,k)
1972 if k == 'suspend' then -- suspend mode is tracked per program
1973 return t.prog.suspend
1975 return self[k]
1977 self.__gc = function (old) old.prog=nil end
1978 -- initialize the new object
1979 o.xid = xid
1980 o.pid = xid_to_pid(xid)
1981 o.prog = get_program (o.pid)
1982 -- raw mode
1983 o.raw = {}
1984 o.raw.toggle = function (cli)
1985 cli.raw.enabled = not cli.raw.enabled
1986 cli:set_raw_mode()
1988 o.raw.enabled = false -- if true, raw mode enabled when client is focused
1989 return o
1992 function client:stop ()
1993 if self.suspend.enabled then
1994 self.prog:stop()
1998 function client:cont ()
1999 self.prog:cont()
2002 function client:set_raw_mode()
2003 if not self or not self.raw.enabled then -- normal mode
2004 update_active_keys ()
2005 else -- raw mode
2006 write ("/keys", "Mod4-space")
2010 function client:toggle(what)
2011 if what and self[what] then
2012 local ctl = self[what]
2014 ctl.toggle (self)
2016 log ("xid=" .. tostring (xid)
2017 .. " pid=" .. tostring (self.pid) .. " (" .. tostring (self.prog.pid) .. ")"
2018 .. " what=" .. tostring (what)
2019 .. " enabled=" .. tostring(ctl["enabled"]))
2021 mode_widget:show (self:flags_string())
2024 function client:flags_string()
2025 local ret = ''
2026 if self.suspend.enabled then ret = ret .. "s" else ret = ret .. "-" end
2027 if self.raw.enabled then ret = ret .. "r" else ret = ret .. "-" end
2028 return ret
2031 function get_client (xid)
2032 local xid = xid or wmixp:read("/client/sel/ctl")
2033 local cli = clients[xid]
2034 if not cli then
2035 cli = client:new (xid)
2036 clients[xid] = cli
2038 return cli
2041 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2042 function client_created (xid)
2043 log ("-client_created " .. tostring(xid))
2044 return get_client(xid)
2047 function client_destoryed (xid)
2048 log ("-client_destoryed " .. tostring(xid))
2049 if clients[xid] then
2050 local cli = clients[xid]
2051 clients[xid] = nil
2052 log (" del pid: " .. tostring(cli.pid))
2053 cli:cont()
2057 function client_focused (xid)
2058 log ("-client_focused " .. tostring(xid))
2059 -- do nothing if the same xid
2060 if focused_xid == xid then
2061 return clients[xid]
2064 local old = clients[focused_xid]
2065 local new = get_client(xid)
2067 -- handle raw mode switch
2068 if not old or ( old and new and old.raw.enabled ~= new.raw.enabled ) then
2069 new:set_raw_mode()
2072 -- do nothing if the same pid
2073 if old and new and old.pid == new.pid then
2074 mode_widget:show (new:flags_string())
2075 return clients[xid]
2078 if old then
2079 --[[
2080 log (" old pid: " .. tostring(old.pid)
2081 .. " xid: " .. tostring(old.xid)
2082 .. " flags: " .. old:flags_string())
2083 ]]--
2084 old:stop()
2087 if new then
2088 --[[
2089 log (" new pid: " .. tostring(new.pid)
2090 .. " xid: " .. tostring(new.xid)
2091 .. " flags: " .. new:flags_string())
2092 ]]--
2093 new:cont()
2096 mode_widget:show (new:flags_string())
2097 focused_xid = xid
2098 return new
2102 -- ========================================================================
2103 -- DOCUMENTATION
2104 -- ========================================================================
2106 --[[
2107 =pod
2109 =back
2111 =head1 ENVIRONMENT
2113 =over 4
2115 =item WMII_ADDRESS
2117 Used to determine location of wmii's listen socket.
2119 =back
2121 =head1 SEE ALSO
2123 L<wmii(1)>, L<lua(1)>
2125 =head1 AUTHOR
2127 Bart Trojanowski B<< <bart@jukie.net> >>
2129 =head1 COPYRIGHT AND LICENSE
2131 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
2133 This is free software. You may redistribute copies of it under the terms of
2134 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
2135 is NO WARRANTY, to the extent permitted by law.
2137 =cut
2138 --]]