added api_version to load_plugin
[wmiirc-lua.git] / core / wmii.lua
blob1198855d085a829536c702aa35b9437df1822730
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 wmiirc = os.getenv("HOME") .. "/.wmii-3.5/wmiirc"
60 package.path = package.path
61 .. ";" .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.lua"
62 package.cpath = package.cpath
63 .. ";" .. os.getenv("HOME") .. "/.wmii-3.5/core/?.so"
64 .. ";" .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.so"
66 local ixp = require "ixp"
67 local eventloop = require "eventloop"
69 local base = _G
70 local io = require("io")
71 local os = require("os")
72 local posix = require("posix")
73 local string = require("string")
74 local table = require("table")
75 local math = require("math")
76 local type = type
77 local error = error
78 local print = print
79 local pcall = pcall
80 local pairs = pairs
81 local package = package
82 local require = require
83 local tostring = tostring
84 local tonumber = tonumber
85 local setmetatable = setmetatable
87 module("wmii")
89 -- get the process id
90 local mypid = posix.getprocessid("pid")
92 -- ========================================================================
93 -- MODULE VARIABLES
94 -- ========================================================================
96 -- wmiir points to the wmiir executable
97 -- TODO: need to make sure that wmiir is in path, and if not find it
98 local wmiir = "wmiir"
100 -- wmii_adr is the address we use when connecting using ixp
101 local wmii_adr = os.getenv("WMII_ADDRESS")
102 or ("unix!/tmp/ns." .. os.getenv("USER") .. "."
103 .. os.getenv("DISPLAY"):match("(:%d+)") .. "/wmii")
105 -- wmixp is the ixp context we use to talk to wmii
106 local wmixp = ixp.new(wmii_adr)
108 -- history of previous views, view_hist[#view_hist] is the last one
109 local view_hist = {} -- sorted with 1 being the oldest
110 local view_hist_max = 10 -- max number to keep track of
112 -- allow for a client to be forced to a tag
113 local next_client_goes_to_tag = nil
115 -- ========================================================================
116 -- LOCAL HELPERS
117 -- ========================================================================
119 --[[
120 =pod
122 =item log ( str )
124 Log the message provided in C<str>
126 Currently just writes to io.stderr
128 =cut
129 --]]
130 function log (str)
131 io.stderr:write (str .. "\n")
134 -- ========================================================================
135 -- MAIN ACCESS FUNCTIONS
136 -- ========================================================================
138 --[[
139 =pod
141 =item ls ( dir, fmt )
143 List the wmii filesystem directory provided in C<dir>, in the format specified
144 by C<fmt>.
146 Returns an iterator of TODO
148 =cut
149 --]]
150 function ls (dir, fmt)
151 local verbose = fmt and fmt:match("l")
153 local s = wmixp:stat(dir)
154 if not s then
155 return function () return nil end
157 if s.modestr:match("^[^d]") then
158 return function ()
159 return stat2str(verbose, s)
163 local itr = wmixp:idir (dir)
164 if not itr then
165 --return function ()
166 return nil
167 --end
171 return function ()
172 local s = itr()
173 if s then
174 return stat2str(verbose, s)
176 return nil
180 local function stat2str(verbose, stat)
181 if verbose then
182 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
183 else
184 if stat.modestr:match("^d") then
185 return stat.name .. "/"
186 else
187 return stat.name
192 -- ------------------------------------------------------------------------
193 -- read all contents of a wmii virtual file
194 function read (file)
195 return wmixp:read (file)
198 -- ------------------------------------------------------------------------
199 -- return an iterator which walks all the lines in the file
201 -- example:
202 -- for event in wmii.iread("/ctl")
203 -- ...
204 -- end
206 -- NOTE: don't use iread for files that could block, as this will interfere
207 -- with timer processing and event delivery. Instead fork off a process to
208 -- execute wmiir and read back the responses via callback.
209 function iread (file)
210 return wmixp:iread(file)
213 -- ------------------------------------------------------------------------
214 -- create a wmii file, optionally write data to it
215 function create (file, data)
216 wmixp:create(file, data)
219 -- ------------------------------------------------------------------------
220 -- remove a wmii file
221 function remove (file)
222 wmixp:remove(file)
225 -- ------------------------------------------------------------------------
226 -- write a value to a wmii virtual file system
227 function write (file, value)
228 wmixp:write (file, value)
231 -- ------------------------------------------------------------------------
232 -- setup a table describing dmenu command
233 local function dmenu_cmd (prompt)
234 local cmdt = { "dmenu", "-b" }
235 local fn = get_ctl("font")
236 if fn then
237 cmdt[#cmdt+1] = "-fn"
238 cmdt[#cmdt+1] = fn
240 local normcolors = get_ctl("normcolors")
241 if normcolors then
242 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
243 if nf then
244 cmdt[#cmdt+1] = "-nf"
245 cmdt[#cmdt+1] = "'" .. nf .. "'"
247 if nb then
248 cmdt[#cmdt+1] = "-nb"
249 cmdt[#cmdt+1] = "'" .. nb .. "'"
252 local focuscolors = get_ctl("focuscolors")
253 if focuscolors then
254 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
255 if sf then
256 cmdt[#cmdt+1] = "-sf"
257 cmdt[#cmdt+1] = "'" .. sf .. "'"
259 if sb then
260 cmdt[#cmdt+1] = "-sb"
261 cmdt[#cmdt+1] = "'" .. sb .. "'"
264 if prompt then
265 cmdt[#cmdt+1] = "-p"
266 cmdt[#cmdt+1] = "'" .. prompt .. "'"
269 return cmdt
272 -- ------------------------------------------------------------------------
273 -- displays the menu given an table of entires, returns selected text
274 function menu (tbl, prompt)
275 local dmenu = dmenu_cmd(prompt)
277 local infile = os.tmpname()
278 local fh = io.open (infile, "w+")
280 local i,v
281 for i,v in pairs(tbl) do
282 if type(i) == 'number' and type(v) == 'string' then
283 fh:write (v)
284 else
285 fh:write (i)
287 fh:write ("\n")
289 fh:close()
291 local outfile = os.tmpname()
293 dmenu[#dmenu+1] = "<"
294 dmenu[#dmenu+1] = infile
295 dmenu[#dmenu+1] = ">"
296 dmenu[#dmenu+1] = outfile
298 local cmd = table.concat(dmenu," ")
299 os.execute (cmd)
301 fh = io.open (outfile, "r")
302 os.remove (outfile)
304 local sel = fh:read("*l")
305 fh:close()
307 return sel
310 -- ------------------------------------------------------------------------
311 -- displays the a tag selection menu, returns selected tag
312 function tag_menu ()
313 local tags = get_tags()
315 return menu(tags, "tag:")
318 -- ------------------------------------------------------------------------
319 -- displays the a program menu, returns selected program
320 function prog_menu ()
321 local dmenu = dmenu_cmd("cmd:")
323 local outfile = os.tmpname()
325 dmenu[#dmenu+1] = ">"
326 dmenu[#dmenu+1] = outfile
328 local cmd = "dmenu_path |" .. table.concat(dmenu," ")
329 os.execute (cmd)
331 local fh = io.open (outfile, "rb")
332 os.remove (outfile)
334 local prog = fh:read("*l")
335 io.close (fh)
337 return prog
340 -- ------------------------------------------------------------------------
341 -- displays the a program menu, returns selected program
342 function get_tags()
343 local t = {}
344 local s
345 for s in wmixp:idir ("/tag") do
346 if s.name and not (s.name == "sel") then
347 t[#t + 1] = s.name
350 table.sort(t)
351 return t
354 -- ------------------------------------------------------------------------
355 -- displays the a program menu, returns selected program
356 function get_view()
357 local v = wmixp:read("/ctl") or ""
358 return v:match("view%s+(%S+)")
361 -- ------------------------------------------------------------------------
362 -- changes the current view to the name given
363 function set_view(sel)
364 local cur = get_view()
365 local all = get_tags()
367 if #all < 2 or sel == cur then
368 -- nothing to do if we have less then 2 tags
369 return
372 if not (type(sel) == "string") then
373 error ("string argument expected")
376 -- set new view
377 write ("/ctl", "view " .. sel)
380 -- ------------------------------------------------------------------------
381 -- changes the current view to the index given
382 function set_view_index(sel)
383 local cur = get_view()
384 local all = get_tags()
386 if #all < 2 then
387 -- nothing to do if we have less then 2 tags
388 return
391 local num = tonumber (sel)
392 if not num then
393 error ("number argument expected")
396 local name = all[sel]
397 if not name or name == cur then
398 return
401 -- set new view
402 write ("/ctl", "view " .. name)
405 -- ------------------------------------------------------------------------
406 -- chnages to current view by offset given
407 function set_view_ofs(jump)
408 local cur = get_view()
409 local all = get_tags()
411 if #all < 2 then
412 -- nothing to do if we have less then 2 tags
413 return
416 -- range check
417 if (jump < - #all) or (jump > #all) then
418 error ("view selector is out of range")
421 -- find the one that's selected index
422 local curi = nil
423 local i,v
424 for i,v in pairs (all) do
425 if v == cur then curi = i end
428 -- adjust by index
429 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
430 if (newi < - #all) or (newi > #all) then
431 error ("error computng new view")
434 write ("/ctl", "view " .. all[newi])
437 -- ------------------------------------------------------------------------
438 -- toggle between last view and current view
439 function toggle_view()
440 local last = view_hist[#view_hist]
441 if last then
442 set_view(last)
446 -- ========================================================================
447 -- ACTION HANDLERS
448 -- ========================================================================
450 local action_handlers = {
451 quit = function ()
452 write ("/ctl", "quit")
453 end,
455 exec = function (act, args)
456 local what = args or wmiirc
457 cleanup()
458 write ("/ctl", "exec " .. what)
459 end,
461 wmiirc = function ()
462 cleanup()
463 posix.exec ("lua", wmiirc)
464 end,
466 rehash = function ()
467 -- TODO: consider storing list of executables around, and
468 -- this will then reinitialize that list
469 log (" TODO: rehash")
470 end,
472 status = function ()
473 -- TODO: this should eventually update something on the /rbar
474 log (" TODO: status")
478 --[[
479 =pod
481 =item add_action_handler (action, fn)
483 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
485 =cut
486 --]]
487 function add_action_handler (action, fn)
489 if type(action) ~= "string" or type(fn) ~= "function" then
490 error ("expecting a string and a function")
493 if action_handlers[action] then
494 error ("action handler already exists for '" .. action .. "'")
497 action_handlers[action] = fn
500 --[[
501 =pod
503 =item remove_action_handler (action)
505 Remove an action handler callback function for the given action string I<action>.
507 =cut
508 --]]
509 function remove_action_handler (action)
511 action_handlers[action] = nil
514 -- ========================================================================
515 -- KEY HANDLERS
516 -- ========================================================================
518 local key_handlers = {
519 ["*"] = function (key)
520 log ("*: " .. key)
521 end,
523 -- execution and actions
524 ["Mod1-Return"] = function (key)
525 local xterm = get_conf("xterm") or "xterm"
526 log (" executing: " .. xterm)
527 os.execute (xterm .. " &")
528 end,
529 ["Mod1-Shift-Return"] = function (key)
530 local tag = tag_menu()
531 if tag then
532 local xterm = get_conf("xterm") or "xterm"
533 log (" executing: " .. xterm .. " on: " .. tag)
534 next_client_goes_to_tag = tag
535 os.execute (xterm .. " &")
537 end,
538 ["Mod1-a"] = function (key)
539 local text = menu(action_handlers, "action:")
540 if text then
541 local act = text
542 local args = nil
543 local si = text:find("%s")
544 if si then
545 act,args = string.match(text .. " ", "(%w+)%s(.+)")
547 if act then
548 local fn = action_handlers[act]
549 if fn then
550 pcall (fn, act,args)
554 end,
555 ["Mod1-p"] = function (key)
556 local prog = prog_menu()
557 if prog then
558 log (" executing: " .. prog)
559 os.execute (prog .. " &")
561 end,
562 ["Mod1-Shift-p"] = function (key)
563 local tag = tag_menu()
564 if tag then
565 local prog = prog_menu()
566 if prog then
567 log (" executing: " .. prog .. " on: " .. tag)
568 next_client_goes_to_tag = tag
569 os.execute (prog .. " &")
572 end,
573 ["Mod1-Shift-c"] = function (key)
574 write ("/client/sel/ctl", "kill")
575 end,
577 -- HJKL active selection
578 ["Mod1-h"] = function (key)
579 write ("/tag/sel/ctl", "select left")
580 end,
581 ["Mod1-l"] = function (key)
582 write ("/tag/sel/ctl", "select right")
583 end,
584 ["Mod1-j"] = function (key)
585 write ("/tag/sel/ctl", "select down")
586 end,
587 ["Mod1-k"] = function (key)
588 write ("/tag/sel/ctl", "select up")
589 end,
591 -- HJKL movement
592 ["Mod1-Shift-h"] = function (key)
593 write ("/tag/sel/ctl", "send sel left")
594 end,
595 ["Mod1-Shift-l"] = function (key)
596 write ("/tag/sel/ctl", "send sel right")
597 end,
598 ["Mod1-Shift-j"] = function (key)
599 write ("/tag/sel/ctl", "send sel down")
600 end,
601 ["Mod1-Shift-k"] = function (key)
602 write ("/tag/sel/ctl", "send sel up")
603 end,
605 -- floating vs tiled
606 ["Mod1-space"] = function (key)
607 write ("/tag/sel/ctl", "select toggle")
608 end,
609 ["Mod1-Shift-space"] = function (key)
610 write ("/tag/sel/ctl", "send sel toggle")
611 end,
613 -- work spaces
614 ["Mod4-#"] = function (key, num)
615 set_view_index (num)
616 end,
617 ["Mod4-Shift-#"] = function (key, num)
618 write ("/client/sel/tags", tostring(num))
619 end,
620 ["Mod1-comma"] = function (key)
621 set_view_ofs (-1)
622 end,
623 ["Mod1-period"] = function (key)
624 set_view_ofs (1)
625 end,
626 ["Mod1-r"] = function (key)
627 -- got to the last view
628 toggle_view()
629 end,
631 -- switching views and retagging
632 ["Mod1-t"] = function (key)
633 -- got to a view
634 local tag = tag_menu()
635 if tag then
636 set_view (tag)
638 end,
639 ["Mod1-Shift-t"] = function (key)
640 -- move selected client to a tag
641 local tag = tag_menu()
642 if tag then
643 write ("/client/sel/tags", tag)
645 end,
646 ["Mod1-Shift-r"] = function (key)
647 -- move selected client to a tag, and follow
648 local tag = tag_menu()
649 if tag then
650 write ("/client/sel/tags", tag)
651 set_view(tag)
653 end,
654 ["Mod1-Control-t"] = function (key)
655 log (" TODO: Mod1-Control-t: " .. key)
656 end,
658 -- column modes
659 ["Mod1-d"] = function (key)
660 write("/tag/sel/ctl", "colmode sel default")
661 end,
662 ["Mod1-s"] = function (key)
663 write("/tag/sel/ctl", "colmode sel stack")
664 end,
665 ["Mod1-m"] = function (key)
666 write("/tag/sel/ctl", "colmode sel max")
670 --[[
671 =pod
673 =item add_key_handler (key, fn)
675 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
677 =cut
678 --]]
679 function add_key_handler (key, fn)
681 if type(key) ~= "string" or type(fn) ~= "function" then
682 error ("expecting a string and a function")
685 if key_handlers[key] then
686 -- TODO: we may wish to allow multiple handlers for one keypress
687 error ("key handler already exists for '" .. key .. "'")
690 key_handlers[key] = fn
693 --[[
694 =pod
696 =item remove_key_handler (key)
698 Remove an key handler callback function for the given key I<key>.
700 =cut
701 --]]
702 function remove_key_handler (key)
704 key_handlers[key] = nil
707 -- ------------------------------------------------------------------------
708 -- update the /keys wmii file with the list of all handlers
709 function update_active_keys ()
710 local t = {}
711 local x, y
712 for x,y in pairs(key_handlers) do
713 if x:find("%w") then
714 local i = x:find("#")
715 if i then
716 local j
717 for j=0,9 do
718 t[#t + 1]
719 = x:sub(1,i-1) .. j
721 else
722 t[#t + 1]
723 = tostring(x)
727 local all_keys = table.concat(t, "\n")
728 --log ("setting /keys to...\n" .. all_keys .. "\n");
729 write ("/keys", all_keys)
732 -- ------------------------------------------------------------------------
733 -- update the /lbar wmii file with the current tags
734 function update_displayed_tags ()
735 -- colours for /lbar
736 local fc = get_ctl("focuscolors") or ""
737 local nc = get_ctl("normcolors") or ""
739 -- build up a table of existing tags in the /lbar
740 local old = {}
741 local s
742 for s in wmixp:idir ("/lbar") do
743 old[s.name] = 1
746 -- for all actual tags in use create any entries in /lbar we don't have
747 -- clear the old table entries if we have them
748 local cur = get_view()
749 local all = get_tags()
750 local i,v
751 for i,v in pairs(all) do
752 local color = nc
753 if cur == v then
754 color = fc
756 if not old[v] then
757 create ("/lbar/" .. v, color .. " " .. v)
759 write ("/lbar/" .. v, color .. " " .. v)
760 old[v] = nil
763 -- anything left in the old table should be removed now
764 for i,v in pairs(old) do
765 if v then
766 remove("/lbar/"..i)
771 -- ========================================================================
772 -- EVENT HANDLERS
773 -- ========================================================================
775 local ev_handlers = {
776 ["*"] = function (ev, arg)
777 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
778 end,
780 -- process timer events
781 ProcessTimerEvents = function (ev, arg)
782 process_timers()
783 end,
785 -- exit if another wmiirc started up
786 Start = function (ev, arg)
787 if arg then
788 if arg == "wmiirc" then
789 -- backwards compatibility with bash version
790 cleanup()
791 os.exit (0)
792 else
793 -- ignore if it came from us
794 local pid = string.match(arg, "wmiirc (%d+)")
795 if pid then
796 local pid = tonumber (pid)
797 if not (pid == mypid) then
798 cleanup()
799 os.exit (0)
804 end,
806 -- tag management
807 CreateTag = function (ev, arg)
808 local nc = get_ctl("normcolors") or ""
809 create ("/lbar/" .. arg, nc .. " " .. arg)
810 end,
811 DestroyTag = function (ev, arg)
812 remove ("/lbar/" .. arg)
813 end,
815 FocusTag = function (ev, arg)
816 local fc = get_ctl("focuscolors") or ""
817 create ("/lbar/" .. arg, fc .. " " .. arg)
818 write ("/lbar/" .. arg, fc .. " " .. arg)
819 end,
820 UnfocusTag = function (ev, arg)
821 local nc = get_ctl("normcolors") or ""
822 create ("/lbar/" .. arg, nc .. " " .. arg)
823 write ("/lbar/" .. arg, nc .. " " .. arg)
825 -- don't duplicate the last entry
826 if not (arg == view_hist[#view_hist]) then
827 view_hist[#view_hist+1] = arg
829 -- limit to view_hist_max
830 if #view_hist > view_hist_max then
831 table.remove(view_hist, 1)
834 end,
836 -- key event handling
837 Key = function (ev, arg)
838 log ("Key: " .. arg)
839 local num = nil
840 -- can we find an exact match?
841 local fn = key_handlers[arg]
842 if not fn then
843 local key = arg:gsub("-%d+", "-#")
844 -- can we find a match with a # wild card for the number
845 fn = key_handlers[key]
846 if fn then
847 -- convert the trailing number to a number
848 num = tonumber(arg:match("-(%d+)"))
849 else
850 -- everything else failed, try default match
851 fn = key_handlers["*"]
854 if fn then
855 pcall (fn, arg, num)
857 end,
859 -- mouse handling on the lbar
860 LeftBarClick = function (ev, arg)
861 local button,tag = string.match(arg, "(%w+)%s+(%w+)")
862 set_view (tag)
863 end,
865 -- focus updates
866 ClientFocus = function (ev, arg)
867 log ("ClientFocus: " .. arg)
868 end,
869 ColumnFocus = function (ev, arg)
870 log ("ColumnFocus: " .. arg)
871 end,
873 -- client handling
874 CreateClient = function (ev, arg)
875 if next_client_goes_to_tag then
876 local tag = next_client_goes_to_tag
877 local cli = arg
878 next_client_goes_to_tag = nil
879 write ("/client/" .. cli .. "/tags", tag)
880 set_view(tag)
882 end,
884 -- urgent tag?
885 UrgentTag = function (ev, arg)
886 log ("UrgentTag: " .. arg)
887 -- wmiir xwrite "/lbar/$@" "*$@"
888 end,
889 NotUrgentTag = function (ev, arg)
890 log ("NotUrgentTag: " .. arg)
891 -- wmiir xwrite "/lbar/$@" "$@"
896 local widget_ev_handlers = {
899 --[[
900 =pod
902 =item _handle_widget_event (ev, arg)
904 Top-level event handler for redispatching events to widgets. This event
905 handler is added for any widget event that currently has a widget registered
906 for it.
908 Valid widget events are currently
910 RightBarMouseDown <buttonnumber> <widgetname>
911 RightBarClick <buttonnumber> <widgetname>
913 the "Click" event is sent on mouseup.
915 The callbacks are given only the button number as their argument, to avoid the
916 need to reparse.
918 =cut
919 --]]
921 function _handle_widget_event (ev, arg)
922 -- parse arg to strip out our widget name
923 local number,wname = string.match(arg, "(%d+)%s+(.+)")
925 -- check our dispatch table for that widget
926 if not wname then
927 return
930 local wtable = widget_ev_handlers[wname]
931 if not wtable then
932 return
935 local fn = wtable[ev] or wtable["*"]
936 if fn then
937 fn (ev, tonumber(number))
941 --[[
942 =pod
944 =item add_widget_event_handler (wname, ev, fn)
946 Add an event handler callback for the I<ev> event on the widget named I<wname>
948 =cut
949 --]]
951 function add_widget_event_handler (wname, ev, fn)
952 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
953 error ("expecting string for widget name, string for event name and a function callback")
956 -- Make sure the widget event handler is present
957 if not ev_handlers[ev] then
958 ev_handlers[ev] = _handle_widget_event
961 if not widget_ev_handlers[wname] then
962 widget_ev_handlers[wname] = { }
965 if widget_ev_handlers[wname][ev] then
966 -- TODO: we may wish to allow multiple handlers for one event
967 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
970 widget_ev_handlers[wname][ev] = fn
973 --[[
974 =pod
976 =item remove_widget_event_handler (wname, ev)
978 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
980 =cut
981 --]]
982 function remove_event_handler (wname, ev)
984 if not widget_ev_handlers[wname] then
985 return
988 widget_ev_handlers[wname][ev] = nil
991 --[[
992 =pod
994 =item add_event_handler (ev, fn)
996 Add an event handler callback function, I<fn>, for the given event I<ev>.
998 =cut
999 --]]
1000 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1001 function add_event_handler (ev, fn)
1002 if type(ev) ~= "string" or type(fn) ~= "function" then
1003 error ("expecting a string and a function")
1006 if ev_handlers[ev] then
1007 -- TODO: we may wish to allow multiple handlers for one event
1008 error ("event handler already exists for '" .. ev .. "'")
1012 ev_handlers[ev] = fn
1015 --[[
1016 =pod
1018 =item remove_event_handler (ev)
1020 Remove an event handler callback function for the given event I<ev>.
1022 =cut
1023 --]]
1024 function remove_event_handler (ev)
1026 ev_handlers[ev] = nil
1030 -- ========================================================================
1031 -- MAIN INTERFACE FUNCTIONS
1032 -- ========================================================================
1034 local config = {
1035 xterm = 'x-terminal-emulator'
1038 -- ------------------------------------------------------------------------
1039 -- write configuration to /ctl wmii file
1040 -- wmii.set_ctl({ "var" = "val", ...})
1041 -- wmii.set_ctl("var, "val")
1042 function set_ctl (first,second)
1043 if type(first) == "table" and second == nil then
1044 local x, y
1045 for x, y in pairs(first) do
1046 write ("/ctl", x .. " " .. y)
1049 elseif type(first) == "string" and type(second) == "string" then
1050 write ("/ctl", first .. " " .. second)
1052 else
1053 error ("expecting a table or two string arguments")
1057 -- ------------------------------------------------------------------------
1058 -- read a value from /ctl wmii file
1059 function get_ctl (name)
1060 local s
1061 for s in iread("/ctl") do
1062 local var,val = s:match("(%w+)%s+(.+)")
1063 if var == name then
1064 return val
1067 return nil
1070 -- ------------------------------------------------------------------------
1071 -- set an internal wmiirc.lua variable
1072 -- wmii.set_conf({ "var" = "val", ...})
1073 -- wmii.set_conf("var, "val")
1074 function set_conf (first,second)
1075 if type(first) == "table" and second == nil then
1076 local x, y
1077 for x, y in pairs(first) do
1078 config[x] = y
1081 elseif type(first) == "string"
1082 and (type(second) == "string"
1083 or type(second) == "number") then
1084 config[first] = second
1086 else
1087 error ("expecting a table, or string and string/number as arguments")
1091 -- ------------------------------------------------------------------------
1092 -- read an internal wmiirc.lua variable
1093 function get_conf (name)
1094 return config[name]
1097 -- ========================================================================
1098 -- THE EVENT LOOP
1099 -- ========================================================================
1101 -- the event loop instance
1102 local el = eventloop.new()
1104 -- add the core event handler for events
1105 el:add_exec (wmiir .. " read /event",
1106 function (line)
1107 local line = line or "nil"
1109 -- try to split off the argument(s)
1110 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1111 if not ev then
1112 ev = line
1115 -- now locate the handler function and call it
1116 local fn = ev_handlers[ev] or ev_handlers["*"]
1117 if fn then
1118 pcall (fn, ev, arg)
1120 end)
1122 -- ------------------------------------------------------------------------
1123 -- run the event loop and process events, this function does not exit
1124 function run_event_loop ()
1125 -- stop any other instance of wmiirc
1126 wmixp:write ("/event", "Start wmiirc " .. tostring(mypid))
1128 log("wmii: updating lbar")
1130 update_displayed_tags ()
1132 log("wmii: updating rbar")
1134 update_displayed_widgets ()
1136 log("wmii: updating active keys")
1138 update_active_keys ()
1140 log("wmii: starting event loop")
1141 while true do
1142 local sleep_for = process_timers()
1143 el:run_loop(sleep_for)
1147 -- ========================================================================
1148 -- PLUGINS API
1149 -- ========================================================================
1151 api_version = 0.1 -- the API version we export
1153 plugins = {} -- all plugins that were loaded
1155 -- ------------------------------------------------------------------------
1156 -- plugin loader which also verifies the version of the api the plugin needs
1158 -- here is what it does
1159 -- - does a manual locate on the file using package.path
1160 -- - reads in the file w/o using the lua interpreter
1161 -- - locates api_version=X.Y string
1162 -- - makes sure that api_version requested can be satisfied
1164 -- TODO: currently the api_version must be in an X.Y format, but we may want
1165 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1167 function load_plugin(name)
1168 local backup_path = package.path or "./?.lua"
1170 log ("loading " .. name)
1172 -- this is the version we want to find
1173 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1174 if (not api_major) or (not api_minor) then
1175 log ("WARNING: could not parse api_version in core/wmii.lua")
1176 return nil
1179 -- first find the plugin file
1180 local s, path_match, full_name, file
1181 for s in string.gmatch(package.path, "[^;]+") do
1182 local fn = s:gsub("%?", name)
1183 file = io.open(fn, "r")
1184 if file then
1185 path_match = s
1186 full_name = fn
1187 break
1191 -- read it in
1192 local txt
1193 if file then
1194 txt = file:read("*all")
1195 file:close()
1198 if not txt then
1199 log ("WARNING: could not load plugin '" .. name .. "'")
1200 return nil
1203 -- find the api_version line
1204 local line, plugin_version
1205 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1206 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1207 if tmp then
1208 break
1212 -- decompose the version string
1213 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1214 if (not plugin_major) or (not plugin_minor) then
1215 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1216 return nil
1219 -- make a version test
1220 if plugin_major ~= api_major then
1221 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1222 .. " (api " .. tonumber(api_version) .. ")")
1223 return nil
1226 if plugin_minor > api_minor then
1227 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1228 .. " (api " .. tonumber(api_version) .. ")")
1229 return nil
1232 -- actually load the module, but use only the path where we though it should be
1233 package.path = path_match
1234 local p,e,n = pcall (require, name)
1235 package.path = backup_path
1236 if not p then
1237 log ("WARNING: failed to load '" .. name .. "' plugin")
1238 log (" - path: " .. tostring(path_match))
1239 log (" - file: " .. tostring(full_name))
1240 log (" - plugin's api_version: " .. tostring(plugin_version))
1241 log (" - reason: " .. tostring(e) .. " (" .. tostring(n) .. ")")
1242 return nil
1245 -- success
1246 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1247 plugins[name] = p
1250 -- ------------------------------------------------------------------------
1251 -- widget template
1252 widget = {}
1253 widgets = {}
1255 -- ------------------------------------------------------------------------
1256 -- create a widget object and add it to the wmii /rbar
1258 -- examples:
1259 -- widget = wmii.widget:new ("999_clock")
1260 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1261 function widget:new (name, fn)
1262 o = {}
1264 if type(name) == "string" then
1265 o.name = name
1266 if type(fn) == "function" then
1267 o.fn = fn
1269 else
1270 error ("expected name followed by an optional function as arguments")
1273 setmetatable (o,self)
1274 self.__index = self
1275 self.__gc = function (o) o:hide() end
1277 widgets[name] = o
1279 o:show()
1280 return o
1283 -- ------------------------------------------------------------------------
1284 -- stop and destroy the timer
1285 function widget:delete ()
1286 widgets[self.name] = nil
1287 self:hide()
1290 -- ------------------------------------------------------------------------
1291 -- displays or updates the widget text
1293 -- examples:
1294 -- w:show("foo")
1295 -- w:show("foo", "#888888 #222222 #333333")
1296 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1298 function widget:show (txt, colors)
1299 local txt = txt or ""
1300 local colors = colors or get_ctl("normcolors") or ""
1301 if not self.txt then
1302 create ("/rbar/" .. self.name, colors .. " " .. txt)
1303 else
1304 write ("/rbar/" .. self.name, txt)
1306 self.txt = txt
1309 -- ------------------------------------------------------------------------
1310 -- hides a widget and removes it from the bar
1311 function widget:hide ()
1312 if self.txt then
1313 remove ("/lbar/" .. self.name)
1314 self.txt = nil
1318 --[[
1319 =pod
1321 =item widget:add_event_handler (ev, fn)
1323 Add an event handler callback for this widget, using I<fn> for event I<ev>
1325 =cut
1326 --]]
1328 function widget:add_event_handler (ev, fn)
1329 add_widget_event_handler( self.name, ev, fn)
1333 -- ------------------------------------------------------------------------
1334 -- remove all /rbar entries that we don't have widget objects for
1335 function update_displayed_widgets ()
1336 -- colours for /rbar
1337 local nc = get_ctl("normcolors") or ""
1339 -- build up a table of existing tags in the /lbar
1340 local old = {}
1341 local s
1342 for s in wmixp:idir ("/rbar") do
1343 old[s.name] = 1
1346 -- for all actual widgets in use we want to remove them from the old list
1347 local i,v
1348 for i,v in pairs(widgets) do
1349 old[v.name] = nil
1352 -- anything left in the old table should be removed now
1353 for i,v in pairs(old) do
1354 if v then
1355 remove("/rbar/"..i)
1360 -- ------------------------------------------------------------------------
1361 -- create a new program and for each line it generates call the callback function
1362 -- returns fd which can be passed to kill_exec()
1363 function add_exec (command, callback)
1364 return el:add_exec (command, callback)
1367 -- ------------------------------------------------------------------------
1368 -- terminates a program spawned off by add_exec()
1369 function kill_exec (fd)
1370 return el:kill_exec (fd)
1373 -- ------------------------------------------------------------------------
1374 -- timer template
1375 timer = {}
1376 local timers = {}
1378 -- ------------------------------------------------------------------------
1379 -- create a timer object and add it to the event loop
1381 -- examples:
1382 -- timer:new (my_timer_fn)
1383 -- timer:new (my_timer_fn, 15)
1384 function timer:new (fn, seconds)
1385 o = {}
1387 if type(fn) == "function" then
1388 o.fn = fn
1389 else
1390 error ("expected function followed by an optional number as arguments")
1393 setmetatable (o,self)
1394 self.__index = self
1395 self.__gc = function (o) o:stop() end
1397 -- add the timer
1398 timers[#timers+1] = o
1400 if seconds then
1401 o:resched(seconds)
1403 return o
1406 -- ------------------------------------------------------------------------
1407 -- stop and destroy the timer
1408 function timer:delete ()
1409 self:stop()
1410 local i,t
1411 for i,t in pairs(timers) do
1412 if t == self then
1413 table.remove (timers,i)
1414 return
1419 -- ------------------------------------------------------------------------
1420 -- run the timer given new interval
1421 function timer:resched (seconds)
1422 local seconds = seconds or self.interval
1423 if not (type(seconds) == "number") then
1424 error ("expected number as argument")
1427 local now = tonumber(os.date("%s"))
1429 self.interval = seconds
1430 self.next_time = now + seconds
1432 -- resort the timer list
1433 table.sort (timers, timer.is_less_then)
1436 -- helper for sorting timers
1437 function timer:is_less_then(another)
1438 if not self.next_time then
1439 return false -- another is smaller, nil means infinity
1441 elseif not another.next_time then
1442 return true -- self is smaller, nil means infinity
1444 elseif self.next_time < another.next_time then
1445 return true -- self is smaller than another
1448 return false -- another is smaller then self
1451 -- ------------------------------------------------------------------------
1452 -- stop the timer
1453 function timer:stop ()
1454 self.next_time = nil
1456 -- resort the timer list
1457 table.sort (timers, timer.is_less_then)
1460 -- ------------------------------------------------------------------------
1461 -- figure out how long before the next event
1462 function time_before_next_timer_event()
1463 local tmr = timers[1]
1464 if tmr and tmr.next_time then
1465 local now = tonumber(os.date("%s"))
1466 local seconds = tmr.next_time - now
1467 if seconds > 0 then
1468 return seconds
1471 return 0 -- sleep for ever
1474 -- ------------------------------------------------------------------------
1475 -- handle outstanding events
1476 function process_timers ()
1477 local now = tonumber(os.date("%s"))
1478 local torun = {}
1479 local i,tmr
1481 for i,tmr in pairs (timers) do
1482 if (not tmr) or (not tmr.next_time) then
1483 table.remove(timers,i)
1484 return 1
1487 if tmr.next_time > now then
1488 return tmr.next_time - now
1491 torun[#torun+1] = tmr
1494 for i,tmr in pairs (torun) do
1495 tmr:stop()
1496 local new_interval = pcall (tmr.fn, tmr)
1497 if new_interval ~= -1 then
1498 tmr:resched(rc)
1502 local sleep_for = time_before_next_timer_event()
1503 return sleep_for
1506 -- ------------------------------------------------------------------------
1507 -- cleanup everything in preparation for exit() or exec()
1508 function cleanup ()
1510 local i,v,tmr,p
1512 log ("wmii: stopping timer events")
1514 for i,tmr in pairs (timers) do
1515 pcall (tmr.delete, tmr)
1517 timers = {}
1519 log ("wmii: terminating eventloop")
1521 pcall(el.kill_all,el)
1523 log ("wmii: disposing of widgets")
1525 -- dispose of all widgets
1526 for i,v in pairs(widgets) do
1527 pcall(v.delete,v)
1529 timers = {}
1531 log ("wmii: releasing plugins")
1533 for i,p in pairs(plugins) do
1534 pcall (p.cleanup, p)
1536 plugins = {}
1538 log ("wmii: dormant")
1541 -- ========================================================================
1542 -- DOCUMENTATION
1543 -- ========================================================================
1545 --[[
1546 =pod
1548 =back
1550 =head1 ENVIRONMENT
1552 =over 4
1554 =item WMII_ADDRESS
1556 Used to determine location of wmii's listen socket.
1558 =back
1560 =head1 SEE ALSO
1562 L<wmii(1)>, L<lua(1)>
1564 =head1 AUTHOR
1566 Bart Trojanowski B<< <bart@jukie.net> >>
1568 =head1 COPYRIGHT AND LICENSE
1570 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
1572 This is free software. You may redistribute copies of it under the terms of
1573 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
1574 is NO WARRANTY, to the extent permitted by law.
1576 =cut
1577 --]]