]> git.sesse.net Git - vlc/blob - share/lua/intf/cli.lua
luaRC: provide deinterlace control
[vlc] / share / lua / intf / cli.lua
1 --[==========================================================================[
2  cli.lua: CLI module for VLC
3 --[==========================================================================[
4  Copyright (C) 2007-2011 the VideoLAN team
5  $Id$
6
7  Authors: Antoine Cellerier <dionoea at videolan dot org>
8           Pierre Ynard
9
10  This program is free software; you can redistribute it and/or modify
11  it under the terms of the GNU General Public License as published by
12  the Free Software Foundation; either version 2 of the License, or
13  (at your option) any later version.
14
15  This program is distributed in the hope that it will be useful,
16  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  GNU General Public License for more details.
19
20  You should have received a copy of the GNU General Public License
21  along with this program; if not, write to the Free Software
22  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
23 --]==========================================================================]
24
25 description=
26 [============================================================================[
27  Command Line Interface for VLC
28
29  This is a modules/control/rc.c look alike (with a bunch of new features).
30  It also provides a VLM interface copied from the telnet interface.
31
32  Use on local term:
33     vlc -I cli
34  Use on tcp connection:
35     vlc -I cli --lua-config "cli={host='localhost:4212'}"
36  Use on telnet connection:
37     vlc -I cli --lua-config "cli={host='telnet://localhost:4212'}"
38  Use on multiple hosts (term + plain tcp port + telnet):
39     vlc -I cli --lua-config "cli={hosts={'*console','localhost:4212','telnet://localhost:5678'}}"
40
41  Note:
42     -I cli and -I luacli are aliases for -I lua --lua-intf cli
43
44  Configuration options setable throught the --lua-config option are:
45     * hosts: A list of hosts to listen on.
46     * host: A host to listen on. (won't be used if `hosts' is set)
47     * password: The password used for telnet clients.
48  The following can be set using the --lua-config option or in the interface
49  itself using the `set' command:
50     * prompt: The prompt.
51     * welcome: The welcome message.
52     * width: The default terminal width (used to format text).
53     * autocompletion: When issuing an unknown command, print a list of
54                       possible commands to autocomplete with. (0 to disable,
55                       1 to enable).
56     * autoalias: If autocompletion returns only one possibility, use it
57                  (0 to disable, 1 to enable).
58     * flatplaylist: 0 to disable, 1 to enable.
59 ]============================================================================]
60
61 require("common")
62 skip = common.skip
63 skip2 = function(foo) return skip(skip(foo)) end
64 setarg = common.setarg
65 strip = common.strip
66
67 _ = vlc.gettext._
68 N_ = vlc.gettext.N_
69
70 --[[ Setup default environement ]]
71 env = { prompt = "> ";
72         width = 70;
73         autocompletion = 1;
74         autoalias = 1;
75         welcome = _("Command Line Interface initialized. Type `help' for help.");
76         flatplaylist = 0;
77       }
78
79 --[[ Import custom environement variables from the command line config (if possible) ]]
80 for k,v in pairs(env) do
81     if config[k] then
82         if type(env[k]) == type(config[k]) then
83             env[k] = config[k]
84             vlc.msg.dbg("set environement variable `"..k.."' to "..tostring(env[k]))
85         else
86             vlc.msg.err("environement variable `"..k.."' should be of type "..type(env[k])..". config value will be discarded.")
87         end
88     end
89 end
90
91 --[[ Command functions ]]
92 function set_env(name,client,value)
93     if value then
94         local var,val = split_input(value)
95         if val then
96             local s = string.gsub(val,"\"(.*)\"","%1")
97             if type(client.env[var])==type(1) then
98                 client.env[var] = tonumber(s)
99             else
100                 client.env[var] = s
101             end
102         else
103             client:append( tostring(client.env[var]) )
104         end
105     else
106         for e,v in common.pairs_sorted(client.env) do
107             client:append(e.."="..v)
108         end
109     end
110 end
111
112 function save_env(name,client,value)
113     env = common.table_copy(client.env)
114 end
115
116 function alias(client,value)
117     if value then
118         local var,val = split_input(value)
119         if commands[var] and type(commands[var]) ~= type("") then
120             client:append("Error: cannot use a primary command as an alias name")
121         else
122             if commands[val] then
123                 commands[var]=val
124             else
125                 client:append("Error: unknown primary command `"..val.."'.")
126             end
127         end
128     else
129         for c,v in common.pairs_sorted(commands) do
130             if type(v)==type("") then
131                 client:append(c.."="..v)
132             end
133         end
134     end
135 end
136
137 function lock(name,client)
138     if client.type == host.client_type.telnet then
139         client:switch_status( host.status.password )
140         client.buffer = ""
141     else
142         client:append("Error: the prompt can only be locked when logged in through telnet")
143     end
144 end
145
146 function logout(name,client)
147     if client.type == host.client_type.net
148     or client.type == host.client_type.telnet then
149         client:send("Bye-bye!\r\n")
150         client:del()
151     else
152         client:append("Error: Can't logout of stdin/stdout. Use quit or shutdown to close VLC.")
153     end
154 end
155
156 function shutdown(name,client)
157     client:append("Bye-bye!")
158     h:broadcast("Shutting down.\r\n")
159     vlc.msg.info("Requested shutdown.")
160     vlc.misc.quit()
161 end
162
163 function quit(name,client)
164     if client.type == host.client_type.net
165     or client.type == host.client_type.telnet then
166         logout(name,client)
167     else
168         shutdown(name,client)
169     end
170 end
171
172 function add(name,client,arg)
173     -- TODO: parse single and double quotes properly
174     local f
175     if name == "enqueue" then
176         f = vlc.playlist.enqueue
177     else
178         f = vlc.playlist.add
179     end
180     local options = {}
181     for o in string.gmatch(arg," +:([^ ]*)") do
182         table.insert(options,o)
183     end
184     arg = string.gsub(arg," +:.*$","")
185     local uri = vlc.strings.make_uri(arg)
186     f({{path=uri,options=options}})
187 end
188
189 function playlist_is_tree( client )
190     if client.env.flatplaylist == 0 then
191         return true
192     else
193         return false
194     end
195 end
196
197 function playlist(name,client,arg)
198     function playlist0(item,prefix)
199         local prefix = prefix or ""
200         if not item.flags.disabled then
201             local str = "| "..prefix..tostring(item.id).." - "..item.name
202             if item.duration > 0 then
203                 str = str.." ("..common.durationtostring(item.duration)..")"
204             end
205             if item.nb_played > 0 then
206                 str = str.." [played "..tostring(item.nb_played).." time"
207                 if item.nb_played > 1 then
208                     str = str .. "s"
209                 end
210                 str = str .. "]"
211             end
212             client:append(str)
213         end
214         if item.children then
215             for _, c in ipairs(item.children) do
216                 playlist0(c,prefix.."  ")
217             end
218         end
219     end
220     local playlist
221     local tree = playlist_is_tree(client)
222     if name == "search" then
223         playlist = vlc.playlist.search(arg or "", tree)
224     else
225         if tonumber(arg) then
226             playlist = vlc.playlist.get(tonumber(arg), tree)
227         elseif arg then
228             playlist = vlc.playlist.get(arg, tree)
229         else
230             playlist = vlc.playlist.get(nil, tree)
231         end
232     end
233     if name == "search" then
234         client:append("+----[ Search - "..(arg or "`reset'").." ]")
235     else
236         client:append("+----[ Playlist - "..playlist.name.." ]")
237     end
238     if playlist.children then
239         for _, item in ipairs(playlist.children) do
240             playlist0(item)
241         end
242     else
243         playlist0(playlist)
244     end
245     if name == "search" then
246         client:append("+----[ End of search - Use `search' to reset ]")
247     else
248         client:append("+----[ End of playlist ]")
249     end
250 end
251
252 function playlist_sort(name,client,arg)
253     if not arg then
254         client:append("Valid sort keys are: id, title, artist, genre, random, duration, album.")
255     else
256         local tree = playlist_is_tree(client)
257         vlc.playlist.sort(arg,false,tree)
258     end
259 end
260
261 function services_discovery(name,client,arg)
262     if arg then
263         if vlc.sd.is_loaded(arg) then
264             vlc.sd.remove(arg)
265             client:append(arg.." disabled.")
266         else
267             vlc.sd.add(arg)
268             client:append(arg.." enabled.")
269         end
270     else
271         local sd = vlc.sd.get_services_names()
272         client:append("+----[ Services discovery ]")
273         for n,ln in pairs(sd) do
274             local status
275             if vlc.sd.is_loaded(n) then
276                 status = "enabled"
277             else
278                 status = "disabled"
279             end
280             client:append("| "..n..": " .. ln .. " (" .. status .. ")")
281         end
282         client:append("+----[ End of services discovery ]")
283     end
284 end
285
286 function print_text(label,text)
287     return function(name,client)
288         client:append("+----[ "..label.." ]")
289         client:append "|"
290         for line in string.gmatch(text,".-\r?\n") do
291             client:append("| "..string.gsub(line,"\r?\n",""))
292         end
293         client:append "|"
294         client:append("+----[ End of "..string.lower(label).." ]")
295     end
296 end
297
298 function help(name,client,arg)
299     if arg == nil then
300         client:append("+----[ VLM commands ]")
301         local message, vlc_err = vlm:execute_command("help")
302         vlm_message_to_string( client, message, "|" )
303     end
304
305     local width = client.env.width
306     local long = (name == "longhelp")
307     local extra = ""
308     if arg then extra = "matching `" .. arg .. "' " end
309     client:append("+----[ CLI commands "..extra.."]")
310     for i, cmd in ipairs(commands_ordered) do
311         if (cmd == "" or not commands[cmd].adv or long)
312         and (not arg or string.match(cmd,arg)) then
313             local str = "| " .. cmd
314             if cmd ~= "" then
315                 local val = commands[cmd]
316                 if val.aliases then
317                     for _,a in ipairs(val.aliases) do
318                         str = str .. ", " .. a
319                     end
320                 end
321                 if val.args then str = str .. " " .. val.args end
322                 if #str%2 == 1 then str = str .. " " end
323                 str = str .. string.rep(" .",(width-(#str+#val.help)-1)/2)
324                 str = str .. string.rep(" ",width-#str-#val.help) .. val.help
325             end
326             client:append(str)
327         end
328     end
329     client:append("+----[ end of help ]")
330 end
331
332 function input_info(name,client)
333     local item = vlc.input.item()
334     if(item == nil) then return end
335     local categories = item:info()
336     for cat, infos in pairs(categories) do
337         client:append("+----[ "..cat.." ]")
338         client:append("|")
339         for name, value in pairs(infos) do
340             client:append("| "..name..": "..value)
341         end
342         client:append("|")
343     end
344     client:append("+----[ end of stream info ]")
345 end
346
347 function stats(name,client)
348     local item = vlc.input.item()
349     if(item == nil) then return end
350     local stats_tab = item:stats()
351
352     client:append("+----[ begin of statistical info")
353     client:append("+-[Incoming]")
354     client:append("| input bytes read : "..string.format("%8.0f KiB",stats_tab["read_bytes"]/1024))
355     client:append("| input bitrate    :   "..string.format("%6.0f kb/s",stats_tab["input_bitrate"]*8000))
356     client:append("| demux bytes read : "..string.format("%8.0f KiB",stats_tab["demux_read_bytes"]/1024))
357     client:append("| demux bitrate    :   "..string.format("%6.0f kb/s",stats_tab["demux_bitrate"]*8000))
358     client:append("| demux corrupted  :    "..string.format("%5i",stats_tab["demux_corrupted"]))
359     client:append("| discontinuities  :    "..string.format("%5i",stats_tab["demux_discontinuity"]))
360     client:append("|")
361     client:append("+-[Video Decoding]")
362     client:append("| video decoded    :    "..string.format("%5i",stats_tab["decoded_video"]))
363     client:append("| frames displayed :    "..string.format("%5i",stats_tab["displayed_pictures"]))
364     client:append("| frames lost      :    "..string.format("%5i",stats_tab["lost_pictures"]))
365     client:append("|")
366     client:append("+-[Audio Decoding]")
367     client:append("| audio decoded    :    "..string.format("%5i",stats_tab["decoded_audio"]))
368     client:append("| buffers played   :    "..string.format("%5i",stats_tab["played_abuffers"]))
369     client:append("| buffers lost     :    "..string.format("%5i",stats_tab["lost_abuffers"]))
370     client:append("|")
371     client:append("+-[Streaming]")
372     client:append("| packets sent     :    "..string.format("%5i",stats_tab["sent_packets"]))
373     client:append("| bytes sent       : "..string.format("%8.0f KiB",stats_tab["sent_bytes"]/1024))
374     client:append("| sending bitrate  :   "..string.format("%6.0f kb/s",stats_tab["send_bitrate"]*8000))
375     client:append("+----[ end of statistical info ]")
376 end
377
378 function playlist_status(name,client)
379     local item = vlc.input.item()
380     if(item ~= nil) then
381         client:append( "( new input: " .. vlc.strings.decode_uri(item:uri()) .. " )" )
382     end
383     client:append( "( audio volume: " .. tostring(vlc.volume.get()) .. " )")
384     client:append( "( state " .. vlc.playlist.status() .. " )")
385 end
386
387 function is_playing(name,client)
388     if vlc.input.is_playing() then client:append "1" else client:append "0" end
389 end
390
391 function get_title(name,client)
392     local item = vlc.input.item()
393     if item then
394         client:append(item:name())
395     else
396         client:append("")
397     end
398 end
399
400 function ret_print(foo,start,stop)
401     local start = start or ""
402     local stop = stop or ""
403     return function(discard,client,...) client:append(start..tostring(foo(...))..stop) end
404 end
405
406 function get_time(var)
407     return function(name,client)
408         local input = vlc.object.input()
409         client:append(math.floor(vlc.var.get( input, var )))
410     end
411 end
412
413 function titlechap(name,client,value)
414     local input = vlc.object.input()
415     local var = string.gsub( name, "_.*$", "" )
416     if value then
417         vlc.var.set( input, var, value )
418     else
419         local item = vlc.var.get( input, var )
420         -- Todo: add item name conversion
421         client:append(item)
422     end
423 end
424
425 function titlechap_offset(var,offset)
426     local input = vlc.object.input()
427     vlc.var.set( input, var, vlc.var.get( input, var ) + offset )
428 end
429
430 function title_next(name,client,value)
431     titlechap_offset('title', 1)
432 end
433
434 function title_previous(name,client,value)
435     titlechap_offset('title', -1)
436 end
437
438 function chapter_next(name,client,value)
439     titlechap_offset('chapter', 1)
440 end
441
442 function chapter_previous(name,client,value)
443     titlechap_offset('chapter', -1)
444 end
445
446 function seek(name,client,value)
447     common.seek(value)
448 end
449
450 function volume(name,client,value)
451     if value then
452         common.volume(value)
453     else
454         client:append(tostring(vlc.volume.get()))
455     end
456 end
457
458 function rate(name,client,value)
459     local input = vlc.object.input()
460     if name == "rate" then
461         vlc.var.set(input, "rate", tonumber(value))
462     elseif name == "normal" then
463         vlc.var.set(input,"rate",1)
464     else
465         vlc.var.set(input,"rate-"..name,nil)
466     end
467 end
468
469 function frame(name,client)
470     vlc.var.trigger_callback(vlc.object.input(),"frame-next");
471 end
472
473 function listvalue(obj,var)
474     return function(client,value)
475         local o = vlc.object.find(nil,obj,"anywhere")
476         if not o then return end
477         if value then
478             vlc.var.set( o, var, value )
479         else
480             local c = vlc.var.get( o, var )
481             local v, l = vlc.var.get_list( o, var )
482             client:append("+----[ "..var.." ]")
483             for i,val in ipairs(v) do
484                 local mark = (val==c)and " *" or ""
485                 client:append("| "..tostring(val).." - "..tostring(l[i])..mark)
486             end
487             client:append("+----[ end of "..var.." ]")
488         end
489     end
490 end
491
492 function menu(name,client,value)
493     local map = { on='show', off='hide', up='up', down='down', left='prev', right='next', ['select']='activate' }
494     if map[value] and vlc.osd.menu[map[value]] then
495         vlc.osd.menu[map[value]]()
496     else
497         client:append("Unknown menu command '"..tostring(value).."'")
498     end
499 end
500
501 function hotkey(name, client, value)
502     if not value then
503         client:append("Please specify a hotkey (ie key-quit or quit)")
504     elseif not common.hotkey(value) and not common.hotkey("key-"..value) then
505         client:append("Unknown hotkey '"..value.."'")
506     end
507 end
508
509 --[[ Declare commands, register their callback functions and provide
510      help strings here.
511      Syntax is:
512      "<command name>"; { func = <function>; [ args = "<str>"; ] help = "<str>"; [ adv = <bool>; ] [ aliases = { ["<str>";]* }; ] }
513      ]]
514 commands_ordered = {
515     { "add"; { func = add; args = "XYZ"; help = "add XYZ to playlist" } };
516     { "enqueue"; { func = add; args = "XYZ"; help = "queue XYZ to playlist" } };
517     { "playlist"; { func = playlist; help = "show items currently in playlist" } };
518     { "search"; { func = playlist; args = "[string]"; help = "search for items in playlist (or reset search)" } };
519     { "sort"; { func = playlist_sort; args = "key"; help = "sort the playlist" } };
520     { "sd"; { func = services_discovery; args = "[sd]"; help = "show services discovery or toggle" } };
521     { "play"; { func = skip2(vlc.playlist.play); help = "play stream" } };
522     { "stop"; { func = skip2(vlc.playlist.stop); help = "stop stream" } };
523     { "next"; { func = skip2(vlc.playlist.next); help = "next playlist item" } };
524     { "prev"; { func = skip2(vlc.playlist.prev); help = "previous playlist item" } };
525     { "goto"; { func = skip2(vlc.playlist.goto); help = "goto item at index" } };
526     { "repeat"; { func = skip2(vlc.playlist.repeat_); args = "[on|off]"; help = "toggle playlist repeat" } };
527     { "loop"; { func = skip2(vlc.playlist.loop); args = "[on|off]"; help = "toggle playlist loop" } };
528     { "random"; { func = skip2(vlc.playlist.random); args = "[on|off]"; help = "toggle playlist random" } };
529     { "clear"; { func = skip2(vlc.playlist.clear); help = "clear the playlist" } };
530     { "status"; { func = playlist_status; help = "current playlist status" } };
531     { "title"; { func = titlechap; args = "[X]"; help = "set/get title in current item" } };
532     { "title_n"; { func = title_next; help = "next title in current item" } };
533     { "title_p"; { func = title_previous; help = "previous title in current item" } };
534     { "chapter"; { func = titlechap; args = "[X]"; help = "set/get chapter in current item" } };
535     { "chapter_n"; { func = chapter_next; help = "next chapter in current item" } };
536     { "chapter_p"; { func = chapter_previous; help = "previous chapter in current item" } };
537     { "" };
538     { "seek"; { func = seek; args = "X"; help = "seek in seconds, for instance `seek 12'" } };
539     { "pause"; { func = skip2(vlc.playlist.pause); help = "toggle pause" } };
540     { "fastforward"; { func = setarg(common.hotkey,"key-jump+extrashort"); help = "set to maximum rate" } };
541     { "rewind"; { func = setarg(common.hotkey,"key-jump-extrashort"); help = "set to minimum rate" } };
542     { "faster"; { func = rate; help = "faster playing of stream" } };
543     { "slower"; { func = rate; help = "slower playing of stream" } };
544     { "normal"; { func = rate; help = "normal playing of stream" } };
545     { "rate"; { func = rate; args = "[playback rate]"; help = "set playback rate to value" } };
546     { "frame"; { func = frame; help = "play frame by frame" } };
547     { "fullscreen"; { func = skip2(vlc.video.fullscreen); args = "[on|off]"; help = "toggle fullscreen"; aliases = { "f", "F" } } };
548     { "info"; { func = input_info; help = "information about the current stream" } };
549     { "stats"; { func = stats; help = "show statistical information" } };
550     { "get_time"; { func = get_time("time"); help = "seconds elapsed since stream's beginning" } };
551     { "is_playing"; { func = is_playing; help = "1 if a stream plays, 0 otherwise" } };
552     { "get_title"; { func = get_title; help = "the title of the current stream" } };
553     { "get_length"; { func = get_time("length"); help = "the length of the current stream" } };
554     { "" };
555     { "volume"; { func = volume; args = "[X]"; help = "set/get audio volume" } };
556     { "volup"; { func = ret_print(vlc.volume.up,"( audio volume: "," )"); args = "[X]"; help = "raise audio volume X steps" } };
557     { "voldown"; { func = ret_print(vlc.volume.down,"( audio volume: "," )"); args = "[X]"; help = "lower audio volume X steps" } };
558     { "adev"; { func = skip(listvalue("aout","audio-device")); args = "[X]"; help = "set/get audio device" } };
559     { "achan"; { func = skip(listvalue("aout","audio-channels")); args = "[X]"; help = "set/get audio channels" } };
560     { "atrack"; { func = skip(listvalue("input","audio-es")); args = "[X]"; help = "set/get audio track" } };
561     { "vtrack"; { func = skip(listvalue("input","video-es")); args = "[X]"; help = "set/get video track" } };
562     { "vratio"; { func = skip(listvalue("vout","aspect-ratio")); args = "[X]"; help = "set/get video aspect ratio" } };
563     { "vcrop"; { func = skip(listvalue("vout","crop")); args = "[X]"; help = "set/get video crop"; aliases = { "crop" } } };
564     { "vzoom"; { func = skip(listvalue("vout","zoom")); args = "[X]"; help = "set/get video zoom"; aliases = { "zoom" } } };
565     { "vdeinterlace"; { func = skip(listvalue("vout","deinterlace")); args = "[X]"; help = "set/get video deintelace" } };
566     { "vdeinterlace_mode"; { func = skip(listvalue("vout","deinterlace-mode")); args = "[X]"; help = "set/get video deintelace mode" } };
567     { "snapshot"; { func = common.snapshot; help = "take video snapshot" } };
568     { "strack"; { func = skip(listvalue("input","spu-es")); args = "[X]"; help = "set/get subtitles track" } };
569     { "hotkey"; { func = hotkey; args = "[hotkey name]"; help = "simulate hotkey press"; adv = true; aliases = { "key" } } };
570     { "menu"; { func = menu; args = "[on|off|up|down|left|right|select]"; help = "use menu"; adv = true } };
571     { "" };
572     { "set"; { func = set_env; args = "[var [value]]"; help = "set/get env var"; adv = true } };
573     { "save_env"; { func = save_env; help = "save env vars (for future clients)"; adv = true } };
574     { "alias"; { func = skip(alias); args = "[cmd]"; help = "set/get command aliases"; adv = true } };
575     { "description"; { func = print_text("Description",description); help = "describe this module" } };
576     { "license"; { func = print_text("License message",vlc.misc.license()); help = "print VLC's license message"; adv = true } };
577     { "help"; { func = help; args = "[pattern]"; help = "a help message"; aliases = { "?" } } };
578     { "longhelp"; { func = help; args = "[pattern]"; help = "a longer help message" } };
579     { "lock"; { func = lock; help = "lock the telnet prompt" } };
580     { "logout"; { func = logout; help = "exit (if in a socket connection)" } };
581     { "quit"; { func = quit; help = "quit VLC (or logout if in a socket connection)" } };
582     { "shutdown"; { func = shutdown; help = "shutdown VLC" } };
583     }
584
585 commands = {}
586 for i, cmd in ipairs( commands_ordered ) do
587     if #cmd == 2 then
588         commands[cmd[1]]=cmd[2]
589         if cmd[2].aliases then
590             for _,a in ipairs(cmd[2].aliases) do
591                 commands[a]=cmd[1]
592             end
593         end
594     end
595     commands_ordered[i]=cmd[1]
596 end
597 --[[ From now on commands_ordered is a list of the different command names
598      and commands is a associative array indexed by the command name. ]]
599
600 -- Compute the column width used when printing a the autocompletion list
601 env.colwidth = 0
602 for c,_ in pairs(commands) do
603     if #c > env.colwidth then env.colwidth = #c end
604 end
605 env.coldwidth = env.colwidth + 1
606
607 --[[ Utils ]]
608 function split_input(input)
609     local input = strip(input)
610     local s = string.find(input," ")
611     if s then
612         return string.sub(input,0,s-1), strip(string.sub(input,s))
613     else
614         return input
615     end
616 end
617
618 function vlm_message_to_string(client,message,prefix)
619     local prefix = prefix or ""
620     if message.value then
621         client:append(prefix .. message.name .. " : " .. message.value)
622     else
623         client:append(prefix .. message.name)
624     end
625     if message.children then
626         for i,c in ipairs(message.children) do
627             vlm_message_to_string(client,c,prefix.."    ")
628         end
629     end
630 end
631
632 --[[ Command dispatch ]]
633 function call_command(cmd,client,arg)
634     if type(commands[cmd]) == type("") then
635         cmd = commands[cmd]
636     end
637     local ok, msg
638     if arg ~= nil then
639         ok, msg = pcall( commands[cmd].func, cmd, client, arg )
640     else
641         ok, msg = pcall( commands[cmd].func, cmd, client )
642     end
643     if not ok then
644         local a = arg and " "..arg or ""
645         client:append("Error in `"..cmd..a.."' ".. msg)
646     end
647 end
648
649 function call_vlm_command(cmd,client,arg)
650     if arg ~= nil then
651         cmd = cmd.." "..arg
652     end
653     local message, vlc_err = vlm:execute_command( cmd )
654     vlm_message_to_string( client, message )
655     return vlc_err
656 end
657
658 function call_libvlc_command(cmd,client,arg)
659     local ok, vlcerr = pcall( vlc.var.libvlc_command, cmd, arg )
660     if not ok then
661         local a = arg and " "..arg or ""
662         client:append("Error in `"..cmd..a.."' ".. vlcerr) -- when pcall fails, the 2nd arg is the error message.
663     end
664     return vlcerr
665 end
666
667 function call_object_command(cmd,client,arg)
668     local var, val
669     if arg ~= nil then
670         var, val = split_input(arg)
671     end
672     local ok, vlcmsg, vlcerr = pcall( vlc.var.command, cmd, var, val )
673     if not ok then
674         local v = var and " "..var or ""
675         local v2 = val and " "..val or ""
676         client:append("Error in `"..cmd..v..v2.."' ".. vlcmsg) -- when pcall fails the 2nd arg is the error message
677     end
678     if vlcmsg ~= "" then
679         client:append(vlcmsg)
680     end
681     return vlcerr
682 end
683
684 function client_command( client )
685     local cmd,arg = split_input(client.buffer)
686     client.buffer = ""
687
688     if commands[cmd] then
689         call_command(cmd,client,arg)
690     elseif string.sub(cmd,0,1)=='@'
691     and call_object_command(string.sub(cmd,2,#cmd),client,arg) == 0 then
692         --
693     elseif call_vlm_command(cmd,client,arg) == 0 then
694         --
695     elseif client.type == host.client_type.stdio
696     and call_libvlc_command(cmd,client,arg) == 0 then
697         --
698     else
699         local choices = {}
700         if client.env.autocompletion ~= 0 then
701             for v,_ in common.pairs_sorted(commands) do
702                 if string.sub(v,0,#cmd)==cmd then
703                     table.insert(choices, v)
704                 end
705             end
706         end
707         if #choices == 1 and client.env.autoalias ~= 0 then
708             -- client:append("Aliasing to \""..choices[1].."\".")
709             cmd = choices[1]
710             call_command(cmd,client,arg)
711         else
712             client:append("Unknown command `"..cmd.."'. Type `help' for help.")
713             if #choices ~= 0 then
714                 client:append("Possible choices are:")
715                 local cols = math.floor(client.env.width/(client.env.colwidth+1))
716                 local fmt = "%-"..client.env.colwidth.."s"
717                 for i = 1, #choices do
718                     choices[i] = string.format(fmt,choices[i])
719                 end
720                 for i = 1, #choices, cols do
721                     local j = i + cols - 1
722                     if j > #choices then j = #choices end
723                     client:append("  "..table.concat(choices," ",i,j))
724                 end
725             end
726         end
727     end
728 end
729
730 --[[ Some telnet command special characters ]]
731 WILL = "\251" -- Indicates the desire to begin performing, or confirmation that you are now performing, the indicated option.
732 WONT = "\252" -- Indicates the refusal to perform, or continue performing, the indicated option.
733 DO   = "\253" -- Indicates the request that the other party perform, or confirmation that you are expecting the other party to perform, the indicated option.
734 DONT = "\254" -- Indicates the demand that the other party stop performing, or confirmation that you are no longer expecting the other party to perform, the indicated option.
735 IAC  = "\255" -- Interpret as command
736
737 ECHO = "\001"
738
739 function telnet_commands( client )
740     -- remove telnet command replies from the client's data
741     client.buffer = string.gsub( client.buffer, IAC.."["..DO..DONT..WILL..WONT.."].", "" )
742 end
743
744 --[[ Client status change callbacks ]]
745 function on_password( client )
746     client.env = common.table_copy( env )
747     if client.type == host.client_type.telnet then
748         client:send( "Password: " ..IAC..WILL..ECHO )
749     else
750         if client.env.welcome ~= "" then
751             client:send( client.env.welcome .. "\r\n")
752         end
753         client:switch_status( host.status.read )
754     end
755 end
756 -- Print prompt when switching a client's status to `read'
757 function on_read( client )
758     client:send( client.env.prompt )
759 end
760 function on_write( client )
761 end
762
763 --[[ Setup host ]]
764 require("host")
765 h = host.host()
766
767 h.status_callbacks[host.status.password] = on_password
768 h.status_callbacks[host.status.read] = on_read
769 h.status_callbacks[host.status.write] = on_write
770
771 h:listen( config.hosts or config.host or "*console" )
772 password = config.password or "admin"
773
774 --[[ Launch vlm ]]
775 vlm = vlc.vlm()
776
777 --[[ The main loop ]]
778 while not vlc.misc.should_die() do
779     local write, read = h:accept_and_select()
780
781     for _, client in pairs(write) do
782         local len = client:send()
783         client.buffer = string.sub(client.buffer,len+1)
784         if client.buffer == "" then client:switch_status(host.status.read) end
785     end
786
787     for _, client in pairs(read) do
788         local input = client:recv(1000)
789
790         if input == "" -- the telnet client program has left
791             or ((client.type == host.client_type.net
792                  or client.type == host.client_type.telnet)
793                 and input == "\004") then
794             -- Caught a ^D
795             client.cmds = "quit\n"
796         else
797             client.cmds = client.cmds .. input
798         end
799
800         client.buffer = ""
801         -- split the command at the first '\n'
802         while string.find(client.cmds, "\n") do
803             -- save the buffer to send to the client
804             local saved_buffer = client.buffer
805
806             -- get the next command
807             local index = string.find(client.cmds, "\n")
808             client.buffer = strip(string.sub(client.cmds, 0, index - 1))
809             client.cmds = string.sub(client.cmds, index + 1)
810
811             -- Remove telnet commands from the command line
812             if client.type == host.client_type.telnet then
813                 telnet_commands( client )
814             end
815
816             -- Run the command
817             if client.status == host.status.password then
818                 if client.buffer == password then
819                     client:send( IAC..WONT..ECHO.."\r\nWelcome, Master\r\n" )
820                     client.buffer = ""
821                     client:switch_status( host.status.write )
822                 elseif client.buffer == "quit" then
823                     client_command( client )
824                 else
825                     client:send( "\r\nWrong password\r\nPassword: " )
826                     client.buffer = ""
827                 end
828             else
829                 client:switch_status( host.status.write )
830                 client_command( client )
831             end
832             client.buffer = saved_buffer .. client.buffer
833         end
834     end
835 end
836
837 --[[ Clean up ]]
838 vlm = nil