Completion
authorMyhailo Danylenko <isbear@ukrpost.net>
Wed, 25 Feb 2009 22:58:34 +0200
changeset 8 fc9060b9b7cc
parent 7 eb6d89bf1fbf
child 9 c2517f8bf647
Completion * commands completion * common hooks lua interface * complete separation of features into modules
TODO
examples/beep.lua
examples/jobs.lua
examples/marking.lua
examples/mcabberrc.lua
examples/mpd.lua
examples/room_priv.lua
examples/transports.lua
examples/urls.lua
examples/xep0004.lua
examples/xep0047.lua
examples/xep0077.lua
main.c
--- a/TODO	Tue Feb 24 19:42:48 2009 +0200
+++ b/TODO	Wed Feb 25 22:58:34 2009 +0200
@@ -3,7 +3,7 @@
 finish roster list information
 settings list?
 non-setting settings?
-set package searching paths?
+mcabber_config_file uses option to set dir?
 do uninitialization of commands and features with objects?
-debug module reloading issue
+help system accessors (needs major rewrite and planning)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/beep.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -0,0 +1,23 @@
+-- BEEPING ON ALL MESSAGES
+
+beep_enable = false
+
+-- FIXME: add generic cid generation routine
+boolean_cid = main.command ( 'beep',
+	function ( args )
+		local enable = yesno ( args )
+		if enable ~= nil then
+			beep_enable = enable
+		end
+		if beep_enable then
+			hooks_d['hook-message-in'].beep = main.beep
+			print ( "Beep on message is enabled" )
+		else
+			hooks_d['hook-message-in'].beep = nil
+			print ( "Beep on message is disabled" )
+		end
+	end, { 'enable', 'disable', 'yes', 'no', 'true', 'false', 'on', 'off' } )
+
+commands_help['beep'] = "[enable|disable|on|off|yes|no|true|false]\n\nEnables or disables beeping on all messages.\nIf state is omitted, prints current state."
+
+-- vim: se ts=4: --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/jobs.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -0,0 +1,73 @@
+--- JOBS
+
+delayed_jobs = {}
+
+-- may fail
+dopath 'saved_jobs.lua'
+
+function save_jobs ()
+	local h = io.open ( main.config_file ( 'saved_jobs.lua' ), "w" )
+	if not h then
+		print ( 'Cannot open jobs file for writing!' )
+		return
+	end
+	h:write ( "-- This is autogenerated file, do not edit it manually\n\ndelayed_jobs = {\n" );
+	for jid, more in pairs ( delayed_jobs ) do
+		h:write ( string.format ( "\t[%q] = {\n", jid ) )
+		for status, action in pairs ( more ) do
+			if action then -- remove fired jobs
+				h:write ( string.format ( "\t\t[%q] = %q,\n", status, action ) )
+			end
+		end
+		h:write ( "\t},\n" )
+	end
+	h:write ( "}\n" )
+	h:close ()
+end
+
+main.command ( 'delay',
+	function ( args )
+		args = parse_args ( args )
+		local who
+		if args.t then
+			who = args.t
+			args.t = nil
+		else
+			who = main.current_buddy ()
+		end
+		local stat = args[1]
+		args[1] = nil
+		delayed_jobs[who] = { }
+		delayed_jobs[who][stat] = 'say_to -q ' .. who .. ' ' .. rebuild_args_string ( args )
+	end )
+
+main.command ( 'job',
+	function ( args )
+		local action, jid, stat = args:match ( "(%w+)%s+(%w+)%s+(%w)" )
+		if action == 'del' then
+			delayed_jobs[jid][stat] = nil
+		else
+			print ( 'List of jobs:' )
+			for jid, jobs in pairs ( delayed_jobs ) do
+				for status, job in pairs ( jobs ) do
+					print ( ' - ' .. jid .. ' -> ' .. status .. ': ' .. job )
+				end
+			end
+		end
+	end, { "del" } )
+
+commands_help['delay'] = "[-t target_jid] status_letter message\n\nDelays sending a message to target jid (or current buddy) until it switches to specified status."
+commands_help['job'] = "[del jid status_letter]\n\nLists available jobs or deletes specified one."
+
+hooks_d['hook-status-change'].jobs =
+	function ( args )
+		if delayed_jobs[args.jid] and delayed_jobs[args.jid][args.new_status] then
+			main.run ( delayed_jobs[args.jid][args.new_status] )
+			delayed_jobs[args.jid][args.new_status] = nil
+		end
+	end
+
+hooks_d['hook-quit'].jobs = save_jobs
+	
+
+-- vim: se ts=4: --
--- a/examples/marking.lua	Tue Feb 24 19:42:48 2009 +0200
+++ b/examples/marking.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -60,8 +60,12 @@
 					print ( ' - ' .. jid )
 				end )
 		end
-	end )
+	end, { 'clear', 'do' } )
 
 commands_help['marked'] = "[clear | do mcabber_command]\n\nOperates on marked buddies. Without arguments prints list of marked jids.\nCommand should contain %s in place, where jid should be inserted."
 
+-- Ins
+-- TODO: check if ins already bound
+main.run ( 'bind 331 = lua mark_toggle ()' )
+
 -- vim: se ts=4: --
--- a/examples/mcabberrc.lua	Tue Feb 24 19:42:48 2009 +0200
+++ b/examples/mcabberrc.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -13,6 +13,7 @@
 XEP-0077 In-Band Registration (only registration, but with data forms too)
 XEP-0146 Remote commands requests
 Jobs (actions, fired when some event occurs, now in one file, updated on exit)
+Room nicks completion
 MPD status polling (can be turned off)
 Beep on all messages, even on chatroom ones
 Url saving to file (for urlview)
@@ -66,23 +67,8 @@
 
 require 'lm'
 
--- OPTIONS, COMMON SUPPORT ROUTINES
 
-url_file       = main.config_file ( 'urls.log' )
-transport_jids = { 'icq.jabber.kiev.ua', 'mrim.unixzone.org.ua' }
-beep_enable    = false
-
--- XXX: to C?
-char2status = {
-	f = 'free',
-	o = 'online',
-	a = 'away',
-	d = 'dnd',
-	n = 'notavail',
-	i = 'invisible',
-	['_'] = 'offline',
-	['?'] = 'message',
-}
+-- COMMON SUPPORT ROUTINES
 
 function shell_escape ( str )
 	if str then
@@ -178,6 +164,36 @@
 	return ret
 end
 
+-- FIXME: get rid of this
+function rebuild_args_string ( tab )
+	local flags = nil
+	local args = nil
+	for k, v in pairs (tab) do
+		if type (k) == 'number' then
+			if args then
+				args = args .. ' ' .. v
+			else
+				args = v
+			end
+		else
+			if flags then
+				flags = flags .. ' ' .. '-' .. k .. ' ' .. v
+			else
+				flags = '-' .. k .. ' ' .. v
+			end
+		end
+	end
+	if flags and args then
+		return flags .. ' ' .. args
+	elseif flags then
+		return flags
+	elseif args then
+		return args
+	else
+		return ''
+	end
+end
+
 -- COMMANDS
 
 -- Help strings should not contain command, only arguments. This is necessary to support soft aliases.
@@ -199,20 +215,7 @@
 main.command ( 's',
 	function ( args )
 		main.run ( ('status %s %s'):format ( args, mpd_getstatus () ) )
-	end )
-main.command ( 'beep',
-	function ( args )
-		local enable = yesno ( args )
-		if enable == nil then
-			if beep_enable then
-				print ( "Beep on message is enabled" )
-			else
-				print ( "Beep on message is disabled" )
-			end
-		else
-			beep_enable = enable
-		end
-	end )
+	end, 0x10 )
 main.command ( 'cmd',
 	function ( args )
 		local to = main.current_buddy ()
@@ -240,7 +243,7 @@
 			print ( list:sub ( 1, -3 ) )
 			print ( "For built-in mcabber commands see /help" )
 		end
-	end )
+	end, 0x1 )
 main.command ( 'reload',
 	function ( args )
 		dofile ( main.config_file ( 'mcabberrc.lua' ) )
@@ -258,6 +261,7 @@
 		print ( "Resource count: " .. count )
 	end )
 
+-- commands to allow leading smileys
 for k, arg in ipairs ( { ')', '/', '(', 'D', '-/', 'S', '1', ']', '[' } ) do
 	main.command ( arg,
 		function ( args )
@@ -265,6 +269,60 @@
 		end )
 end
 
+-- HOOKS
+
+hooks_d = {
+	['hook-message-in']       = { },
+	['hook-message-out']      = { },
+	['hook-status-change']    = { },
+	['hook-my-status-change'] = { },
+	['hook-post-connect']     = { },
+	['hook-pre-disconnect']   = { },
+	['hook-start']            = { },
+	['hook-quit']             = { },
+}
+
+-- hook:
+-- - hook-message-in
+--   jid
+--   groupchat
+--   message
+-- - hook-message-out
+--   jid
+--   message
+-- - hook-status-change
+--   jid
+--   resource
+--   new_status
+--   old_status
+--   message
+-- - hook-my-status-change
+--   new_status
+--   message
+-- - hook-post-connect
+-- - hook-pre-disconnect
+-- - hook-start
+-- - hook-quit
+function hook_handler ( args )
+	for mod, cb in pairs ( hooks_d[args.hook] ) do
+		if cb then
+			cb ( args )
+		end
+	end
+end
+
+-- SAVING URLS TO FILE
+
+dopath 'urls'
+
+-- TRANSPORTED BUDDIES AVAILABILITY INDICATION
+
+dopath 'transports'
+
+-- BEEPING ON ALL MESSAGES
+
+dopath 'beep'
+
 -- MARKING
 
 dopath 'marking'
@@ -295,171 +353,10 @@
 
 -- JOBS
 
-delayed_jobs = {}
-
-dopath 'jobs.lua'
-
-function save_jobs ()
-	local h = io.open ( main.config_file ( 'jobs.lua' ), "w" )
-	if not h then
-		print ( 'Cannot open jobs file for writing!' )
-		return
-	end
-	h:write ( "-- This is autogenerated file, do not edit it manually\n\ndelayed_jobs = {\n" );
-	for jid, more in pairs ( delayed_jobs ) do
-		h:write ( string.format ( "\t[%q] = {\n", jid ) )
-		for status, action in pairs ( more ) do
-			if action then -- remove fired jobs
-				h:write ( string.format ( "\t\t[%q] = %q,\n", status, action ) )
-			end
-		end
-		h:write ( "\t},\n" )
-	end
-	h:write ( "}\n" )
-	h:close ()
-end
-
-main.command ( 'delay',
-	function ( args )
-		args = parse_args ( args )
-		local who
-		if args.t then
-			who = args.t
-			args.t = nil
-		else
-			who = main.current_buddy ()
-		end
-		local stat = args[1]
-		args[1] = nil
-		delayed_jobs[who] = { }
-		delayed_jobs[who][stat] =
-			function ()
-				main.run ( 'say_to -q ' .. who .. ' ' .. rebuild_args_string ( args ) )
-			end
-	end )
-
-main.command ( 'job',
-	function ( args )
-		local action, jid, stat = args:match ( "(%w+)%s+(%w+)%s+(%w)" )
-		if action == 'del' then
-			delayed_jobs[jid][stat] = nil
-		else
-			print ( 'List of jobs:' )
-			for jid, jobs in pairs ( delayed_jobs ) do
-				for status in pairs ( jobs ) do
-					print ( ' - ' .. jid .. ' -> ' .. status )
-				end
-			end
-		end
-	end )
-
-commands_help['delay'] = "[-t target_jid] status_letter message\n\nDelays sending a message to target jid (or current buddy) until it switches to specified status."
-commands_help['job'] = "[del jid status_letter]\n\nLists available jobs or deletes specified one."
-
--- HOOKS
-
-ibb_handler_registered = false
-
--- Hard hooks, implemented in C
+dopath 'jobs'
 
--- hook:
--- - hook-message-in
---   jid
---   groupchat
---   message
--- - hook-message-out
---   jid
---   message
--- - hook-status-change
---   jid
---   resource
---   new_status
---   old_status
---   message
--- - hook-my-status-change
---   new_status
---   message
--- - hook-post-connect
--- - hook-pre-disconnect
--- - hook-start
--- - hook-quit
-function hook_handler ( args )
-	if args.hook == 'hook-message-in' then
-
-		-- beep on ALL messages, no matter, is it chat or something else.
-		if beep_enable then
-			main.beep ()
-		end
-	
-		-- save urls to file from where urlview can get them...
-		for url in args.message:gmatch ( "https?://[%w%p]+" ) do
-			fd = io.open ( url_file, "a" )
-			if fd then
-				fd:write ( url .. "\n" )
-				fd:close ()
-			else
-				print 'Cannot open urls log file'
-			end
-		end
-
-	elseif args.hook == 'hook-status-change' then
+-- ROOM NICK COMPLETION
 
-		-- delayed actions
-		if delayed_jobs[args.jid] and delayed_jobs[args.jid][args.new_status] then
-			delayed_jobs[args.jid][args.new_status] ()
-			delayed_jobs[args.jid][args.new_status] = nil
-		end
-		
-		-- transported buddies availability indication
-		for k, jid in pairs ( transport_jids ) do
-			if args.jid == jid then
-				if args.new_status == '_' then
-					main.run ( ("color roster * *@%s red"):format ( jid ) )
-					main.run ( ("color roster dn_? *@%s red"):format ( jid ) )
-				else
-					main.run ( ("color roster * *@%s white"):format ( jid ) )
-					main.run ( ("color roster dn_? *@%s brightblack"):format ( jid ) )
-				end
-			end
-		end
-
-	elseif args.hook == 'hook-post-connect' then
-	
-		if mpd_enabled then
-			mpd_callback ()
-		end
-	
-		-- FIXME
-		if not ibb_handler_registered then
-			lm.connection.bless( main.connection () ):handler ( ibb_incoming_iq_handler, 'iq', 'normal' )
-			main.add_feature ( 'http://jabber.org/protocol/ibb' )
-			ibb_handler_registered = true
-		end
-
-	elseif args.hook == 'hook-start' then
-
-		-- XXX: for some reason, after module reloading, this is not working properly.
-		for k, jid in pairs ( transport_jids ) do
-			if not online ( jid ) then
-				main.run ( ("color roster * *@%s red"):format ( jid ) )
-				main.run ( ("color roster dn_? *@%s red"):format ( jid ) )
-			else
-				main.run ( ("color roster * *@%s white"):format ( jid ) )
-				main.run ( ("color roster dn_? *@%s brightblack"):format ( jid ) )
-			end
-		end
-
-	elseif args.hook == 'hook-quit' then
-
-		save_jobs ()
-
-		-- FIXME
-		if ibb_handler_registered then
-			lm.connection.bless( main.connection () ):handler ( ibb_incoming_iq_handler, 'iq' )
-		end
-
-	end
-end
-
+dopath 'room_priv'
 
 -- The End -- vim: se ts=4: --
--- a/examples/mpd.lua	Tue Feb 24 19:42:48 2009 +0200
+++ b/examples/mpd.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -103,6 +103,18 @@
 
 mpd_enabled = false
 
+-- XXX: to C?
+char2status = {
+	f = 'free',
+	o = 'online',
+	a = 'away',
+	d = 'dnd',
+	n = 'notavail',
+	i = 'invisible',
+	['_'] = 'offline',
+	['?'] = 'message',
+}
+
 function mpd_getstatus ()
 	if not mpd_enabled then
 		return ''
@@ -188,8 +200,16 @@
 		else
 			enable_mpd ( enable )
 		end
-	end )
+	end, boolean_cid )
 
 commands_help['mpd'] = "[enable|disable|on|off|yes|no|true|false]\n\nSets state of mpd string in your status.\nIf state is omitted, prints current state."
 
+-- XXX: why post-connect, why not start?
+hooks_d['hook-post-connect'].mpd =
+	function ( args )
+		if mpd_enabled then
+			mpd_callback ()
+		end
+	end
+
 -- vim: se ts=4: --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/room_priv.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -0,0 +1,35 @@
+
+-- ROOM NICK COMPLETION
+
+room_cid = main.command ( 'priv',
+	function ( args )
+		main.run ( 'room privmsg ' .. args )
+	end, {} )
+
+commands_help['priv'] = "nick message\n\nSends private message to room participant. Nick completion available! ;)"
+
+registered_nicks = {}
+
+function register_nicks ()
+	for k, nick in pairs ( registered_nicks ) do
+		main.del_completion ( room_cid, nick )
+	end
+	local buddy = main.current_buddy ()
+	if buddy then
+		local info = main.buddy_info ( buddy )
+		if info then
+			registered_nicks = { }
+			if info.type == 'room' then
+				for nick, k in pairs ( info.resources ) do
+					main.add_completion ( room_cid, nick )
+					table.insert ( registered_nicks, nick )
+				end
+			end
+		end
+	end
+end
+
+main.run ( 'bind 338 = lua main.run ( "roster down" ); register_nicks ()' )
+main.run ( 'bind 339 = lua main.run ( "roster up" ); register_nicks ()' )
+
+-- vim: se ts=4: --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/transports.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -0,0 +1,33 @@
+-- TRANSPORTED BUDDIES AVAILABILITY INDICATION
+
+transport_jids = { 'icq.jabber.kiev.ua', 'mrim.unixzone.org.ua' }
+
+hooks_d['hook-status-change'].transports =
+	function ( args )
+		for k, jid in pairs ( transport_jids ) do
+			if args.jid == jid then
+				if args.new_status == '_' then
+					main.run ( ("color roster * *@%s red"):format ( jid ) )
+					main.run ( ("color roster dn_? *@%s red"):format ( jid ) )
+				else
+					main.run ( ("color roster * *@%s white"):format ( jid ) )
+					main.run ( ("color roster dn_? *@%s brightblack"):format ( jid ) )
+				end
+			end
+		end
+	end
+
+hooks_d['hook-start'].transports =
+	function ( args )
+		for k, jid in pairs ( transport_jids ) do
+			if not online ( jid ) then
+				main.run ( ("color roster * *@%s red"):format ( jid ) )
+				main.run ( ("color roster dn_? *@%s red"):format ( jid ) )
+			else
+				main.run ( ("color roster * *@%s white"):format ( jid ) )
+				main.run ( ("color roster dn_? *@%s brightblack"):format ( jid ) )
+			end
+		end
+	end
+
+-- vim: se ts=4: --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/urls.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -0,0 +1,18 @@
+-- SAVING URLS TO FILE
+
+url_file = main.config_file ( 'urls.log' )
+
+hooks_d['hook-message-in'].urls =
+	function ( args )
+		for url in args.message:gmatch ( "https?://[%w%p]+" ) do
+			fd = io.open ( url_file, "a" )
+			if fd then
+				fd:write ( url .. "\n" )
+				fd:close ()
+			else
+				print 'Cannot open urls log file'
+			end
+		end
+	end
+
+-- vim: se ts=4: --
--- a/examples/xep0004.lua	Tue Feb 24 19:42:48 2009 +0200
+++ b/examples/xep0004.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -1,5 +1,12 @@
 
 forms = { }
+form_cid = 0
+
+function insert_form ( form )
+	table.insert ( forms, form )
+	main.add_completion ( form_cid, tostring(#forms) )
+	return #forms
+end
 
 -- expects lm node   x = { xmlns = 'jabber:x:data', type = 'form', ... }
 -- returns form index in forms table, you can do whatever you want to upper level table, except exp and val fields.
@@ -77,8 +84,7 @@
 		end
 		field = field:next ()
 	end
-	table.insert ( forms, form )
-	return #forms
+	return insert_form ( form )
 end
 
 function form_set ( id, name, value )
@@ -99,7 +105,7 @@
 	end
 end
 
-main.command ( 'form',
+form_cid = main.command ( 'form',
 	function ( args )
 		args = parse_args ( args )
 		local action = args[1]
@@ -133,6 +139,7 @@
 			end
 		elseif action == 'del' then
 			forms[tonumber(args[2])] = nil
+			main.del_completion ( form_cid, tostring(args[2]) )
 		elseif action == 'send' then
 			local form = forms[tonumber(args[2])]
 			if form then
@@ -144,7 +151,7 @@
 				print ( ' - ' .. tostring(id) .. ' ' .. form.title .. ' [' .. ( form.status or 'unknown' ) .. ']' )
 			end
 		end
-	end )
+	end, { "del", "send" } )
 
 commands_help['form'] = "[del form_id | send form_id | form_id [field_name {clear | [set] value}]\n\nWith bare form id prints info on that form.\nWith field name sets or clears field value. Set subcommand is optional, just to allow values, starting with 'clear'.\nWithout arguments prints form list."
 
--- a/examples/xep0047.lua	Tue Feb 24 19:42:48 2009 +0200
+++ b/examples/xep0047.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -195,8 +195,22 @@
 				print ( sid .. ': ' .. ( data.name or '(not set)' ) .. ' [' .. data.status .. ']' )
 			end
 		end
-	end )
+	end, { "send", "accept", "reject", "del" } )
 
 commands_help['ibb'] = "[[-t target_jid] send filename | accept sid filename | reject sid filename | del sid]\n\nRequests, accepts or rejects sending file via in-band bytestream."
 
+hooks_d['hook-post-connect'].xep0047 =
+	function ( args )
+		lm.connection.bless( main.connection () ):handler ( ibb_incoming_iq_handler, 'iq', 'normal' )
+		main.add_feature ( 'http://jabber.org/protocol/ibb' )
+		ibb_handler_registered = true
+		hooks_d['hook-post-connect'].xep0047 = nil
+		hooks_d['hook-quit'].xep0047 =
+			function ( args )
+				if ibb_handler_registered then
+					lm.connection.bless( main.connection () ):handler ( ibb_incoming_iq_handler, 'iq' )
+				end
+			end
+	end
+
 -- vim: se ts=4: --
--- a/examples/xep0077.lua	Tue Feb 24 19:42:48 2009 +0200
+++ b/examples/xep0077.lua	Wed Feb 25 22:58:34 2009 +0200
@@ -20,8 +20,7 @@
 		end
 		field = field:next ()
 	end
-	table.insert ( forms, form )
-	return #forms
+	return insert_form ( form )
 end
 
 function format_iq_register ( fields )
@@ -146,13 +145,13 @@
 main.command ( 'register',
 	function ( args )
 		local who
-		if args[1] then
-			who = rebuild_args_string ( args )
+		if args and args ~= '' then
+			who = args
 		else
 			who = full_current_jid ()
 		end
 		register_to ( who )
-	end )
+	end, 0x2 )
 
 main.command ( 'cancel',
 	function ( args )
@@ -163,7 +162,7 @@
 			who = full_current_jid ()
 		end
 		unregister_from ( who )
-	end )
+	end, 0x2 )
 
 commands_help['register'] = "[jid]\n\nSends registration request to jid (or current buddy). You, probably, then will need to fill and send some form."
 commands_help['cancel'] = "[jid]\n\nSends registration cancellation request to jid (or current buddy). May require a form filling."
--- a/main.c	Tue Feb 24 19:42:48 2009 +0200
+++ b/main.c	Wed Feb 25 22:58:34 2009 +0200
@@ -20,6 +20,7 @@
 #include "utils.h"       // from_utf8, jidtodisp
 #include "hooks.h"       // hk_add_handler, hk_del_handler
 #include "settings.h"    // settings_set, settings_del, settings_get
+#include "compl.h"       // compl_new_category, compl_add_category_word, compl_del_category_word
 
 
 // global lua state object, necessary for uninitialization function
@@ -341,23 +342,69 @@
 	}
 }
 
+/// main.add_completion
+/// Adds word to a completion list.
+/// A: integer (completion group id), string (word)
+static int lua_main_add_completion (lua_State *L)
+{
+	guint       cid  = luaL_checkinteger (L, 1);
+	const char *word = luaL_checkstring (L, 2);
+	compl_add_category_word (cid, word);
+}
+
+/// main.del_completion
+/// Removes word from a completion list.
+/// A: integer (completion group id), string (word)
+static int lua_main_del_completion (lua_State *L)
+{
+	guint       cid  = luaL_checkinteger (L, 1);
+	const char *word = luaL_checkstring (L, 2);
+	compl_del_category_word (cid, word);
+}
+
 /// main.command
 /// Associates or breaks association between mcabber command name and lua function.
 /// To unregister command omit function argument.
-/// A: string (command name), command function (optional)
+/// If you specify a third argument, table (even empty), function will return completion group id or nothing.
+/// You can also specify an integer instead of table, then no new id will be registered.
+/// Note, that for now there are no way to unregister completion group, so, resources can be exausted easily.
+/// Also note, that it ignores keys in a completion table.
+/// A: string (command name), command function (optional), table (completions, optional)/integer (comletion group id, optional)
+/// R: integer (completion group id, optional) or nil
 static int lua_main_command (lua_State *L)
 {
 	const char             *name = luaL_checkstring (L, 1);
 	lua_command_callback_t *cb;
 	if (lua_gettop (L) > 1) { // Register
+		guint cid = 0;
 		luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");
 
+		if (lua_gettop (L) > 2) { // Completions provided
+			if (lua_type (L, 3) == LUA_TTABLE) {
+				cid = compl_new_category ();
+				if (cid) {
+					lua_pushnil (L);
+					while (lua_next (L, 3)) {
+						compl_add_category_word (cid, luaL_checkstring (L, -1));
+						lua_pop (L, 1);
+					}
+				}
+			} else
+				cid = luaL_checkinteger (L, 3);
+		}
+
 		cb = luaL_malloc (L, sizeof (lua_command_callback_t));
+		lua_pushvalue (L, 2);
 		cb->reference = luaL_ref (L, LUA_REGISTRYINDEX);
 		cb->L         = L;
-		cmd_add (name, "", 0, 0, (void (*) (char *p)) lua_main_command_handler, cb);
+		cmd_add (name, "", cid, 0, (void (*) (char *p)) lua_main_command_handler, cb);
 
 		lua_added_commands = g_slist_prepend (lua_added_commands, g_strdup (name));
+
+		if (cid) {
+			lua_pushinteger (L, cid);
+			return 1;
+		}
 	} else { // Unregister
 		GSList *el = g_slist_find_custom (lua_added_commands, name, (GCompareFunc) g_strcmp0);
 
@@ -546,7 +593,7 @@
  
 	if (lua_pcall (L, 0, 0, 0)) {
 		scr_LogPrint (LPRINT_NORMAL, "lua: Runtime error: %s", lua_tostring(lua, -1));
-		lua_pop (L, -1);
+		lua_pop (L, 1);
 		return;
 	}
 }
@@ -590,6 +637,8 @@
 	{ "option",        lua_main_option        },
 	{ "add_feature",   lua_main_add_feature   },
 	{ "del_feature",   lua_main_del_feature   },
+	{ "add_completion",lua_main_add_completion},
+	{ "del_completion",lua_main_del_completion},
 	{ "command",       lua_main_command       },
 	{ "print_info",    lua_main_print_info    },
 	{ "beep",          lua_main_beep          },