fix new battery plugin to work better with multiple batteries present
[wmiirc-lua.git] / src / core / wmii.lua.in
blobf369fb465cc80485ead5283e1e235bc1dc9563cc
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 -- returns boolean indicating if path is a directory
119 local function is_directory(path)
120 if have_posix then
121 local stat = posix.stat(wmiidir)
122 return stat and stat.type == "directory"
123 else
124 local path = path:gsub('([\\"])', '\\%1')
125 local cmd = '[ -d "' .. path .. '" ]'
126 local rc = os.execute (cmd)
127 return rc == 0
131 -- ========================================================================
132 -- MODULE VARIABLES
133 -- ========================================================================
135 -- wmiir points to the wmiir executable
136 -- TODO: need to make sure that wmiir is in path, and if not find it
137 local wmiir = "wmiir"
139 -- wimenu points to the wimenu
140 local wimenu = "wimenu"
141 if 0 ~= os.execute("which wimenu") then
142 if 0 ~= os.execute("which dmenu") then
143 error ("could not find wimenu or dmenu in the PATH")
145 wimenu = "dmenu"
148 -- wmii_adr is the address we use when connecting using ixp
149 -- TODO: hide this magic in luaixp
150 local wmii_adr = os.getenv("WMII_ADDRESS") or ""
151 if wmii_adr == "" then
152 local user = os.getenv("USER") or ""
153 if user == "" then
154 local cmd = "id -n -u"
155 local file = io.popen (cmd)
156 if file then
157 user = file:read("*l") or ""
158 file:close()
160 if not user or user == "" then
161 error ("no WMII_ADDRESS environment variable set, and "
162 .. "could not determine user name for socket location")
165 local disp = os.getenv("DISPLAY") or ":0.0"
166 wmii_adr = "unix!/tmp/ns." .. user .. "."
167 .. disp:match("(:%d+)") .. "/wmii"
170 -- wmixp is the ixp context we use to talk to wmii
171 local wmixp = ixp.new(wmii_adr)
173 -- history of previous views, view_hist[#view_hist] is the last one
174 local view_hist = {} -- sorted with 1 being the oldest
175 local view_hist_max = 50 -- max number to keep track of
177 -- allow for a client to be forced to a tag
178 local next_client_goes_to_tag = nil
180 -- program and action histories
181 local prog_hist = history.new (20)
182 local action_hist = history.new(10)
184 -- where to find plugins
185 plugin_paths = {}
187 table.insert(plugin_paths, wmiidir .. "/plugins/?.so")
188 table.insert(plugin_paths, wmiidir .. "/plugins/?.lua")
190 for path in string.gmatch(package.path, "[^;]+") do
191 local wmiidir = path:gsub("%?.*$", "wmii")
192 if is_directory(wmiidir) then
193 local path = path:gsub("%?", "wmii/?")
194 table.insert(plugin_paths, path)
198 for path in string.gmatch(package.cpath, "[^;]+") do
199 local wmiidir = path:gsub("%?.*$", "wmii")
200 if is_directory(wmiidir) then
201 local path = path:gsub("%?", "wmii/?")
202 table.insert(plugin_paths, path)
206 -- where to find wmiirc (see find_wmiirc())
207 wmiirc_path = wmiidir .. "/wmiirc.lua;"
208 .. wmiidir .. "/wmiirc;"
209 .. "%RC_DIR%/wmiirc.lua;"
210 .. "%RC_DIR%/wmiirc"
212 -- ========================================================================
213 -- LOCAL HELPERS
214 -- ========================================================================
216 --[[
217 =pod
219 =item log ( str )
221 log the message provided in c<str>
223 currently just writes to io.stderr
225 =cut
226 --]]
227 function log (str)
228 if get_conf("debug") then
229 io.stderr:write (str .. "\n")
235 --[[
236 =pod
238 =item warn ( str )
240 generate an error message, c<str>, but do not terminate lua
242 returns the string argument
244 =cut
245 --]]
246 function warn (str)
247 pcall(error,str)
248 return str
253 --[[
254 =pod
256 =item execute ( cmd )
258 setsid wrapper for os.execute(c<cmd>)
260 =cut
261 --]]
262 local wmiir_has_setsid = nil
263 function execute (cmd)
264 log (" executing: " .. cmd)
265 if wmiir_has_setsid == nil then
266 -- test if wmiir has setsid support
267 local rc = os.execute (wmiir .. " setsid true")
268 wmiir_has_setsid = (rc == 0)
269 log ("wmiir " .. (wmiir_has_setsid and "has" or "does not have") .. " setsid support")
272 if wmiir_has_setsid then
273 cmd = wmiir .. " setsid " .. cmd
276 log (" ... " .. cmd)
277 local rc = os.execute (cmd)
278 log (" ... rc=" .. tostring(rc))
279 return rc
284 --[[
285 =pod
287 =item shell_execute ( cmd )
289 setsid wrapper for os.execute(c<cmd>)
290 like execute() but runs under a subshell
292 =cut
293 --]]
294 function shell_execute (cmd)
295 cmd = cmd:gsub('([\\"])', '\\%1')
296 cmd = 'sh -c "' .. cmd .. '"'
298 return execute(cmd)
303 --[[
304 =pod
306 =item find_wmiirc ( )
308 Locates the wmiirc script. It looks in %HOME_WMII% and %RC_DIR%
309 for the first lua script bearing the name wmiirc.lua or wmiirc. Returns
310 first match.
312 =cut
313 --]]
314 function find_wmiirc()
315 local fn
316 for fn in string.gmatch(wmiirc_path, "[^;]+") do
317 -- try to locate the files locally
318 local file = io.open(fn, "r")
319 if file then
320 local txt = file:read("*line")
321 file:close()
322 if type(txt) == 'string' and txt:match("lua") then
323 return fn
327 return nil
331 -- ========================================================================
332 -- MAIN ACCESS FUNCTIONS
333 -- ========================================================================
335 --[[
336 =pod
338 =item ls ( dir, fmt )
340 List the wmii filesystem directory provided in C<dir>, in the format specified
341 by C<fmt>.
343 Returns an iterator of TODO
345 =cut
346 --]]
347 function ls (dir, fmt)
348 local verbose = fmt and fmt:match("l")
350 local s = wmixp:stat(dir)
351 if not s then
352 return function () return nil end
354 if s.modestr:match("^[^d]") then
355 return function ()
356 return stat2str(verbose, s)
360 local itr = wmixp:idir (dir)
361 if not itr then
362 --return function ()
363 return nil
364 --end
368 return function ()
369 local s = itr()
370 if s then
371 return stat2str(verbose, s)
373 return nil
377 local function stat2str(verbose, stat)
378 if verbose then
379 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
380 else
381 if stat.modestr:match("^d") then
382 return stat.name .. "/"
383 else
384 return stat.name
389 -- ------------------------------------------------------------------------
390 -- read all contents of a wmii virtual file
391 function read (file)
392 return wmixp:read (file)
395 -- ------------------------------------------------------------------------
396 -- return an iterator which walks all the lines in the file
398 -- example:
399 -- for event in wmii.iread("/ctl")
400 -- ...
401 -- end
403 -- NOTE: don't use iread for files that could block, as this will interfere
404 -- with timer processing and event delivery. Instead fork off a process to
405 -- execute wmiir and read back the responses via callback.
406 function iread (file)
407 return wmixp:iread(file)
410 -- ------------------------------------------------------------------------
411 -- create a wmii file, optionally write data to it
412 function create (file, data)
413 wmixp:create(file, data)
416 -- ------------------------------------------------------------------------
417 -- remove a wmii file
418 function remove (file)
419 wmixp:remove(file)
422 -- ------------------------------------------------------------------------
423 -- write a value to a wmii virtual file system
424 function write (file, value)
425 wmixp:write (file, value)
428 -- ------------------------------------------------------------------------
429 -- setup a table describing the menu command
430 local function menu_cmd (prompt)
431 local cmdt = { wimenu }
432 if prompt then
433 cmdt[#cmdt+1] = "-p"
434 cmdt[#cmdt+1] = "'" .. prompt .. "'"
437 if wimenu == "dmenu" then
438 cmdt[#cmdt+1] = "-b"
439 local normcolors = get_ctl("normcolors")
440 if normcolors then
441 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
442 if nf then
443 cmdt[#cmdt+1] = "-nf"
444 cmdt[#cmdt+1] = "'" .. nf .. "'"
446 if nb then
447 cmdt[#cmdt+1] = "-nb"
448 cmdt[#cmdt+1] = "'" .. nb .. "'"
451 local focuscolors = get_ctl("focuscolors")
452 if focuscolors then
453 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
454 if sf then
455 cmdt[#cmdt+1] = "-sf"
456 cmdt[#cmdt+1] = "'" .. sf .. "'"
458 if sb then
459 cmdt[#cmdt+1] = "-sb"
460 cmdt[#cmdt+1] = "'" .. sb .. "'"
465 return cmdt
468 -- ------------------------------------------------------------------------
469 -- displays the menu given an table of entires, returns selected text
470 function menu (tbl, prompt)
471 local menu = menu_cmd(prompt)
473 local infile = os.tmpname()
474 local fh = io.open (infile, "w+")
476 local i,v
477 for i,v in pairs(tbl) do
478 if type(i) == 'number' and type(v) == 'string' then
479 fh:write (v)
480 else
481 fh:write (i)
483 fh:write ("\n")
485 fh:close()
487 local outfile = os.tmpname()
489 menu[#menu+1] = "<"
490 menu[#menu+1] = infile
491 menu[#menu+1] = ">"
492 menu[#menu+1] = outfile
494 local cmd = table.concat(menu," ")
495 execute (cmd)
497 fh = io.open (outfile, "r")
498 os.remove (outfile)
500 local sel = fh:read("*l")
501 fh:close()
503 return sel
506 -- ------------------------------------------------------------------------
507 -- displays the a tag selection menu, returns selected tag
508 function tag_menu ()
509 local tags = get_tags()
511 return menu(tags, "tag:")
514 -- ------------------------------------------------------------------------
515 -- displays the a program menu, returns selected program
516 function prog_menu ()
517 local menu = menu_cmd("cmd:")
519 local outfile = os.tmpname()
521 menu[#menu+1] = ">"
522 menu[#menu+1] = outfile
524 local hstt = { }
525 for n in prog_hist:walk_reverse_unique() do
526 hstt[#hstt+1] = "echo '" .. n .. "' ; "
529 local cmd = "(" .. table.concat(hstt)
530 .. "dmenu_path ) |"
531 .. table.concat(menu," ")
532 shell_execute (cmd)
534 local fh = io.open (outfile, "rb")
535 os.remove (outfile)
537 local prog = fh:read("*l")
538 io.close (fh)
540 return prog
543 -- ------------------------------------------------------------------------
544 -- returns a table of sorted tags names
545 function get_tags()
546 local t = {}
547 local s
548 for s in wmixp:idir ("/tag") do
549 if s.name and not (s.name == "sel") then
550 t[#t + 1] = s.name
553 table.sort(t)
554 return t
557 -- ------------------------------------------------------------------------
558 -- returns a table of sorted screen names
559 function get_screens()
560 local t = {}
561 local s
562 local empty = true
563 for s in wmixp:idir ("/screen") do
564 if s.name and not (s.name == "sel") then
565 t[#t + 1] = s.name
566 empty = false
569 if empty then
570 return nil
572 table.sort(t)
573 return t
576 -- ------------------------------------------------------------------------
577 -- returns current view, on current screen or specified screen
578 function get_view(screen)
579 return get_screen_ctl(screen, "view") or get_ctl("view")
582 -- ------------------------------------------------------------------------
583 -- changes the current view to the name given
584 function set_view(sel)
585 local cur = get_view()
586 local all = get_tags()
588 if #all < 2 or sel == cur then
589 -- nothing to do if we have less then 2 tags
590 return
593 if not (type(sel) == "string") then
594 error ("string argument expected")
597 -- set new view
598 write ("/ctl", "view " .. sel)
601 -- ------------------------------------------------------------------------
602 -- changes the current view to the index given
603 function set_view_index(sel)
604 local cur = get_view()
605 local all = get_tags()
607 if #all < 2 then
608 -- nothing to do if we have less then 2 tags
609 return
612 local num = tonumber (sel)
613 if not num then
614 error ("number argument expected")
617 local name = all[sel]
618 if not name or name == cur then
619 return
622 -- set new view
623 write ("/ctl", "view " .. name)
626 -- ------------------------------------------------------------------------
627 -- chnages to current view by offset given
628 function set_view_ofs(jump)
629 local cur = get_view()
630 local all = get_tags()
632 if #all < 2 then
633 -- nothing to do if we have less then 2 tags
634 return
637 -- range check
638 if (jump < - #all) or (jump > #all) then
639 error ("view selector is out of range")
642 -- find the one that's selected index
643 local curi = nil
644 local i,v
645 for i,v in pairs (all) do
646 if v == cur then curi = i end
649 -- adjust by index
650 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
651 if (newi < - #all) or (newi > #all) then
652 error ("error computng new view")
655 write ("/ctl", "view " .. all[newi])
658 -- ------------------------------------------------------------------------
659 -- toggle between last view and current view
660 function toggle_view()
661 local last = view_hist[#view_hist]
662 if last then
663 set_view(last)
667 -- ========================================================================
668 -- ACTION HANDLERS
669 -- ========================================================================
671 local action_handlers = {
672 man = function (act, args)
673 local xterm = get_conf("xterm") or "xterm"
674 local page = args
675 if (not page) or (not page:match("%S")) then
676 page = wmiidir .. "/wmii.3lua"
678 local cmd = xterm .. " -e man " .. page .. " &"
679 execute (cmd)
680 end,
682 quit = function ()
683 write ("/ctl", "quit")
684 end,
686 exec = function (act, args)
687 local what = args or "wmii-lua"
688 log (" asking wmii to exec " .. tostring(what))
689 cleanup()
690 write ("/ctl", "exec " .. what)
691 end,
693 xlock = function (act)
694 local cmd = get_conf("xlock") or "xscreensaver-command --lock"
695 execute (cmd)
696 end,
698 wmiirc = function ()
699 if have_posix then
700 local wmiirc = find_wmiirc()
701 if wmiirc then
702 log (" executing: lua " .. wmiirc)
703 cleanup()
704 posix.exec (wmiirc)
705 posix.exec ("/bin/sh", "-c", "exec lua wmiirc")
706 posix.exec ("%LUA_BIN%", wmiirc)
707 posix.exec ("/usr/bin/lua", wmiirc)
709 else
710 log("sorry cannot restart; you don't have lua's posix library.")
712 end,
714 urgent = function ()
715 wmixp:write ("/client/sel/ctl", "Urgent toggle")
716 end,
718 --[[
719 rehash = function ()
720 -- TODO: consider storing list of executables around, and
721 -- this will then reinitialize that list
722 log (" TODO: rehash")
723 end,
725 status = function ()
726 -- TODO: this should eventually update something on the /rbar
727 log (" TODO: status")
728 end,
729 --]]
732 --[[
733 =pod
735 =item add_action_handler (action, fn)
737 Add an Alt-a action handler callback function, I<fn>, for the given action string I<action>.
739 =cut
740 --]]
741 function add_action_handler (action, fn)
743 if type(action) ~= "string" or type(fn) ~= "function" then
744 error ("expecting a string and a function")
747 if action_handlers[action] then
748 error ("action handler already exists for '" .. action .. "'")
751 action_handlers[action] = fn
754 --[[
755 =pod
757 =item remove_action_handler (action)
759 Remove an action handler callback function for the given action string I<action>.
761 =cut
762 --]]
763 function remove_action_handler (action)
765 action_handlers[action] = nil
768 -- ========================================================================
769 -- KEY HANDLERS
770 -- ========================================================================
772 function ke_fullscreen_toggle()
773 wmixp:write ("/client/sel/ctl", "Fullscreen toggle")
776 function ke_view_starting_with_letter (letter)
777 local i,v
779 -- find the view name in history in reverse order
780 for i=#view_hist,1,-1 do
781 v = view_hist[i]
782 if letter == v:sub(1,1) then
783 set_view(v)
784 return true
788 -- otherwise just pick the first view that matches
789 local all = get_tags()
790 for i,v in pairs(all) do
791 if letter == v:sub(1,1) then
792 set_view_index (i)
793 return true
797 return false
800 function ke_handle_action()
801 local actions = { }
802 local seen = {}
804 local n
805 for n in action_hist:walk_reverse() do
806 if not seen[n] then
807 actions[#actions+1] = n
808 seen[n] = 1
812 local v
813 for n,v in pairs(action_handlers) do
814 if not seen[n] then
815 actions[#actions+1] = n
816 seen[n] = 1
820 local text = menu(actions, "action:")
821 if text then
822 log ("Action: " .. text)
823 local act = text
824 local args = nil
825 local si = text:find("%s")
826 if si then
827 act,args = string.match(text .. " ", "(%w+)%s(.+)")
829 if act then
830 local fn = action_handlers[act]
831 if fn then
832 action_hist:add (act)
833 local r, err = pcall (fn, act, args)
834 if not r then
835 log ("WARNING: " .. tostring(err))
843 local key_handlers = {
844 ["*"] = function (key)
845 log ("*: " .. key)
846 end,
848 -- execution and actions
849 ["Mod1-Return"] = function (key)
850 local xterm = get_conf("xterm") or "xterm"
851 execute (xterm .. " &")
852 end,
853 ["Mod1-Shift-Return"] = function (key)
854 local tag = tag_menu()
855 if tag then
856 local xterm = get_conf("xterm") or "xterm"
857 log (" executing on: " .. tag)
858 next_client_goes_to_tag = tag
859 execute (xterm .. " &")
861 end,
862 ["Mod1-a"] = function (key)
863 ke_handle_action()
864 end,
865 ["Mod1-p"] = function (key)
866 local prog = prog_menu()
867 if prog then
868 prog_hist:add(prog:match("([^ ]+)"))
869 execute (prog .. " &")
871 end,
872 ["Mod1-Shift-p"] = function (key)
873 local tag = tag_menu()
874 if tag then
875 local prog = prog_menu()
876 if prog then
877 log (" executing on: " .. tag)
878 next_client_goes_to_tag = tag
879 execute (prog .. " &")
882 end,
883 ["Mod1-Shift-c"] = function (key)
884 write ("/client/sel/ctl", "kill")
885 end,
887 -- HJKL active selection
888 ["Mod1-h"] = function (key)
889 write ("/tag/sel/ctl", "select left")
890 end,
891 ["Mod1-l"] = function (key)
892 write ("/tag/sel/ctl", "select right")
893 end,
894 ["Mod1-j"] = function (key)
895 write ("/tag/sel/ctl", "select down")
896 end,
897 ["Mod1-k"] = function (key)
898 write ("/tag/sel/ctl", "select up")
899 end,
901 -- HJKL movement
902 ["Mod1-Shift-h"] = function (key)
903 write ("/tag/sel/ctl", "send sel left")
904 end,
905 ["Mod1-Shift-l"] = function (key)
906 write ("/tag/sel/ctl", "send sel right")
907 end,
908 ["Mod1-Shift-j"] = function (key)
909 write ("/tag/sel/ctl", "send sel down")
910 end,
911 ["Mod1-Shift-k"] = function (key)
912 write ("/tag/sel/ctl", "send sel up")
913 end,
915 -- floating vs tiled
916 ["Mod1-space"] = function (key)
917 write ("/tag/sel/ctl", "select toggle")
918 end,
919 ["Mod1-Shift-space"] = function (key)
920 write ("/tag/sel/ctl", "send sel toggle")
921 end,
923 -- work spaces (# and @ are wildcards for numbers and letters)
924 ["Mod4-#"] = function (key, num)
925 -- first attempt to find a view that starts with the number requested
926 local num_str = tostring(num)
927 if not ke_view_starting_with_letter (num_str) then
928 -- if we fail, then set it to the index requested
929 set_view_index (num)
931 end,
932 ["Mod4-Shift-#"] = function (key, num)
933 write ("/client/sel/tags", tostring(num))
934 end,
935 ["Mod4-@"] = function (key, letter)
936 ke_view_starting_with_letter (letter)
937 end,
938 ["Mod4-Shift-@"] = function (key, letter)
939 local all = get_tags()
940 local i,v
941 for i,v in pairs(all) do
942 if letter == v:sub(1,1) then
943 write ("/client/sel/tags", v)
944 break
947 end,
948 ["Mod1-comma"] = function (key)
949 set_view_ofs (-1)
950 end,
951 ["Mod1-period"] = function (key)
952 set_view_ofs (1)
953 end,
954 ["Mod1-r"] = function (key)
955 -- got to the last view
956 toggle_view()
957 end,
959 -- switching views and retagging
960 ["Mod1-t"] = function (key)
961 -- got to a view
962 local tag = tag_menu()
963 if tag then
964 set_view (tag)
966 end,
967 ["Mod1-Shift-t"] = function (key)
968 -- move selected client to a tag
969 local tag = tag_menu()
970 if tag then
971 write ("/client/sel/tags", tag)
973 end,
974 ["Mod1-Shift-r"] = function (key)
975 -- move selected client to a tag, and follow
976 local tag = tag_menu()
977 if tag then
978 -- get the current window id
979 local xid = wmixp:read("/client/sel/ctl") or ""
981 -- modify the tag
982 write("/client/sel/tags", tag)
984 -- if the client is still in this tag, then
985 -- it might have been a regexp tag... check
986 local test = wmixp:read("/client/sel/ctl")
987 if not test or test ~= xid then
988 -- if the window moved, follow it
989 set_view(tag)
992 end,
993 ["Mod1-Control-t"] = function (key)
994 log (" TODO: Mod1-Control-t: " .. key)
995 end,
997 -- column modes
998 ["Mod1-d"] = function (key)
999 write("/tag/sel/ctl", "colmode sel default-max")
1000 end,
1001 ["Mod1-s"] = function (key)
1002 write("/tag/sel/ctl", "colmode sel stack-max")
1003 end,
1004 ["Mod1-m"] = function (key)
1005 write("/tag/sel/ctl", "colmode sel stack+max")
1006 end,
1007 ["Mod1-f"] = function (key)
1008 ke_fullscreen_toggle()
1009 end,
1011 -- changing client flags
1012 ["Shift-Mod1-f"] = function (key)
1013 log ("setting flags")
1015 local cli = get_client ()
1017 local flags = { "suspend", "raw" }
1018 local current_flags = cli:flags_string()
1020 local what = menu(flags, "current flags: " .. current_flags .. " toggle:")
1022 cli:toggle(what)
1023 end,
1024 ["Mod4-space"] = function (key)
1025 local cli = get_client ()
1026 cli:toggle("raw")
1027 end,
1030 --[[
1031 =pod
1033 =item add_key_handler (key, fn)
1035 Add a keypress handler callback function, I<fn>, for the given key sequence I<key>.
1037 =cut
1038 --]]
1039 function add_key_handler (key, fn)
1041 if type(key) ~= "string" or type(fn) ~= "function" then
1042 error ("expecting a string and a function")
1045 local onlyKey = key:match("([^-]+)$")
1046 if 0 ~= os.execute("xmodmap -pk | grep -q '(" .. onlyKey .. ")'") then
1047 return warn ("xmodmap -pk doesn't know about '" .. onlyKey .. "'")
1050 if key_handlers[key] then
1051 -- TODO: we may wish to allow multiple handlers for one keypress
1052 error ("key handler already exists for '" .. key .. "'")
1055 key_handlers[key] = fn
1058 --[[
1059 =pod
1061 =item remove_key_handler (key)
1063 Remove an key handler callback function for the given key I<key>.
1065 Returns the handler callback function.
1067 =cut
1068 --]]
1069 function remove_key_handler (key)
1071 local fn = key_handlers[key]
1072 key_handlers[key] = nil
1073 return fn
1076 --[[
1077 =pod
1079 =item remap_key_handler (old_key, new_key)
1081 Remove a key handler callback function from the given key I<old_key>,
1082 and assign it to a new key I<new_key>.
1084 =cut
1085 --]]
1086 function remap_key_handler (old_key, new_key)
1088 local fn = remove_key_handler(old_key)
1090 return add_key_handler (new_key, fn)
1094 -- ------------------------------------------------------------------------
1095 -- update the /keys wmii file with the list of all handlers
1096 local alphabet="abcdefghijklmnopqrstuvwxyz"
1097 function update_active_keys ()
1098 local t = {}
1099 local x, y
1100 for x,y in pairs(key_handlers) do
1101 if x:find("%w") then
1102 local i = x:find("#$")
1103 if i then
1104 local j
1105 for j=0,9 do
1106 t[#t + 1] = x:sub(1,i-1) .. j
1108 else
1109 i = x:find("@$")
1110 if i then
1111 local j
1112 for j=1,alphabet:len() do
1113 local a = alphabet:sub(j,j)
1114 t[#t + 1] = x:sub(1,i-1) .. a
1116 else
1117 t[#t + 1] = tostring(x)
1122 local all_keys = table.concat(t, "\n")
1123 --log ("setting /keys to...\n" .. all_keys .. "\n");
1124 write ("/keys", all_keys)
1127 -- ------------------------------------------------------------------------
1128 -- update the /lbar wmii file with the current tags
1129 function update_displayed_tags ()
1130 -- list of all screens
1131 local screens = get_screens()
1132 if not screens then
1133 update_displayed_tags_on_screen()
1134 return
1137 local i, s
1138 for i,s in pairs(screens) do
1139 update_displayed_tags_on_screen(s)
1143 function tag_display(tag, selected)
1144 return tag
1147 function update_displayed_tags_on_screen(s)
1148 local lbar = "/lbar"
1149 if s then
1150 lbar = "/screen/" .. s .. "/lbar"
1153 -- colours for screen
1154 local fc = get_screen_ctl(s, "focuscolors") or get_ctl("focuscolors") or ""
1155 local nc = get_screen_ctl(s, "normcolors") or get_ctl("normcolors") or ""
1157 -- build up a table of existing tags in the /lbar
1158 local old = {}
1159 local ent
1160 for ent in wmixp:idir (lbar) do
1161 old[ent.name] = 1
1164 -- for all actual tags in use create any entries in /lbar we don't have
1165 -- clear the old table entries if we have them
1166 local cur = get_view(s)
1167 local all = get_tags()
1168 local i,v
1169 for i,v in pairs(all) do
1170 local color = nc
1171 if cur == v then
1172 color = fc
1174 local str = tag_display(v,selected)
1175 if not old[v] then
1176 create (lbar .. "/" .. v, color .. " " .. str)
1178 write (lbar .. "/" .. v, color .. " " .. str)
1179 old[v] = nil
1182 -- ignore widgets on the lbar
1183 for i,v in pairs(widgets) do
1184 if v.bar == 'lbar' then
1185 old[v.name] = nil
1189 -- anything left in the old table should be removed now
1190 for i,v in pairs(old) do
1191 if v then
1192 remove(lbar.."/"..i)
1196 -- this is a hack, and should brobably be rethought
1197 -- the intent is to distinguish the multiple screens
1198 if s then
1199 create ("/screen/"..s.."/lbar/000000000000000000", '-'..s..'-')
1203 function create_tag_widget(name)
1204 local nc = get_ctl("normcolors") or ""
1205 local screens = get_screens()
1206 if not screens then
1207 create ("/lbar/" .. name, nc .. " " .. name)
1208 return
1210 local i, s
1211 for i,s in pairs(screens) do
1212 create ("/screen/"..s.."/lbar/" .. name, nc .. " " .. tag_display(name))
1216 function destroy_tag_widget(name)
1217 local screens = get_screens()
1218 if not screens then
1219 remove ("/lbar/" .. name)
1220 return
1222 local i, s
1223 for i,s in pairs(screens) do
1224 remove ("/screen/"..s.."/lbar/" .. name)
1229 -- ========================================================================
1230 -- EVENT HANDLERS
1231 -- ========================================================================
1233 local widget_ev_handlers = {
1236 --[[
1237 =pod
1239 =item _handle_widget_event (ev, arg)
1241 Top-level event handler for redispatching events to widgets. This event
1242 handler is added for any widget event that currently has a widget registered
1243 for it.
1245 Valid widget events are currently
1247 RightBarMouseDown <buttonnumber> <widgetname>
1248 RightBarClick <buttonnumber> <widgetname>
1250 the "Click" event is sent on mouseup.
1252 The callbacks are given only the button number as their argument, to avoid the
1253 need to reparse.
1255 =cut
1256 --]]
1258 local function _handle_widget_event (ev, arg)
1260 log("_handle_widget_event: " .. tostring(ev) .. " - " .. tostring(arg))
1262 -- parse arg to strip out our widget name
1263 local number,wname = string.match(arg, "(%d+)%s+(.+)")
1265 -- check our dispatch table for that widget
1266 if not wname then
1267 log("Didn't find wname")
1268 return
1271 local wtable = widget_ev_handlers[wname]
1272 if not wtable then
1273 log("No widget cares about" .. wname)
1274 return
1277 local fn = wtable[ev] or wtable["*"]
1278 if fn then
1279 success, err = pcall( fn, ev, tonumber(number) )
1280 if not success then
1281 log("Callback had an error in _handle_widget_event: " .. tostring(err) )
1282 return nil
1284 else
1285 log("no function found for " .. ev)
1289 local ev_handlers = {
1290 ["*"] = function (ev, arg)
1291 log ("ev: " .. tostring(ev) .. " - " .. tostring(arg))
1292 end,
1294 RightBarClick = _handle_widget_event,
1296 -- process timer events
1297 ProcessTimerEvents = function (ev, arg)
1298 process_timers()
1299 end,
1301 -- exit if another wmiirc started up
1302 Start = function (ev, arg)
1303 if arg then
1304 if arg == "wmiirc" then
1305 -- backwards compatibility with bash version
1306 log (" exiting; pid=" .. tostring(myid))
1307 cleanup()
1308 os.exit (0)
1309 else
1310 -- ignore if it came from us
1311 local pid = string.match(arg, "wmiirc (%d+)")
1312 if pid then
1313 local pid = tonumber (pid)
1314 if not (pid == myid) then
1315 log (" exiting; pid=" .. tostring(myid))
1316 cleanup()
1317 os.exit (0)
1322 end,
1324 -- tag management
1325 CreateTag = function (ev, arg)
1326 log ("CreateTag: " .. arg)
1327 create_tag_widget(arg)
1328 end,
1329 DestroyTag = function (ev, arg)
1330 log ("DestroyTag: " .. arg)
1331 destroy_tag_widget(arg)
1333 -- remove the tag from history
1334 local i,v
1335 for i=#view_hist,1,-1 do
1336 v = view_hist[i]
1337 if arg == v then
1338 table.remove(view_hist,i)
1341 end,
1343 FocusTag = function (ev, arg)
1344 log ("FocusTag: " .. arg)
1346 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1347 if not tag then
1348 return
1351 local file = "/lbar/" .. tag
1352 if scrn and scrn:len() > 0 then
1353 file = "/screen/" .. scrn .. file
1356 local fc = get_screen_ctl(scrn, "focuscolors") or get_ctl("focuscolors") or ""
1357 log ("# echo " .. fc .. " " .. tag .. " | wmiir write " .. file)
1359 str = tag_display(tag,true)
1360 create (file, fc .. " " .. str)
1361 write (file, fc .. " " .. str)
1362 end,
1363 UnfocusTag = function (ev, arg)
1364 log ("UnfocusTag: " .. arg)
1366 local tag,scrn = arg:match("(%w+)%s*(%w*)")
1367 if not tag then
1368 return
1371 local file = "/lbar/" .. tag
1372 if scrn and scrn:len() > 0 then
1373 file = "/screen/" .. scrn .. file
1376 local nc = get_screen_ctl(scrn, "normcolors") or get_ctl("normcolors") or ""
1377 log ("# echo " .. nc .. " " .. tag .. " | wmiir write " .. file)
1379 str = tag_display(tag,true)
1380 create (file, nc .. " " .. str)
1381 write (file, nc .. " " .. str)
1383 -- don't duplicate the last entry
1384 if not (tag == view_hist[#view_hist]) then
1385 view_hist[#view_hist+1] = tag
1387 -- limit to view_hist_max
1388 if #view_hist > view_hist_max then
1389 table.remove(view_hist, 1)
1392 end,
1394 -- key event handling
1395 Key = function (ev, arg)
1396 log ("Key: " .. arg)
1397 local magic = nil
1398 -- can we find an exact match?
1399 local fn = key_handlers[arg]
1400 if not fn then
1401 local key = arg:gsub("-%d$", "-#")
1402 -- can we find a match with a # wild card for the number
1403 fn = key_handlers[key]
1404 if fn then
1405 -- convert the trailing number to a number
1406 magic = tonumber(arg:match("-(%d)$"))
1409 if not fn then
1410 local key = arg:gsub("-%a$", "-@")
1411 -- can we find a match with a @ wild card for a letter
1412 fn = key_handlers[key]
1413 if fn then
1414 -- split off the trailing letter
1415 magic = arg:match("-(%a)$")
1418 if not fn then
1419 -- everything else failed, try default match
1420 fn = key_handlers["*"]
1422 if fn then
1423 local r, err = pcall (fn, arg, magic)
1424 if not r then
1425 log ("WARNING: " .. tostring(err))
1428 end,
1430 -- mouse handling on the lbar
1431 LeftBarClick = function (ev, arg)
1432 local button,tag = string.match(arg, "(%w+)%s+(%S+)")
1433 set_view (tag)
1434 end,
1436 -- focus updates
1437 ClientFocus = function (ev, arg)
1438 log ("ClientFocus: " .. arg)
1439 client_focused (arg)
1440 end,
1441 ColumnFocus = function (ev, arg)
1442 log ("ColumnFocus: " .. arg)
1443 end,
1445 -- client handling
1446 CreateClient = function (ev, arg)
1447 if next_client_goes_to_tag then
1448 local tag = next_client_goes_to_tag
1449 local cli = arg
1450 next_client_goes_to_tag = nil
1451 write ("/client/" .. cli .. "/tags", tag)
1452 set_view(tag)
1454 client_created (arg)
1455 end,
1456 DestroyClient = function (ev, arg)
1457 client_destoryed (arg)
1458 end,
1460 -- urgent tag
1461 UrgentTag = function (ev, arg)
1462 log ("UrgentTag: " .. arg)
1463 write ("/lbar/" .. arg, "*" .. arg);
1464 end,
1465 NotUrgentTag = function (ev, arg)
1466 log ("NotUrgentTag: " .. arg)
1467 write ("/lbar/" .. arg, arg);
1468 end,
1470 -- notifications
1471 Unresponsive = function (ev, arg)
1472 log ("Unresponsive: " .. arg)
1473 -- TODO ask the user if it shoudl be killed off
1474 end,
1476 Notice = function (ev, arg)
1477 log ("Notice: " .. arg)
1478 -- TODO send to the message plugin (or implement there)
1479 end,
1481 -- /
1484 --[[
1485 =pod
1487 =item add_widget_event_handler (wname, ev, fn)
1489 Add an event handler callback for the I<ev> event on the widget named I<wname>
1491 =cut
1492 --]]
1494 function add_widget_event_handler (wname, ev, fn)
1495 if type(wname) ~= "string" or type(ev) ~= "string" or type(fn) ~= "function" then
1496 error ("expecting string for widget name, string for event name and a function callback")
1499 -- Make sure the widget event handler is present
1500 if not ev_handlers[ev] then
1501 ev_handlers[ev] = _handle_widget_event
1504 if not widget_ev_handlers[wname] then
1505 widget_ev_handlers[wname] = { }
1508 if widget_ev_handlers[wname][ev] then
1509 -- TODO: we may wish to allow multiple handlers for one event
1510 error ("event handler already exists on widget '" .. wname .. "' for '" .. ev .. "'")
1513 widget_ev_handlers[wname][ev] = fn
1516 --[[
1517 =pod
1519 =item remove_widget_event_handler (wname, ev)
1521 Remove an event handler callback function for the I<ev> on the widget named I<wname>.
1523 =cut
1524 --]]
1525 function remove_event_handler (wname, ev)
1527 if not widget_ev_handlers[wname] then
1528 return
1531 widget_ev_handlers[wname][ev] = nil
1534 --[[
1535 =pod
1537 =item add_event_handler (ev, fn)
1539 Add an event handler callback function, I<fn>, for the given event I<ev>.
1541 =cut
1542 --]]
1543 -- TODO: Need to allow registering widgets for RightBar* events. Should probably be done with its own event table, though
1544 function add_event_handler (ev, fn)
1545 if type(ev) ~= "string" or type(fn) ~= "function" then
1546 error ("expecting a string and a function")
1549 if ev_handlers[ev] then
1550 -- TODO: we may wish to allow multiple handlers for one event
1551 error ("event handler already exists for '" .. ev .. "'")
1555 ev_handlers[ev] = fn
1558 --[[
1559 =pod
1561 =item remove_event_handler (ev)
1563 Remove an event handler callback function for the given event I<ev>.
1565 =cut
1566 --]]
1567 function remove_event_handler (ev)
1569 ev_handlers[ev] = nil
1573 -- ========================================================================
1574 -- MAIN INTERFACE FUNCTIONS
1575 -- ========================================================================
1577 local config = {
1578 xterm = 'x-terminal-emulator',
1579 xlock = "xscreensaver-command --lock",
1580 debug = false,
1583 -- ------------------------------------------------------------------------
1584 -- write configuration to /ctl wmii file
1585 -- wmii.set_ctl({ "var" = "val", ...})
1586 -- wmii.set_ctl("var, "val")
1587 function set_ctl (first,second)
1588 if type(first) == "table" and second == nil then
1589 local x, y
1590 for x, y in pairs(first) do
1591 write ("/ctl", x .. " " .. y)
1594 elseif type(first) == "string" and type(second) == "string" then
1595 write ("/ctl", first .. " " .. second)
1597 else
1598 error ("expecting a table or two string arguments")
1602 -- ------------------------------------------------------------------------
1603 -- read a value from /ctl wmii file
1604 -- table = wmii.get_ctl()
1605 -- value = wmii.get_ctl("variable")
1606 function get_ctl (name)
1607 local s
1608 local t = {}
1609 for s in iread("/ctl") do
1610 local var,val = s:match("(%w+)%s+(.+)")
1611 if var == name then
1612 return val
1614 t[var] = val
1616 if not name then
1617 return t
1619 return nil
1622 -- ------------------------------------------------------------------------
1623 -- write configuration to /screen/*/ctl wmii file
1624 -- wmii.set_screen_ctl("screen", { "var" = "val", ...})
1625 -- wmii.set_screen_ctl("screen", "var, "val")
1626 function set_screen_ctl (screen, first, second)
1627 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1628 if not screen then
1629 error ("screen is not set")
1630 elseif type(first) == "table" and second == nil then
1631 local x, y
1632 for x, y in pairs(first) do
1633 write (ctl, x .. " " .. y)
1636 elseif type(first) == "string" and type(second) == "string" then
1637 write (ctl, first .. " " .. second)
1639 else
1640 error ("expecting a screen name, followed by a table or two string arguments")
1644 -- ------------------------------------------------------------------------
1645 -- read a value from /screen/*/ctl wmii file
1646 -- table = wmii.get_screen_ctl("screen")
1647 -- value = wmii.get_screen_ctl("screen", "variable")
1648 function get_screen_ctl (screen, name)
1649 local s
1650 local t = {}
1651 if not screen then
1652 return nil
1654 local ctl = "/screen/" .. tostring(screen) .. "/ctl"
1655 for s in iread(ctl) do
1656 local var,val = s:match("(%w+)%s+(.+)")
1657 if var == name then
1658 return val
1660 -- sometimes first line is the name of the entry
1661 -- in which case there will be no space
1662 t[var or ""] = val
1664 if not name then
1665 return t
1667 return nil
1670 -- ------------------------------------------------------------------------
1671 -- set an internal wmiirc.lua variable
1672 -- wmii.set_conf({ "var" = "val", ...})
1673 -- wmii.set_conf("var, "val")
1674 function set_conf (first,second)
1675 if type(first) == "table" and second == nil then
1676 local x, y
1677 for x, y in pairs(first) do
1678 config[x] = y
1681 elseif type(first) == "string"
1682 and (type(second) == "string"
1683 or type(second) == "number"
1684 or type(second) == "boolean") then
1685 config[first] = second
1687 else
1688 error ("expecting a table, or string and string/number as arguments")
1692 -- ------------------------------------------------------------------------
1693 -- read an internal wmiirc.lua variable
1694 function get_conf (name)
1695 if name then
1696 return config[name]
1698 return config
1701 -- ========================================================================
1702 -- THE EVENT LOOP
1703 -- ========================================================================
1705 -- the event loop instance
1706 local el = eventloop.new()
1707 local event_read_fd = -1
1708 local wmiirc_running = false
1709 local event_read_start = 0
1711 -- ------------------------------------------------------------------------
1712 -- start/restart the core event reading process
1713 local function start_event_reader ()
1714 -- prevent adding two readers
1715 if event_read_fd ~= -1 then
1716 if el:check_exec(event_read_fd) then
1717 return
1720 -- prevert rapid restarts
1721 local now = os.time()
1722 if os.difftime(now, event_read_start) < 5 then
1723 log("wmii: detected rapid restart of /event reader")
1724 local cmd = wmiir .. " ls /ctl"
1725 if os.execute(cmd) ~= 0 then
1726 log("wmii: cannot confirm communication with wmii, shutting down!")
1727 wmiirc_running = false
1728 return
1730 log("wmii: but things look ok, so we will restart it")
1732 event_read_start = now
1734 -- start a new event reader
1735 log("wmii: starting /event reading process")
1736 event_read_fd = el:add_exec (wmiir .. " read /event",
1737 function (line)
1738 local line = line or "nil"
1740 -- try to split off the argument(s)
1741 local ev,arg = string.match(line, "(%S+)%s+(.+)")
1742 if not ev then
1743 ev = line
1746 -- now locate the handler function and call it
1747 local fn = ev_handlers[ev] or ev_handlers["*"]
1748 if fn then
1749 local r, err = pcall (fn, ev, arg)
1750 if not r then
1751 log ("WARNING: " .. tostring(err))
1756 log("wmii: ... fd=" .. tostring(event_read_fd))
1759 -- ------------------------------------------------------------------------
1760 -- run the event loop and process events, this function does not exit
1761 function run_event_loop ()
1762 -- stop any other instance of wmiirc
1763 wmixp:write ("/event", "Start wmiirc " .. tostring(myid))
1765 log("wmii: updating lbar")
1767 update_displayed_tags ()
1769 log("wmii: updating rbar")
1771 update_displayed_widgets ()
1773 log("wmii: updating active keys")
1775 update_active_keys ()
1777 log("wmii: starting event loop")
1778 wmiirc_running = true
1779 while wmiirc_running do
1780 start_event_reader()
1781 local sleep_for = process_timers()
1782 el:run_loop(sleep_for)
1784 log ("wmii: exiting")
1787 -- ========================================================================
1788 -- PLUGINS API
1789 -- ========================================================================
1791 api_version = 0.1 -- the API version we export
1793 plugins = {} -- all plugins that were loaded
1795 -- ------------------------------------------------------------------------
1796 -- plugin loader which also verifies the version of the api the plugin needs
1798 -- here is what it does
1799 -- - does a manual locate on the file using package.path
1800 -- - reads in the file w/o using the lua interpreter
1801 -- - locates api_version=X.Y string
1802 -- - makes sure that api_version requested can be satisfied
1803 -- - if the plugins is available it will set variables passed in
1804 -- - it then loads the plugin
1806 -- TODO: currently the api_version must be in an X.Y format, but we may want
1807 -- to expend this so plugins can say they want '0.1 | 1.3 | 2.0' etc
1809 function load_plugin(name, vars)
1810 local backup_path = package.path or "./?.lua"
1812 log ("loading " .. name)
1814 -- this is the version we want to find
1815 local api_major, api_minor = tostring(api_version):match("(%d+)%.0*(%d+)")
1816 if (not api_major) or (not api_minor) then
1817 log ("WARNING: could not parse api_version in core/wmii.lua")
1818 return nil
1821 -- first find the plugin file
1822 local s, path_match, full_name, file
1823 for i,s in pairs(plugin_paths) do
1824 -- try to locate the files locally
1825 local fn = s:gsub("%?", name)
1826 file = io.open(fn, "r")
1827 if file then
1828 path_match = s
1829 full_name = fn
1830 break
1834 -- read it in
1835 local txt
1836 if file then
1837 txt = file:read("*all")
1838 file:close()
1841 if not txt then
1842 log ("WARNING: could not load plugin '" .. name .. "'")
1843 return nil
1846 -- find the api_version line
1847 local line, plugin_version
1848 for line in string.gmatch(txt, "%s*api_version%s*=%s*%d+%.%d+%s*") do
1849 plugin_version = line:match("api_version%s*=%s*(%d+%.%d+)%s*")
1850 if plugin_version then
1851 break
1855 if not plugin_version then
1856 log ("WARNING: could not find api_version string in plugin '" .. name .. "'")
1857 return nil
1860 -- decompose the version string
1861 local plugin_major, plugin_minor = plugin_version:match("(%d+)%.0*(%d+)")
1862 if (not plugin_major) or (not plugin_minor) then
1863 log ("WARNING: could not parse api_version for '" .. name .. "' plugin")
1864 return nil
1867 -- make a version test
1868 if plugin_major ~= api_major then
1869 log ("WARNING: " .. name .. " plugin major version missmatch, is " .. plugin_version
1870 .. " (api " .. tonumber(api_version) .. ")")
1871 return nil
1874 if plugin_minor > api_minor then
1875 log ("WARNING: '" .. name .. "' plugin minor version missmatch, is " .. plugin_version
1876 .. " (api " .. tonumber(api_version) .. ")")
1877 return nil
1880 -- the configuration parameters before loading
1881 if type(vars) == "table" then
1882 local var, val
1883 for var,val in pairs(vars) do
1884 local success = pcall (set_conf, name .. "." .. var, val)
1885 if not success then
1886 log ("WARNING: bad variable {" .. tostring(var) .. ", " .. tostring(val) .. "} "
1887 .. "given; loading '" .. name .. "' plugin failed.")
1888 return nil
1893 -- actually load the module, but use only the path where we though it should be
1894 package.path = path_match
1895 local success,what = pcall (require, name)
1896 package.path = backup_path
1897 if not success then
1898 log ("WARNING: failed to load '" .. name .. "' plugin")
1899 log (" - path: " .. tostring(path_match))
1900 log (" - file: " .. tostring(full_name))
1901 log (" - plugin's api_version: " .. tostring(plugin_version))
1902 log (" - reason: " .. tostring(what))
1903 return nil
1906 -- success
1907 log ("OK, plugin " .. name .. " loaded, requested api v" .. plugin_version)
1908 plugins[name] = what
1909 return what
1912 -- ------------------------------------------------------------------------
1913 -- widget template
1914 widget = {}
1915 widgets = {}
1917 -- ------------------------------------------------------------------------
1918 -- create a widget object and add it to the wmii /rbar
1920 -- examples:
1921 -- widget = wmii.widget:new ("999_clock")
1922 -- widget = wmii.widget:new ("999_clock", clock_event_handler)
1923 function widget:new (name, fn, bar)
1924 local o = {}
1926 if type(name) == "string" then
1927 o.name = name
1928 if type(fn) == "function" then
1929 o.fn = fn
1931 o.bar = bar or "rbar"
1932 else
1933 error ("expected name followed by an optional function as arguments")
1936 setmetatable (o,self)
1937 self.__index = self
1938 self.__gc = function (o) o:hide() end
1940 widgets[name] = o
1942 o:show()
1943 return o
1946 -- ------------------------------------------------------------------------
1947 -- stop and destroy the timer
1948 function widget:delete ()
1949 widgets[self.name] = nil
1950 self:hide()
1953 -- ------------------------------------------------------------------------
1954 -- displays or updates the widget text
1956 -- examples:
1957 -- w:show("foo")
1958 -- w:show("foo", "#888888 #222222 #333333")
1959 -- w:show("foo", cell_fg .. " " .. cell_bg .. " " .. border)
1961 function widget:show (txt, colors)
1962 local colors = colors or get_ctl("normcolors") or ""
1963 local txt = txt or self.txt or ""
1964 local towrite = txt
1965 if colors then
1966 towrite = colors .. " " .. towrite
1968 if not self.txt then
1969 create ('/'..self.bar..'/'.. self.name, towrite)
1970 else
1971 write ('/'..self.bar..'/'.. self.name, towrite)
1973 self.txt = txt
1976 -- ------------------------------------------------------------------------
1977 -- hides a widget and removes it from the bar
1978 function widget:hide ()
1979 if self.txt then
1980 remove ('/'..self.bar ..'/'.. self.name)
1981 self.txt = nil
1985 --[[
1986 =pod
1988 =item widget:add_event_handler (ev, fn)
1990 Add an event handler callback for this widget, using I<fn> for event I<ev>
1992 =cut
1993 --]]
1995 function widget:add_event_handler (ev, fn)
1996 add_widget_event_handler( self.name, ev, fn)
2000 -- ------------------------------------------------------------------------
2001 -- remove all /rbar entries that we don't have widget objects for
2002 function update_displayed_widgets ()
2003 -- colours for /rbar
2004 local nc = get_ctl("normcolors") or ""
2006 -- build up a table of existing tags in the /lbar
2007 local old = {}
2008 local s
2009 for s in wmixp:idir ("/rbar") do
2010 old[s.name] = 1
2013 -- for all actual widgets in use we want to remove them from the old list
2014 local i,v
2015 for i,v in pairs(widgets) do
2016 old[v.name] = nil
2019 -- anything left in the old table should be removed now
2020 for i,v in pairs(old) do
2021 if v then
2022 remove("/rbar/"..i)
2027 -- ------------------------------------------------------------------------
2028 -- create a new program and for each line it generates call the callback function
2029 -- returns fd which can be passed to kill_exec()
2030 function add_exec (command, callback)
2031 return el:add_exec (command, callback)
2034 -- ------------------------------------------------------------------------
2035 -- terminates a program spawned off by add_exec()
2036 function kill_exec (fd)
2037 return el:kill_exec (fd)
2040 -- ------------------------------------------------------------------------
2041 -- timer template
2042 timer = {}
2043 local timers = {}
2045 -- ------------------------------------------------------------------------
2046 -- create a timer object and add it to the event loop
2048 -- examples:
2049 -- timer:new (my_timer_fn)
2050 -- timer:new (my_timer_fn, 15)
2051 function timer:new (fn, seconds)
2052 local o = {}
2054 if type(fn) == "function" then
2055 o.fn = fn
2056 else
2057 error ("expected function followed by an optional number as arguments")
2060 setmetatable (o,self)
2061 self.__index = self
2062 self.__gc = function (o) o:stop() end
2064 -- add the timer
2065 timers[#timers+1] = o
2067 if seconds then
2068 o:resched(seconds)
2070 return o
2073 -- ------------------------------------------------------------------------
2074 -- stop and destroy the timer
2075 function timer:delete ()
2076 self:stop()
2077 local i,t
2078 for i,t in pairs(timers) do
2079 if t == self then
2080 table.remove (timers,i)
2081 return
2086 -- ------------------------------------------------------------------------
2087 -- run the timer given new interval
2088 function timer:resched (seconds)
2089 local seconds = seconds or self.interval
2090 if not (type(seconds) == "number") then
2091 error ("timer:resched expected number as argument")
2094 local now = tonumber(os.date("%s"))
2096 self.interval = seconds
2097 self.next_time = now + seconds
2099 -- resort the timer list
2100 table.sort (timers, timer.is_less_then)
2103 -- helper for sorting timers
2104 function timer:is_less_then(another)
2105 if not self.next_time then
2106 return false -- another is smaller, nil means infinity
2108 elseif not another.next_time then
2109 return true -- self is smaller, nil means infinity
2111 elseif self.next_time < another.next_time then
2112 return true -- self is smaller than another
2115 return false -- another is smaller then self
2118 -- ------------------------------------------------------------------------
2119 -- stop the timer
2120 function timer:stop ()
2121 self.next_time = nil
2123 -- resort the timer list
2124 table.sort (timers, timer.is_less_then)
2127 -- ------------------------------------------------------------------------
2128 -- figure out how long before the next event
2129 function time_before_next_timer_event()
2130 local tmr = timers[1]
2131 if tmr and tmr.next_time then
2132 local now = tonumber(os.date("%s"))
2133 local seconds = tmr.next_time - now
2134 if seconds > 0 then
2135 return seconds
2138 return 0 -- sleep for ever
2141 -- ------------------------------------------------------------------------
2142 -- handle outstanding events
2143 function process_timers ()
2144 local now = tonumber(os.date("%s"))
2145 local torun = {}
2146 local i,tmr
2148 for i,tmr in pairs (timers) do
2149 if not tmr then
2150 -- prune out removed timers
2151 table.remove(timers,i)
2152 break
2154 elseif not tmr.next_time then
2155 -- break out once we find a timer that is stopped
2156 break
2158 elseif tmr.next_time > now then
2159 -- break out once we get to the future
2160 break
2163 -- this one is good to go
2164 torun[#torun+1] = tmr
2167 for i,tmr in pairs (torun) do
2168 tmr:stop()
2169 local status,new_interval = pcall (tmr.fn, tmr)
2170 if status then
2171 new_interval = new_interval or self.interval
2172 if new_interval and (new_interval ~= -1) then
2173 tmr:resched(new_interval)
2175 else
2176 log ("ERROR: " .. tostring(new_interval))
2180 local sleep_for = time_before_next_timer_event()
2181 return sleep_for
2184 -- ------------------------------------------------------------------------
2185 -- cleanup everything in preparation for exit() or exec()
2186 function cleanup ()
2188 local i,v,tmr,p
2190 log ("wmii: stopping timer events")
2192 for i,tmr in pairs (timers) do
2193 pcall (tmr.delete, tmr)
2195 timers = {}
2197 log ("wmii: terminating eventloop")
2199 pcall(el.kill_all,el)
2201 log ("wmii: disposing of widgets")
2203 -- dispose of all widgets
2204 for i,v in pairs(widgets) do
2205 pcall(v.delete,v)
2207 timers = {}
2209 -- FIXME: it doesn't seem to do what I want
2210 --[[
2211 log ("wmii: releasing plugins")
2213 for i,p in pairs(plugins) do
2214 if p.cleanup then
2215 pcall (p.cleanup, p)
2218 plugins = {}
2219 --]]
2221 log ("wmii: dormant")
2222 wmiirc_running = false
2225 -- ========================================================================
2226 -- CLIENT HANDLING
2227 -- ========================================================================
2229 --[[
2230 -- Notes on client tracking
2232 -- When a client is created wmii sends us a CreateClient message, and
2233 -- we in turn create a 'client' object and store it in the 'clients'
2234 -- table indexed by the client's ID.
2236 -- Each client object stores the following:
2237 -- .xid - the X client ID
2238 -- .pid - the process ID
2239 -- .prog - program object representing the process
2241 -- The client and program objects track the following modes for each program:
2243 -- raw mode:
2244 -- - for each client window
2245 -- - Mod4-space toggles the state between normal and raw
2246 -- - Mod1-f raw also toggles the state
2247 -- - in raw mode all input goes to the client, except for Mod4-space
2248 -- - a focused client with raw mode enabled is put into raw mode
2250 -- suspend mode:
2251 -- - for each program
2252 -- - Mod1-f suspend toggles the state for current client's program
2253 -- - a focused client, whose program was previous suspended is resumed
2254 -- - an unfocused client, with suspend enabled, will be suspended
2255 -- - suspend/resume is done by sending the STOP/CONT signals to the PID
2256 --]]
2258 function xid_to_pid (xid)
2259 local cmd = "xprop -id " .. tostring(xid) .. " _NET_WM_PID"
2260 local file = io.popen (cmd)
2261 local out = file:read("*a")
2262 file:close()
2263 local pid = out:match("^_NET_WM_PID.*%s+=%s+(%d+)%s+$")
2264 return tonumber(pid)
2267 local focused_xid = nil
2268 local clients = {} -- table of client objects indexed by xid
2269 local programs = {} -- table of program objects indexed by pid
2270 local mode_widget = widget:new ("999_client_mode")
2272 -- make programs table have weak values
2273 -- programs go away as soon as no clients point to it
2274 local programs_mt = {}
2275 setmetatable(programs, programs_mt)
2276 programs_mt.__mode = 'v'
2278 -- program class
2279 program = {}
2280 function program:new (pid)
2281 -- make an object
2282 local o = {}
2283 setmetatable (o,self)
2284 self.__index = self
2285 self.__gc = function (old) old:cont() end
2286 -- initialize the new object
2287 o.pid = pid
2288 -- suspend mode
2289 o.suspend = {}
2290 o.suspend.toggle = function (prog)
2291 prog.suspend.enabled = not prog.suspend.enabled
2293 o.suspend.enabled = false -- if true, defocusing suspends (SIGSTOP)
2294 o.suspend.active = true -- if true, focusing resumes (SIGCONT)
2295 return o
2298 function program:stop ()
2299 if not self.suspend.active then
2300 local cmd = "kill -STOP " .. tostring(self.pid)
2301 log (" executing: " .. cmd)
2302 os.execute (cmd)
2303 self.suspend.active = true
2307 function program:cont ()
2308 if self.suspend.active then
2309 local cmd = "kill -CONT " .. tostring(self.pid)
2310 log (" executing: " .. cmd)
2311 os.execute (cmd)
2312 self.suspend.active = false
2316 function get_program (pid)
2317 local prog = programs[pid]
2318 if pid and not prog then
2319 prog = program:new (pid)
2320 programs[pid] = prog
2322 return prog
2325 -- client class
2326 client = {}
2327 function client:new (xid)
2328 local pid = xid_to_pid(xid)
2329 if not pid then
2330 log ("WARNING: failed to convert XID " .. tostring(xid) .. " to a PID")
2331 return
2333 -- make an object
2334 local o = {}
2335 setmetatable (o,self)
2336 self.__index = function (t,k)
2337 if k == 'suspend' then -- suspend mode is tracked per program
2338 return t.prog.suspend
2340 return self[k]
2342 self.__gc = function (old) old.prog=nil end
2343 -- initialize the new object
2344 o.xid = xid
2345 o.pid = pid
2346 o.prog = get_program (pid)
2347 -- raw mode
2348 o.raw = {}
2349 o.raw.toggle = function (cli)
2350 cli.raw.enabled = not cli.raw.enabled
2351 cli:set_raw_mode()
2353 o.raw.enabled = false -- if true, raw mode enabled when client is focused
2354 return o
2357 function client:stop ()
2358 if self.suspend.enabled then
2359 self.prog:stop()
2363 function client:cont ()
2364 self.prog:cont()
2367 function client:set_raw_mode()
2368 if not self or not self.raw.enabled then -- normal mode
2369 update_active_keys ()
2370 else -- raw mode
2371 write ("/keys", "Mod4-space")
2375 function client:toggle(what)
2376 if what and self[what] then
2377 local ctl = self[what]
2379 ctl.toggle (self)
2381 log ("xid=" .. tostring (xid)
2382 .. " pid=" .. tostring (self.pid) .. " (" .. tostring (self.prog.pid) .. ")"
2383 .. " what=" .. tostring (what)
2384 .. " enabled=" .. tostring(ctl["enabled"]))
2386 mode_widget:show (self:flags_string())
2389 function client:flags_string()
2390 local ret = ''
2391 if self.suspend.enabled then ret = ret .. "s" else ret = ret .. "-" end
2392 if self.raw.enabled then ret = ret .. "r" else ret = ret .. "-" end
2393 return ret
2396 function get_client (xid)
2397 local xid = xid or wmixp:read("/client/sel/ctl")
2398 local cli = clients[xid]
2399 if not cli then
2400 cli = client:new (xid)
2401 clients[xid] = cli
2403 return cli
2406 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
2407 function client_created (xid)
2408 log ("-client_created " .. tostring(xid))
2409 return get_client(xid)
2412 function client_destoryed (xid)
2413 log ("-client_destoryed " .. tostring(xid))
2414 if clients[xid] then
2415 local cli = clients[xid]
2416 clients[xid] = nil
2417 log (" del pid: " .. tostring(cli.pid))
2418 cli:cont()
2420 if focused_xid == xid then
2421 focused_xid = nil
2425 function client_focused (xid)
2426 log ("-client_focused " .. tostring(xid))
2427 -- return the current focused xid if nil is passed
2428 if type(xid) ~= 'string' or not xid:match("0x%x*$") then
2429 return focused_xid
2431 -- do nothing if the same xid
2432 if focused_xid == xid then
2433 return clients[xid]
2436 local old = clients[focused_xid]
2437 local new = get_client(xid)
2439 -- handle raw mode switch
2440 if not old or ( old and new and old.raw.enabled ~= new.raw.enabled ) then
2441 new:set_raw_mode()
2444 -- do nothing if the same pid
2445 if old and new and old.pid == new.pid then
2446 mode_widget:show (new:flags_string())
2447 return clients[xid]
2450 if old then
2451 --[[
2452 log (" old pid: " .. tostring(old.pid)
2453 .. " xid: " .. tostring(old.xid)
2454 .. " flags: " .. old:flags_string())
2455 ]]--
2456 old:stop()
2459 if new then
2460 --[[
2461 log (" new pid: " .. tostring(new.pid)
2462 .. " xid: " .. tostring(new.xid)
2463 .. " flags: " .. new:flags_string())
2464 ]]--
2465 new:cont()
2468 mode_widget:show (new:flags_string())
2469 focused_xid = xid
2470 return new
2474 -- ========================================================================
2475 -- DOCUMENTATION
2476 -- ========================================================================
2478 --[[
2479 =pod
2481 =back
2483 =head1 ENVIRONMENT
2485 =over 4
2487 =item WMII_ADDRESS
2489 Used to determine location of wmii's listen socket.
2491 =back
2493 =head1 SEE ALSO
2495 L<wmii(1)>, L<lua(1)>
2497 =head1 AUTHOR
2499 Bart Trojanowski B<< <bart@jukie.net> >>
2501 =head1 COPYRIGHT AND LICENSE
2503 Copyright (c) 2007, Bart Trojanowski <bart@jukie.net>
2505 This is free software. You may redistribute copies of it under the terms of
2506 the GNU General Public License L<http://www.gnu.org/licenses/gpl.html>. There
2507 is NO WARRANTY, to the extent permitted by law.
2509 =cut
2510 --]]
2511 -- vim: set noet ts=8 sw=8 :