avoid polutting the name space when creating new timers and widgets
[wmiirc-lua.git] / core / wmii.lua
blob940a858906cd3fabf21fb4a4bf21f8796f705e6f
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 = package.path
62 .. ";" .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.lua"
63 package.cpath = package.cpath
64 .. ";" .. os.getenv("HOME") .. "/.wmii-3.5/core/?.so"
65 .. ";" .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.so"
67 local ixp = require "ixp"
68 local eventloop = require "eventloop"
70 local base = _G
71 local io = require("io")
72 local os = require("os")
73 local posix = require("posix")
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 module("wmii")
90 -- get the process id
91 local mypid = posix.getprocessid("pid")
93 -- ========================================================================
94 -- MODULE VARIABLES
95 -- ========================================================================
97 -- wmiir points to the wmiir executable
98 -- TODO: need to make sure that wmiir is in path, and if not find it
99 local wmiir = "wmiir"
101 -- wmii_adr is the address we use when connecting using ixp
102 local wmii_adr = os.getenv("WMII_ADDRESS")
103 or ("unix!/tmp/ns." .. os.getenv("USER") .. "."
104 .. os.getenv("DISPLAY"):match("(:%d+)") .. "/wmii")
106 -- wmixp is the ixp context we use to talk to wmii
107 local wmixp = ixp.new(wmii_adr)
109 -- history of previous views, view_hist[#view_hist] is the last one
110 local view_hist = {} -- sorted with 1 being the oldest
111 local view_hist_max = 10 -- max number to keep track of
113 -- allow for a client to be forced to a tag
114 local next_client_goes_to_tag = nil
116 -- where to find plugins
117 plugin_path = os.getenv("HOME") .. "/.wmii-3.5/plugins/?.so;"
118 .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.lua;"
119 .. "/usr/local/lib/lua/5.1/wmii/?.so;"
120 .. "/usr/local/share/lua/5.1/wmii/?.lua;"
121 .. "/usr/lib/lua/5.1/wmii/?.so;"
122 .. "/usr/share/lua/5.1/wmii/?.lua"
124 -- ========================================================================
125 -- LOCAL HELPERS
126 -- ========================================================================
128 --[[
129 =pod
131 =item log ( str )
133 Log the message provided in C<str>
135 Currently just writes to io.stderr
137 =cut
138 --]]
139 function log (str)
140 io.stderr:write (str .. "\n")
143 -- ========================================================================
144 -- MAIN ACCESS FUNCTIONS
145 -- ========================================================================
147 --[[
148 =pod
150 =item ls ( dir, fmt )
152 List the wmii filesystem directory provided in C<dir>, in the format specified
153 by C<fmt>.
155 Returns an iterator of TODO
157 =cut
158 --]]
159 function ls (dir, fmt)
160 local verbose = fmt and fmt:match("l")
162 local s = wmixp:stat(dir)
163 if not s then
164 return function () return nil end
166 if s.modestr:match("^[^d]") then
167 return function ()
168 return stat2str(verbose, s)
172 local itr = wmixp:idir (dir)
173 if not itr then
174 --return function ()
175 return nil
176 --end
180 return function ()
181 local s = itr()
182 if s then
183 return stat2str(verbose, s)
185 return nil
189 local function stat2str(verbose, stat)
190 if verbose then
191 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
192 else
193 if stat.modestr:match("^d") then
194 return stat.name .. "/"
195 else
196 return stat.name
201 -- ------------------------------------------------------------------------
202 -- read all contents of a wmii virtual file
203 function read (file)
204 return wmixp:read (file)
207 -- ------------------------------------------------------------------------
208 -- return an iterator which walks all the lines in the file
210 -- example:
211 -- for event in wmii.iread("/ctl")
212 -- ...
213 -- end
215 -- NOTE: don't use iread for files that could block, as this will interfere
216 -- with timer processing and event delivery. Instead fork off a process to
217 -- execute wmiir and read back the responses via callback.
218 function iread (file)
219 return wmixp:iread(file)
222 -- ------------------------------------------------------------------------
223 -- create a wmii file, optionally write data to it
224 function create (file, data)
225 wmixp:create(file, data)
228 -- ------------------------------------------------------------------------
229 -- remove a wmii file
230 function remove (file)
231 wmixp:remove(file)
234 -- ------------------------------------------------------------------------
235 -- write a value to a wmii virtual file system
236 function write (file, value)
237 wmixp:write (file, value)
240 -- ------------------------------------------------------------------------
241 -- setup a table describing dmenu command
242 local function dmenu_cmd (prompt)
243 local cmdt = { "dmenu", "-b" }
244 local fn = get_ctl("font")
245 if fn then
246 cmdt[#cmdt+1] = "-fn"
247 cmdt[#cmdt+1] = fn
249 local normcolors = get_ctl("normcolors")
250 if normcolors then
251 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
252 if nf then
253 cmdt[#cmdt+1] = "-nf"
254 cmdt[#cmdt+1] = "'" .. nf .. "'"
256 if nb then
257 cmdt[#cmdt+1] = "-nb"
258 cmdt[#cmdt+1] = "'" .. nb .. "'"
261 local focuscolors = get_ctl("focuscolors")
262 if focuscolors then
263 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
264 if sf then
265 cmdt[#cmdt+1] = "-sf"
266 cmdt[#cmdt+1] = "'" .. sf .. "'"
268 if sb then
269 cmdt[#cmdt+1] = "-sb"
270 cmdt[#cmdt+1] = "'" .. sb .. "'"
273 if prompt then
274 cmdt[#cmdt+1] = "-p"
275 cmdt[#cmdt+1] = "'" .. prompt .. "'"
278 return cmdt
281 -- ------------------------------------------------------------------------
282 -- displays the menu given an table of entires, returns selected text
283 function menu (tbl, prompt)
284 local dmenu = dmenu_cmd(prompt)
286 local infile = os.tmpname()
287 local fh = io.open (infile, "w+")
289 local i,v
290 for i,v in pairs(tbl) do
291 if type(i) == 'number' and type(v) == 'string' then
292 fh:write (v)
293 else
294 fh:write (i)
296 fh:write ("\n")
298 fh:close()
300 local outfile = os.tmpname()
302 dmenu[#dmenu+1] = "<"
303 dmenu[#dmenu+1] = infile
304 dmenu[#dmenu+1] = ">"
305 dmenu[#dmenu+1] = outfile
307 local cmd = table.concat(dmenu," ")
308 os.execute (cmd)
310 fh = io.open (outfile, "r")
311 os.remove (outfile)
313 local sel = fh:read("*l")
314 fh:close()
316 return sel
319 -- ------------------------------------------------------------------------
320 -- displays the a tag selection menu, returns selected tag
321 function tag_menu ()
322 local tags = get_tags()
324 return menu(tags, "tag:")
327 -- ------------------------------------------------------------------------
328 -- displays the a program menu, returns selected program
329 function prog_menu ()
330 local dmenu = dmenu_cmd("cmd:")
332 local outfile = os.tmpname()
334 dmenu[#dmenu+1] = ">"
335 dmenu[#dmenu+1] = outfile
337 local cmd = "dmenu_path |" .. table.concat(dmenu," ")
338 os.execute (cmd)
340 local fh = io.open (outfile, "rb")
341 os.remove (outfile)
343 local prog = fh:read("*l")
344 io.close (fh)
346 return prog
349 -- ------------------------------------------------------------------------
350 -- displays the a program menu, returns selected program
351 function get_tags()
352 local t = {}
353 local s
354 for s in wmixp:idir ("/tag") do
355 if s.name and not (s.name == "sel") then
356 t[#t + 1] = s.name
359 table.sort(t)
360 return t
363 -- ------------------------------------------------------------------------
364 -- displays the a program menu, returns selected program
365 function get_view()
366 local v = wmixp:read("/ctl") or ""
367 return v:match("view%s+(%S+)")
370 -- ------------------------------------------------------------------------
371 -- changes the current view to the name given
372 function set_view(sel)
373 local cur = get_view()
374 local all = get_tags()
376 if #all < 2 or sel == cur then
377 -- nothing to do if we have less then 2 tags
378 return
381 if not (type(sel) == "string") then
382 error ("string argument expected")
385 -- set new view
386 write ("/ctl", "view " .. sel)
389 -- ------------------------------------------------------------------------
390 -- changes the current view to the index given
391 function set_view_index(sel)
392 local cur = get_view()
393 local all = get_tags()
395 if #all < 2 then
396 -- nothing to do if we have less then 2 tags
397 return
400 local num = tonumber (sel)
401 if not num then
402 error ("number argument expected")
405 local name = all[sel]
406 if not name or name == cur then
407 return
410 -- set new view
411 write ("/ctl", "view " .. name)
414 -- ------------------------------------------------------------------------
415 -- chnages to current view by offset given
416 function set_view_ofs(jump)
417 local cur = get_view()
418 local all = get_tags()
420 if #all < 2 then
421 -- nothing to do if we have less then 2 tags
422 return
425 -- range check
426 if (jump < - #all) or (jump > #all) then
427 error ("view selector is out of range")
430 -- find the one that's selected index
431 local curi = nil
432 local i,v
433 for i,v in pairs (all) do
434 if v == cur then curi = i end
437 -- adjust by index
438 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
439 if (newi < - #all) or (newi > #all) then
440 error ("error computng new view")
443 write ("/ctl", "view " .. all[newi])
446 -- ------------------------------------------------------------------------
447 -- toggle between last view and current view
448 function toggle_view()
449 local last = view_hist[#view_hist]
450 if last then
451 set_view(last)
455 -- ========================================================================
456 -- ACTION HANDLERS
457 -- ========================================================================
459 local action_handlers = {
460 man = function (act, args)
461 local xterm = get_conf("xterm") or "xterm"
462 local page = args
463 if (not page) or (not page:match("%S")) then
464 page = wmiidir .. "/wmii.3lua"
466 local cmd = xterm .. " -e man " .. page .. " &"
467 log (" executing: " .. cmd)
468 os.execute (cmd)
469 end,
471 quit = function ()
472 write ("/ctl", "quit")
473 end,
475 exec = function (act, args)
476 local what = args or wmiirc
477 cleanup()
478 write ("/ctl", "exec " .. what)
479 end,
481 xlock = function (act)
482 local cmd = get_conf("xlock") or "xscreensaver-command --lock"
483 os.execute (cmd)
484 end,
486 wmiirc = function ()
487 cleanup()
488 posix.exec ("lua", wmiirc)
489 end,
491 rehash = function ()
492 -- TODO: consider storing list of executables around, and
493 -- this will then reinitialize that list
494 log (" TODO: rehash")
495 end,
497 status = function ()
498 -- TODO: this should eventually update something on the /rbar
499 log (" TODO: status")
503 --[[
504 =pod
506 =item add_action_handler (action, fn)
508 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
510 =cut
511 --]]
512 function add_action_handler (action, fn)
514 if type(action) ~= "string" or type(fn) ~= "function" then
515 error ("expecting a string and a function")
518 if action_handlers[action] then
519 error ("action handler already exists for '" .. action .. "'")
522 action_handlers[action] = fn
525 --[[
526 =pod
528 =item remove_action_handler (action)
530 Remove an action handler callback function for the given action string I<action>.
532 =cut
533 --]]
534 function remove_action_handler (action)
536 action_handlers[action] = nil
539 -- ========================================================================
540 -- KEY HANDLERS
541 -- ========================================================================
543 local key_handlers = {
544 ["*"] = function (key)
545 log ("*: " .. key)
546 end,
548 -- execution and actions
549 ["Mod1-Return"] = function (key)
550 local xterm = get_conf("xterm") or "xterm"
551 log (" executing: " .. xterm)
552 os.execute (xterm .. " &")
553 end,
554 ["Mod1-Shift-Return"] = function (key)
555 local tag = tag_menu()
556 if tag then
557 local xterm = get_conf("xterm") or "xterm"
558 log (" executing: " .. xterm .. " on: " .. tag)
559 next_client_goes_to_tag = tag
560 os.execute (xterm .. " &")
562 end,
563 ["Mod1-a"] = function (key)
564 local text = menu(action_handlers, "action:")
565 if text then
566 log ("Action: " .. text)
567 local act = text
568 local args = nil
569 local si = text:find("%s")
570 if si then
571 act,args = string.match(text .. " ", "(%w+)%s(.+)")
573 if act then
574 local fn = action_handlers[act]
575 if fn then
576 local r, err = pcall (fn, act, args)
577 if not r then
578 log ("WARNING: " .. tostring(err))
583 end,
584 ["Mod1-p"] = function (key)
585 local prog = prog_menu()
586 if prog then
587 log (" executing: " .. prog)
588 os.execute (prog .. " &")
590 end,
591 ["Mod1-Shift-p"] = function (key)
592 local tag = tag_menu()
593 if tag then
594 local prog = prog_menu()
595 if prog then
596 log (" executing: " .. prog .. " on: " .. tag)
597 next_client_goes_to_tag = tag
598 os.execute (prog .. " &")
601 end,
602 ["Mod1-Shift-c"] = function (key)
603 write ("/client/sel/ctl", "kill")
604 end,
606 -- HJKL active selection
607 ["Mod1-h"] = function (key)
608 write ("/tag/sel/ctl", "select left")
609 end,
610 ["Mod1-l"] = function (key)
611 write ("/tag/sel/ctl", "select right")
612 end,
613 ["Mod1-j"] = function (key)
614 write ("/tag/sel/ctl", "select down")
615 end,
616 ["Mod1-k"] = function (key)
617 write ("/tag/sel/ctl", "select up")
618 end,
620 -- HJKL movement
621 ["Mod1-Shift-h"] = function (key)
622 write ("/tag/sel/ctl", "send sel left")
623 end,
624 ["Mod1-Shift-l"] = function (key)
625 write ("/tag/sel/ctl", "send sel right")
626 end,
627 ["Mod1-Shift-j"] = function (key)
628 write ("/tag/sel/ctl", "send sel down")
629 end,
630 ["Mod1-Shift-k"] = function (key)
631 write ("/tag/sel/ctl", "send sel up")
632 end,
634 -- floating vs tiled
635 ["Mod1-space"] = function (key)
636 write ("/tag/sel/ctl", "select toggle")
637 end,
638 ["Mod1-Shift-space"] = function (key)
639 write ("/tag/sel/ctl", "send sel toggle")
640 end,
642 -- work spaces (# and @ are wildcards for numbers and letters)
643 ["Mod4-#"] = function (key, num)
644 local all = get_tags()
645 -- first attempt to find a view that starts with the number requested
646 local num_str = tostring(num)
647 local i,v
648 for i,v in pairs(all) do
649 if num_str == v:sub(1,1) then
650 set_view_index (i)
651 return
655 -- if we fail, then set it to the index requested
656 set_view_index (num)
657 end,
658 ["Mod4-Shift-#"] = function (key, num)
659 write ("/client/sel/tags", tostring(num))
660 end,
661 ["Mod4-@"] = function (key, letter)
662 local all = get_tags()
663 local i,v
664 for i,v in pairs(all) do
665 if letter == v:sub(1,1) then
666 set_view_index (i)
667 break
670 end,
671 ["Mod4-Shift-@"] = function (key, letter)
672 local all = get_tags()
673 local i,v
674 for i,v in pairs(all) do
675 if letter == v:sub(1,1) then
676 write ("/client/sel/tags", v)
677 break
680 end,
681 ["Mod1-comma"] = function (key)
682 set_view_ofs (-1)
683 end,
684 ["Mod1-period"] = function (key)
685 set_view_ofs (1)
686 end,
687 ["Mod1-r"] = function (key)
688 -- got to the last view
689 toggle_view()
690 end,
692 -- switching views and retagging
693 ["Mod1-t"] = function (key)
694 -- got to a view
695 local tag = tag_menu()
696 if tag then
697 set_view (tag)
699 end,
700 ["Mod1-Shift-t"] = function (key)
701 -- move selected client to a tag
702 local tag = tag_menu()
703 if tag then
704 write ("/client/sel/tags", tag)
706 end,
707 ["Mod1-Shift-r"] = function (key)
708 -- move selected client to a tag, and follow
709 local tag = tag_menu()
710 if tag then
711 write ("/client/sel/tags", tag)
712 set_view(tag)
714 end,
715 ["Mod1-Control-t"] = function (key)
716 log (" TODO: Mod1-Control-t: " .. key)
717 end,
719 -- column modes
720 ["Mod1-d"] = function (key)
721 write("/tag/sel/ctl", "colmode sel default")
722 end,
723 ["Mod1-s"] = function (key)
724 write("/tag/sel/ctl", "colmode sel stack")
725 end,
726 ["Mod1-m"] = function (key)
727 write("/tag/sel/ctl", "colmode sel max")
731 --[[
732 =pod
734 =item add_key_handler (key, fn)
736 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
738 =cut
739 --]]
740 function add_key_handler (key, fn)
742 if type(key) ~= "string" or type(fn) ~= "function" then
743 error ("expecting a string and a function")
746 if key_handlers[key] then
747 -- TODO: we may wish to allow multiple handlers for one keypress
748 error ("key handler already exists for '" .. key .. "'")
751 key_handlers[key] = fn
754 --[[
755 =pod
757 =item remove_key_handler (key)
759 Remove an key handler callback function for the given key I<key>.
761 Returns the handler callback function.
763 =cut
764 --]]
765 function remove_key_handler (key)
767 local fn = key_handlers[key]
768 key_handlers[key] = nil
769 return fn
772 --[[
773 =pod
775 =item remap_key_handler (old_key, new_key)
777 Remove a key handler callback function from the given key I<old_key>,
778 and assign it to a new key I<new_key>.
780 =cut
781 --]]
782 function remap_key_handler (old_key, new_key)
784 local fn = remove_key_handler(old_key)
786 return add_key_handler (new_key, fn)
790 -- ------------------------------------------------------------------------
791 -- update the /keys wmii file with the list of all handlers
792 local alphabet="abcdefghijklmnopqrstuvwxyz"
793 function update_active_keys ()
794 local t = {}
795 local x, y
796 for x,y in pairs(key_handlers) do
797 if x:find("%w") then
798 local i = x:find("#$")
799 if i then
800 local j
801 for j=0,9 do
802 t[#t + 1] = x:sub(1,i-1) .. j
804 else
805 i = x:find("@$")
806 if i then
807 local j
808 for j=1,alphabet:len() do
809 local a = alphabet:sub(j,j)
810 t[#t + 1] = x:sub(1,i-1) .. a
812 else
813 t[#t + 1] = tostring(x)
818 local all_keys = table.concat(t, "\n")
819 --log ("setting /keys to...\n" .. all_keys .. "\n");
820 write ("/keys", all_keys)
823 -- ------------------------------------------------------------------------
824 -- update the /lbar wmii file with the current tags
825 function update_displayed_tags ()
826 -- colours for /lbar
827 local fc = get_ctl("focuscolors") or ""
828 local nc = get_ctl("normcolors") or ""
830 -- build up a table of existing tags in the /lbar
831 local old = {}
832 local s
833 for s in wmixp:idir ("/lbar") do
834 old[s.name] = 1
837 -- for all actual tags in use create any entries in /lbar we don't have
838 -- clear the old table entries if we have them
839 local cur = get_view()
840 local all = get_tags()
841 local i,v
842 for i,v in pairs(all) do
843 local color = nc
844 if cur == v then
845 color = fc
847 if not old[v] then
848 create ("/lbar/" .. v, color .. " " .. v)
850 write ("/lbar/" .. v, color .. " " .. v)
851 old[v] = nil
854 -- anything left in the old table should be removed now
855 for i,v in pairs(old) do
856 if v then
857 remove("/lbar/"..i)
862 -- ========================================================================
863 -- EVENT HANDLERS
864 -- ========================================================================
866 local widget_ev_handlers = {
869 --[[
870 =pod
872 =item _handle_widget_event (ev, arg)
874 Top-level event handler for redispatching events to widgets. This event
875 handler is added for any widget event that currently has a widget registered
876 for it.
878 Valid widget events are currently
880 RightBarMouseDown <buttonnumber> <widgetname>
881 RightBarClick <buttonnumber> <widgetname>
883 the "Click" event is sent on mouseup.
885 The callbacks are given only the button number as their argument, to avoid the
886 need to reparse.
888 =cut
889 --]]
891 local function _handle_widget_event (ev, arg)
893 log("_handle_widget_event: " .. tostring(ev) .. " - " .. tostring(arg))
895 -- parse arg to strip out our widget name
896 local number,wname = string.match(arg, "(%d+)%s+(.+)")
898 -- check our dispatch table for that widget
899 if not wname then
900 log("Didn't find wname")
901 return
904 local wtable = widget_ev_handlers[wname]
905 if not wtable then
906 log("No widget cares about" .. wname)
907 return
910 local fn = wtable[ev] or wtable["*"]
911 if fn then
912 success, err = pcall( fn, ev, tonumber(number) )
913 if not success then
914 log("Callback had an error in _handle_widget_event: " .. tostring(err) )
915 return nil
917 else
918 log("no function found for " .. ev)
922 local ev_handlers = {
923 ["*"] = function (ev, arg)
924 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
925 end,
927 RightBarClick = _handle_widget_event,
929 -- process timer events
930 ProcessTimerEvents = function (ev, arg)
931 process_timers()
932 end,
934 -- exit if another wmiirc started up
935 Start = function (ev, arg)
936 if arg then
937 if arg == "wmiirc" then
938 -- backwards compatibility with bash version
939 cleanup()
940 os.exit (0)
941 else
942 -- ignore if it came from us
943 local pid = string.match(arg, "wmiirc (%d+)")
944 if pid then
945 local pid = tonumber (pid)
946 if not (pid == mypid) then
947 cleanup()
948 os.exit (0)
953 end,
955 -- tag management
956 CreateTag = function (ev, arg)
957 local nc = get_ctl("normcolors") or ""
958 create ("/lbar/" .. arg, nc .. " " .. arg)
959 end,
960 DestroyTag = function (ev, arg)
961 remove ("/lbar/" .. arg)
962 end,
964 FocusTag = function (ev, arg)
965 local fc = get_ctl("focuscolors") or ""
966 create ("/lbar/" .. arg, fc .. " " .. arg)
967 write ("/lbar/" .. arg, fc .. " " .. arg)
968 end,
969 UnfocusTag = function (ev, arg)
970 local nc = get_ctl("normcolors") or ""
971 create ("/lbar/" .. arg, nc .. " " .. arg)
972 write ("/lbar/" .. arg, nc .. " " .. arg)
974 -- don't duplicate the last entry
975 if not (arg == view_hist[#view_hist]) then
976 view_hist[#view_hist+1] = arg
978 -- limit to view_hist_max
979 if #view_hist > view_hist_max then
980 table.remove(view_hist, 1)
983 end,
985 -- key event handling
986 Key = function (ev, arg)
987 log ("Key: " .. arg)
988 local magic = nil
989 -- can we find an exact match?
990 local fn = key_handlers[arg]
991 if not fn then
992 local key = arg:gsub("-%d$", "-#")
993 -- can we find a match with a # wild card for the number
994 fn = key_handlers[key]
995 if fn then
996 -- convert the trailing number to a number
997 magic = tonumber(arg:match("-(%d)$"))
1000 if not fn then
1001 local key = arg:gsub("-%a$", "-@")
1002 -- can we find a match with a @ wild card for a letter
1003 fn = key_handlers[key]
1004 if fn then
1005 -- split off the trailing letter
1006 magic = arg:match("-(%a)$")
1009 if not fn then
1010 -- everything else failed, try default match
1011 fn = key_handlers["*"]
1013 if fn then
1014 local r, err = pcall (fn, arg, magic)
1015 if not r then
1016 log ("WARNING: " .. tostring(err))
1019 end,
1021 -- mouse handling on the lbar
1022 LeftBarClick = function (ev, arg)
1023 local button,tag = string.match(arg, "(%w+)%s+(%S+)")
1024 set_view (tag)
1025 end,
1027 -- focus updates
1028 ClientFocus = function (ev, arg)
1029 log ("ClientFocus: " .. arg)
1030 end,
1031 ColumnFocus = function (ev, arg)
1032 log ("ColumnFocus: " .. arg)
1033 end,
1035 -- client handling
1036 CreateClient = function (ev, arg)
1037 if next_client_goes_to_tag then
1038 local tag = next_client_goes_to_tag
1039 local cli = arg
1040 next_client_goes_to_tag = nil
1041 write ("/client/" .. cli .. "/tags", tag)
1042 set_view(tag)
1044 end,
1046 -- urgent tag?
1047 UrgentTag = function (ev, arg)
1048 log ("UrgentTag: " .. arg)
1049 -- wmiir xwrite "/lbar/$@" "*$@"
1050 end,
1051 NotUrgentTag = function (ev, arg)
1052 log ("NotUrgentTag: " .. arg)
1053 -- wmiir xwrite "/lbar/$@" "$@"
1058 --[[
1059 =pod
1061 =item add_widget_event_handler (wname, ev, fn)
1063 Add an event handler callback for the I<ev> event on the widget named I<wname>
1065 =cut
1066 --]]
1068 function add_widget_event_handler (wname, ev, fn)
1069 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
1070 error ("expecting string for widget name, string for event name and a function callback")
1073 -- Make sure the widget event handler is present
1074 if not ev_handlers[ev] then
1075 ev_handlers[ev] = _handle_widget_event
1078 if not widget_ev_handlers[wname] then
1079 widget_ev_handlers[wname] = { }
1082 if widget_ev_handlers[wname][ev] then
1083 -- TODO: we may wish to allow multiple handlers for one event
1084 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
1087 widget_ev_handlers[wname][ev] = fn
1090 --[[
1091 =pod
1093 =item remove_widget_event_handler (wname, ev)
1095 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1097 =cut
1098 --]]
1099 function remove_event_handler (wname, ev)
1101 if not widget_ev_handlers[wname] then
1102 return
1105 widget_ev_handlers[wname][ev] = nil
1108 --[[
1109 =pod
1111 =item add_event_handler (ev, fn)
1113 Add an event handler callback function, I<fn>, for the given event I<ev>.
1115 =cut
1116 --]]
1117 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1118 function add_event_handler (ev, fn)
1119 if type(ev) ~= "string" or type(fn) ~= "function" then
1120 error ("expecting a string and a function")
1123 if ev_handlers[ev] then
1124 -- TODO: we may wish to allow multiple handlers for one event
1125 error ("event handler already exists for '" .. ev .. "'")
1129 ev_handlers[ev] = fn
1132 --[[
1133 =pod
1135 =item remove_event_handler (ev)
1137 Remove an event handler callback function for the given event I<ev>.
1139 =cut
1140 --]]
1141 function remove_event_handler (ev)
1143 ev_handlers[ev] = nil
1147 -- ========================================================================
1148 -- MAIN INTERFACE FUNCTIONS
1149 -- ========================================================================
1151 local config = {
1152 xterm = 'x-terminal-emulator',
1153 xlock = "xscreensaver-command --lock"
1156 -- ------------------------------------------------------------------------
1157 -- write configuration to /ctl wmii file
1158 -- wmii.set_ctl({ "var" = "val", ...})
1159 -- wmii.set_ctl("var, "val")
1160 function set_ctl (first,second)
1161 if type(first) == "table" and second == nil then
1162 local x, y
1163 for x, y in pairs(first) do
1164 write ("/ctl", x .. " " .. y)
1167 elseif type(first) == "string" and type(second) == "string" then
1168 write ("/ctl", first .. " " .. second)
1170 else
1171 error ("expecting a table or two string arguments")
1175 -- ------------------------------------------------------------------------
1176 -- read a value from /ctl wmii file
1177 -- table = wmii.get_ctl()
1178 -- value = wmii.get_ctl("variable"
1179 function get_ctl (name)
1180 local s
1181 local t = {}
1182 for s in iread("/ctl") do
1183 local var,val = s:match("(%w+)%s+(.+)")
1184 if var == name then
1185 return val
1187 t[var] = val
1189 if not name then
1190 return t
1192 return nil
1195 -- ------------------------------------------------------------------------
1196 -- set an internal wmiirc.lua variable
1197 -- wmii.set_conf({ "var" = "val", ...})
1198 -- wmii.set_conf("var, "val")
1199 function set_conf (first,second)
1200 if type(first) == "table" and second == nil then
1201 local x, y
1202 for x, y in pairs(first) do
1203 config[x] = y
1206 elseif type(first) == "string"
1207 and (type(second) == "string"
1208 or type(second) == "number") then
1209 config[first] = second
1211 else
1212 error ("expecting a table, or string and string/number as arguments")
1216 -- ------------------------------------------------------------------------
1217 -- read an internal wmiirc.lua variable
1218 function get_conf (name)
1219 if name then
1220 return config[name]
1222 return config
1225 -- ========================================================================
1226 -- THE EVENT LOOP
1227 -- ========================================================================
1229 -- the event loop instance
1230 local el = eventloop.new()
1232 -- add the core event handler for events
1233 el:add_exec (wmiir .. " read /event",
1234 function (line)
1235 local line = line or "nil"
1237 -- try to split off the argument(s)
1238 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1239 if not ev then
1240 ev = line
1243 -- now locate the handler function and call it
1244 local fn = ev_handlers[ev] or ev_handlers["*"]
1245 if fn then
1246 local r, err = pcall (fn, ev, arg)
1247 if not r then
1248 log ("WARNING: " .. tostring(err))
1251 end)
1253 -- ------------------------------------------------------------------------
1254 -- run the event loop and process events, this function does not exit
1255 function run_event_loop ()
1256 -- stop any other instance of wmiirc
1257 wmixp:write ("/event", "Start wmiirc " .. tostring(mypid))
1259 log("wmii: updating lbar")
1261 update_displayed_tags ()
1263 log("wmii: updating rbar")
1265 update_displayed_widgets ()
1267 log("wmii: updating active keys")
1269 update_active_keys ()
1271 log("wmii: starting event loop")
1272 while true do
1273 local sleep_for = process_timers()
1274 el:run_loop(sleep_for)
1278 -- ========================================================================
1279 -- PLUGINS API
1280 -- ========================================================================
1282 api_version = 0.1 -- the API version we export
1284 plugins = {} -- all plugins that were loaded
1286 -- ------------------------------------------------------------------------
1287 -- plugin loader which also verifies the version of the api the plugin needs
1289 -- here is what it does
1290 -- - does a manual locate on the file using package.path
1291 -- - reads in the file w/o using the lua interpreter
1292 -- - locates api_version=X.Y string
1293 -- - makes sure that api_version requested can be satisfied
1294 -- - if the plugins is available it will set variables passed in
1295 -- - it then loads the plugin
1297 -- TODO: currently the api_version must be in an X.Y format, but we may want
1298 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1300 function load_plugin(name, vars)
1301 local backup_path = package.path or "./?.lua"
1303 log ("loading " .. name)
1305 -- this is the version we want to find
1306 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1307 if (not api_major) or (not api_minor) then
1308 log ("WARNING: could not parse api_version in core/wmii.lua")
1309 return nil
1312 -- first find the plugin file
1313 local s, path_match, full_name, file
1314 for s in string.gmatch(plugin_path, "[^;]+") do
1315 -- try to locate the files locally
1316 local fn = s:gsub("%?", name)
1317 file = io.open(fn, "r")
1318 if file then
1319 path_match = s
1320 full_name = fn
1321 break
1325 -- read it in
1326 local txt
1327 if file then
1328 txt = file:read("*all")
1329 file:close()
1332 if not txt then
1333 log ("WARNING: could not load plugin '" .. name .. "'")
1334 return nil
1337 -- find the api_version line
1338 local line, plugin_version
1339 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1340 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1341 if plugin_version then
1342 break
1346 if not plugin_version then
1347 log ("WARNING: could not find api_version string in plugin '" .. name .. "'")
1348 return nil
1351 -- decompose the version string
1352 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1353 if (not plugin_major) or (not plugin_minor) then
1354 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1355 return nil
1358 -- make a version test
1359 if plugin_major ~= api_major then
1360 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1361 .. " (api " .. tonumber(api_version) .. ")")
1362 return nil
1365 if plugin_minor > api_minor then
1366 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1367 .. " (api " .. tonumber(api_version) .. ")")
1368 return nil
1371 -- the configuration parameters before loading
1372 if type(vars) == "table" then
1373 local var, val
1374 for var,val in pairs(vars) do
1375 local success = pcall (set_conf, name .. "." .. var, val)
1376 if not success then
1377 log ("WARNING: bad variable {" .. tostring(var) .. ", " .. tostring(val) .. "} "
1378 .. "given; loading '" .. name .. "' plugin failed.")
1379 return nil
1384 -- actually load the module, but use only the path where we though it should be
1385 package.path = path_match
1386 local success,what = pcall (require, name)
1387 package.path = backup_path
1388 if not success then
1389 log ("WARNING: failed to load '" .. name .. "' plugin")
1390 log (" - path: " .. tostring(path_match))
1391 log (" - file: " .. tostring(full_name))
1392 log (" - plugin's api_version: " .. tostring(plugin_version))
1393 log (" - reason: " .. tostring(what))
1394 return nil
1397 -- success
1398 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1399 plugins[name] = what
1400 return what
1403 -- ------------------------------------------------------------------------
1404 -- widget template
1405 widget = {}
1406 widgets = {}
1408 -- ------------------------------------------------------------------------
1409 -- create a widget object and add it to the wmii /rbar
1411 -- examples:
1412 -- widget = wmii.widget:new ("999_clock")
1413 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1414 function widget:new (name, fn)
1415 local o = {}
1417 if type(name) == "string" then
1418 o.name = name
1419 if type(fn) == "function" then
1420 o.fn = fn
1422 else
1423 error ("expected name followed by an optional function as arguments")
1426 setmetatable (o,self)
1427 self.__index = self
1428 self.__gc = function (o) o:hide() end
1430 widgets[name] = o
1432 o:show()
1433 return o
1436 -- ------------------------------------------------------------------------
1437 -- stop and destroy the timer
1438 function widget:delete ()
1439 widgets[self.name] = nil
1440 self:hide()
1443 -- ------------------------------------------------------------------------
1444 -- displays or updates the widget text
1446 -- examples:
1447 -- w:show("foo")
1448 -- w:show("foo", "#888888 #222222 #333333")
1449 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1451 function widget:show (txt, colors)
1452 local colors = colors or get_ctl("normcolors") or ""
1453 local txt = txt or self.txt or ""
1454 local towrite = txt
1455 if colors then
1456 towrite = colors .. " " .. towrite
1458 if not self.txt then
1459 create ("/rbar/" .. self.name, towrite)
1460 else
1461 write ("/rbar/" .. self.name, towrite)
1463 self.txt = txt
1466 -- ------------------------------------------------------------------------
1467 -- hides a widget and removes it from the bar
1468 function widget:hide ()
1469 if self.txt then
1470 remove ("/lbar/" .. self.name)
1471 self.txt = nil
1475 --[[
1476 =pod
1478 =item widget:add_event_handler (ev, fn)
1480 Add an event handler callback for this widget, using I<fn> for event I<ev>
1482 =cut
1483 --]]
1485 function widget:add_event_handler (ev, fn)
1486 add_widget_event_handler( self.name, ev, fn)
1490 -- ------------------------------------------------------------------------
1491 -- remove all /rbar entries that we don't have widget objects for
1492 function update_displayed_widgets ()
1493 -- colours for /rbar
1494 local nc = get_ctl("normcolors") or ""
1496 -- build up a table of existing tags in the /lbar
1497 local old = {}
1498 local s
1499 for s in wmixp:idir ("/rbar") do
1500 old[s.name] = 1
1503 -- for all actual widgets in use we want to remove them from the old list
1504 local i,v
1505 for i,v in pairs(widgets) do
1506 old[v.name] = nil
1509 -- anything left in the old table should be removed now
1510 for i,v in pairs(old) do
1511 if v then
1512 remove("/rbar/"..i)
1517 -- ------------------------------------------------------------------------
1518 -- create a new program and for each line it generates call the callback function
1519 -- returns fd which can be passed to kill_exec()
1520 function add_exec (command, callback)
1521 return el:add_exec (command, callback)
1524 -- ------------------------------------------------------------------------
1525 -- terminates a program spawned off by add_exec()
1526 function kill_exec (fd)
1527 return el:kill_exec (fd)
1530 -- ------------------------------------------------------------------------
1531 -- timer template
1532 timer = {}
1533 local timers = {}
1535 -- ------------------------------------------------------------------------
1536 -- create a timer object and add it to the event loop
1538 -- examples:
1539 -- timer:new (my_timer_fn)
1540 -- timer:new (my_timer_fn, 15)
1541 function timer:new (fn, seconds)
1542 local o = {}
1544 if type(fn) == "function" then
1545 o.fn = fn
1546 else
1547 error ("expected function followed by an optional number as arguments")
1550 setmetatable (o,self)
1551 self.__index = self
1552 self.__gc = function (o) o:stop() end
1554 -- add the timer
1555 timers[#timers+1] = o
1557 if seconds then
1558 o:resched(seconds)
1560 return o
1563 -- ------------------------------------------------------------------------
1564 -- stop and destroy the timer
1565 function timer:delete ()
1566 self:stop()
1567 local i,t
1568 for i,t in pairs(timers) do
1569 if t == self then
1570 table.remove (timers,i)
1571 return
1576 -- ------------------------------------------------------------------------
1577 -- run the timer given new interval
1578 function timer:resched (seconds)
1579 local seconds = seconds or self.interval
1580 if not (type(seconds) == "number") then
1581 error ("timer:resched expected number as argument")
1584 local now = tonumber(os.date("%s"))
1586 self.interval = seconds
1587 self.next_time = now + seconds
1589 -- resort the timer list
1590 table.sort (timers, timer.is_less_then)
1593 -- helper for sorting timers
1594 function timer:is_less_then(another)
1595 if not self.next_time then
1596 return false -- another is smaller, nil means infinity
1598 elseif not another.next_time then
1599 return true -- self is smaller, nil means infinity
1601 elseif self.next_time < another.next_time then
1602 return true -- self is smaller than another
1605 return false -- another is smaller then self
1608 -- ------------------------------------------------------------------------
1609 -- stop the timer
1610 function timer:stop ()
1611 self.next_time = nil
1613 -- resort the timer list
1614 table.sort (timers, timer.is_less_then)
1617 -- ------------------------------------------------------------------------
1618 -- figure out how long before the next event
1619 function time_before_next_timer_event()
1620 local tmr = timers[1]
1621 if tmr and tmr.next_time then
1622 local now = tonumber(os.date("%s"))
1623 local seconds = tmr.next_time - now
1624 if seconds > 0 then
1625 return seconds
1628 return 0 -- sleep for ever
1631 -- ------------------------------------------------------------------------
1632 -- handle outstanding events
1633 function process_timers ()
1634 local now = tonumber(os.date("%s"))
1635 local torun = {}
1636 local i,tmr
1638 for i,tmr in pairs (timers) do
1639 if not tmr then
1640 -- prune out removed timers
1641 table.remove(timers,i)
1642 break
1644 elseif not tmr.next_time then
1645 -- break out once we find a timer that is stopped
1646 break
1648 elseif tmr.next_time > now then
1649 -- break out once we get to the future
1650 break
1653 -- this one is good to go
1654 torun[#torun+1] = tmr
1657 for i,tmr in pairs (torun) do
1658 tmr:stop()
1659 local status,new_interval = pcall (tmr.fn, tmr)
1660 if status then
1661 new_interval = new_interval or self.interval
1662 if new_interval and (new_interval ~= -1) then
1663 tmr:resched(new_interval)
1665 else
1666 log ("ERROR: " .. tostring(new_interval))
1670 local sleep_for = time_before_next_timer_event()
1671 return sleep_for
1674 -- ------------------------------------------------------------------------
1675 -- cleanup everything in preparation for exit() or exec()
1676 function cleanup ()
1678 local i,v,tmr,p
1680 log ("wmii: stopping timer events")
1682 for i,tmr in pairs (timers) do
1683 pcall (tmr.delete, tmr)
1685 timers = {}
1687 log ("wmii: terminating eventloop")
1689 pcall(el.kill_all,el)
1691 log ("wmii: disposing of widgets")
1693 -- dispose of all widgets
1694 for i,v in pairs(widgets) do
1695 pcall(v.delete,v)
1697 timers = {}
1699 -- FIXME: it doesn't seem to do what I want
1700 --[[
1701 log ("wmii: releasing plugins")
1703 for i,p in pairs(plugins) do
1704 if p.cleanup then
1705 pcall (p.cleanup, p)
1708 plugins = {}
1709 --]]
1711 log ("wmii: dormant")
1714 -- ========================================================================
1715 -- DOCUMENTATION
1716 -- ========================================================================
1718 --[[
1719 =pod
1721 =back
1723 =head1 ENVIRONMENT
1725 =over 4
1727 =item WMII_ADDRESS
1729 Used to determine location of wmii's listen socket.
1731 =back
1733 =head1 SEE ALSO
1735 L<wmii(1)>, L<lua(1)>
1737 =head1 AUTHOR
1739 Bart Trojanowski B<< <bart@jukie.net> >>
1741 =head1 COPYRIGHT AND LICENSE
1743 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
1745 This is free software. You may redistribute copies of it under the terms of
1746 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
1747 is NO WARRANTY, to the extent permitted by law.
1749 =cut
1750 --]]