trunk: changeset 1958
[notion/jeffpc.git] / mod_query / mod_query.lua
blobf0559047288bec8e976e2e9e6266ff84e5fc9c32
1 --
2 -- ion/query/mod_query.lua -- Some common queries for Ion
3 --
4 -- Copyright (c) Tuomo Valkonen 2004-2005.
5 --
6 -- Ion is free software; you can redistribute it and/or modify it under
7 -- the terms of the GNU Lesser General Public License as published by
8 -- the Free Software Foundation; either version 2.1 of the License, or
9 -- (at your option) any later version.
13 -- This is a slight abuse of the _LOADED variable perhaps, but library-like
14 -- packages should handle checking if they're loaded instead of confusing
15 -- the user with require/include differences.
16 if _LOADED["mod_query"] then return end
18 if not ioncore.load_module("mod_query") then
19 return
20 end
22 local mod_query=_G["mod_query"]
24 assert(mod_query)
27 -- Generic helper functions {{{
30 function mod_query.make_completor(completefn)
31 local function completor(wedln, str)
32 wedln:set_completions(completefn(str))
33 end
34 return completor
35 end
38 function mod_query.query(mplex, prompt, initvalue, handler, completor,
39 context)
40 local function handle_it(str)
41 handler(mplex, str)
42 end
43 -- Check that no other queries are open in the mplex.
44 local l=mplex:llist(2)
45 for i, r in l do
46 if obj_is(r, "WEdln") then
47 return
48 end
49 end
50 wedln=mod_query.do_query(mplex, prompt, initvalue, handle_it, completor)
51 if context then
52 wedln:set_context(context)
53 end
54 end
57 --DOC
58 -- This function query will display a query with prompt \var{prompt} in
59 -- \var{mplex} and if the user answers affirmately, call \var{handler}
60 -- with \var{mplex} as parameter.
61 function mod_query.query_yesno(mplex, prompt, handler)
62 local function handler_yesno(mplex, str)
63 if str=="y" or str=="Y" or str=="yes" then
64 handler(mplex)
65 end
66 end
67 return mod_query.query(mplex, prompt, nil, handler_yesno, nil,
68 "yesno")
69 end
72 function mod_query.query_execfile(mplex, prompt, prog)
73 assert(prog~=nil)
74 local function handle_execwith(mplex, str)
75 ioncore.exec_on(mplex, prog.." "..string.shell_safe(str))
76 end
77 return mod_query.query(mplex, prompt, mod_query.get_initdir(mplex),
78 handle_execwith, mod_query.file_completor,
79 "filename")
80 end
83 function mod_query.query_execwith(mplex, prompt, dflt, prog, completor,
84 context)
85 local function handle_execwith(frame, str)
86 if not str or str=="" then
87 str=dflt
88 end
89 ioncore.exec_on(mplex, prog.." "..string.shell_safe(str))
90 end
91 return mod_query.query(mplex, prompt, nil, handle_execwith, completor,
92 context)
93 end
96 function mod_query.lookup_script_warn(mplex, script)
97 local script=ioncore.lookup_script(script)
98 if not script then
99 mod_query.warn(mplex, TR("Could not find %s", script))
101 return script
105 function mod_query.get_initdir(mplex)
106 --if mod_query.last_dir then
107 -- return mod_query.last_dir
108 --end
109 local wd=(ioncore.get_dir_for(mplex) or os.getenv("PWD"))
110 if wd==nil then
111 wd="/"
112 elseif string.sub(wd, -1)~="/" then
113 wd=wd .. "/"
115 return wd
118 local MAXDEPTH=10
120 function mod_query.lookup_workspace_classes()
121 local classes={}
123 for k, v in _G do
124 if type(v)=="table" and v.__typename then
125 v2=v.__parentclass
126 for i=1, MAXDEPTH do
127 if not v2 then
128 break
130 if v2.__typename=="WGenWS" then
131 table.insert(classes, v.__typename)
132 break
134 v2=v2.__parentclass
139 return classes
143 function mod_query.complete_from_list(list, str)
144 local results={}
145 local len=string.len(str)
146 if len==0 then
147 results=list
148 else
149 for _, m in list do
150 if string.sub(m, 1, len)==str then
151 table.insert(results, m)
156 return results
157 end
160 local pipes={}
162 mod_query.COLLECT_THRESHOLD=2000
164 --DOC
165 -- This function can be used to read completions from an external source.
166 -- The string \var{cmd} is a shell command to be executed. To its stdout,
167 -- the command should on the first line write the \var{common_part}
168 -- parameter of \fnref{WEdln.set_completions} and a single actual completion
169 -- on each of the successive lines.
170 function mod_query.popen_completions(wedln, cmd, beg)
172 local pst={wedln=wedln, maybe_stalled=0}
174 local function rcv(str)
175 local data=""
176 local results={}
177 local totallen=0
178 local lines=0
180 while str do
181 if pst.maybe_stalled>=2 then
182 pipes[rcv]=nil
183 return
185 pst.maybe_stalled=0
187 totallen=totallen+string.len(str)
188 if totallen>ioncore.RESULT_DATA_LIMIT then
189 error(TR("Too much result data"))
192 data=string.gsub(data..str, "([^\n]*)\n",
193 function(a)
194 -- ion-completefile will return possible
195 -- common part of path on the first line
196 -- and the entries in that directory on the
197 -- following lines.
198 if not results.common_part then
199 results.common_part=(beg or "")..a
200 else
201 table.insert(results, a)
203 lines=lines+1
204 end)
206 if lines>mod_query.COLLECT_THRESHOLD then
207 collectgarbage()
208 lines=0
211 str=coroutine.yield()
214 if not results.common_part then
215 results.common_part=beg
218 wedln:set_completions(results)
220 pipes[rcv]=nil
221 results={}
223 collectgarbage()
226 local found_clean=false
228 for k, v in pipes do
229 if v.wedln==wedln then
230 if v.maybe_stalled<2 then
231 v.maybe_stalled=v.maybe_stalled+1
232 found_clean=true
237 if not found_clean then
238 pipes[rcv]=pst
239 ioncore.popen_bgread(cmd, coroutine.wrap(rcv))
244 -- }}}
247 -- Simple queries for internal actions {{{
250 function mod_query.complete_name(str, list)
251 local entries={}
252 local l=string.len(str)
253 for i, reg in list do
254 local nm=reg:name()
255 if nm and string.sub(nm, 1, l)==str then
256 table.insert(entries, nm)
259 if table.getn(entries)==0 then
260 for i, reg in list do
261 local nm=reg:name()
262 if nm and string.find(nm, str, 1, true) then
263 table.insert(entries, nm)
267 return entries
270 function mod_query.complete_clientwin(str)
271 return mod_query.complete_name(str, ioncore.clientwin_list())
274 function mod_query.complete_workspace(str)
275 return mod_query.complete_name(str, ioncore.region_list("WGenWS"))
278 function mod_query.complete_region(str)
279 return mod_query.complete_name(str, ioncore.region_list())
283 function mod_query.gotoclient_handler(frame, str)
284 local cwin=ioncore.lookup_clientwin(str)
286 if cwin==nil then
287 mod_query.warn(frame, TR("Could not find client window %s.", str))
288 else
289 cwin:goto()
293 function mod_query.attachclient_handler(frame, str)
294 local cwin=ioncore.lookup_clientwin(str)
296 if not cwin then
297 mod_query.warn(frame, TR("Could not find client window %s.", str))
298 elseif frame:rootwin_of()~=cwin:rootwin_of() then
299 mod_query.warn(frame, TR("Cannot attach: different root windows."))
300 else
301 frame:attach(cwin, { switchto = true })
306 function mod_query.workspace_handler(mplex, name)
307 local ws=ioncore.lookup_region(name, "WGenWS")
308 if ws then
309 ws:goto()
310 return
313 local classes=mod_query.lookup_workspace_classes()
315 local function completor(wedln, what)
316 local results=mod_query.complete_from_list(classes, what)
317 wedln:set_completions(results)
320 local function handler(mplex, cls)
321 local scr=mplex:screen_of()
322 if not scr then
323 mod_query.warn(mplex, TR("Unable to create workspace: no screen."))
324 return
327 if not cls or cls=="" then
328 cls=ioncore.get().default_ws_type
331 local err=collect_errors(function()
332 ws=scr:attach_new({
333 type=cls,
334 name=name,
335 switchto=true
337 end)
338 if not ws then
339 mod_query.warn(mplex, err or TR("Unknown error"))
343 local defcls=ioncore.get().default_ws_type
344 local prompt=TR("Workspace type (%s):", defcls or TR("none"))
346 mod_query.query(mplex, prompt, "", handler, completor,
347 "workspacename")
351 --DOC
352 -- This query asks for the name of a client window and attaches
353 -- it to the frame the query was opened in. It uses the completion
354 -- function \fnref{ioncore.complete_clientwin}.
355 function mod_query.query_gotoclient(mplex)
356 mod_query.query(mplex, TR("Go to window:"), nil,
357 mod_query.gotoclient_handler,
358 mod_query.make_completor(mod_query.complete_clientwin),
359 "windowname")
362 --DOC
363 -- This query asks for the name of a client window and switches
364 -- focus to the one entered. It uses the completion function
365 -- \fnref{ioncore.complete_clientwin}.
366 function mod_query.query_attachclient(mplex)
367 mod_query.query(mplex, TR("Attach window:"), nil,
368 mod_query.attachclient_handler,
369 mod_query.make_completor(mod_query.complete_clientwin),
370 "windowname")
374 --DOC
375 -- This query asks for the name of a workspace. If a workspace
376 -- (an object inheriting \type{WGenWS}) with such a name exists,
377 -- it will be switched to. Otherwise a new workspace with the
378 -- entered name will be created and the user will be queried for
379 -- the type of the workspace.
380 function mod_query.query_workspace(mplex)
381 mod_query.query(mplex, TR("Go to or create workspace:"), nil,
382 mod_query.workspace_handler,
383 mod_query.make_completor(mod_query.complete_workspace),
384 "workspacename")
388 --DOC
389 -- This query asks whether the user wants to exit Ion (no session manager)
390 -- or close the session (running under a session manager that supports such
391 -- requests). If the answer is 'y', 'Y' or 'yes', so will happen.
392 function mod_query.query_shutdown(mplex)
393 mod_query.query_yesno(mplex, TR("Exit Ion/Shutdown session (y/n)?"),
394 ioncore.shutdown)
398 --DOC
399 -- This query asks whether the user wants restart Ioncore.
400 -- If the answer is 'y', 'Y' or 'yes', so will happen.
401 function mod_query.query_restart(mplex)
402 mod_query.query_yesno(mplex, TR("Restart Ion (y/n)?"), ioncore.restart)
406 --DOC
407 -- This function asks for a name new for the frame where the query
408 -- was created.
409 function mod_query.query_renameframe(frame)
410 mod_query.query(frame, TR("Frame name:"), frame:name(),
411 function(frame, str) frame:set_name(str) end,
412 nil, "framename")
416 --DOC
417 -- This function asks for a name new for the workspace on which the
418 -- query resides.
419 function mod_query.query_renameworkspace(mplex)
420 local ws=ioncore.find_manager(mplex, "WGenWS")
421 mod_query.query(mplex, TR("Workspace name:"), ws:name(),
422 function(mplex, str) ws:set_name(str) end,
423 nil, "framename")
427 -- }}}
430 -- Run/view/edit {{{
433 function mod_query.file_completor(wedln, str, wp, beg)
434 local ic=ioncore.lookup_script("ion-completefile")
435 if ic then
436 mod_query.popen_completions(wedln,
437 ic..(wp or " ")..string.shell_safe(str),
438 beg)
443 --DOC
444 -- Asks for a file to be edited. It uses the script \file{ion-edit} to
445 -- start a program to edit the file. This script uses \file{run-mailcap}
446 -- by default, but if you don't have it, you may customise the script.
447 function mod_query.query_editfile(mplex)
448 local script=mod_query.lookup_script_warn(mplex, "ion-edit")
449 mod_query.query_execfile(mplex, TR("Edit file:"), script)
453 --DOC
454 -- Asks for a file to be viewed. It uses the script \file{ion-view} to
455 -- start a program to view the file. This script uses \file{run-mailcap}
456 -- by default, but if you don't have it, you may customise the script.
457 function mod_query.query_runfile(mplex)
458 local script=mod_query.lookup_script_warn(mplex, "ion-view")
459 mod_query.query_execfile(mplex, TR("View file:"), script)
463 function mod_query.exec_completor(wedln, str)
464 local st, en, beg, tocompl=string.find(str, "^(.-)([^%s]*)$")
465 if string.len(beg)==0 then
466 mod_query.file_completor(wedln, tocompl, " -wp ")
467 else
468 mod_query.file_completor(wedln, tocompl, " ", beg)
473 local cmd_overrides={}
475 --DOC
476 -- Define a command override for the \fnrefx{mod_query}{query_exec} query.
477 function mod_query.defcmd(cmd, fn)
478 cmd_overrides[cmd]=fn
481 local function trim(s)
482 local _, _, sn=string.find(s, "^[%s]*(.-)[%s]*$")
483 return sn
486 function mod_query.exec_handler(frame, cmdline)
487 local _, _, cmd, params=string.find(cmdline, "^([^%s]+)(.*)")
488 if cmd and cmd_overrides[cmd] then
489 cmd_overrides[cmd](frame, trim(params))
490 else
491 if string.sub(cmdline, 1, 1)==":" then
492 local ix=mod_query.lookup_script_warn(frame, "ion-runinxterm")
493 if not ix then return end
494 cmdline=ix.." "..string.sub(cmdline, 2)
496 ioncore.exec_on(frame, cmdline)
500 --DOC
501 -- This function asks for a command to execute with \file{/bin/sh}.
502 -- If the command is prefixed with a colon (':'), the command will
503 -- be run in an XTerm (or other terminal emulator) using the script
504 -- \file{ion-runinxterm}.
505 function mod_query.query_exec(mplex)
506 mod_query.query(mplex, TR("Run:"), nil, mod_query.exec_handler,
507 mod_query.exec_completor,
508 "run")
512 -- }}}
515 -- SSH {{{
518 mod_query.known_hosts={}
521 function mod_query.get_known_hosts(mplex)
522 mod_query.known_hosts={}
523 local f
524 local h=os.getenv("HOME")
525 if h then
526 f=io.open(h.."/.ssh/known_hosts")
528 if not f then
529 warn(TR("Failed to open ~/.ssh/known_hosts"))
530 return
532 for l in f:lines() do
533 local st, en, hostname=string.find(l, "^([^%s,]+)")
534 if hostname then
535 table.insert(mod_query.known_hosts, hostname)
538 f:close()
542 function mod_query.complete_ssh(str)
543 local st, en, user, at, host=string.find(str, "^([^@]*)(@?)(.*)$")
545 if string.len(at)==0 and string.len(host)==0 then
546 host = user; user = ""
549 if at=="@" then
550 user = user .. at
553 local res = {}
555 if string.len(host)==0 then
556 if string.len(user)==0 then
557 return mod_query.known_hosts
560 for _, v in ipairs(mod_query.known_hosts) do
561 table.insert(res, user .. v)
563 return res
566 for _, v in ipairs(mod_query.known_hosts) do
567 local s, e=string.find(v, host, 1, true)
568 if s==1 and e>=1 then
569 table.insert(res, user .. v)
573 return res
577 --DOC
578 -- This query asks for a host to connect to with SSH. It starts
579 -- up ssh in a terminal using \file{ion-ssh}. Hosts to tab-complete
580 -- are read from \file{\~{}/.ssh/known\_hosts}.
581 function mod_query.query_ssh(mplex)
582 mod_query.get_known_hosts(mplex)
583 local script=mod_query.lookup_script_warn(mplex, "ion-ssh")
584 mod_query.query_execwith(mplex, TR("SSH to:"), nil, script,
585 mod_query.make_completor(mod_query.complete_ssh),
586 "ssh")
590 -- }}}
593 -- Man pages {{{{
596 function mod_query.man_completor(wedln, str)
597 local mc=ioncore.lookup_script("ion-completeman")
598 if mc then
599 mod_query.popen_completions(wedln, mc.." -complete "..
600 string.shell_safe(str))
605 --DOC
606 -- This query asks for a manual page to display. It uses the command
607 -- \file{ion-man} to run \file{man} in a terminal emulator. By customizing
608 -- this script it is possible use some other man page viewer. The script
609 -- \file{ion-completeman} is used to complete manual pages.
610 function mod_query.query_man(mplex)
611 local script=mod_query.lookup_script_warn(mplex, "ion-man")
612 local prgm=ioncore.progname()
613 local prompt=TR("Manual page (%s):", prgm)
614 mod_query.query_execwith(mplex, prompt, prgm, script,
615 mod_query.man_completor, "man")
619 -- }}}
622 -- Lua code execution {{{
625 function mod_query.create_run_env(mplex)
626 local origenv=getfenv()
627 local meta={__index=origenv, __newindex=origenv}
628 local env={
629 _=mplex,
630 _sub=mplex:current(),
632 setmetatable(env, meta)
633 return env
636 function mod_query.do_handle_lua(mplex, env, code)
637 local f, err=loadstring(code)
638 if not f then
639 mod_query.warn(mplex, err)
640 return
642 setfenv(f, env)
643 err=collect_errors(f)
644 if err then
645 mod_query.warn(mplex, err)
649 local function getindex(t)
650 local mt=getmetatable(t)
651 if mt then return mt.__index end
652 return nil
655 function mod_query.do_complete_lua(env, str)
656 -- Get the variable to complete, including containing tables.
657 -- This will also match string concatenations and such because
658 -- Lua's regexps don't support optional subexpressions, but we
659 -- handle them in the next step.
660 local comptab=env
661 local metas=true
662 local _, _, tocomp=string.find(str, "([%w_.:]*)$")
664 -- Descend into tables
665 if tocomp and string.len(tocomp)>=1 then
666 for t in string.gfind(tocomp, "([^.:]*)[.:]") do
667 metas=false
668 if string.len(t)==0 then
669 comptab=env;
670 elseif comptab then
671 if type(comptab[t])=="table" then
672 comptab=comptab[t]
673 elseif type(comptab[t])=="userdata" then
674 comptab=getindex(comptab[t])
675 metas=true
676 else
677 comptab=nil
683 if not comptab then return {} end
685 local compl={}
687 -- Get the actual variable to complete without containing tables
688 _, _, compl.common_part, tocomp=string.find(str, "(.-)([%w_]*)$")
690 local l=string.len(tocomp)
692 local tab=comptab
693 local seen={}
694 while true do
695 for k in tab do
696 if type(k)=="string" then
697 if string.sub(k, 1, l)==tocomp then
698 table.insert(compl, k)
703 -- We only want to display full list of functions for objects, not
704 -- the tables representing the classes.
705 --if not metas then break end
707 seen[tab]=true
708 tab=getindex(tab)
709 if not tab or seen[tab] then break end
712 -- If there was only one completion and it is a string or function,
713 -- concatenate it with "." or "(", respectively.
714 if table.getn(compl)==1 then
715 if type(comptab[compl[1]])=="table" then
716 compl[1]=compl[1] .. "."
717 elseif type(comptab[compl[1]])=="function" then
718 compl[1]=compl[1] .. "("
722 return compl
726 --DOC
727 -- This query asks for Lua code to execute. It sets the variable '\var{\_}'
728 -- in the local environment of the string to point to the mplex where the
729 -- query was created. It also sets the table \var{arg} in the local
730 -- environment to \code{\{_, _:current()\}}.
731 function mod_query.query_lua(mplex)
732 local env=mod_query.create_run_env(mplex)
734 local function complete(wedln, code)
735 wedln:set_completions(mod_query.do_complete_lua(env, code))
738 local function handler(mplex, code)
739 return mod_query.do_handle_lua(mplex, env, code)
742 mod_query.query(mplex, TR("Lua code: "), nil, handler, complete, "lua")
745 -- }}}
748 -- Menu query {{{
750 --DOC
751 -- This query can be used to create a query of a defined menu.
752 function mod_query.query_menu(mplex, prompt, menuname)
753 local menu=mod_menu.getmenu(menuname)
755 if not menu then
756 mod_query.warn(mplex, TR("Unknown menu %s.", tostring(menuname)))
757 return
759 function complete(str)
760 local results={}
761 local len=string.len(str)
762 for _, m in menu do
763 local mn=m.name
764 if len==0 or string.sub(mn, 1, len)==str then
765 table.insert(results, mn)
768 return results
769 end
771 local function handle(mplex, str)
772 local e
773 for k, v in menu do
774 if v.name==str then
776 break
779 if e then
780 if e.func then
781 local err=collect_errors(function() e.func(mplex) end)
782 if err then
783 mod_query.warn(mplex, err)
785 elseif e.submenu_fn then
786 mod_query.query_menu(mplex, TR("%s menu:", e.name),
787 e.submenu_fn())
789 else
790 mod_query.warn(mplex, TR("No entry '%s'", str))
794 mod_query.query(mplex, prompt, nil, handle,
795 mod_query.make_completor(complete), "menu")
798 -- }}}
801 -- Miscellaneous {{{
804 --DOC
805 -- Display an "About Ion" message in \var{mplex}.
806 function mod_query.show_about_ion(mplex)
807 mod_query.message(mplex, ioncore.aboutmsg())
811 --DOC
812 -- Show information about a client window.
813 function mod_query.show_clientwin(mplex, cwin)
814 local function indent(s)
815 local i=" "
816 return i..string.gsub(s, "\n", "\n"..i)
819 local function get_info(cwin)
820 local function n(s) return (s or "") end
821 local i=cwin:get_ident()
822 local s=TR("Title: %s\nClass: %s\nRole: %s\nInstance: %s\nXID: 0x%x",
823 n(cwin:name()), n(i.class), n(i.role), n(i.instance),
824 cwin:xid())
825 local t=TR("\nTransients:\n")
826 for k, v in cwin:managed_list() do
827 if obj_is(v, "WClientWin") then
828 s=s..t..indent(get_info(v))
829 t="\n"
832 return s
835 mod_query.message(mplex, get_info(cwin))
838 -- }}}
840 -- Load extras
841 dopath('mod_query_chdir')
843 -- Mark ourselves loaded.
844 _LOADED["mod_query"]=true
847 -- Load configuration file
848 dopath('cfg_query', true)