2 -- ion/query/mod_query.lua -- Some common queries for Ion
4 -- Copyright (c) Tuomo Valkonen 2004-2005.
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.
13 -- This is a slight abuse of the _LOADED variable perhaps, but library-like
14 -- packages should handle checking if they're loaded instead of confusing
15 -- the user with require/include differences.
16 if _LOADED
["mod_query"] then return end
18 if not ioncore
.load_module("mod_query") then
22 local mod_query
=_G
["mod_query"]
27 -- Generic helper functions {{{
30 function mod_query
.make_completor(completefn
)
31 local function completor(wedln
, str
)
32 wedln
:set_completions(completefn(str
))
38 function mod_query
.query(mplex
, prompt
, initvalue
, handler
, completor
,
40 local function handle_it(str
)
43 -- Check that no other queries are open in the mplex.
44 local l
=mplex
:llist(2)
46 if obj_is(r
, "WEdln") then
50 wedln
=mod_query
.do_query(mplex
, prompt
, initvalue
, handle_it
, completor
)
52 wedln
:set_context(context
)
58 -- This function query will display a query with prompt \var{prompt} in
59 -- \var{mplex} and if the user answers affirmately, call \var{handler}
60 -- with \var{mplex} as parameter.
61 function mod_query
.query_yesno(mplex
, prompt
, handler
)
62 local function handler_yesno(mplex
, str
)
63 if str
=="y" or str
=="Y" or str
=="yes" then
67 return mod_query
.query(mplex
, prompt
, nil, handler_yesno
, nil,
72 function mod_query
.query_execfile(mplex
, prompt
, prog
)
74 local function handle_execwith(mplex
, str
)
75 ioncore
.exec_on(mplex
, prog
.." "..string.shell_safe(str
))
77 return mod_query
.query(mplex
, prompt
, mod_query
.get_initdir(mplex
),
78 handle_execwith
, mod_query
.file_completor
,
83 function mod_query
.query_execwith(mplex
, prompt
, dflt
, prog
, completor
,
85 local function handle_execwith(frame
, str
)
86 if not str
or str
=="" then
89 ioncore
.exec_on(mplex
, prog
.." "..string.shell_safe(str
))
91 return mod_query
.query(mplex
, prompt
, nil, handle_execwith
, completor
,
96 function mod_query
.lookup_script_warn(mplex
, script
)
97 local script
=ioncore
.lookup_script(script
)
99 mod_query
.warn(mplex
, TR("Could not find %s", script
))
105 function mod_query
.get_initdir(mplex
)
106 --if mod_query.last_dir then
107 -- return mod_query.last_dir
109 local wd
=(ioncore
.get_dir_for(mplex
) or os
.getenv("PWD"))
112 elseif string.sub(wd
, -1)~="/" then
120 function mod_query
.lookup_workspace_classes()
124 if type(v
)=="table" and v
.__typename
then
130 if v2
.__typename
=="WGenWS" then
131 table.insert(classes
, v
.__typename
)
143 function mod_query
.complete_from_list(list
, str
)
145 local len
=string.len(str
)
150 if string.sub(m
, 1, len
)==str
then
151 table.insert(results
, m
)
162 mod_query
.COLLECT_THRESHOLD
=2000
165 -- This function can be used to read completions from an external source.
166 -- The string \var{cmd} is a shell command to be executed. To its stdout,
167 -- the command should on the first line write the \var{common_part}
168 -- parameter of \fnref{WEdln.set_completions} and a single actual completion
169 -- on each of the successive lines.
170 function mod_query
.popen_completions(wedln
, cmd
, beg
)
172 local pst
={wedln
=wedln
, maybe_stalled
=0}
174 local function rcv(str
)
181 if pst
.maybe_stalled
>=2 then
187 totallen
=totallen
+string.len(str
)
188 if totallen
>ioncore
.RESULT_DATA_LIMIT
then
189 error(TR("Too much result data"))
192 data
=string.gsub(data
..str
, "([^\n]*)\n",
194 -- ion-completefile will return possible
195 -- common part of path on the first line
196 -- and the entries in that directory on the
198 if not results
.common_part
then
199 results
.common_part
=(beg
or "")..a
201 table.insert(results
, a
)
206 if lines
>mod_query
.COLLECT_THRESHOLD
then
211 str
=coroutine
.yield()
214 if not results
.common_part
then
215 results
.common_part
=beg
218 wedln
:set_completions(results
)
226 local found_clean
=false
229 if v
.wedln
==wedln
then
230 if v
.maybe_stalled
<2 then
231 v
.maybe_stalled
=v
.maybe_stalled
+1
237 if not found_clean
then
239 ioncore
.popen_bgread(cmd
, coroutine
.wrap(rcv
))
247 -- Simple queries for internal actions {{{
250 function mod_query
.complete_name(str
, list
)
252 local l
=string.len(str
)
253 for i
, reg
in list
do
255 if nm
and string.sub(nm
, 1, l
)==str
then
256 table.insert(entries
, nm
)
259 if table.getn(entries
)==0 then
260 for i
, reg
in list
do
262 if nm
and string.find(nm
, str
, 1, true) then
263 table.insert(entries
, nm
)
270 function mod_query
.complete_clientwin(str
)
271 return mod_query
.complete_name(str
, ioncore
.clientwin_list())
274 function mod_query
.complete_workspace(str
)
275 return mod_query
.complete_name(str
, ioncore
.region_list("WGenWS"))
278 function mod_query
.complete_region(str
)
279 return mod_query
.complete_name(str
, ioncore
.region_list())
283 function mod_query
.gotoclient_handler(frame
, str
)
284 local cwin
=ioncore
.lookup_clientwin(str
)
287 mod_query
.warn(frame
, TR("Could not find client window %s.", str
))
293 function mod_query
.attachclient_handler(frame
, str
)
294 local cwin
=ioncore
.lookup_clientwin(str
)
297 mod_query
.warn(frame
, TR("Could not find client window %s.", str
))
298 elseif frame
:rootwin_of()~=cwin
:rootwin_of() then
299 mod_query
.warn(frame
, TR("Cannot attach: different root windows."))
301 frame
:attach(cwin
, { switchto
= true })
306 function mod_query
.workspace_handler(mplex
, name
)
307 local ws
=ioncore
.lookup_region(name
, "WGenWS")
313 local classes
=mod_query
.lookup_workspace_classes()
315 local function completor(wedln
, what
)
316 local results
=mod_query
.complete_from_list(classes
, what
)
317 wedln
:set_completions(results
)
320 local function handler(mplex
, cls
)
321 local scr
=mplex
:screen_of()
323 mod_query
.warn(mplex
, TR("Unable to create workspace: no screen."))
327 if not cls
or cls
=="" then
328 cls
=ioncore
.get().default_ws_type
331 local err
=collect_errors(function()
339 mod_query
.warn(mplex
, err
or TR("Unknown error"))
343 local defcls
=ioncore
.get().default_ws_type
344 local prompt
=TR("Workspace type (%s):", defcls
or TR("none"))
346 mod_query
.query(mplex
, prompt
, "", handler
, completor
,
352 -- This query asks for the name of a client window and attaches
353 -- it to the frame the query was opened in. It uses the completion
354 -- function \fnref{ioncore.complete_clientwin}.
355 function mod_query
.query_gotoclient(mplex
)
356 mod_query
.query(mplex
, TR("Go to window:"), nil,
357 mod_query
.gotoclient_handler
,
358 mod_query
.make_completor(mod_query
.complete_clientwin
),
363 -- This query asks for the name of a client window and switches
364 -- focus to the one entered. It uses the completion function
365 -- \fnref{ioncore.complete_clientwin}.
366 function mod_query
.query_attachclient(mplex
)
367 mod_query
.query(mplex
, TR("Attach window:"), nil,
368 mod_query
.attachclient_handler
,
369 mod_query
.make_completor(mod_query
.complete_clientwin
),
375 -- This query asks for the name of a workspace. If a workspace
376 -- (an object inheriting \type{WGenWS}) with such a name exists,
377 -- it will be switched to. Otherwise a new workspace with the
378 -- entered name will be created and the user will be queried for
379 -- the type of the workspace.
380 function mod_query
.query_workspace(mplex
)
381 mod_query
.query(mplex
, TR("Go to or create workspace:"), nil,
382 mod_query
.workspace_handler
,
383 mod_query
.make_completor(mod_query
.complete_workspace
),
389 -- This query asks whether the user wants to exit Ion (no session manager)
390 -- or close the session (running under a session manager that supports such
391 -- requests). If the answer is 'y', 'Y' or 'yes', so will happen.
392 function mod_query
.query_shutdown(mplex
)
393 mod_query
.query_yesno(mplex
, TR("Exit Ion/Shutdown session (y/n)?"),
399 -- This query asks whether the user wants restart Ioncore.
400 -- If the answer is 'y', 'Y' or 'yes', so will happen.
401 function mod_query
.query_restart(mplex
)
402 mod_query
.query_yesno(mplex
, TR("Restart Ion (y/n)?"), ioncore
.restart
)
407 -- This function asks for a name new for the frame where the query
409 function mod_query
.query_renameframe(frame
)
410 mod_query
.query(frame
, TR("Frame name:"), frame
:name(),
411 function(frame
, str
) frame
:set_name(str
) end,
417 -- This function asks for a name new for the workspace on which the
419 function mod_query
.query_renameworkspace(mplex
)
420 local ws
=ioncore
.find_manager(mplex
, "WGenWS")
421 mod_query
.query(mplex
, TR("Workspace name:"), ws
:name(),
422 function(mplex
, str
) ws
:set_name(str
) end,
433 function mod_query
.file_completor(wedln
, str
, wp
, beg
)
434 local ic
=ioncore
.lookup_script("ion-completefile")
436 mod_query
.popen_completions(wedln
,
437 ic
..(wp
or " ")..string.shell_safe(str
),
444 -- Asks for a file to be edited. It uses the script \file{ion-edit} to
445 -- start a program to edit the file. This script uses \file{run-mailcap}
446 -- by default, but if you don't have it, you may customise the script.
447 function mod_query
.query_editfile(mplex
)
448 local script
=mod_query
.lookup_script_warn(mplex
, "ion-edit")
449 mod_query
.query_execfile(mplex
, TR("Edit file:"), script
)
454 -- Asks for a file to be viewed. It uses the script \file{ion-view} to
455 -- start a program to view the file. This script uses \file{run-mailcap}
456 -- by default, but if you don't have it, you may customise the script.
457 function mod_query
.query_runfile(mplex
)
458 local script
=mod_query
.lookup_script_warn(mplex
, "ion-view")
459 mod_query
.query_execfile(mplex
, TR("View file:"), script
)
463 function mod_query
.exec_completor(wedln
, str
)
464 local st
, en
, beg
, tocompl
=string.find(str
, "^(.-)([^%s]*)$")
465 if string.len(beg
)==0 then
466 mod_query
.file_completor(wedln
, tocompl
, " -wp ")
468 mod_query
.file_completor(wedln
, tocompl
, " ", beg
)
473 local cmd_overrides
={}
476 -- Define a command override for the \fnrefx{mod_query}{query_exec} query.
477 function mod_query
.defcmd(cmd
, fn
)
478 cmd_overrides
[cmd
]=fn
481 local function trim(s
)
482 local _
, _
, sn
=string.find(s
, "^[%s]*(.-)[%s]*$")
486 function mod_query
.exec_handler(frame
, cmdline
)
487 local _
, _
, cmd
, params
=string.find(cmdline
, "^([^%s]+)(.*)")
488 if cmd
and cmd_overrides
[cmd
] then
489 cmd_overrides
[cmd
](frame
, trim(params
))
491 if string.sub(cmdline
, 1, 1)==":" then
492 local ix
=mod_query
.lookup_script_warn(frame
, "ion-runinxterm")
493 if not ix
then return end
494 cmdline
=ix
.." "..string.sub(cmdline
, 2)
496 ioncore
.exec_on(frame
, cmdline
)
501 -- This function asks for a command to execute with \file{/bin/sh}.
502 -- If the command is prefixed with a colon (':'), the command will
503 -- be run in an XTerm (or other terminal emulator) using the script
504 -- \file{ion-runinxterm}.
505 function mod_query
.query_exec(mplex
)
506 mod_query
.query(mplex
, TR("Run:"), nil, mod_query
.exec_handler
,
507 mod_query
.exec_completor
,
518 mod_query
.known_hosts
={}
521 function mod_query
.get_known_hosts(mplex
)
522 mod_query
.known_hosts
={}
524 local h
=os
.getenv("HOME")
526 f
=io
.open(h
.."/.ssh/known_hosts")
529 warn(TR("Failed to open ~/.ssh/known_hosts"))
532 for l
in f
:lines() do
533 local st
, en
, hostname
=string.find(l
, "^([^%s,]+)")
535 table.insert(mod_query
.known_hosts
, hostname
)
542 function mod_query
.complete_ssh(str
)
543 local st
, en
, user
, at
, host
=string.find(str
, "^([^@]*)(@?)(.*)$")
545 if string.len(at
)==0 and string.len(host
)==0 then
546 host
= user
; user
= ""
555 if string.len(host
)==0 then
556 if string.len(user
)==0 then
557 return mod_query
.known_hosts
560 for _
, v
in ipairs(mod_query
.known_hosts
) do
561 table.insert(res
, user
.. v
)
566 for _
, v
in ipairs(mod_query
.known_hosts
) do
567 local s
, e
=string.find(v
, host
, 1, true)
568 if s
==1 and e
>=1 then
569 table.insert(res
, user
.. v
)
578 -- This query asks for a host to connect to with SSH. It starts
579 -- up ssh in a terminal using \file{ion-ssh}. Hosts to tab-complete
580 -- are read from \file{\~{}/.ssh/known\_hosts}.
581 function mod_query
.query_ssh(mplex
)
582 mod_query
.get_known_hosts(mplex
)
583 local script
=mod_query
.lookup_script_warn(mplex
, "ion-ssh")
584 mod_query
.query_execwith(mplex
, TR("SSH to:"), nil, script
,
585 mod_query
.make_completor(mod_query
.complete_ssh
),
596 function mod_query
.man_completor(wedln
, str
)
597 local mc
=ioncore
.lookup_script("ion-completeman")
599 mod_query
.popen_completions(wedln
, mc
.." -complete "..
600 string.shell_safe(str
))
606 -- This query asks for a manual page to display. It uses the command
607 -- \file{ion-man} to run \file{man} in a terminal emulator. By customizing
608 -- this script it is possible use some other man page viewer. The script
609 -- \file{ion-completeman} is used to complete manual pages.
610 function mod_query
.query_man(mplex
)
611 local script
=mod_query
.lookup_script_warn(mplex
, "ion-man")
612 local prgm
=ioncore
.progname()
613 local prompt
=TR("Manual page (%s):", prgm
)
614 mod_query
.query_execwith(mplex
, prompt
, prgm
, script
,
615 mod_query
.man_completor
, "man")
622 -- Lua code execution {{{
625 function mod_query
.create_run_env(mplex
)
626 local origenv
=getfenv()
627 local meta
={__index
=origenv
, __newindex
=origenv
}
630 _sub
=mplex
:current(),
632 setmetatable(env
, meta
)
636 function mod_query
.do_handle_lua(mplex
, env
, code
)
637 local f
, err
=loadstring(code
)
639 mod_query
.warn(mplex
, err
)
643 err
=collect_errors(f
)
645 mod_query
.warn(mplex
, err
)
649 local function getindex(t
)
650 local mt
=getmetatable(t
)
651 if mt
then return mt
.__index
end
655 function mod_query
.do_complete_lua(env
, str
)
656 -- Get the variable to complete, including containing tables.
657 -- This will also match string concatenations and such because
658 -- Lua's regexps don't support optional subexpressions, but we
659 -- handle them in the next step.
662 local _
, _
, tocomp
=string.find(str
, "([%w_.:]*)$")
664 -- Descend into tables
665 if tocomp
and string.len(tocomp
)>=1 then
666 for t
in string.gfind(tocomp
, "([^.:]*)[.:]") do
668 if string.len(t
)==0 then
671 if type(comptab
[t
])=="table" then
673 elseif type(comptab
[t
])=="userdata" then
674 comptab
=getindex(comptab
[t
])
683 if not comptab
then return {} end
687 -- Get the actual variable to complete without containing tables
688 _
, _
, compl
.common_part
, tocomp
=string.find(str
, "(.-)([%w_]*)$")
690 local l
=string.len(tocomp
)
696 if type(k
)=="string" then
697 if string.sub(k
, 1, l
)==tocomp
then
698 table.insert(compl
, k
)
703 -- We only want to display full list of functions for objects, not
704 -- the tables representing the classes.
705 --if not metas then break end
709 if not tab
or seen
[tab
] then break end
712 -- If there was only one completion and it is a string or function,
713 -- concatenate it with "." or "(", respectively.
714 if table.getn(compl
)==1 then
715 if type(comptab
[compl
[1]]
)=="table" then
716 compl
[1]=compl
[1] .. "."
717 elseif type(comptab
[compl
[1]]
)=="function" then
718 compl
[1]=compl
[1] .. "("
727 -- This query asks for Lua code to execute. It sets the variable '\var{\_}'
728 -- in the local environment of the string to point to the mplex where the
729 -- query was created. It also sets the table \var{arg} in the local
730 -- environment to \code{\{_, _:current()\}}.
731 function mod_query
.query_lua(mplex
)
732 local env
=mod_query
.create_run_env(mplex
)
734 local function complete(wedln
, code
)
735 wedln
:set_completions(mod_query
.do_complete_lua(env
, code
))
738 local function handler(mplex
, code
)
739 return mod_query
.do_handle_lua(mplex
, env
, code
)
742 mod_query
.query(mplex
, TR("Lua code: "), nil, handler
, complete
, "lua")
751 -- This query can be used to create a query of a defined menu.
752 function mod_query
.query_menu(mplex
, prompt
, menuname
)
753 local menu
=mod_menu
.getmenu(menuname
)
756 mod_query
.warn(mplex
, TR("Unknown menu %s.", tostring(menuname
)))
759 function complete(str
)
761 local len
=string.len(str
)
764 if len
==0 or string.sub(mn
, 1, len
)==str
then
765 table.insert(results
, mn
)
771 local function handle(mplex
, str
)
781 local err
=collect_errors(function() e
.func(mplex
) end)
783 mod_query
.warn(mplex
, err
)
785 elseif e
.submenu_fn
then
786 mod_query
.query_menu(mplex
, TR("%s menu:", e
.name
),
790 mod_query
.warn(mplex
, TR("No entry '%s'", str
))
794 mod_query
.query(mplex
, prompt
, nil, handle
,
795 mod_query
.make_completor(complete
), "menu")
805 -- Display an "About Ion" message in \var{mplex}.
806 function mod_query
.show_about_ion(mplex
)
807 mod_query
.message(mplex
, ioncore
.aboutmsg())
812 -- Show information about a client window.
813 function mod_query
.show_clientwin(mplex
, cwin
)
814 local function indent(s
)
816 return i
..string.gsub(s
, "\n", "\n"..i
)
819 local function get_info(cwin
)
820 local function n(s
) return (s
or "") end
821 local i
=cwin
:get_ident()
822 local s
=TR("Title: %s\nClass: %s\nRole: %s\nInstance: %s\nXID: 0x%x",
823 n(cwin
:name()), n(i
.class
), n(i
.role
), n(i
.instance
),
825 local t
=TR("\nTransients:\n")
826 for k
, v
in cwin
:managed_list() do
827 if obj_is(v
, "WClientWin") then
828 s
=s
..t
..indent(get_info(v
))
835 mod_query
.message(mplex
, get_info(cwin
))
841 dopath('mod_query_chdir')
843 -- Mark ourselves loaded.
844 _LOADED
["mod_query"]=true
847 -- Load configuration file
848 dopath('cfg_query', true)