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