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
= wmiidir
.. "/core/?.lua;" ..
62 wmiidir
.. "/plugins/?.lua;" ..
64 package
.cpath
= wmiidir
.. "/core/?.so;" ..
65 wmiidir
.. "/plugins/?.so;" ..
68 local ixp
= require
"ixp"
69 local eventloop
= require
"eventloop"
70 local history
= require
"history"
72 local io
= require("io")
73 local os
= require("os")
74 local string = require("string")
75 local table = require("table")
76 local math
= require("math")
82 local package
= package
83 local require
= require
84 local tostring = tostring
85 local tonumber = tonumber
86 local setmetatable
= setmetatable
88 -- kinda silly, but there is no working liblua5.1-posix0 in ubuntu
89 -- so we make it optional
90 local have_posix
, posix
= pcall(require
,"posix")
97 myid
= posix
.getprocessid("pid")
99 local now
= tonumber(os
.date("%s"))
101 myid
= math
.random(10000)
104 -- ========================================================================
106 -- ========================================================================
108 -- wmiir points to the wmiir executable
109 -- TODO: need to make sure that wmiir is in path, and if not find it
110 local wmiir
= "wmiir"
112 -- wmii_adr is the address we use when connecting using ixp
113 local wmii_adr
= os
.getenv("WMII_ADDRESS")
114 or ("unix!/tmp/ns." .. os
.getenv("USER") .. "."
115 .. os
.getenv("DISPLAY"):match("(:%d+)") .. "/wmii")
117 -- wmixp is the ixp context we use to talk to wmii
118 local wmixp
= ixp
.new(wmii_adr
)
120 -- history of previous views, view_hist[#view_hist] is the last one
121 local view_hist
= {} -- sorted with 1 being the oldest
122 local view_hist_max
= 50 -- max number to keep track of
124 -- allow for a client to be forced to a tag
125 local next_client_goes_to_tag
= nil
127 -- program and action histories
128 local prog_hist
= history
.new (20)
129 local action_hist
= history
.new(10)
131 -- where to find plugins
132 plugin_path
= os
.getenv("HOME") .. "/.wmii-3.5/plugins/?.so;"
133 .. os
.getenv("HOME") .. "/.wmii-3.5/plugins/?.lua;"
134 .. "/usr/local/lib/lua/5.1/wmii/?.so;"
135 .. "/usr/local/share/lua/5.1/wmii/?.lua;"
136 .. "/usr/lib/lua/5.1/wmii/?.so;"
137 .. "/usr/share/lua/5.1/wmii/?.lua"
139 -- where to find wmiirc (see find_wmiirc())
140 wmiirc_path
= os
.getenv("HOME") .. "/.wmii-3.5/wmiirc.lua;"
141 .. os
.getenv("HOME") .. "/.wmii-3.5/wmiirc;"
142 .. "/etc/X11/wmii-3.5/wmiirc.lua;"
143 .. "/etc/X11/wmii-3.5/wmiirc"
145 -- ========================================================================
147 -- ========================================================================
154 Log the message provided in C<str>
156 Currently just writes to io.stderr
161 if get_conf("debug") then
162 io
.stderr
:write (str
.. "\n")
169 =item find_wmiirc ( )
171 Locates the wmiirc script. It looks in ~/.wmii-3.5 and /etc/X11/wmii-3.5
172 for the first lua script bearing the name wmiirc.lua or wmiirc. Returns
177 function find_wmiirc()
179 for fn
in string.gmatch(wmiirc_path
, "[^;]+") do
180 -- try to locate the files locally
181 local file
= io
.open(fn
, "r")
183 local txt
= file
:read("*line")
185 if type(txt
) == 'string' and txt
:match("lua") then
194 -- ========================================================================
195 -- MAIN ACCESS FUNCTIONS
196 -- ========================================================================
201 =item ls ( dir, fmt )
203 List the wmii filesystem directory provided in C<dir>, in the format specified
206 Returns an iterator of TODO
210 function ls (dir
, fmt
)
211 local verbose
= fmt
and fmt
:match("l")
213 local s
= wmixp
:stat(dir
)
215 return function () return nil end
217 if s
.modestr
:match("^[^d]") then
219 return stat2str(verbose
, s
)
223 local itr
= wmixp
:idir (dir
)
234 return stat2str(verbose
, s
)
240 local function stat2str(verbose
, stat
)
242 return string.format("%s %s %s %5d %s %s", stat
.modestr
, stat
.uid
, stat
.gid
, stat
.length
, stat
.timestr
, stat
.name
)
244 if stat
.modestr
:match("^d") then
245 return stat
.name
.. "/"
252 -- ------------------------------------------------------------------------
253 -- read all contents of a wmii virtual file
255 return wmixp
:read (file
)
258 -- ------------------------------------------------------------------------
259 -- return an iterator which walks all the lines in the file
262 -- for event in wmii.iread("/ctl")
266 -- NOTE: don't use iread for files that could block, as this will interfere
267 -- with timer processing and event delivery. Instead fork off a process to
268 -- execute wmiir and read back the responses via callback.
269 function iread (file
)
270 return wmixp
:iread(file
)
273 -- ------------------------------------------------------------------------
274 -- create a wmii file, optionally write data to it
275 function create (file
, data
)
276 wmixp
:create(file
, data
)
279 -- ------------------------------------------------------------------------
280 -- remove a wmii file
281 function remove (file
)
285 -- ------------------------------------------------------------------------
286 -- write a value to a wmii virtual file system
287 function write (file
, value
)
288 wmixp
:write (file
, value
)
291 -- ------------------------------------------------------------------------
292 -- setup a table describing dmenu command
293 local function dmenu_cmd (prompt
, iterator
)
294 local cmdt
= { "dmenu", "-b" }
295 local fn
= get_ctl("font")
297 cmdt
[#cmdt
+1] = "-fn"
300 local normcolors
= get_ctl("normcolors")
302 local nf
, nb
= normcolors
:match("(#%x+)%s+(#%x+)%s#%x+")
304 cmdt
[#cmdt
+1] = "-nf"
305 cmdt
[#cmdt
+1] = "'" .. nf
.. "'"
308 cmdt
[#cmdt
+1] = "-nb"
309 cmdt
[#cmdt
+1] = "'" .. nb
.. "'"
312 local focuscolors
= get_ctl("focuscolors")
314 local sf
, sb
= focuscolors
:match("(#%x+)%s+(#%x+)%s#%x+")
316 cmdt
[#cmdt
+1] = "-sf"
317 cmdt
[#cmdt
+1] = "'" .. sf
.. "'"
320 cmdt
[#cmdt
+1] = "-sb"
321 cmdt
[#cmdt
+1] = "'" .. sb
.. "'"
326 cmdt
[#cmdt
+1] = "'" .. prompt
.. "'"
332 -- ------------------------------------------------------------------------
333 -- displays the menu given an table of entires, returns selected text
334 function menu (tbl
, prompt
)
335 local dmenu
= dmenu_cmd(prompt
)
337 local infile
= os
.tmpname()
338 local fh
= io
.open (infile
, "w+")
341 for i
,v
in pairs(tbl
) do
342 if type(i
) == 'number' and type(v
) == 'string' then
351 local outfile
= os
.tmpname()
353 dmenu
[#dmenu
+1] = "<"
354 dmenu
[#dmenu
+1] = infile
355 dmenu
[#dmenu
+1] = ">"
356 dmenu
[#dmenu
+1] = outfile
358 local cmd
= table.concat(dmenu
," ")
361 fh
= io
.open (outfile
, "r")
364 local sel
= fh
:read("*l")
370 -- ------------------------------------------------------------------------
371 -- displays the a tag selection menu, returns selected tag
373 local tags
= get_tags()
375 return menu(tags
, "tag:")
378 -- ------------------------------------------------------------------------
379 -- displays the a program menu, returns selected program
380 function prog_menu ()
381 local dmenu
= dmenu_cmd("cmd:")
383 local outfile
= os
.tmpname()
385 dmenu
[#dmenu
+1] = ">"
386 dmenu
[#dmenu
+1] = outfile
389 for n
in prog_hist
:walk_reverse_unique() do
390 hstt
[#hstt
+1] = "echo '" .. n
.. "' ; "
393 local cmd
= "(" .. table.concat(hstt
)
395 .. table.concat(dmenu
," ")
398 local fh
= io
.open (outfile
, "rb")
401 local prog
= fh
:read("*l")
407 -- ------------------------------------------------------------------------
408 -- displays the a program menu, returns selected program
412 for s
in wmixp
:idir ("/tag") do
413 if s
.name
and not (s
.name
== "sel") then
421 -- ------------------------------------------------------------------------
422 -- displays the a program menu, returns selected program
424 local v
= wmixp
:read("/ctl") or ""
425 return v
:match("view%s+(%S+)")
428 -- ------------------------------------------------------------------------
429 -- changes the current view to the name given
430 function set_view(sel
)
431 local cur
= get_view()
432 local all
= get_tags()
434 if #all
< 2 or sel
== cur
then
435 -- nothing to do if we have less then 2 tags
439 if not (type(sel
) == "string") then
440 error ("string argument expected")
444 write ("/ctl", "view " .. sel
)
447 -- ------------------------------------------------------------------------
448 -- changes the current view to the index given
449 function set_view_index(sel
)
450 local cur
= get_view()
451 local all
= get_tags()
454 -- nothing to do if we have less then 2 tags
458 local num
= tonumber (sel
)
460 error ("number argument expected")
463 local name
= all
[sel
]
464 if not name
or name
== cur
then
469 write ("/ctl", "view " .. name
)
472 -- ------------------------------------------------------------------------
473 -- chnages to current view by offset given
474 function set_view_ofs(jump
)
475 local cur
= get_view()
476 local all
= get_tags()
479 -- nothing to do if we have less then 2 tags
484 if (jump
< - #all
) or (jump
> #all
) then
485 error ("view selector is out of range")
488 -- find the one that's selected index
491 for i
,v
in pairs (all
) do
492 if v
== cur
then curi
= i
end
496 local newi
= math
.fmod(#all
+ curi
+ jump
- 1, #all
) + 1
497 if (newi
< - #all
) or (newi
> #all
) then
498 error ("error computng new view")
501 write ("/ctl", "view " .. all
[newi
])
504 -- ------------------------------------------------------------------------
505 -- toggle between last view and current view
506 function toggle_view()
507 local last
= view_hist
[#view_hist
]
513 -- ========================================================================
515 -- ========================================================================
517 local action_handlers
= {
518 man
= function (act
, args
)
519 local xterm
= get_conf("xterm") or "xterm"
521 if (not page
) or (not page
:match("%S")) then
522 page
= wmiidir
.. "/wmii.3lua"
524 local cmd
= xterm
.. " -e man " .. page
.. " &"
525 log (" executing: " .. cmd
)
530 write ("/ctl", "quit")
533 exec
= function (act
, args
)
534 local what
= args
or "wmii"
535 log (" asking wmii to exec " .. tostring(what
))
537 write ("/ctl", "exec " .. what
)
540 xlock
= function (act
)
541 local cmd
= get_conf("xlock") or "xscreensaver-command --lock"
547 local wmiirc
= find_wmiirc()
549 log (" executing: lua " .. wmiirc
)
551 posix
.exec ("lua", wmiirc
)
554 log("sorry cannot restart; you don't have lua's posix library.")
559 wmixp
:write ("/client/sel/ctl", "Urgent toggle")
564 -- TODO: consider storing list of executables around, and
565 -- this will then reinitialize that list
566 log (" TODO: rehash")
570 -- TODO: this should eventually update something on the /rbar
571 log (" TODO: status")
579 =item add_action_handler (action, fn)
581 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
585 function add_action_handler (action
, fn
)
587 if type(action
) ~= "string" or type(fn
) ~= "function" then
588 error ("expecting a string and a function")
591 if action_handlers
[action
] then
592 error ("action handler already exists for '" .. action
.. "'")
595 action_handlers
[action
] = fn
601 =item remove_action_handler (action)
603 Remove an action handler callback function for the given action string I<action>.
607 function remove_action_handler (action
)
609 action_handlers
[action
] = nil
612 -- ========================================================================
614 -- ========================================================================
616 function ke_fullscreen_toggle()
617 wmixp
:write ("/client/sel/ctl", "Fullscreen toggle")
620 function ke_view_starting_with_letter (letter
)
623 -- find the view name in history in reverse order
624 for i
=#view_hist
,1,-1 do
626 if letter
== v
:sub(1,1) then
632 -- otherwise just pick the first view that matches
633 local all
= get_tags()
634 for i
,v
in pairs(all
) do
635 if letter
== v
:sub(1,1) then
644 function ke_handle_action()
649 for n
in action_hist
:walk_reverse() do
651 actions
[#actions
+1] = n
657 for n
,v
in pairs(action_handlers
) do
659 actions
[#actions
+1] = n
664 local text
= menu(actions
, "action:")
666 log ("Action: " .. text
)
669 local si
= text
:find("%s")
671 act
,args
= string.match(text
.. " ", "(%w+)%s(.+)")
674 local fn
= action_handlers
[act
]
676 action_hist
:add (act
)
677 local r
, err
= pcall (fn
, act
, args
)
679 log ("WARNING: " .. tostring(err
))
687 local key_handlers
= {
688 ["*"] = function (key
)
692 -- execution and actions
693 ["Mod1-Return"] = function (key
)
694 local xterm
= get_conf("xterm") or "xterm"
695 log (" executing: " .. xterm
)
696 os
.execute (xterm
.. " &")
698 ["Mod1-Shift-Return"] = function (key
)
699 local tag = tag_menu()
701 local xterm
= get_conf("xterm") or "xterm"
702 log (" executing: " .. xterm
.. " on: " .. tag)
703 next_client_goes_to_tag
= tag
704 os
.execute (xterm
.. " &")
707 ["Mod1-a"] = function (key
)
710 ["Mod1-p"] = function (key
)
711 local prog
= prog_menu()
713 prog_hist
:add(prog
:match("(%w+)"))
714 log (" executing: " .. prog
)
715 os
.execute (prog
.. " &")
718 ["Mod1-Shift-p"] = function (key
)
719 local tag = tag_menu()
721 local prog
= prog_menu()
723 log (" executing: " .. prog
.. " on: " .. tag)
724 next_client_goes_to_tag
= tag
725 os
.execute (prog
.. " &")
729 ["Mod1-Shift-c"] = function (key
)
730 write ("/client/sel/ctl", "kill")
733 -- HJKL active selection
734 ["Mod1-h"] = function (key
)
735 write ("/tag/sel/ctl", "select left")
737 ["Mod1-l"] = function (key
)
738 write ("/tag/sel/ctl", "select right")
740 ["Mod1-j"] = function (key
)
741 write ("/tag/sel/ctl", "select down")
743 ["Mod1-k"] = function (key
)
744 write ("/tag/sel/ctl", "select up")
748 ["Mod1-Shift-h"] = function (key
)
749 write ("/tag/sel/ctl", "send sel left")
751 ["Mod1-Shift-l"] = function (key
)
752 write ("/tag/sel/ctl", "send sel right")
754 ["Mod1-Shift-j"] = function (key
)
755 write ("/tag/sel/ctl", "send sel down")
757 ["Mod1-Shift-k"] = function (key
)
758 write ("/tag/sel/ctl", "send sel up")
762 ["Mod1-space"] = function (key
)
763 write ("/tag/sel/ctl", "select toggle")
765 ["Mod1-Shift-space"] = function (key
)
766 write ("/tag/sel/ctl", "send sel toggle")
769 -- work spaces (# and @ are wildcards for numbers and letters)
770 ["Mod4-#"] = function (key
, num
)
771 -- first attempt to find a view that starts with the number requested
772 local num_str
= tostring(num
)
773 if not ke_view_starting_with_letter (num_str
) then
774 -- if we fail, then set it to the index requested
778 ["Mod4-Shift-#"] = function (key
, num
)
779 write ("/client/sel/tags", tostring(num
))
781 ["Mod4-@"] = function (key
, letter
)
782 ke_view_starting_with_letter (letter
)
784 ["Mod4-Shift-@"] = function (key
, letter
)
785 local all
= get_tags()
787 for i
,v
in pairs(all
) do
788 if letter
== v
:sub(1,1) then
789 write ("/client/sel/tags", v
)
794 ["Mod1-comma"] = function (key
)
797 ["Mod1-period"] = function (key
)
800 ["Mod1-r"] = function (key
)
801 -- got to the last view
805 -- switching views and retagging
806 ["Mod1-t"] = function (key
)
808 local tag = tag_menu()
813 ["Mod1-Shift-t"] = function (key
)
814 -- move selected client to a tag
815 local tag = tag_menu()
817 write ("/client/sel/tags", tag)
820 ["Mod1-Shift-r"] = function (key
)
821 -- move selected client to a tag, and follow
822 local tag = tag_menu()
824 write ("/client/sel/tags", tag)
828 ["Mod1-Control-t"] = function (key
)
829 log (" TODO: Mod1-Control-t: " .. key
)
833 ["Mod1-d"] = function (key
)
834 write("/tag/sel/ctl", "colmode sel default")
836 ["Mod1-s"] = function (key
)
837 write("/tag/sel/ctl", "colmode sel stack")
839 ["Mod1-m"] = function (key
)
840 write("/tag/sel/ctl", "colmode sel max")
842 ["Mod1-f"] = function (key
)
843 ke_fullscreen_toggle()
846 -- changing client flags
847 ["Shift-Mod1-f"] = function (key
)
848 log ("setting flags")
850 local cli
= get_client ()
852 local flags
= { "suspend", "raw" }
853 local current_flags
= cli
:flags_string()
855 local what
= menu(flags
, "current flags: " .. current_flags
.. " toggle:")
859 ["Mod4-space"] = function (key
)
860 local cli
= get_client ()
868 =item add_key_handler (key, fn)
870 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
874 function add_key_handler (key
, fn
)
876 if type(key
) ~= "string" or type(fn
) ~= "function" then
877 error ("expecting a string and a function")
880 if key_handlers
[key
] then
881 -- TODO: we may wish to allow multiple handlers for one keypress
882 error ("key handler already exists for '" .. key
.. "'")
885 key_handlers
[key
] = fn
891 =item remove_key_handler (key)
893 Remove an key handler callback function for the given key I<key>.
895 Returns the handler callback function.
899 function remove_key_handler (key
)
901 local fn
= key_handlers
[key
]
902 key_handlers
[key
] = nil
909 =item remap_key_handler (old_key, new_key)
911 Remove a key handler callback function from the given key I<old_key>,
912 and assign it to a new key I<new_key>.
916 function remap_key_handler (old_key
, new_key
)
918 local fn
= remove_key_handler(old_key
)
920 return add_key_handler (new_key
, fn
)
924 -- ------------------------------------------------------------------------
925 -- update the /keys wmii file with the list of all handlers
926 local alphabet
="abcdefghijklmnopqrstuvwxyz"
927 function update_active_keys ()
930 for x
,y
in pairs(key_handlers
) do
932 local i
= x
:find("#$")
936 t
[#t
+ 1] = x
:sub(1,i
-1) .. j
942 for j
=1,alphabet
:len() do
943 local a
= alphabet
:sub(j
,j
)
944 t
[#t
+ 1] = x
:sub(1,i
-1) .. a
947 t
[#t
+ 1] = tostring(x
)
952 local all_keys
= table.concat(t
, "\n")
953 --log ("setting /keys to...\n" .. all_keys .. "\n");
954 write ("/keys", all_keys
)
957 -- ------------------------------------------------------------------------
958 -- update the /lbar wmii file with the current tags
959 function update_displayed_tags ()
961 local fc
= get_ctl("focuscolors") or ""
962 local nc
= get_ctl("normcolors") or ""
964 -- build up a table of existing tags in the /lbar
967 for s
in wmixp
:idir ("/lbar") do
971 -- for all actual tags in use create any entries in /lbar we don't have
972 -- clear the old table entries if we have them
973 local cur
= get_view()
974 local all
= get_tags()
976 for i
,v
in pairs(all
) do
982 create ("/lbar/" .. v
, color
.. " " .. v
)
984 write ("/lbar/" .. v
, color
.. " " .. v
)
988 -- anything left in the old table should be removed now
989 for i
,v
in pairs(old
) do
996 -- ========================================================================
998 -- ========================================================================
1000 local widget_ev_handlers
= {
1006 =item _handle_widget_event (ev, arg)
1008 Top-level event handler for redispatching events to widgets. This event
1009 handler is added for any widget event that currently has a widget registered
1012 Valid widget events are currently
1014 RightBarMouseDown <buttonnumber> <widgetname>
1015 RightBarClick <buttonnumber> <widgetname>
1017 the "Click" event is sent on mouseup.
1019 The callbacks are given only the button number as their argument, to avoid the
1025 local function _handle_widget_event (ev
, arg
)
1027 log("_handle_widget_event: " .. tostring(ev
) .. " - " .. tostring(arg
))
1029 -- parse arg to strip out our widget name
1030 local number,wname
= string.match(arg
, "(%d+)%s+(.+)")
1032 -- check our dispatch table for that widget
1034 log("Didn't find wname")
1038 local wtable
= widget_ev_handlers
[wname
]
1040 log("No widget cares about" .. wname
)
1044 local fn
= wtable
[ev
] or wtable
["*"]
1046 success
, err
= pcall( fn
, ev
, tonumber(number) )
1048 log("Callback had an error in _handle_widget_event: " .. tostring(err
) )
1052 log("no function found for " .. ev
)
1056 local ev_handlers
= {
1057 ["*"] = function (ev
, arg
)
1058 log ("ev: " .. tostring(ev
) .. " - " .. tostring(arg
))
1061 RightBarClick
= _handle_widget_event
,
1063 -- process timer events
1064 ProcessTimerEvents
= function (ev
, arg
)
1068 -- exit if another wmiirc started up
1069 Start
= function (ev
, arg
)
1071 if arg
== "wmiirc" then
1072 -- backwards compatibility with bash version
1073 log (" exiting; pid=" .. tostring(myid
))
1077 -- ignore if it came from us
1078 local pid
= string.match(arg
, "wmiirc (%d+)")
1080 local pid
= tonumber (pid
)
1081 if not (pid
== myid
) then
1082 log (" exiting; pid=" .. tostring(myid
))
1092 CreateTag
= function (ev
, arg
)
1093 local nc
= get_ctl("normcolors") or ""
1094 create ("/lbar/" .. arg
, nc
.. " " .. arg
)
1096 DestroyTag
= function (ev
, arg
)
1097 remove ("/lbar/" .. arg
)
1099 -- remove the tag from history
1101 for i
=#view_hist
,1,-1 do
1104 table.remove(view_hist
,i
)
1109 FocusTag
= function (ev
, arg
)
1110 local fc
= get_ctl("focuscolors") or ""
1111 create ("/lbar/" .. arg
, fc
.. " " .. arg
)
1112 write ("/lbar/" .. arg
, fc
.. " " .. arg
)
1114 UnfocusTag
= function (ev
, arg
)
1115 local nc
= get_ctl("normcolors") or ""
1116 create ("/lbar/" .. arg
, nc
.. " " .. arg
)
1117 write ("/lbar/" .. arg
, nc
.. " " .. arg
)
1119 -- don't duplicate the last entry
1120 if not (arg
== view_hist
[#view_hist
]) then
1121 view_hist
[#view_hist
+1] = arg
1123 -- limit to view_hist_max
1124 if #view_hist
> view_hist_max
then
1125 table.remove(view_hist
, 1)
1130 -- key event handling
1131 Key
= function (ev
, arg
)
1132 log ("Key: " .. arg
)
1134 -- can we find an exact match?
1135 local fn
= key_handlers
[arg
]
1137 local key
= arg
:gsub("-%d$", "-#")
1138 -- can we find a match with a # wild card for the number
1139 fn
= key_handlers
[key
]
1141 -- convert the trailing number to a number
1142 magic
= tonumber(arg
:match("-(%d)$"))
1146 local key
= arg
:gsub("-%a$", "-@")
1147 -- can we find a match with a @ wild card for a letter
1148 fn
= key_handlers
[key
]
1150 -- split off the trailing letter
1151 magic
= arg
:match("-(%a)$")
1155 -- everything else failed, try default match
1156 fn
= key_handlers
["*"]
1159 local r
, err
= pcall (fn
, arg
, magic
)
1161 log ("WARNING: " .. tostring(err
))
1166 -- mouse handling on the lbar
1167 LeftBarClick
= function (ev
, arg
)
1168 local button
,tag = string.match(arg
, "(%w+)%s+(%S+)")
1173 ClientFocus
= function (ev
, arg
)
1174 log ("ClientFocus: " .. arg
)
1175 client_focused (arg
)
1177 ColumnFocus
= function (ev
, arg
)
1178 log ("ColumnFocus: " .. arg
)
1182 CreateClient
= function (ev
, arg
)
1183 if next_client_goes_to_tag
then
1184 local tag = next_client_goes_to_tag
1186 next_client_goes_to_tag
= nil
1187 write ("/client/" .. cli
.. "/tags", tag)
1190 client_created (arg
)
1192 DestroyClient
= function (ev
, arg
)
1193 client_destoryed (arg
)
1197 UrgentTag
= function (ev
, arg
)
1198 log ("UrgentTag: " .. arg
)
1199 -- wmiir xwrite "/lbar/$@" "*$@"
1201 NotUrgentTag
= function (ev
, arg
)
1202 log ("NotUrgentTag: " .. arg
)
1203 -- wmiir xwrite "/lbar/$@" "$@"
1211 =item add_widget_event_handler (wname, ev, fn)
1213 Add an event handler callback for the I<ev> event on the widget named I<wname>
1218 function add_widget_event_handler (wname
, ev
, fn
)
1219 if type(wname
) ~= "string" or type(ev
) ~= "string" or type(fn
) ~= "function" then
1220 error ("expecting string for widget name, string for event name and a function callback")
1223 -- Make sure the widget event handler is present
1224 if not ev_handlers
[ev
] then
1225 ev_handlers
[ev
] = _handle_widget_event
1228 if not widget_ev_handlers
[wname
] then
1229 widget_ev_handlers
[wname
] = { }
1232 if widget_ev_handlers
[wname
][ev
] then
1233 -- TODO: we may wish to allow multiple handlers for one event
1234 error ("event handler already exists on widget '" .. wname
.. "' for '" .. ev
.. "'")
1237 widget_ev_handlers
[wname
][ev
] = fn
1243 =item remove_widget_event_handler (wname, ev)
1245 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1249 function remove_event_handler (wname
, ev
)
1251 if not widget_ev_handlers
[wname
] then
1255 widget_ev_handlers
[wname
][ev
] = nil
1261 =item add_event_handler (ev, fn)
1263 Add an event handler callback function, I<fn>, for the given event I<ev>.
1267 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1268 function add_event_handler (ev
, fn
)
1269 if type(ev
) ~= "string" or type(fn
) ~= "function" then
1270 error ("expecting a string and a function")
1273 if ev_handlers
[ev
] then
1274 -- TODO: we may wish to allow multiple handlers for one event
1275 error ("event handler already exists for '" .. ev
.. "'")
1279 ev_handlers
[ev
] = fn
1285 =item remove_event_handler (ev)
1287 Remove an event handler callback function for the given event I<ev>.
1291 function remove_event_handler (ev
)
1293 ev_handlers
[ev
] = nil
1297 -- ========================================================================
1298 -- MAIN INTERFACE FUNCTIONS
1299 -- ========================================================================
1302 xterm
= 'x-terminal-emulator',
1303 xlock
= "xscreensaver-command --lock",
1307 -- ------------------------------------------------------------------------
1308 -- write configuration to /ctl wmii file
1309 -- wmii.set_ctl({ "var" = "val", ...})
1310 -- wmii.set_ctl("var, "val")
1311 function set_ctl (first
,second
)
1312 if type(first
) == "table" and second
== nil then
1314 for x
, y
in pairs(first
) do
1315 write ("/ctl", x
.. " " .. y
)
1318 elseif type(first
) == "string" and type(second
) == "string" then
1319 write ("/ctl", first
.. " " .. second
)
1322 error ("expecting a table or two string arguments")
1326 -- ------------------------------------------------------------------------
1327 -- read a value from /ctl wmii file
1328 -- table = wmii.get_ctl()
1329 -- value = wmii.get_ctl("variable"
1330 function get_ctl (name
)
1333 for s
in iread("/ctl") do
1334 local var
,val
= s
:match("(%w+)%s+(.+)")
1346 -- ------------------------------------------------------------------------
1347 -- set an internal wmiirc.lua variable
1348 -- wmii.set_conf({ "var" = "val", ...})
1349 -- wmii.set_conf("var, "val")
1350 function set_conf (first
,second
)
1351 if type(first
) == "table" and second
== nil then
1353 for x
, y
in pairs(first
) do
1357 elseif type(first
) == "string"
1358 and (type(second
) == "string"
1359 or type(second
) == "number") then
1360 config
[first
] = second
1363 error ("expecting a table, or string and string/number as arguments")
1367 -- ------------------------------------------------------------------------
1368 -- read an internal wmiirc.lua variable
1369 function get_conf (name
)
1376 -- ========================================================================
1378 -- ========================================================================
1380 -- the event loop instance
1381 local el
= eventloop
.new()
1383 -- add the core event handler for events
1384 el
:add_exec (wmiir
.. " read /event",
1386 local line
= line
or "nil"
1388 -- try to split off the argument(s)
1389 local ev
,arg
= string.match(line
, "(%S+)%s+(.+)")
1394 -- now locate the handler function and call it
1395 local fn
= ev_handlers
[ev
] or ev_handlers
["*"]
1397 local r
, err
= pcall (fn
, ev
, arg
)
1399 log ("WARNING: " .. tostring(err
))
1404 -- ------------------------------------------------------------------------
1405 -- run the event loop and process events, this function does not exit
1406 function run_event_loop ()
1407 -- stop any other instance of wmiirc
1408 wmixp
:write ("/event", "Start wmiirc " .. tostring(myid
))
1410 log("wmii: updating lbar")
1412 update_displayed_tags ()
1414 log("wmii: updating rbar")
1416 update_displayed_widgets ()
1418 log("wmii: updating active keys")
1420 update_active_keys ()
1422 log("wmii: starting event loop")
1424 local sleep_for
= process_timers()
1425 el
:run_loop(sleep_for
)
1429 -- ========================================================================
1431 -- ========================================================================
1433 api_version
= 0.1 -- the API version we export
1435 plugins
= {} -- all plugins that were loaded
1437 -- ------------------------------------------------------------------------
1438 -- plugin loader which also verifies the version of the api the plugin needs
1440 -- here is what it does
1441 -- - does a manual locate on the file using package.path
1442 -- - reads in the file w/o using the lua interpreter
1443 -- - locates api_version=X.Y string
1444 -- - makes sure that api_version requested can be satisfied
1445 -- - if the plugins is available it will set variables passed in
1446 -- - it then loads the plugin
1448 -- TODO: currently the api_version must be in an X.Y format, but we may want
1449 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1451 function load_plugin(name
, vars
)
1452 local backup_path
= package
.path
or "./?.lua"
1454 log ("loading " .. name
)
1456 -- this is the version we want to find
1457 local api_major
, api_minor
= tostring(api_version
):match("(%d+)%.0*(%d+)")
1458 if (not api_major
) or (not api_minor
) then
1459 log ("WARNING: could not parse api_version in core/wmii.lua")
1463 -- first find the plugin file
1464 local s
, path_match
, full_name
, file
1465 for s
in string.gmatch(plugin_path
, "[^;]+") do
1466 -- try to locate the files locally
1467 local fn
= s
:gsub("%?", name
)
1468 file
= io
.open(fn
, "r")
1479 txt
= file
:read("*all")
1484 log ("WARNING: could not load plugin '" .. name
.. "'")
1488 -- find the api_version line
1489 local line
, plugin_version
1490 for line
in string.gmatch(txt
, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1491 plugin_version
= line
:match("api_version%s*=%s*(%d+%.%d+)%s*")
1492 if plugin_version
then
1497 if not plugin_version
then
1498 log ("WARNING: could not find api_version string in plugin '" .. name
.. "'")
1502 -- decompose the version string
1503 local plugin_major
, plugin_minor
= plugin_version
:match("(%d+)%.0*(%d+)")
1504 if (not plugin_major
) or (not plugin_minor
) then
1505 log ("WARNING: could not parse api_version for '" .. name
.. "' plugin")
1509 -- make a version test
1510 if plugin_major
~= api_major
then
1511 log ("WARNING: " .. name
.. " plugin major version missmatch, is " .. plugin_version
1512 .. " (api " .. tonumber(api_version
) .. ")")
1516 if plugin_minor
> api_minor
then
1517 log ("WARNING: '" .. name
.. "' plugin minor version missmatch, is " .. plugin_version
1518 .. " (api " .. tonumber(api_version
) .. ")")
1522 -- the configuration parameters before loading
1523 if type(vars
) == "table" then
1525 for var
,val
in pairs(vars
) do
1526 local success
= pcall (set_conf
, name
.. "." .. var
, val
)
1528 log ("WARNING: bad variable {" .. tostring(var
) .. ", " .. tostring(val
) .. "} "
1529 .. "given; loading '" .. name
.. "' plugin failed.")
1535 -- actually load the module, but use only the path where we though it should be
1536 package
.path
= path_match
1537 local success
,what
= pcall (require
, name
)
1538 package
.path
= backup_path
1540 log ("WARNING: failed to load '" .. name
.. "' plugin")
1541 log (" - path: " .. tostring(path_match
))
1542 log (" - file: " .. tostring(full_name
))
1543 log (" - plugin's api_version: " .. tostring(plugin_version
))
1544 log (" - reason: " .. tostring(what
))
1549 log ("OK, plugin " .. name
.. " loaded, requested api v" .. plugin_version
)
1550 plugins
[name
] = what
1554 -- ------------------------------------------------------------------------
1559 -- ------------------------------------------------------------------------
1560 -- create a widget object and add it to the wmii /rbar
1563 -- widget = wmii.widget:new ("999_clock")
1564 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1565 function widget
:new (name
, fn
)
1568 if type(name
) == "string" then
1570 if type(fn
) == "function" then
1574 error ("expected name followed by an optional function as arguments")
1577 setmetatable (o
,self
)
1579 self
.__gc
= function (o
) o
:hide() end
1587 -- ------------------------------------------------------------------------
1588 -- stop and destroy the timer
1589 function widget
:delete ()
1590 widgets
[self
.name
] = nil
1594 -- ------------------------------------------------------------------------
1595 -- displays or updates the widget text
1599 -- w:show("foo", "#888888 #222222 #333333")
1600 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1602 function widget
:show (txt
, colors
)
1603 local colors
= colors
or get_ctl("normcolors") or ""
1604 local txt
= txt
or self
.txt
or ""
1607 towrite
= colors
.. " " .. towrite
1609 if not self
.txt
then
1610 create ("/rbar/" .. self
.name
, towrite
)
1612 write ("/rbar/" .. self
.name
, towrite
)
1617 -- ------------------------------------------------------------------------
1618 -- hides a widget and removes it from the bar
1619 function widget
:hide ()
1621 remove ("/lbar/" .. self
.name
)
1629 =item widget:add_event_handler (ev, fn)
1631 Add an event handler callback for this widget, using I<fn> for event I<ev>
1636 function widget
:add_event_handler (ev
, fn
)
1637 add_widget_event_handler( self
.name
, ev
, fn
)
1641 -- ------------------------------------------------------------------------
1642 -- remove all /rbar entries that we don't have widget objects for
1643 function update_displayed_widgets ()
1644 -- colours for /rbar
1645 local nc
= get_ctl("normcolors") or ""
1647 -- build up a table of existing tags in the /lbar
1650 for s
in wmixp
:idir ("/rbar") do
1654 -- for all actual widgets in use we want to remove them from the old list
1656 for i
,v
in pairs(widgets
) do
1660 -- anything left in the old table should be removed now
1661 for i
,v
in pairs(old
) do
1668 -- ------------------------------------------------------------------------
1669 -- create a new program and for each line it generates call the callback function
1670 -- returns fd which can be passed to kill_exec()
1671 function add_exec (command
, callback
)
1672 return el
:add_exec (command
, callback
)
1675 -- ------------------------------------------------------------------------
1676 -- terminates a program spawned off by add_exec()
1677 function kill_exec (fd
)
1678 return el
:kill_exec (fd
)
1681 -- ------------------------------------------------------------------------
1686 -- ------------------------------------------------------------------------
1687 -- create a timer object and add it to the event loop
1690 -- timer:new (my_timer_fn)
1691 -- timer:new (my_timer_fn, 15)
1692 function timer
:new (fn
, seconds
)
1695 if type(fn
) == "function" then
1698 error ("expected function followed by an optional number as arguments")
1701 setmetatable (o
,self
)
1703 self
.__gc
= function (o
) o
:stop() end
1706 timers
[#timers
+1] = o
1714 -- ------------------------------------------------------------------------
1715 -- stop and destroy the timer
1716 function timer
:delete ()
1719 for i
,t
in pairs(timers
) do
1721 table.remove (timers
,i
)
1727 -- ------------------------------------------------------------------------
1728 -- run the timer given new interval
1729 function timer
:resched (seconds
)
1730 local seconds
= seconds
or self
.interval
1731 if not (type(seconds
) == "number") then
1732 error ("timer:resched expected number as argument")
1735 local now
= tonumber(os
.date("%s"))
1737 self
.interval
= seconds
1738 self
.next_time
= now
+ seconds
1740 -- resort the timer list
1741 table.sort (timers
, timer
.is_less_then
)
1744 -- helper for sorting timers
1745 function timer
:is_less_then(another
)
1746 if not self
.next_time
then
1747 return false -- another is smaller, nil means infinity
1749 elseif not another
.next_time
then
1750 return true -- self is smaller, nil means infinity
1752 elseif self
.next_time
< another
.next_time
then
1753 return true -- self is smaller than another
1756 return false -- another is smaller then self
1759 -- ------------------------------------------------------------------------
1761 function timer
:stop ()
1762 self
.next_time
= nil
1764 -- resort the timer list
1765 table.sort (timers
, timer
.is_less_then
)
1768 -- ------------------------------------------------------------------------
1769 -- figure out how long before the next event
1770 function time_before_next_timer_event()
1771 local tmr
= timers
[1]
1772 if tmr
and tmr
.next_time
then
1773 local now
= tonumber(os
.date("%s"))
1774 local seconds
= tmr
.next_time
- now
1779 return 0 -- sleep for ever
1782 -- ------------------------------------------------------------------------
1783 -- handle outstanding events
1784 function process_timers ()
1785 local now
= tonumber(os
.date("%s"))
1789 for i
,tmr
in pairs (timers
) do
1791 -- prune out removed timers
1792 table.remove(timers
,i
)
1795 elseif not tmr
.next_time
then
1796 -- break out once we find a timer that is stopped
1799 elseif tmr
.next_time
> now
then
1800 -- break out once we get to the future
1804 -- this one is good to go
1805 torun
[#torun
+1] = tmr
1808 for i
,tmr
in pairs (torun
) do
1810 local status
,new_interval
= pcall (tmr
.fn
, tmr
)
1812 new_interval
= new_interval
or self
.interval
1813 if new_interval
and (new_interval
~= -1) then
1814 tmr
:resched(new_interval
)
1817 log ("ERROR: " .. tostring(new_interval
))
1821 local sleep_for
= time_before_next_timer_event()
1825 -- ------------------------------------------------------------------------
1826 -- cleanup everything in preparation for exit() or exec()
1831 log ("wmii: stopping timer events")
1833 for i
,tmr
in pairs (timers
) do
1834 pcall (tmr
.delete
, tmr
)
1838 log ("wmii: terminating eventloop")
1840 pcall(el
.kill_all
,el
)
1842 log ("wmii: disposing of widgets")
1844 -- dispose of all widgets
1845 for i
,v
in pairs(widgets
) do
1850 -- FIXME: it doesn't seem to do what I want
1852 log ("wmii: releasing plugins")
1854 for i,p in pairs(plugins) do
1856 pcall (p.cleanup, p)
1862 log ("wmii: dormant")
1865 -- ========================================================================
1867 -- ========================================================================
1870 -- Notes on client tracking
1872 -- When a client is created wmii sends us a CreateClient message, and
1873 -- we in turn create a 'client' object and store it in the 'clients'
1874 -- table indexed by the client's ID.
1876 -- Each client object stores the following:
1877 -- .xid - the X client ID
1878 -- .pid - the process ID
1879 -- .prog - program object representing the process
1881 -- The client and program objects track the following modes for each program:
1884 -- - for each client window
1885 -- - Mod4-space toggles the state between normal and raw
1886 -- - Mod1-f raw also toggles the state
1887 -- - in raw mode all input goes to the client, except for Mod4-space
1888 -- - a focused client with raw mode enabled is put into raw mode
1891 -- - for each program
1892 -- - Mod1-f suspend toggles the state for current client's program
1893 -- - a focused client, whose program was previous suspended is resumed
1894 -- - an unfocused client, with suspend enabled, will be suspended
1895 -- - suspend/resume is done by sending the STOP/CONT signals to the PID
1898 function xid_to_pid (xid
)
1899 local cmd
= "xprop -id " .. tostring(xid
) .. " _NET_WM_PID"
1900 local file
= io
.popen (cmd
)
1901 local out
= file
:read("*a")
1903 local pid
= out
:match("^_NET_WM_PID.*%s+=%s+(%d+)%s+$")
1904 return tonumber(pid
)
1907 local focused_xid
= nil
1908 local clients
= {} -- table of client objects indexed by xid
1909 local programs
= {} -- table of program objects indexed by pid
1910 local mode_widget
= widget
:new ("999_client_mode")
1912 -- make programs table have weak values
1913 -- programs go away as soon as no clients point to it
1914 local programs_mt
= {}
1915 setmetatable(programs
, programs_mt
)
1916 programs_mt
.__mode
= 'v'
1920 function program
:new (pid
)
1923 setmetatable (o
,self
)
1925 self
.__gc
= function (old
) old
:cont() end
1926 -- initialize the new object
1930 o
.suspend
.toggle
= function (prog
)
1931 prog
.suspend
.enabled
= not prog
.suspend
.enabled
1933 o
.suspend
.enabled
= false -- if true, defocusing suspends (SIGSTOP)
1934 o
.suspend
.active
= true -- if true, focusing resumes (SIGCONT)
1938 function program
:stop ()
1939 if not self
.suspend
.active
then
1940 local cmd
= "kill -STOP " .. tostring(self
.pid
)
1941 log (" executing: " .. cmd
)
1943 self
.suspend
.active
= true
1947 function program
:cont ()
1948 if self
.suspend
.active
then
1949 local cmd
= "kill -CONT " .. tostring(self
.pid
)
1950 log (" executing: " .. cmd
)
1952 self
.suspend
.active
= false
1956 function get_program (pid
)
1957 local prog
= programs
[pid
]
1959 prog
= program
:new (pid
)
1960 programs
[pid
] = prog
1967 function client
:new (xid
)
1970 setmetatable (o
,self
)
1971 self
.__index
= function (t
,k
)
1972 if k
== 'suspend' then -- suspend mode is tracked per program
1973 return t
.prog
.suspend
1977 self
.__gc
= function (old
) old
.prog
=nil end
1978 -- initialize the new object
1980 o
.pid
= xid_to_pid(xid
)
1981 o
.prog
= get_program (o
.pid
)
1984 o
.raw
.toggle
= function (cli
)
1985 cli
.raw
.enabled
= not cli
.raw
.enabled
1988 o
.raw
.enabled
= false -- if true, raw mode enabled when client is focused
1992 function client
:stop ()
1993 if self
.suspend
.enabled
then
1998 function client
:cont ()
2002 function client
:set_raw_mode()
2003 if not self
or not self
.raw
.enabled
then -- normal mode
2004 update_active_keys ()
2006 write ("/keys", "Mod4-space")
2010 function client
:toggle(what
)
2011 if what
and self
[what
] then
2012 local ctl
= self
[what
]
2016 log ("xid=" .. tostring (xid
)
2017 .. " pid=" .. tostring (self
.pid
) .. " (" .. tostring (self
.prog
.pid
) .. ")"
2018 .. " what=" .. tostring (what
)
2019 .. " enabled=" .. tostring(ctl
["enabled"]))
2021 mode_widget
:show (self
:flags_string())
2024 function client
:flags_string()
2026 if self
.suspend
.enabled
then ret
= ret
.. "s" else ret
= ret
.. "-" end
2027 if self
.raw
.enabled
then ret
= ret
.. "r" else ret
= ret
.. "-" end
2031 function get_client (xid
)
2032 local xid
= xid
or wmixp
:read("/client/sel/ctl")
2033 local cli
= clients
[xid
]
2035 cli
= client
:new (xid
)
2041 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2042 function client_created (xid
)
2043 log ("-client_created " .. tostring(xid
))
2044 return get_client(xid
)
2047 function client_destoryed (xid
)
2048 log ("-client_destoryed " .. tostring(xid
))
2049 if clients
[xid
] then
2050 local cli
= clients
[xid
]
2052 log (" del pid: " .. tostring(cli
.pid
))
2057 function client_focused (xid
)
2058 log ("-client_focused " .. tostring(xid
))
2059 -- do nothing if the same xid
2060 if focused_xid
== xid
then
2064 local old
= clients
[focused_xid
]
2065 local new
= get_client(xid
)
2067 -- handle raw mode switch
2068 if not old
or ( old
and new
and old
.raw
.enabled
~= new
.raw
.enabled
) then
2072 -- do nothing if the same pid
2073 if old
and new
and old
.pid
== new
.pid
then
2074 mode_widget
:show (new
:flags_string())
2080 log (" old pid: " .. tostring(old.pid)
2081 .. " xid: " .. tostring(old.xid)
2082 .. " flags: " .. old:flags_string())
2089 log (" new pid: " .. tostring(new.pid)
2090 .. " xid: " .. tostring(new.xid)
2091 .. " flags: " .. new:flags_string())
2096 mode_widget
:show (new
:flags_string())
2102 -- ========================================================================
2104 -- ========================================================================
2117 Used to determine location of wmii's listen socket.
2123 L<wmii(1)>, L<lua(1)>
2127 Bart Trojanowski B<< <bart@jukie.net> >>
2129 =head1 COPYRIGHT AND LICENSE
2131 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
2133 This is free software. You may redistribute copies of it under the terms of
2134 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
2135 is NO WARRANTY, to the extent permitted by law.