Populate the paths where the plugins live when the module is instanciated.
[wmiirc-lua.git] / src / core / wmii.lua.in
blob94281e62ec0ce643f6eba16274c38d247ce2633b
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 = ("%HOME_WMII%"):gsub("^~", os.getenv("HOME"))
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_paths = {}
148 table.insert(plugin_paths, wmiidir .. "/plugins/?.so")
149 table.insert(plugin_paths, wmiidir .. "/plugins/?.lua")
151 for path in string.gmatch(package.path, "[^;]+") do
152 if not path:find("^./") and not path:find("core") and not path:find("plugins") then
153 local path = path:gsub("%?", "wmii/?")
154 table.insert(plugin_paths, path)
158 for path in string.gmatch(package.cpath, "[^;]+") do
159 if not path:find("^./") and not path:find("core") and not path:find("plugins") and not path:find("loadall.so") then
160 local path = path:gsub("%?", "wmii/?")
161 table.insert(plugin_paths, path)
165 -- where to find wmiirc (see find_wmiirc())
166 wmiirc_path = wmiidir .. "/wmiirc.lua;"
167 .. wmiidir .. "/wmiirc;"
168 .. "%RC_DIR%/wmiirc.lua;"
169 .. "%RC_DIR%/wmiirc"
171 -- ========================================================================
172 -- LOCAL HELPERS
173 -- ========================================================================
175 --[[
176 =pod
178 =item log ( str )
180 Log the message provided in C<str>
182 Currently just writes to io.stderr
184 =cut
185 --]]
186 function log (str)
187 if get_conf("debug") then
188 io.stderr:write (str .. "\n")
192 --[[
193 =pod
195 =item find_wmiirc ( )
197 Locates the wmiirc script. It looks in %HOME_WMII% and %RC_DIR%
198 for the first lua script bearing the name wmiirc.lua or wmiirc. Returns
199 first match.
201 =cut
202 --]]
203 function find_wmiirc()
204 local fn
205 for fn in string.gmatch(wmiirc_path, "[^;]+") do
206 -- try to locate the files locally
207 local file = io.open(fn, "r")
208 if file then
209 local txt = file:read("*line")
210 file:close()
211 if type(txt) == 'string' and txt:match("lua") then
212 return fn
216 return nil
220 -- ========================================================================
221 -- MAIN ACCESS FUNCTIONS
222 -- ========================================================================
224 --[[
225 =pod
227 =item ls ( dir, fmt )
229 List the wmii filesystem directory provided in C<dir>, in the format specified
230 by C<fmt>.
232 Returns an iterator of TODO
234 =cut
235 --]]
236 function ls (dir, fmt)
237 local verbose = fmt and fmt:match("l")
239 local s = wmixp:stat(dir)
240 if not s then
241 return function () return nil end
243 if s.modestr:match("^[^d]") then
244 return function ()
245 return stat2str(verbose, s)
249 local itr = wmixp:idir (dir)
250 if not itr then
251 --return function ()
252 return nil
253 --end
257 return function ()
258 local s = itr()
259 if s then
260 return stat2str(verbose, s)
262 return nil
266 local function stat2str(verbose, stat)
267 if verbose then
268 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
269 else
270 if stat.modestr:match("^d") then
271 return stat.name .. "/"
272 else
273 return stat.name
278 -- ------------------------------------------------------------------------
279 -- read all contents of a wmii virtual file
280 function read (file)
281 return wmixp:read (file)
284 -- ------------------------------------------------------------------------
285 -- return an iterator which walks all the lines in the file
287 -- example:
288 -- for event in wmii.iread("/ctl")
289 -- ...
290 -- end
292 -- NOTE: don't use iread for files that could block, as this will interfere
293 -- with timer processing and event delivery. Instead fork off a process to
294 -- execute wmiir and read back the responses via callback.
295 function iread (file)
296 return wmixp:iread(file)
299 -- ------------------------------------------------------------------------
300 -- create a wmii file, optionally write data to it
301 function create (file, data)
302 wmixp:create(file, data)
305 -- ------------------------------------------------------------------------
306 -- remove a wmii file
307 function remove (file)
308 wmixp:remove(file)
311 -- ------------------------------------------------------------------------
312 -- write a value to a wmii virtual file system
313 function write (file, value)
314 wmixp:write (file, value)
317 -- ------------------------------------------------------------------------
318 -- setup a table describing the menu command
319 local function menu_cmd (prompt)
320 local cmdt = { wmiir, "setsid", "dmenu", "-b" }
321 local fn = get_ctl("font")
322 if fn then
323 cmdt[#cmdt+1] = "-fn"
324 cmdt[#cmdt+1] = fn
326 local normcolors = get_ctl("normcolors")
327 if normcolors then
328 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
329 if nf then
330 cmdt[#cmdt+1] = "-nf"
331 cmdt[#cmdt+1] = "'" .. nf .. "'"
333 if nb then
334 cmdt[#cmdt+1] = "-nb"
335 cmdt[#cmdt+1] = "'" .. nb .. "'"
338 local focuscolors = get_ctl("focuscolors")
339 if focuscolors then
340 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
341 if sf then
342 cmdt[#cmdt+1] = "-sf"
343 cmdt[#cmdt+1] = "'" .. sf .. "'"
345 if sb then
346 cmdt[#cmdt+1] = "-sb"
347 cmdt[#cmdt+1] = "'" .. sb .. "'"
350 if prompt then
351 cmdt[#cmdt+1] = "-p"
352 cmdt[#cmdt+1] = "'" .. prompt .. "'"
355 return cmdt
358 -- ------------------------------------------------------------------------
359 -- displays the menu given an table of entires, returns selected text
360 function menu (tbl, prompt)
361 local menu = menu_cmd(prompt)
363 local infile = os.tmpname()
364 local fh = io.open (infile, "w+")
366 local i,v
367 for i,v in pairs(tbl) do
368 if type(i) == 'number' and type(v) == 'string' then
369 fh:write (v)
370 else
371 fh:write (i)
373 fh:write ("\n")
375 fh:close()
377 local outfile = os.tmpname()
379 menu[#menu+1] = "<"
380 menu[#menu+1] = infile
381 menu[#menu+1] = ">"
382 menu[#menu+1] = outfile
384 local cmd = table.concat(menu," ")
385 os.execute (cmd)
387 fh = io.open (outfile, "r")
388 os.remove (outfile)
390 local sel = fh:read("*l")
391 fh:close()
393 return sel
396 -- ------------------------------------------------------------------------
397 -- displays the a tag selection menu, returns selected tag
398 function tag_menu ()
399 local tags = get_tags()
401 return menu(tags, "tag:")
404 -- ------------------------------------------------------------------------
405 -- displays the a program menu, returns selected program
406 function prog_menu ()
407 local menu = menu_cmd("cmd:")
409 local outfile = os.tmpname()
411 menu[#menu+1] = ">"
412 menu[#menu+1] = outfile
414 local hstt = { }
415 for n in prog_hist:walk_reverse_unique() do
416 hstt[#hstt+1] = "echo '" .. n .. "' ; "
419 local cmd = "(" .. table.concat(hstt)
420 .. "dmenu_path ) |"
421 .. table.concat(menu," ")
422 os.execute (cmd)
424 local fh = io.open (outfile, "rb")
425 os.remove (outfile)
427 local prog = fh:read("*l")
428 io.close (fh)
430 return prog
433 -- ------------------------------------------------------------------------
434 -- returns a table of sorted tags names
435 function get_tags()
436 local t = {}
437 local s
438 for s in wmixp:idir ("/tag") do
439 if s.name and not (s.name == "sel") then
440 t[#t + 1] = s.name
443 table.sort(t)
444 return t
447 -- ------------------------------------------------------------------------
448 -- returns a table of sorted screen names
449 function get_screens()
450 local t = {}
451 local s
452 local empty = true
453 for s in wmixp:idir ("/screen") do
454 if s.name and not (s.name == "sel") then
455 t[#t + 1] = s.name
456 empty = false
459 if empty then
460 return nil
462 table.sort(t)
463 return t
466 -- ------------------------------------------------------------------------
467 -- returns current view, on current screen or specified screen
468 function get_view(screen)
469 return get_screen_ctl(screen, "view") or get_ctl("view")
472 -- ------------------------------------------------------------------------
473 -- changes the current view to the name given
474 function set_view(sel)
475 local cur = get_view()
476 local all = get_tags()
478 if #all < 2 or sel == cur then
479 -- nothing to do if we have less then 2 tags
480 return
483 if not (type(sel) == "string") then
484 error ("string argument expected")
487 -- set new view
488 write ("/ctl", "view " .. sel)
491 -- ------------------------------------------------------------------------
492 -- changes the current view to the index given
493 function set_view_index(sel)
494 local cur = get_view()
495 local all = get_tags()
497 if #all < 2 then
498 -- nothing to do if we have less then 2 tags
499 return
502 local num = tonumber (sel)
503 if not num then
504 error ("number argument expected")
507 local name = all[sel]
508 if not name or name == cur then
509 return
512 -- set new view
513 write ("/ctl", "view " .. name)
516 -- ------------------------------------------------------------------------
517 -- chnages to current view by offset given
518 function set_view_ofs(jump)
519 local cur = get_view()
520 local all = get_tags()
522 if #all < 2 then
523 -- nothing to do if we have less then 2 tags
524 return
527 -- range check
528 if (jump < - #all) or (jump > #all) then
529 error ("view selector is out of range")
532 -- find the one that's selected index
533 local curi = nil
534 local i,v
535 for i,v in pairs (all) do
536 if v == cur then curi = i end
539 -- adjust by index
540 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
541 if (newi < - #all) or (newi > #all) then
542 error ("error computng new view")
545 write ("/ctl", "view " .. all[newi])
548 -- ------------------------------------------------------------------------
549 -- toggle between last view and current view
550 function toggle_view()
551 local last = view_hist[#view_hist]
552 if last then
553 set_view(last)
557 -- ========================================================================
558 -- ACTION HANDLERS
559 -- ========================================================================
561 local action_handlers = {
562 man = function (act, args)
563 local xterm = get_conf("xterm") or "xterm"
564 local page = args
565 if (not page) or (not page:match("%S")) then
566 page = wmiidir .. "/wmii.3lua"
568 local cmd = xterm .. " -e man " .. page .. " &"
569 log (" executing: " .. cmd)
570 os.execute (wmiir .. " setsid " .. cmd)
571 end,
573 quit = function ()
574 write ("/ctl", "quit")
575 end,
577 exec = function (act, args)
578 local what = args or "wmii"
579 log (" asking wmii to exec " .. tostring(what))
580 cleanup()
581 write ("/ctl", "exec " .. what)
582 end,
584 xlock = function (act)
585 local cmd = get_conf("xlock") or "xscreensaver-command --lock"
586 os.execute (wmiir .. " setsid " .. cmd)
587 end,
589 wmiirc = function ()
590 if have_posix then
591 local wmiirc = find_wmiirc()
592 if wmiirc then
593 log (" executing: lua " .. wmiirc)
594 cleanup()
595 posix.exec (wmiirc)
596 posix.exec ("/bin/sh", "-c", "exec lua wmiirc")
597 posix.exec ("%LUA_BIN%", wmiirc)
598 posix.exec ("/usr/bin/lua", wmiirc)
600 else
601 log("sorry cannot restart; you don't have lua's posix library.")
603 end,
605 urgent = function ()
606 wmixp:write ("/client/sel/ctl", "Urgent toggle")
607 end,
609 --[[
610 rehash = function ()
611 -- TODO: consider storing list of executables around, and
612 -- this will then reinitialize that list
613 log (" TODO: rehash")
614 end,
616 status = function ()
617 -- TODO: this should eventually update something on the /rbar
618 log (" TODO: status")
619 end,
620 --]]
623 --[[
624 =pod
626 =item add_action_handler (action, fn)
628 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
630 =cut
631 --]]
632 function add_action_handler (action, fn)
634 if type(action) ~= "string" or type(fn) ~= "function" then
635 error ("expecting a string and a function")
638 if action_handlers[action] then
639 error ("action handler already exists for '" .. action .. "'")
642 action_handlers[action] = fn
645 --[[
646 =pod
648 =item remove_action_handler (action)
650 Remove an action handler callback function for the given action string I<action>.
652 =cut
653 --]]
654 function remove_action_handler (action)
656 action_handlers[action] = nil
659 -- ========================================================================
660 -- KEY HANDLERS
661 -- ========================================================================
663 function ke_fullscreen_toggle()
664 wmixp:write ("/client/sel/ctl", "Fullscreen toggle")
667 function ke_view_starting_with_letter (letter)
668 local i,v
670 -- find the view name in history in reverse order
671 for i=#view_hist,1,-1 do
672 v = view_hist[i]
673 if letter == v:sub(1,1) then
674 set_view(v)
675 return true
679 -- otherwise just pick the first view that matches
680 local all = get_tags()
681 for i,v in pairs(all) do
682 if letter == v:sub(1,1) then
683 set_view_index (i)
684 return true
688 return false
691 function ke_handle_action()
692 local actions = { }
693 local seen = {}
695 local n
696 for n in action_hist:walk_reverse() do
697 if not seen[n] then
698 actions[#actions+1] = n
699 seen[n] = 1
703 local v
704 for n,v in pairs(action_handlers) do
705 if not seen[n] then
706 actions[#actions+1] = n
707 seen[n] = 1
711 local text = menu(actions, "action:")
712 if text then
713 log ("Action: " .. text)
714 local act = text
715 local args = nil
716 local si = text:find("%s")
717 if si then
718 act,args = string.match(text .. " ", "(%w+)%s(.+)")
720 if act then
721 local fn = action_handlers[act]
722 if fn then
723 action_hist:add (act)
724 local r, err = pcall (fn, act, args)
725 if not r then
726 log ("WARNING: " .. tostring(err))
734 local key_handlers = {
735 ["*"] = function (key)
736 log ("*: " .. key)
737 end,
739 -- execution and actions
740 ["Mod1-Return"] = function (key)
741 local xterm = get_conf("xterm") or "xterm"
742 log (" executing: " .. xterm)
743 os.execute (wmiir .. " setsid " .. xterm .. " &")
744 end,
745 ["Mod1-Shift-Return"] = function (key)
746 local tag = tag_menu()
747 if tag then
748 local xterm = get_conf("xterm") or "xterm"
749 log (" executing: " .. xterm .. " on: " .. tag)
750 next_client_goes_to_tag = tag
751 os.execute (wmiir .. " setsid " .. xterm .. " &")
753 end,
754 ["Mod1-a"] = function (key)
755 ke_handle_action()
756 end,
757 ["Mod1-p"] = function (key)
758 local prog = prog_menu()
759 if prog then
760 prog_hist:add(prog:match("([^ ]+)"))
761 log (" executing: " .. prog)
762 os.execute (wmiir .. " setsid " .. prog .. " &")
764 end,
765 ["Mod1-Shift-p"] = function (key)
766 local tag = tag_menu()
767 if tag then
768 local prog = prog_menu()
769 if prog then
770 log (" executing: " .. prog .. " on: " .. tag)
771 next_client_goes_to_tag = tag
772 os.execute (wmiir .. " setsid " .. prog .. " &")
775 end,
776 ["Mod1-Shift-c"] = function (key)
777 write ("/client/sel/ctl", "kill")
778 end,
780 -- HJKL active selection
781 ["Mod1-h"] = function (key)
782 write ("/tag/sel/ctl", "select left")
783 end,
784 ["Mod1-l"] = function (key)
785 write ("/tag/sel/ctl", "select right")
786 end,
787 ["Mod1-j"] = function (key)
788 write ("/tag/sel/ctl", "select down")
789 end,
790 ["Mod1-k"] = function (key)
791 write ("/tag/sel/ctl", "select up")
792 end,
794 -- HJKL movement
795 ["Mod1-Shift-h"] = function (key)
796 write ("/tag/sel/ctl", "send sel left")
797 end,
798 ["Mod1-Shift-l"] = function (key)
799 write ("/tag/sel/ctl", "send sel right")
800 end,
801 ["Mod1-Shift-j"] = function (key)
802 write ("/tag/sel/ctl", "send sel down")
803 end,
804 ["Mod1-Shift-k"] = function (key)
805 write ("/tag/sel/ctl", "send sel up")
806 end,
808 -- floating vs tiled
809 ["Mod1-space"] = function (key)
810 write ("/tag/sel/ctl", "select toggle")
811 end,
812 ["Mod1-Shift-space"] = function (key)
813 write ("/tag/sel/ctl", "send sel toggle")
814 end,
816 -- work spaces (# and @ are wildcards for numbers and letters)
817 ["Mod4-#"] = function (key, num)
818 -- first attempt to find a view that starts with the number requested
819 local num_str = tostring(num)
820 if not ke_view_starting_with_letter (num_str) then
821 -- if we fail, then set it to the index requested
822 set_view_index (num)
824 end,
825 ["Mod4-Shift-#"] = function (key, num)
826 write ("/client/sel/tags", tostring(num))
827 end,
828 ["Mod4-@"] = function (key, letter)
829 ke_view_starting_with_letter (letter)
830 end,
831 ["Mod4-Shift-@"] = function (key, letter)
832 local all = get_tags()
833 local i,v
834 for i,v in pairs(all) do
835 if letter == v:sub(1,1) then
836 write ("/client/sel/tags", v)
837 break
840 end,
841 ["Mod1-comma"] = function (key)
842 set_view_ofs (-1)
843 end,
844 ["Mod1-period"] = function (key)
845 set_view_ofs (1)
846 end,
847 ["Mod1-r"] = function (key)
848 -- got to the last view
849 toggle_view()
850 end,
852 -- switching views and retagging
853 ["Mod1-t"] = function (key)
854 -- got to a view
855 local tag = tag_menu()
856 if tag then
857 set_view (tag)
859 end,
860 ["Mod1-Shift-t"] = function (key)
861 -- move selected client to a tag
862 local tag = tag_menu()
863 if tag then
864 write ("/client/sel/tags", tag)
866 end,
867 ["Mod1-Shift-r"] = function (key)
868 -- move selected client to a tag, and follow
869 local tag = tag_menu()
870 if tag then
871 -- get the current window id
872 local xid = wmixp:read("/client/sel/ctl") or ""
874 -- modify the tag
875 write("/client/sel/tags", tag)
877 -- if the client is still in this tag, then
878 -- it might have been a regexp tag... check
879 local test = wmixp:read("/client/sel/ctl")
880 if not test or test ~= xid then
881 -- if the window moved, follow it
882 set_view(tag)
885 end,
886 ["Mod1-Control-t"] = function (key)
887 log (" TODO: Mod1-Control-t: " .. key)
888 end,
890 -- column modes
891 ["Mod1-d"] = function (key)
892 write("/tag/sel/ctl", "colmode sel default-max")
893 end,
894 ["Mod1-s"] = function (key)
895 write("/tag/sel/ctl", "colmode sel stack-max")
896 end,
897 ["Mod1-m"] = function (key)
898 write("/tag/sel/ctl", "colmode sel stack+max")
899 end,
900 ["Mod1-f"] = function (key)
901 ke_fullscreen_toggle()
902 end,
904 -- changing client flags
905 ["Shift-Mod1-f"] = function (key)
906 log ("setting flags")
908 local cli = get_client ()
910 local flags = { "suspend", "raw" }
911 local current_flags = cli:flags_string()
913 local what = menu(flags, "current flags: " .. current_flags .. " toggle:")
915 cli:toggle(what)
916 end,
917 ["Mod4-space"] = function (key)
918 local cli = get_client ()
919 cli:toggle("raw")
920 end,
923 --[[
924 =pod
926 =item add_key_handler (key, fn)
928 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
930 =cut
931 --]]
932 function add_key_handler (key, fn)
934 if type(key) ~= "string" or type(fn) ~= "function" then
935 error ("expecting a string and a function")
938 if key_handlers[key] then
939 -- TODO: we may wish to allow multiple handlers for one keypress
940 error ("key handler already exists for '" .. key .. "'")
943 key_handlers[key] = fn
946 --[[
947 =pod
949 =item remove_key_handler (key)
951 Remove an key handler callback function for the given key I<key>.
953 Returns the handler callback function.
955 =cut
956 --]]
957 function remove_key_handler (key)
959 local fn = key_handlers[key]
960 key_handlers[key] = nil
961 return fn
964 --[[
965 =pod
967 =item remap_key_handler (old_key, new_key)
969 Remove a key handler callback function from the given key I<old_key>,
970 and assign it to a new key I<new_key>.
972 =cut
973 --]]
974 function remap_key_handler (old_key, new_key)
976 local fn = remove_key_handler(old_key)
978 return add_key_handler (new_key, fn)
982 -- ------------------------------------------------------------------------
983 -- update the /keys wmii file with the list of all handlers
984 local alphabet="abcdefghijklmnopqrstuvwxyz"
985 function update_active_keys ()
986 local t = {}
987 local x, y
988 for x,y in pairs(key_handlers) do
989 if x:find("%w") then
990 local i = x:find("#$")
991 if i then
992 local j
993 for j=0,9 do
994 t[#t + 1] = x:sub(1,i-1) .. j
996 else
997 i = x:find("@$")
998 if i then
999 local j
1000 for j=1,alphabet:len() do
1001 local a = alphabet:sub(j,j)
1002 t[#t + 1] = x:sub(1,i-1) .. a
1004 else
1005 t[#t + 1] = tostring(x)
1010 local all_keys = table.concat(t, "\n")
1011 --log ("setting /keys to...\n" .. all_keys .. "\n");
1012 write ("/keys", all_keys)
1015 -- ------------------------------------------------------------------------
1016 -- update the /lbar wmii file with the current tags
1017 function update_displayed_tags ()
1018 -- list of all screens
1019 local screens = get_screens()
1020 if not screens then
1021 update_displayed_tags_on_screen()
1022 return
1025 local i, s
1026 for i,s in pairs(screens) do
1027 update_displayed_tags_on_screen(s)
1031 function tag_display(tag, selected)
1032 return tag
1035 function update_displayed_tags_on_screen(s)
1036 local lbar = "/lbar"
1037 if s then
1038 lbar = "/screen/" .. s .. "/lbar"
1041 -- colours for screen
1042 local fc = get_screen_ctl(s, "focuscolors") or get_ctl("focuscolors") or ""
1043 local nc = get_screen_ctl(s, "normcolors") or get_ctl("normcolors") or ""
1045 -- build up a table of existing tags in the /lbar
1046 local old = {}
1047 local ent
1048 for ent in wmixp:idir (lbar) do
1049 old[ent.name] = 1
1052 -- for all actual tags in use create any entries in /lbar we don't have
1053 -- clear the old table entries if we have them
1054 local cur = get_view(s)
1055 local all = get_tags()
1056 local i,v
1057 for i,v in pairs(all) do
1058 local color = nc
1059 if cur == v then
1060 color = fc
1062 local str = tag_display(v,selected)
1063 if not old[v] then
1064 create (lbar .. "/" .. v, color .. " " .. str)
1066 write (lbar .. "/" .. v, color .. " " .. str)
1067 old[v] = nil
1070 -- ignore widgets on the lbar
1071 for i,v in pairs(widgets) do
1072 if v.bar == 'lbar' then
1073 old[v.name] = nil
1077 -- anything left in the old table should be removed now
1078 for i,v in pairs(old) do
1079 if v then
1080 remove(lbar.."/"..i)
1084 -- this is a hack, and should brobably be rethought
1085 -- the intent is to distinguish the multiple screens
1086 if s then
1087 create ("/screen/"..s.."/lbar/000000000000000000", '-'..s..'-')
1091 function create_tag_widget(name)
1092 local nc = get_ctl("normcolors") or ""
1093 local screens = get_screens()
1094 if not screens then
1095 create ("/lbar/" .. name, nc .. " " .. name)
1096 return
1098 local i, s
1099 for i,s in pairs(screens) do
1100 create ("/screen/"..s.."/lbar/" .. name, nc .. " " .. tag_display(name))
1104 function destroy_tag_widget(name)
1105 local screens = get_screens()
1106 if not screens then
1107 remove ("/lbar/" .. name)
1108 return
1110 local i, s
1111 for i,s in pairs(screens) do
1112 remove ("/screen/"..s.."/lbar/" .. name)
1117 -- ========================================================================
1118 -- EVENT HANDLERS
1119 -- ========================================================================
1121 local widget_ev_handlers = {
1124 --[[
1125 =pod
1127 =item _handle_widget_event (ev, arg)
1129 Top-level event handler for redispatching events to widgets. This event
1130 handler is added for any widget event that currently has a widget registered
1131 for it.
1133 Valid widget events are currently
1135 RightBarMouseDown <buttonnumber> <widgetname>
1136 RightBarClick <buttonnumber> <widgetname>
1138 the "Click" event is sent on mouseup.
1140 The callbacks are given only the button number as their argument, to avoid the
1141 need to reparse.
1143 =cut
1144 --]]
1146 local function _handle_widget_event (ev, arg)
1148 log("_handle_widget_event: " .. tostring(ev) .. " - " .. tostring(arg))
1150 -- parse arg to strip out our widget name
1151 local number,wname = string.match(arg, "(%d+)%s+(.+)")
1153 -- check our dispatch table for that widget
1154 if not wname then
1155 log("Didn't find wname")
1156 return
1159 local wtable = widget_ev_handlers[wname]
1160 if not wtable then
1161 log("No widget cares about" .. wname)
1162 return
1165 local fn = wtable[ev] or wtable["*"]
1166 if fn then
1167 success, err = pcall( fn, ev, tonumber(number) )
1168 if not success then
1169 log("Callback had an error in _handle_widget_event: " .. tostring(err) )
1170 return nil
1172 else
1173 log("no function found for " .. ev)
1177 local ev_handlers = {
1178 ["*"] = function (ev, arg)
1179 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
1180 end,
1182 RightBarClick = _handle_widget_event,
1184 -- process timer events
1185 ProcessTimerEvents = function (ev, arg)
1186 process_timers()
1187 end,
1189 -- exit if another wmiirc started up
1190 Start = function (ev, arg)
1191 if arg then
1192 if arg == "wmiirc" then
1193 -- backwards compatibility with bash version
1194 log (" exiting; pid=" .. tostring(myid))
1195 cleanup()
1196 os.exit (0)
1197 else
1198 -- ignore if it came from us
1199 local pid = string.match(arg, "wmiirc (%d+)")
1200 if pid then
1201 local pid = tonumber (pid)
1202 if not (pid == myid) then
1203 log (" exiting; pid=" .. tostring(myid))
1204 cleanup()
1205 os.exit (0)
1210 end,
1212 -- tag management
1213 CreateTag = function (ev, arg)
1214 log ("CreateTag: " .. arg)
1215 create_tag_widget(arg)
1216 end,
1217 DestroyTag = function (ev, arg)
1218 log ("DestroyTag: " .. arg)
1219 destroy_tag_widget(arg)
1221 -- remove the tag from history
1222 local i,v
1223 for i=#view_hist,1,-1 do
1224 v = view_hist[i]
1225 if arg == v then
1226 table.remove(view_hist,i)
1229 end,
1231 FocusTag = function (ev, arg)
1232 log ("FocusTag: " .. arg)
1234 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1235 if not tag then
1236 return
1239 local file = "/lbar/" .. tag
1240 if scrn and scrn:len() > 0 then
1241 file = "/screen/" .. scrn .. file
1244 local fc = get_screen_ctl(scrn, "focuscolors") or get_ctl("focuscolors") or ""
1245 log ("# echo " .. fc .. " " .. tag .. " | wmiir write " .. file)
1247 str = tag_display(tag,true)
1248 create (file, fc .. " " .. str)
1249 write (file, fc .. " " .. str)
1250 end,
1251 UnfocusTag = function (ev, arg)
1252 log ("UnfocusTag: " .. arg)
1254 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1255 if not tag then
1256 return
1259 local file = "/lbar/" .. tag
1260 if scrn and scrn:len() > 0 then
1261 file = "/screen/" .. scrn .. file
1264 local nc = get_screen_ctl(scrn, "normcolors") or get_ctl("normcolors") or ""
1265 log ("# echo " .. nc .. " " .. tag .. " | wmiir write " .. file)
1267 str = tag_display(tag,true)
1268 create (file, nc .. " " .. str)
1269 write (file, nc .. " " .. str)
1271 -- don't duplicate the last entry
1272 if not (tag == view_hist[#view_hist]) then
1273 view_hist[#view_hist+1] = tag
1275 -- limit to view_hist_max
1276 if #view_hist > view_hist_max then
1277 table.remove(view_hist, 1)
1280 end,
1282 -- key event handling
1283 Key = function (ev, arg)
1284 log ("Key: " .. arg)
1285 local magic = nil
1286 -- can we find an exact match?
1287 local fn = key_handlers[arg]
1288 if not fn then
1289 local key = arg:gsub("-%d$", "-#")
1290 -- can we find a match with a # wild card for the number
1291 fn = key_handlers[key]
1292 if fn then
1293 -- convert the trailing number to a number
1294 magic = tonumber(arg:match("-(%d)$"))
1297 if not fn then
1298 local key = arg:gsub("-%a$", "-@")
1299 -- can we find a match with a @ wild card for a letter
1300 fn = key_handlers[key]
1301 if fn then
1302 -- split off the trailing letter
1303 magic = arg:match("-(%a)$")
1306 if not fn then
1307 -- everything else failed, try default match
1308 fn = key_handlers["*"]
1310 if fn then
1311 local r, err = pcall (fn, arg, magic)
1312 if not r then
1313 log ("WARNING: " .. tostring(err))
1316 end,
1318 -- mouse handling on the lbar
1319 LeftBarClick = function (ev, arg)
1320 local button,tag = string.match(arg, "(%w+)%s+(%S+)")
1321 set_view (tag)
1322 end,
1324 -- focus updates
1325 ClientFocus = function (ev, arg)
1326 log ("ClientFocus: " .. arg)
1327 client_focused (arg)
1328 end,
1329 ColumnFocus = function (ev, arg)
1330 log ("ColumnFocus: " .. arg)
1331 end,
1333 -- client handling
1334 CreateClient = function (ev, arg)
1335 if next_client_goes_to_tag then
1336 local tag = next_client_goes_to_tag
1337 local cli = arg
1338 next_client_goes_to_tag = nil
1339 write ("/client/" .. cli .. "/tags", tag)
1340 set_view(tag)
1342 client_created (arg)
1343 end,
1344 DestroyClient = function (ev, arg)
1345 client_destoryed (arg)
1346 end,
1348 -- urgent tag
1349 UrgentTag = function (ev, arg)
1350 log ("UrgentTag: " .. arg)
1351 write ("/lbar/" .. arg, "*" .. arg);
1352 end,
1353 NotUrgentTag = function (ev, arg)
1354 log ("NotUrgentTag: " .. arg)
1355 write ("/lbar/" .. arg, arg);
1356 end,
1358 -- notifications
1359 Unresponsive = function (ev, arg)
1360 log ("Unresponsive: " .. arg)
1361 -- TODO ask the user if it shoudl be killed off
1362 end,
1364 Notice = function (ev, arg)
1365 log ("Notice: " .. arg)
1366 -- TODO send to the message plugin (or implement there)
1367 end,
1369 -- /
1372 --[[
1373 =pod
1375 =item add_widget_event_handler (wname, ev, fn)
1377 Add an event handler callback for the I<ev> event on the widget named I<wname>
1379 =cut
1380 --]]
1382 function add_widget_event_handler (wname, ev, fn)
1383 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
1384 error ("expecting string for widget name, string for event name and a function callback")
1387 -- Make sure the widget event handler is present
1388 if not ev_handlers[ev] then
1389 ev_handlers[ev] = _handle_widget_event
1392 if not widget_ev_handlers[wname] then
1393 widget_ev_handlers[wname] = { }
1396 if widget_ev_handlers[wname][ev] then
1397 -- TODO: we may wish to allow multiple handlers for one event
1398 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
1401 widget_ev_handlers[wname][ev] = fn
1404 --[[
1405 =pod
1407 =item remove_widget_event_handler (wname, ev)
1409 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1411 =cut
1412 --]]
1413 function remove_event_handler (wname, ev)
1415 if not widget_ev_handlers[wname] then
1416 return
1419 widget_ev_handlers[wname][ev] = nil
1422 --[[
1423 =pod
1425 =item add_event_handler (ev, fn)
1427 Add an event handler callback function, I<fn>, for the given event I<ev>.
1429 =cut
1430 --]]
1431 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1432 function add_event_handler (ev, fn)
1433 if type(ev) ~= "string" or type(fn) ~= "function" then
1434 error ("expecting a string and a function")
1437 if ev_handlers[ev] then
1438 -- TODO: we may wish to allow multiple handlers for one event
1439 error ("event handler already exists for '" .. ev .. "'")
1443 ev_handlers[ev] = fn
1446 --[[
1447 =pod
1449 =item remove_event_handler (ev)
1451 Remove an event handler callback function for the given event I<ev>.
1453 =cut
1454 --]]
1455 function remove_event_handler (ev)
1457 ev_handlers[ev] = nil
1461 -- ========================================================================
1462 -- MAIN INTERFACE FUNCTIONS
1463 -- ========================================================================
1465 local config = {
1466 xterm = 'x-terminal-emulator',
1467 xlock = "xscreensaver-command --lock",
1468 debug = false,
1471 -- ------------------------------------------------------------------------
1472 -- write configuration to /ctl wmii file
1473 -- wmii.set_ctl({ "var" = "val", ...})
1474 -- wmii.set_ctl("var, "val")
1475 function set_ctl (first,second)
1476 if type(first) == "table" and second == nil then
1477 local x, y
1478 for x, y in pairs(first) do
1479 write ("/ctl", x .. " " .. y)
1482 elseif type(first) == "string" and type(second) == "string" then
1483 write ("/ctl", first .. " " .. second)
1485 else
1486 error ("expecting a table or two string arguments")
1490 -- ------------------------------------------------------------------------
1491 -- read a value from /ctl wmii file
1492 -- table = wmii.get_ctl()
1493 -- value = wmii.get_ctl("variable")
1494 function get_ctl (name)
1495 local s
1496 local t = {}
1497 for s in iread("/ctl") do
1498 local var,val = s:match("(%w+)%s+(.+)")
1499 if var == name then
1500 return val
1502 t[var] = val
1504 if not name then
1505 return t
1507 return nil
1510 -- ------------------------------------------------------------------------
1511 -- write configuration to /screen/*/ctl wmii file
1512 -- wmii.set_screen_ctl("screen", { "var" = "val", ...})
1513 -- wmii.set_screen_ctl("screen", "var, "val")
1514 function set_screen_ctl (screen, first, second)
1515 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1516 if not screen then
1517 error ("screen is not set")
1518 elseif type(first) == "table" and second == nil then
1519 local x, y
1520 for x, y in pairs(first) do
1521 write (ctl, x .. " " .. y)
1524 elseif type(first) == "string" and type(second) == "string" then
1525 write (ctl, first .. " " .. second)
1527 else
1528 error ("expecting a screen name, followed by a table or two string arguments")
1532 -- ------------------------------------------------------------------------
1533 -- read a value from /screen/*/ctl wmii file
1534 -- table = wmii.get_screen_ctl("screen")
1535 -- value = wmii.get_screen_ctl("screen", "variable")
1536 function get_screen_ctl (screen, name)
1537 local s
1538 local t = {}
1539 if not screen then
1540 return nil
1542 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1543 for s in iread(ctl) do
1544 local var,val = s:match("(%w+)%s+(.+)")
1545 if var == name then
1546 return val
1548 -- sometimes first line is the name of the entry
1549 -- in which case there will be no space
1550 t[var or ""] = val
1552 if not name then
1553 return t
1555 return nil
1558 -- ------------------------------------------------------------------------
1559 -- set an internal wmiirc.lua variable
1560 -- wmii.set_conf({ "var" = "val", ...})
1561 -- wmii.set_conf("var, "val")
1562 function set_conf (first,second)
1563 if type(first) == "table" and second == nil then
1564 local x, y
1565 for x, y in pairs(first) do
1566 config[x] = y
1569 elseif type(first) == "string"
1570 and (type(second) == "string"
1571 or type(second) == "number"
1572 or type(second) == "boolean") then
1573 config[first] = second
1575 else
1576 error ("expecting a table, or string and string/number as arguments")
1580 -- ------------------------------------------------------------------------
1581 -- read an internal wmiirc.lua variable
1582 function get_conf (name)
1583 if name then
1584 return config[name]
1586 return config
1589 -- ========================================================================
1590 -- THE EVENT LOOP
1591 -- ========================================================================
1593 -- the event loop instance
1594 local el = eventloop.new()
1595 local event_read_fd = -1
1596 local wmiirc_running = false
1597 local event_read_start = 0
1599 -- ------------------------------------------------------------------------
1600 -- start/restart the core event reading process
1601 local function start_event_reader ()
1602 -- prevent adding two readers
1603 if event_read_fd ~= -1 then
1604 if el:check_exec(event_read_fd) then
1605 return
1608 -- prevert rapid restarts
1609 local now = os.time()
1610 if os.difftime(now, event_read_start) < 5 then
1611 log("wmii: detected rapid restart of /event reader")
1612 local cmd = "wmiir ls /ctl"
1613 if os.execute(cmd) ~= 0 then
1614 log("wmii: cannot confirm communication with wmii, shutting down!")
1615 wmiirc_running = false
1616 return
1618 log("wmii: but things look ok, so we will restart it")
1620 event_read_start = now
1622 -- start a new event reader
1623 log("wmii: starting /event reading process")
1624 event_read_fd = el:add_exec (wmiir .. " read /event",
1625 function (line)
1626 local line = line or "nil"
1628 -- try to split off the argument(s)
1629 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1630 if not ev then
1631 ev = line
1634 -- now locate the handler function and call it
1635 local fn = ev_handlers[ev] or ev_handlers["*"]
1636 if fn then
1637 local r, err = pcall (fn, ev, arg)
1638 if not r then
1639 log ("WARNING: " .. tostring(err))
1644 log("wmii: ... fd=" .. tostring(event_read_fd))
1647 -- ------------------------------------------------------------------------
1648 -- run the event loop and process events, this function does not exit
1649 function run_event_loop ()
1650 -- stop any other instance of wmiirc
1651 wmixp:write ("/event", "Start wmiirc " .. tostring(myid))
1653 log("wmii: updating lbar")
1655 update_displayed_tags ()
1657 log("wmii: updating rbar")
1659 update_displayed_widgets ()
1661 log("wmii: updating active keys")
1663 update_active_keys ()
1665 log("wmii: starting event loop")
1666 wmiirc_running = true
1667 while wmiirc_running do
1668 start_event_reader()
1669 local sleep_for = process_timers()
1670 el:run_loop(sleep_for)
1672 log ("wmii: exiting")
1675 -- ========================================================================
1676 -- PLUGINS API
1677 -- ========================================================================
1679 api_version = 0.1 -- the API version we export
1681 plugins = {} -- all plugins that were loaded
1683 -- ------------------------------------------------------------------------
1684 -- plugin loader which also verifies the version of the api the plugin needs
1686 -- here is what it does
1687 -- - does a manual locate on the file using package.path
1688 -- - reads in the file w/o using the lua interpreter
1689 -- - locates api_version=X.Y string
1690 -- - makes sure that api_version requested can be satisfied
1691 -- - if the plugins is available it will set variables passed in
1692 -- - it then loads the plugin
1694 -- TODO: currently the api_version must be in an X.Y format, but we may want
1695 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1697 function load_plugin(name, vars)
1698 local backup_path = package.path or "./?.lua"
1700 log ("loading " .. name)
1702 -- this is the version we want to find
1703 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1704 if (not api_major) or (not api_minor) then
1705 log ("WARNING: could not parse api_version in core/wmii.lua")
1706 return nil
1709 -- first find the plugin file
1710 local s, path_match, full_name, file
1711 for i,s in pairs(plugin_paths) do
1712 -- try to locate the files locally
1713 local fn = s:gsub("%?", name)
1714 file = io.open(fn, "r")
1715 if file then
1716 path_match = s
1717 full_name = fn
1718 break
1722 -- read it in
1723 local txt
1724 if file then
1725 txt = file:read("*all")
1726 file:close()
1729 if not txt then
1730 log ("WARNING: could not load plugin '" .. name .. "'")
1731 return nil
1734 -- find the api_version line
1735 local line, plugin_version
1736 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1737 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1738 if plugin_version then
1739 break
1743 if not plugin_version then
1744 log ("WARNING: could not find api_version string in plugin '" .. name .. "'")
1745 return nil
1748 -- decompose the version string
1749 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1750 if (not plugin_major) or (not plugin_minor) then
1751 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1752 return nil
1755 -- make a version test
1756 if plugin_major ~= api_major then
1757 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1758 .. " (api " .. tonumber(api_version) .. ")")
1759 return nil
1762 if plugin_minor > api_minor then
1763 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1764 .. " (api " .. tonumber(api_version) .. ")")
1765 return nil
1768 -- the configuration parameters before loading
1769 if type(vars) == "table" then
1770 local var, val
1771 for var,val in pairs(vars) do
1772 local success = pcall (set_conf, name .. "." .. var, val)
1773 if not success then
1774 log ("WARNING: bad variable {" .. tostring(var) .. ", " .. tostring(val) .. "} "
1775 .. "given; loading '" .. name .. "' plugin failed.")
1776 return nil
1781 -- actually load the module, but use only the path where we though it should be
1782 package.path = path_match
1783 local success,what = pcall (require, name)
1784 package.path = backup_path
1785 if not success then
1786 log ("WARNING: failed to load '" .. name .. "' plugin")
1787 log (" - path: " .. tostring(path_match))
1788 log (" - file: " .. tostring(full_name))
1789 log (" - plugin's api_version: " .. tostring(plugin_version))
1790 log (" - reason: " .. tostring(what))
1791 return nil
1794 -- success
1795 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1796 plugins[name] = what
1797 return what
1800 -- ------------------------------------------------------------------------
1801 -- widget template
1802 widget = {}
1803 widgets = {}
1805 -- ------------------------------------------------------------------------
1806 -- create a widget object and add it to the wmii /rbar
1808 -- examples:
1809 -- widget = wmii.widget:new ("999_clock")
1810 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1811 function widget:new (name, fn, bar)
1812 local o = {}
1814 if type(name) == "string" then
1815 o.name = name
1816 if type(fn) == "function" then
1817 o.fn = fn
1819 o.bar = bar or "rbar"
1820 else
1821 error ("expected name followed by an optional function as arguments")
1824 setmetatable (o,self)
1825 self.__index = self
1826 self.__gc = function (o) o:hide() end
1828 widgets[name] = o
1830 o:show()
1831 return o
1834 -- ------------------------------------------------------------------------
1835 -- stop and destroy the timer
1836 function widget:delete ()
1837 widgets[self.name] = nil
1838 self:hide()
1841 -- ------------------------------------------------------------------------
1842 -- displays or updates the widget text
1844 -- examples:
1845 -- w:show("foo")
1846 -- w:show("foo", "#888888 #222222 #333333")
1847 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1849 function widget:show (txt, colors)
1850 local colors = colors or get_ctl("normcolors") or ""
1851 local txt = txt or self.txt or ""
1852 local towrite = txt
1853 if colors then
1854 towrite = colors .. " " .. towrite
1856 if not self.txt then
1857 create ('/'..self.bar..'/'.. self.name, towrite)
1858 else
1859 write ('/'..self.bar..'/'.. self.name, towrite)
1861 self.txt = txt
1864 -- ------------------------------------------------------------------------
1865 -- hides a widget and removes it from the bar
1866 function widget:hide ()
1867 if self.txt then
1868 remove ('/'..self.bar ..'/'.. self.name)
1869 self.txt = nil
1873 --[[
1874 =pod
1876 =item widget:add_event_handler (ev, fn)
1878 Add an event handler callback for this widget, using I<fn> for event I<ev>
1880 =cut
1881 --]]
1883 function widget:add_event_handler (ev, fn)
1884 add_widget_event_handler( self.name, ev, fn)
1888 -- ------------------------------------------------------------------------
1889 -- remove all /rbar entries that we don't have widget objects for
1890 function update_displayed_widgets ()
1891 -- colours for /rbar
1892 local nc = get_ctl("normcolors") or ""
1894 -- build up a table of existing tags in the /lbar
1895 local old = {}
1896 local s
1897 for s in wmixp:idir ("/rbar") do
1898 old[s.name] = 1
1901 -- for all actual widgets in use we want to remove them from the old list
1902 local i,v
1903 for i,v in pairs(widgets) do
1904 old[v.name] = nil
1907 -- anything left in the old table should be removed now
1908 for i,v in pairs(old) do
1909 if v then
1910 remove("/rbar/"..i)
1915 -- ------------------------------------------------------------------------
1916 -- create a new program and for each line it generates call the callback function
1917 -- returns fd which can be passed to kill_exec()
1918 function add_exec (command, callback)
1919 return el:add_exec (command, callback)
1922 -- ------------------------------------------------------------------------
1923 -- terminates a program spawned off by add_exec()
1924 function kill_exec (fd)
1925 return el:kill_exec (fd)
1928 -- ------------------------------------------------------------------------
1929 -- timer template
1930 timer = {}
1931 local timers = {}
1933 -- ------------------------------------------------------------------------
1934 -- create a timer object and add it to the event loop
1936 -- examples:
1937 -- timer:new (my_timer_fn)
1938 -- timer:new (my_timer_fn, 15)
1939 function timer:new (fn, seconds)
1940 local o = {}
1942 if type(fn) == "function" then
1943 o.fn = fn
1944 else
1945 error ("expected function followed by an optional number as arguments")
1948 setmetatable (o,self)
1949 self.__index = self
1950 self.__gc = function (o) o:stop() end
1952 -- add the timer
1953 timers[#timers+1] = o
1955 if seconds then
1956 o:resched(seconds)
1958 return o
1961 -- ------------------------------------------------------------------------
1962 -- stop and destroy the timer
1963 function timer:delete ()
1964 self:stop()
1965 local i,t
1966 for i,t in pairs(timers) do
1967 if t == self then
1968 table.remove (timers,i)
1969 return
1974 -- ------------------------------------------------------------------------
1975 -- run the timer given new interval
1976 function timer:resched (seconds)
1977 local seconds = seconds or self.interval
1978 if not (type(seconds) == "number") then
1979 error ("timer:resched expected number as argument")
1982 local now = tonumber(os.date("%s"))
1984 self.interval = seconds
1985 self.next_time = now + seconds
1987 -- resort the timer list
1988 table.sort (timers, timer.is_less_then)
1991 -- helper for sorting timers
1992 function timer:is_less_then(another)
1993 if not self.next_time then
1994 return false -- another is smaller, nil means infinity
1996 elseif not another.next_time then
1997 return true -- self is smaller, nil means infinity
1999 elseif self.next_time < another.next_time then
2000 return true -- self is smaller than another
2003 return false -- another is smaller then self
2006 -- ------------------------------------------------------------------------
2007 -- stop the timer
2008 function timer:stop ()
2009 self.next_time = nil
2011 -- resort the timer list
2012 table.sort (timers, timer.is_less_then)
2015 -- ------------------------------------------------------------------------
2016 -- figure out how long before the next event
2017 function time_before_next_timer_event()
2018 local tmr = timers[1]
2019 if tmr and tmr.next_time then
2020 local now = tonumber(os.date("%s"))
2021 local seconds = tmr.next_time - now
2022 if seconds > 0 then
2023 return seconds
2026 return 0 -- sleep for ever
2029 -- ------------------------------------------------------------------------
2030 -- handle outstanding events
2031 function process_timers ()
2032 local now = tonumber(os.date("%s"))
2033 local torun = {}
2034 local i,tmr
2036 for i,tmr in pairs (timers) do
2037 if not tmr then
2038 -- prune out removed timers
2039 table.remove(timers,i)
2040 break
2042 elseif not tmr.next_time then
2043 -- break out once we find a timer that is stopped
2044 break
2046 elseif tmr.next_time > now then
2047 -- break out once we get to the future
2048 break
2051 -- this one is good to go
2052 torun[#torun+1] = tmr
2055 for i,tmr in pairs (torun) do
2056 tmr:stop()
2057 local status,new_interval = pcall (tmr.fn, tmr)
2058 if status then
2059 new_interval = new_interval or self.interval
2060 if new_interval and (new_interval ~= -1) then
2061 tmr:resched(new_interval)
2063 else
2064 log ("ERROR: " .. tostring(new_interval))
2068 local sleep_for = time_before_next_timer_event()
2069 return sleep_for
2072 -- ------------------------------------------------------------------------
2073 -- cleanup everything in preparation for exit() or exec()
2074 function cleanup ()
2076 local i,v,tmr,p
2078 log ("wmii: stopping timer events")
2080 for i,tmr in pairs (timers) do
2081 pcall (tmr.delete, tmr)
2083 timers = {}
2085 log ("wmii: terminating eventloop")
2087 pcall(el.kill_all,el)
2089 log ("wmii: disposing of widgets")
2091 -- dispose of all widgets
2092 for i,v in pairs(widgets) do
2093 pcall(v.delete,v)
2095 timers = {}
2097 -- FIXME: it doesn't seem to do what I want
2098 --[[
2099 log ("wmii: releasing plugins")
2101 for i,p in pairs(plugins) do
2102 if p.cleanup then
2103 pcall (p.cleanup, p)
2106 plugins = {}
2107 --]]
2109 log ("wmii: dormant")
2110 wmiirc_running = false
2113 -- ========================================================================
2114 -- CLIENT HANDLING
2115 -- ========================================================================
2117 --[[
2118 -- Notes on client tracking
2120 -- When a client is created wmii sends us a CreateClient message, and
2121 -- we in turn create a 'client' object and store it in the 'clients'
2122 -- table indexed by the client's ID.
2124 -- Each client object stores the following:
2125 -- .xid - the X client ID
2126 -- .pid - the process ID
2127 -- .prog - program object representing the process
2129 -- The client and program objects track the following modes for each program:
2131 -- raw mode:
2132 -- - for each client window
2133 -- - Mod4-space toggles the state between normal and raw
2134 -- - Mod1-f raw also toggles the state
2135 -- - in raw mode all input goes to the client, except for Mod4-space
2136 -- - a focused client with raw mode enabled is put into raw mode
2138 -- suspend mode:
2139 -- - for each program
2140 -- - Mod1-f suspend toggles the state for current client's program
2141 -- - a focused client, whose program was previous suspended is resumed
2142 -- - an unfocused client, with suspend enabled, will be suspended
2143 -- - suspend/resume is done by sending the STOP/CONT signals to the PID
2144 --]]
2146 function xid_to_pid (xid)
2147 local cmd = "xprop -id " .. tostring(xid) .. " _NET_WM_PID"
2148 local file = io.popen (cmd)
2149 local out = file:read("*a")
2150 file:close()
2151 local pid = out:match("^_NET_WM_PID.*%s+=%s+(%d+)%s+$")
2152 return tonumber(pid)
2155 local focused_xid = nil
2156 local clients = {} -- table of client objects indexed by xid
2157 local programs = {} -- table of program objects indexed by pid
2158 local mode_widget = widget:new ("999_client_mode")
2160 -- make programs table have weak values
2161 -- programs go away as soon as no clients point to it
2162 local programs_mt = {}
2163 setmetatable(programs, programs_mt)
2164 programs_mt.__mode = 'v'
2166 -- program class
2167 program = {}
2168 function program:new (pid)
2169 -- make an object
2170 local o = {}
2171 setmetatable (o,self)
2172 self.__index = self
2173 self.__gc = function (old) old:cont() end
2174 -- initialize the new object
2175 o.pid = pid
2176 -- suspend mode
2177 o.suspend = {}
2178 o.suspend.toggle = function (prog)
2179 prog.suspend.enabled = not prog.suspend.enabled
2181 o.suspend.enabled = false -- if true, defocusing suspends (SIGSTOP)
2182 o.suspend.active = true -- if true, focusing resumes (SIGCONT)
2183 return o
2186 function program:stop ()
2187 if not self.suspend.active then
2188 local cmd = "kill -STOP " .. tostring(self.pid)
2189 log (" executing: " .. cmd)
2190 os.execute (cmd)
2191 self.suspend.active = true
2195 function program:cont ()
2196 if self.suspend.active then
2197 local cmd = "kill -CONT " .. tostring(self.pid)
2198 log (" executing: " .. cmd)
2199 os.execute (cmd)
2200 self.suspend.active = false
2204 function get_program (pid)
2205 local prog = programs[pid]
2206 if pid and not prog then
2207 prog = program:new (pid)
2208 programs[pid] = prog
2210 return prog
2213 -- client class
2214 client = {}
2215 function client:new (xid)
2216 local pid = xid_to_pid(xid)
2217 if not pid then
2218 log ("WARNING: failed to convert XID " .. tostring(xid) .. " to a PID")
2219 return
2221 -- make an object
2222 local o = {}
2223 setmetatable (o,self)
2224 self.__index = function (t,k)
2225 if k == 'suspend' then -- suspend mode is tracked per program
2226 return t.prog.suspend
2228 return self[k]
2230 self.__gc = function (old) old.prog=nil end
2231 -- initialize the new object
2232 o.xid = xid
2233 o.pid = pid
2234 o.prog = get_program (pid)
2235 -- raw mode
2236 o.raw = {}
2237 o.raw.toggle = function (cli)
2238 cli.raw.enabled = not cli.raw.enabled
2239 cli:set_raw_mode()
2241 o.raw.enabled = false -- if true, raw mode enabled when client is focused
2242 return o
2245 function client:stop ()
2246 if self.suspend.enabled then
2247 self.prog:stop()
2251 function client:cont ()
2252 self.prog:cont()
2255 function client:set_raw_mode()
2256 if not self or not self.raw.enabled then -- normal mode
2257 update_active_keys ()
2258 else -- raw mode
2259 write ("/keys", "Mod4-space")
2263 function client:toggle(what)
2264 if what and self[what] then
2265 local ctl = self[what]
2267 ctl.toggle (self)
2269 log ("xid=" .. tostring (xid)
2270 .. " pid=" .. tostring (self.pid) .. " (" .. tostring (self.prog.pid) .. ")"
2271 .. " what=" .. tostring (what)
2272 .. " enabled=" .. tostring(ctl["enabled"]))
2274 mode_widget:show (self:flags_string())
2277 function client:flags_string()
2278 local ret = ''
2279 if self.suspend.enabled then ret = ret .. "s" else ret = ret .. "-" end
2280 if self.raw.enabled then ret = ret .. "r" else ret = ret .. "-" end
2281 return ret
2284 function get_client (xid)
2285 local xid = xid or wmixp:read("/client/sel/ctl")
2286 local cli = clients[xid]
2287 if not cli then
2288 cli = client:new (xid)
2289 clients[xid] = cli
2291 return cli
2294 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2295 function client_created (xid)
2296 log ("-client_created " .. tostring(xid))
2297 return get_client(xid)
2300 function client_destoryed (xid)
2301 log ("-client_destoryed " .. tostring(xid))
2302 if clients[xid] then
2303 local cli = clients[xid]
2304 clients[xid] = nil
2305 log (" del pid: " .. tostring(cli.pid))
2306 cli:cont()
2308 if focused_xid == xid then
2309 focused_xid = nil
2313 function client_focused (xid)
2314 log ("-client_focused " .. tostring(xid))
2315 -- return the current focused xid if nil is passed
2316 if type(xid) ~= 'string' or not xid:match("0x%x*$") then
2317 return focused_xid
2319 -- do nothing if the same xid
2320 if focused_xid == xid then
2321 return clients[xid]
2324 local old = clients[focused_xid]
2325 local new = get_client(xid)
2327 -- handle raw mode switch
2328 if not old or ( old and new and old.raw.enabled ~= new.raw.enabled ) then
2329 new:set_raw_mode()
2332 -- do nothing if the same pid
2333 if old and new and old.pid == new.pid then
2334 mode_widget:show (new:flags_string())
2335 return clients[xid]
2338 if old then
2339 --[[
2340 log (" old pid: " .. tostring(old.pid)
2341 .. " xid: " .. tostring(old.xid)
2342 .. " flags: " .. old:flags_string())
2343 ]]--
2344 old:stop()
2347 if new then
2348 --[[
2349 log (" new pid: " .. tostring(new.pid)
2350 .. " xid: " .. tostring(new.xid)
2351 .. " flags: " .. new:flags_string())
2352 ]]--
2353 new:cont()
2356 mode_widget:show (new:flags_string())
2357 focused_xid = xid
2358 return new
2362 -- ========================================================================
2363 -- DOCUMENTATION
2364 -- ========================================================================
2366 --[[
2367 =pod
2369 =back
2371 =head1 ENVIRONMENT
2373 =over 4
2375 =item WMII_ADDRESS
2377 Used to determine location of wmii's listen socket.
2379 =back
2381 =head1 SEE ALSO
2383 L<wmii(1)>, L<lua(1)>
2385 =head1 AUTHOR
2387 Bart Trojanowski B<< <bart@jukie.net> >>
2389 =head1 COPYRIGHT AND LICENSE
2391 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
2393 This is free software. You may redistribute copies of it under the terms of
2394 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
2395 is NO WARRANTY, to the extent permitted by law.
2397 =cut
2398 --]]