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