ignore client change events when there is no client
[wmiirc-lua.git] / core / wmii.lua
blobd08ab1a8b556b4aa5b8a4b9cf49edc5cf1f62d2c
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 -- but having posix is not enough as the API changes, so we try each one
98 if posix.getprocessid then
99 local stat,rc = pcall (posix.getprocessid, "pid")
100 if stat then
101 myid = rc
104 if not myid and posix.getpid then
105 local stat,rc = pcall (posix.getpid, "pid")
106 if stat then
107 myid = rc
111 if not myid then
112 -- we were not able to get the PID, but we can create a random number
113 local now = tonumber(os.date("%s"))
114 math.randomseed(now)
115 myid = math.random(10000)
118 -- ========================================================================
119 -- MODULE VARIABLES
120 -- ========================================================================
122 -- wmiir points to the wmiir executable
123 -- TODO: need to make sure that wmiir is in path, and if not find it
124 local wmiir = "wmiir"
126 -- wmii_adr is the address we use when connecting using ixp
127 local wmii_adr = os.getenv("WMII_ADDRESS")
128 or ("unix!/tmp/ns." .. os.getenv("USER") .. "."
129 .. os.getenv("DISPLAY"):match("(:%d+)") .. "/wmii")
131 -- wmixp is the ixp context we use to talk to wmii
132 local wmixp = ixp.new(wmii_adr)
134 -- history of previous views, view_hist[#view_hist] is the last one
135 local view_hist = {} -- sorted with 1 being the oldest
136 local view_hist_max = 50 -- max number to keep track of
138 -- allow for a client to be forced to a tag
139 local next_client_goes_to_tag = nil
141 -- program and action histories
142 local prog_hist = history.new (20)
143 local action_hist = history.new(10)
145 -- where to find plugins
146 plugin_path = os.getenv("HOME") .. "/.wmii-3.5/plugins/?.so;"
147 .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.lua;"
148 .. "/usr/local/lib/lua/5.1/wmii/?.so;"
149 .. "/usr/local/share/lua/5.1/wmii/?.lua;"
150 .. "/usr/lib/lua/5.1/wmii/?.so;"
151 .. "/usr/share/lua/5.1/wmii/?.lua"
153 -- where to find wmiirc (see find_wmiirc())
154 wmiirc_path = os.getenv("HOME") .. "/.wmii-3.5/wmiirc.lua;"
155 .. os.getenv("HOME") .. "/.wmii-3.5/wmiirc;"
156 .. "/etc/X11/wmii-3.5/wmiirc.lua;"
157 .. "/etc/X11/wmii-3.5/wmiirc"
159 -- ========================================================================
160 -- LOCAL HELPERS
161 -- ========================================================================
163 --[[
164 =pod
166 =item log ( str )
168 Log the message provided in C<str>
170 Currently just writes to io.stderr
172 =cut
173 --]]
174 function log (str)
175 if get_conf("debug") then
176 io.stderr:write (str .. "\n")
180 --[[
181 =pod
183 =item find_wmiirc ( )
185 Locates the wmiirc script. It looks in ~/.wmii-3.5 and /etc/X11/wmii-3.5
186 for the first lua script bearing the name wmiirc.lua or wmiirc. Returns
187 first match.
189 =cut
190 --]]
191 function find_wmiirc()
192 local fn
193 for fn in string.gmatch(wmiirc_path, "[^;]+") do
194 -- try to locate the files locally
195 local file = io.open(fn, "r")
196 if file then
197 local txt = file:read("*line")
198 file:close()
199 if type(txt) == 'string' and txt:match("lua") then
200 return fn
204 return nil
208 -- ========================================================================
209 -- MAIN ACCESS FUNCTIONS
210 -- ========================================================================
212 --[[
213 =pod
215 =item ls ( dir, fmt )
217 List the wmii filesystem directory provided in C<dir>, in the format specified
218 by C<fmt>.
220 Returns an iterator of TODO
222 =cut
223 --]]
224 function ls (dir, fmt)
225 local verbose = fmt and fmt:match("l")
227 local s = wmixp:stat(dir)
228 if not s then
229 return function () return nil end
231 if s.modestr:match("^[^d]") then
232 return function ()
233 return stat2str(verbose, s)
237 local itr = wmixp:idir (dir)
238 if not itr then
239 --return function ()
240 return nil
241 --end
245 return function ()
246 local s = itr()
247 if s then
248 return stat2str(verbose, s)
250 return nil
254 local function stat2str(verbose, stat)
255 if verbose then
256 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
257 else
258 if stat.modestr:match("^d") then
259 return stat.name .. "/"
260 else
261 return stat.name
266 -- ------------------------------------------------------------------------
267 -- read all contents of a wmii virtual file
268 function read (file)
269 return wmixp:read (file)
272 -- ------------------------------------------------------------------------
273 -- return an iterator which walks all the lines in the file
275 -- example:
276 -- for event in wmii.iread("/ctl")
277 -- ...
278 -- end
280 -- NOTE: don't use iread for files that could block, as this will interfere
281 -- with timer processing and event delivery. Instead fork off a process to
282 -- execute wmiir and read back the responses via callback.
283 function iread (file)
284 return wmixp:iread(file)
287 -- ------------------------------------------------------------------------
288 -- create a wmii file, optionally write data to it
289 function create (file, data)
290 wmixp:create(file, data)
293 -- ------------------------------------------------------------------------
294 -- remove a wmii file
295 function remove (file)
296 wmixp:remove(file)
299 -- ------------------------------------------------------------------------
300 -- write a value to a wmii virtual file system
301 function write (file, value)
302 wmixp:write (file, value)
305 -- ------------------------------------------------------------------------
306 -- setup a table describing dmenu command
307 local function dmenu_cmd (prompt, iterator)
308 local cmdt = { wmiir, "setsid", "dmenu", "-b" }
309 local fn = get_ctl("font")
310 if fn then
311 cmdt[#cmdt+1] = "-fn"
312 cmdt[#cmdt+1] = fn
314 local normcolors = get_ctl("normcolors")
315 if normcolors then
316 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
317 if nf then
318 cmdt[#cmdt+1] = "-nf"
319 cmdt[#cmdt+1] = "'" .. nf .. "'"
321 if nb then
322 cmdt[#cmdt+1] = "-nb"
323 cmdt[#cmdt+1] = "'" .. nb .. "'"
326 local focuscolors = get_ctl("focuscolors")
327 if focuscolors then
328 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
329 if sf then
330 cmdt[#cmdt+1] = "-sf"
331 cmdt[#cmdt+1] = "'" .. sf .. "'"
333 if sb then
334 cmdt[#cmdt+1] = "-sb"
335 cmdt[#cmdt+1] = "'" .. sb .. "'"
338 if prompt then
339 cmdt[#cmdt+1] = "-p"
340 cmdt[#cmdt+1] = "'" .. prompt .. "'"
343 return cmdt
346 -- ------------------------------------------------------------------------
347 -- displays the menu given an table of entires, returns selected text
348 function menu (tbl, prompt)
349 local dmenu = dmenu_cmd(prompt)
351 local infile = os.tmpname()
352 local fh = io.open (infile, "w+")
354 local i,v
355 for i,v in pairs(tbl) do
356 if type(i) == 'number' and type(v) == 'string' then
357 fh:write (v)
358 else
359 fh:write (i)
361 fh:write ("\n")
363 fh:close()
365 local outfile = os.tmpname()
367 dmenu[#dmenu+1] = "<"
368 dmenu[#dmenu+1] = infile
369 dmenu[#dmenu+1] = ">"
370 dmenu[#dmenu+1] = outfile
372 local cmd = table.concat(dmenu," ")
373 os.execute (cmd)
375 fh = io.open (outfile, "r")
376 os.remove (outfile)
378 local sel = fh:read("*l")
379 fh:close()
381 return sel
384 -- ------------------------------------------------------------------------
385 -- displays the a tag selection menu, returns selected tag
386 function tag_menu ()
387 local tags = get_tags()
389 return menu(tags, "tag:")
392 -- ------------------------------------------------------------------------
393 -- displays the a program menu, returns selected program
394 function prog_menu ()
395 local dmenu = dmenu_cmd("cmd:")
397 local outfile = os.tmpname()
399 dmenu[#dmenu+1] = ">"
400 dmenu[#dmenu+1] = outfile
402 local hstt = { }
403 for n in prog_hist:walk_reverse_unique() do
404 hstt[#hstt+1] = "echo '" .. n .. "' ; "
407 local cmd = "(" .. table.concat(hstt)
408 .. "dmenu_path ) |"
409 .. table.concat(dmenu," ")
410 os.execute (cmd)
412 local fh = io.open (outfile, "rb")
413 os.remove (outfile)
415 local prog = fh:read("*l")
416 io.close (fh)
418 return prog
421 -- ------------------------------------------------------------------------
422 -- displays the a program menu, returns selected program
423 function get_tags()
424 local t = {}
425 local s
426 for s in wmixp:idir ("/tag") do
427 if s.name and not (s.name == "sel") then
428 t[#t + 1] = s.name
431 table.sort(t)
432 return t
435 -- ------------------------------------------------------------------------
436 -- displays the a program menu, returns selected program
437 function get_view()
438 local v = wmixp:read("/ctl") or ""
439 return v:match("view%s+(%S+)")
442 -- ------------------------------------------------------------------------
443 -- changes the current view to the name given
444 function set_view(sel)
445 local cur = get_view()
446 local all = get_tags()
448 if #all < 2 or sel == cur then
449 -- nothing to do if we have less then 2 tags
450 return
453 if not (type(sel) == "string") then
454 error ("string argument expected")
457 -- set new view
458 write ("/ctl", "view " .. sel)
461 -- ------------------------------------------------------------------------
462 -- changes the current view to the index given
463 function set_view_index(sel)
464 local cur = get_view()
465 local all = get_tags()
467 if #all < 2 then
468 -- nothing to do if we have less then 2 tags
469 return
472 local num = tonumber (sel)
473 if not num then
474 error ("number argument expected")
477 local name = all[sel]
478 if not name or name == cur then
479 return
482 -- set new view
483 write ("/ctl", "view " .. name)
486 -- ------------------------------------------------------------------------
487 -- chnages to current view by offset given
488 function set_view_ofs(jump)
489 local cur = get_view()
490 local all = get_tags()
492 if #all < 2 then
493 -- nothing to do if we have less then 2 tags
494 return
497 -- range check
498 if (jump < - #all) or (jump > #all) then
499 error ("view selector is out of range")
502 -- find the one that's selected index
503 local curi = nil
504 local i,v
505 for i,v in pairs (all) do
506 if v == cur then curi = i end
509 -- adjust by index
510 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
511 if (newi < - #all) or (newi > #all) then
512 error ("error computng new view")
515 write ("/ctl", "view " .. all[newi])
518 -- ------------------------------------------------------------------------
519 -- toggle between last view and current view
520 function toggle_view()
521 local last = view_hist[#view_hist]
522 if last then
523 set_view(last)
527 -- ========================================================================
528 -- ACTION HANDLERS
529 -- ========================================================================
531 local action_handlers = {
532 man = function (act, args)
533 local xterm = get_conf("xterm") or "xterm"
534 local page = args
535 if (not page) or (not page:match("%S")) then
536 page = wmiidir .. "/wmii.3lua"
538 local cmd = xterm .. " -e man " .. page .. " &"
539 log (" executing: " .. cmd)
540 os.execute (wmiir .. " setsid " .. cmd)
541 end,
543 quit = function ()
544 write ("/ctl", "quit")
545 end,
547 exec = function (act, args)
548 local what = args or "wmii"
549 log (" asking wmii to exec " .. tostring(what))
550 cleanup()
551 write ("/ctl", "exec " .. what)
552 end,
554 xlock = function (act)
555 local cmd = get_conf("xlock") or "xscreensaver-command --lock"
556 os.execute (wmiir .. " setsid " .. cmd)
557 end,
559 wmiirc = function ()
560 if have_posix then
561 local wmiirc = find_wmiirc()
562 if wmiirc then
563 log (" executing: lua " .. wmiirc)
564 cleanup()
565 posix.exec (wmiirc)
566 posix.exec ("/bin/sh", "-c", "exec lua wmiirc")
567 posix.exec ("/usr/bin/lua", wmiirc)
569 else
570 log("sorry cannot restart; you don't have lua's posix library.")
572 end,
574 urgent = function ()
575 wmixp:write ("/client/sel/ctl", "Urgent toggle")
576 end,
578 --[[
579 rehash = function ()
580 -- TODO: consider storing list of executables around, and
581 -- this will then reinitialize that list
582 log (" TODO: rehash")
583 end,
585 status = function ()
586 -- TODO: this should eventually update something on the /rbar
587 log (" TODO: status")
588 end,
589 --]]
592 --[[
593 =pod
595 =item add_action_handler (action, fn)
597 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
599 =cut
600 --]]
601 function add_action_handler (action, fn)
603 if type(action) ~= "string" or type(fn) ~= "function" then
604 error ("expecting a string and a function")
607 if action_handlers[action] then
608 error ("action handler already exists for '" .. action .. "'")
611 action_handlers[action] = fn
614 --[[
615 =pod
617 =item remove_action_handler (action)
619 Remove an action handler callback function for the given action string I<action>.
621 =cut
622 --]]
623 function remove_action_handler (action)
625 action_handlers[action] = nil
628 -- ========================================================================
629 -- KEY HANDLERS
630 -- ========================================================================
632 function ke_fullscreen_toggle()
633 wmixp:write ("/client/sel/ctl", "Fullscreen toggle")
636 function ke_view_starting_with_letter (letter)
637 local i,v
639 -- find the view name in history in reverse order
640 for i=#view_hist,1,-1 do
641 v = view_hist[i]
642 if letter == v:sub(1,1) then
643 set_view(v)
644 return true
648 -- otherwise just pick the first view that matches
649 local all = get_tags()
650 for i,v in pairs(all) do
651 if letter == v:sub(1,1) then
652 set_view_index (i)
653 return true
657 return false
660 function ke_handle_action()
661 local actions = { }
662 local seen = {}
664 local n
665 for n in action_hist:walk_reverse() do
666 if not seen[n] then
667 actions[#actions+1] = n
668 seen[n] = 1
672 local v
673 for n,v in pairs(action_handlers) do
674 if not seen[n] then
675 actions[#actions+1] = n
676 seen[n] = 1
680 local text = menu(actions, "action:")
681 if text then
682 log ("Action: " .. text)
683 local act = text
684 local args = nil
685 local si = text:find("%s")
686 if si then
687 act,args = string.match(text .. " ", "(%w+)%s(.+)")
689 if act then
690 local fn = action_handlers[act]
691 if fn then
692 action_hist:add (act)
693 local r, err = pcall (fn, act, args)
694 if not r then
695 log ("WARNING: " .. tostring(err))
703 local key_handlers = {
704 ["*"] = function (key)
705 log ("*: " .. key)
706 end,
708 -- execution and actions
709 ["Mod1-Return"] = function (key)
710 local xterm = get_conf("xterm") or "xterm"
711 log (" executing: " .. xterm)
712 os.execute (wmiir .. " setsid " .. xterm .. " &")
713 end,
714 ["Mod1-Shift-Return"] = function (key)
715 local tag = tag_menu()
716 if tag then
717 local xterm = get_conf("xterm") or "xterm"
718 log (" executing: " .. xterm .. " on: " .. tag)
719 next_client_goes_to_tag = tag
720 os.execute (wmiir .. " setsid " .. xterm .. " &")
722 end,
723 ["Mod1-a"] = function (key)
724 ke_handle_action()
725 end,
726 ["Mod1-p"] = function (key)
727 local prog = prog_menu()
728 if prog then
729 prog_hist:add(prog:match("(%w+)"))
730 log (" executing: " .. prog)
731 os.execute (wmiir .. " setsid " .. prog .. " &")
733 end,
734 ["Mod1-Shift-p"] = function (key)
735 local tag = tag_menu()
736 if tag then
737 local prog = prog_menu()
738 if prog then
739 log (" executing: " .. prog .. " on: " .. tag)
740 next_client_goes_to_tag = tag
741 os.execute (wmiir .. " setsid " .. prog .. " &")
744 end,
745 ["Mod1-Shift-c"] = function (key)
746 write ("/client/sel/ctl", "kill")
747 end,
749 -- HJKL active selection
750 ["Mod1-h"] = function (key)
751 write ("/tag/sel/ctl", "select left")
752 end,
753 ["Mod1-l"] = function (key)
754 write ("/tag/sel/ctl", "select right")
755 end,
756 ["Mod1-j"] = function (key)
757 write ("/tag/sel/ctl", "select down")
758 end,
759 ["Mod1-k"] = function (key)
760 write ("/tag/sel/ctl", "select up")
761 end,
763 -- HJKL movement
764 ["Mod1-Shift-h"] = function (key)
765 write ("/tag/sel/ctl", "send sel left")
766 end,
767 ["Mod1-Shift-l"] = function (key)
768 write ("/tag/sel/ctl", "send sel right")
769 end,
770 ["Mod1-Shift-j"] = function (key)
771 write ("/tag/sel/ctl", "send sel down")
772 end,
773 ["Mod1-Shift-k"] = function (key)
774 write ("/tag/sel/ctl", "send sel up")
775 end,
777 -- floating vs tiled
778 ["Mod1-space"] = function (key)
779 write ("/tag/sel/ctl", "select toggle")
780 end,
781 ["Mod1-Shift-space"] = function (key)
782 write ("/tag/sel/ctl", "send sel toggle")
783 end,
785 -- work spaces (# and @ are wildcards for numbers and letters)
786 ["Mod4-#"] = function (key, num)
787 -- first attempt to find a view that starts with the number requested
788 local num_str = tostring(num)
789 if not ke_view_starting_with_letter (num_str) then
790 -- if we fail, then set it to the index requested
791 set_view_index (num)
793 end,
794 ["Mod4-Shift-#"] = function (key, num)
795 write ("/client/sel/tags", tostring(num))
796 end,
797 ["Mod4-@"] = function (key, letter)
798 ke_view_starting_with_letter (letter)
799 end,
800 ["Mod4-Shift-@"] = function (key, letter)
801 local all = get_tags()
802 local i,v
803 for i,v in pairs(all) do
804 if letter == v:sub(1,1) then
805 write ("/client/sel/tags", v)
806 break
809 end,
810 ["Mod1-comma"] = function (key)
811 set_view_ofs (-1)
812 end,
813 ["Mod1-period"] = function (key)
814 set_view_ofs (1)
815 end,
816 ["Mod1-r"] = function (key)
817 -- got to the last view
818 toggle_view()
819 end,
821 -- switching views and retagging
822 ["Mod1-t"] = function (key)
823 -- got to a view
824 local tag = tag_menu()
825 if tag then
826 set_view (tag)
828 end,
829 ["Mod1-Shift-t"] = function (key)
830 -- move selected client to a tag
831 local tag = tag_menu()
832 if tag then
833 write ("/client/sel/tags", tag)
835 end,
836 ["Mod1-Shift-r"] = function (key)
837 -- move selected client to a tag, and follow
838 local tag = tag_menu()
839 if tag then
840 -- get the current window id
841 local xid = wmixp:read("/client/sel/ctl") or ""
843 -- modify the tag
844 write("/client/sel/tags", tag)
846 -- if the client is still in this tag, then
847 -- it might have been a regexp tag... check
848 local test = wmixp:read("/client/sel/ctl")
849 if not test or test ~= xid then
850 -- if the window moved, follow it
851 set_view(tag)
854 end,
855 ["Mod1-Control-t"] = function (key)
856 log (" TODO: Mod1-Control-t: " .. key)
857 end,
859 -- column modes
860 ["Mod1-d"] = function (key)
861 write("/tag/sel/ctl", "colmode sel default-max")
862 end,
863 ["Mod1-s"] = function (key)
864 write("/tag/sel/ctl", "colmode sel stack-max")
865 end,
866 ["Mod1-m"] = function (key)
867 write("/tag/sel/ctl", "colmode sel stack+max")
868 end,
869 ["Mod1-f"] = function (key)
870 ke_fullscreen_toggle()
871 end,
873 -- changing client flags
874 ["Shift-Mod1-f"] = function (key)
875 log ("setting flags")
877 local cli = get_client ()
879 local flags = { "suspend", "raw" }
880 local current_flags = cli:flags_string()
882 local what = menu(flags, "current flags: " .. current_flags .. " toggle:")
884 cli:toggle(what)
885 end,
886 ["Mod4-space"] = function (key)
887 local cli = get_client ()
888 cli:toggle("raw")
889 end,
892 --[[
893 =pod
895 =item add_key_handler (key, fn)
897 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
899 =cut
900 --]]
901 function add_key_handler (key, fn)
903 if type(key) ~= "string" or type(fn) ~= "function" then
904 error ("expecting a string and a function")
907 if key_handlers[key] then
908 -- TODO: we may wish to allow multiple handlers for one keypress
909 error ("key handler already exists for '" .. key .. "'")
912 key_handlers[key] = fn
915 --[[
916 =pod
918 =item remove_key_handler (key)
920 Remove an key handler callback function for the given key I<key>.
922 Returns the handler callback function.
924 =cut
925 --]]
926 function remove_key_handler (key)
928 local fn = key_handlers[key]
929 key_handlers[key] = nil
930 return fn
933 --[[
934 =pod
936 =item remap_key_handler (old_key, new_key)
938 Remove a key handler callback function from the given key I<old_key>,
939 and assign it to a new key I<new_key>.
941 =cut
942 --]]
943 function remap_key_handler (old_key, new_key)
945 local fn = remove_key_handler(old_key)
947 return add_key_handler (new_key, fn)
951 -- ------------------------------------------------------------------------
952 -- update the /keys wmii file with the list of all handlers
953 local alphabet="abcdefghijklmnopqrstuvwxyz"
954 function update_active_keys ()
955 local t = {}
956 local x, y
957 for x,y in pairs(key_handlers) do
958 if x:find("%w") then
959 local i = x:find("#$")
960 if i then
961 local j
962 for j=0,9 do
963 t[#t + 1] = x:sub(1,i-1) .. j
965 else
966 i = x:find("@$")
967 if i then
968 local j
969 for j=1,alphabet:len() do
970 local a = alphabet:sub(j,j)
971 t[#t + 1] = x:sub(1,i-1) .. a
973 else
974 t[#t + 1] = tostring(x)
979 local all_keys = table.concat(t, "\n")
980 --log ("setting /keys to...\n" .. all_keys .. "\n");
981 write ("/keys", all_keys)
984 -- ------------------------------------------------------------------------
985 -- update the /lbar wmii file with the current tags
986 function update_displayed_tags ()
987 -- colours for /lbar
988 local fc = get_ctl("focuscolors") or ""
989 local nc = get_ctl("normcolors") or ""
991 -- build up a table of existing tags in the /lbar
992 local old = {}
993 local s
994 for s in wmixp:idir ("/lbar") do
995 old[s.name] = 1
998 -- for all actual tags in use create any entries in /lbar we don't have
999 -- clear the old table entries if we have them
1000 local cur = get_view()
1001 local all = get_tags()
1002 local i,v
1003 for i,v in pairs(all) do
1004 local color = nc
1005 if cur == v then
1006 color = fc
1008 if not old[v] then
1009 create ("/lbar/" .. v, color .. " " .. v)
1011 write ("/lbar/" .. v, color .. " " .. v)
1012 old[v] = nil
1015 -- anything left in the old table should be removed now
1016 for i,v in pairs(old) do
1017 if v then
1018 remove("/lbar/"..i)
1023 -- ========================================================================
1024 -- EVENT HANDLERS
1025 -- ========================================================================
1027 local widget_ev_handlers = {
1030 --[[
1031 =pod
1033 =item _handle_widget_event (ev, arg)
1035 Top-level event handler for redispatching events to widgets. This event
1036 handler is added for any widget event that currently has a widget registered
1037 for it.
1039 Valid widget events are currently
1041 RightBarMouseDown <buttonnumber> <widgetname>
1042 RightBarClick <buttonnumber> <widgetname>
1044 the "Click" event is sent on mouseup.
1046 The callbacks are given only the button number as their argument, to avoid the
1047 need to reparse.
1049 =cut
1050 --]]
1052 local function _handle_widget_event (ev, arg)
1054 log("_handle_widget_event: " .. tostring(ev) .. " - " .. tostring(arg))
1056 -- parse arg to strip out our widget name
1057 local number,wname = string.match(arg, "(%d+)%s+(.+)")
1059 -- check our dispatch table for that widget
1060 if not wname then
1061 log("Didn't find wname")
1062 return
1065 local wtable = widget_ev_handlers[wname]
1066 if not wtable then
1067 log("No widget cares about" .. wname)
1068 return
1071 local fn = wtable[ev] or wtable["*"]
1072 if fn then
1073 success, err = pcall( fn, ev, tonumber(number) )
1074 if not success then
1075 log("Callback had an error in _handle_widget_event: " .. tostring(err) )
1076 return nil
1078 else
1079 log("no function found for " .. ev)
1083 local ev_handlers = {
1084 ["*"] = function (ev, arg)
1085 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
1086 end,
1088 RightBarClick = _handle_widget_event,
1090 -- process timer events
1091 ProcessTimerEvents = function (ev, arg)
1092 process_timers()
1093 end,
1095 -- exit if another wmiirc started up
1096 Start = function (ev, arg)
1097 if arg then
1098 if arg == "wmiirc" then
1099 -- backwards compatibility with bash version
1100 log (" exiting; pid=" .. tostring(myid))
1101 cleanup()
1102 os.exit (0)
1103 else
1104 -- ignore if it came from us
1105 local pid = string.match(arg, "wmiirc (%d+)")
1106 if pid then
1107 local pid = tonumber (pid)
1108 if not (pid == myid) then
1109 log (" exiting; pid=" .. tostring(myid))
1110 cleanup()
1111 os.exit (0)
1116 end,
1118 -- tag management
1119 CreateTag = function (ev, arg)
1120 local nc = get_ctl("normcolors") or ""
1121 create ("/lbar/" .. arg, nc .. " " .. arg)
1122 end,
1123 DestroyTag = function (ev, arg)
1124 remove ("/lbar/" .. arg)
1126 -- remove the tag from history
1127 local i,v
1128 for i=#view_hist,1,-1 do
1129 v = view_hist[i]
1130 if arg == v then
1131 table.remove(view_hist,i)
1134 end,
1136 FocusTag = function (ev, arg)
1137 log ("FocusTag: " .. arg)
1139 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1140 if not tag then
1141 return
1144 local file = "/lbar/" .. tag
1145 if scrn and scrn:len() > 0 then
1146 file = "/screen/" .. scrn .. file
1149 local fc = get_ctl("focuscolors") or ""
1150 log ("# echo " .. fc .. " " .. tag .. " | wmiir write " .. file)
1152 create (file, fc .. " " .. tag)
1153 write (file, fc .. " " .. tag)
1154 end,
1155 UnfocusTag = function (ev, arg)
1156 log ("UnfocusTag: " .. arg)
1158 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1159 if not tag then
1160 return
1163 local file = "/lbar/" .. tag
1164 if scrn and scrn:len() > 0 then
1165 file = "/screen/" .. scrn .. file
1168 local nc = get_ctl("normcolors") or ""
1169 log ("# echo " .. nc .. " " .. tag .. " | wmiir write " .. file)
1171 create (file, nc .. " " .. tag)
1172 write (file, nc .. " " .. tag)
1174 -- don't duplicate the last entry
1175 if not (tag == view_hist[#view_hist]) then
1176 view_hist[#view_hist+1] = tag
1178 -- limit to view_hist_max
1179 if #view_hist > view_hist_max then
1180 table.remove(view_hist, 1)
1183 end,
1185 -- key event handling
1186 Key = function (ev, arg)
1187 log ("Key: " .. arg)
1188 local magic = nil
1189 -- can we find an exact match?
1190 local fn = key_handlers[arg]
1191 if not fn then
1192 local key = arg:gsub("-%d$", "-#")
1193 -- can we find a match with a # wild card for the number
1194 fn = key_handlers[key]
1195 if fn then
1196 -- convert the trailing number to a number
1197 magic = tonumber(arg:match("-(%d)$"))
1200 if not fn then
1201 local key = arg:gsub("-%a$", "-@")
1202 -- can we find a match with a @ wild card for a letter
1203 fn = key_handlers[key]
1204 if fn then
1205 -- split off the trailing letter
1206 magic = arg:match("-(%a)$")
1209 if not fn then
1210 -- everything else failed, try default match
1211 fn = key_handlers["*"]
1213 if fn then
1214 local r, err = pcall (fn, arg, magic)
1215 if not r then
1216 log ("WARNING: " .. tostring(err))
1219 end,
1221 -- mouse handling on the lbar
1222 LeftBarClick = function (ev, arg)
1223 local button,tag = string.match(arg, "(%w+)%s+(%S+)")
1224 set_view (tag)
1225 end,
1227 -- focus updates
1228 ClientFocus = function (ev, arg)
1229 log ("ClientFocus: " .. arg)
1230 client_focused (arg)
1231 end,
1232 ColumnFocus = function (ev, arg)
1233 log ("ColumnFocus: " .. arg)
1234 end,
1236 -- client handling
1237 CreateClient = function (ev, arg)
1238 if next_client_goes_to_tag then
1239 local tag = next_client_goes_to_tag
1240 local cli = arg
1241 next_client_goes_to_tag = nil
1242 write ("/client/" .. cli .. "/tags", tag)
1243 set_view(tag)
1245 client_created (arg)
1246 end,
1247 DestroyClient = function (ev, arg)
1248 client_destoryed (arg)
1249 end,
1251 -- urgent tag
1252 UrgentTag = function (ev, arg)
1253 log ("UrgentTag: " .. arg)
1254 write ("/lbar/" .. arg, "*" .. arg);
1255 end,
1256 NotUrgentTag = function (ev, arg)
1257 log ("NotUrgentTag: " .. arg)
1258 write ("/lbar/" .. arg, arg);
1259 end,
1261 -- notifications
1262 Unresponsive = function (ev, arg)
1263 log ("Unresponsive: " .. arg)
1264 -- TODO ask the user if it shoudl be killed off
1265 end,
1267 Notice = function (ev, arg)
1268 log ("Notice: " .. arg)
1269 -- TODO send to the message plugin (or implement there)
1270 end,
1272 -- /
1275 --[[
1276 =pod
1278 =item add_widget_event_handler (wname, ev, fn)
1280 Add an event handler callback for the I<ev> event on the widget named I<wname>
1282 =cut
1283 --]]
1285 function add_widget_event_handler (wname, ev, fn)
1286 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
1287 error ("expecting string for widget name, string for event name and a function callback")
1290 -- Make sure the widget event handler is present
1291 if not ev_handlers[ev] then
1292 ev_handlers[ev] = _handle_widget_event
1295 if not widget_ev_handlers[wname] then
1296 widget_ev_handlers[wname] = { }
1299 if widget_ev_handlers[wname][ev] then
1300 -- TODO: we may wish to allow multiple handlers for one event
1301 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
1304 widget_ev_handlers[wname][ev] = fn
1307 --[[
1308 =pod
1310 =item remove_widget_event_handler (wname, ev)
1312 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1314 =cut
1315 --]]
1316 function remove_event_handler (wname, ev)
1318 if not widget_ev_handlers[wname] then
1319 return
1322 widget_ev_handlers[wname][ev] = nil
1325 --[[
1326 =pod
1328 =item add_event_handler (ev, fn)
1330 Add an event handler callback function, I<fn>, for the given event I<ev>.
1332 =cut
1333 --]]
1334 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1335 function add_event_handler (ev, fn)
1336 if type(ev) ~= "string" or type(fn) ~= "function" then
1337 error ("expecting a string and a function")
1340 if ev_handlers[ev] then
1341 -- TODO: we may wish to allow multiple handlers for one event
1342 error ("event handler already exists for '" .. ev .. "'")
1346 ev_handlers[ev] = fn
1349 --[[
1350 =pod
1352 =item remove_event_handler (ev)
1354 Remove an event handler callback function for the given event I<ev>.
1356 =cut
1357 --]]
1358 function remove_event_handler (ev)
1360 ev_handlers[ev] = nil
1364 -- ========================================================================
1365 -- MAIN INTERFACE FUNCTIONS
1366 -- ========================================================================
1368 local config = {
1369 xterm = 'x-terminal-emulator',
1370 xlock = "xscreensaver-command --lock",
1371 debug = false,
1374 -- ------------------------------------------------------------------------
1375 -- write configuration to /ctl wmii file
1376 -- wmii.set_ctl({ "var" = "val", ...})
1377 -- wmii.set_ctl("var, "val")
1378 function set_ctl (first,second)
1379 if type(first) == "table" and second == nil then
1380 local x, y
1381 for x, y in pairs(first) do
1382 write ("/ctl", x .. " " .. y)
1385 elseif type(first) == "string" and type(second) == "string" then
1386 write ("/ctl", first .. " " .. second)
1388 else
1389 error ("expecting a table or two string arguments")
1393 -- ------------------------------------------------------------------------
1394 -- read a value from /ctl wmii file
1395 -- table = wmii.get_ctl()
1396 -- value = wmii.get_ctl("variable"
1397 function get_ctl (name)
1398 local s
1399 local t = {}
1400 for s in iread("/ctl") do
1401 local var,val = s:match("(%w+)%s+(.+)")
1402 if var == name then
1403 return val
1405 t[var] = val
1407 if not name then
1408 return t
1410 return nil
1413 -- ------------------------------------------------------------------------
1414 -- set an internal wmiirc.lua variable
1415 -- wmii.set_conf({ "var" = "val", ...})
1416 -- wmii.set_conf("var, "val")
1417 function set_conf (first,second)
1418 if type(first) == "table" and second == nil then
1419 local x, y
1420 for x, y in pairs(first) do
1421 config[x] = y
1424 elseif type(first) == "string"
1425 and (type(second) == "string"
1426 or type(second) == "number"
1427 or type(second) == "boolean") then
1428 config[first] = second
1430 else
1431 error ("expecting a table, or string and string/number as arguments")
1435 -- ------------------------------------------------------------------------
1436 -- read an internal wmiirc.lua variable
1437 function get_conf (name)
1438 if name then
1439 return config[name]
1441 return config
1444 -- ========================================================================
1445 -- THE EVENT LOOP
1446 -- ========================================================================
1448 -- the event loop instance
1449 local el = eventloop.new()
1450 local event_read_fd = -1
1452 -- ------------------------------------------------------------------------
1453 -- start/restart the core event reading process
1454 local function start_event_reader ()
1455 if event_read_fd ~= -1 then
1456 if el:check_exec(event_read_fd) then
1457 return
1460 log("wmii: starting /event reading process")
1461 event_read_fd = el:add_exec (wmiir .. " read /event",
1462 function (line)
1463 local line = line or "nil"
1465 -- try to split off the argument(s)
1466 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1467 if not ev then
1468 ev = line
1471 -- now locate the handler function and call it
1472 local fn = ev_handlers[ev] or ev_handlers["*"]
1473 if fn then
1474 local r, err = pcall (fn, ev, arg)
1475 if not r then
1476 log ("WARNING: " .. tostring(err))
1481 log("wmii: ... fd=" .. tostring(event_read_fd))
1484 -- ------------------------------------------------------------------------
1485 -- run the event loop and process events, this function does not exit
1486 function run_event_loop ()
1487 -- stop any other instance of wmiirc
1488 wmixp:write ("/event", "Start wmiirc " .. tostring(myid))
1490 log("wmii: updating lbar")
1492 update_displayed_tags ()
1494 log("wmii: updating rbar")
1496 update_displayed_widgets ()
1498 log("wmii: updating active keys")
1500 update_active_keys ()
1502 log("wmii: starting event loop")
1503 while true do
1504 start_event_reader()
1505 local sleep_for = process_timers()
1506 el:run_loop(sleep_for)
1510 -- ========================================================================
1511 -- PLUGINS API
1512 -- ========================================================================
1514 api_version = 0.1 -- the API version we export
1516 plugins = {} -- all plugins that were loaded
1518 -- ------------------------------------------------------------------------
1519 -- plugin loader which also verifies the version of the api the plugin needs
1521 -- here is what it does
1522 -- - does a manual locate on the file using package.path
1523 -- - reads in the file w/o using the lua interpreter
1524 -- - locates api_version=X.Y string
1525 -- - makes sure that api_version requested can be satisfied
1526 -- - if the plugins is available it will set variables passed in
1527 -- - it then loads the plugin
1529 -- TODO: currently the api_version must be in an X.Y format, but we may want
1530 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1532 function load_plugin(name, vars)
1533 local backup_path = package.path or "./?.lua"
1535 log ("loading " .. name)
1537 -- this is the version we want to find
1538 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1539 if (not api_major) or (not api_minor) then
1540 log ("WARNING: could not parse api_version in core/wmii.lua")
1541 return nil
1544 -- first find the plugin file
1545 local s, path_match, full_name, file
1546 for s in string.gmatch(plugin_path, "[^;]+") do
1547 -- try to locate the files locally
1548 local fn = s:gsub("%?", name)
1549 file = io.open(fn, "r")
1550 if file then
1551 path_match = s
1552 full_name = fn
1553 break
1557 -- read it in
1558 local txt
1559 if file then
1560 txt = file:read("*all")
1561 file:close()
1564 if not txt then
1565 log ("WARNING: could not load plugin '" .. name .. "'")
1566 return nil
1569 -- find the api_version line
1570 local line, plugin_version
1571 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1572 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1573 if plugin_version then
1574 break
1578 if not plugin_version then
1579 log ("WARNING: could not find api_version string in plugin '" .. name .. "'")
1580 return nil
1583 -- decompose the version string
1584 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1585 if (not plugin_major) or (not plugin_minor) then
1586 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1587 return nil
1590 -- make a version test
1591 if plugin_major ~= api_major then
1592 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1593 .. " (api " .. tonumber(api_version) .. ")")
1594 return nil
1597 if plugin_minor > api_minor then
1598 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1599 .. " (api " .. tonumber(api_version) .. ")")
1600 return nil
1603 -- the configuration parameters before loading
1604 if type(vars) == "table" then
1605 local var, val
1606 for var,val in pairs(vars) do
1607 local success = pcall (set_conf, name .. "." .. var, val)
1608 if not success then
1609 log ("WARNING: bad variable {" .. tostring(var) .. ", " .. tostring(val) .. "} "
1610 .. "given; loading '" .. name .. "' plugin failed.")
1611 return nil
1616 -- actually load the module, but use only the path where we though it should be
1617 package.path = path_match
1618 local success,what = pcall (require, name)
1619 package.path = backup_path
1620 if not success then
1621 log ("WARNING: failed to load '" .. name .. "' plugin")
1622 log (" - path: " .. tostring(path_match))
1623 log (" - file: " .. tostring(full_name))
1624 log (" - plugin's api_version: " .. tostring(plugin_version))
1625 log (" - reason: " .. tostring(what))
1626 return nil
1629 -- success
1630 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1631 plugins[name] = what
1632 return what
1635 -- ------------------------------------------------------------------------
1636 -- widget template
1637 widget = {}
1638 widgets = {}
1640 -- ------------------------------------------------------------------------
1641 -- create a widget object and add it to the wmii /rbar
1643 -- examples:
1644 -- widget = wmii.widget:new ("999_clock")
1645 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1646 function widget:new (name, fn)
1647 local o = {}
1649 if type(name) == "string" then
1650 o.name = name
1651 if type(fn) == "function" then
1652 o.fn = fn
1654 else
1655 error ("expected name followed by an optional function as arguments")
1658 setmetatable (o,self)
1659 self.__index = self
1660 self.__gc = function (o) o:hide() end
1662 widgets[name] = o
1664 o:show()
1665 return o
1668 -- ------------------------------------------------------------------------
1669 -- stop and destroy the timer
1670 function widget:delete ()
1671 widgets[self.name] = nil
1672 self:hide()
1675 -- ------------------------------------------------------------------------
1676 -- displays or updates the widget text
1678 -- examples:
1679 -- w:show("foo")
1680 -- w:show("foo", "#888888 #222222 #333333")
1681 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1683 function widget:show (txt, colors)
1684 local colors = colors or get_ctl("normcolors") or ""
1685 local txt = txt or self.txt or ""
1686 local towrite = txt
1687 if colors then
1688 towrite = colors .. " " .. towrite
1690 if not self.txt then
1691 create ("/rbar/" .. self.name, towrite)
1692 else
1693 write ("/rbar/" .. self.name, towrite)
1695 self.txt = txt
1698 -- ------------------------------------------------------------------------
1699 -- hides a widget and removes it from the bar
1700 function widget:hide ()
1701 if self.txt then
1702 remove ("/lbar/" .. self.name)
1703 self.txt = nil
1707 --[[
1708 =pod
1710 =item widget:add_event_handler (ev, fn)
1712 Add an event handler callback for this widget, using I<fn> for event I<ev>
1714 =cut
1715 --]]
1717 function widget:add_event_handler (ev, fn)
1718 add_widget_event_handler( self.name, ev, fn)
1722 -- ------------------------------------------------------------------------
1723 -- remove all /rbar entries that we don't have widget objects for
1724 function update_displayed_widgets ()
1725 -- colours for /rbar
1726 local nc = get_ctl("normcolors") or ""
1728 -- build up a table of existing tags in the /lbar
1729 local old = {}
1730 local s
1731 for s in wmixp:idir ("/rbar") do
1732 old[s.name] = 1
1735 -- for all actual widgets in use we want to remove them from the old list
1736 local i,v
1737 for i,v in pairs(widgets) do
1738 old[v.name] = nil
1741 -- anything left in the old table should be removed now
1742 for i,v in pairs(old) do
1743 if v then
1744 remove("/rbar/"..i)
1749 -- ------------------------------------------------------------------------
1750 -- create a new program and for each line it generates call the callback function
1751 -- returns fd which can be passed to kill_exec()
1752 function add_exec (command, callback)
1753 return el:add_exec (command, callback)
1756 -- ------------------------------------------------------------------------
1757 -- terminates a program spawned off by add_exec()
1758 function kill_exec (fd)
1759 return el:kill_exec (fd)
1762 -- ------------------------------------------------------------------------
1763 -- timer template
1764 timer = {}
1765 local timers = {}
1767 -- ------------------------------------------------------------------------
1768 -- create a timer object and add it to the event loop
1770 -- examples:
1771 -- timer:new (my_timer_fn)
1772 -- timer:new (my_timer_fn, 15)
1773 function timer:new (fn, seconds)
1774 local o = {}
1776 if type(fn) == "function" then
1777 o.fn = fn
1778 else
1779 error ("expected function followed by an optional number as arguments")
1782 setmetatable (o,self)
1783 self.__index = self
1784 self.__gc = function (o) o:stop() end
1786 -- add the timer
1787 timers[#timers+1] = o
1789 if seconds then
1790 o:resched(seconds)
1792 return o
1795 -- ------------------------------------------------------------------------
1796 -- stop and destroy the timer
1797 function timer:delete ()
1798 self:stop()
1799 local i,t
1800 for i,t in pairs(timers) do
1801 if t == self then
1802 table.remove (timers,i)
1803 return
1808 -- ------------------------------------------------------------------------
1809 -- run the timer given new interval
1810 function timer:resched (seconds)
1811 local seconds = seconds or self.interval
1812 if not (type(seconds) == "number") then
1813 error ("timer:resched expected number as argument")
1816 local now = tonumber(os.date("%s"))
1818 self.interval = seconds
1819 self.next_time = now + seconds
1821 -- resort the timer list
1822 table.sort (timers, timer.is_less_then)
1825 -- helper for sorting timers
1826 function timer:is_less_then(another)
1827 if not self.next_time then
1828 return false -- another is smaller, nil means infinity
1830 elseif not another.next_time then
1831 return true -- self is smaller, nil means infinity
1833 elseif self.next_time < another.next_time then
1834 return true -- self is smaller than another
1837 return false -- another is smaller then self
1840 -- ------------------------------------------------------------------------
1841 -- stop the timer
1842 function timer:stop ()
1843 self.next_time = nil
1845 -- resort the timer list
1846 table.sort (timers, timer.is_less_then)
1849 -- ------------------------------------------------------------------------
1850 -- figure out how long before the next event
1851 function time_before_next_timer_event()
1852 local tmr = timers[1]
1853 if tmr and tmr.next_time then
1854 local now = tonumber(os.date("%s"))
1855 local seconds = tmr.next_time - now
1856 if seconds > 0 then
1857 return seconds
1860 return 0 -- sleep for ever
1863 -- ------------------------------------------------------------------------
1864 -- handle outstanding events
1865 function process_timers ()
1866 local now = tonumber(os.date("%s"))
1867 local torun = {}
1868 local i,tmr
1870 for i,tmr in pairs (timers) do
1871 if not tmr then
1872 -- prune out removed timers
1873 table.remove(timers,i)
1874 break
1876 elseif not tmr.next_time then
1877 -- break out once we find a timer that is stopped
1878 break
1880 elseif tmr.next_time > now then
1881 -- break out once we get to the future
1882 break
1885 -- this one is good to go
1886 torun[#torun+1] = tmr
1889 for i,tmr in pairs (torun) do
1890 tmr:stop()
1891 local status,new_interval = pcall (tmr.fn, tmr)
1892 if status then
1893 new_interval = new_interval or self.interval
1894 if new_interval and (new_interval ~= -1) then
1895 tmr:resched(new_interval)
1897 else
1898 log ("ERROR: " .. tostring(new_interval))
1902 local sleep_for = time_before_next_timer_event()
1903 return sleep_for
1906 -- ------------------------------------------------------------------------
1907 -- cleanup everything in preparation for exit() or exec()
1908 function cleanup ()
1910 local i,v,tmr,p
1912 log ("wmii: stopping timer events")
1914 for i,tmr in pairs (timers) do
1915 pcall (tmr.delete, tmr)
1917 timers = {}
1919 log ("wmii: terminating eventloop")
1921 pcall(el.kill_all,el)
1923 log ("wmii: disposing of widgets")
1925 -- dispose of all widgets
1926 for i,v in pairs(widgets) do
1927 pcall(v.delete,v)
1929 timers = {}
1931 -- FIXME: it doesn't seem to do what I want
1932 --[[
1933 log ("wmii: releasing plugins")
1935 for i,p in pairs(plugins) do
1936 if p.cleanup then
1937 pcall (p.cleanup, p)
1940 plugins = {}
1941 --]]
1943 log ("wmii: dormant")
1946 -- ========================================================================
1947 -- CLIENT HANDLING
1948 -- ========================================================================
1950 --[[
1951 -- Notes on client tracking
1953 -- When a client is created wmii sends us a CreateClient message, and
1954 -- we in turn create a 'client' object and store it in the 'clients'
1955 -- table indexed by the client's ID.
1957 -- Each client object stores the following:
1958 -- .xid - the X client ID
1959 -- .pid - the process ID
1960 -- .prog - program object representing the process
1962 -- The client and program objects track the following modes for each program:
1964 -- raw mode:
1965 -- - for each client window
1966 -- - Mod4-space toggles the state between normal and raw
1967 -- - Mod1-f raw also toggles the state
1968 -- - in raw mode all input goes to the client, except for Mod4-space
1969 -- - a focused client with raw mode enabled is put into raw mode
1971 -- suspend mode:
1972 -- - for each program
1973 -- - Mod1-f suspend toggles the state for current client's program
1974 -- - a focused client, whose program was previous suspended is resumed
1975 -- - an unfocused client, with suspend enabled, will be suspended
1976 -- - suspend/resume is done by sending the STOP/CONT signals to the PID
1977 --]]
1979 function xid_to_pid (xid)
1980 local cmd = "xprop -id " .. tostring(xid) .. " _NET_WM_PID"
1981 local file = io.popen (cmd)
1982 local out = file:read("*a")
1983 file:close()
1984 local pid = out:match("^_NET_WM_PID.*%s+=%s+(%d+)%s+$")
1985 return tonumber(pid)
1988 local focused_xid = nil
1989 local clients = {} -- table of client objects indexed by xid
1990 local programs = {} -- table of program objects indexed by pid
1991 local mode_widget = widget:new ("999_client_mode")
1993 -- make programs table have weak values
1994 -- programs go away as soon as no clients point to it
1995 local programs_mt = {}
1996 setmetatable(programs, programs_mt)
1997 programs_mt.__mode = 'v'
1999 -- program class
2000 program = {}
2001 function program:new (pid)
2002 -- make an object
2003 local o = {}
2004 setmetatable (o,self)
2005 self.__index = self
2006 self.__gc = function (old) old:cont() end
2007 -- initialize the new object
2008 o.pid = pid
2009 -- suspend mode
2010 o.suspend = {}
2011 o.suspend.toggle = function (prog)
2012 prog.suspend.enabled = not prog.suspend.enabled
2014 o.suspend.enabled = false -- if true, defocusing suspends (SIGSTOP)
2015 o.suspend.active = true -- if true, focusing resumes (SIGCONT)
2016 return o
2019 function program:stop ()
2020 if not self.suspend.active then
2021 local cmd = "kill -STOP " .. tostring(self.pid)
2022 log (" executing: " .. cmd)
2023 os.execute (cmd)
2024 self.suspend.active = true
2028 function program:cont ()
2029 if self.suspend.active then
2030 local cmd = "kill -CONT " .. tostring(self.pid)
2031 log (" executing: " .. cmd)
2032 os.execute (cmd)
2033 self.suspend.active = false
2037 function get_program (pid)
2038 local prog = programs[pid]
2039 if pid and not prog then
2040 prog = program:new (pid)
2041 programs[pid] = prog
2043 return prog
2046 -- client class
2047 client = {}
2048 function client:new (xid)
2049 local pid = xid_to_pid(xid)
2050 if not pid then
2051 log ("WARNING: failed to convert XID " .. tostring(xid) .. " to a PID")
2052 return
2054 -- make an object
2055 local o = {}
2056 setmetatable (o,self)
2057 self.__index = function (t,k)
2058 if k == 'suspend' then -- suspend mode is tracked per program
2059 return t.prog.suspend
2061 return self[k]
2063 self.__gc = function (old) old.prog=nil end
2064 -- initialize the new object
2065 o.xid = xid
2066 o.pid = pid
2067 o.prog = get_program (pid)
2068 -- raw mode
2069 o.raw = {}
2070 o.raw.toggle = function (cli)
2071 cli.raw.enabled = not cli.raw.enabled
2072 cli:set_raw_mode()
2074 o.raw.enabled = false -- if true, raw mode enabled when client is focused
2075 return o
2078 function client:stop ()
2079 if self.suspend.enabled then
2080 self.prog:stop()
2084 function client:cont ()
2085 self.prog:cont()
2088 function client:set_raw_mode()
2089 if not self or not self.raw.enabled then -- normal mode
2090 update_active_keys ()
2091 else -- raw mode
2092 write ("/keys", "Mod4-space")
2096 function client:toggle(what)
2097 if what and self[what] then
2098 local ctl = self[what]
2100 ctl.toggle (self)
2102 log ("xid=" .. tostring (xid)
2103 .. " pid=" .. tostring (self.pid) .. " (" .. tostring (self.prog.pid) .. ")"
2104 .. " what=" .. tostring (what)
2105 .. " enabled=" .. tostring(ctl["enabled"]))
2107 mode_widget:show (self:flags_string())
2110 function client:flags_string()
2111 local ret = ''
2112 if self.suspend.enabled then ret = ret .. "s" else ret = ret .. "-" end
2113 if self.raw.enabled then ret = ret .. "r" else ret = ret .. "-" end
2114 return ret
2117 function get_client (xid)
2118 local xid = xid or wmixp:read("/client/sel/ctl")
2119 local cli = clients[xid]
2120 if not cli then
2121 cli = client:new (xid)
2122 clients[xid] = cli
2124 return cli
2127 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2128 function client_created (xid)
2129 log ("-client_created " .. tostring(xid))
2130 return get_client(xid)
2133 function client_destoryed (xid)
2134 log ("-client_destoryed " .. tostring(xid))
2135 if clients[xid] then
2136 local cli = clients[xid]
2137 clients[xid] = nil
2138 log (" del pid: " .. tostring(cli.pid))
2139 cli:cont()
2141 if focused_xid == xid then
2142 focused_xid = nil
2146 function client_focused (xid)
2147 log ("-client_focused " .. tostring(xid))
2148 -- return the current focused xid if nil is passed
2149 if type(xid) ~= 'string' or not xid:match("0x[0-9][a-f][A-F]*$") then
2150 return focused_xid
2152 -- do nothing if the same xid
2153 if focused_xid == xid then
2154 return clients[xid]
2157 local old = clients[focused_xid]
2158 local new = get_client(xid)
2160 -- handle raw mode switch
2161 if not old or ( old and new and old.raw.enabled ~= new.raw.enabled ) then
2162 new:set_raw_mode()
2165 -- do nothing if the same pid
2166 if old and new and old.pid == new.pid then
2167 mode_widget:show (new:flags_string())
2168 return clients[xid]
2171 if old then
2172 --[[
2173 log (" old pid: " .. tostring(old.pid)
2174 .. " xid: " .. tostring(old.xid)
2175 .. " flags: " .. old:flags_string())
2176 ]]--
2177 old:stop()
2180 if new then
2181 --[[
2182 log (" new pid: " .. tostring(new.pid)
2183 .. " xid: " .. tostring(new.xid)
2184 .. " flags: " .. new:flags_string())
2185 ]]--
2186 new:cont()
2189 mode_widget:show (new:flags_string())
2190 focused_xid = xid
2191 return new
2195 -- ========================================================================
2196 -- DOCUMENTATION
2197 -- ========================================================================
2199 --[[
2200 =pod
2202 =back
2204 =head1 ENVIRONMENT
2206 =over 4
2208 =item WMII_ADDRESS
2210 Used to determine location of wmii's listen socket.
2212 =back
2214 =head1 SEE ALSO
2216 L<wmii(1)>, L<lua(1)>
2218 =head1 AUTHOR
2220 Bart Trojanowski B<< <bart@jukie.net> >>
2222 =head1 COPYRIGHT AND LICENSE
2224 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
2226 This is free software. You may redistribute copies of it under the terms of
2227 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
2228 is NO WARRANTY, to the extent permitted by law.
2230 =cut
2231 --]]