created tags are unselected
[wmiirc-lua.git] / wmii.lua
blob52606daca6bbc638e5e1ec4a023d769d44d32388
1 --
2 -- Copyrigh (c) 2007, Bart Trojanowski <bart@jukie.net>
3 --
4 -- Simple wmiir like interface.
5 --
6 -- The current intent is to wrap around the wmiir executable.
7 -- This is just a proof of concept, and eventually this will
8 -- be rewritten in C to use libixp.
9 --
10 -- git://www.jukie.net/wmiirc-lua.git/
12 package.cpath = package.cpath .. ";" .. os.getenv("HOME") .. "/.wmii-3.5/ixp/?.so"
13 require "ixp"
14 local ixp = ixp
16 local base = _G
17 local io = require("io")
18 local os = require("os")
19 local posix = require("posix")
20 local string = require("string")
21 local table = require("table")
22 local math = require("math")
23 local type = type
24 local error = error
25 local print = print
26 local pairs = pairs
27 local tostring = tostring
28 local tonumber = tonumber
30 module("wmii")
32 -- ========================================================================
33 -- MODULE VARIABLES
34 -- ========================================================================
36 -- wmiir points to the wmiir executable
37 local wmiir = "wmiir"
39 -- wmii_adr is the address we use when connecting using ixp
40 local wmii_adr = os.getenv("WMII_ADDRESS")
41 or ("unix!/tmp/ns." .. os.getenv("USER") .. "."
42 .. os.getenv("DISPLAY"):match("(:%d+)") .. "/wmii")
44 -- wmixp is the ixp context we use to talk to wmii
45 local wmixp = ixp.new(wmii_adr)
47 -- history of previous views, view_hist[#view_hist] is the last one
48 local view_hist = {} -- sorted with 1 being the oldest
49 local view_hist_max = 10 -- max number to keep track of
51 -- ========================================================================
52 -- LOCAL HELPERS
53 -- ========================================================================
55 -- ------------------------------------------------------------------------
56 -- log, right now write to stderr
57 function log (str)
58 io.stderr:write (str .. "\n")
59 end
61 -- ========================================================================
62 -- MAIN ACCESS FUNCTIONS
63 -- ========================================================================
65 -- ------------------------------------------------------------------------
66 -- returns an iterator
67 function ls (dir, fmt)
68 local verbose = fmt and fmt:match("l")
70 local s = wmixp:stat(dir)
71 if not s then
72 return function () return nil end
73 end
74 if s.modestr:match("^[^d]") then
75 return function ()
76 return stat2str(verbose, s)
77 end
78 end
80 local itr = wmixp:idir (dir)
81 if not itr then
82 --return function ()
83 return nil
84 --end
85 end
88 return function ()
89 local s = itr()
90 if s then
91 return stat2str(verbose, s)
92 end
93 return nil
94 end
95 end
97 function stat2str(verbose, stat)
98 if verbose then
99 return string.format("%s %s %s %5d %s %s", stat.modestr, stat.uid, stat.gid, stat.length, stat.timestr, stat.name)
100 else
101 if stat.modestr:match("^d") then
102 return stat.name .. "/"
103 else
104 return stat.name
109 -- ------------------------------------------------------------------------
110 -- read all contents of a wmii virtual file
111 function read (file)
112 return wmixp:read (file)
115 -- ------------------------------------------------------------------------
116 -- return an iterator which walks all the lines in the file
118 -- example:
119 -- for event in wmii.iread("/event")
120 -- ...
121 -- end
122 function iread (file)
123 return wmixp:iread(file)
126 -- ------------------------------------------------------------------------
127 -- returns an events iterator
128 function ievents ()
129 local it = iread("/event")
131 return function ()
132 local line = it()
133 return string.match(line, "(%S+)%s(.+)")
137 -- ------------------------------------------------------------------------
138 -- create a wmii file, optionally write data to it
139 function create (file, data)
140 wmixp:create(file, data)
143 -- ------------------------------------------------------------------------
144 -- remove a wmii file
145 function remove (file)
146 wmixp:remove(file)
149 -- ------------------------------------------------------------------------
150 -- write a value to a wmii virtual file system
151 function write (file, value)
152 wmixp:write (file, value)
155 -- ------------------------------------------------------------------------
156 -- setup a table describing dmenu command
157 local function dmenu_cmd (prompt)
158 local cmdt = { "dmenu", "-b" }
159 local fn = getctl("font")
160 if fn then
161 cmdt[#cmdt+1] = "-fn"
162 cmdt[#cmdt+1] = fn
164 local normcolors = getctl("normcolors")
165 if normcolors then
166 local nf, nb = normcolors:match("(#%x+)%s+(#%x+)%s#%x+")
167 if nf then
168 cmdt[#cmdt+1] = "-nf"
169 cmdt[#cmdt+1] = "'" .. nf .. "'"
171 if nb then
172 cmdt[#cmdt+1] = "-nb"
173 cmdt[#cmdt+1] = "'" .. nb .. "'"
176 local focuscolors = getctl("focuscolors")
177 if focuscolors then
178 local sf, sb = focuscolors:match("(#%x+)%s+(#%x+)%s#%x+")
179 if sf then
180 cmdt[#cmdt+1] = "-sf"
181 cmdt[#cmdt+1] = "'" .. sf .. "'"
183 if sb then
184 cmdt[#cmdt+1] = "-sb"
185 cmdt[#cmdt+1] = "'" .. sb .. "'"
188 if prompt then
189 cmdt[#cmdt+1] = "-p"
190 cmdt[#cmdt+1] = "'" .. prompt .. "'"
193 return cmdt
196 -- ------------------------------------------------------------------------
197 -- displays the menu given an table of entires, returns selected text
198 function menu (tbl, prompt)
199 local dmenu = dmenu_cmd(prompt)
201 local infile = os.tmpname()
202 local fh = io.open (infile, "w+")
204 local i,v
205 for i,v in pairs(tbl) do
206 if type(i) == 'number' and type(v) == 'string' then
207 fh:write (v)
208 else
209 fh:write (i)
211 fh:write ("\n")
213 fh:close()
215 local outfile = os.tmpname()
217 dmenu[#dmenu+1] = "<"
218 dmenu[#dmenu+1] = infile
219 dmenu[#dmenu+1] = ">"
220 dmenu[#dmenu+1] = outfile
222 local cmd = table.concat(dmenu," ")
223 os.execute (cmd)
225 fh = io.open (outfile, "r")
226 os.remove (outfile)
228 local sel = fh:read("*l")
229 fh:close()
231 return sel
234 -- ------------------------------------------------------------------------
235 -- displays the a tag selection menu, returns selected tag
236 function tagmenu ()
237 local tags = gettags()
239 return menu(tags, "tag:")
242 -- ------------------------------------------------------------------------
243 -- displays the a program menu, returns selected program
244 function progmenu ()
245 local dmenu = dmenu_cmd("cmd:")
247 local outfile = os.tmpname()
249 dmenu[#dmenu+1] = ">"
250 dmenu[#dmenu+1] = outfile
252 local cmd = "dmenu_path |" .. table.concat(dmenu," ")
253 os.execute (cmd)
255 local fh = io.open (outfile, "rb")
256 os.remove (outfile)
258 local prog = fh:read("*l")
259 io.close (fh)
261 return prog
264 -- ------------------------------------------------------------------------
265 -- displays the a program menu, returns selected program
266 function gettags()
267 local t = {}
268 local s
269 for s in wmixp:idir ("/tag") do
270 if s.name and not (s.name == "sel") then
271 t[#t + 1] = s.name
274 table.sort(t)
275 return t
278 -- ------------------------------------------------------------------------
279 -- displays the a program menu, returns selected program
280 function getview()
281 local v = wmixp:read("/ctl") or ""
282 return v:match("view%s+(%S+)")
285 -- ------------------------------------------------------------------------
286 -- changes the current view
287 -- if the argument is a number it moves to that view at that index
288 -- if the argument is a string it moves to that view name
289 function setview(sel)
290 local cur = getview()
291 local all = gettags()
293 local view_num = nil
295 if #all < 2 then
296 -- nothing to do if we have less then 2 tags
297 return
299 elseif type(sel) == "number" then
302 elseif not (type(sel) == "string") then
303 error ("number or string argument expected")
306 -- set new view
307 write ("/ctl", "view " .. sel)
310 -- ------------------------------------------------------------------------
311 -- chnages to current view by offset given
312 function setviewofs(jump)
313 local cur = getview()
314 local all = gettags()
316 if #all < 2 then
317 -- nothing to do if we have less then 2 tags
318 return
321 -- range check
322 if (jump < - #all) or (jump > #all) then
323 error ("view selector is out of range")
326 -- find the one that's selected index
327 local curi = nil
328 local i,v
329 for i,v in pairs (all) do
330 if v == cur then curi = i end
333 -- adjust by index
334 local newi = math.fmod(#all + curi + jump - 1, #all) + 1
335 if (newi < - #all) or (newi > #all) then
336 error ("error computng new view")
339 write ("/ctl", "view " .. all[newi])
342 -- ------------------------------------------------------------------------
343 -- toggle between last view and current view
344 function toggleview()
345 local last = view_hist[#view_hist]
346 if last then
347 setview(last)
351 -- ========================================================================
352 -- ACTION HANDLERS
353 -- ========================================================================
355 local action_handlers = {
356 quit = function ()
357 write ("/ctl", "quit")
358 end,
360 exec = function (act, args)
361 local what = args or wmiirc
362 write ("/ctl", "exec " .. what)
363 end,
365 wmiirc = function ()
366 posix.exec ("lua", wmiirc)
367 end,
369 rehash = function ()
370 -- TODO: consider storing list of executables around, and
371 -- this will then reinitialize that list
372 log (" TODO: rehash")
373 end,
375 status = function ()
376 -- TODO: this should eventually update something on the /rbar
377 log (" TODO: status")
381 -- ========================================================================
382 -- KEY HANDLERS
383 -- ========================================================================
385 local key_handlers = {
386 ["*"] = function (key)
387 log ("*: " .. key)
388 end,
390 -- execution and actions
391 ["Mod1-Return"] = function (key)
392 local xterm = getconf("xterm")
393 log (" executing: " .. xterm)
394 os.execute (xterm .. " &")
395 end,
396 ["Mod1-a"] = function (key)
397 local text = menu(action_handlers, "action:")
398 if text then
399 local act = text
400 local args = nil
401 local si = text:find("%s")
402 if si then
403 act,args = string.match(text .. " ", "(%w+)%s(.+)")
405 if act then
406 local fn = action_handlers[act]
407 if fn then
408 fn (act,args)
412 end,
413 ["Mod1-p"] = function (key)
414 local prog = progmenu()
415 if prog then
416 log (" executing: " .. prog)
417 os.execute (prog .. " &")
419 end,
420 ["Mod1-Shift-c"] = function (key)
421 write ("/client/sel/ctl", "kill")
422 end,
424 -- HJKL active selection
425 ["Mod1-h"] = function (key)
426 write ("/tag/sel/ctl", "select left")
427 end,
428 ["Mod1-l"] = function (key)
429 write ("/tag/sel/ctl", "select right")
430 end,
431 ["Mod1-j"] = function (key)
432 write ("/tag/sel/ctl", "select down")
433 end,
434 ["Mod1-k"] = function (key)
435 write ("/tag/sel/ctl", "select up")
436 end,
438 -- HJKL movement
439 ["Mod1-Shift-h"] = function (key)
440 write ("/tag/sel/ctl", "send sel left")
441 end,
442 ["Mod1-Shift-l"] = function (key)
443 write ("/tag/sel/ctl", "send sel right")
444 end,
445 ["Mod1-Shift-j"] = function (key)
446 write ("/tag/sel/ctl", "send sel down")
447 end,
448 ["Mod1-Shift-k"] = function (key)
449 write ("/tag/sel/ctl", "send sel up")
450 end,
452 -- floating vs tiled
453 ["Mod1-space"] = function (key)
454 write ("/tag/sel/ctl", "select toggle")
455 end,
456 ["Mod1-Shift-space"] = function (key)
457 write ("/tag/sel/ctl", "send sel toggle")
458 end,
460 -- work spaces
461 ["Mod4-#"] = function (key, num)
462 setview (num)
463 end,
464 ["Mod4-Shift-#"] = function (key, num)
465 write ("/client/sel/tags", tostring(num))
466 end,
467 ["Mod1-comma"] = function (key)
468 setviewofs (-1)
469 end,
470 ["Mod1-period"] = function (key)
471 setviewofs (1)
472 end,
473 ["Mod1-r"] = function (key)
474 toggleview()
475 end,
477 -- switching views and retagging
478 ["Mod1-t"] = function (key)
479 local tag = tagmenu()
480 if tag then
481 setview (tag)
484 end,
485 ["Mod1-Shift-t"] = function (key)
486 local tag = tagmenu()
487 if tag then
488 local cli = read ("/client/sel/ctl")
489 write ("/client/" .. cli .. "/tags", tag)
491 end,
492 ["Mod1-Control-t"] = function (key)
493 log (" TODO: Mod1-Control-t: " .. key)
494 end,
496 -- column modes
497 ["Mod1-d"] = function (key)
498 write("/tag/sel/ctl", "colmode sel default")
499 end,
500 ["Mod1-s"] = function (key)
501 write("/tag/sel/ctl", "colmode sel stack")
502 end,
503 ["Mod1-m"] = function (key)
504 write("/tag/sel/ctl", "colmode sel max")
508 -- ------------------------------------------------------------------------
509 -- update the /keys wmii file with the list of all handlers
510 function update_active_keys ()
511 local t = {}
512 local x, y
513 for x,y in pairs(key_handlers) do
514 if x:find("%w") then
515 local i = x:find("#")
516 if i then
517 local j
518 for j=0,9 do
519 t[#t + 1]
520 = x:sub(1,i-1) .. j
522 else
523 t[#t + 1]
524 = tostring(x)
528 local all_keys = table.concat(t, "\n")
529 --log ("setting /keys to...\n" .. all_keys .. "\n");
530 write ("/keys", all_keys)
533 -- ------------------------------------------------------------------------
534 -- update the /lbar wmii file with the current tags
535 function update_displayed_tags ()
536 -- colours for /lbar
537 local fc = getctl("focuscolors") or ""
538 local nc = getctl("normcolors") or ""
540 -- build up a table of existing tags in the /lbar
541 local old = {}
542 local s
543 for s in wmixp:idir ("/lbar") do
544 old[s.name] = 1
547 -- for all actual tags in use create any entries in /lbar we don't have
548 -- clear the old table entries if we have them
549 local cur = getview()
550 local all = gettags()
551 local i,v
552 for i,v in pairs(all) do
553 local color = nc
554 if cur == v then
555 color = fc
557 if not old[v] then
558 create ("/lbar/" .. v, color .. " " .. v)
560 write ("/lbar/" .. v, color .. " " .. v)
561 old[v] = nil
564 -- anything left in the old table should be removed now
565 for i,v in pairs(old) do
566 if v then
567 remove("/lbar/"..i)
572 -- ========================================================================
573 -- EVENT HANDLERS
574 -- ========================================================================
576 local ev_handlers = {
577 ["*"] = function (ev, arg)
578 log ("ev: " .. ev .. " - " .. arg)
579 end,
581 -- exit if another wmiirc started up
582 Start = function (ev, arg)
583 if arg == "wmiirc" then
584 posix.exit (0)
586 end,
588 -- tag management
589 CreateTag = function (ev, arg)
590 local nc = getctl("normcolors") or ""
591 create ("/lbar/" .. arg, nc .. " " .. arg)
592 end,
593 DestroyTag = function (ev, arg)
594 remove ("/lbar/" .. arg)
595 end,
597 FocusTag = function (ev, arg)
598 local fc = getctl("focuscolors") or ""
599 create ("/lbar/" .. arg, fc .. " " .. arg)
600 write ("/lbar/" .. arg, fc .. " " .. arg)
601 end,
602 UnfocusTag = function (ev, arg)
603 local nc = getctl("normcolors") or ""
604 create ("/lbar/" .. arg, nc .. " " .. arg)
605 write ("/lbar/" .. arg, nc .. " " .. arg)
607 -- don't duplicate the last entry
608 if not (arg == view_hist[#view_hist]) then
609 view_hist[#view_hist+1] = arg
611 -- limit to view_hist_max
612 if #view_hist > view_hist_max then
613 table.remove(view_hist, 1)
616 end,
618 -- key event handling
619 Key = function (ev, arg)
620 log ("Key: " .. arg)
621 local num = nil
622 -- can we find an exact match?
623 local fn = key_handlers[arg]
624 if not fn then
625 local key = arg:gsub("-%d+", "-#")
626 -- can we find a match with a # wild card for the number
627 fn = key_handlers[key]
628 if fn then
629 -- convert the trailing number to a number
630 num = tonumber(arg:match("-(%d+)"))
631 else
632 -- everything else failed, try default match
633 fn = key_handlers["*"]
636 if fn then
637 fn (arg, num)
639 end,
641 -- mouse handling on the lbar
642 LeftBarClick = function (ev, arg)
643 local button,tag = string.match(arg, "(%w+)%s+(%w+)")
644 setview (tag)
645 end,
647 -- focus updates
648 ClientFocus = function (ev, arg)
649 log ("ClientFocus: " .. arg)
650 end,
651 ColumnFocus = function (ev, arg)
652 log ("ColumnFocus: " .. arg)
653 end,
655 -- urgent tag?
656 UrgentTag = function (ev, arg)
657 log ("UrgentTag: " .. arg)
658 -- wmiir xwrite "/lbar/$@" "*$@"
659 end,
660 NotUrgentTag = function (ev, arg)
661 log ("NotUrgentTag: " .. arg)
662 -- wmiir xwrite "/lbar/$@" "$@"
667 -- ========================================================================
668 -- MAIN INTERFACE FUNCTIONS
669 -- ========================================================================
671 local config = {
672 xterm = 'x-terminal-emulator'
675 -- ------------------------------------------------------------------------
676 -- write configuration to /ctl wmii file
677 -- setctl({ "var" = "val", ...})
678 -- setctl("var, "val")
679 function setctl (first,second)
680 if type(first) == "table" and second == nil then
681 local x, y
682 for x, y in pairs(first) do
683 write ("/ctl", x .. " " .. y)
686 elseif type(first) == "string" and type(second) == "string" then
687 write ("/ctl", first .. " " .. second)
689 else
690 error ("expecting a table or two string arguments")
694 -- ------------------------------------------------------------------------
695 -- read a value from /ctl wmii file
696 function getctl (name)
697 local s
698 for s in iread("/ctl") do
699 local var,val = s:match("(%w+)%s+(.+)")
700 if var == name then
701 return val
704 return nil
707 -- ------------------------------------------------------------------------
708 -- set an internal wmiirc.lua variable
709 -- setconf({ "var" = "val", ...})
710 -- setconf("var, "val")
711 function setconf (first,second)
712 if type(first) == "table" and second == nil then
713 local x, y
714 for x, y in pairs(first) do
715 config[x] = y
718 elseif type(first) == "string" and type(second) == "string" then
719 config[first] = second
721 else
722 error ("expecting a table or two string arguments")
726 -- ------------------------------------------------------------------------
727 -- read an internal wmiirc.lua variable
728 function getconf (name)
729 return config[name]
732 -- ------------------------------------------------------------------------
733 -- run the event loop and process events, this function does not exit
734 function run_event_loop ()
735 log("wmii: updating lbar")
737 update_displayed_tags ()
739 log("wmii: updating active keys")
741 update_active_keys ()
743 log("wmii: starting event loop")
744 local ev, arg
745 for ev, arg in ievents() do
747 local fn = ev_handlers[ev] or ev_handlers["*"]
748 if fn then
749 fn (ev, arg)
752 log("wmii: event loop exited")