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