use V=1 to make Makefile verbose
[wmiirc-lua.git] / src / core / wmii.lua
blob8d98924a1dae9b364921f2215c3d53548d1d61f4
1 --
2 -- Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
3 --
4 -- WMII event loop, in lua
5 --
6 -- http://www.jukie.net/~bart/blog/tag/wmiirc-lua
7 -- git://www.jukie.net/wmiirc-lua.git/
8 --
10 -- ========================================================================
11 -- DOCUMENTATION
12 -- ========================================================================
13 --[[
14 =pod
16 =head1 NAME
18 wmii.lua - WMII event-loop methods in lua
20 =head1 SYNOPSIS
22 require "wmii"
24 -- Write something to the wmii filesystem, in this case a key event.
25 wmii.write ("/event", "Key Mod1-j")
27 -- Set your wmii /ctl parameters
28 wmii.set_ctl({
29 font = '....'
32 -- Configure wmii.lua parameters
33 wmii.set_conf ({
34 xterm = 'x-terminal-emulator'
37 -- Now start the event loop
38 wmii.run_event_loop()
40 =head1 DESCRIPTION
42 wmii.lua provides methods for replacing the stock sh-based wmiirc shipped with
43 wmii 3.6 and newer with a lua-based event loop.
45 It should be used by your wmiirc
47 =head1 METHODS
49 =over 4
51 =cut
52 --]]
54 -- ========================================================================
55 -- MODULE SETUP
56 -- ========================================================================
58 local wmiidir = os.getenv("HOME") .. "/.wmii-3.5"
59 local wmiirc = wmiidir .. "/wmiirc"
61 package.path = wmiidir .. "/core/?.lua;" ..
62 wmiidir .. "/plugins/?.lua;" ..
63 package.path
64 package.cpath = wmiidir .. "/core/?.so;" ..
65 wmiidir .. "/plugins/?.so;" ..
66 package.cpath
68 local ixp = require "ixp"
69 local eventloop = require "eventloop"
70 local history = require "history"
72 local io = require("io")
73 local os = require("os")
74 local string = require("string")
75 local table = require("table")
76 local math = require("math")
77 local type = type
78 local error = error
79 local print = print
80 local pcall = pcall
81 local pairs = pairs
82 local package = package
83 local require = require
84 local tostring = tostring
85 local tonumber = tonumber
86 local setmetatable = setmetatable
88 -- kinda silly, but there is no working liblua5.1-posix0 in ubuntu
89 -- so we make it optional
90 local have_posix, posix = pcall(require,"posix")
92 module("wmii")
94 -- get the process id
95 local myid
96 if have_posix then
97 -- but having posix is not enough as the API changes, so we try each one
98 if posix.getprocessid then
99 local stat,rc = pcall (posix.getprocessid, "pid")
100 if stat then
101 myid = rc
104 if not myid and posix.getpid then
105 local stat,rc = pcall (posix.getpid, "pid")
106 if stat then
107 myid = rc
111 if not myid then
112 -- we were not able to get the PID, but we can create a random number
113 local now = tonumber(os.date("%s"))
114 math.randomseed(now)
115 myid = math.random(10000)
118 -- ========================================================================
119 -- MODULE VARIABLES
120 -- ========================================================================
122 -- wmiir points to the wmiir executable
123 -- TODO: need to make sure that wmiir is in path, and if not find it
124 local wmiir = "wmiir"
126 -- wmii_adr is the address we use when connecting using ixp
127 local wmii_adr = os.getenv("WMII_ADDRESS")
128 or ("unix!/tmp/ns." .. os.getenv("USER") .. "."
129 .. os.getenv("DISPLAY"):match("(:%d+)") .. "/wmii")
131 -- wmixp is the ixp context we use to talk to wmii
132 local wmixp = ixp.new(wmii_adr)
134 -- history of previous views, view_hist[#view_hist] is the last one
135 local view_hist = {} -- sorted with 1 being the oldest
136 local view_hist_max = 50 -- max number to keep track of
138 -- allow for a client to be forced to a tag
139 local next_client_goes_to_tag = nil
141 -- program and action histories
142 local prog_hist = history.new (20)
143 local action_hist = history.new(10)
145 -- where to find plugins
146 plugin_path = os.getenv("HOME") .. "/.wmii-3.5/plugins/?.so;"
147 .. os.getenv("HOME") .. "/.wmii-3.5/plugins/?.lua;"
148 .. "/usr/local/lib/lua/5.1/wmii/?.so;"
149 .. "/usr/local/share/lua/5.1/wmii/?.lua;"
150 .. "/usr/lib/lua/5.1/wmii/?.so;"
151 .. "/usr/share/lua/5.1/wmii/?.lua"
153 -- where to find wmiirc (see find_wmiirc())
154 wmiirc_path = os.getenv("HOME") .. "/.wmii-3.5/wmiirc.lua;"
155 .. os.getenv("HOME") .. "/.wmii-3.5/wmiirc;"
156 .. "/etc/X11/wmii-3.5/wmiirc.lua;"
157 .. "/etc/X11/wmii-3.5/wmiirc"
159 -- ========================================================================
160 -- LOCAL HELPERS
161 -- ========================================================================
163 --[[
164 =pod
166 =item log ( str )
168 Log the message provided in C<str>
170 Currently just writes to io.stderr
172 =cut
173 --]]
174 function log (str)
175 if get_conf("debug") then
176 io.stderr:write (str .. "\n")
180 --[[
181 =pod
183 =item find_wmiirc ( )
185 Locates the wmiirc script. It looks in ~/.wmii-3.5 and /etc/X11/wmii-3.5
186 for the first lua script bearing the name wmiirc.lua or wmiirc. Returns
187 first match.
189 =cut
190 --]]
191 function find_wmiirc()
192 local fn
193 for fn in string.gmatch(wmiirc_path, "[^;]+") do
194 -- try to locate the files locally
195 local file = io.open(fn, "r")
196 if file then
197 local txt = file:read("*line")
198 file:close()
199 if type(txt) == 'string' and txt:match("lua") then
200 return fn
204 return nil
208 -- ========================================================================
209 -- MAIN ACCESS FUNCTIONS
210 -- ========================================================================
212 --[[
213 =pod
215 =item ls ( dir, fmt )
217 List the wmii filesystem directory provided in C<dir>, in the format specified
218 by C<fmt>.
220 Returns an iterator of TODO
222 =cut
223 --]]
224 function ls (dir, fmt)
225 local verbose = fmt and fmt:match("l")
227 local s = wmixp:stat(dir)
228 if not s then
229 return function () return nil end
231 if s.modestr:match("^[^d]") then
232 return function ()
233 return stat2str(verbose, s)
237 local itr = wmixp:idir (dir)
238 if not itr then
239 --return function ()
240 return nil
241 --end
245 return function ()
246 local s = itr()
247 if s then
248 return stat2str(verbose, s)
250 return nil
254 local function stat2str(verbose, stat)
255 if verbose then
256 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
257 else
258 if stat.modestr:match("^d") then
259 return stat.name .. "/"
260 else
261 return stat.name
266 -- ------------------------------------------------------------------------
267 -- read all contents of a wmii virtual file
268 function read (file)
269 return wmixp:read (file)
272 -- ------------------------------------------------------------------------
273 -- return an iterator which walks all the lines in the file
275 -- example:
276 -- for event in wmii.iread("/ctl")
277 -- ...
278 -- end
280 -- NOTE: don't use iread for files that could block, as this will interfere
281 -- with timer processing and event delivery. Instead fork off a process to
282 -- execute wmiir and read back the responses via callback.
283 function iread (file)
284 return wmixp:iread(file)
287 -- ------------------------------------------------------------------------
288 -- create a wmii file, optionally write data to it
289 function create (file, data)
290 wmixp:create(file, data)
293 -- ------------------------------------------------------------------------
294 -- remove a wmii file
295 function remove (file)
296 wmixp:remove(file)
299 -- ------------------------------------------------------------------------
300 -- write a value to a wmii virtual file system
301 function write (file, value)
302 wmixp:write (file, value)
305 -- ------------------------------------------------------------------------
306 -- setup a table describing the menu command
307 local function menu_cmd (prompt)
308 local cmdt = { wmiir, "setsid", "dmenu", "-b" }
309 local fn = get_ctl("font")
310 if fn then
311 cmdt[#cmdt+1] = "-fn"
312 cmdt[#cmdt+1] = fn
314 local normcolors = get_ctl("normcolors")
315 if normcolors then
316 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
317 if nf then
318 cmdt[#cmdt+1] = "-nf"
319 cmdt[#cmdt+1] = "'" .. nf .. "'"
321 if nb then
322 cmdt[#cmdt+1] = "-nb"
323 cmdt[#cmdt+1] = "'" .. nb .. "'"
326 local focuscolors = get_ctl("focuscolors")
327 if focuscolors then
328 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
329 if sf then
330 cmdt[#cmdt+1] = "-sf"
331 cmdt[#cmdt+1] = "'" .. sf .. "'"
333 if sb then
334 cmdt[#cmdt+1] = "-sb"
335 cmdt[#cmdt+1] = "'" .. sb .. "'"
338 if prompt then
339 cmdt[#cmdt+1] = "-p"
340 cmdt[#cmdt+1] = "'" .. prompt .. "'"
343 return cmdt
346 -- ------------------------------------------------------------------------
347 -- displays the menu given an table of entires, returns selected text
348 function menu (tbl, prompt)
349 local menu = menu_cmd(prompt)
351 local infile = os.tmpname()
352 local fh = io.open (infile, "w+")
354 local i,v
355 for i,v in pairs(tbl) do
356 if type(i) == 'number' and type(v) == 'string' then
357 fh:write (v)
358 else
359 fh:write (i)
361 fh:write ("\n")
363 fh:close()
365 local outfile = os.tmpname()
367 menu[#menu+1] = "<"
368 menu[#menu+1] = infile
369 menu[#menu+1] = ">"
370 menu[#menu+1] = outfile
372 local cmd = table.concat(menu," ")
373 os.execute (cmd)
375 fh = io.open (outfile, "r")
376 os.remove (outfile)
378 local sel = fh:read("*l")
379 fh:close()
381 return sel
384 -- ------------------------------------------------------------------------
385 -- displays the a tag selection menu, returns selected tag
386 function tag_menu ()
387 local tags = get_tags()
389 return menu(tags, "tag:")
392 -- ------------------------------------------------------------------------
393 -- displays the a program menu, returns selected program
394 function prog_menu ()
395 local menu = menu_cmd("cmd:")
397 local outfile = os.tmpname()
399 menu[#menu+1] = ">"
400 menu[#menu+1] = outfile
402 local hstt = { }
403 for n in prog_hist:walk_reverse_unique() do
404 hstt[#hstt+1] = "echo '" .. n .. "' ; "
407 local cmd = "(" .. table.concat(hstt)
408 .. "dmenu_path ) |"
409 .. table.concat(menu," ")
410 os.execute (cmd)
412 local fh = io.open (outfile, "rb")
413 os.remove (outfile)
415 local prog = fh:read("*l")
416 io.close (fh)
418 return prog
421 -- ------------------------------------------------------------------------
422 -- returns a table of sorted tags names
423 function get_tags()
424 local t = {}
425 local s
426 for s in wmixp:idir ("/tag") do
427 if s.name and not (s.name == "sel") then
428 t[#t + 1] = s.name
431 table.sort(t)
432 return t
435 -- ------------------------------------------------------------------------
436 -- returns a table of sorted screen names
437 function get_screens()
438 local t = {}
439 local s
440 local empty = true
441 for s in wmixp:idir ("/screen") do
442 if s.name and not (s.name == "sel") then
443 t[#t + 1] = s.name
444 empty = false
447 if empty then
448 return nil
450 table.sort(t)
451 return t
454 -- ------------------------------------------------------------------------
455 -- returns current view, on current screen or specified screen
456 function get_view(screen)
457 return get_screen_ctl(screen, "view") or get_ctl("view")
460 -- ------------------------------------------------------------------------
461 -- changes the current view to the name given
462 function set_view(sel)
463 local cur = get_view()
464 local all = get_tags()
466 if #all < 2 or sel == cur then
467 -- nothing to do if we have less then 2 tags
468 return
471 if not (type(sel) == "string") then
472 error ("string argument expected")
475 -- set new view
476 write ("/ctl", "view " .. sel)
479 -- ------------------------------------------------------------------------
480 -- changes the current view to the index given
481 function set_view_index(sel)
482 local cur = get_view()
483 local all = get_tags()
485 if #all < 2 then
486 -- nothing to do if we have less then 2 tags
487 return
490 local num = tonumber (sel)
491 if not num then
492 error ("number argument expected")
495 local name = all[sel]
496 if not name or name == cur then
497 return
500 -- set new view
501 write ("/ctl", "view " .. name)
504 -- ------------------------------------------------------------------------
505 -- chnages to current view by offset given
506 function set_view_ofs(jump)
507 local cur = get_view()
508 local all = get_tags()
510 if #all < 2 then
511 -- nothing to do if we have less then 2 tags
512 return
515 -- range check
516 if (jump < - #all) or (jump > #all) then
517 error ("view selector is out of range")
520 -- find the one that's selected index
521 local curi = nil
522 local i,v
523 for i,v in pairs (all) do
524 if v == cur then curi = i end
527 -- adjust by index
528 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
529 if (newi < - #all) or (newi > #all) then
530 error ("error computng new view")
533 write ("/ctl", "view " .. all[newi])
536 -- ------------------------------------------------------------------------
537 -- toggle between last view and current view
538 function toggle_view()
539 local last = view_hist[#view_hist]
540 if last then
541 set_view(last)
545 -- ========================================================================
546 -- ACTION HANDLERS
547 -- ========================================================================
549 local action_handlers = {
550 man = function (act, args)
551 local xterm = get_conf("xterm") or "xterm"
552 local page = args
553 if (not page) or (not page:match("%S")) then
554 page = wmiidir .. "/wmii.3lua"
556 local cmd = xterm .. " -e man " .. page .. " &"
557 log (" executing: " .. cmd)
558 os.execute (wmiir .. " setsid " .. cmd)
559 end,
561 quit = function ()
562 write ("/ctl", "quit")
563 end,
565 exec = function (act, args)
566 local what = args or "wmii"
567 log (" asking wmii to exec " .. tostring(what))
568 cleanup()
569 write ("/ctl", "exec " .. what)
570 end,
572 xlock = function (act)
573 local cmd = get_conf("xlock") or "xscreensaver-command --lock"
574 os.execute (wmiir .. " setsid " .. cmd)
575 end,
577 wmiirc = function ()
578 if have_posix then
579 local wmiirc = find_wmiirc()
580 if wmiirc then
581 log (" executing: lua " .. wmiirc)
582 cleanup()
583 posix.exec (wmiirc)
584 posix.exec ("/bin/sh", "-c", "exec lua wmiirc")
585 posix.exec ("/usr/bin/lua", wmiirc)
587 else
588 log("sorry cannot restart; you don't have lua's posix library.")
590 end,
592 urgent = function ()
593 wmixp:write ("/client/sel/ctl", "Urgent toggle")
594 end,
596 --[[
597 rehash = function ()
598 -- TODO: consider storing list of executables around, and
599 -- this will then reinitialize that list
600 log (" TODO: rehash")
601 end,
603 status = function ()
604 -- TODO: this should eventually update something on the /rbar
605 log (" TODO: status")
606 end,
607 --]]
610 --[[
611 =pod
613 =item add_action_handler (action, fn)
615 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
617 =cut
618 --]]
619 function add_action_handler (action, fn)
621 if type(action) ~= "string" or type(fn) ~= "function" then
622 error ("expecting a string and a function")
625 if action_handlers[action] then
626 error ("action handler already exists for '" .. action .. "'")
629 action_handlers[action] = fn
632 --[[
633 =pod
635 =item remove_action_handler (action)
637 Remove an action handler callback function for the given action string I<action>.
639 =cut
640 --]]
641 function remove_action_handler (action)
643 action_handlers[action] = nil
646 -- ========================================================================
647 -- KEY HANDLERS
648 -- ========================================================================
650 function ke_fullscreen_toggle()
651 wmixp:write ("/client/sel/ctl", "Fullscreen toggle")
654 function ke_view_starting_with_letter (letter)
655 local i,v
657 -- find the view name in history in reverse order
658 for i=#view_hist,1,-1 do
659 v = view_hist[i]
660 if letter == v:sub(1,1) then
661 set_view(v)
662 return true
666 -- otherwise just pick the first view that matches
667 local all = get_tags()
668 for i,v in pairs(all) do
669 if letter == v:sub(1,1) then
670 set_view_index (i)
671 return true
675 return false
678 function ke_handle_action()
679 local actions = { }
680 local seen = {}
682 local n
683 for n in action_hist:walk_reverse() do
684 if not seen[n] then
685 actions[#actions+1] = n
686 seen[n] = 1
690 local v
691 for n,v in pairs(action_handlers) do
692 if not seen[n] then
693 actions[#actions+1] = n
694 seen[n] = 1
698 local text = menu(actions, "action:")
699 if text then
700 log ("Action: " .. text)
701 local act = text
702 local args = nil
703 local si = text:find("%s")
704 if si then
705 act,args = string.match(text .. " ", "(%w+)%s(.+)")
707 if act then
708 local fn = action_handlers[act]
709 if fn then
710 action_hist:add (act)
711 local r, err = pcall (fn, act, args)
712 if not r then
713 log ("WARNING: " .. tostring(err))
721 local key_handlers = {
722 ["*"] = function (key)
723 log ("*: " .. key)
724 end,
726 -- execution and actions
727 ["Mod1-Return"] = function (key)
728 local xterm = get_conf("xterm") or "xterm"
729 log (" executing: " .. xterm)
730 os.execute (wmiir .. " setsid " .. xterm .. " &")
731 end,
732 ["Mod1-Shift-Return"] = function (key)
733 local tag = tag_menu()
734 if tag then
735 local xterm = get_conf("xterm") or "xterm"
736 log (" executing: " .. xterm .. " on: " .. tag)
737 next_client_goes_to_tag = tag
738 os.execute (wmiir .. " setsid " .. xterm .. " &")
740 end,
741 ["Mod1-a"] = function (key)
742 ke_handle_action()
743 end,
744 ["Mod1-p"] = function (key)
745 local prog = prog_menu()
746 if prog then
747 prog_hist:add(prog:match("([^ ]+)"))
748 log (" executing: " .. prog)
749 os.execute (wmiir .. " setsid " .. prog .. " &")
751 end,
752 ["Mod1-Shift-p"] = function (key)
753 local tag = tag_menu()
754 if tag then
755 local prog = prog_menu()
756 if prog then
757 log (" executing: " .. prog .. " on: " .. tag)
758 next_client_goes_to_tag = tag
759 os.execute (wmiir .. " setsid " .. prog .. " &")
762 end,
763 ["Mod1-Shift-c"] = function (key)
764 write ("/client/sel/ctl", "kill")
765 end,
767 -- HJKL active selection
768 ["Mod1-h"] = function (key)
769 write ("/tag/sel/ctl", "select left")
770 end,
771 ["Mod1-l"] = function (key)
772 write ("/tag/sel/ctl", "select right")
773 end,
774 ["Mod1-j"] = function (key)
775 write ("/tag/sel/ctl", "select down")
776 end,
777 ["Mod1-k"] = function (key)
778 write ("/tag/sel/ctl", "select up")
779 end,
781 -- HJKL movement
782 ["Mod1-Shift-h"] = function (key)
783 write ("/tag/sel/ctl", "send sel left")
784 end,
785 ["Mod1-Shift-l"] = function (key)
786 write ("/tag/sel/ctl", "send sel right")
787 end,
788 ["Mod1-Shift-j"] = function (key)
789 write ("/tag/sel/ctl", "send sel down")
790 end,
791 ["Mod1-Shift-k"] = function (key)
792 write ("/tag/sel/ctl", "send sel up")
793 end,
795 -- floating vs tiled
796 ["Mod1-space"] = function (key)
797 write ("/tag/sel/ctl", "select toggle")
798 end,
799 ["Mod1-Shift-space"] = function (key)
800 write ("/tag/sel/ctl", "send sel toggle")
801 end,
803 -- work spaces (# and @ are wildcards for numbers and letters)
804 ["Mod4-#"] = function (key, num)
805 -- first attempt to find a view that starts with the number requested
806 local num_str = tostring(num)
807 if not ke_view_starting_with_letter (num_str) then
808 -- if we fail, then set it to the index requested
809 set_view_index (num)
811 end,
812 ["Mod4-Shift-#"] = function (key, num)
813 write ("/client/sel/tags", tostring(num))
814 end,
815 ["Mod4-@"] = function (key, letter)
816 ke_view_starting_with_letter (letter)
817 end,
818 ["Mod4-Shift-@"] = function (key, letter)
819 local all = get_tags()
820 local i,v
821 for i,v in pairs(all) do
822 if letter == v:sub(1,1) then
823 write ("/client/sel/tags", v)
824 break
827 end,
828 ["Mod1-comma"] = function (key)
829 set_view_ofs (-1)
830 end,
831 ["Mod1-period"] = function (key)
832 set_view_ofs (1)
833 end,
834 ["Mod1-r"] = function (key)
835 -- got to the last view
836 toggle_view()
837 end,
839 -- switching views and retagging
840 ["Mod1-t"] = function (key)
841 -- got to a view
842 local tag = tag_menu()
843 if tag then
844 set_view (tag)
846 end,
847 ["Mod1-Shift-t"] = function (key)
848 -- move selected client to a tag
849 local tag = tag_menu()
850 if tag then
851 write ("/client/sel/tags", tag)
853 end,
854 ["Mod1-Shift-r"] = function (key)
855 -- move selected client to a tag, and follow
856 local tag = tag_menu()
857 if tag then
858 -- get the current window id
859 local xid = wmixp:read("/client/sel/ctl") or ""
861 -- modify the tag
862 write("/client/sel/tags", tag)
864 -- if the client is still in this tag, then
865 -- it might have been a regexp tag... check
866 local test = wmixp:read("/client/sel/ctl")
867 if not test or test ~= xid then
868 -- if the window moved, follow it
869 set_view(tag)
872 end,
873 ["Mod1-Control-t"] = function (key)
874 log (" TODO: Mod1-Control-t: " .. key)
875 end,
877 -- column modes
878 ["Mod1-d"] = function (key)
879 write("/tag/sel/ctl", "colmode sel default-max")
880 end,
881 ["Mod1-s"] = function (key)
882 write("/tag/sel/ctl", "colmode sel stack-max")
883 end,
884 ["Mod1-m"] = function (key)
885 write("/tag/sel/ctl", "colmode sel stack+max")
886 end,
887 ["Mod1-f"] = function (key)
888 ke_fullscreen_toggle()
889 end,
891 -- changing client flags
892 ["Shift-Mod1-f"] = function (key)
893 log ("setting flags")
895 local cli = get_client ()
897 local flags = { "suspend", "raw" }
898 local current_flags = cli:flags_string()
900 local what = menu(flags, "current flags: " .. current_flags .. " toggle:")
902 cli:toggle(what)
903 end,
904 ["Mod4-space"] = function (key)
905 local cli = get_client ()
906 cli:toggle("raw")
907 end,
910 --[[
911 =pod
913 =item add_key_handler (key, fn)
915 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
917 =cut
918 --]]
919 function add_key_handler (key, fn)
921 if type(key) ~= "string" or type(fn) ~= "function" then
922 error ("expecting a string and a function")
925 if key_handlers[key] then
926 -- TODO: we may wish to allow multiple handlers for one keypress
927 error ("key handler already exists for '" .. key .. "'")
930 key_handlers[key] = fn
933 --[[
934 =pod
936 =item remove_key_handler (key)
938 Remove an key handler callback function for the given key I<key>.
940 Returns the handler callback function.
942 =cut
943 --]]
944 function remove_key_handler (key)
946 local fn = key_handlers[key]
947 key_handlers[key] = nil
948 return fn
951 --[[
952 =pod
954 =item remap_key_handler (old_key, new_key)
956 Remove a key handler callback function from the given key I<old_key>,
957 and assign it to a new key I<new_key>.
959 =cut
960 --]]
961 function remap_key_handler (old_key, new_key)
963 local fn = remove_key_handler(old_key)
965 return add_key_handler (new_key, fn)
969 -- ------------------------------------------------------------------------
970 -- update the /keys wmii file with the list of all handlers
971 local alphabet="abcdefghijklmnopqrstuvwxyz"
972 function update_active_keys ()
973 local t = {}
974 local x, y
975 for x,y in pairs(key_handlers) do
976 if x:find("%w") then
977 local i = x:find("#$")
978 if i then
979 local j
980 for j=0,9 do
981 t[#t + 1] = x:sub(1,i-1) .. j
983 else
984 i = x:find("@$")
985 if i then
986 local j
987 for j=1,alphabet:len() do
988 local a = alphabet:sub(j,j)
989 t[#t + 1] = x:sub(1,i-1) .. a
991 else
992 t[#t + 1] = tostring(x)
997 local all_keys = table.concat(t, "\n")
998 --log ("setting /keys to...\n" .. all_keys .. "\n");
999 write ("/keys", all_keys)
1002 -- ------------------------------------------------------------------------
1003 -- update the /lbar wmii file with the current tags
1004 function update_displayed_tags ()
1005 -- list of all screens
1006 local screens = get_screens()
1007 if not screens then
1008 update_displayed_tags_on_screen()
1009 return
1012 local i, s
1013 for i,s in pairs(screens) do
1014 update_displayed_tags_on_screen(s)
1018 function update_displayed_tags_on_screen(s)
1019 local lbar = "/lbar"
1020 if s then
1021 lbar = "/screen/" .. s .. "/lbar"
1024 -- colours for screen
1025 local fc = get_screen_ctl(s, "focuscolors") or get_ctl("focuscolors") or ""
1026 local nc = get_screen_ctl(s, "normcolors") or get_ctl("normcolors") or ""
1028 -- build up a table of existing tags in the /lbar
1029 local old = {}
1030 local ent
1031 for ent in wmixp:idir (lbar) do
1032 old[ent.name] = 1
1035 -- for all actual tags in use create any entries in /lbar we don't have
1036 -- clear the old table entries if we have them
1037 local cur = get_view(s)
1038 local all = get_tags()
1039 local i,v
1040 for i,v in pairs(all) do
1041 local color = nc
1042 if cur == v then
1043 color = fc
1045 if not old[v] then
1046 create (lbar .. "/" .. v, color .. " " .. v)
1048 write (lbar .. "/" .. v, color .. " " .. v)
1049 old[v] = nil
1052 -- anything left in the old table should be removed now
1053 for i,v in pairs(old) do
1054 if v then
1055 remove(lbar.."/"..i)
1059 -- this is a hack, and should brobably be rethought
1060 -- the intent is to distinguish the multiple screens
1061 if s then
1062 create ("/screen/"..s.."/lbar/000000000000000000", '-'..s..'-')
1066 function create_tag_widget(name)
1067 local nc = get_ctl("normcolors") or ""
1068 local screens = get_screens()
1069 if not screens then
1070 create ("/lbar/" .. name, nc .. " " .. name)
1071 return
1073 local i, s
1074 for i,s in pairs(screens) do
1075 create ("/screen/"..s.."/lbar/" .. name, nc .. " " .. name)
1079 function destroy_tag_widget(name)
1080 local screens = get_screens()
1081 if not screens then
1082 remove ("/lbar/" .. name)
1083 return
1085 local i, s
1086 for i,s in pairs(screens) do
1087 remove ("/screen/"..s.."/lbar/" .. name)
1092 -- ========================================================================
1093 -- EVENT HANDLERS
1094 -- ========================================================================
1096 local widget_ev_handlers = {
1099 --[[
1100 =pod
1102 =item _handle_widget_event (ev, arg)
1104 Top-level event handler for redispatching events to widgets. This event
1105 handler is added for any widget event that currently has a widget registered
1106 for it.
1108 Valid widget events are currently
1110 RightBarMouseDown <buttonnumber> <widgetname>
1111 RightBarClick <buttonnumber> <widgetname>
1113 the "Click" event is sent on mouseup.
1115 The callbacks are given only the button number as their argument, to avoid the
1116 need to reparse.
1118 =cut
1119 --]]
1121 local function _handle_widget_event (ev, arg)
1123 log("_handle_widget_event: " .. tostring(ev) .. " - " .. tostring(arg))
1125 -- parse arg to strip out our widget name
1126 local number,wname = string.match(arg, "(%d+)%s+(.+)")
1128 -- check our dispatch table for that widget
1129 if not wname then
1130 log("Didn't find wname")
1131 return
1134 local wtable = widget_ev_handlers[wname]
1135 if not wtable then
1136 log("No widget cares about" .. wname)
1137 return
1140 local fn = wtable[ev] or wtable["*"]
1141 if fn then
1142 success, err = pcall( fn, ev, tonumber(number) )
1143 if not success then
1144 log("Callback had an error in _handle_widget_event: " .. tostring(err) )
1145 return nil
1147 else
1148 log("no function found for " .. ev)
1152 local ev_handlers = {
1153 ["*"] = function (ev, arg)
1154 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
1155 end,
1157 RightBarClick = _handle_widget_event,
1159 -- process timer events
1160 ProcessTimerEvents = function (ev, arg)
1161 process_timers()
1162 end,
1164 -- exit if another wmiirc started up
1165 Start = function (ev, arg)
1166 if arg then
1167 if arg == "wmiirc" then
1168 -- backwards compatibility with bash version
1169 log (" exiting; pid=" .. tostring(myid))
1170 cleanup()
1171 os.exit (0)
1172 else
1173 -- ignore if it came from us
1174 local pid = string.match(arg, "wmiirc (%d+)")
1175 if pid then
1176 local pid = tonumber (pid)
1177 if not (pid == myid) then
1178 log (" exiting; pid=" .. tostring(myid))
1179 cleanup()
1180 os.exit (0)
1185 end,
1187 -- tag management
1188 CreateTag = function (ev, arg)
1189 log ("CreateTag: " .. arg)
1190 create_tag_widget(arg)
1191 end,
1192 DestroyTag = function (ev, arg)
1193 log ("DestroyTag: " .. arg)
1194 destroy_tag_widget(arg)
1196 -- remove the tag from history
1197 local i,v
1198 for i=#view_hist,1,-1 do
1199 v = view_hist[i]
1200 if arg == v then
1201 table.remove(view_hist,i)
1204 end,
1206 FocusTag = function (ev, arg)
1207 log ("FocusTag: " .. arg)
1209 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1210 if not tag then
1211 return
1214 local file = "/lbar/" .. tag
1215 if scrn and scrn:len() > 0 then
1216 file = "/screen/" .. scrn .. file
1219 local fc = get_screen_ctl(scrn, "focuscolors") or get_ctl("focuscolors") or ""
1220 log ("# echo " .. fc .. " " .. tag .. " | wmiir write " .. file)
1222 create (file, fc .. " " .. tag)
1223 write (file, fc .. " " .. tag)
1224 end,
1225 UnfocusTag = function (ev, arg)
1226 log ("UnfocusTag: " .. arg)
1228 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1229 if not tag then
1230 return
1233 local file = "/lbar/" .. tag
1234 if scrn and scrn:len() > 0 then
1235 file = "/screen/" .. scrn .. file
1238 local nc = get_screen_ctl(scrn, "normcolors") or get_ctl("normcolors") or ""
1239 log ("# echo " .. nc .. " " .. tag .. " | wmiir write " .. file)
1241 create (file, nc .. " " .. tag)
1242 write (file, nc .. " " .. tag)
1244 -- don't duplicate the last entry
1245 if not (tag == view_hist[#view_hist]) then
1246 view_hist[#view_hist+1] = tag
1248 -- limit to view_hist_max
1249 if #view_hist > view_hist_max then
1250 table.remove(view_hist, 1)
1253 end,
1255 -- key event handling
1256 Key = function (ev, arg)
1257 log ("Key: " .. arg)
1258 local magic = nil
1259 -- can we find an exact match?
1260 local fn = key_handlers[arg]
1261 if not fn then
1262 local key = arg:gsub("-%d$", "-#")
1263 -- can we find a match with a # wild card for the number
1264 fn = key_handlers[key]
1265 if fn then
1266 -- convert the trailing number to a number
1267 magic = tonumber(arg:match("-(%d)$"))
1270 if not fn then
1271 local key = arg:gsub("-%a$", "-@")
1272 -- can we find a match with a @ wild card for a letter
1273 fn = key_handlers[key]
1274 if fn then
1275 -- split off the trailing letter
1276 magic = arg:match("-(%a)$")
1279 if not fn then
1280 -- everything else failed, try default match
1281 fn = key_handlers["*"]
1283 if fn then
1284 local r, err = pcall (fn, arg, magic)
1285 if not r then
1286 log ("WARNING: " .. tostring(err))
1289 end,
1291 -- mouse handling on the lbar
1292 LeftBarClick = function (ev, arg)
1293 local button,tag = string.match(arg, "(%w+)%s+(%S+)")
1294 set_view (tag)
1295 end,
1297 -- focus updates
1298 ClientFocus = function (ev, arg)
1299 log ("ClientFocus: " .. arg)
1300 client_focused (arg)
1301 end,
1302 ColumnFocus = function (ev, arg)
1303 log ("ColumnFocus: " .. arg)
1304 end,
1306 -- client handling
1307 CreateClient = function (ev, arg)
1308 if next_client_goes_to_tag then
1309 local tag = next_client_goes_to_tag
1310 local cli = arg
1311 next_client_goes_to_tag = nil
1312 write ("/client/" .. cli .. "/tags", tag)
1313 set_view(tag)
1315 client_created (arg)
1316 end,
1317 DestroyClient = function (ev, arg)
1318 client_destoryed (arg)
1319 end,
1321 -- urgent tag
1322 UrgentTag = function (ev, arg)
1323 log ("UrgentTag: " .. arg)
1324 write ("/lbar/" .. arg, "*" .. arg);
1325 end,
1326 NotUrgentTag = function (ev, arg)
1327 log ("NotUrgentTag: " .. arg)
1328 write ("/lbar/" .. arg, arg);
1329 end,
1331 -- notifications
1332 Unresponsive = function (ev, arg)
1333 log ("Unresponsive: " .. arg)
1334 -- TODO ask the user if it shoudl be killed off
1335 end,
1337 Notice = function (ev, arg)
1338 log ("Notice: " .. arg)
1339 -- TODO send to the message plugin (or implement there)
1340 end,
1342 -- /
1345 --[[
1346 =pod
1348 =item add_widget_event_handler (wname, ev, fn)
1350 Add an event handler callback for the I<ev> event on the widget named I<wname>
1352 =cut
1353 --]]
1355 function add_widget_event_handler (wname, ev, fn)
1356 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
1357 error ("expecting string for widget name, string for event name and a function callback")
1360 -- Make sure the widget event handler is present
1361 if not ev_handlers[ev] then
1362 ev_handlers[ev] = _handle_widget_event
1365 if not widget_ev_handlers[wname] then
1366 widget_ev_handlers[wname] = { }
1369 if widget_ev_handlers[wname][ev] then
1370 -- TODO: we may wish to allow multiple handlers for one event
1371 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
1374 widget_ev_handlers[wname][ev] = fn
1377 --[[
1378 =pod
1380 =item remove_widget_event_handler (wname, ev)
1382 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1384 =cut
1385 --]]
1386 function remove_event_handler (wname, ev)
1388 if not widget_ev_handlers[wname] then
1389 return
1392 widget_ev_handlers[wname][ev] = nil
1395 --[[
1396 =pod
1398 =item add_event_handler (ev, fn)
1400 Add an event handler callback function, I<fn>, for the given event I<ev>.
1402 =cut
1403 --]]
1404 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1405 function add_event_handler (ev, fn)
1406 if type(ev) ~= "string" or type(fn) ~= "function" then
1407 error ("expecting a string and a function")
1410 if ev_handlers[ev] then
1411 -- TODO: we may wish to allow multiple handlers for one event
1412 error ("event handler already exists for '" .. ev .. "'")
1416 ev_handlers[ev] = fn
1419 --[[
1420 =pod
1422 =item remove_event_handler (ev)
1424 Remove an event handler callback function for the given event I<ev>.
1426 =cut
1427 --]]
1428 function remove_event_handler (ev)
1430 ev_handlers[ev] = nil
1434 -- ========================================================================
1435 -- MAIN INTERFACE FUNCTIONS
1436 -- ========================================================================
1438 local config = {
1439 xterm = 'x-terminal-emulator',
1440 xlock = "xscreensaver-command --lock",
1441 debug = false,
1444 -- ------------------------------------------------------------------------
1445 -- write configuration to /ctl wmii file
1446 -- wmii.set_ctl({ "var" = "val", ...})
1447 -- wmii.set_ctl("var, "val")
1448 function set_ctl (first,second)
1449 if type(first) == "table" and second == nil then
1450 local x, y
1451 for x, y in pairs(first) do
1452 write ("/ctl", x .. " " .. y)
1455 elseif type(first) == "string" and type(second) == "string" then
1456 write ("/ctl", first .. " " .. second)
1458 else
1459 error ("expecting a table or two string arguments")
1463 -- ------------------------------------------------------------------------
1464 -- read a value from /ctl wmii file
1465 -- table = wmii.get_ctl()
1466 -- value = wmii.get_ctl("variable")
1467 function get_ctl (name)
1468 local s
1469 local t = {}
1470 for s in iread("/ctl") do
1471 local var,val = s:match("(%w+)%s+(.+)")
1472 if var == name then
1473 return val
1475 t[var] = val
1477 if not name then
1478 return t
1480 return nil
1483 -- ------------------------------------------------------------------------
1484 -- write configuration to /screen/*/ctl wmii file
1485 -- wmii.set_screen_ctl("screen", { "var" = "val", ...})
1486 -- wmii.set_screen_ctl("screen", "var, "val")
1487 function set_screen_ctl (screen, first, second)
1488 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1489 if not screen then
1490 error ("screen is not set")
1491 elseif type(first) == "table" and second == nil then
1492 local x, y
1493 for x, y in pairs(first) do
1494 write (ctl, x .. " " .. y)
1497 elseif type(first) == "string" and type(second) == "string" then
1498 write (ctl, first .. " " .. second)
1500 else
1501 error ("expecting a screen name, followed by a table or two string arguments")
1505 -- ------------------------------------------------------------------------
1506 -- read a value from /screen/*/ctl wmii file
1507 -- table = wmii.get_screen_ctl("screen")
1508 -- value = wmii.get_screen_ctl("screen", "variable")
1509 function get_screen_ctl (screen, name)
1510 local s
1511 local t = {}
1512 if not screen then
1513 return nil
1515 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1516 for s in iread(ctl) do
1517 local var,val = s:match("(%w+)%s+(.+)")
1518 if var == name then
1519 return val
1521 -- sometimes first line is the name of the entry
1522 -- in which case there will be no space
1523 t[var or ""] = val
1525 if not name then
1526 return t
1528 return nil
1531 -- ------------------------------------------------------------------------
1532 -- set an internal wmiirc.lua variable
1533 -- wmii.set_conf({ "var" = "val", ...})
1534 -- wmii.set_conf("var, "val")
1535 function set_conf (first,second)
1536 if type(first) == "table" and second == nil then
1537 local x, y
1538 for x, y in pairs(first) do
1539 config[x] = y
1542 elseif type(first) == "string"
1543 and (type(second) == "string"
1544 or type(second) == "number"
1545 or type(second) == "boolean") then
1546 config[first] = second
1548 else
1549 error ("expecting a table, or string and string/number as arguments")
1553 -- ------------------------------------------------------------------------
1554 -- read an internal wmiirc.lua variable
1555 function get_conf (name)
1556 if name then
1557 return config[name]
1559 return config
1562 -- ========================================================================
1563 -- THE EVENT LOOP
1564 -- ========================================================================
1566 -- the event loop instance
1567 local el = eventloop.new()
1568 local event_read_fd = -1
1569 local wmiirc_running = false
1570 local event_read_start = 0
1572 -- ------------------------------------------------------------------------
1573 -- start/restart the core event reading process
1574 local function start_event_reader ()
1575 -- prevent adding two readers
1576 if event_read_fd ~= -1 then
1577 if el:check_exec(event_read_fd) then
1578 return
1581 -- prevert rapid restarts
1582 local now = os.time()
1583 if os.difftime(now, event_read_start) < 5 then
1584 log("wmii: detected rapid restart of /event reader")
1585 local cmd = "wmiir ls /ctl"
1586 if os.execute(cmd) ~= 0 then
1587 log("wmii: cannot confirm communication with wmii, shutting down!")
1588 wmiirc_running = false
1589 return
1591 log("wmii: but things look ok, so we will restart it")
1593 event_read_start = now
1595 -- start a new event reader
1596 log("wmii: starting /event reading process")
1597 event_read_fd = el:add_exec (wmiir .. " read /event",
1598 function (line)
1599 local line = line or "nil"
1601 -- try to split off the argument(s)
1602 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1603 if not ev then
1604 ev = line
1607 -- now locate the handler function and call it
1608 local fn = ev_handlers[ev] or ev_handlers["*"]
1609 if fn then
1610 local r, err = pcall (fn, ev, arg)
1611 if not r then
1612 log ("WARNING: " .. tostring(err))
1617 log("wmii: ... fd=" .. tostring(event_read_fd))
1620 -- ------------------------------------------------------------------------
1621 -- run the event loop and process events, this function does not exit
1622 function run_event_loop ()
1623 -- stop any other instance of wmiirc
1624 wmixp:write ("/event", "Start wmiirc " .. tostring(myid))
1626 log("wmii: updating lbar")
1628 update_displayed_tags ()
1630 log("wmii: updating rbar")
1632 update_displayed_widgets ()
1634 log("wmii: updating active keys")
1636 update_active_keys ()
1638 log("wmii: starting event loop")
1639 wmiirc_running = true
1640 while wmiirc_running do
1641 start_event_reader()
1642 local sleep_for = process_timers()
1643 el:run_loop(sleep_for)
1645 log ("wmii: exiting")
1648 -- ========================================================================
1649 -- PLUGINS API
1650 -- ========================================================================
1652 api_version = 0.1 -- the API version we export
1654 plugins = {} -- all plugins that were loaded
1656 -- ------------------------------------------------------------------------
1657 -- plugin loader which also verifies the version of the api the plugin needs
1659 -- here is what it does
1660 -- - does a manual locate on the file using package.path
1661 -- - reads in the file w/o using the lua interpreter
1662 -- - locates api_version=X.Y string
1663 -- - makes sure that api_version requested can be satisfied
1664 -- - if the plugins is available it will set variables passed in
1665 -- - it then loads the plugin
1667 -- TODO: currently the api_version must be in an X.Y format, but we may want
1668 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1670 function load_plugin(name, vars)
1671 local backup_path = package.path or "./?.lua"
1673 log ("loading " .. name)
1675 -- this is the version we want to find
1676 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1677 if (not api_major) or (not api_minor) then
1678 log ("WARNING: could not parse api_version in core/wmii.lua")
1679 return nil
1682 -- first find the plugin file
1683 local s, path_match, full_name, file
1684 for s in string.gmatch(plugin_path, "[^;]+") do
1685 -- try to locate the files locally
1686 local fn = s:gsub("%?", name)
1687 file = io.open(fn, "r")
1688 if file then
1689 path_match = s
1690 full_name = fn
1691 break
1695 -- read it in
1696 local txt
1697 if file then
1698 txt = file:read("*all")
1699 file:close()
1702 if not txt then
1703 log ("WARNING: could not load plugin '" .. name .. "'")
1704 return nil
1707 -- find the api_version line
1708 local line, plugin_version
1709 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1710 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1711 if plugin_version then
1712 break
1716 if not plugin_version then
1717 log ("WARNING: could not find api_version string in plugin '" .. name .. "'")
1718 return nil
1721 -- decompose the version string
1722 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1723 if (not plugin_major) or (not plugin_minor) then
1724 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1725 return nil
1728 -- make a version test
1729 if plugin_major ~= api_major then
1730 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1731 .. " (api " .. tonumber(api_version) .. ")")
1732 return nil
1735 if plugin_minor > api_minor then
1736 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1737 .. " (api " .. tonumber(api_version) .. ")")
1738 return nil
1741 -- the configuration parameters before loading
1742 if type(vars) == "table" then
1743 local var, val
1744 for var,val in pairs(vars) do
1745 local success = pcall (set_conf, name .. "." .. var, val)
1746 if not success then
1747 log ("WARNING: bad variable {" .. tostring(var) .. ", " .. tostring(val) .. "} "
1748 .. "given; loading '" .. name .. "' plugin failed.")
1749 return nil
1754 -- actually load the module, but use only the path where we though it should be
1755 package.path = path_match
1756 local success,what = pcall (require, name)
1757 package.path = backup_path
1758 if not success then
1759 log ("WARNING: failed to load '" .. name .. "' plugin")
1760 log (" - path: " .. tostring(path_match))
1761 log (" - file: " .. tostring(full_name))
1762 log (" - plugin's api_version: " .. tostring(plugin_version))
1763 log (" - reason: " .. tostring(what))
1764 return nil
1767 -- success
1768 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1769 plugins[name] = what
1770 return what
1773 -- ------------------------------------------------------------------------
1774 -- widget template
1775 widget = {}
1776 widgets = {}
1778 -- ------------------------------------------------------------------------
1779 -- create a widget object and add it to the wmii /rbar
1781 -- examples:
1782 -- widget = wmii.widget:new ("999_clock")
1783 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1784 function widget:new (name, fn)
1785 local o = {}
1787 if type(name) == "string" then
1788 o.name = name
1789 if type(fn) == "function" then
1790 o.fn = fn
1792 else
1793 error ("expected name followed by an optional function as arguments")
1796 setmetatable (o,self)
1797 self.__index = self
1798 self.__gc = function (o) o:hide() end
1800 widgets[name] = o
1802 o:show()
1803 return o
1806 -- ------------------------------------------------------------------------
1807 -- stop and destroy the timer
1808 function widget:delete ()
1809 widgets[self.name] = nil
1810 self:hide()
1813 -- ------------------------------------------------------------------------
1814 -- displays or updates the widget text
1816 -- examples:
1817 -- w:show("foo")
1818 -- w:show("foo", "#888888 #222222 #333333")
1819 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1821 function widget:show (txt, colors)
1822 local colors = colors or get_ctl("normcolors") or ""
1823 local txt = txt or self.txt or ""
1824 local towrite = txt
1825 if colors then
1826 towrite = colors .. " " .. towrite
1828 if not self.txt then
1829 create ("/rbar/" .. self.name, towrite)
1830 else
1831 write ("/rbar/" .. self.name, towrite)
1833 self.txt = txt
1836 -- ------------------------------------------------------------------------
1837 -- hides a widget and removes it from the bar
1838 function widget:hide ()
1839 if self.txt then
1840 remove ("/lbar/" .. self.name)
1841 self.txt = nil
1845 --[[
1846 =pod
1848 =item widget:add_event_handler (ev, fn)
1850 Add an event handler callback for this widget, using I<fn> for event I<ev>
1852 =cut
1853 --]]
1855 function widget:add_event_handler (ev, fn)
1856 add_widget_event_handler( self.name, ev, fn)
1860 -- ------------------------------------------------------------------------
1861 -- remove all /rbar entries that we don't have widget objects for
1862 function update_displayed_widgets ()
1863 -- colours for /rbar
1864 local nc = get_ctl("normcolors") or ""
1866 -- build up a table of existing tags in the /lbar
1867 local old = {}
1868 local s
1869 for s in wmixp:idir ("/rbar") do
1870 old[s.name] = 1
1873 -- for all actual widgets in use we want to remove them from the old list
1874 local i,v
1875 for i,v in pairs(widgets) do
1876 old[v.name] = nil
1879 -- anything left in the old table should be removed now
1880 for i,v in pairs(old) do
1881 if v then
1882 remove("/rbar/"..i)
1887 -- ------------------------------------------------------------------------
1888 -- create a new program and for each line it generates call the callback function
1889 -- returns fd which can be passed to kill_exec()
1890 function add_exec (command, callback)
1891 return el:add_exec (command, callback)
1894 -- ------------------------------------------------------------------------
1895 -- terminates a program spawned off by add_exec()
1896 function kill_exec (fd)
1897 return el:kill_exec (fd)
1900 -- ------------------------------------------------------------------------
1901 -- timer template
1902 timer = {}
1903 local timers = {}
1905 -- ------------------------------------------------------------------------
1906 -- create a timer object and add it to the event loop
1908 -- examples:
1909 -- timer:new (my_timer_fn)
1910 -- timer:new (my_timer_fn, 15)
1911 function timer:new (fn, seconds)
1912 local o = {}
1914 if type(fn) == "function" then
1915 o.fn = fn
1916 else
1917 error ("expected function followed by an optional number as arguments")
1920 setmetatable (o,self)
1921 self.__index = self
1922 self.__gc = function (o) o:stop() end
1924 -- add the timer
1925 timers[#timers+1] = o
1927 if seconds then
1928 o:resched(seconds)
1930 return o
1933 -- ------------------------------------------------------------------------
1934 -- stop and destroy the timer
1935 function timer:delete ()
1936 self:stop()
1937 local i,t
1938 for i,t in pairs(timers) do
1939 if t == self then
1940 table.remove (timers,i)
1941 return
1946 -- ------------------------------------------------------------------------
1947 -- run the timer given new interval
1948 function timer:resched (seconds)
1949 local seconds = seconds or self.interval
1950 if not (type(seconds) == "number") then
1951 error ("timer:resched expected number as argument")
1954 local now = tonumber(os.date("%s"))
1956 self.interval = seconds
1957 self.next_time = now + seconds
1959 -- resort the timer list
1960 table.sort (timers, timer.is_less_then)
1963 -- helper for sorting timers
1964 function timer:is_less_then(another)
1965 if not self.next_time then
1966 return false -- another is smaller, nil means infinity
1968 elseif not another.next_time then
1969 return true -- self is smaller, nil means infinity
1971 elseif self.next_time < another.next_time then
1972 return true -- self is smaller than another
1975 return false -- another is smaller then self
1978 -- ------------------------------------------------------------------------
1979 -- stop the timer
1980 function timer:stop ()
1981 self.next_time = nil
1983 -- resort the timer list
1984 table.sort (timers, timer.is_less_then)
1987 -- ------------------------------------------------------------------------
1988 -- figure out how long before the next event
1989 function time_before_next_timer_event()
1990 local tmr = timers[1]
1991 if tmr and tmr.next_time then
1992 local now = tonumber(os.date("%s"))
1993 local seconds = tmr.next_time - now
1994 if seconds > 0 then
1995 return seconds
1998 return 0 -- sleep for ever
2001 -- ------------------------------------------------------------------------
2002 -- handle outstanding events
2003 function process_timers ()
2004 local now = tonumber(os.date("%s"))
2005 local torun = {}
2006 local i,tmr
2008 for i,tmr in pairs (timers) do
2009 if not tmr then
2010 -- prune out removed timers
2011 table.remove(timers,i)
2012 break
2014 elseif not tmr.next_time then
2015 -- break out once we find a timer that is stopped
2016 break
2018 elseif tmr.next_time > now then
2019 -- break out once we get to the future
2020 break
2023 -- this one is good to go
2024 torun[#torun+1] = tmr
2027 for i,tmr in pairs (torun) do
2028 tmr:stop()
2029 local status,new_interval = pcall (tmr.fn, tmr)
2030 if status then
2031 new_interval = new_interval or self.interval
2032 if new_interval and (new_interval ~= -1) then
2033 tmr:resched(new_interval)
2035 else
2036 log ("ERROR: " .. tostring(new_interval))
2040 local sleep_for = time_before_next_timer_event()
2041 return sleep_for
2044 -- ------------------------------------------------------------------------
2045 -- cleanup everything in preparation for exit() or exec()
2046 function cleanup ()
2048 local i,v,tmr,p
2050 log ("wmii: stopping timer events")
2052 for i,tmr in pairs (timers) do
2053 pcall (tmr.delete, tmr)
2055 timers = {}
2057 log ("wmii: terminating eventloop")
2059 pcall(el.kill_all,el)
2061 log ("wmii: disposing of widgets")
2063 -- dispose of all widgets
2064 for i,v in pairs(widgets) do
2065 pcall(v.delete,v)
2067 timers = {}
2069 -- FIXME: it doesn't seem to do what I want
2070 --[[
2071 log ("wmii: releasing plugins")
2073 for i,p in pairs(plugins) do
2074 if p.cleanup then
2075 pcall (p.cleanup, p)
2078 plugins = {}
2079 --]]
2081 log ("wmii: dormant")
2082 wmiirc_running = false
2085 -- ========================================================================
2086 -- CLIENT HANDLING
2087 -- ========================================================================
2089 --[[
2090 -- Notes on client tracking
2092 -- When a client is created wmii sends us a CreateClient message, and
2093 -- we in turn create a 'client' object and store it in the 'clients'
2094 -- table indexed by the client's ID.
2096 -- Each client object stores the following:
2097 -- .xid - the X client ID
2098 -- .pid - the process ID
2099 -- .prog - program object representing the process
2101 -- The client and program objects track the following modes for each program:
2103 -- raw mode:
2104 -- - for each client window
2105 -- - Mod4-space toggles the state between normal and raw
2106 -- - Mod1-f raw also toggles the state
2107 -- - in raw mode all input goes to the client, except for Mod4-space
2108 -- - a focused client with raw mode enabled is put into raw mode
2110 -- suspend mode:
2111 -- - for each program
2112 -- - Mod1-f suspend toggles the state for current client's program
2113 -- - a focused client, whose program was previous suspended is resumed
2114 -- - an unfocused client, with suspend enabled, will be suspended
2115 -- - suspend/resume is done by sending the STOP/CONT signals to the PID
2116 --]]
2118 function xid_to_pid (xid)
2119 local cmd = "xprop -id " .. tostring(xid) .. " _NET_WM_PID"
2120 local file = io.popen (cmd)
2121 local out = file:read("*a")
2122 file:close()
2123 local pid = out:match("^_NET_WM_PID.*%s+=%s+(%d+)%s+$")
2124 return tonumber(pid)
2127 local focused_xid = nil
2128 local clients = {} -- table of client objects indexed by xid
2129 local programs = {} -- table of program objects indexed by pid
2130 local mode_widget = widget:new ("999_client_mode")
2132 -- make programs table have weak values
2133 -- programs go away as soon as no clients point to it
2134 local programs_mt = {}
2135 setmetatable(programs, programs_mt)
2136 programs_mt.__mode = 'v'
2138 -- program class
2139 program = {}
2140 function program:new (pid)
2141 -- make an object
2142 local o = {}
2143 setmetatable (o,self)
2144 self.__index = self
2145 self.__gc = function (old) old:cont() end
2146 -- initialize the new object
2147 o.pid = pid
2148 -- suspend mode
2149 o.suspend = {}
2150 o.suspend.toggle = function (prog)
2151 prog.suspend.enabled = not prog.suspend.enabled
2153 o.suspend.enabled = false -- if true, defocusing suspends (SIGSTOP)
2154 o.suspend.active = true -- if true, focusing resumes (SIGCONT)
2155 return o
2158 function program:stop ()
2159 if not self.suspend.active then
2160 local cmd = "kill -STOP " .. tostring(self.pid)
2161 log (" executing: " .. cmd)
2162 os.execute (cmd)
2163 self.suspend.active = true
2167 function program:cont ()
2168 if self.suspend.active then
2169 local cmd = "kill -CONT " .. tostring(self.pid)
2170 log (" executing: " .. cmd)
2171 os.execute (cmd)
2172 self.suspend.active = false
2176 function get_program (pid)
2177 local prog = programs[pid]
2178 if pid and not prog then
2179 prog = program:new (pid)
2180 programs[pid] = prog
2182 return prog
2185 -- client class
2186 client = {}
2187 function client:new (xid)
2188 local pid = xid_to_pid(xid)
2189 if not pid then
2190 log ("WARNING: failed to convert XID " .. tostring(xid) .. " to a PID")
2191 return
2193 -- make an object
2194 local o = {}
2195 setmetatable (o,self)
2196 self.__index = function (t,k)
2197 if k == 'suspend' then -- suspend mode is tracked per program
2198 return t.prog.suspend
2200 return self[k]
2202 self.__gc = function (old) old.prog=nil end
2203 -- initialize the new object
2204 o.xid = xid
2205 o.pid = pid
2206 o.prog = get_program (pid)
2207 -- raw mode
2208 o.raw = {}
2209 o.raw.toggle = function (cli)
2210 cli.raw.enabled = not cli.raw.enabled
2211 cli:set_raw_mode()
2213 o.raw.enabled = false -- if true, raw mode enabled when client is focused
2214 return o
2217 function client:stop ()
2218 if self.suspend.enabled then
2219 self.prog:stop()
2223 function client:cont ()
2224 self.prog:cont()
2227 function client:set_raw_mode()
2228 if not self or not self.raw.enabled then -- normal mode
2229 update_active_keys ()
2230 else -- raw mode
2231 write ("/keys", "Mod4-space")
2235 function client:toggle(what)
2236 if what and self[what] then
2237 local ctl = self[what]
2239 ctl.toggle (self)
2241 log ("xid=" .. tostring (xid)
2242 .. " pid=" .. tostring (self.pid) .. " (" .. tostring (self.prog.pid) .. ")"
2243 .. " what=" .. tostring (what)
2244 .. " enabled=" .. tostring(ctl["enabled"]))
2246 mode_widget:show (self:flags_string())
2249 function client:flags_string()
2250 local ret = ''
2251 if self.suspend.enabled then ret = ret .. "s" else ret = ret .. "-" end
2252 if self.raw.enabled then ret = ret .. "r" else ret = ret .. "-" end
2253 return ret
2256 function get_client (xid)
2257 local xid = xid or wmixp:read("/client/sel/ctl")
2258 local cli = clients[xid]
2259 if not cli then
2260 cli = client:new (xid)
2261 clients[xid] = cli
2263 return cli
2266 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2267 function client_created (xid)
2268 log ("-client_created " .. tostring(xid))
2269 return get_client(xid)
2272 function client_destoryed (xid)
2273 log ("-client_destoryed " .. tostring(xid))
2274 if clients[xid] then
2275 local cli = clients[xid]
2276 clients[xid] = nil
2277 log (" del pid: " .. tostring(cli.pid))
2278 cli:cont()
2280 if focused_xid == xid then
2281 focused_xid = nil
2285 function client_focused (xid)
2286 log ("-client_focused " .. tostring(xid))
2287 -- return the current focused xid if nil is passed
2288 if type(xid) ~= 'string' or not xid:match("0x%x*$") then
2289 return focused_xid
2291 -- do nothing if the same xid
2292 if focused_xid == xid then
2293 return clients[xid]
2296 local old = clients[focused_xid]
2297 local new = get_client(xid)
2299 -- handle raw mode switch
2300 if not old or ( old and new and old.raw.enabled ~= new.raw.enabled ) then
2301 new:set_raw_mode()
2304 -- do nothing if the same pid
2305 if old and new and old.pid == new.pid then
2306 mode_widget:show (new:flags_string())
2307 return clients[xid]
2310 if old then
2311 --[[
2312 log (" old pid: " .. tostring(old.pid)
2313 .. " xid: " .. tostring(old.xid)
2314 .. " flags: " .. old:flags_string())
2315 ]]--
2316 old:stop()
2319 if new then
2320 --[[
2321 log (" new pid: " .. tostring(new.pid)
2322 .. " xid: " .. tostring(new.xid)
2323 .. " flags: " .. new:flags_string())
2324 ]]--
2325 new:cont()
2328 mode_widget:show (new:flags_string())
2329 focused_xid = xid
2330 return new
2334 -- ========================================================================
2335 -- DOCUMENTATION
2336 -- ========================================================================
2338 --[[
2339 =pod
2341 =back
2343 =head1 ENVIRONMENT
2345 =over 4
2347 =item WMII_ADDRESS
2349 Used to determine location of wmii's listen socket.
2351 =back
2353 =head1 SEE ALSO
2355 L<wmii(1)>, L<lua(1)>
2357 =head1 AUTHOR
2359 Bart Trojanowski B<< <bart@jukie.net> >>
2361 =head1 COPYRIGHT AND LICENSE
2363 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
2365 This is free software. You may redistribute copies of it under the terms of
2366 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
2367 is NO WARRANTY, to the extent permitted by law.
2369 =cut
2370 --]]