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