2 -- Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
4 -- WMII event loop, in lua
6 -- http://www.jukie.net/~bart/blog/tag/wmiirc-lua
7 -- git://www.jukie.net/wmiirc-lua.git/
10 -- ========================================================================
12 -- ========================================================================
18 wmii.lua - WMII event-loop methods in lua
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
32 -- Configure wmii.lua parameters
34 xterm = 'x-terminal-emulator'
37 -- Now start the event loop
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
54 -- ========================================================================
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"
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")
82 local package
= package
83 local require
= require
84 local tostring = tostring
85 local tonumber = tonumber
86 local setmetatable
= setmetatable
91 local mypid
= posix
.getprocessid("pid")
93 -- ========================================================================
95 -- ========================================================================
97 -- wmiir points to the wmiir executable
98 -- TODO: need to make sure that wmiir is in path, and if not find it
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 -- ========================================================================
126 -- ========================================================================
133 Log the message provided in C<str>
135 Currently just writes to io.stderr
140 io
.stderr
:write (str
.. "\n")
143 -- ========================================================================
144 -- MAIN ACCESS FUNCTIONS
145 -- ========================================================================
150 =item ls ( dir, fmt )
152 List the wmii filesystem directory provided in C<dir>, in the format specified
155 Returns an iterator of TODO
159 function ls (dir
, fmt
)
160 local verbose
= fmt
and fmt
:match("l")
162 local s
= wmixp
:stat(dir
)
164 return function () return nil end
166 if s
.modestr
:match("^[^d]") then
168 return stat2str(verbose
, s
)
172 local itr
= wmixp
:idir (dir
)
183 return stat2str(verbose
, s
)
189 local function stat2str(verbose
, stat
)
191 return string.format("%s %s %s %5d %s %s", stat
.modestr
, stat
.uid
, stat
.gid
, stat
.length
, stat
.timestr
, stat
.name
)
193 if stat
.modestr
:match("^d") then
194 return stat
.name
.. "/"
201 -- ------------------------------------------------------------------------
202 -- read all contents of a wmii virtual file
204 return wmixp
:read (file
)
207 -- ------------------------------------------------------------------------
208 -- return an iterator which walks all the lines in the file
211 -- for event in wmii.iread("/ctl")
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
)
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")
246 cmdt
[#cmdt
+1] = "-fn"
249 local normcolors
= get_ctl("normcolors")
251 local nf
, nb
= normcolors
:match("(#%x+)%s+(#%x+)%s#%x+")
253 cmdt
[#cmdt
+1] = "-nf"
254 cmdt
[#cmdt
+1] = "'" .. nf
.. "'"
257 cmdt
[#cmdt
+1] = "-nb"
258 cmdt
[#cmdt
+1] = "'" .. nb
.. "'"
261 local focuscolors
= get_ctl("focuscolors")
263 local sf
, sb
= focuscolors
:match("(#%x+)%s+(#%x+)%s#%x+")
265 cmdt
[#cmdt
+1] = "-sf"
266 cmdt
[#cmdt
+1] = "'" .. sf
.. "'"
269 cmdt
[#cmdt
+1] = "-sb"
270 cmdt
[#cmdt
+1] = "'" .. sb
.. "'"
275 cmdt
[#cmdt
+1] = "'" .. prompt
.. "'"
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+")
290 for i
,v
in pairs(tbl
) do
291 if type(i
) == 'number' and type(v
) == 'string' then
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
," ")
310 fh
= io
.open (outfile
, "r")
313 local sel
= fh
:read("*l")
319 -- ------------------------------------------------------------------------
320 -- displays the a tag selection menu, returns selected tag
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
," ")
340 local fh
= io
.open (outfile
, "rb")
343 local prog
= fh
:read("*l")
349 -- ------------------------------------------------------------------------
350 -- displays the a program menu, returns selected program
354 for s
in wmixp
:idir ("/tag") do
355 if s
.name
and not (s
.name
== "sel") then
363 -- ------------------------------------------------------------------------
364 -- displays the a program menu, returns selected program
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
381 if not (type(sel
) == "string") then
382 error ("string argument expected")
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()
396 -- nothing to do if we have less then 2 tags
400 local num
= tonumber (sel
)
402 error ("number argument expected")
405 local name
= all
[sel
]
406 if not name
or name
== cur
then
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()
421 -- nothing to do if we have less then 2 tags
426 if (jump
< - #all
) or (jump
> #all
) then
427 error ("view selector is out of range")
430 -- find the one that's selected index
433 for i
,v
in pairs (all
) do
434 if v
== cur
then curi
= i
end
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
]
455 -- ========================================================================
457 -- ========================================================================
459 local action_handlers
= {
460 man
= function (act
, args
)
461 local xterm
= get_conf("xterm") or "xterm"
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
)
472 write ("/ctl", "quit")
475 exec
= function (act
, args
)
476 local what
= args
or wmiirc
478 write ("/ctl", "exec " .. what
)
481 xlock
= function (act
)
482 local cmd
= get_conf("xlock") or "xscreensaver-command --lock"
488 posix
.exec ("lua", wmiirc
)
492 -- TODO: consider storing list of executables around, and
493 -- this will then reinitialize that list
494 log (" TODO: rehash")
498 -- TODO: this should eventually update something on the /rbar
499 log (" TODO: status")
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>.
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
528 =item remove_action_handler (action)
530 Remove an action handler callback function for the given action string I<action>.
534 function remove_action_handler (action
)
536 action_handlers
[action
] = nil
539 -- ========================================================================
541 -- ========================================================================
543 local key_handlers
= {
544 ["*"] = function (key
)
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
.. " &")
554 ["Mod1-Shift-Return"] = function (key
)
555 local tag = tag_menu()
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
.. " &")
563 ["Mod1-a"] = function (key
)
564 local text
= menu(action_handlers
, "action:")
566 log ("Action: " .. text
)
569 local si
= text
:find("%s")
571 act
,args
= string.match(text
.. " ", "(%w+)%s(.+)")
574 local fn
= action_handlers
[act
]
576 local r
, err
= pcall (fn
, act
, args
)
578 log ("WARNING: " .. tostring(err
))
584 ["Mod1-p"] = function (key
)
585 local prog
= prog_menu()
587 log (" executing: " .. prog
)
588 os
.execute (prog
.. " &")
591 ["Mod1-Shift-p"] = function (key
)
592 local tag = tag_menu()
594 local prog
= prog_menu()
596 log (" executing: " .. prog
.. " on: " .. tag)
597 next_client_goes_to_tag
= tag
598 os
.execute (prog
.. " &")
602 ["Mod1-Shift-c"] = function (key
)
603 write ("/client/sel/ctl", "kill")
606 -- HJKL active selection
607 ["Mod1-h"] = function (key
)
608 write ("/tag/sel/ctl", "select left")
610 ["Mod1-l"] = function (key
)
611 write ("/tag/sel/ctl", "select right")
613 ["Mod1-j"] = function (key
)
614 write ("/tag/sel/ctl", "select down")
616 ["Mod1-k"] = function (key
)
617 write ("/tag/sel/ctl", "select up")
621 ["Mod1-Shift-h"] = function (key
)
622 write ("/tag/sel/ctl", "send sel left")
624 ["Mod1-Shift-l"] = function (key
)
625 write ("/tag/sel/ctl", "send sel right")
627 ["Mod1-Shift-j"] = function (key
)
628 write ("/tag/sel/ctl", "send sel down")
630 ["Mod1-Shift-k"] = function (key
)
631 write ("/tag/sel/ctl", "send sel up")
635 ["Mod1-space"] = function (key
)
636 write ("/tag/sel/ctl", "select toggle")
638 ["Mod1-Shift-space"] = function (key
)
639 write ("/tag/sel/ctl", "send sel toggle")
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
)
648 for i
,v
in pairs(all
) do
649 if num_str
== v
:sub(1,1) then
655 -- if we fail, then set it to the index requested
658 ["Mod4-Shift-#"] = function (key
, num
)
659 write ("/client/sel/tags", tostring(num
))
661 ["Mod4-@"] = function (key
, letter
)
662 local all
= get_tags()
664 for i
,v
in pairs(all
) do
665 if letter
== v
:sub(1,1) then
671 ["Mod4-Shift-@"] = function (key
, letter
)
672 local all
= get_tags()
674 for i
,v
in pairs(all
) do
675 if letter
== v
:sub(1,1) then
676 write ("/client/sel/tags", v
)
681 ["Mod1-comma"] = function (key
)
684 ["Mod1-period"] = function (key
)
687 ["Mod1-r"] = function (key
)
688 -- got to the last view
692 -- switching views and retagging
693 ["Mod1-t"] = function (key
)
695 local tag = tag_menu()
700 ["Mod1-Shift-t"] = function (key
)
701 -- move selected client to a tag
702 local tag = tag_menu()
704 write ("/client/sel/tags", tag)
707 ["Mod1-Shift-r"] = function (key
)
708 -- move selected client to a tag, and follow
709 local tag = tag_menu()
711 write ("/client/sel/tags", tag)
715 ["Mod1-Control-t"] = function (key
)
716 log (" TODO: Mod1-Control-t: " .. key
)
720 ["Mod1-d"] = function (key
)
721 write("/tag/sel/ctl", "colmode sel default")
723 ["Mod1-s"] = function (key
)
724 write("/tag/sel/ctl", "colmode sel stack")
726 ["Mod1-m"] = function (key
)
727 write("/tag/sel/ctl", "colmode sel max")
734 =item add_key_handler (key, fn)
736 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
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
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.
765 function remove_key_handler (key
)
767 local fn
= key_handlers
[key
]
768 key_handlers
[key
] = nil
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>.
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 ()
796 for x
,y
in pairs(key_handlers
) do
798 local i
= x
:find("#$")
802 t
[#t
+ 1] = x
:sub(1,i
-1) .. 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
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 ()
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
833 for s
in wmixp
:idir ("/lbar") do
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()
842 for i
,v
in pairs(all
) do
848 create ("/lbar/" .. v
, color
.. " " .. v
)
850 write ("/lbar/" .. v
, color
.. " " .. v
)
854 -- anything left in the old table should be removed now
855 for i
,v
in pairs(old
) do
862 -- ========================================================================
864 -- ========================================================================
866 local widget_ev_handlers
= {
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
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
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
900 log("Didn't find wname")
904 local wtable
= widget_ev_handlers
[wname
]
906 log("No widget cares about" .. wname
)
910 local fn
= wtable
[ev
] or wtable
["*"]
912 success
, err
= pcall( fn
, ev
, tonumber(number) )
914 log("Callback had an error in _handle_widget_event: " .. tostring(err
) )
918 log("no function found for " .. ev
)
922 local ev_handlers
= {
923 ["*"] = function (ev
, arg
)
924 log ("ev: " .. tostring(ev
) .. " - " .. tostring(arg
))
927 RightBarClick
= _handle_widget_event
,
929 -- process timer events
930 ProcessTimerEvents
= function (ev
, arg
)
934 -- exit if another wmiirc started up
935 Start
= function (ev
, arg
)
937 if arg
== "wmiirc" then
938 -- backwards compatibility with bash version
942 -- ignore if it came from us
943 local pid
= string.match(arg
, "wmiirc (%d+)")
945 local pid
= tonumber (pid
)
946 if not (pid
== mypid
) then
956 CreateTag
= function (ev
, arg
)
957 local nc
= get_ctl("normcolors") or ""
958 create ("/lbar/" .. arg
, nc
.. " " .. arg
)
960 DestroyTag
= function (ev
, arg
)
961 remove ("/lbar/" .. arg
)
964 FocusTag
= function (ev
, arg
)
965 local fc
= get_ctl("focuscolors") or ""
966 create ("/lbar/" .. arg
, fc
.. " " .. arg
)
967 write ("/lbar/" .. arg
, fc
.. " " .. arg
)
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)
985 -- key event handling
986 Key
= function (ev
, arg
)
989 -- can we find an exact match?
990 local fn
= key_handlers
[arg
]
992 local key
= arg
:gsub("-%d$", "-#")
993 -- can we find a match with a # wild card for the number
994 fn
= key_handlers
[key
]
996 -- convert the trailing number to a number
997 magic
= tonumber(arg
:match("-(%d)$"))
1001 local key
= arg
:gsub("-%a$", "-@")
1002 -- can we find a match with a @ wild card for a letter
1003 fn
= key_handlers
[key
]
1005 -- split off the trailing letter
1006 magic
= arg
:match("-(%a)$")
1010 -- everything else failed, try default match
1011 fn
= key_handlers
["*"]
1014 local r
, err
= pcall (fn
, arg
, magic
)
1016 log ("WARNING: " .. tostring(err
))
1021 -- mouse handling on the lbar
1022 LeftBarClick
= function (ev
, arg
)
1023 local button
,tag = string.match(arg
, "(%w+)%s+(%S+)")
1028 ClientFocus
= function (ev
, arg
)
1029 log ("ClientFocus: " .. arg
)
1031 ColumnFocus
= function (ev
, arg
)
1032 log ("ColumnFocus: " .. arg
)
1036 CreateClient
= function (ev
, arg
)
1037 if next_client_goes_to_tag
then
1038 local tag = next_client_goes_to_tag
1040 next_client_goes_to_tag
= nil
1041 write ("/client/" .. cli
.. "/tags", tag)
1047 UrgentTag
= function (ev
, arg
)
1048 log ("UrgentTag: " .. arg
)
1049 -- wmiir xwrite "/lbar/$@" "*$@"
1051 NotUrgentTag
= function (ev
, arg
)
1052 log ("NotUrgentTag: " .. arg
)
1053 -- wmiir xwrite "/lbar/$@" "$@"
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>
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
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>.
1099 function remove_event_handler (wname
, ev
)
1101 if not widget_ev_handlers
[wname
] then
1105 widget_ev_handlers
[wname
][ev
] = nil
1111 =item add_event_handler (ev, fn)
1113 Add an event handler callback function, I<fn>, for the given event I<ev>.
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
1135 =item remove_event_handler (ev)
1137 Remove an event handler callback function for the given event I<ev>.
1141 function remove_event_handler (ev
)
1143 ev_handlers
[ev
] = nil
1147 -- ========================================================================
1148 -- MAIN INTERFACE FUNCTIONS
1149 -- ========================================================================
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
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
)
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
)
1182 for s
in iread("/ctl") do
1183 local var
,val
= s
:match("(%w+)%s+(.+)")
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
1202 for x
, y
in pairs(first
) do
1206 elseif type(first
) == "string"
1207 and (type(second
) == "string"
1208 or type(second
) == "number") then
1209 config
[first
] = second
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
)
1225 -- ========================================================================
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",
1235 local line
= line
or "nil"
1237 -- try to split off the argument(s)
1238 local ev
,arg
= string.match(line
, "(%S+)%s+(.+)")
1243 -- now locate the handler function and call it
1244 local fn
= ev_handlers
[ev
] or ev_handlers
["*"]
1246 local r
, err
= pcall (fn
, ev
, arg
)
1248 log ("WARNING: " .. tostring(err
))
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")
1273 local sleep_for
= process_timers()
1274 el
:run_loop(sleep_for
)
1278 -- ========================================================================
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")
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")
1328 txt
= file
:read("*all")
1333 log ("WARNING: could not load plugin '" .. name
.. "'")
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
1346 if not plugin_version
then
1347 log ("WARNING: could not find api_version string in plugin '" .. name
.. "'")
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")
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
) .. ")")
1365 if plugin_minor
> api_minor
then
1366 log ("WARNING: '" .. name
.. "' plugin minor version missmatch, is " .. plugin_version
1367 .. " (api " .. tonumber(api_version
) .. ")")
1371 -- the configuration parameters before loading
1372 if type(vars
) == "table" then
1374 for var
,val
in pairs(vars
) do
1375 local success
= pcall (set_conf
, name
.. "." .. var
, val
)
1377 log ("WARNING: bad variable {" .. tostring(var
) .. ", " .. tostring(val
) .. "} "
1378 .. "given; loading '" .. name
.. "' plugin failed.")
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
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
))
1398 log ("OK, plugin " .. name
.. " loaded, requested api v" .. plugin_version
)
1399 plugins
[name
] = what
1403 -- ------------------------------------------------------------------------
1408 -- ------------------------------------------------------------------------
1409 -- create a widget object and add it to the wmii /rbar
1412 -- widget = wmii.widget:new ("999_clock")
1413 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1414 function widget
:new (name
, fn
)
1417 if type(name
) == "string" then
1419 if type(fn
) == "function" then
1423 error ("expected name followed by an optional function as arguments")
1426 setmetatable (o
,self
)
1428 self
.__gc
= function (o
) o
:hide() end
1436 -- ------------------------------------------------------------------------
1437 -- stop and destroy the timer
1438 function widget
:delete ()
1439 widgets
[self
.name
] = nil
1443 -- ------------------------------------------------------------------------
1444 -- displays or updates the widget text
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 ""
1456 towrite
= colors
.. " " .. towrite
1458 if not self
.txt
then
1459 create ("/rbar/" .. self
.name
, towrite
)
1461 write ("/rbar/" .. self
.name
, towrite
)
1466 -- ------------------------------------------------------------------------
1467 -- hides a widget and removes it from the bar
1468 function widget
:hide ()
1470 remove ("/lbar/" .. self
.name
)
1478 =item widget:add_event_handler (ev, fn)
1480 Add an event handler callback for this widget, using I<fn> for event I<ev>
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
1499 for s
in wmixp
:idir ("/rbar") do
1503 -- for all actual widgets in use we want to remove them from the old list
1505 for i
,v
in pairs(widgets
) do
1509 -- anything left in the old table should be removed now
1510 for i
,v
in pairs(old
) do
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 -- ------------------------------------------------------------------------
1535 -- ------------------------------------------------------------------------
1536 -- create a timer object and add it to the event loop
1539 -- timer:new (my_timer_fn)
1540 -- timer:new (my_timer_fn, 15)
1541 function timer
:new (fn
, seconds
)
1544 if type(fn
) == "function" then
1547 error ("expected function followed by an optional number as arguments")
1550 setmetatable (o
,self
)
1552 self
.__gc
= function (o
) o
:stop() end
1555 timers
[#timers
+1] = o
1563 -- ------------------------------------------------------------------------
1564 -- stop and destroy the timer
1565 function timer
:delete ()
1568 for i
,t
in pairs(timers
) do
1570 table.remove (timers
,i
)
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 -- ------------------------------------------------------------------------
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
1628 return 0 -- sleep for ever
1631 -- ------------------------------------------------------------------------
1632 -- handle outstanding events
1633 function process_timers ()
1634 local now
= tonumber(os
.date("%s"))
1638 for i
,tmr
in pairs (timers
) do
1640 -- prune out removed timers
1641 table.remove(timers
,i
)
1644 elseif not tmr
.next_time
then
1645 -- break out once we find a timer that is stopped
1648 elseif tmr
.next_time
> now
then
1649 -- break out once we get to the future
1653 -- this one is good to go
1654 torun
[#torun
+1] = tmr
1657 for i
,tmr
in pairs (torun
) do
1659 local status
,new_interval
= pcall (tmr
.fn
, tmr
)
1661 new_interval
= new_interval
or self
.interval
1662 if new_interval
and (new_interval
~= -1) then
1663 tmr
:resched(new_interval
)
1666 log ("ERROR: " .. tostring(new_interval
))
1670 local sleep_for
= time_before_next_timer_event()
1674 -- ------------------------------------------------------------------------
1675 -- cleanup everything in preparation for exit() or exec()
1680 log ("wmii: stopping timer events")
1682 for i
,tmr
in pairs (timers
) do
1683 pcall (tmr
.delete
, tmr
)
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
1699 -- FIXME: it doesn't seem to do what I want
1701 log ("wmii: releasing plugins")
1703 for i,p in pairs(plugins) do
1705 pcall (p.cleanup, p)
1711 log ("wmii: dormant")
1714 -- ========================================================================
1716 -- ========================================================================
1729 Used to determine location of wmii's listen socket.
1735 L<wmii(1)>, L<lua(1)>
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.