]> git.decadent.org.uk Git - ion3.git/blob - mod_query/mod_query.lua
[svn-inject] Installing original source of ion3
[ion3.git] / mod_query / mod_query.lua
1 --
2 -- ion/query/mod_query.lua -- Some common queries for Ion
3 -- 
4 -- Copyright (c) Tuomo Valkonen 2004-2006.
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.
10 --
11
12
13 -- This is a slight abuse of the package.loaded variable perhaps, but
14 -- library-like packages should handle checking if they're loaded instead of
15 -- confusing the user with require/include differences.
16 if package.loaded["mod_query"] then return end
17
18 if not ioncore.load_module("mod_query") then
19     return
20 end
21
22 local mod_query=_G["mod_query"]
23
24 assert(mod_query)
25
26
27 local DIE_TIMEOUT_ERRORCODE=10 -- 10 seconds
28 local DIE_TIMEOUT_NO_ERRORCODE=2 -- 2 seconds
29
30
31 -- Generic helper functions {{{
32
33
34 function mod_query.make_completor(completefn)
35     local function completor(cp, str, point)
36         cp:set_completions(completefn(str, point))
37     end
38     return completor
39 end
40
41
42 --DOC
43 -- Low-level query routine. \var{mplex} is the \type{WMPlex} to display
44 -- the query in, \var{prompt} the prompt string, and \var{initvalue}
45 -- the initial contents of the query box. \var{handler} is a function
46 -- that receives (\var{mplex}, result string) as parameter when the
47 -- query has been succesfully completed, \var{completor} the completor
48 -- routine which receives a (\var{cp}, \var{str}, \var{point}) as parameters.
49 -- The parameter \var{str} is the string to be completed and \var{point}
50 -- cursor's location within it. Completions should be eventually,
51 -- possibly asynchronously, set with \fnref{WComplProxy.set_completions} 
52 -- on \var{cp}.
53 function mod_query.query(mplex, prompt, initvalue, handler, completor,
54                          context)
55     local function handle_it(str)
56         handler(mplex, str)
57     end
58     local function cycle(wedln)
59         wedln:complete('next', 'normal')
60     end
61
62     -- Check that no other queries are open in the mplex.
63     local l=mplex:managed_list()
64     for i, r in pairs(l) do
65         if obj_is(r, "WEdln") then
66             return
67         end
68     end
69     local wedln=mod_query.do_query(mplex, prompt, initvalue, 
70                                    handle_it, completor, cycle)
71     if context then
72         wedln:set_context(context)
73     end
74     
75     return wedln
76 end
77
78
79 --DOC
80 -- This function query will display a query with prompt \var{prompt} in
81 -- \var{mplex} and if the user answers affirmately, call \var{handler}
82 -- with \var{mplex} as parameter.
83 function mod_query.query_yesno(mplex, prompt, handler)
84     local function handler_yesno(mplex, str)
85         if str=="y" or str=="Y" or str=="yes" then
86             handler(mplex)
87         end
88     end
89     return mod_query.query(mplex, prompt, nil, handler_yesno, nil,
90                            "yesno")
91 end
92
93
94 local errdata={}
95
96 local function maybe_finish(pid)
97     local t=errdata[pid]
98
99     if t and t.closed and t.dietime then
100         errdata[pid]=nil
101         local tmd=os.difftime(t.dietime, t.starttime)
102         --if tmd<DIE_TIMEOUT_ERRORCODE and t.signaled then
103         --    local msg=TR("Program received signal ")..t.termsig.."\n"
104         --    mod_query.warn(t.mplex, msg..(t.errs or ""))
105         --else
106         if ((tmd<DIE_TIMEOUT_ERRORCODE and (t.hadcode or t.signaled)) or
107                 (tmd<DIE_TIMEOUT_NO_ERRORCODE)) and t.errs then
108             mod_query.warn(t.mplex, t.errs)
109         end
110     end
111 end
112
113
114 local badsig_={4, 5, 6, 7, 8, 11}
115 local badsig={}
116 for _, v in pairs(badsig_) do 
117     badsig[v]=true 
118 end
119
120 local function chld_handler(p)
121     local t=errdata[p.pid]
122     if t then
123         t.dietime=os.time()
124         t.signaled=(p.signaled and badsig[p.termsig])
125         t.termsig=p.termsig
126         t.hadcode=(p.exited and p.exitstatus~=0)
127         maybe_finish(pid)
128     end
129 end
130
131 ioncore.get_hook("ioncore_sigchld_hook"):add(chld_handler)
132
133 function mod_query.exec_on_merr(mplex, cmd)
134     local pid
135     
136     local function monitor(str)
137         if pid then
138             local t=errdata[pid]
139             if t then
140                 if str then
141                     t.errs=(t.errs or "")..str
142                 else
143                     t.closed=true
144                     maybe_finish(pid)
145                 end
146             end
147         end
148     end
149     
150     local function timeout()
151         errdata[pid]=nil
152     end
153     
154     pid=ioncore.exec_on(mplex, cmd, monitor)
155     
156     if pid<=0 then
157         return
158     end
159
160     local tmr=ioncore.create_timer();
161     local tmd=math.max(DIE_TIMEOUT_NO_ERRORCODE, DIE_TIMEOUT_ERRORCODE)
162     local now=os.time()  
163     tmr:set(tmd*1000, timeout)
164     
165     errdata[pid]={tmr=tmr, mplex=mplex, starttime=now}
166 end
167
168
169 function mod_query.file_completor(wedln, str)
170     local ic=ioncore.lookup_script("ion-completefile")
171     if ic then
172         mod_query.popen_completions(wedln,
173                                    ic.." "..string.shell_safe(str))
174     end
175 end
176
177
178 function mod_query.query_execfile(mplex, prompt, prog)
179     assert(prog~=nil)
180     local function handle_execwith(mplex, str)
181         mod_query.exec_on_merr(mplex, prog.." "..string.shell_safe(str))
182     end
183     return mod_query.query(mplex, prompt, mod_query.get_initdir(mplex),
184                            handle_execwith, mod_query.file_completor,
185                            "filename")
186 end
187
188
189 function mod_query.query_execwith(mplex, prompt, dflt, prog, completor,
190                                   context, noquote)
191     local function handle_execwith(frame, str)
192         if not str or str=="" then
193             str=dflt
194         end
195         local args=(noquote and str or string.shell_safe(str))
196         mod_query.exec_on_merr(mplex, prog.." "..args)
197     end
198     return mod_query.query(mplex, prompt, nil, handle_execwith, completor,
199                            context)
200 end
201
202
203 function mod_query.get_initdir(mplex)
204     --if mod_query.last_dir then
205     --    return mod_query.last_dir
206     --end
207     local wd=(ioncore.get_dir_for(mplex) or os.getenv("PWD"))
208     if wd==nil then
209         wd="/"
210     elseif string.sub(wd, -1)~="/" then
211         wd=wd .. "/"
212     end
213     return wd
214 end
215
216
217 local MAXDEPTH=10
218
219
220 function mod_query.complete_from_list(list, str)
221     local results={}
222     local len=string.len(str)
223     if len==0 then
224         results=list
225     else
226         for _, m in pairs(list) do
227             if string.sub(m, 1, len)==str then
228                 table.insert(results, m)
229             end
230         end
231     end
232     
233     return results
234 end    
235
236
237 local pipes={}
238
239 mod_query.COLLECT_THRESHOLD=2000
240
241 --DOC
242 -- This function can be used to read completions from an external source.
243 -- The parameter \var{cp} is the completion proxy to be used,
244 -- and the string \var{cmd} the shell command to be executed. To its stdout, 
245 -- the command should on the first line write the \var{common_beg} 
246 -- parameter of \fnref{WComplProxy.set_completions} (which \var{fn} maybe used
247 -- to override) and a single actual completion on each of the successive lines.
248 -- The function \var{reshnd} may be used to override a result table
249 -- building routine.
250 function mod_query.popen_completions(cp, cmd, fn, reshnd)
251     
252     local pst={cp=cp, maybe_stalled=0}
253     
254     if not reshnd then
255         reshnd = function(rs, a)
256                      if not rs.common_beg then
257                          rs.common_beg=a
258                      else
259                          table.insert(rs, a)
260                      end
261                  end
262     end
263
264     local function rcv(str)
265         local data=""
266         local results={}
267         local totallen=0
268         local lines=0
269         
270         while str do
271             if pst.maybe_stalled>=2 then
272                 pipes[rcv]=nil
273                 return
274             end
275             pst.maybe_stalled=0
276             
277             totallen=totallen+string.len(str)
278             if totallen>ioncore.RESULT_DATA_LIMIT then
279                 error(TR("Too much result data"))
280             end
281             
282
283             data=string.gsub(data..str, "([^\n]*)\n", 
284                              function(s) 
285                                  reshnd(results, s) 
286                                  lines=lines+1
287                                  return ""
288                              end)
289             
290             if lines>mod_query.COLLECT_THRESHOLD then
291                 collectgarbage()
292                 lines=0
293             end
294             
295             str=coroutine.yield()
296         end
297         
298         if not results.common_beg then
299             results.common_beg=beg
300         end
301         
302         (fn or WComplProxy.set_completions)(cp, results)
303         
304         pipes[rcv]=nil
305         results={}
306         
307         collectgarbage()
308     end
309     
310     local found_clean=false
311     
312     for k, v in pairs(pipes) do
313         if v.cp==cp then
314             if v.maybe_stalled<2 then
315                 v.maybe_stalled=v.maybe_stalled+1
316                 found_clean=true
317             end
318         end
319     end
320     
321     if not found_clean then
322         pipes[rcv]=pst
323         ioncore.popen_bgread(cmd, coroutine.wrap(rcv))
324     end
325 end
326
327
328 -- }}}
329
330
331 -- Simple queries for internal actions {{{
332
333
334 function mod_query.call_warn(mplex, fn)
335     local err = collect_errors(fn)
336     if err then
337         mod_query.warn(mplex, err)
338     end
339     return err
340 end
341
342
343 function mod_query.complete_name(str, list)
344     local entries={}
345     local l=string.len(str)
346     for i, reg in pairs(list) do
347         local nm=reg:name()
348         if nm and string.sub(nm, 1, l)==str then
349             table.insert(entries, nm)
350         end
351     end
352     if #entries==0 then
353         for i, reg in pairs(list) do
354             local nm=reg:name()
355             if nm and string.find(nm, str, 1, true) then
356                 table.insert(entries, nm)
357             end
358         end
359     end
360     return entries
361 end
362
363 function mod_query.complete_clientwin(str)
364     return mod_query.complete_name(str, ioncore.clientwin_list())
365 end
366
367 function mod_query.complete_workspace(str)
368     return mod_query.complete_name(str, ioncore.region_list("WGroupWS"))
369 end
370
371 function mod_query.complete_region(str)
372     return mod_query.complete_name(str, ioncore.region_list())
373 end
374
375
376 function mod_query.gotoclient_handler(frame, str)
377     local cwin=ioncore.lookup_clientwin(str)
378     
379     if cwin==nil then
380         mod_query.warn(frame, TR("Could not find client window %s.", str))
381     else
382         cwin:goto()
383     end
384 end
385
386 function mod_query.attachclient_handler(frame, str)
387     local cwin=ioncore.lookup_clientwin(str)
388     
389     if not cwin then
390         mod_query.warn(frame, TR("Could not find client window %s.", str))
391         return
392     end
393     
394     local reg=cwin:manager()
395     local attach
396     
397     if not obj_is(reg, "WGroupCW") then
398         reg = cwin
399         attach = function()
400                      frame:attach_new {
401                          type = "WGroupCW", 
402                          switchto = true,
403                          managed = {{ reg = cwin, bottom = true }}
404                      }
405                  end
406     else
407         attach = function()
408                      frame:attach(reg, { switchto = true })
409                  end
410     end
411         
412     if frame:rootwin_of()~=reg:rootwin_of() then
413         mod_query.warn(frame, TR("Cannot attach: different root windows."))
414     elseif reg:manager()==frame then
415         reg:goto()
416     else
417         mod_query.call_warn(frame, attach)
418     end
419 end
420
421
422 function mod_query.workspace_handler(mplex, name)
423     local ws=ioncore.lookup_region(name, "WGroupWS")
424     if ws then
425         ws:goto()
426         return
427     end
428
429     local scr=mplex:screen_of()
430     
431     local function mkws()
432         if not ioncore.create_ws(scr, {name=name}) then
433             error(TR("Unknown error"))
434         end
435     end
436
437     mod_query.call_warn(mplex, mkws)
438 end
439
440
441 --DOC
442 -- This query asks for the name of a client window and attaches
443 -- it to the frame the query was opened in. It uses the completion
444 -- function \fnref{ioncore.complete_clientwin}.
445 function mod_query.query_gotoclient(mplex)
446     mod_query.query(mplex, TR("Go to window:"), nil,
447                     mod_query.gotoclient_handler,
448                     mod_query.make_completor(mod_query.complete_clientwin),
449                     "windowname")
450 end
451
452 --DOC
453 -- This query asks for the name of a client window and switches
454 -- focus to the one entered. It uses the completion function
455 -- \fnref{ioncore.complete_clientwin}.
456 function mod_query.query_attachclient(mplex)
457     mod_query.query(mplex, TR("Attach window:"), nil,
458                     mod_query.attachclient_handler, 
459                     mod_query.make_completor(mod_query.complete_clientwin),
460                     "windowname")
461 end
462
463
464 --DOC
465 -- This query asks for the name of a workspace. If a workspace
466 -- (an object inheriting \type{WGroupWS}) with such a name exists,
467 -- it will be switched to. Otherwise a new workspace with the
468 -- entered name will be created and the user will be queried for
469 -- the type of the workspace.
470 function mod_query.query_workspace(mplex)
471     mod_query.query(mplex, TR("Go to or create workspace:"), nil, 
472                     mod_query.workspace_handler,
473                     mod_query.make_completor(mod_query.complete_workspace),
474                     "workspacename")
475 end
476
477
478 --DOC
479 -- This query asks whether the user wants to exit Ion (no session manager)
480 -- or close the session (running under a session manager that supports such
481 -- requests). If the answer is 'y', 'Y' or 'yes', so will happen.
482 function mod_query.query_shutdown(mplex)
483     mod_query.query_yesno(mplex, TR("Exit Ion/Shutdown session (y/n)?"),
484                          ioncore.shutdown)
485 end
486
487
488 --DOC
489 -- This query asks whether the user wants restart Ioncore.
490 -- If the answer is 'y', 'Y' or 'yes', so will happen.
491 function mod_query.query_restart(mplex)
492     mod_query.query_yesno(mplex, TR("Restart Ion (y/n)?"), ioncore.restart)
493 end
494
495
496 --DOC
497 -- This function asks for a name new for the frame where the query
498 -- was created.
499 function mod_query.query_renameframe(frame)
500     mod_query.query(frame, TR("Frame name:"), frame:name(),
501                     function(frame, str) frame:set_name(str) end,
502                     nil, "framename")
503 end
504
505
506 --DOC
507 -- This function asks for a name new for the workspace on which the
508 -- query resides.
509 function mod_query.query_renameworkspace(mplex)
510     local ws=ioncore.find_manager(mplex, "WGroupWS")
511     mod_query.query(mplex, TR("Workspace name:"), ws:name(),
512                     function(mplex, str) ws:set_name(str) end,
513                     nil, "framename")
514 end
515
516
517 -- }}}
518
519
520 -- Run/view/edit {{{
521
522
523 --DOC
524 -- Asks for a file to be edited. This script uses 
525 -- \command{run-mailcap --mode=edit} by default, but you may provide an
526 -- alternative script to use. The default prompt is "Edit file:" (translated).
527 function mod_query.query_editfile(mplex, script, prompt)
528     mod_query.query_execfile(mplex, 
529                              prompt or TR("Edit file:"), 
530                              script or "run-mailcap --action=edit")
531 end
532
533
534 --DOC
535 -- Asks for a file to be viewed. This script uses 
536 -- \command{run-mailcap --action=view} by default, but you may provide an
537 -- alternative script to use. The default prompt is "View file:" (translated).
538 function mod_query.query_runfile(mplex, script, prompt)
539     mod_query.query_execfile(mplex, 
540                              prompt or TR("View file:"), 
541                              script or "run-mailcap --action=view")
542
543 end
544
545
546 local function isspace(s)
547     return string.find(s, "^%s*$")~=nil
548 end
549
550
551 local function break_cmdline(str, no_ws)
552     local st, en, beg, rest, ch, rem
553     local res={""}
554
555     local function ins(str)
556         local n=#res
557         if string.find(res[n], "^%s+$") then
558             table.insert(res, str)
559         else
560             res[n]=res[n]..str
561         end
562     end
563
564     local function ins_space(str)
565         local n=#res
566         if no_ws then
567             if res[n]~="" then
568                 table.insert(res, "")
569             end
570         else
571             if isspace(res[n]) then
572                 res[n]=res[n]..str
573             else
574                 table.insert(res, str)
575             end
576         end
577     end
578
579     -- Handle terminal startup syntax
580     st, en, beg, ch, rest=string.find(str, "^(%s*)(:+)(.*)")
581     if beg then
582         if string.len(beg)>0 then
583             ins_space(beg)
584         end
585         ins(ch)
586         ins_space("")
587         str=rest
588     end
589
590     while str~="" do
591         st, en, beg, rest, ch=string.find(str, "^(.-)(([%s'\"\\|])(.*))")
592         if not beg then
593             ins(str)
594             break
595         end
596
597         ins(beg)
598         str=rest
599         
600         local sp=false
601         
602         if ch=="\\" then
603             st, en, beg, rest=string.find(str, "^(\\.)(.*)")
604         elseif ch=='"' then
605             st, en, beg, rest=string.find(str, "^(\".-[^\\]\")(.*)")
606             
607             if not beg then
608                 st, en, beg, rest=string.find(str, "^(\"\")(.*)")
609             end
610         elseif ch=="'" then
611             st, en, beg, rest=string.find(str, "^('.-')(.*)")
612         else
613             if ch=='|' then
614                 ins_space('')
615                 ins(ch)
616             else -- ch==' '
617                 ins_space(ch)
618             end
619             st, en, beg, rest=string.find(str, "^.(%s*)(.*)")
620             assert(beg and rest)
621             ins_space(beg)
622             sp=true
623             str=rest
624         end
625         
626         if not sp then
627             if not beg then
628                 beg=str
629                 rest=""
630             end
631             ins(beg)
632             str=rest
633         end
634     end
635     
636     return res
637 end
638
639
640 local function unquote(str)
641     str=string.gsub(str, "^['\"]", "")
642     str=string.gsub(str, "([^\\])['\"]", "%1")
643     str=string.gsub(str, "\\(.)", "%1")
644     return str
645 end
646
647
648 local function quote(str)
649     return string.gsub(str, "([%(%)\"'\\%*%?%[%]%| ])", "\\%1")
650 end
651
652
653 local function find_point(strs, point)
654     for i, s in ipairs(strs) do
655         point=point-string.len(s)
656         if point<=1 then
657             return i
658         end
659     end
660     return #strs
661 end
662
663
664 function mod_query.exec_completor(wedln, str, point)
665     local parts=break_cmdline(str)
666     local complidx=find_point(parts, point+1)
667     
668     local s_compl, s_beg, s_end="", "", ""
669     
670     if complidx==1 and string.find(parts[1], "^:+$") then
671         complidx=complidx+1
672     end
673     
674     if string.find(parts[complidx], "[^%s]") then
675         s_compl=unquote(parts[complidx])
676     end
677     
678     for i=1, complidx-1 do
679         s_beg=s_beg..parts[i]
680     end
681     
682     for i=complidx+1, #parts do
683         s_end=s_end..parts[i]
684     end
685     
686     local wp=" "
687     if complidx==1 or (complidx==2 and isspace(parts[1])) then
688         wp=" -wp "
689     elseif string.find(parts[1], "^:+$") then
690         if complidx==2 then
691             wp=" -wp "
692         elseif string.find(parts[2], "^%s*$") then
693             if complidx==3 then
694                 wp=" -wp "
695             end
696         end
697     end
698
699     local function set_fn(cp, res)
700         res=table.map(quote, res)
701         res.common_beg=s_beg..(res.common_beg or "")
702         res.common_end=(res.common_end or "")..s_end
703         cp:set_completions(res)
704     end
705
706     local function filter_fn(res, s)
707         if not res.common_beg then
708             if s=="./" then
709                 res.common_beg=""
710             else
711                 res.common_beg=s
712             end
713         else
714             table.insert(res, s)
715         end
716     end
717     
718     local ic=ioncore.lookup_script("ion-completefile")
719     if ic then
720         mod_query.popen_completions(wedln,
721                                    ic..wp..string.shell_safe(s_compl),
722                                    set_fn, filter_fn)
723     end
724 end
725
726
727 local cmd_overrides={}
728
729
730 --DOC
731 -- Define a command override for the \fnrefx{mod_query}{query_exec} query.
732 function mod_query.defcmd(cmd, fn)
733     cmd_overrides[cmd]=fn
734 end
735
736
737 function mod_query.exec_handler(mplex, cmdline)
738     local parts=break_cmdline(cmdline, true)
739     local cmd=table.remove(parts, 1)
740     
741     if cmd_overrides[cmd] then
742         cmd_overrides[cmd](mplex, table.map(unquote, parts))
743     elseif cmd~="" then
744         mod_query.exec_on_merr(mplex, cmdline)
745     end
746 end
747
748
749 --DOC
750 -- This function asks for a command to execute with \file{/bin/sh}.
751 -- If the command is prefixed with a colon (':'), the command will
752 -- be run in an XTerm (or other terminal emulator) using the script
753 -- \file{ion-runinxterm}. Two colons ('::') will ask you to press 
754 -- enter after the command has finished.
755 function mod_query.query_exec(mplex)
756     mod_query.query(mplex, TR("Run:"), nil, mod_query.exec_handler, 
757                     mod_query.exec_completor,
758                     "run")
759 end
760
761
762 -- }}}
763
764
765 -- SSH {{{
766
767
768 mod_query.known_hosts={}
769
770
771 function mod_query.get_known_hosts(mplex)
772     mod_query.known_hosts={}
773     local f
774     local h=os.getenv("HOME")
775     if h then 
776         f=io.open(h.."/.ssh/known_hosts")
777     end
778     if not f then 
779         warn(TR("Failed to open ~/.ssh/known_hosts"))
780         return
781     end
782     for l in f:lines() do
783         local st, en, hostname=string.find(l, "^([^%s,]+)")
784         if hostname then
785             table.insert(mod_query.known_hosts, hostname)
786         end
787     end
788     f:close()
789 end
790
791
792 mod_query.hostnicks={}
793
794 function mod_query.get_hostnicks(mplex)
795     mod_query.hostnicks={}
796     local f
797     local substr, pat, patterns
798     local h=os.getenv("HOME")
799
800     if h then
801         f=io.open(h.."/.ssh/config")
802     end
803     if not f then 
804         warn(TR("Failed to open ~/.ssh/config"))
805         return
806     end
807
808     for l in f:lines() do
809         _, _, substr=string.find(l, "^%s*[hH][oO][sS][tT](.*)")
810         if substr then
811             _, _, pat=string.find(substr, "^%s*[=%s]%s*(%S.*)")
812             if pat then
813                 patterns=pat
814             elseif string.find(substr, "^[nN][aA][mM][eE]")
815                 and patterns then
816                 for s in string.gfind(patterns, "%S+") do
817                     if not string.find(s, "[*?]") then
818                         table.insert(mod_query.hostnicks, s)
819                     end
820                 end
821             end
822         end
823     end
824     f:close()
825 end
826
827
828 function mod_query.complete_ssh(str)
829     local st, en, user, at, host=string.find(str, "^([^@]*)(@?)(.*)$")
830     
831     if string.len(at)==0 and string.len(host)==0 then
832         host = user; user = ""
833     end
834     
835     if at=="@" then 
836         user = user .. at 
837     end
838     
839     local res = {}
840     
841     if string.len(host)==0 then
842         if string.len(user)==0 then
843             return mod_query.ssh_completions
844         end
845         
846         for _, v in ipairs(mod_query.ssh_completions) do
847             table.insert(res, user .. v)
848         end
849         return res
850     end
851     
852     for _, v in ipairs(mod_query.ssh_completions) do
853         local s, e=string.find(v, host, 1, true)
854         if s==1 and e>=1 then
855             table.insert(res, user .. v)
856         end
857     end
858     
859     return res
860 end
861
862 mod_query.ssh_completions={}
863
864 --DOC
865 -- This query asks for a host to connect to with SSH. 
866 -- Hosts to tab-complete are read from \file{\~{}/.ssh/known\_hosts}.
867 function mod_query.query_ssh(mplex, ssh)
868     mod_query.get_known_hosts(mplex)
869     mod_query.get_hostnicks(mplex)
870
871     for _, v in ipairs(mod_query.known_hosts) do
872         table.insert(mod_query.ssh_completions, v)
873     end
874     for _, v in ipairs(mod_query.hostnicks) do
875         table.insert(mod_query.ssh_completions, v)
876     end
877
878     ssh=(ssh or ":ssh")
879
880     local function handle_exec(mplex, str)
881         if not (str and string.find(str, "[^%s]")) then
882             return
883         end
884         
885         mod_query.exec_on_merr(mplex, ssh.." "..string.shell_safe(str))
886     end
887     
888     return mod_query.query(mplex, TR("SSH to:"), nil, handle_exec,
889                            mod_query.make_completor(mod_query.complete_ssh),
890                            "ssh")
891 end
892
893 -- }}}
894
895
896 -- Man pages {{{{
897
898
899 function mod_query.man_completor(wedln, str)
900     local mc=ioncore.lookup_script("ion-completeman")
901     if mc then
902         mod_query.popen_completions(wedln, (mc.." -complete "
903                                             ..string.shell_safe(str)))
904     end
905 end
906
907
908 --DOC
909 -- This query asks for a manual page to display. By default it runs the
910 -- \command{man} command in an \command{xterm} using \command{ion-runinxterm},
911 -- but it is possible to pass another program as the \var{prog} argument.
912 function mod_query.query_man(mplex, prog)
913     local dflt=ioncore.progname()
914     mod_query.query_execwith(mplex, TR("Manual page (%s):", dflt), 
915                              dflt, prog or ":man", 
916                              mod_query.man_completor, "man",
917                              true --[[ no quoting ]])
918 end
919
920
921 -- }}}
922
923
924 -- Lua code execution {{{
925
926
927 function mod_query.create_run_env(mplex)
928     local origenv=getfenv()
929     local meta={__index=origenv, __newindex=origenv}
930     local env={
931         _=mplex, 
932         _sub=mplex:current(),
933         print=my_print
934     }
935     setmetatable(env, meta)
936     return env
937 end
938
939 function mod_query.do_handle_lua(mplex, env, code)
940     local print_res
941     local function collect_print(...)
942         local tmp=""
943         local l=#arg
944         for i=1,l do
945             tmp=tmp..tostring(arg[i])..(i==l and "\n" or "\t")
946         end
947         print_res=(print_res and print_res..tmp or tmp)
948     end
949
950     local f, err=loadstring(code)
951     if not f then
952         mod_query.warn(mplex, err)
953         return
954     end
955     
956     env.print=collect_print
957     setfenv(f, env)
958     
959     err=collect_errors(f)
960     if err then
961         mod_query.warn(mplex, err)
962     elseif print_res then
963         mod_query.message(mplex, print_res)
964     end
965 end
966
967 local function getindex(t)
968     local mt=getmetatable(t)
969     if mt then return mt.__index end
970     return nil
971 end
972
973 function mod_query.do_complete_lua(env, str)
974     -- Get the variable to complete, including containing tables.
975     -- This will also match string concatenations and such because
976     -- Lua's regexps don't support optional subexpressions, but we
977     -- handle them in the next step.
978     local comptab=env
979     local metas=true
980     local _, _, tocomp=string.find(str, "([%w_.:]*)$")
981     
982     -- Descend into tables
983     if tocomp and string.len(tocomp)>=1 then
984         for t in string.gfind(tocomp, "([^.:]*)[.:]") do
985             metas=false
986             if string.len(t)==0 then
987                 comptab=env;
988             elseif comptab then
989                 if type(comptab[t])=="table" then
990                     comptab=comptab[t]
991                 elseif type(comptab[t])=="userdata" then
992                     comptab=getindex(comptab[t])
993                     metas=true
994                 else
995                     comptab=nil
996                 end
997             end
998         end
999     end
1000     
1001     if not comptab then return {} end
1002     
1003     local compl={}
1004     
1005     -- Get the actual variable to complete without containing tables
1006     _, _, compl.common_beg, tocomp=string.find(str, "(.-)([%w_]*)$")
1007     
1008     local l=string.len(tocomp)
1009     
1010     local tab=comptab
1011     local seen={}
1012     while true do
1013         if type(tab) == "table" then
1014             for k in pairs(tab) do
1015                 if type(k)=="string" then
1016                     if string.sub(k, 1, l)==tocomp then
1017                         table.insert(compl, k)
1018                     end
1019                 end
1020             end
1021         end
1022
1023         -- We only want to display full list of functions for objects, not 
1024         -- the tables representing the classes.
1025         --if not metas then break end
1026         
1027         seen[tab]=true
1028         tab=getindex(tab)
1029         if not tab or seen[tab] then break end
1030     end
1031     
1032     -- If there was only one completion and it is a string or function,
1033     -- concatenate it with "." or "(", respectively.
1034     if #compl==1 then
1035         if type(comptab[compl[1]])=="table" then
1036             compl[1]=compl[1] .. "."
1037         elseif type(comptab[compl[1]])=="function" then
1038             compl[1]=compl[1] .. "("
1039         end
1040     end
1041     
1042     return compl
1043 end
1044
1045
1046 --DOC
1047 -- This query asks for Lua code to execute. It sets the variable '\var{\_}'
1048 -- in the local environment of the string to point to the mplex where the
1049 -- query was created. It also sets the table \var{arg} in the local
1050 -- environment to \code{\{_, _:current()\}}.
1051 function mod_query.query_lua(mplex)
1052     local env=mod_query.create_run_env(mplex)
1053     
1054     local function complete(cp, code)
1055         cp:set_completions(mod_query.do_complete_lua(env, code))
1056     end
1057     
1058     local function handler(mplex, code)
1059         return mod_query.do_handle_lua(mplex, env, code)
1060     end
1061     
1062     mod_query.query(mplex, TR("Lua code:"), nil, handler, complete, "lua")
1063 end
1064
1065 -- }}}
1066
1067
1068 -- Menu query {{{
1069
1070 --DOC
1071 -- This query can be used to create a query of a defined menu.
1072 function mod_query.query_menu(mplex, themenu, prompt)
1073     local _sub=mplex:current()
1074     local menu=ioncore.evalmenu(themenu, {mplex, _sub})
1075     local menuname=(type(themenu)=="string" and themenu or "?")
1076     
1077     if not menu then
1078         mod_query.warn(mplex, TR("Unknown menu %s.", tostring(themenu)))
1079         return
1080     end
1081     
1082     if not prompt then
1083         prompt=menuname..":"
1084     else
1085         prompt=TR(prompt)
1086     end
1087
1088     local function xform_name(n, is_submenu)
1089         return (string.lower(string.gsub(n, "[-%s]+", "-"))
1090                 ..(is_submenu and "/" or ""))
1091     end
1092
1093     local function xform_menu(t, m, p)
1094         for _, v in ipairs(m) do
1095             if v.name then
1096                 local is_submenu=v.submenu_fn
1097                 local n=p..xform_name(v.name, is_submenu)
1098                 while t[n] do
1099                     n=n.."'"
1100                 end
1101                 t[n]=v
1102                 if is_submenu and not v.noautoexpand then
1103                     local sm=v.submenu_fn()
1104                     if sm then
1105                         xform_menu(t, sm, n)
1106                     else
1107                         ioncore.warn_traced(TR("Missing submenu ")
1108                                             ..(v.name or ""))
1109                     end
1110                 end
1111             end
1112         end
1113         return t
1114     end
1115     
1116     local ntab=xform_menu({}, menu, "")
1117     
1118     local function complete(str)
1119         local results={}
1120         for s, e in pairs(ntab) do
1121             if string.find(s, str, 1, true) then
1122                 table.insert(results, s)
1123             end
1124         end
1125         return results
1126     end
1127     
1128     local function handle(mplex, str)
1129         local e=ntab[str]
1130         if e then
1131             if e.func then
1132                 local err=collect_errors(function() 
1133                                              e.func(mplex, _sub) 
1134                                          end)
1135                 if err then
1136                     mod_query.warn(mplex, err)
1137                 end
1138             elseif e.submenu_fn then
1139                 mod_query.query_menu(mplex, e.submenu_fn(),
1140                                      TR("%s:", e.name))
1141             end
1142         else
1143             mod_query.warn(mplex, TR("No entry '%s'", str))
1144         end
1145     end
1146     
1147     mod_query.query(mplex, prompt, nil, handle, 
1148                     mod_query.make_completor(complete), "menu."..menuname)
1149 end
1150
1151 -- }}}
1152
1153
1154 -- Miscellaneous {{{
1155
1156
1157 --DOC 
1158 -- Display an "About Ion" message in \var{mplex}.
1159 function mod_query.show_about_ion(mplex)
1160     mod_query.message(mplex, ioncore.aboutmsg())
1161 end
1162
1163
1164 --DOC
1165 -- Show information about a region tree 
1166 function mod_query.show_tree(mplex, reg, max_depth)
1167     local function indent(s)
1168         local i="    "
1169         return i..string.gsub(s, "\n", "\n"..i)
1170     end
1171     
1172     local function get_info(reg, indent, d)
1173         if not reg then
1174             return (indent .. "No region")
1175         end
1176         
1177         local function n(s) return (s or "") end
1178
1179         local s=string.format("%s%s \"%s\"", indent, obj_typename(reg),
1180                               n(reg:name()))
1181         indent = indent .. "  "
1182         if obj_is(reg, "WClientWin") then
1183             local i=reg:get_ident()
1184             s=s .. TR("\n%sClass: %s\n%sRole: %s\n%sInstance: %s\n%sXID: 0x%x",
1185                       indent, n(i.class), 
1186                       indent, n(i.role), 
1187                       indent, n(i.instance), 
1188                       indent, reg:xid())
1189         end
1190         
1191         if (not max_depth or max_depth > d) and reg.managed_list then
1192             local mgd=reg:managed_list()
1193             if #mgd > 0 then
1194                 s=s .. "\n" .. indent .. "---"
1195                 for k, v in pairs(mgd) do
1196                     s=s .. "\n" .. get_info(v, indent, d+1)
1197                 end
1198             end
1199         end
1200         
1201         return s
1202     end
1203     
1204     mod_query.message(mplex, get_info(reg, "", 0))
1205 end
1206
1207 -- }}}
1208
1209 -- Load extras
1210 dopath('mod_query_chdir')
1211
1212 -- Mark ourselves loaded.
1213 package.loaded["mod_query"]=true
1214
1215
1216 -- Load configuration file
1217 dopath('cfg_query', true)