fix alt-p when wmiir setsid is available
[wmiirc-lua.git] / src / core / wmii.lua.in
blob7950a9621f7ca821f15f12937d29b4cc49a37745
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 = ("%HOME_WMII%"):gsub("^~", os.getenv("HOME"))
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 -- wimenu points to the wimenu
127 local wimenu = "wimenu"
128 if 0 ~= os.execute("which wimenu") then
129 if 0 ~= os.execute("which dmenu") then
130 error ("could not find wimenu or dmenu in the PATH")
132 wimenu = "dmenu"
135 -- wmii_adr is the address we use when connecting using ixp
136 -- TODO: hide this magic in luaixp
137 local wmii_adr = os.getenv("WMII_ADDRESS") or ""
138 if wmii_adr == "" then
139 local user = os.getenv("USER") or ""
140 if user == "" then
141 local cmd = "id -n -u"
142 local file = io.popen (cmd)
143 if file then
144 user = file:read("*l") or ""
145 file:close()
147 if not user or user == "" then
148 error ("no WMII_ADDRESS environment variable set, and "
149 .. "could not determine user name for socket location")
152 local disp = os.getenv("DISPLAY") or ":0.0"
153 wmii_adr = "unix!/tmp/ns." .. user .. "."
154 .. disp:match("(:%d+)") .. "/wmii"
157 -- wmixp is the ixp context we use to talk to wmii
158 local wmixp = ixp.new(wmii_adr)
160 -- history of previous views, view_hist[#view_hist] is the last one
161 local view_hist = {} -- sorted with 1 being the oldest
162 local view_hist_max = 50 -- max number to keep track of
164 -- allow for a client to be forced to a tag
165 local next_client_goes_to_tag = nil
167 -- program and action histories
168 local prog_hist = history.new (20)
169 local action_hist = history.new(10)
171 -- where to find plugins
172 plugin_paths = {}
174 table.insert(plugin_paths, wmiidir .. "/plugins/?.so")
175 table.insert(plugin_paths, wmiidir .. "/plugins/?.lua")
177 for path in string.gmatch(package.path, "[^;]+") do
178 local wmiidir = path:gsub("%?.*$", "wmii")
179 local stat = posix.stat(wmiidir)
180 if stat and stat.type == "directory" then
181 local path = path:gsub("%?", "wmii/?")
182 table.insert(plugin_paths, path)
186 for path in string.gmatch(package.cpath, "[^;]+") do
187 local wmiidir = path:gsub("%?.*$", "wmii")
188 local stat = posix.stat(wmiidir)
189 if stat and stat.type == "directory" then
190 local path = path:gsub("%?", "wmii/?")
191 table.insert(plugin_paths, path)
195 -- where to find wmiirc (see find_wmiirc())
196 wmiirc_path = wmiidir .. "/wmiirc.lua;"
197 .. wmiidir .. "/wmiirc;"
198 .. "%RC_DIR%/wmiirc.lua;"
199 .. "%RC_DIR%/wmiirc"
201 -- ========================================================================
202 -- LOCAL HELPERS
203 -- ========================================================================
205 --[[
206 =pod
208 =item log ( str )
210 log the message provided in c<str>
212 currently just writes to io.stderr
214 =cut
215 --]]
216 function log (str)
217 if get_conf("debug") then
218 io.stderr:write (str .. "\n")
224 --[[
225 =pod
227 =item warn ( str )
229 generate an error message, c<str>, but do not terminate lua
231 returns the string argument
233 =cut
234 --]]
235 function warn (str)
236 pcall(error,str)
237 return str
242 --[[
243 =pod
245 =item execute ( cmd )
247 setsid wrapper for os.execute(c<cmd>)
249 =cut
250 --]]
251 local wmiir_has_setsid = nil
252 function execute (cmd)
253 log (" executing: " .. cmd)
254 if wmiir_has_setsid == nil then
255 -- test if wmiir has setsid support
256 local rc = os.execute (wmiir .. " setsid true")
257 wmiir_has_setsid = (rc == 0)
258 log ("wmiir " .. (wmiir_has_setsid and "has" or "does not have") .. " setsid support")
261 if wmiir_has_setsid then
262 cmd = wmiir .. " setsid " .. cmd
265 log (" ... " .. cmd)
266 local rc = os.execute (cmd)
267 log (" ... rc=" .. tostring(rc))
268 return rc
273 --[[
274 =pod
276 =item shell_execute ( cmd )
278 setsid wrapper for os.execute(c<cmd>)
279 like execute() but runs under a subshell
281 =cut
282 --]]
283 function shell_execute (cmd)
284 cmd = cmd:gsub('([\\"])', '\\%1')
285 cmd = 'sh -c "' .. cmd .. '"'
287 return execute(cmd)
292 --[[
293 =pod
295 =item find_wmiirc ( )
297 Locates the wmiirc script. It looks in %HOME_WMII% and %RC_DIR%
298 for the first lua script bearing the name wmiirc.lua or wmiirc. Returns
299 first match.
301 =cut
302 --]]
303 function find_wmiirc()
304 local fn
305 for fn in string.gmatch(wmiirc_path, "[^;]+") do
306 -- try to locate the files locally
307 local file = io.open(fn, "r")
308 if file then
309 local txt = file:read("*line")
310 file:close()
311 if type(txt) == 'string' and txt:match("lua") then
312 return fn
316 return nil
320 -- ========================================================================
321 -- MAIN ACCESS FUNCTIONS
322 -- ========================================================================
324 --[[
325 =pod
327 =item ls ( dir, fmt )
329 List the wmii filesystem directory provided in C<dir>, in the format specified
330 by C<fmt>.
332 Returns an iterator of TODO
334 =cut
335 --]]
336 function ls (dir, fmt)
337 local verbose = fmt and fmt:match("l")
339 local s = wmixp:stat(dir)
340 if not s then
341 return function () return nil end
343 if s.modestr:match("^[^d]") then
344 return function ()
345 return stat2str(verbose, s)
349 local itr = wmixp:idir (dir)
350 if not itr then
351 --return function ()
352 return nil
353 --end
357 return function ()
358 local s = itr()
359 if s then
360 return stat2str(verbose, s)
362 return nil
366 local function stat2str(verbose, stat)
367 if verbose then
368 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
369 else
370 if stat.modestr:match("^d") then
371 return stat.name .. "/"
372 else
373 return stat.name
378 -- ------------------------------------------------------------------------
379 -- read all contents of a wmii virtual file
380 function read (file)
381 return wmixp:read (file)
384 -- ------------------------------------------------------------------------
385 -- return an iterator which walks all the lines in the file
387 -- example:
388 -- for event in wmii.iread("/ctl")
389 -- ...
390 -- end
392 -- NOTE: don't use iread for files that could block, as this will interfere
393 -- with timer processing and event delivery. Instead fork off a process to
394 -- execute wmiir and read back the responses via callback.
395 function iread (file)
396 return wmixp:iread(file)
399 -- ------------------------------------------------------------------------
400 -- create a wmii file, optionally write data to it
401 function create (file, data)
402 wmixp:create(file, data)
405 -- ------------------------------------------------------------------------
406 -- remove a wmii file
407 function remove (file)
408 wmixp:remove(file)
411 -- ------------------------------------------------------------------------
412 -- write a value to a wmii virtual file system
413 function write (file, value)
414 wmixp:write (file, value)
417 -- ------------------------------------------------------------------------
418 -- setup a table describing the menu command
419 local function menu_cmd (prompt)
420 local cmdt = { wimenu }
421 if prompt then
422 cmdt[#cmdt+1] = "-p"
423 cmdt[#cmdt+1] = "'" .. prompt .. "'"
426 if wimenu == "dmenu" then
427 cmdt[#cmdt+1] = "-b"
428 local normcolors = get_ctl("normcolors")
429 if normcolors then
430 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
431 if nf then
432 cmdt[#cmdt+1] = "-nf"
433 cmdt[#cmdt+1] = "'" .. nf .. "'"
435 if nb then
436 cmdt[#cmdt+1] = "-nb"
437 cmdt[#cmdt+1] = "'" .. nb .. "'"
440 local focuscolors = get_ctl("focuscolors")
441 if focuscolors then
442 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
443 if sf then
444 cmdt[#cmdt+1] = "-sf"
445 cmdt[#cmdt+1] = "'" .. sf .. "'"
447 if sb then
448 cmdt[#cmdt+1] = "-sb"
449 cmdt[#cmdt+1] = "'" .. sb .. "'"
454 return cmdt
457 -- ------------------------------------------------------------------------
458 -- displays the menu given an table of entires, returns selected text
459 function menu (tbl, prompt)
460 local menu = menu_cmd(prompt)
462 local infile = os.tmpname()
463 local fh = io.open (infile, "w+")
465 local i,v
466 for i,v in pairs(tbl) do
467 if type(i) == 'number' and type(v) == 'string' then
468 fh:write (v)
469 else
470 fh:write (i)
472 fh:write ("\n")
474 fh:close()
476 local outfile = os.tmpname()
478 menu[#menu+1] = "<"
479 menu[#menu+1] = infile
480 menu[#menu+1] = ">"
481 menu[#menu+1] = outfile
483 local cmd = table.concat(menu," ")
484 execute (cmd)
486 fh = io.open (outfile, "r")
487 os.remove (outfile)
489 local sel = fh:read("*l")
490 fh:close()
492 return sel
495 -- ------------------------------------------------------------------------
496 -- displays the a tag selection menu, returns selected tag
497 function tag_menu ()
498 local tags = get_tags()
500 return menu(tags, "tag:")
503 -- ------------------------------------------------------------------------
504 -- displays the a program menu, returns selected program
505 function prog_menu ()
506 local menu = menu_cmd("cmd:")
508 local outfile = os.tmpname()
510 menu[#menu+1] = ">"
511 menu[#menu+1] = outfile
513 local hstt = { }
514 for n in prog_hist:walk_reverse_unique() do
515 hstt[#hstt+1] = "echo '" .. n .. "' ; "
518 local cmd = "(" .. table.concat(hstt)
519 .. "dmenu_path ) |"
520 .. table.concat(menu," ")
521 shell_execute (cmd)
523 local fh = io.open (outfile, "rb")
524 os.remove (outfile)
526 local prog = fh:read("*l")
527 io.close (fh)
529 return prog
532 -- ------------------------------------------------------------------------
533 -- returns a table of sorted tags names
534 function get_tags()
535 local t = {}
536 local s
537 for s in wmixp:idir ("/tag") do
538 if s.name and not (s.name == "sel") then
539 t[#t + 1] = s.name
542 table.sort(t)
543 return t
546 -- ------------------------------------------------------------------------
547 -- returns a table of sorted screen names
548 function get_screens()
549 local t = {}
550 local s
551 local empty = true
552 for s in wmixp:idir ("/screen") do
553 if s.name and not (s.name == "sel") then
554 t[#t + 1] = s.name
555 empty = false
558 if empty then
559 return nil
561 table.sort(t)
562 return t
565 -- ------------------------------------------------------------------------
566 -- returns current view, on current screen or specified screen
567 function get_view(screen)
568 return get_screen_ctl(screen, "view") or get_ctl("view")
571 -- ------------------------------------------------------------------------
572 -- changes the current view to the name given
573 function set_view(sel)
574 local cur = get_view()
575 local all = get_tags()
577 if #all < 2 or sel == cur then
578 -- nothing to do if we have less then 2 tags
579 return
582 if not (type(sel) == "string") then
583 error ("string argument expected")
586 -- set new view
587 write ("/ctl", "view " .. sel)
590 -- ------------------------------------------------------------------------
591 -- changes the current view to the index given
592 function set_view_index(sel)
593 local cur = get_view()
594 local all = get_tags()
596 if #all < 2 then
597 -- nothing to do if we have less then 2 tags
598 return
601 local num = tonumber (sel)
602 if not num then
603 error ("number argument expected")
606 local name = all[sel]
607 if not name or name == cur then
608 return
611 -- set new view
612 write ("/ctl", "view " .. name)
615 -- ------------------------------------------------------------------------
616 -- chnages to current view by offset given
617 function set_view_ofs(jump)
618 local cur = get_view()
619 local all = get_tags()
621 if #all < 2 then
622 -- nothing to do if we have less then 2 tags
623 return
626 -- range check
627 if (jump < - #all) or (jump > #all) then
628 error ("view selector is out of range")
631 -- find the one that's selected index
632 local curi = nil
633 local i,v
634 for i,v in pairs (all) do
635 if v == cur then curi = i end
638 -- adjust by index
639 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
640 if (newi < - #all) or (newi > #all) then
641 error ("error computng new view")
644 write ("/ctl", "view " .. all[newi])
647 -- ------------------------------------------------------------------------
648 -- toggle between last view and current view
649 function toggle_view()
650 local last = view_hist[#view_hist]
651 if last then
652 set_view(last)
656 -- ========================================================================
657 -- ACTION HANDLERS
658 -- ========================================================================
660 local action_handlers = {
661 man = function (act, args)
662 local xterm = get_conf("xterm") or "xterm"
663 local page = args
664 if (not page) or (not page:match("%S")) then
665 page = wmiidir .. "/wmii.3lua"
667 local cmd = xterm .. " -e man " .. page .. " &"
668 execute (cmd)
669 end,
671 quit = function ()
672 write ("/ctl", "quit")
673 end,
675 exec = function (act, args)
676 local what = args or "wmii-lua"
677 log (" asking wmii to exec " .. tostring(what))
678 cleanup()
679 write ("/ctl", "exec " .. what)
680 end,
682 xlock = function (act)
683 local cmd = get_conf("xlock") or "xscreensaver-command --lock"
684 execute (cmd)
685 end,
687 wmiirc = function ()
688 if have_posix then
689 local wmiirc = find_wmiirc()
690 if wmiirc then
691 log (" executing: lua " .. wmiirc)
692 cleanup()
693 posix.exec (wmiirc)
694 posix.exec ("/bin/sh", "-c", "exec lua wmiirc")
695 posix.exec ("%LUA_BIN%", wmiirc)
696 posix.exec ("/usr/bin/lua", wmiirc)
698 else
699 log("sorry cannot restart; you don't have lua's posix library.")
701 end,
703 urgent = function ()
704 wmixp:write ("/client/sel/ctl", "Urgent toggle")
705 end,
707 --[[
708 rehash = function ()
709 -- TODO: consider storing list of executables around, and
710 -- this will then reinitialize that list
711 log (" TODO: rehash")
712 end,
714 status = function ()
715 -- TODO: this should eventually update something on the /rbar
716 log (" TODO: status")
717 end,
718 --]]
721 --[[
722 =pod
724 =item add_action_handler (action, fn)
726 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
728 =cut
729 --]]
730 function add_action_handler (action, fn)
732 if type(action) ~= "string" or type(fn) ~= "function" then
733 error ("expecting a string and a function")
736 if action_handlers[action] then
737 error ("action handler already exists for '" .. action .. "'")
740 action_handlers[action] = fn
743 --[[
744 =pod
746 =item remove_action_handler (action)
748 Remove an action handler callback function for the given action string I<action>.
750 =cut
751 --]]
752 function remove_action_handler (action)
754 action_handlers[action] = nil
757 -- ========================================================================
758 -- KEY HANDLERS
759 -- ========================================================================
761 function ke_fullscreen_toggle()
762 wmixp:write ("/client/sel/ctl", "Fullscreen toggle")
765 function ke_view_starting_with_letter (letter)
766 local i,v
768 -- find the view name in history in reverse order
769 for i=#view_hist,1,-1 do
770 v = view_hist[i]
771 if letter == v:sub(1,1) then
772 set_view(v)
773 return true
777 -- otherwise just pick the first view that matches
778 local all = get_tags()
779 for i,v in pairs(all) do
780 if letter == v:sub(1,1) then
781 set_view_index (i)
782 return true
786 return false
789 function ke_handle_action()
790 local actions = { }
791 local seen = {}
793 local n
794 for n in action_hist:walk_reverse() do
795 if not seen[n] then
796 actions[#actions+1] = n
797 seen[n] = 1
801 local v
802 for n,v in pairs(action_handlers) do
803 if not seen[n] then
804 actions[#actions+1] = n
805 seen[n] = 1
809 local text = menu(actions, "action:")
810 if text then
811 log ("Action: " .. text)
812 local act = text
813 local args = nil
814 local si = text:find("%s")
815 if si then
816 act,args = string.match(text .. " ", "(%w+)%s(.+)")
818 if act then
819 local fn = action_handlers[act]
820 if fn then
821 action_hist:add (act)
822 local r, err = pcall (fn, act, args)
823 if not r then
824 log ("WARNING: " .. tostring(err))
832 local key_handlers = {
833 ["*"] = function (key)
834 log ("*: " .. key)
835 end,
837 -- execution and actions
838 ["Mod1-Return"] = function (key)
839 local xterm = get_conf("xterm") or "xterm"
840 execute (xterm .. " &")
841 end,
842 ["Mod1-Shift-Return"] = function (key)
843 local tag = tag_menu()
844 if tag then
845 local xterm = get_conf("xterm") or "xterm"
846 log (" executing on: " .. tag)
847 next_client_goes_to_tag = tag
848 execute (xterm .. " &")
850 end,
851 ["Mod1-a"] = function (key)
852 ke_handle_action()
853 end,
854 ["Mod1-p"] = function (key)
855 local prog = prog_menu()
856 if prog then
857 prog_hist:add(prog:match("([^ ]+)"))
858 execute (prog .. " &")
860 end,
861 ["Mod1-Shift-p"] = function (key)
862 local tag = tag_menu()
863 if tag then
864 local prog = prog_menu()
865 if prog then
866 log (" executing on: " .. tag)
867 next_client_goes_to_tag = tag
868 execute (prog .. " &")
871 end,
872 ["Mod1-Shift-c"] = function (key)
873 write ("/client/sel/ctl", "kill")
874 end,
876 -- HJKL active selection
877 ["Mod1-h"] = function (key)
878 write ("/tag/sel/ctl", "select left")
879 end,
880 ["Mod1-l"] = function (key)
881 write ("/tag/sel/ctl", "select right")
882 end,
883 ["Mod1-j"] = function (key)
884 write ("/tag/sel/ctl", "select down")
885 end,
886 ["Mod1-k"] = function (key)
887 write ("/tag/sel/ctl", "select up")
888 end,
890 -- HJKL movement
891 ["Mod1-Shift-h"] = function (key)
892 write ("/tag/sel/ctl", "send sel left")
893 end,
894 ["Mod1-Shift-l"] = function (key)
895 write ("/tag/sel/ctl", "send sel right")
896 end,
897 ["Mod1-Shift-j"] = function (key)
898 write ("/tag/sel/ctl", "send sel down")
899 end,
900 ["Mod1-Shift-k"] = function (key)
901 write ("/tag/sel/ctl", "send sel up")
902 end,
904 -- floating vs tiled
905 ["Mod1-space"] = function (key)
906 write ("/tag/sel/ctl", "select toggle")
907 end,
908 ["Mod1-Shift-space"] = function (key)
909 write ("/tag/sel/ctl", "send sel toggle")
910 end,
912 -- work spaces (# and @ are wildcards for numbers and letters)
913 ["Mod4-#"] = function (key, num)
914 -- first attempt to find a view that starts with the number requested
915 local num_str = tostring(num)
916 if not ke_view_starting_with_letter (num_str) then
917 -- if we fail, then set it to the index requested
918 set_view_index (num)
920 end,
921 ["Mod4-Shift-#"] = function (key, num)
922 write ("/client/sel/tags", tostring(num))
923 end,
924 ["Mod4-@"] = function (key, letter)
925 ke_view_starting_with_letter (letter)
926 end,
927 ["Mod4-Shift-@"] = function (key, letter)
928 local all = get_tags()
929 local i,v
930 for i,v in pairs(all) do
931 if letter == v:sub(1,1) then
932 write ("/client/sel/tags", v)
933 break
936 end,
937 ["Mod1-comma"] = function (key)
938 set_view_ofs (-1)
939 end,
940 ["Mod1-period"] = function (key)
941 set_view_ofs (1)
942 end,
943 ["Mod1-r"] = function (key)
944 -- got to the last view
945 toggle_view()
946 end,
948 -- switching views and retagging
949 ["Mod1-t"] = function (key)
950 -- got to a view
951 local tag = tag_menu()
952 if tag then
953 set_view (tag)
955 end,
956 ["Mod1-Shift-t"] = function (key)
957 -- move selected client to a tag
958 local tag = tag_menu()
959 if tag then
960 write ("/client/sel/tags", tag)
962 end,
963 ["Mod1-Shift-r"] = function (key)
964 -- move selected client to a tag, and follow
965 local tag = tag_menu()
966 if tag then
967 -- get the current window id
968 local xid = wmixp:read("/client/sel/ctl") or ""
970 -- modify the tag
971 write("/client/sel/tags", tag)
973 -- if the client is still in this tag, then
974 -- it might have been a regexp tag... check
975 local test = wmixp:read("/client/sel/ctl")
976 if not test or test ~= xid then
977 -- if the window moved, follow it
978 set_view(tag)
981 end,
982 ["Mod1-Control-t"] = function (key)
983 log (" TODO: Mod1-Control-t: " .. key)
984 end,
986 -- column modes
987 ["Mod1-d"] = function (key)
988 write("/tag/sel/ctl", "colmode sel default-max")
989 end,
990 ["Mod1-s"] = function (key)
991 write("/tag/sel/ctl", "colmode sel stack-max")
992 end,
993 ["Mod1-m"] = function (key)
994 write("/tag/sel/ctl", "colmode sel stack+max")
995 end,
996 ["Mod1-f"] = function (key)
997 ke_fullscreen_toggle()
998 end,
1000 -- changing client flags
1001 ["Shift-Mod1-f"] = function (key)
1002 log ("setting flags")
1004 local cli = get_client ()
1006 local flags = { "suspend", "raw" }
1007 local current_flags = cli:flags_string()
1009 local what = menu(flags, "current flags: " .. current_flags .. " toggle:")
1011 cli:toggle(what)
1012 end,
1013 ["Mod4-space"] = function (key)
1014 local cli = get_client ()
1015 cli:toggle("raw")
1016 end,
1019 --[[
1020 =pod
1022 =item add_key_handler (key, fn)
1024 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
1026 =cut
1027 --]]
1028 function add_key_handler (key, fn)
1030 if type(key) ~= "string" or type(fn) ~= "function" then
1031 error ("expecting a string and a function")
1034 local onlyKey = key:match("([^-]+)$")
1035 if 0 ~= os.execute("xmodmap -pk | grep -q '(" .. onlyKey .. ")'") then
1036 return warn ("xmodmap -pk doesn't know about '" .. onlyKey .. "'")
1039 if key_handlers[key] then
1040 -- TODO: we may wish to allow multiple handlers for one keypress
1041 error ("key handler already exists for '" .. key .. "'")
1044 key_handlers[key] = fn
1047 --[[
1048 =pod
1050 =item remove_key_handler (key)
1052 Remove an key handler callback function for the given key I<key>.
1054 Returns the handler callback function.
1056 =cut
1057 --]]
1058 function remove_key_handler (key)
1060 local fn = key_handlers[key]
1061 key_handlers[key] = nil
1062 return fn
1065 --[[
1066 =pod
1068 =item remap_key_handler (old_key, new_key)
1070 Remove a key handler callback function from the given key I<old_key>,
1071 and assign it to a new key I<new_key>.
1073 =cut
1074 --]]
1075 function remap_key_handler (old_key, new_key)
1077 local fn = remove_key_handler(old_key)
1079 return add_key_handler (new_key, fn)
1083 -- ------------------------------------------------------------------------
1084 -- update the /keys wmii file with the list of all handlers
1085 local alphabet="abcdefghijklmnopqrstuvwxyz"
1086 function update_active_keys ()
1087 local t = {}
1088 local x, y
1089 for x,y in pairs(key_handlers) do
1090 if x:find("%w") then
1091 local i = x:find("#$")
1092 if i then
1093 local j
1094 for j=0,9 do
1095 t[#t + 1] = x:sub(1,i-1) .. j
1097 else
1098 i = x:find("@$")
1099 if i then
1100 local j
1101 for j=1,alphabet:len() do
1102 local a = alphabet:sub(j,j)
1103 t[#t + 1] = x:sub(1,i-1) .. a
1105 else
1106 t[#t + 1] = tostring(x)
1111 local all_keys = table.concat(t, "\n")
1112 --log ("setting /keys to...\n" .. all_keys .. "\n");
1113 write ("/keys", all_keys)
1116 -- ------------------------------------------------------------------------
1117 -- update the /lbar wmii file with the current tags
1118 function update_displayed_tags ()
1119 -- list of all screens
1120 local screens = get_screens()
1121 if not screens then
1122 update_displayed_tags_on_screen()
1123 return
1126 local i, s
1127 for i,s in pairs(screens) do
1128 update_displayed_tags_on_screen(s)
1132 function tag_display(tag, selected)
1133 return tag
1136 function update_displayed_tags_on_screen(s)
1137 local lbar = "/lbar"
1138 if s then
1139 lbar = "/screen/" .. s .. "/lbar"
1142 -- colours for screen
1143 local fc = get_screen_ctl(s, "focuscolors") or get_ctl("focuscolors") or ""
1144 local nc = get_screen_ctl(s, "normcolors") or get_ctl("normcolors") or ""
1146 -- build up a table of existing tags in the /lbar
1147 local old = {}
1148 local ent
1149 for ent in wmixp:idir (lbar) do
1150 old[ent.name] = 1
1153 -- for all actual tags in use create any entries in /lbar we don't have
1154 -- clear the old table entries if we have them
1155 local cur = get_view(s)
1156 local all = get_tags()
1157 local i,v
1158 for i,v in pairs(all) do
1159 local color = nc
1160 if cur == v then
1161 color = fc
1163 local str = tag_display(v,selected)
1164 if not old[v] then
1165 create (lbar .. "/" .. v, color .. " " .. str)
1167 write (lbar .. "/" .. v, color .. " " .. str)
1168 old[v] = nil
1171 -- ignore widgets on the lbar
1172 for i,v in pairs(widgets) do
1173 if v.bar == 'lbar' then
1174 old[v.name] = nil
1178 -- anything left in the old table should be removed now
1179 for i,v in pairs(old) do
1180 if v then
1181 remove(lbar.."/"..i)
1185 -- this is a hack, and should brobably be rethought
1186 -- the intent is to distinguish the multiple screens
1187 if s then
1188 create ("/screen/"..s.."/lbar/000000000000000000", '-'..s..'-')
1192 function create_tag_widget(name)
1193 local nc = get_ctl("normcolors") or ""
1194 local screens = get_screens()
1195 if not screens then
1196 create ("/lbar/" .. name, nc .. " " .. name)
1197 return
1199 local i, s
1200 for i,s in pairs(screens) do
1201 create ("/screen/"..s.."/lbar/" .. name, nc .. " " .. tag_display(name))
1205 function destroy_tag_widget(name)
1206 local screens = get_screens()
1207 if not screens then
1208 remove ("/lbar/" .. name)
1209 return
1211 local i, s
1212 for i,s in pairs(screens) do
1213 remove ("/screen/"..s.."/lbar/" .. name)
1218 -- ========================================================================
1219 -- EVENT HANDLERS
1220 -- ========================================================================
1222 local widget_ev_handlers = {
1225 --[[
1226 =pod
1228 =item _handle_widget_event (ev, arg)
1230 Top-level event handler for redispatching events to widgets. This event
1231 handler is added for any widget event that currently has a widget registered
1232 for it.
1234 Valid widget events are currently
1236 RightBarMouseDown <buttonnumber> <widgetname>
1237 RightBarClick <buttonnumber> <widgetname>
1239 the "Click" event is sent on mouseup.
1241 The callbacks are given only the button number as their argument, to avoid the
1242 need to reparse.
1244 =cut
1245 --]]
1247 local function _handle_widget_event (ev, arg)
1249 log("_handle_widget_event: " .. tostring(ev) .. " - " .. tostring(arg))
1251 -- parse arg to strip out our widget name
1252 local number,wname = string.match(arg, "(%d+)%s+(.+)")
1254 -- check our dispatch table for that widget
1255 if not wname then
1256 log("Didn't find wname")
1257 return
1260 local wtable = widget_ev_handlers[wname]
1261 if not wtable then
1262 log("No widget cares about" .. wname)
1263 return
1266 local fn = wtable[ev] or wtable["*"]
1267 if fn then
1268 success, err = pcall( fn, ev, tonumber(number) )
1269 if not success then
1270 log("Callback had an error in _handle_widget_event: " .. tostring(err) )
1271 return nil
1273 else
1274 log("no function found for " .. ev)
1278 local ev_handlers = {
1279 ["*"] = function (ev, arg)
1280 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
1281 end,
1283 RightBarClick = _handle_widget_event,
1285 -- process timer events
1286 ProcessTimerEvents = function (ev, arg)
1287 process_timers()
1288 end,
1290 -- exit if another wmiirc started up
1291 Start = function (ev, arg)
1292 if arg then
1293 if arg == "wmiirc" then
1294 -- backwards compatibility with bash version
1295 log (" exiting; pid=" .. tostring(myid))
1296 cleanup()
1297 os.exit (0)
1298 else
1299 -- ignore if it came from us
1300 local pid = string.match(arg, "wmiirc (%d+)")
1301 if pid then
1302 local pid = tonumber (pid)
1303 if not (pid == myid) then
1304 log (" exiting; pid=" .. tostring(myid))
1305 cleanup()
1306 os.exit (0)
1311 end,
1313 -- tag management
1314 CreateTag = function (ev, arg)
1315 log ("CreateTag: " .. arg)
1316 create_tag_widget(arg)
1317 end,
1318 DestroyTag = function (ev, arg)
1319 log ("DestroyTag: " .. arg)
1320 destroy_tag_widget(arg)
1322 -- remove the tag from history
1323 local i,v
1324 for i=#view_hist,1,-1 do
1325 v = view_hist[i]
1326 if arg == v then
1327 table.remove(view_hist,i)
1330 end,
1332 FocusTag = function (ev, arg)
1333 log ("FocusTag: " .. arg)
1335 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1336 if not tag then
1337 return
1340 local file = "/lbar/" .. tag
1341 if scrn and scrn:len() > 0 then
1342 file = "/screen/" .. scrn .. file
1345 local fc = get_screen_ctl(scrn, "focuscolors") or get_ctl("focuscolors") or ""
1346 log ("# echo " .. fc .. " " .. tag .. " | wmiir write " .. file)
1348 str = tag_display(tag,true)
1349 create (file, fc .. " " .. str)
1350 write (file, fc .. " " .. str)
1351 end,
1352 UnfocusTag = function (ev, arg)
1353 log ("UnfocusTag: " .. arg)
1355 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1356 if not tag then
1357 return
1360 local file = "/lbar/" .. tag
1361 if scrn and scrn:len() > 0 then
1362 file = "/screen/" .. scrn .. file
1365 local nc = get_screen_ctl(scrn, "normcolors") or get_ctl("normcolors") or ""
1366 log ("# echo " .. nc .. " " .. tag .. " | wmiir write " .. file)
1368 str = tag_display(tag,true)
1369 create (file, nc .. " " .. str)
1370 write (file, nc .. " " .. str)
1372 -- don't duplicate the last entry
1373 if not (tag == view_hist[#view_hist]) then
1374 view_hist[#view_hist+1] = tag
1376 -- limit to view_hist_max
1377 if #view_hist > view_hist_max then
1378 table.remove(view_hist, 1)
1381 end,
1383 -- key event handling
1384 Key = function (ev, arg)
1385 log ("Key: " .. arg)
1386 local magic = nil
1387 -- can we find an exact match?
1388 local fn = key_handlers[arg]
1389 if not fn then
1390 local key = arg:gsub("-%d$", "-#")
1391 -- can we find a match with a # wild card for the number
1392 fn = key_handlers[key]
1393 if fn then
1394 -- convert the trailing number to a number
1395 magic = tonumber(arg:match("-(%d)$"))
1398 if not fn then
1399 local key = arg:gsub("-%a$", "-@")
1400 -- can we find a match with a @ wild card for a letter
1401 fn = key_handlers[key]
1402 if fn then
1403 -- split off the trailing letter
1404 magic = arg:match("-(%a)$")
1407 if not fn then
1408 -- everything else failed, try default match
1409 fn = key_handlers["*"]
1411 if fn then
1412 local r, err = pcall (fn, arg, magic)
1413 if not r then
1414 log ("WARNING: " .. tostring(err))
1417 end,
1419 -- mouse handling on the lbar
1420 LeftBarClick = function (ev, arg)
1421 local button,tag = string.match(arg, "(%w+)%s+(%S+)")
1422 set_view (tag)
1423 end,
1425 -- focus updates
1426 ClientFocus = function (ev, arg)
1427 log ("ClientFocus: " .. arg)
1428 client_focused (arg)
1429 end,
1430 ColumnFocus = function (ev, arg)
1431 log ("ColumnFocus: " .. arg)
1432 end,
1434 -- client handling
1435 CreateClient = function (ev, arg)
1436 if next_client_goes_to_tag then
1437 local tag = next_client_goes_to_tag
1438 local cli = arg
1439 next_client_goes_to_tag = nil
1440 write ("/client/" .. cli .. "/tags", tag)
1441 set_view(tag)
1443 client_created (arg)
1444 end,
1445 DestroyClient = function (ev, arg)
1446 client_destoryed (arg)
1447 end,
1449 -- urgent tag
1450 UrgentTag = function (ev, arg)
1451 log ("UrgentTag: " .. arg)
1452 write ("/lbar/" .. arg, "*" .. arg);
1453 end,
1454 NotUrgentTag = function (ev, arg)
1455 log ("NotUrgentTag: " .. arg)
1456 write ("/lbar/" .. arg, arg);
1457 end,
1459 -- notifications
1460 Unresponsive = function (ev, arg)
1461 log ("Unresponsive: " .. arg)
1462 -- TODO ask the user if it shoudl be killed off
1463 end,
1465 Notice = function (ev, arg)
1466 log ("Notice: " .. arg)
1467 -- TODO send to the message plugin (or implement there)
1468 end,
1470 -- /
1473 --[[
1474 =pod
1476 =item add_widget_event_handler (wname, ev, fn)
1478 Add an event handler callback for the I<ev> event on the widget named I<wname>
1480 =cut
1481 --]]
1483 function add_widget_event_handler (wname, ev, fn)
1484 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
1485 error ("expecting string for widget name, string for event name and a function callback")
1488 -- Make sure the widget event handler is present
1489 if not ev_handlers[ev] then
1490 ev_handlers[ev] = _handle_widget_event
1493 if not widget_ev_handlers[wname] then
1494 widget_ev_handlers[wname] = { }
1497 if widget_ev_handlers[wname][ev] then
1498 -- TODO: we may wish to allow multiple handlers for one event
1499 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
1502 widget_ev_handlers[wname][ev] = fn
1505 --[[
1506 =pod
1508 =item remove_widget_event_handler (wname, ev)
1510 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1512 =cut
1513 --]]
1514 function remove_event_handler (wname, ev)
1516 if not widget_ev_handlers[wname] then
1517 return
1520 widget_ev_handlers[wname][ev] = nil
1523 --[[
1524 =pod
1526 =item add_event_handler (ev, fn)
1528 Add an event handler callback function, I<fn>, for the given event I<ev>.
1530 =cut
1531 --]]
1532 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1533 function add_event_handler (ev, fn)
1534 if type(ev) ~= "string" or type(fn) ~= "function" then
1535 error ("expecting a string and a function")
1538 if ev_handlers[ev] then
1539 -- TODO: we may wish to allow multiple handlers for one event
1540 error ("event handler already exists for '" .. ev .. "'")
1544 ev_handlers[ev] = fn
1547 --[[
1548 =pod
1550 =item remove_event_handler (ev)
1552 Remove an event handler callback function for the given event I<ev>.
1554 =cut
1555 --]]
1556 function remove_event_handler (ev)
1558 ev_handlers[ev] = nil
1562 -- ========================================================================
1563 -- MAIN INTERFACE FUNCTIONS
1564 -- ========================================================================
1566 local config = {
1567 xterm = 'x-terminal-emulator',
1568 xlock = "xscreensaver-command --lock",
1569 debug = false,
1572 -- ------------------------------------------------------------------------
1573 -- write configuration to /ctl wmii file
1574 -- wmii.set_ctl({ "var" = "val", ...})
1575 -- wmii.set_ctl("var, "val")
1576 function set_ctl (first,second)
1577 if type(first) == "table" and second == nil then
1578 local x, y
1579 for x, y in pairs(first) do
1580 write ("/ctl", x .. " " .. y)
1583 elseif type(first) == "string" and type(second) == "string" then
1584 write ("/ctl", first .. " " .. second)
1586 else
1587 error ("expecting a table or two string arguments")
1591 -- ------------------------------------------------------------------------
1592 -- read a value from /ctl wmii file
1593 -- table = wmii.get_ctl()
1594 -- value = wmii.get_ctl("variable")
1595 function get_ctl (name)
1596 local s
1597 local t = {}
1598 for s in iread("/ctl") do
1599 local var,val = s:match("(%w+)%s+(.+)")
1600 if var == name then
1601 return val
1603 t[var] = val
1605 if not name then
1606 return t
1608 return nil
1611 -- ------------------------------------------------------------------------
1612 -- write configuration to /screen/*/ctl wmii file
1613 -- wmii.set_screen_ctl("screen", { "var" = "val", ...})
1614 -- wmii.set_screen_ctl("screen", "var, "val")
1615 function set_screen_ctl (screen, first, second)
1616 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1617 if not screen then
1618 error ("screen is not set")
1619 elseif type(first) == "table" and second == nil then
1620 local x, y
1621 for x, y in pairs(first) do
1622 write (ctl, x .. " " .. y)
1625 elseif type(first) == "string" and type(second) == "string" then
1626 write (ctl, first .. " " .. second)
1628 else
1629 error ("expecting a screen name, followed by a table or two string arguments")
1633 -- ------------------------------------------------------------------------
1634 -- read a value from /screen/*/ctl wmii file
1635 -- table = wmii.get_screen_ctl("screen")
1636 -- value = wmii.get_screen_ctl("screen", "variable")
1637 function get_screen_ctl (screen, name)
1638 local s
1639 local t = {}
1640 if not screen then
1641 return nil
1643 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1644 for s in iread(ctl) do
1645 local var,val = s:match("(%w+)%s+(.+)")
1646 if var == name then
1647 return val
1649 -- sometimes first line is the name of the entry
1650 -- in which case there will be no space
1651 t[var or ""] = val
1653 if not name then
1654 return t
1656 return nil
1659 -- ------------------------------------------------------------------------
1660 -- set an internal wmiirc.lua variable
1661 -- wmii.set_conf({ "var" = "val", ...})
1662 -- wmii.set_conf("var, "val")
1663 function set_conf (first,second)
1664 if type(first) == "table" and second == nil then
1665 local x, y
1666 for x, y in pairs(first) do
1667 config[x] = y
1670 elseif type(first) == "string"
1671 and (type(second) == "string"
1672 or type(second) == "number"
1673 or type(second) == "boolean") then
1674 config[first] = second
1676 else
1677 error ("expecting a table, or string and string/number as arguments")
1681 -- ------------------------------------------------------------------------
1682 -- read an internal wmiirc.lua variable
1683 function get_conf (name)
1684 if name then
1685 return config[name]
1687 return config
1690 -- ========================================================================
1691 -- THE EVENT LOOP
1692 -- ========================================================================
1694 -- the event loop instance
1695 local el = eventloop.new()
1696 local event_read_fd = -1
1697 local wmiirc_running = false
1698 local event_read_start = 0
1700 -- ------------------------------------------------------------------------
1701 -- start/restart the core event reading process
1702 local function start_event_reader ()
1703 -- prevent adding two readers
1704 if event_read_fd ~= -1 then
1705 if el:check_exec(event_read_fd) then
1706 return
1709 -- prevert rapid restarts
1710 local now = os.time()
1711 if os.difftime(now, event_read_start) < 5 then
1712 log("wmii: detected rapid restart of /event reader")
1713 local cmd = wmiir .. " ls /ctl"
1714 if os.execute(cmd) ~= 0 then
1715 log("wmii: cannot confirm communication with wmii, shutting down!")
1716 wmiirc_running = false
1717 return
1719 log("wmii: but things look ok, so we will restart it")
1721 event_read_start = now
1723 -- start a new event reader
1724 log("wmii: starting /event reading process")
1725 event_read_fd = el:add_exec (wmiir .. " read /event",
1726 function (line)
1727 local line = line or "nil"
1729 -- try to split off the argument(s)
1730 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1731 if not ev then
1732 ev = line
1735 -- now locate the handler function and call it
1736 local fn = ev_handlers[ev] or ev_handlers["*"]
1737 if fn then
1738 local r, err = pcall (fn, ev, arg)
1739 if not r then
1740 log ("WARNING: " .. tostring(err))
1745 log("wmii: ... fd=" .. tostring(event_read_fd))
1748 -- ------------------------------------------------------------------------
1749 -- run the event loop and process events, this function does not exit
1750 function run_event_loop ()
1751 -- stop any other instance of wmiirc
1752 wmixp:write ("/event", "Start wmiirc " .. tostring(myid))
1754 log("wmii: updating lbar")
1756 update_displayed_tags ()
1758 log("wmii: updating rbar")
1760 update_displayed_widgets ()
1762 log("wmii: updating active keys")
1764 update_active_keys ()
1766 log("wmii: starting event loop")
1767 wmiirc_running = true
1768 while wmiirc_running do
1769 start_event_reader()
1770 local sleep_for = process_timers()
1771 el:run_loop(sleep_for)
1773 log ("wmii: exiting")
1776 -- ========================================================================
1777 -- PLUGINS API
1778 -- ========================================================================
1780 api_version = 0.1 -- the API version we export
1782 plugins = {} -- all plugins that were loaded
1784 -- ------------------------------------------------------------------------
1785 -- plugin loader which also verifies the version of the api the plugin needs
1787 -- here is what it does
1788 -- - does a manual locate on the file using package.path
1789 -- - reads in the file w/o using the lua interpreter
1790 -- - locates api_version=X.Y string
1791 -- - makes sure that api_version requested can be satisfied
1792 -- - if the plugins is available it will set variables passed in
1793 -- - it then loads the plugin
1795 -- TODO: currently the api_version must be in an X.Y format, but we may want
1796 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1798 function load_plugin(name, vars)
1799 local backup_path = package.path or "./?.lua"
1801 log ("loading " .. name)
1803 -- this is the version we want to find
1804 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1805 if (not api_major) or (not api_minor) then
1806 log ("WARNING: could not parse api_version in core/wmii.lua")
1807 return nil
1810 -- first find the plugin file
1811 local s, path_match, full_name, file
1812 for i,s in pairs(plugin_paths) do
1813 -- try to locate the files locally
1814 local fn = s:gsub("%?", name)
1815 file = io.open(fn, "r")
1816 if file then
1817 path_match = s
1818 full_name = fn
1819 break
1823 -- read it in
1824 local txt
1825 if file then
1826 txt = file:read("*all")
1827 file:close()
1830 if not txt then
1831 log ("WARNING: could not load plugin '" .. name .. "'")
1832 return nil
1835 -- find the api_version line
1836 local line, plugin_version
1837 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1838 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1839 if plugin_version then
1840 break
1844 if not plugin_version then
1845 log ("WARNING: could not find api_version string in plugin '" .. name .. "'")
1846 return nil
1849 -- decompose the version string
1850 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1851 if (not plugin_major) or (not plugin_minor) then
1852 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1853 return nil
1856 -- make a version test
1857 if plugin_major ~= api_major then
1858 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1859 .. " (api " .. tonumber(api_version) .. ")")
1860 return nil
1863 if plugin_minor > api_minor then
1864 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1865 .. " (api " .. tonumber(api_version) .. ")")
1866 return nil
1869 -- the configuration parameters before loading
1870 if type(vars) == "table" then
1871 local var, val
1872 for var,val in pairs(vars) do
1873 local success = pcall (set_conf, name .. "." .. var, val)
1874 if not success then
1875 log ("WARNING: bad variable {" .. tostring(var) .. ", " .. tostring(val) .. "} "
1876 .. "given; loading '" .. name .. "' plugin failed.")
1877 return nil
1882 -- actually load the module, but use only the path where we though it should be
1883 package.path = path_match
1884 local success,what = pcall (require, name)
1885 package.path = backup_path
1886 if not success then
1887 log ("WARNING: failed to load '" .. name .. "' plugin")
1888 log (" - path: " .. tostring(path_match))
1889 log (" - file: " .. tostring(full_name))
1890 log (" - plugin's api_version: " .. tostring(plugin_version))
1891 log (" - reason: " .. tostring(what))
1892 return nil
1895 -- success
1896 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1897 plugins[name] = what
1898 return what
1901 -- ------------------------------------------------------------------------
1902 -- widget template
1903 widget = {}
1904 widgets = {}
1906 -- ------------------------------------------------------------------------
1907 -- create a widget object and add it to the wmii /rbar
1909 -- examples:
1910 -- widget = wmii.widget:new ("999_clock")
1911 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1912 function widget:new (name, fn, bar)
1913 local o = {}
1915 if type(name) == "string" then
1916 o.name = name
1917 if type(fn) == "function" then
1918 o.fn = fn
1920 o.bar = bar or "rbar"
1921 else
1922 error ("expected name followed by an optional function as arguments")
1925 setmetatable (o,self)
1926 self.__index = self
1927 self.__gc = function (o) o:hide() end
1929 widgets[name] = o
1931 o:show()
1932 return o
1935 -- ------------------------------------------------------------------------
1936 -- stop and destroy the timer
1937 function widget:delete ()
1938 widgets[self.name] = nil
1939 self:hide()
1942 -- ------------------------------------------------------------------------
1943 -- displays or updates the widget text
1945 -- examples:
1946 -- w:show("foo")
1947 -- w:show("foo", "#888888 #222222 #333333")
1948 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1950 function widget:show (txt, colors)
1951 local colors = colors or get_ctl("normcolors") or ""
1952 local txt = txt or self.txt or ""
1953 local towrite = txt
1954 if colors then
1955 towrite = colors .. " " .. towrite
1957 if not self.txt then
1958 create ('/'..self.bar..'/'.. self.name, towrite)
1959 else
1960 write ('/'..self.bar..'/'.. self.name, towrite)
1962 self.txt = txt
1965 -- ------------------------------------------------------------------------
1966 -- hides a widget and removes it from the bar
1967 function widget:hide ()
1968 if self.txt then
1969 remove ('/'..self.bar ..'/'.. self.name)
1970 self.txt = nil
1974 --[[
1975 =pod
1977 =item widget:add_event_handler (ev, fn)
1979 Add an event handler callback for this widget, using I<fn> for event I<ev>
1981 =cut
1982 --]]
1984 function widget:add_event_handler (ev, fn)
1985 add_widget_event_handler( self.name, ev, fn)
1989 -- ------------------------------------------------------------------------
1990 -- remove all /rbar entries that we don't have widget objects for
1991 function update_displayed_widgets ()
1992 -- colours for /rbar
1993 local nc = get_ctl("normcolors") or ""
1995 -- build up a table of existing tags in the /lbar
1996 local old = {}
1997 local s
1998 for s in wmixp:idir ("/rbar") do
1999 old[s.name] = 1
2002 -- for all actual widgets in use we want to remove them from the old list
2003 local i,v
2004 for i,v in pairs(widgets) do
2005 old[v.name] = nil
2008 -- anything left in the old table should be removed now
2009 for i,v in pairs(old) do
2010 if v then
2011 remove("/rbar/"..i)
2016 -- ------------------------------------------------------------------------
2017 -- create a new program and for each line it generates call the callback function
2018 -- returns fd which can be passed to kill_exec()
2019 function add_exec (command, callback)
2020 return el:add_exec (command, callback)
2023 -- ------------------------------------------------------------------------
2024 -- terminates a program spawned off by add_exec()
2025 function kill_exec (fd)
2026 return el:kill_exec (fd)
2029 -- ------------------------------------------------------------------------
2030 -- timer template
2031 timer = {}
2032 local timers = {}
2034 -- ------------------------------------------------------------------------
2035 -- create a timer object and add it to the event loop
2037 -- examples:
2038 -- timer:new (my_timer_fn)
2039 -- timer:new (my_timer_fn, 15)
2040 function timer:new (fn, seconds)
2041 local o = {}
2043 if type(fn) == "function" then
2044 o.fn = fn
2045 else
2046 error ("expected function followed by an optional number as arguments")
2049 setmetatable (o,self)
2050 self.__index = self
2051 self.__gc = function (o) o:stop() end
2053 -- add the timer
2054 timers[#timers+1] = o
2056 if seconds then
2057 o:resched(seconds)
2059 return o
2062 -- ------------------------------------------------------------------------
2063 -- stop and destroy the timer
2064 function timer:delete ()
2065 self:stop()
2066 local i,t
2067 for i,t in pairs(timers) do
2068 if t == self then
2069 table.remove (timers,i)
2070 return
2075 -- ------------------------------------------------------------------------
2076 -- run the timer given new interval
2077 function timer:resched (seconds)
2078 local seconds = seconds or self.interval
2079 if not (type(seconds) == "number") then
2080 error ("timer:resched expected number as argument")
2083 local now = tonumber(os.date("%s"))
2085 self.interval = seconds
2086 self.next_time = now + seconds
2088 -- resort the timer list
2089 table.sort (timers, timer.is_less_then)
2092 -- helper for sorting timers
2093 function timer:is_less_then(another)
2094 if not self.next_time then
2095 return false -- another is smaller, nil means infinity
2097 elseif not another.next_time then
2098 return true -- self is smaller, nil means infinity
2100 elseif self.next_time < another.next_time then
2101 return true -- self is smaller than another
2104 return false -- another is smaller then self
2107 -- ------------------------------------------------------------------------
2108 -- stop the timer
2109 function timer:stop ()
2110 self.next_time = nil
2112 -- resort the timer list
2113 table.sort (timers, timer.is_less_then)
2116 -- ------------------------------------------------------------------------
2117 -- figure out how long before the next event
2118 function time_before_next_timer_event()
2119 local tmr = timers[1]
2120 if tmr and tmr.next_time then
2121 local now = tonumber(os.date("%s"))
2122 local seconds = tmr.next_time - now
2123 if seconds > 0 then
2124 return seconds
2127 return 0 -- sleep for ever
2130 -- ------------------------------------------------------------------------
2131 -- handle outstanding events
2132 function process_timers ()
2133 local now = tonumber(os.date("%s"))
2134 local torun = {}
2135 local i,tmr
2137 for i,tmr in pairs (timers) do
2138 if not tmr then
2139 -- prune out removed timers
2140 table.remove(timers,i)
2141 break
2143 elseif not tmr.next_time then
2144 -- break out once we find a timer that is stopped
2145 break
2147 elseif tmr.next_time > now then
2148 -- break out once we get to the future
2149 break
2152 -- this one is good to go
2153 torun[#torun+1] = tmr
2156 for i,tmr in pairs (torun) do
2157 tmr:stop()
2158 local status,new_interval = pcall (tmr.fn, tmr)
2159 if status then
2160 new_interval = new_interval or self.interval
2161 if new_interval and (new_interval ~= -1) then
2162 tmr:resched(new_interval)
2164 else
2165 log ("ERROR: " .. tostring(new_interval))
2169 local sleep_for = time_before_next_timer_event()
2170 return sleep_for
2173 -- ------------------------------------------------------------------------
2174 -- cleanup everything in preparation for exit() or exec()
2175 function cleanup ()
2177 local i,v,tmr,p
2179 log ("wmii: stopping timer events")
2181 for i,tmr in pairs (timers) do
2182 pcall (tmr.delete, tmr)
2184 timers = {}
2186 log ("wmii: terminating eventloop")
2188 pcall(el.kill_all,el)
2190 log ("wmii: disposing of widgets")
2192 -- dispose of all widgets
2193 for i,v in pairs(widgets) do
2194 pcall(v.delete,v)
2196 timers = {}
2198 -- FIXME: it doesn't seem to do what I want
2199 --[[
2200 log ("wmii: releasing plugins")
2202 for i,p in pairs(plugins) do
2203 if p.cleanup then
2204 pcall (p.cleanup, p)
2207 plugins = {}
2208 --]]
2210 log ("wmii: dormant")
2211 wmiirc_running = false
2214 -- ========================================================================
2215 -- CLIENT HANDLING
2216 -- ========================================================================
2218 --[[
2219 -- Notes on client tracking
2221 -- When a client is created wmii sends us a CreateClient message, and
2222 -- we in turn create a 'client' object and store it in the 'clients'
2223 -- table indexed by the client's ID.
2225 -- Each client object stores the following:
2226 -- .xid - the X client ID
2227 -- .pid - the process ID
2228 -- .prog - program object representing the process
2230 -- The client and program objects track the following modes for each program:
2232 -- raw mode:
2233 -- - for each client window
2234 -- - Mod4-space toggles the state between normal and raw
2235 -- - Mod1-f raw also toggles the state
2236 -- - in raw mode all input goes to the client, except for Mod4-space
2237 -- - a focused client with raw mode enabled is put into raw mode
2239 -- suspend mode:
2240 -- - for each program
2241 -- - Mod1-f suspend toggles the state for current client's program
2242 -- - a focused client, whose program was previous suspended is resumed
2243 -- - an unfocused client, with suspend enabled, will be suspended
2244 -- - suspend/resume is done by sending the STOP/CONT signals to the PID
2245 --]]
2247 function xid_to_pid (xid)
2248 local cmd = "xprop -id " .. tostring(xid) .. " _NET_WM_PID"
2249 local file = io.popen (cmd)
2250 local out = file:read("*a")
2251 file:close()
2252 local pid = out:match("^_NET_WM_PID.*%s+=%s+(%d+)%s+$")
2253 return tonumber(pid)
2256 local focused_xid = nil
2257 local clients = {} -- table of client objects indexed by xid
2258 local programs = {} -- table of program objects indexed by pid
2259 local mode_widget = widget:new ("999_client_mode")
2261 -- make programs table have weak values
2262 -- programs go away as soon as no clients point to it
2263 local programs_mt = {}
2264 setmetatable(programs, programs_mt)
2265 programs_mt.__mode = 'v'
2267 -- program class
2268 program = {}
2269 function program:new (pid)
2270 -- make an object
2271 local o = {}
2272 setmetatable (o,self)
2273 self.__index = self
2274 self.__gc = function (old) old:cont() end
2275 -- initialize the new object
2276 o.pid = pid
2277 -- suspend mode
2278 o.suspend = {}
2279 o.suspend.toggle = function (prog)
2280 prog.suspend.enabled = not prog.suspend.enabled
2282 o.suspend.enabled = false -- if true, defocusing suspends (SIGSTOP)
2283 o.suspend.active = true -- if true, focusing resumes (SIGCONT)
2284 return o
2287 function program:stop ()
2288 if not self.suspend.active then
2289 local cmd = "kill -STOP " .. tostring(self.pid)
2290 log (" executing: " .. cmd)
2291 os.execute (cmd)
2292 self.suspend.active = true
2296 function program:cont ()
2297 if self.suspend.active then
2298 local cmd = "kill -CONT " .. tostring(self.pid)
2299 log (" executing: " .. cmd)
2300 os.execute (cmd)
2301 self.suspend.active = false
2305 function get_program (pid)
2306 local prog = programs[pid]
2307 if pid and not prog then
2308 prog = program:new (pid)
2309 programs[pid] = prog
2311 return prog
2314 -- client class
2315 client = {}
2316 function client:new (xid)
2317 local pid = xid_to_pid(xid)
2318 if not pid then
2319 log ("WARNING: failed to convert XID " .. tostring(xid) .. " to a PID")
2320 return
2322 -- make an object
2323 local o = {}
2324 setmetatable (o,self)
2325 self.__index = function (t,k)
2326 if k == 'suspend' then -- suspend mode is tracked per program
2327 return t.prog.suspend
2329 return self[k]
2331 self.__gc = function (old) old.prog=nil end
2332 -- initialize the new object
2333 o.xid = xid
2334 o.pid = pid
2335 o.prog = get_program (pid)
2336 -- raw mode
2337 o.raw = {}
2338 o.raw.toggle = function (cli)
2339 cli.raw.enabled = not cli.raw.enabled
2340 cli:set_raw_mode()
2342 o.raw.enabled = false -- if true, raw mode enabled when client is focused
2343 return o
2346 function client:stop ()
2347 if self.suspend.enabled then
2348 self.prog:stop()
2352 function client:cont ()
2353 self.prog:cont()
2356 function client:set_raw_mode()
2357 if not self or not self.raw.enabled then -- normal mode
2358 update_active_keys ()
2359 else -- raw mode
2360 write ("/keys", "Mod4-space")
2364 function client:toggle(what)
2365 if what and self[what] then
2366 local ctl = self[what]
2368 ctl.toggle (self)
2370 log ("xid=" .. tostring (xid)
2371 .. " pid=" .. tostring (self.pid) .. " (" .. tostring (self.prog.pid) .. ")"
2372 .. " what=" .. tostring (what)
2373 .. " enabled=" .. tostring(ctl["enabled"]))
2375 mode_widget:show (self:flags_string())
2378 function client:flags_string()
2379 local ret = ''
2380 if self.suspend.enabled then ret = ret .. "s" else ret = ret .. "-" end
2381 if self.raw.enabled then ret = ret .. "r" else ret = ret .. "-" end
2382 return ret
2385 function get_client (xid)
2386 local xid = xid or wmixp:read("/client/sel/ctl")
2387 local cli = clients[xid]
2388 if not cli then
2389 cli = client:new (xid)
2390 clients[xid] = cli
2392 return cli
2395 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2396 function client_created (xid)
2397 log ("-client_created " .. tostring(xid))
2398 return get_client(xid)
2401 function client_destoryed (xid)
2402 log ("-client_destoryed " .. tostring(xid))
2403 if clients[xid] then
2404 local cli = clients[xid]
2405 clients[xid] = nil
2406 log (" del pid: " .. tostring(cli.pid))
2407 cli:cont()
2409 if focused_xid == xid then
2410 focused_xid = nil
2414 function client_focused (xid)
2415 log ("-client_focused " .. tostring(xid))
2416 -- return the current focused xid if nil is passed
2417 if type(xid) ~= 'string' or not xid:match("0x%x*$") then
2418 return focused_xid
2420 -- do nothing if the same xid
2421 if focused_xid == xid then
2422 return clients[xid]
2425 local old = clients[focused_xid]
2426 local new = get_client(xid)
2428 -- handle raw mode switch
2429 if not old or ( old and new and old.raw.enabled ~= new.raw.enabled ) then
2430 new:set_raw_mode()
2433 -- do nothing if the same pid
2434 if old and new and old.pid == new.pid then
2435 mode_widget:show (new:flags_string())
2436 return clients[xid]
2439 if old then
2440 --[[
2441 log (" old pid: " .. tostring(old.pid)
2442 .. " xid: " .. tostring(old.xid)
2443 .. " flags: " .. old:flags_string())
2444 ]]--
2445 old:stop()
2448 if new then
2449 --[[
2450 log (" new pid: " .. tostring(new.pid)
2451 .. " xid: " .. tostring(new.xid)
2452 .. " flags: " .. new:flags_string())
2453 ]]--
2454 new:cont()
2457 mode_widget:show (new:flags_string())
2458 focused_xid = xid
2459 return new
2463 -- ========================================================================
2464 -- DOCUMENTATION
2465 -- ========================================================================
2467 --[[
2468 =pod
2470 =back
2472 =head1 ENVIRONMENT
2474 =over 4
2476 =item WMII_ADDRESS
2478 Used to determine location of wmii's listen socket.
2480 =back
2482 =head1 SEE ALSO
2484 L<wmii(1)>, L<lua(1)>
2486 =head1 AUTHOR
2488 Bart Trojanowski B<< <bart@jukie.net> >>
2490 =head1 COPYRIGHT AND LICENSE
2492 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
2494 This is free software. You may redistribute copies of it under the terms of
2495 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
2496 is NO WARRANTY, to the extent permitted by law.
2498 =cut
2499 --]]
2500 -- vim: set noet ts=8 sw=8 :