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