about summary refs log tree commit diff stats
path: root/src/command
diff options
context:
space:
mode:
Diffstat (limited to 'src/command')
-rw-r--r--src/command/command.c2671
-rw-r--r--src/command/command.h52
-rw-r--r--src/command/history.c69
-rw-r--r--src/command/history.h28
-rw-r--r--src/command/parser.c263
-rw-r--r--src/command/parser.h31
6 files changed, 3114 insertions, 0 deletions
diff --git a/src/command/command.c b/src/command/command.c
new file mode 100644
index 00000000..778339fa
--- /dev/null
+++ b/src/command/command.c
@@ -0,0 +1,2671 @@
+/*
+ * command.c
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+
+#include "accounts.h"
+#include "chat_session.h"
+#include "command/command.h"
+#include "command/history.h"
+#include "command/parser.h"
+#include "common.h"
+#include "contact.h"
+#include "contact_list.h"
+#include "jid.h"
+#include "log.h"
+#include "muc.h"
+#include "preferences.h"
+#include "profanity.h"
+#include "theme.h"
+#include "tools/autocomplete.h"
+#include "tools/tinyurl.h"
+#include "ui/ui.h"
+#include "xmpp/xmpp.h"
+
+typedef char*(*autocomplete_func)(char *);
+
+/*
+ * Command structure
+ *
+ * cmd - The command string including leading '/'
+ * func - The function to execute for the command
+ * parser - The function used to parse arguments
+ * min_args - Minimum number of arguments
+ * max_args - Maximum number of arguments
+ * help - A help struct containing usage info etc
+ */
+struct cmd_t {
+    const gchar *cmd;
+    gboolean (*func)(gchar **args, struct cmd_help_t help);
+    gchar** (*parser)(const char * const inp, int min, int max);
+    int min_args;
+    int max_args;
+    struct cmd_help_t help;
+};
+
+static struct cmd_t * _cmd_get_command(const char * const command);
+static void _update_presence(const jabber_presence_t presence,
+    const char * const show, gchar **args);
+static gboolean _cmd_set_boolean_preference(gchar *arg, struct cmd_help_t help,
+    const char * const display,
+    void (*set_func)(gboolean));
+
+static void _cmd_complete_parameters(char *input, int *size);
+static void _notify_autocomplete(char *input, int *size);
+static void _titlebar_autocomplete(char *input, int *size);
+static void _theme_autocomplete(char *input, int *size);
+static void _autoaway_autocomplete(char *input, int *size);
+static void _account_autocomplete(char *input, int *size);
+static void _parameter_autocomplete(char *input, int *size, char *command,
+    autocomplete_func func);
+static void _parameter_autocomplete_with_ac(char *input, int *size, char *command,
+    Autocomplete ac);
+
+static int _strtoi(char *str, int *saveptr, int min, int max);
+static jabber_presence_t _presence_type_from_string(const char * const str);
+
+// command prototypes
+static gboolean _cmd_quit(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_help(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_about(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_prefs(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_who(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_connect(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_account(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_disconnect(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_sub(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_msg(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_tiny(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_close(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_join(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_beep(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_notify(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_log(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_priority(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_reconnect(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_intype(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_flash(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_splash(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_chlog(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_history(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_states(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_outtype(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_gone(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_autoping(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_titlebar(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_autoaway(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_mouse(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_set_statuses(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_vercheck(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_away(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_online(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_dnd(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_chat(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_xa(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_info(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_wins(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_nick(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_theme(gchar **args, struct cmd_help_t help);
+static gboolean _cmd_status(gchar **args, struct cmd_help_t help);
+
+/*
+ * The commands are broken down into three groups:
+ * Main commands
+ * Commands to change preferences
+ * Commands to change users status
+ */
+static struct cmd_t main_commands[] =
+{
+    { "/help",
+        _cmd_help, parse_args, 0, 1,
+        { "/help [list|area|command]", "Get help on using Profanity",
+        { "/help [list|area|command]",
+          "-------------------------",
+          "list    : List of all commands.",
+          "area    : One of 'basic', 'presence', 'settings', 'navigation' for more summary help in that area.",
+          "command : Detailed help on a specific command.",
+          "",
+          "Example : /help list",
+          "Example : /help connect",
+          "Example : /help settings",
+          NULL } } },
+
+    { "/about",
+        _cmd_about, parse_args, 0, 0,
+        { "/about", "About Profanity",
+        { "/about",
+          "------",
+          "Show versioning and license information.",
+          NULL  } } },
+
+    { "/connect",
+        _cmd_connect, parse_args, 1, 2,
+        { "/connect account [server]", "Login to a chat service.",
+        { "/connect account [server]",
+          "-------------------------",
+          "Connect to an XMPP service using the specified account.",
+          "Use the server argument for chat services hosted at a different domain to the 'domain' part of the Jabber ID.",
+          "An account is automatically created if one does not exist.  See the /account command for more details.",
+          "",
+          "Example: /connect myuser@gmail.com",
+          "Example: /connect myuser@mycompany.com talk.google.com",
+          NULL  } } },
+
+    { "/disconnect",
+        _cmd_disconnect, parse_args, 0, 0,
+        { "/disconnect", "Logout of current session.",
+        { "/disconnect",
+          "------------------",
+          "Disconnect from the current session session.",
+          NULL  } } },
+
+    { "/account",
+        _cmd_account, parse_args, 1, 4,
+        { "/account command [account] [property] [value]", "Manage accounts.",
+        { "/account command [account] [property] [value]",
+          "---------------------------------------------",
+          "Commands for creating and managing accounts.",
+          "list                       : List all accounts.",
+          "show account               : Show information about an account.",
+          "enable account             : Enable the account, so it is used for autocomplete.",
+          "disable account            : Disable the account.",
+          "add account                : Create a new account.",
+          "rename account newname     : Rename account to newname.",
+          "set account property value : Set 'property' of 'account' to 'value'.",
+          "",
+          "The 'property' may be one of.",
+          "jid              : The Jabber ID of the account, the account name will be used if this property is not set.",
+          "server           : The chat service server, if different to the domain part of the JID.",
+          "status           : The presence status to use on login, use 'last' to use whatever your last status was.",
+          "online|chat|away",
+          "|xa|dnd          : Priority for the specified presence.",
+          "resource         : The resource to be used.",
+          "",
+          "Example : /account add work",
+          "        : /account set work jid myuser@mycompany.com",
+          "        : /account set work server talk.google.com",
+          "        : /account set work resource desktop",
+          "        : /account set work status dnd",
+          "        : /account set dnd -1",
+          "        : /account set online 10",
+          "        : /account rename work gtalk",
+          NULL  } } },
+
+    { "/prefs",
+        _cmd_prefs, parse_args, 0, 1,
+        { "/prefs [area]", "Show configuration.",
+        { "/prefs [area]",
+          "-------------",
+          "Area is one of:",
+          "ui       : User interface preferences.",
+          "desktop  : Desktop notification preferences.",
+          "chat     : Chat state preferences.",
+          "log      : Logging preferences.",
+          "conn     : Connection handling preferences.",
+          "presence : Chat presence preferences.",
+          "",
+          "No argument shows all categories.",
+          NULL } } },
+
+    { "/theme",
+        _cmd_theme, parse_args, 1, 2,
+        { "/theme command [theme-name]", "Change colour theme.",
+        { "/theme command [theme-name]",
+          "---------------------------",
+          "Change the colour settings used.",
+          "",
+          "command : One of the following,",
+          "list             : List all available themes.",
+          "set [theme-name] : Load the named theme.\"default\" will reset to the default colours.",
+          "",
+          "Example : /theme list",
+          "Example : /theme set mycooltheme",
+          NULL } } },
+
+    { "/msg",
+        _cmd_msg, parse_args_with_freetext, 1, 2,
+        { "/msg jid|nick [message]", "Start chat with user.",
+        { "/msg jid|nick [message]",
+          "-----------------------",
+          "Open a chat window with for the user JID (Jabber ID)  and send the message if one is supplied.",
+          "When in a chat room, supply the nickname to start private chat with the room member.",
+          "Use quotes if the nickname includes spaces.",
+          "",
+          "Example : /msg myfriend@server.com Hey, here's a message!",
+          "Example : /msg otherfriend@server.com",
+          "Example : /msg Bob Here is a private message",
+          "Example : /msg \"My Friend\" Hi, how are you?",
+          NULL } } },
+
+    { "/info",
+        _cmd_info, parse_args, 0, 1,
+        { "/info [jid|nick]", "Find out a contacts presence information.",
+        { "/info [jid|nick]",
+          "----------------",
+          "Find out a contact, or room members presence information.",
+          "If in a chat window the parameter is not required, the current recipient will be used.",
+          NULL } } },
+
+    { "/status",
+        _cmd_status, parse_args, 0, 1,
+        { "/status [jid|nick]", "Find out a contacts presence information.",
+        { "/status [jid|nick]",
+          "------------------",
+          "Find out a contact, or room members presence information.",
+          "If in a chat window the parameter is not required, the current recipient will be used.",
+          NULL } } },
+
+    { "/join",
+        _cmd_join, parse_args_with_freetext, 1, 2,
+        { "/join room [nick]", "Join a chat room.",
+        { "/join room [nick]",
+          "-----------------",
+          "Join a chat room at the conference server.",
+          "If nick is specified you will join with this nickname.",
+          "Otherwise the first part of your JID (before the @) will be used.",
+          "If the room doesn't exist, and the server allows it, a new one will be created.",
+          "",
+          "Example : /join jdev@conference.jabber.org",
+          "Example : /join jdev@conference.jabber.org mynick",
+          NULL } } },
+
+    { "/nick",
+        _cmd_nick, parse_args_with_freetext, 1, 1,
+        { "/nick nickname", "Change nickname in chat room.",
+        { "/nick nickname",
+          "--------------",
+          "Change the name by which other members of a chat room see you.",
+          "This command is only valid when called within a chat room window.",
+          "",
+          "Example : /nick kai hansen",
+          "Example : /nick bob",
+          NULL } } },
+
+    { "/wins",
+        _cmd_wins, parse_args, 0, 0,
+        { "/wins", "List active windows.",
+        { "/wins",
+          "-----",
+          "List all currently active windows and information about their usage.",
+          NULL } } },
+
+    { "/sub",
+        _cmd_sub, parse_args, 1, 2,
+        { "/sub command [jid]", "Manage subscriptions.",
+        { "/sub command [jid]",
+          "------------------",
+          "command : One of the following,",
+          "request  : Send a subscription request to the user to be informed of their",
+          "         : presence.",
+          "allow    : Approve a contact's subscription reqeust to see your presence.",
+          "deny     : Remove subscription for a contact, or deny a request",
+          "show     : Show subscriprion status for a contact.",
+          "sent     : Show all sent subscription requests pending a response.",
+          "received : Show all received subscription requests awaiting your response.",
+          "",
+          "The optional 'jid' parameter only applys to 'request', 'allow', 'deny' and 'show'",
+          "If it is omitted the contact of the current window is used.",
+          "",
+          "Example: /sub request myfriend@jabber.org",
+          "Example: /sub allow myfriend@jabber.org",
+          "Example: /sub request (whilst in chat with contact)",
+          "Example: /sub sent",
+          NULL  } } },
+
+    { "/tiny",
+        _cmd_tiny, parse_args, 1, 1,
+        { "/tiny url", "Send url as tinyurl in current chat.",
+        { "/tiny url",
+          "---------",
+          "Send the url as a tiny url.",
+          "",
+          "Example : /tiny http://www.google.com",
+          NULL } } },
+
+    { "/who",
+        _cmd_who, parse_args, 0, 1,
+        { "/who [status]", "Show contacts/room participants with chosen status.",
+        { "/who [status]",
+          "-------------",
+          "Show contacts with the specified status, no status shows all contacts.",
+          "Possible statuses are: online, offline, away, dnd, xa, chat, available, unavailable.",
+          "If in a chat room, the participants with the supplied status are displayed.",
+          "",
+          "online      : Contacts that are connected, i.e. online, chat, away, xa, dnd",
+          "available   : Contacts that are available for chat, i.e. online, chat.",
+          "unavailable : Contacts that are not available for chat, i.e. offline, away, xa, dnd.",
+          NULL } } },
+
+    { "/close",
+        _cmd_close, parse_args, 0, 0,
+        { "/close", "Close current chat window.",
+        { "/close",
+          "------",
+          "Close the current chat window, no message is sent to the recipient,",
+          "The chat window will become available for new chats.",
+          "If in a chat room, you will leave the room.",
+          NULL } } },
+
+    { "/quit",
+        _cmd_quit, parse_args, 0, 0,
+        { "/quit", "Quit Profanity.",
+        { "/quit",
+          "-----",
+          "Logout of any current session, and quit Profanity.",
+          NULL } } }
+};
+
+static struct cmd_t setting_commands[] =
+{
+    { "/beep",
+        _cmd_set_beep, parse_args, 1, 1,
+        { "/beep on|off", "Terminal beep on new messages.",
+        { "/beep on|off",
+          "------------",
+          "Switch the terminal bell on or off.",
+          "The bell will sound when incoming messages are received.",
+          "If the terminal does not support sounds, it may attempt to flash the screen instead.",
+          NULL } } },
+
+    { "/notify",
+        _cmd_set_notify, parse_args, 2, 2,
+        { "/notify type value", "Control various desktop noficiations.",
+        { "/notify type value",
+          "------------------",
+          "Settings for various desktop notifications where type is one of:",
+          "message : Notificaitons for messages.",
+          "        : on|off",
+          "remind  : Notification reminders of unread messages.",
+          "        : where value is the reminder period in seconds,",
+          "        : use 0 to disable.",
+          "typing  : Notifications when contacts are typing.",
+          "        : on|off",
+          "",
+          "Example : /notify message on (enable message notifications)",
+          "Example : /notify remind 10  (remind every 10 seconds)",
+          "Example : /notify remind 0   (switch off reminders)",
+          "Example : /notify typing on  (enable typing notifications)",
+          NULL } } },
+
+    { "/flash",
+        _cmd_set_flash, parse_args, 1, 1,
+        { "/flash on|off", "Terminal flash on new messages.",
+        { "/flash on|off",
+          "-------------",
+          "Make the terminal flash when incoming messages are recieved.",
+          "The flash will only occur if you are not in the chat window associated with the user sending the message.",
+          "If the terminal doesn't support flashing, it may attempt to beep.",
+          NULL } } },
+
+    { "/intype",
+        _cmd_set_intype, parse_args, 1, 1,
+        { "/intype on|off", "Show when contact is typing.",
+        { "/intype on|off",
+          "--------------",
+          "Show when a contact is typing in the console, and in active message window.",
+          NULL } } },
+
+    { "/splash",
+        _cmd_set_splash, parse_args, 1, 1,
+        { "/splash on|off", "Splash logo on startup.",
+        { "/splash on|off",
+          "--------------",
+          "Switch on or off the ascii logo on start up.",
+          NULL } } },
+
+    { "/vercheck",
+        _cmd_vercheck, parse_args, 0, 1,
+        { "/vercheck [on|off]", "Check for a new release.",
+        { "/vercheck [on|off]",
+          "------------------",
+          "Without a parameter will check for a new release.",
+          "Switching on or off will enable/disable a version check when Profanity starts, and each time the /about command is run.",
+          NULL  } } },
+
+    { "/titlebar",
+        _cmd_set_titlebar, parse_args, 2, 2,
+        { "/titlebar property on|off", "Show various properties in the window title bar.",
+        { "/titlebar property on|off",
+          "-------------------------",
+          "Show various properties in the window title bar.",
+          "Possible properties are 'version'.",
+          NULL  } } },
+
+    { "/mouse",
+        _cmd_set_mouse, parse_args, 1, 1,
+        { "/mouse on|off", "Use profanity mouse handling.",
+        { "/mouse on|off",
+          "-------------",
+          "If set to 'on', profanity will handle mouse actions, which enabled scrolling the main window with the mouse wheel.",
+          "To select text, use the shift key while selcting an area.",
+          "If set to 'off', profanity leaves mouse handling to the terminal implementation.",
+          "The default is 'on', if you have strange behaviour with mouse actions, set to 'off'.",
+          NULL } } },
+
+    { "/chlog",
+        _cmd_set_chlog, parse_args, 1, 1,
+        { "/chlog on|off", "Chat logging to file",
+        { "/chlog on|off",
+          "-------------",
+          "Switch chat logging on or off.",
+          NULL } } },
+
+    { "/states",
+        _cmd_set_states, parse_args, 1, 1,
+        { "/states on|off", "Send chat states during a chat session.",
+        { "/states on|off",
+          "--------------",
+          "Sending of chat state notifications during chat sessions.",
+          "Such as whether you have become inactive, or have close the chat window.",
+          NULL } } },
+
+    { "/outtype",
+        _cmd_set_outtype, parse_args, 1, 1,
+        { "/outtype on|off", "Send typing notification to recipient.",
+        { "/outtype on|off",
+          "--------------",
+          "Send an indication that you are typing to the other person in chat.",
+          "Chat states must be enabled for this to work, see the /states command.",
+          NULL } } },
+
+    { "/gone",
+        _cmd_set_gone, parse_args, 1, 1,
+        { "/gone minutes", "Send 'gone' state to recipient after a period.",
+        { "/gone minutes",
+          "--------------",
+          "Send a 'gone' state to the recipient after the specified number of minutes."
+          "This indicates to the recipient's client that you have left the conversation.",
+          "A value of 0 will disable sending this chat state automatically after a period.",
+          "Chat states must be enabled for this to work, see the /states command.",
+          NULL } } },
+
+    { "/history",
+        _cmd_set_history, parse_args, 1, 1,
+        { "/history on|off", "Chat history in message windows.",
+        { "/history on|off",
+          "---------------",
+          "Switch chat history on or off, requires /chlog to be enabled.",
+          "When history is enabled, previous messages are shown in chat windows.",
+          NULL } } },
+
+    { "/log",
+        _cmd_set_log, parse_args, 2, 2,
+        { "/log maxsize value", "Manage system logging settings.",
+        { "/log maxsize value",
+          "------------------",
+          "maxsize : When log file size exceeds this value it will be automatically",
+          "          rotated (file will be renamed). Default value is 1048580 (1MB)",
+          NULL } } },
+
+    { "/reconnect",
+        _cmd_set_reconnect, parse_args, 1, 1,
+        { "/reconnect seconds", "Set reconnect interval.",
+        { "/reconnect seconds",
+          "--------------------",
+          "Set the reconnect attempt interval in seconds for when the connection is lost.",
+          "A value of 0 will switch of reconnect attempts.",
+          NULL } } },
+
+    { "/autoping",
+        _cmd_set_autoping, parse_args, 1, 1,
+        { "/autoping seconds", "Server ping interval.",
+        { "/autoping seconds",
+          "-----------------",
+          "Set the number of seconds between server pings, so ensure connection kept alive.",
+          "A value of 0 will switch off autopinging the server.",
+          NULL } } },
+
+    { "/autoaway",
+        _cmd_set_autoaway, parse_args_with_freetext, 2, 2,
+        { "/autoaway setting value", "Set auto idle/away properties.",
+        { "/autoaway setting value",
+          "-----------------------",
+          "'setting' may be one of 'mode', 'minutes', 'message' or 'check', with the following values:",
+          "",
+          "mode    : idle - Sends idle time, whilst your status remains online.",
+          "          away - Sends an away presence.",
+          "          off - Disabled (default).",
+          "time    : Number of minutes before the presence change is sent, the default is 15.",
+          "message : Optional message to send with the presence change.",
+          "        : off - Disable message (default).",
+          "check   : on|off, when enabled, checks for activity and sends online presence, default is 'on'.",
+          "",
+          "Example: /autoaway mode idle",
+          "Example: /autoaway time 30",
+          "Example: /autoaway message I'm not really doing much",
+          "Example: /autoaway check false",
+          NULL } } },
+
+    { "/priority",
+        _cmd_set_priority, parse_args, 1, 1,
+        { "/priority value", "Set priority for the current account.",
+        { "/priority value",
+          "---------------",
+          "Set priority for the current account, presence will be sent when calling this command.",
+          "See the /account command for more specific priority settings per presence status.",
+          "value : Number between -128 and 127. Default value is 0.",
+          NULL } } },
+
+    { "/statuses",
+        _cmd_set_statuses, parse_args, 1, 1,
+        { "/statuses on|off", "Set notifications for status messages.",
+        { "/statuses on|off",
+          "---------------",
+          "Set notifications for status messages, such as online/offline or join/part channels.",
+          "When notifications are off status messages, such as online/offline or join/part, are not displayed.",
+          NULL } } }
+};
+
+static struct cmd_t presence_commands[] =
+{
+    { "/away",
+        _cmd_away, parse_args_with_freetext, 0, 1,
+        { "/away [msg]", "Set status to away.",
+        { "/away [msg]",
+          "-----------",
+          "Set your status to 'away' with the optional message.",
+          "Your current status can be found in the top right of the screen.",
+          "",
+          "Example : /away Gone for lunch",
+          NULL } } },
+
+    { "/chat",
+        _cmd_chat, parse_args_with_freetext, 0, 1,
+        { "/chat [msg]", "Set status to chat (available for chat).",
+        { "/chat [msg]",
+          "-----------",
+          "Set your status to 'chat', meaning 'available for chat', with the optional message.",
+          "Your current status can be found in the top right of the screen.",
+          "",
+          "Example : /chat Please talk to me!",
+          NULL } } },
+
+    { "/dnd",
+        _cmd_dnd, parse_args_with_freetext, 0, 1,
+        { "/dnd [msg]", "Set status to dnd (do not disturb).",
+        { "/dnd [msg]",
+          "----------",
+          "Set your status to 'dnd', meaning 'do not disturb', with the optional message.",
+          "Your current status can be found in the top right of the screen.",
+          "",
+          "Example : /dnd I'm in the zone",
+          NULL } } },
+
+    { "/online",
+        _cmd_online, parse_args_with_freetext, 0, 1,
+        { "/online [msg]", "Set status to online.",
+        { "/online [msg]",
+          "-------------",
+          "Set your status to 'online' with the optional message.",
+          "Your current status can be found in the top right of the screen.",
+          "",
+          "Example : /online Up the Irons!",
+          NULL } } },
+
+    { "/xa",
+        _cmd_xa, parse_args_with_freetext, 0, 1,
+        { "/xa [msg]", "Set status to xa (extended away).",
+        { "/xa [msg]",
+          "---------",
+          "Set your status to 'xa', meaning 'extended away', with the optional message.",
+          "Your current status can be found in the top right of the screen.",
+          "",
+          "Example : /xa This meeting is going to be a long one",
+          NULL } } },
+};
+
+static Autocomplete commands_ac;
+static Autocomplete who_ac;
+static Autocomplete help_ac;
+static Autocomplete notify_ac;
+static Autocomplete prefs_ac;
+static Autocomplete sub_ac;
+static Autocomplete log_ac;
+static Autocomplete autoaway_ac;
+static Autocomplete autoaway_mode_ac;
+static Autocomplete titlebar_ac;
+static Autocomplete theme_ac;
+static Autocomplete theme_load_ac;
+static Autocomplete account_ac;
+
+/*
+ * Initialise command autocompleter and history
+ */
+void
+cmd_init(void)
+{
+    log_info("Initialising commands");
+
+    commands_ac = autocomplete_new();
+    who_ac = autocomplete_new();
+
+    prefs_ac = autocomplete_new();
+    autocomplete_add(prefs_ac, strdup("ui"));
+    autocomplete_add(prefs_ac, strdup("desktop"));
+    autocomplete_add(prefs_ac, strdup("chat"));
+    autocomplete_add(prefs_ac, strdup("log"));
+    autocomplete_add(prefs_ac, strdup("conn"));
+    autocomplete_add(prefs_ac, strdup("presence"));
+
+    help_ac = autocomplete_new();
+    autocomplete_add(help_ac, strdup("list"));
+    autocomplete_add(help_ac, strdup("basic"));
+    autocomplete_add(help_ac, strdup("presence"));
+    autocomplete_add(help_ac, strdup("settings"));
+    autocomplete_add(help_ac, strdup("navigation"));
+
+    notify_ac = autocomplete_new();
+    autocomplete_add(notify_ac, strdup("message"));
+    autocomplete_add(notify_ac, strdup("typing"));
+    autocomplete_add(notify_ac, strdup("remind"));
+    autocomplete_add(notify_ac, strdup("status"));
+
+    sub_ac = autocomplete_new();
+    autocomplete_add(sub_ac, strdup("request"));
+    autocomplete_add(sub_ac, strdup("allow"));
+    autocomplete_add(sub_ac, strdup("deny"));
+    autocomplete_add(sub_ac, strdup("show"));
+    autocomplete_add(sub_ac, strdup("sent"));
+    autocomplete_add(sub_ac, strdup("received"));
+
+    titlebar_ac = autocomplete_new();
+    autocomplete_add(titlebar_ac, strdup("version"));
+
+    log_ac = autocomplete_new();
+    autocomplete_add(log_ac, strdup("maxsize"));
+
+    autoaway_ac = autocomplete_new();
+    autocomplete_add(autoaway_ac, strdup("mode"));
+    autocomplete_add(autoaway_ac, strdup("time"));
+    autocomplete_add(autoaway_ac, strdup("message"));
+    autocomplete_add(autoaway_ac, strdup("check"));
+
+    autoaway_mode_ac = autocomplete_new();
+    autocomplete_add(autoaway_mode_ac, strdup("away"));
+    autocomplete_add(autoaway_mode_ac, strdup("idle"));
+    autocomplete_add(autoaway_mode_ac, strdup("off"));
+
+    theme_ac = autocomplete_new();
+    autocomplete_add(theme_ac, strdup("list"));
+    autocomplete_add(theme_ac, strdup("set"));
+
+    account_ac = autocomplete_new();
+    autocomplete_add(account_ac, strdup("list"));
+    autocomplete_add(account_ac, strdup("show"));
+    autocomplete_add(account_ac, strdup("add"));
+    autocomplete_add(account_ac, strdup("enable"));
+    autocomplete_add(account_ac, strdup("disable"));
+    autocomplete_add(account_ac, strdup("rename"));
+    autocomplete_add(account_ac, strdup("set"));
+
+    theme_load_ac = NULL;
+
+    unsigned int i;
+    for (i = 0; i < ARRAY_SIZE(main_commands); i++) {
+        struct cmd_t *pcmd = main_commands+i;
+        autocomplete_add(commands_ac, (gchar *)strdup(pcmd->cmd));
+        autocomplete_add(help_ac, (gchar *)strdup(pcmd->cmd+1));
+    }
+
+    for (i = 0; i < ARRAY_SIZE(setting_commands); i++) {
+        struct cmd_t *pcmd = setting_commands+i;
+        autocomplete_add(commands_ac, (gchar *)strdup(pcmd->cmd));
+        autocomplete_add(help_ac, (gchar *)strdup(pcmd->cmd+1));
+    }
+
+    for (i = 0; i < ARRAY_SIZE(presence_commands); i++) {
+        struct cmd_t *pcmd = presence_commands+i;
+        autocomplete_add(commands_ac, (gchar *)strdup(pcmd->cmd));
+        autocomplete_add(help_ac, (gchar *)strdup(pcmd->cmd+1));
+        autocomplete_add(who_ac, (gchar *)strdup(pcmd->cmd+1));
+    }
+
+    autocomplete_add(who_ac, strdup("offline"));
+    autocomplete_add(who_ac, strdup("available"));
+    autocomplete_add(who_ac, strdup("unavailable"));
+
+    cmd_history_init();
+}
+
+void
+cmd_close(void)
+{
+    autocomplete_free(commands_ac);
+    autocomplete_free(who_ac);
+    autocomplete_free(help_ac);
+    autocomplete_free(notify_ac);
+    autocomplete_free(sub_ac);
+    autocomplete_free(log_ac);
+    autocomplete_free(prefs_ac);
+    autocomplete_free(autoaway_ac);
+    autocomplete_free(autoaway_mode_ac);
+    autocomplete_free(theme_ac);
+    if (theme_load_ac != NULL) {
+        autocomplete_free(theme_load_ac);
+    }
+    autocomplete_free(account_ac);
+}
+
+// Command autocompletion functions
+void
+cmd_autocomplete(char *input, int *size)
+{
+    int i = 0;
+    char *found = NULL;
+    char *auto_msg = NULL;
+    char inp_cpy[*size];
+
+    // autocomplete command
+    if ((strncmp(input, "/", 1) == 0) && (!str_contains(input, *size, ' '))) {
+        for(i = 0; i < *size; i++) {
+            inp_cpy[i] = input[i];
+        }
+        inp_cpy[i] = '\0';
+        found = autocomplete_complete(commands_ac, inp_cpy);
+        if (found != NULL) {
+            auto_msg = (char *) malloc((strlen(found) + 1) * sizeof(char));
+            strcpy(auto_msg, found);
+            inp_replace_input(input, auto_msg, size);
+            free(auto_msg);
+            free(found);
+        }
+
+    // autocomplete parameters
+    } else {
+        _cmd_complete_parameters(input, size);
+    }
+}
+
+void
+cmd_reset_autocomplete()
+{
+    contact_list_reset_search_attempts();
+    accounts_reset_all_search();
+    accounts_reset_enabled_search();
+    prefs_reset_boolean_choice();
+    autocomplete_reset(help_ac);
+    autocomplete_reset(notify_ac);
+    autocomplete_reset(sub_ac);
+
+    if (win_current_is_groupchat()) {
+        Autocomplete nick_ac = muc_get_roster_ac(win_current_get_recipient());
+        if (nick_ac != NULL) {
+            autocomplete_reset(nick_ac);
+        }
+    }
+
+    autocomplete_reset(who_ac);
+    autocomplete_reset(prefs_ac);
+    autocomplete_reset(log_ac);
+    autocomplete_reset(commands_ac);
+    autocomplete_reset(autoaway_ac);
+    autocomplete_reset(autoaway_mode_ac);
+    autocomplete_reset(theme_ac);
+    if (theme_load_ac != NULL) {
+        autocomplete_reset(theme_load_ac);
+        theme_load_ac = NULL;
+    }
+    autocomplete_reset(account_ac);
+}
+
+GSList *
+cmd_get_basic_help(void)
+{
+    GSList *result = NULL;
+
+    unsigned int i;
+    for (i = 0; i < ARRAY_SIZE(main_commands); i++) {
+        result = g_slist_append(result, &((main_commands+i)->help));
+    }
+
+    return result;
+}
+
+GSList *
+cmd_get_settings_help(void)
+{
+    GSList *result = NULL;
+
+    unsigned int i;
+    for (i = 0; i < ARRAY_SIZE(setting_commands); i++) {
+        result = g_slist_append(result, &((setting_commands+i)->help));
+    }
+
+    return result;
+}
+
+GSList *
+cmd_get_presence_help(void)
+{
+    GSList *result = NULL;
+
+    unsigned int i;
+    for (i = 0; i < ARRAY_SIZE(presence_commands); i++) {
+        result = g_slist_append(result, &((presence_commands+i)->help));
+    }
+
+    return result;
+}
+
+// Command execution
+
+gboolean
+cmd_execute(const char * const command, const char * const inp)
+{
+    struct cmd_t *cmd = _cmd_get_command(command);
+
+    if (cmd != NULL) {
+        gchar **args = cmd->parser(inp, cmd->min_args, cmd->max_args);
+        if (args == NULL) {
+            cons_show("Usage: %s", cmd->help.usage);
+            if (win_current_is_chat()) {
+                char usage[strlen(cmd->help.usage) + 8];
+                sprintf(usage, "Usage: %s", cmd->help.usage);
+                win_current_show(usage);
+            }
+            return TRUE;
+        } else {
+            gboolean result = cmd->func(args, cmd->help);
+            g_strfreev(args);
+            return result;
+        }
+    } else {
+        return cmd_execute_default(inp);
+    }
+}
+
+gboolean
+cmd_execute_default(const char * const inp)
+{
+    if (win_current_is_groupchat()) {
+        jabber_conn_status_t status = jabber_get_connection_status();
+        if (status != JABBER_CONNECTED) {
+            win_current_show("You are not currently connected.");
+        } else {
+            char *recipient = win_current_get_recipient();
+            message_send_groupchat(inp, recipient);
+            free(recipient);
+        }
+    } else if (win_current_is_chat() || win_current_is_private()) {
+        jabber_conn_status_t status = jabber_get_connection_status();
+        if (status != JABBER_CONNECTED) {
+            win_current_show("You are not currently connected.");
+        } else {
+            char *recipient = win_current_get_recipient();
+            message_send(inp, recipient);
+
+            if (win_current_is_chat() && prefs_get_chlog()) {
+                const char *jid = jabber_get_jid();
+                Jid *jidp = jid_create(jid);
+                chat_log_chat(jidp->barejid, recipient, inp, PROF_OUT_LOG, NULL);
+                jid_destroy(jidp);
+            }
+
+            win_show_outgoing_msg("me", recipient, inp);
+            free(recipient);
+        }
+    } else {
+        cons_bad_command(inp);
+    }
+
+    return TRUE;
+}
+
+static void
+_cmd_complete_parameters(char *input, int *size)
+{
+    _parameter_autocomplete(input, size, "/beep",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/intype",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/states",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/outtype",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/flash",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/splash",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/chlog",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/mouse",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/history",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/vercheck",
+        prefs_autocomplete_boolean_choice);
+    _parameter_autocomplete(input, size, "/statuses",
+        prefs_autocomplete_boolean_choice);
+
+    if (win_current_is_groupchat()) {
+        Autocomplete nick_ac = muc_get_roster_ac(win_current_get_recipient());
+        if (nick_ac != NULL) {
+            _parameter_autocomplete_with_ac(input, size, "/msg", nick_ac);
+            _parameter_autocomplete_with_ac(input, size, "/info", nick_ac);
+            _parameter_autocomplete_with_ac(input, size, "/status", nick_ac);
+        }
+    } else {
+        _parameter_autocomplete(input, size, "/msg",
+            contact_list_find_contact);
+        _parameter_autocomplete(input, size, "/info",
+            contact_list_find_contact);
+        _parameter_autocomplete(input, size, "/status",
+            contact_list_find_contact);
+    }
+
+    _parameter_autocomplete(input, size, "/connect",
+        accounts_find_enabled);
+    _parameter_autocomplete_with_ac(input, size, "/sub", sub_ac);
+    _parameter_autocomplete_with_ac(input, size, "/help", help_ac);
+    _parameter_autocomplete_with_ac(input, size, "/who", who_ac);
+    _parameter_autocomplete_with_ac(input, size, "/prefs", prefs_ac);
+    _parameter_autocomplete_with_ac(input, size, "/log", log_ac);
+
+    _notify_autocomplete(input, size);
+    _autoaway_autocomplete(input, size);
+    _titlebar_autocomplete(input, size);
+    _theme_autocomplete(input, size);
+    _account_autocomplete(input, size);
+}
+
+// The command functions
+
+static gboolean
+_cmd_connect(gchar **args, struct cmd_help_t help)
+{
+    gboolean result = FALSE;
+
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if ((conn_status != JABBER_DISCONNECTED) && (conn_status != JABBER_STARTED)) {
+        cons_show("You are either connected already, or a login is in process.");
+        result = TRUE;
+    } else {
+        char *user = args[0];
+        char *altdomain = args[1];
+        char *lower = g_utf8_strdown(user, -1);
+        char *jid;
+
+        status_bar_get_password();
+        status_bar_refresh();
+        char passwd[21];
+        inp_block();
+        inp_get_password(passwd);
+        inp_non_block();
+
+        ProfAccount *account = accounts_get_account(lower);
+        if (account != NULL) {
+            if (account->resource != NULL) {
+                jid = create_fulljid(account->jid, account->resource);
+            } else {
+                jid = strdup(account->jid);
+            }
+            cons_show("Connecting with account %s as %s", account->name, jid);
+            conn_status = jabber_connect_with_account(account, passwd);
+        } else {
+            jid = strdup(lower);
+            cons_show("Connecting as %s", jid);
+            conn_status = jabber_connect_with_details(jid, passwd, altdomain);
+        }
+
+        if (conn_status == JABBER_DISCONNECTED) {
+            cons_bad_show("Connection attempt for %s failed.", jid);
+            log_debug("Connection attempt for %s failed", jid);
+        }
+
+        accounts_free_account(account);
+        free(jid);
+
+        result = TRUE;
+    }
+
+    return result;
+}
+
+static gboolean
+_cmd_account(gchar **args, struct cmd_help_t help)
+{
+    char *command = args[0];
+
+    if (strcmp(command, "list") == 0) {
+        gchar **accounts = accounts_get_list();
+        int size = g_strv_length(accounts);
+
+        if (size > 0) {
+            cons_show("Accounts:");
+            int i = 0;
+            for (i = 0; i < size; i++) {
+                cons_show(accounts[i]);
+            }
+            cons_show("");
+        } else {
+            cons_show("No accounts created yet.");
+            cons_show("");
+        }
+    } else if (strcmp(command, "show") == 0) {
+        char *account_name = args[1];
+        if (account_name == NULL) {
+            cons_show("Usage: %s", help.usage);
+        } else {
+            ProfAccount *account = accounts_get_account(account_name);
+            if (account == NULL) {
+                cons_show("No such account.");
+                cons_show("");
+            } else {
+                cons_show_account(account);
+                accounts_free_account(account);
+            }
+        }
+    } else if (strcmp(command, "add") == 0) {
+        char *account_name = args[1];
+        if (account_name == NULL) {
+            cons_show("Usage: %s", help.usage);
+        } else {
+            accounts_add(account_name, NULL);
+            cons_show("Account created.");
+            cons_show("");
+        }
+    } else if (strcmp(command, "enable") == 0) {
+        char *account_name = args[1];
+        if (account_name == NULL) {
+            cons_show("Usage: %s", help.usage);
+        } else {
+            if (accounts_enable(account_name)) {
+                cons_show("Account enabled.");
+                cons_show("");
+            } else {
+                cons_show("No such account: %s", account_name);
+                cons_show("");
+            }
+        }
+    } else if (strcmp(command, "disable") == 0) {
+        char *account_name = args[1];
+        if (account_name == NULL) {
+            cons_show("Usage: %s", help.usage);
+        } else {
+            if (accounts_disable(account_name)) {
+                cons_show("Account disabled.");
+                cons_show("");
+            } else {
+                cons_show("No such account: %s", account_name);
+                cons_show("");
+            }
+        }
+    } else if (strcmp(command, "rename") == 0) {
+        if (g_strv_length(args) != 3) {
+            cons_show("Usage: %s", help.usage);
+        } else {
+            char *account_name = args[1];
+            char *new_name = args[2];
+
+            if (accounts_rename(account_name, new_name)) {
+                cons_show("Account renamed.");
+                cons_show("");
+            } else {
+                cons_show("Either account %s doesn't exist, or account %s already exists.", account_name, new_name);
+                cons_show("");
+            }
+        }
+    } else if (strcmp(command, "set") == 0) {
+        if (g_strv_length(args) != 4) {
+            cons_show("Usage: %s", help.usage);
+        } else {
+            char *account_name = args[1];
+            char *property = args[2];
+            char *value = args[3];
+
+            if (!accounts_account_exists(account_name)) {
+                cons_show("Account %s doesn't exist");
+                cons_show("");
+            } else {
+                if (strcmp(property, "jid") == 0) {
+                    Jid *jid = jid_create(args[3]);
+                    if (jid == NULL) {
+                        cons_show("Malformed jid: %s", value);
+                    } else {
+                        accounts_set_jid(account_name, jid->barejid);
+                        cons_show("Updated jid for account %s: %s", account_name, jid->barejid);
+                        if (jid->resourcepart != NULL) {
+                            accounts_set_resource(account_name, jid->resourcepart);
+                            cons_show("Updated resource for account %s: %s", account_name, jid->resourcepart);
+                        }
+                        cons_show("");
+                    }
+                    jid_destroy(jid);
+                } else if (strcmp(property, "server") == 0) {
+                    accounts_set_server(account_name, value);
+                    cons_show("Updated server for account %s: %s", account_name, value);
+                    cons_show("");
+                } else if (strcmp(property, "resource") == 0) {
+                    accounts_set_resource(account_name, value);
+                    cons_show("Updated resource for account %s: %s", account_name, value);
+                    cons_show("");
+                } else if (strcmp(property, "status") == 0) {
+                    if (!presence_valid_string(value) && (strcmp(value, "last") != 0)) {
+                        cons_show("Invalud status: %s", value);
+                    } else {
+                        accounts_set_login_presence(account_name, value);
+                        cons_show("Updated login status for account %s: %s", account_name, value);
+                    }
+                    cons_show("");
+                } else if (presence_valid_string(property)) {
+                        int intval;
+
+                        if (_strtoi(value, &intval, -128, 127) == 0) {
+                            jabber_presence_t presence_type = _presence_type_from_string(property);
+                            switch (presence_type)
+                            {
+                                case (PRESENCE_ONLINE):
+                                    accounts_set_priority_online(account_name, intval);
+                                    break;
+                                case (PRESENCE_CHAT):
+                                    accounts_set_priority_chat(account_name, intval);
+                                    break;
+                                case (PRESENCE_AWAY):
+                                    accounts_set_priority_away(account_name, intval);
+                                    break;
+                                case (PRESENCE_XA):
+                                    accounts_set_priority_xa(account_name, intval);
+                                    break;
+                                case (PRESENCE_DND):
+                                    accounts_set_priority_dnd(account_name, intval);
+                                    break;
+                                default:
+                                    accounts_set_priority_online(account_name, intval);
+                                    break;
+                            }
+                            jabber_conn_status_t conn_status = jabber_get_connection_status();
+                            if (conn_status == JABBER_CONNECTED && presence_type == jabber_get_presence_type()) {
+                                presence_update(jabber_get_presence_type(), jabber_get_presence_message(), 0);
+                            }
+                            cons_show("Updated %s priority for account %s: %s", property, account_name, value);
+                            cons_show("");
+                        }
+                } else {
+                    cons_show("Invalid property: %s", property);
+                    cons_show("");
+                }
+            }
+        }
+    } else {
+        cons_show("");
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_sub(gchar **args, struct cmd_help_t help)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are currently not connected.");
+        return TRUE;
+    }
+
+    char *subcmd, *jid, *bare_jid;
+    subcmd = args[0];
+    jid = args[1];
+
+    if (subcmd == NULL) {
+        cons_show("Usage: %s", help.usage);
+        return TRUE;
+    }
+
+    if (strcmp(subcmd, "sent") == 0) {
+        if (contact_list_has_pending_subscriptions()) {
+            cons_show("Awaiting subscription responses from:");
+            GSList *contacts = get_contact_list();
+            while (contacts != NULL) {
+                PContact contact = (PContact) contacts->data;
+                if (p_contact_pending_out(contact)) {
+                    cons_show(p_contact_jid(contact));
+                }
+                contacts = g_slist_next(contacts);
+            }
+        } else {
+            cons_show("No pending requests sent.");
+        }
+
+        return TRUE;
+    }
+
+    if (strcmp(subcmd, "received") == 0) {
+        GList *received = presence_get_subscription_requests();
+
+        if (received == NULL) {
+            cons_show("No outstanding subscription requests.");
+        } else {
+            cons_show("Outstanding subscription requests from:",
+                g_list_length(received));
+            while (received != NULL) {
+                cons_show(received->data);
+                received = g_list_next(received);
+            }
+        }
+
+        return TRUE;
+    }
+
+    if (!win_current_is_chat() && (jid == NULL)) {
+        cons_show("You must specify a contact.");
+        return TRUE;
+    }
+
+    if (jid != NULL) {
+        jid = strdup(jid);
+    } else {
+        jid = win_current_get_recipient();
+    }
+
+    bare_jid = strtok(jid, "/");
+
+    if (strcmp(subcmd, "allow") == 0) {
+        presence_subscription(bare_jid, PRESENCE_SUBSCRIBED);
+        cons_show("Accepted subscription for %s", bare_jid);
+        log_info("Accepted subscription for %s", bare_jid);
+    } else if (strcmp(subcmd, "deny") == 0) {
+        presence_subscription(bare_jid, PRESENCE_UNSUBSCRIBED);
+        cons_show("Deleted/denied subscription for %s", bare_jid);
+        log_info("Deleted/denied subscription for %s", bare_jid);
+    } else if (strcmp(subcmd, "request") == 0) {
+        presence_subscription(bare_jid, PRESENCE_SUBSCRIBE);
+        cons_show("Sent subscription request to %s.", bare_jid);
+        log_info("Sent subscription request to %s.", bare_jid);
+    } else if (strcmp(subcmd, "show") == 0) {
+        PContact contact = contact_list_get_contact(bare_jid);
+        if ((contact == NULL) || (p_contact_subscription(contact) == NULL)) {
+            if (win_current_is_chat()) {
+                win_current_show("No subscription information for %s.", bare_jid);
+            } else {
+                cons_show("No subscription information for %s.", bare_jid);
+            }
+        } else {
+            if (win_current_is_chat()) {
+                if (p_contact_pending_out(contact)) {
+                    win_current_show("%s subscription status: %s, request pending.",
+                        bare_jid, p_contact_subscription(contact));
+                } else {
+                    win_current_show("%s subscription status: %s.", bare_jid,
+                        p_contact_subscription(contact));
+                }
+            } else {
+                if (p_contact_pending_out(contact)) {
+                    cons_show("%s subscription status: %s, request pending.",
+                        bare_jid, p_contact_subscription(contact));
+                } else {
+                    cons_show("%s subscription status: %s.", bare_jid,
+                        p_contact_subscription(contact));
+                }
+            }
+        }
+    } else {
+        cons_show("Usage: %s", help.usage);
+    }
+
+    free(jid);
+    return TRUE;
+}
+
+static gboolean
+_cmd_disconnect(gchar **args, struct cmd_help_t help)
+{
+    if (jabber_get_connection_status() == JABBER_CONNECTED) {
+        char *jid = strdup(jabber_get_jid());
+        prof_handle_disconnect(jid);
+        free(jid);
+    } else {
+        cons_show("You are not currently connected.");
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_quit(gchar **args, struct cmd_help_t help)
+{
+    log_info("Profanity is shutting down...");
+    exit(0);
+    return FALSE;
+}
+
+static gboolean
+_cmd_wins(gchar **args, struct cmd_help_t help)
+{
+    cons_show_wins();
+    return TRUE;
+}
+
+static gboolean
+_cmd_help(gchar **args, struct cmd_help_t help)
+{
+    int num_args = g_strv_length(args);
+    if (num_args == 0) {
+        cons_help();
+    } else if (strcmp(args[0], "list") == 0) {
+        cons_show("");
+        cons_show("Basic commands:");
+        cons_show_time();
+        unsigned int i;
+        for (i = 0; i < ARRAY_SIZE(main_commands); i++) {
+            cons_show_word( (main_commands+i)->cmd );
+            if (i < ARRAY_SIZE(main_commands) - 1) {
+                cons_show_word(", ");
+            }
+        }
+        cons_show_word("\n");
+        cons_show("Presence commands:");
+        cons_show_time();
+        for (i = 0; i < ARRAY_SIZE(presence_commands); i++) {
+            cons_show_word( (presence_commands+i)->cmd );
+            if (i < ARRAY_SIZE(presence_commands) - 1) {
+                cons_show_word(", ");
+            }
+        }
+        cons_show_word("\n");
+        cons_show("Settings commands:");
+        cons_show_time();
+        for (i = 0; i < ARRAY_SIZE(setting_commands); i++) {
+            cons_show_word( (setting_commands+i)->cmd );
+            if (i < ARRAY_SIZE(setting_commands) - 1) {
+                cons_show_word(", ");
+            }
+        }
+        cons_show_word("\n");
+    } else if (strcmp(args[0], "basic") == 0) {
+        cons_basic_help();
+    } else if (strcmp(args[0], "presence") == 0) {
+        cons_presence_help();
+    } else if (strcmp(args[0], "settings") == 0) {
+        cons_settings_help();
+    } else if (strcmp(args[0], "navigation") == 0) {
+        cons_navigation_help();
+    } else {
+        char *cmd = args[0];
+        char cmd_with_slash[1 + strlen(cmd) + 1];
+        sprintf(cmd_with_slash, "/%s", cmd);
+
+        const gchar **help_text = NULL;
+        struct cmd_t *command = _cmd_get_command(cmd_with_slash);
+
+        if (command != NULL) {
+            help_text = command->help.long_help;
+        }
+
+        cons_show("");
+
+        if (help_text != NULL) {
+            int i;
+            for (i = 0; help_text[i] != NULL; i++) {
+                cons_show(help_text[i]);
+            }
+        } else {
+            cons_show("No such command.");
+        }
+
+        cons_show("");
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_about(gchar **args, struct cmd_help_t help)
+{
+    cons_show("");
+    cons_about();
+    return TRUE;
+}
+
+static gboolean
+_cmd_prefs(gchar **args, struct cmd_help_t help)
+{
+    if (args[0] == NULL) {
+        cons_prefs();
+    } else if (strcmp(args[0], "ui") == 0) {
+        cons_show("");
+        cons_show_ui_prefs();
+        cons_show("");
+    } else if (strcmp(args[0], "desktop") == 0) {
+        cons_show("");
+        cons_show_desktop_prefs();
+        cons_show("");
+    } else if (strcmp(args[0], "chat") == 0) {
+        cons_show("");
+        cons_show_chat_prefs();
+        cons_show("");
+    } else if (strcmp(args[0], "log") == 0) {
+        cons_show("");
+        cons_show_log_prefs();
+        cons_show("");
+    } else if (strcmp(args[0], "conn") == 0) {
+        cons_show("");
+        cons_show_connection_prefs();
+        cons_show("");
+    } else if (strcmp(args[0], "presence") == 0) {
+        cons_show("");
+        cons_show_presence_prefs();
+        cons_show("");
+    } else {
+        cons_show("Usage: %s", help.usage);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_theme(gchar **args, struct cmd_help_t help)
+{
+    // list themes
+    if (strcmp(args[0], "list") == 0) {
+        GSList *themes = theme_list();
+        cons_show_themes(themes);
+        g_slist_free_full(themes, g_free);
+
+    // load a theme
+    } else if (strcmp(args[0], "set") == 0) {
+        if (args[1] == NULL) {
+            cons_show("Usage: %s", help.usage);
+        } else if (theme_load(args[1])) {
+            ui_load_colours();
+            prefs_set_theme(args[1]);
+            cons_show("Loaded theme: %s", args[1]);
+        } else {
+            cons_show("Couldn't find theme: %s", args[1]);
+        }
+    } else {
+        cons_show("Usage: %s", help.usage);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_who(gchar **args, struct cmd_help_t help)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+    } else {
+        char *presence = args[0];
+
+        // bad arg
+        if ((presence != NULL)
+                && (strcmp(presence, "online") != 0)
+                && (strcmp(presence, "available") != 0)
+                && (strcmp(presence, "unavailable") != 0)
+                && (strcmp(presence, "offline") != 0)
+                && (strcmp(presence, "away") != 0)
+                && (strcmp(presence, "chat") != 0)
+                && (strcmp(presence, "xa") != 0)
+                && (strcmp(presence, "dnd") != 0)) {
+            cons_show("Usage: %s", help.usage);
+
+        // valid arg
+        } else {
+            if (win_current_is_groupchat()) {
+                char *room = win_current_get_recipient();
+                GList *list = muc_get_roster(room);
+
+                // no arg, show all contacts
+                if (presence == NULL) {
+                    win_show_room_roster(room, list, NULL);
+
+                // available
+                } else if (strcmp("available", presence) == 0) {
+                    GList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        const char * const contact_presence = (p_contact_presence(contact));
+                        if ((strcmp(contact_presence, "online") == 0)
+                                || (strcmp(contact_presence, "chat") == 0)) {
+                            filtered = g_list_append(filtered, contact);
+                        }
+                        list = g_list_next(list);
+                    }
+
+                    win_show_room_roster(room, filtered, "available");
+
+                // unavailable
+                } else if (strcmp("unavailable", presence) == 0) {
+                    GList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        const char * const contact_presence = (p_contact_presence(contact));
+                        if ((strcmp(contact_presence, "offline") == 0)
+                                || (strcmp(contact_presence, "away") == 0)
+                                || (strcmp(contact_presence, "dnd") == 0)
+                                || (strcmp(contact_presence, "xa") == 0)) {
+                            filtered = g_list_append(filtered, contact);
+                        }
+                        list = g_list_next(list);
+                    }
+
+                    win_show_room_roster(room, filtered, "unavailable");
+
+                // online, show all status that indicate online
+                } else if (strcmp("online", presence) == 0) {
+                    GList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        const char * const contact_presence = (p_contact_presence(contact));
+                        if ((strcmp(contact_presence, "online") == 0)
+                                || (strcmp(contact_presence, "away") == 0)
+                                || (strcmp(contact_presence, "dnd") == 0)
+                                || (strcmp(contact_presence, "xa") == 0)
+                                || (strcmp(contact_presence, "chat") == 0)) {
+                            filtered = g_list_append(filtered, contact);
+                        }
+                        list = g_list_next(list);
+                    }
+
+                    win_show_room_roster(room, filtered, "online");
+
+                // show specific status
+                } else {
+                    GList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        if (strcmp(p_contact_presence(contact), presence) == 0) {
+                            filtered = g_list_append(filtered, contact);
+                        }
+                        list = g_list_next(list);
+                    }
+
+                    win_show_room_roster(room, filtered, presence);
+                }
+
+            // not in groupchat window
+            } else {
+                cons_show("");
+                GSList *list = get_contact_list();
+
+                // no arg, show all contacts
+                if (presence == NULL) {
+                    cons_show("All contacts:");
+                    cons_show_contacts(list);
+
+                // available
+                } else if (strcmp("available", presence) == 0) {
+                    cons_show("Contacts (%s):", presence);
+                    GSList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        const char * const contact_presence = (p_contact_presence(contact));
+                        if ((strcmp(contact_presence, "online") == 0)
+                                || (strcmp(contact_presence, "chat") == 0)) {
+                            filtered = g_slist_append(filtered, contact);
+                        }
+                        list = g_slist_next(list);
+                    }
+
+                    cons_show_contacts(filtered);
+
+                // unavailable
+                } else if (strcmp("unavailable", presence) == 0) {
+                    cons_show("Contacts (%s):", presence);
+                    GSList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        const char * const contact_presence = (p_contact_presence(contact));
+                        if ((strcmp(contact_presence, "offline") == 0)
+                                || (strcmp(contact_presence, "away") == 0)
+                                || (strcmp(contact_presence, "dnd") == 0)
+                                || (strcmp(contact_presence, "xa") == 0)) {
+                            filtered = g_slist_append(filtered, contact);
+                        }
+                        list = g_slist_next(list);
+                    }
+
+                    cons_show_contacts(filtered);
+
+                // online, show all status that indicate online
+                } else if (strcmp("online", presence) == 0) {
+                    cons_show("Contacts (%s):", presence);
+                    GSList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        const char * const contact_presence = (p_contact_presence(contact));
+                        if ((strcmp(contact_presence, "online") == 0)
+                                || (strcmp(contact_presence, "away") == 0)
+                                || (strcmp(contact_presence, "dnd") == 0)
+                                || (strcmp(contact_presence, "xa") == 0)
+                                || (strcmp(contact_presence, "chat") == 0)) {
+                            filtered = g_slist_append(filtered, contact);
+                        }
+                        list = g_slist_next(list);
+                    }
+
+                    cons_show_contacts(filtered);
+
+                // show specific status
+                } else {
+                    cons_show("Contacts (%s):", presence);
+                    GSList *filtered = NULL;
+
+                    while (list != NULL) {
+                        PContact contact = list->data;
+                        if (strcmp(p_contact_presence(contact), presence) == 0) {
+                            filtered = g_slist_append(filtered, contact);
+                        }
+                        list = g_slist_next(list);
+                    }
+
+                    cons_show_contacts(filtered);
+                }
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_msg(gchar **args, struct cmd_help_t help)
+{
+    char *usr = args[0];
+    char *msg = args[1];
+
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    if (ui_windows_full()) {
+        cons_bad_show("Windows all used, close a window and try again.");
+        return TRUE;
+    }
+
+    if (win_current_is_groupchat()) {
+        char *room_name = win_current_get_recipient();
+        if (muc_nick_in_roster(room_name, usr)) {
+            GString *full_jid = g_string_new(room_name);
+            g_string_append(full_jid, "/");
+            g_string_append(full_jid, usr);
+
+            if (msg != NULL) {
+                message_send(msg, full_jid->str);
+                win_show_outgoing_msg("me", full_jid->str, msg);
+            } else {
+                win_new_chat_win(full_jid->str);
+            }
+
+            g_string_free(full_jid, TRUE);
+
+        } else {
+            win_current_show("No such participant \"%s\" in room.", usr);
+        }
+
+        return TRUE;
+
+    } else {
+        if (msg != NULL) {
+            message_send(msg, usr);
+            win_show_outgoing_msg("me", usr, msg);
+
+            if (win_current_is_chat() && prefs_get_chlog()) {
+                const char *jid = jabber_get_jid();
+                Jid *jidp = jid_create(jid);
+                chat_log_chat(jidp->barejid, usr, msg, PROF_OUT_LOG, NULL);
+                jid_destroy(jidp);
+            }
+
+            return TRUE;
+        } else {
+            win_new_chat_win(usr);
+            return TRUE;
+        }
+    }
+}
+
+static gboolean
+_cmd_status(gchar **args, struct cmd_help_t help)
+{
+    char *usr = args[0];
+
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+    } else {
+        if (win_current_is_groupchat()) {
+            if (usr != NULL) {
+                win_room_show_status(usr);
+            } else {
+                win_current_show("You must specify a nickname.");
+            }
+        } else if (win_current_is_chat()) {
+            if (usr != NULL) {
+                win_current_show("No parameter required when in chat.");
+            } else {
+                win_show_status();
+            }
+        } else if (win_current_is_private()) {
+            if (usr != NULL) {
+                win_current_show("No parameter required when in chat.");
+            } else {
+                win_private_show_status();
+            }
+        } else {
+            if (usr != NULL) {
+                cons_show_status(usr);
+            } else {
+                cons_show("Usage: %s", help.usage);
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_info(gchar **args, struct cmd_help_t help)
+{
+    char *usr = args[0];
+
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+    } else {
+        if (win_current_is_groupchat()) {
+            if (usr != NULL) {
+                PContact pcontact = muc_get_participant(win_current_get_recipient(), usr);
+                if (pcontact != NULL) {
+                    cons_show_info(pcontact);
+                } else {
+                    cons_show("No such participant \"%s\" in room.", usr);
+                }
+            } else {
+                cons_show("No nickname supplied to /info in chat room.");
+            }
+
+        } else if (win_current_is_chat()) {
+            if (usr != NULL) {
+                cons_show("No parameter required for /info in chat.");
+            } else {
+                PContact pcontact = contact_list_get_contact(win_current_get_recipient());
+                if (pcontact != NULL) {
+                    cons_show_info(pcontact);
+                } else {
+                    cons_show("No such contact \"%s\" in roster.", win_current_get_recipient());
+                }
+            }
+
+        } else if (win_current_is_private()) {
+            if (usr != NULL) {
+                win_current_show("No parameter required when in chat.");
+            } else {
+                Jid *jid = jid_create(win_current_get_recipient());
+                PContact pcontact = muc_get_participant(jid->barejid, jid->resourcepart);
+                if (pcontact != NULL) {
+                    cons_show_info(pcontact);
+                } else {
+                    cons_show("No such participant \"%s\" in room.", jid->resourcepart);
+                }
+                jid_destroy(jid);
+            }
+        } else {
+            if (usr != NULL) {
+                PContact pcontact = contact_list_get_contact(usr);
+                if (pcontact != NULL) {
+                    cons_show_info(pcontact);
+                } else {
+                    cons_show("No such contact \"%s\" in roster.", usr);
+                }
+            } else {
+                cons_show("Usage: %s", help.usage);
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_join(gchar **args, struct cmd_help_t help)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    if (ui_windows_full()) {
+        cons_bad_show("Windows all used, close a window and try again.");
+        return TRUE;
+    }
+
+    char *room = args[0];
+    char *nick = NULL;
+
+    int num_args = g_strv_length(args);
+    if (num_args == 2) {
+        nick = args[1];
+    } else {
+        Jid *jid = jid_create(jabber_get_jid());
+        nick = strdup(jid->localpart);
+        jid_destroy(jid);
+    }
+
+    Jid *room_jid = jid_create_from_bare_and_resource(room, nick);
+
+    if (!muc_room_is_active(room_jid)) {
+        presence_join_room(room_jid);
+    }
+    win_join_chat(room_jid);
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_nick(gchar **args, struct cmd_help_t help)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+    if (!win_current_is_groupchat()) {
+        cons_show("You can only change your nickname in a chat room window.");
+        return TRUE;
+    }
+
+    char *room = win_current_get_recipient();
+    char *nick = args[0];
+    presence_change_room_nick(room, nick);
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_tiny(gchar **args, struct cmd_help_t help)
+{
+    char *url = args[0];
+
+    if (!tinyurl_valid(url)) {
+        GString *error = g_string_new("/tiny, badly formed URL: ");
+        g_string_append(error, url);
+        cons_bad_show(error->str);
+        if (!win_current_is_console()) {
+            win_current_bad_show(error->str);
+        }
+        g_string_free(error, TRUE);
+    } else if (!win_current_is_console()) {
+        char *tiny = tinyurl_get(url);
+
+        if (tiny != NULL) {
+            if (win_current_is_chat()) {
+                char *recipient = win_current_get_recipient();
+                message_send(tiny, recipient);
+
+                if (prefs_get_chlog()) {
+                    const char *jid = jabber_get_jid();
+                    Jid *jidp = jid_create(jid);
+                    chat_log_chat(jidp->barejid, recipient, tiny, PROF_OUT_LOG, NULL);
+                    jid_destroy(jidp);
+                }
+
+                win_show_outgoing_msg("me", recipient, tiny);
+                free(recipient);
+            } else if (win_current_is_private()) {
+                char *recipient = win_current_get_recipient();
+                message_send(tiny, recipient);
+                win_show_outgoing_msg("me", recipient, tiny);
+                free(recipient);
+            } else { // groupchat
+                char *recipient = win_current_get_recipient();
+                message_send_groupchat(tiny, recipient);
+                free(recipient);
+            }
+            free(tiny);
+        } else {
+            cons_bad_show("Couldn't get tinyurl.");
+        }
+    } else {
+        cons_show("/tiny can only be used in chat windows");
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_close(gchar **args, struct cmd_help_t help)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    // cannot close console window
+    if (win_current_is_console()) {
+        cons_show("Cannot close console window.");
+        return TRUE;
+    }
+
+    // handle leaving rooms, or chat
+    if (conn_status == JABBER_CONNECTED) {
+        if (win_current_is_groupchat()) {
+            char *room_jid = win_current_get_recipient();
+            presence_leave_chat_room(room_jid);
+        } else if (win_current_is_chat() || win_current_is_private()) {
+
+            if (prefs_get_states()) {
+                char *recipient = win_current_get_recipient();
+
+                // send <gone/> chat state before closing
+                if (chat_session_get_recipient_supports(recipient)) {
+                    chat_session_set_gone(recipient);
+                    message_send_gone(recipient);
+                    chat_session_end(recipient);
+                }
+            }
+        }
+    }
+
+    // close the window
+    win_current_close();
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_set_beep(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help, "Sound", prefs_set_beep);
+}
+
+static gboolean
+_cmd_set_states(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help, "Sending chat states",
+        prefs_set_states);
+}
+
+static gboolean
+_cmd_set_titlebar(gchar **args, struct cmd_help_t help)
+{
+    if (strcmp(args[0], "version") != 0) {
+        cons_show("Usage: %s", help.usage);
+        return TRUE;
+    } else {
+        return _cmd_set_boolean_preference(args[1], help,
+        "Show version in window title", prefs_set_titlebarversion);
+    }
+}
+
+static gboolean
+_cmd_set_outtype(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Sending typing notifications", prefs_set_outtype);
+}
+
+static gboolean
+_cmd_set_gone(gchar **args, struct cmd_help_t help)
+{
+    char *value = args[0];
+
+    gint period = atoi(value);
+    prefs_set_gone(period);
+    if (period == 0) {
+        cons_show("Automatic leaving conversations after period disabled.");
+    } else if (period == 1) {
+        cons_show("Leaving conversations after 1 minute of inactivity.");
+    } else {
+        cons_show("Leaving conversations after %d minutes of inactivity.", period);
+    }
+
+    return TRUE;
+}
+
+
+static gboolean
+_cmd_set_notify(gchar **args, struct cmd_help_t help)
+{
+    char *kind = args[0];
+    char *value = args[1];
+
+    // bad kind
+    if ((strcmp(kind, "message") != 0) && (strcmp(kind, "typing") != 0) &&
+            (strcmp(kind, "remind") != 0)) {
+        cons_show("Usage: %s", help.usage);
+
+    // set message setting
+    } else if (strcmp(kind, "message") == 0) {
+        if (strcmp(value, "on") == 0) {
+            cons_show("Message notifications enabled.");
+            prefs_set_notify_message(TRUE);
+        } else if (strcmp(value, "off") == 0) {
+            cons_show("Message notifications disabled.");
+            prefs_set_notify_message(FALSE);
+        } else {
+            cons_show("Usage: /notify message on|off");
+        }
+
+    // set typing setting
+    } else if (strcmp(kind, "typing") == 0) {
+        if (strcmp(value, "on") == 0) {
+            cons_show("Typing notifications enabled.");
+            prefs_set_notify_typing(TRUE);
+        } else if (strcmp(value, "off") == 0) {
+            cons_show("Typing notifications disabled.");
+            prefs_set_notify_typing(FALSE);
+        } else {
+            cons_show("Usage: /notify typing on|off");
+        }
+
+    // set remind setting
+    } else if (strcmp(kind, "remind") == 0) {
+        gint period = atoi(value);
+        prefs_set_notify_remind(period);
+        if (period == 0) {
+            cons_show("Message reminders disabled.");
+        } else if (period == 1) {
+            cons_show("Message reminder period set to 1 second.");
+        } else {
+            cons_show("Message reminder period set to %d seconds.", period);
+        }
+
+    } else {
+        cons_show("Unknown command: %s.", kind);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_set_log(gchar **args, struct cmd_help_t help)
+{
+    char *subcmd = args[0];
+    char *value = args[1];
+    int intval;
+
+    if (strcmp(subcmd, "maxsize") == 0) {
+        if (_strtoi(value, &intval, PREFS_MIN_LOG_SIZE, INT_MAX) == 0) {
+            prefs_set_max_log_size(intval);
+            cons_show("Log maxinum size set to %d bytes", intval);
+        }
+    } else {
+        cons_show("Usage: %s", help.usage);
+    }
+
+    /* TODO: make 'level' subcommand for debug level */
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_set_reconnect(gchar **args, struct cmd_help_t help)
+{
+    char *value = args[0];
+    int intval;
+
+    if (_strtoi(value, &intval, 0, INT_MAX) == 0) {
+        prefs_set_reconnect(intval);
+        if (intval == 0) {
+            cons_show("Reconnect disabled.", intval);
+        } else {
+            cons_show("Reconnect interval set to %d seconds.", intval);
+        }
+    } else {
+        cons_show("Usage: %s", help.usage);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_set_autoping(gchar **args, struct cmd_help_t help)
+{
+    char *value = args[0];
+    int intval;
+
+    if (_strtoi(value, &intval, 0, INT_MAX) == 0) {
+        prefs_set_autoping(intval);
+        jabber_set_autoping(intval);
+        if (intval == 0) {
+            cons_show("Autoping disabled.", intval);
+        } else {
+            cons_show("Autoping interval set to %d seconds.", intval);
+        }
+    } else {
+        cons_show("Usage: %s", help.usage);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_set_autoaway(gchar **args, struct cmd_help_t help)
+{
+    char *setting = args[0];
+    char *value = args[1];
+    int minutesval;
+
+    if ((strcmp(setting, "mode") != 0) && (strcmp(setting, "time") != 0) &&
+            (strcmp(setting, "message") != 0) && (strcmp(setting, "check") != 0)) {
+        cons_show("Setting must be one of 'mode', 'time', 'message' or 'check'");
+        return TRUE;
+    }
+
+    if (strcmp(setting, "mode") == 0) {
+        if ((strcmp(value, "idle") != 0) && (strcmp(value, "away") != 0) &&
+                (strcmp(value, "off") != 0)) {
+            cons_show("Mode must be one of 'idle', 'away' or 'off'");
+        } else {
+            prefs_set_autoaway_mode(value);
+            cons_show("Auto away mode set to: %s.", value);
+        }
+
+        return TRUE;
+    }
+
+    if (strcmp(setting, "time") == 0) {
+        if (_strtoi(value, &minutesval, 1, INT_MAX) == 0) {
+            prefs_set_autoaway_time(minutesval);
+            cons_show("Auto away time set to: %d minutes.", minutesval);
+        }
+
+        return TRUE;
+    }
+
+    if (strcmp(setting, "message") == 0) {
+        if (strcmp(value, "off") == 0) {
+            prefs_set_autoaway_message(NULL);
+            cons_show("Auto away message cleared.");
+        } else {
+            prefs_set_autoaway_message(value);
+            cons_show("Auto away message set to: \"%s\".", value);
+        }
+
+        return TRUE;
+    }
+
+    if (strcmp(setting, "check") == 0) {
+        return _cmd_set_boolean_preference(value, help, "Online check",
+            prefs_set_autoaway_check);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_set_priority(gchar **args, struct cmd_help_t help)
+{
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    char *value = args[0];
+    int intval;
+
+    if (_strtoi(value, &intval, -128, 127) == 0) {
+        accounts_set_priority_all(jabber_get_account_name(), intval);
+        //prefs_set_priority((int)intval);
+        // update presence with new priority
+        presence_update(jabber_get_presence_type(), jabber_get_presence_message(), 0);
+        cons_show("Priority set to %d.", intval);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_cmd_set_statuses(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Status notifications", prefs_set_statuses);
+}
+
+static gboolean
+_cmd_vercheck(gchar **args, struct cmd_help_t help)
+{
+    int num_args = g_strv_length(args);
+
+    if (num_args == 0) {
+        cons_check_version(TRUE);
+        return TRUE;
+    } else {
+        return _cmd_set_boolean_preference(args[0], help,
+            "Version checking", prefs_set_vercheck);
+    }
+}
+
+static gboolean
+_cmd_set_flash(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Screen flash", prefs_set_flash);
+}
+
+static gboolean
+_cmd_set_intype(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Show contact typing", prefs_set_intype);
+}
+
+static gboolean
+_cmd_set_splash(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Splash screen", prefs_set_splash);
+}
+
+static gboolean
+_cmd_set_chlog(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Chat logging", prefs_set_chlog);
+}
+
+static gboolean
+_cmd_set_mouse(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Mouse handling", prefs_set_mouse);
+}
+
+static gboolean
+_cmd_set_history(gchar **args, struct cmd_help_t help)
+{
+    return _cmd_set_boolean_preference(args[0], help,
+        "Chat history", prefs_set_history);
+}
+
+static gboolean
+_cmd_away(gchar **args, struct cmd_help_t help)
+{
+    _update_presence(PRESENCE_AWAY, "away", args);
+    return TRUE;
+}
+
+static gboolean
+_cmd_online(gchar **args, struct cmd_help_t help)
+{
+    _update_presence(PRESENCE_ONLINE, "online", args);
+    return TRUE;
+}
+
+static gboolean
+_cmd_dnd(gchar **args, struct cmd_help_t help)
+{
+    _update_presence(PRESENCE_DND, "dnd", args);
+    return TRUE;
+}
+
+static gboolean
+_cmd_chat(gchar **args, struct cmd_help_t help)
+{
+    _update_presence(PRESENCE_CHAT, "chat", args);
+    return TRUE;
+}
+
+static gboolean
+_cmd_xa(gchar **args, struct cmd_help_t help)
+{
+    _update_presence(PRESENCE_XA, "xa", args);
+    return TRUE;
+}
+
+// helper function for status change commands
+
+static void
+_update_presence(const jabber_presence_t presence,
+    const char * const show, gchar **args)
+{
+    char *msg = NULL;
+    int num_args = g_strv_length(args);
+    if (num_args == 1) {
+        msg = args[0];
+    }
+
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+    } else {
+        presence_update(presence, msg, 0);
+        title_bar_set_status(presence);
+        gint priority = accounts_get_priority_for_presence_type(jabber_get_account_name(), presence);
+        if (msg != NULL) {
+            cons_show("Status set to %s (priority %d), \"%s\".", show, priority, msg);
+        } else {
+            cons_show("Status set to %s (priority %d).", show, priority);
+        }
+    }
+
+}
+
+// helper function for boolean preference commands
+
+static gboolean
+_cmd_set_boolean_preference(gchar *arg, struct cmd_help_t help,
+    const char * const display,
+    void (*set_func)(gboolean))
+{
+    GString *enabled = g_string_new(display);
+    g_string_append(enabled, " enabled.");
+
+    GString *disabled = g_string_new(display);
+    g_string_append(disabled, " disabled.");
+
+    if (strcmp(arg, "on") == 0) {
+        cons_show(enabled->str);
+        set_func(TRUE);
+    } else if (strcmp(arg, "off") == 0) {
+        cons_show(disabled->str);
+        set_func(FALSE);
+    } else {
+        char usage[strlen(help.usage) + 8];
+        sprintf(usage, "Usage: %s", help.usage);
+        cons_show(usage);
+    }
+
+    g_string_free(enabled, TRUE);
+    g_string_free(disabled, TRUE);
+
+    return TRUE;
+}
+
+// helper to get command by string
+
+static struct cmd_t *
+_cmd_get_command(const char * const command)
+{
+    unsigned int i;
+    for (i = 0; i < ARRAY_SIZE(main_commands); i++) {
+        struct cmd_t *pcmd = main_commands+i;
+        if (strcmp(pcmd->cmd, command) == 0) {
+            return pcmd;
+        }
+    }
+
+    for (i = 0; i < ARRAY_SIZE(setting_commands); i++) {
+        struct cmd_t *pcmd = setting_commands+i;
+        if (strcmp(pcmd->cmd, command) == 0) {
+            return pcmd;
+        }
+    }
+
+    for (i = 0; i < ARRAY_SIZE(presence_commands); i++) {
+        struct cmd_t *pcmd = presence_commands+i;
+        if (strcmp(pcmd->cmd, command) == 0) {
+            return pcmd;
+        }
+    }
+
+    return NULL;
+}
+
+static void
+_parameter_autocomplete(char *input, int *size, char *command,
+    autocomplete_func func)
+{
+    char *found = NULL;
+    char *auto_msg = NULL;
+    char inp_cpy[*size];
+    int i;
+    char *command_cpy = malloc(strlen(command) + 2);
+    sprintf(command_cpy, "%s ", command);
+    int len = strlen(command_cpy);
+    if ((strncmp(input, command_cpy, len) == 0) && (*size > len)) {
+        for(i = len; i < *size; i++) {
+            inp_cpy[i-len] = input[i];
+        }
+        inp_cpy[(*size) - len] = '\0';
+        found = func(inp_cpy);
+        if (found != NULL) {
+            auto_msg = (char *) malloc((len + (strlen(found) + 1)) * sizeof(char));
+            strcpy(auto_msg, command_cpy);
+            strcat(auto_msg, found);
+            inp_replace_input(input, auto_msg, size);
+            free(auto_msg);
+            free(found);
+        }
+    }
+    free(command_cpy);
+}
+
+static void
+_parameter_autocomplete_with_ac(char *input, int *size, char *command,
+    Autocomplete ac)
+{
+    char *found = NULL;
+    char *auto_msg = NULL;
+    char inp_cpy[*size];
+    int i;
+    char *command_cpy = malloc(strlen(command) + 2);
+    sprintf(command_cpy, "%s ", command);
+    int len = strlen(command_cpy);
+    if ((strncmp(input, command_cpy, len) == 0) && (*size > len)) {
+        for(i = len; i < *size; i++) {
+            inp_cpy[i-len] = input[i];
+        }
+        inp_cpy[(*size) - len] = '\0';
+        found = autocomplete_complete(ac, inp_cpy);
+        if (found != NULL) {
+            auto_msg = (char *) malloc((len + (strlen(found) + 1)) * sizeof(char));
+            strcpy(auto_msg, command_cpy);
+            strcat(auto_msg, found);
+            inp_replace_input(input, auto_msg, size);
+            free(auto_msg);
+            free(found);
+        }
+    }
+    free(command_cpy);
+}
+
+static void
+_notify_autocomplete(char *input, int *size)
+{
+    char *found = NULL;
+    char *auto_msg = NULL;
+    char inp_cpy[*size];
+    int i;
+
+    if ((strncmp(input, "/notify message ", 16) == 0) && (*size > 16)) {
+        for(i = 16; i < *size; i++) {
+            inp_cpy[i-16] = input[i];
+        }
+        inp_cpy[(*size) - 16] = '\0';
+        found = prefs_autocomplete_boolean_choice(inp_cpy);
+        if (found != NULL) {
+            auto_msg = (char *) malloc((16 + (strlen(found) + 1)) * sizeof(char));
+            strcpy(auto_msg, "/notify message ");
+            strcat(auto_msg, found);
+            inp_replace_input(input, auto_msg, size);
+            free(auto_msg);
+            free(found);
+        }
+    } else if ((strncmp(input, "/notify typing ", 15) == 0) && (*size > 15)) {
+        for(i = 15; i < *size; i++) {
+            inp_cpy[i-15] = input[i];
+        }
+        inp_cpy[(*size) - 15] = '\0';
+        found = prefs_autocomplete_boolean_choice(inp_cpy);
+        if (found != NULL) {
+            auto_msg = (char *) malloc((15 + (strlen(found) + 1)) * sizeof(char));
+            strcpy(auto_msg, "/notify typing ");
+            strcat(auto_msg, found);
+            inp_replace_input(input, auto_msg, size);
+            free(auto_msg);
+            free(found);
+        }
+    } else if ((strncmp(input, "/notify ", 8) == 0) && (*size > 8)) {
+        _parameter_autocomplete_with_ac(input, size, "/notify", notify_ac);
+    }
+}
+
+static void
+_titlebar_autocomplete(char *input, int *size)
+{
+    char *found = NULL;
+    char *auto_msg = NULL;
+    char inp_cpy[*size];
+    int i;
+
+    if ((strncmp(input, "/titlebar version ", 18) == 0) && (*size > 18)) {
+        for(i = 18; i < *size; i++) {
+            inp_cpy[i-18] = input[i];
+        }
+        inp_cpy[(*size) - 18] = '\0';
+        found = prefs_autocomplete_boolean_choice(inp_cpy);
+        if (found != NULL) {
+            auto_msg = (char *) malloc((18 + (strlen(found) + 1)) * sizeof(char));
+            strcpy(auto_msg, "/titlebar version ");
+            strcat(auto_msg, found);
+            inp_replace_input(input, auto_msg, size);
+            free(auto_msg);
+            free(found);
+        }
+    } else if ((strncmp(input, "/titlebar ", 10) == 0) && (*size > 10)) {
+        _parameter_autocomplete_with_ac(input, size, "/titlebar", titlebar_ac);
+    }
+}
+
+static void
+_autoaway_autocomplete(char *input, int *size)
+{
+    char *found = NULL;
+    char *auto_msg = NULL;
+    char inp_cpy[*size];
+    int i;
+
+    if ((strncmp(input, "/autoaway mode ", 15) == 0) && (*size > 15)) {
+        _parameter_autocomplete_with_ac(input, size, "/autoaway mode", autoaway_mode_ac);
+    } else if ((strncmp(input, "/autoaway check ", 16) == 0) && (*size > 16)) {
+        for(i = 16; i < *size; i++) {
+            inp_cpy[i-16] = input[i];
+        }
+        inp_cpy[(*size) - 16] = '\0';
+        found = prefs_autocomplete_boolean_choice(inp_cpy);
+        if (found != NULL) {
+            auto_msg = (char *) malloc((16 + (strlen(found) + 1)) * sizeof(char));
+            strcpy(auto_msg, "/autoaway check ");
+            strcat(auto_msg, found);
+            inp_replace_input(input, auto_msg, size);
+            free(auto_msg);
+            free(found);
+        }
+    } else if ((strncmp(input, "/autoaway ", 10) == 0) && (*size > 10)) {
+        _parameter_autocomplete_with_ac(input, size, "/autoaway", autoaway_ac);
+    }
+}
+
+static void
+_theme_autocomplete(char *input, int *size)
+{
+    if ((strncmp(input, "/theme set ", 11) == 0) && (*size > 11)) {
+        if (theme_load_ac == NULL) {
+            theme_load_ac = autocomplete_new();
+            GSList *themes = theme_list();
+            while (themes != NULL) {
+                autocomplete_add(theme_load_ac, strdup(themes->data));
+                themes = g_slist_next(themes);
+            }
+            g_slist_free(themes);
+            autocomplete_add(theme_load_ac, "default");
+         }
+        _parameter_autocomplete_with_ac(input, size, "/theme set", theme_load_ac);
+    } else if ((strncmp(input, "/theme ", 7) == 0) && (*size > 7)) {
+        _parameter_autocomplete_with_ac(input, size, "/theme", theme_ac);
+    }
+}
+
+static void
+_account_autocomplete(char *input, int *size)
+{
+    if ((strncmp(input, "/account set ", 13) == 0) && (*size > 13)) {
+        _parameter_autocomplete(input, size, "/account set", accounts_find_all);
+    } else if ((strncmp(input, "/account show ", 14) == 0) && (*size > 14)) {
+        _parameter_autocomplete(input, size, "/account show", accounts_find_all);
+    } else if ((strncmp(input, "/account enable ", 16) == 0) && (*size > 16)) {
+        _parameter_autocomplete(input, size, "/account enable", accounts_find_all);
+    } else if ((strncmp(input, "/account disable ", 17) == 0) && (*size > 17)) {
+        _parameter_autocomplete(input, size, "/account disable", accounts_find_all);
+    } else if ((strncmp(input, "/account rename ", 16) == 0) && (*size > 16)) {
+        _parameter_autocomplete(input, size, "/account rename", accounts_find_all);
+    } else if ((strncmp(input, "/account ", 9) == 0) && (*size > 9)) {
+        _parameter_autocomplete_with_ac(input, size, "/account", account_ac);
+    }
+}
+
+static int
+_strtoi(char *str, int *saveptr, int min, int max)
+{
+    char *ptr;
+    int val;
+
+    errno = 0;
+    val = (int)strtol(str, &ptr, 0);
+    if (*str == '\0' || *ptr != '\0') {
+        cons_show("Illegal character. Must be a number.");
+        return -1;
+    } else if (errno == ERANGE || val < min || val > max) {
+        cons_show("Value out of range. Must be in %d..%d.", min, max);
+        return -1;
+    }
+
+    *saveptr = val;
+
+    return 0;
+}
+
+static jabber_presence_t
+_presence_type_from_string(const char * const str)
+{
+    if (str == NULL) {
+        return PRESENCE_ONLINE;
+    } else if (!presence_valid_string(str)) {
+        return PRESENCE_ONLINE;
+    } else if (strcmp(str, "online") == 0) {
+        return PRESENCE_ONLINE;
+    } else if (strcmp(str, "chat") == 0) {
+        return PRESENCE_CHAT;
+    } else if (strcmp(str, "away") == 0) {
+        return PRESENCE_AWAY;
+    } else if (strcmp(str, "xa") == 0) {
+        return PRESENCE_XA;
+    } else {
+        return PRESENCE_DND;
+    }
+}
diff --git a/src/command/command.h b/src/command/command.h
new file mode 100644
index 00000000..00c18a9c
--- /dev/null
+++ b/src/command/command.h
@@ -0,0 +1,52 @@
+/*
+ * command.h
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef COMMAND_H
+#define COMMAND_H
+
+#include <glib.h>
+
+// Command help strings
+struct cmd_help_t {
+    const gchar *usage;
+    const gchar *short_help;
+    const gchar *long_help[50];
+};
+
+void cmd_init(void);
+void cmd_close(void);
+
+void cmd_autocomplete(char *input, int *size);
+void cmd_reset_autocomplete(void);
+
+gboolean cmd_execute(const char * const command, const char * const inp);
+gboolean cmd_execute_default(const char * const inp);
+
+GSList * cmd_get_basic_help(void);
+GSList * cmd_get_settings_help(void);
+GSList * cmd_get_presence_help(void);
+
+void cmd_history_append(char *inp);
+char *cmd_history_previous(char *inp, int *size);
+char *cmd_history_next(char *inp, int *size);
+
+#endif
diff --git a/src/command/history.c b/src/command/history.c
new file mode 100644
index 00000000..29a952c1
--- /dev/null
+++ b/src/command/history.c
@@ -0,0 +1,69 @@
+/*
+ * command_history.c
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "tools/history.h"
+
+#define MAX_HISTORY 100
+
+static History history;
+
+void _stringify_input(char *inp, int size, char *string);
+
+void
+cmd_history_init(void)
+{
+    history = history_new(MAX_HISTORY);
+}
+
+void
+cmd_history_append(char *inp)
+{
+    history_append(history, inp);
+}
+
+char *
+cmd_history_previous(char *inp, int *size)
+{
+    char inp_str[*size + 1];
+    _stringify_input(inp, *size, inp_str);
+
+    return history_previous(history, inp_str);
+}
+
+char *
+cmd_history_next(char *inp, int *size)
+{
+    char inp_str[*size + 1];
+    _stringify_input(inp, *size, inp_str);
+
+    return history_next(history, inp_str);
+}
+
+void
+_stringify_input(char *inp, int size, char *string)
+{
+    int i;
+    for (i = 0; i < size; i++) {
+        string[i] = inp[i];
+    }
+    string[size] = '\0';
+}
diff --git a/src/command/history.h b/src/command/history.h
new file mode 100644
index 00000000..3b0b168f
--- /dev/null
+++ b/src/command/history.h
@@ -0,0 +1,28 @@
+/*
+ * command_history.h
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef COMMAND_HISTORY_H
+#define COMMAND_HISTORY_H
+
+void cmd_history_init(void);
+
+#endif
diff --git a/src/command/parser.c b/src/command/parser.c
new file mode 100644
index 00000000..e8197444
--- /dev/null
+++ b/src/command/parser.c
@@ -0,0 +1,263 @@
+/*
+ * command_parser.c
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+
+/*
+ * Take a full line of input and return an array of strings representing
+ * the arguments of a command.
+ * If the number of arguments found is less than min, or more than max
+ * NULL is returned.
+ *
+ * inp - The line of input
+ * min - The minimum allowed number of arguments
+ * max - The maxmimum allowed number of arguments
+ *
+ * Returns - An NULL terminated array of strings representing the aguments
+ * of the command, or NULL if the validation fails.
+ *
+ * E.g. the following input line:
+ *
+ * /cmd arg1 arg2
+ *
+ * Will return a pointer to the following array:
+ *
+ * { "arg1", "arg2", NULL }
+ *
+ */
+gchar **
+parse_args(const char * const inp, int min, int max)
+{
+    if (inp == NULL) {
+        return NULL;
+    }
+
+    // copy and strip input of leading/trailing whitepsace
+    char *copy = strdup(inp);
+    g_strstrip(copy);
+
+    int inp_size = strlen(copy);
+    gboolean in_token = FALSE;
+    gboolean in_quotes = FALSE;
+    char *token_start = &copy[0];
+    int token_size = 0;
+    GSList *tokens = NULL;
+
+    // add tokens to GSList
+    int i;
+    for (i = 0; i <= inp_size; i++) {
+        if (!in_token) {
+            if (copy[i] == ' ') {
+                continue;
+            } else {
+                in_token = TRUE;
+                if (copy[i] == '"') {
+                    in_quotes = TRUE;
+                    i++;
+                }
+                token_start = &copy[i];
+                token_size++;
+            }
+        } else {
+            if (in_quotes) {
+                if ((copy[i] == '\0') || (copy[i] == '"')) {
+                    tokens = g_slist_append(tokens, g_strndup(token_start,
+                        token_size));
+                    token_size = 0;
+                    in_token = FALSE;
+                    in_quotes = FALSE;
+                } else {
+                    token_size++;
+                }
+            } else {
+                if (copy[i] == ' ' || copy[i] == '\0') {
+                    tokens = g_slist_append(tokens, g_strndup(token_start,
+                        token_size));
+                    token_size = 0;
+                    in_token = FALSE;
+                } else {
+                    token_size++;
+                }
+            }
+        }
+    }
+
+    int num = g_slist_length(tokens) - 1;
+
+    // if num args not valid return NULL
+    if ((num < min) || (num > max)) {
+        g_slist_free_full(tokens, free);
+        g_free(copy);
+        return NULL;
+
+    // if min allowed is 0 and 0 found, return empty char* array
+    } else if (min == 0 && num == 0) {
+        g_slist_free_full(tokens, free);
+        gchar **args = malloc((num + 1) * sizeof(*args));
+        args[0] = NULL;
+        g_free(copy);
+        return args;
+
+    // otherwise return args array
+    } else {
+        gchar **args = malloc((num + 1) * sizeof(*args));
+        GSList *token = tokens;
+        token = g_slist_next(token);
+        int arg_count = 0;
+
+        while (token != NULL) {
+            args[arg_count++] = strdup(token->data);
+            token = g_slist_next(token);
+        }
+
+        args[arg_count] = NULL;
+        g_slist_free_full(tokens, free);
+        g_free(copy);
+
+        return args;
+    }
+}
+
+/*
+ * Take a full line of input and return an array of strings representing
+ * the arguments of a command.  This function handles when the last parameter
+ * to the command is free text e.g.
+ *
+ * /msg user@host here is a message
+ *
+ * If the number of arguments found is less than min, or more than max
+ * NULL is returned.
+ *
+ * inp - The line of input
+ * min - The minimum allowed number of arguments
+ * max - The maxmimum allowed number of arguments
+ *
+ * Returns - An NULL terminated array of strings representing the aguments
+ * of the command, or NULL if the validation fails.
+ *
+ * E.g. the following input line:
+ *
+ * /cmd arg1 arg2 some free text
+ *
+ * Will return a pointer to the following array:
+ *
+ * { "arg1", "arg2", "some free text", NULL }
+ *
+ */
+gchar **
+parse_args_with_freetext(const char * const inp, int min, int max)
+{
+    if (inp == NULL) {
+        return NULL;
+    }
+
+    // copy and strip input of leading/trailing whitepsace
+    char *copy = strdup(inp);
+    g_strstrip(copy);
+
+    int inp_size = strlen(copy);
+    gboolean in_token = FALSE;
+    gboolean in_freetext = FALSE;
+    gboolean in_quotes = FALSE;
+    char *token_start = &copy[0];
+    int token_size = 0;
+    int num_tokens = 0;
+    GSList *tokens = NULL;
+
+    // add tokens to GSList
+    int i;
+    for (i = 0; i <= inp_size; i++) {
+        if (!in_token) {
+            if (copy[i] == ' ') {
+                continue;
+            } else {
+                in_token = TRUE;
+                num_tokens++;
+                if (num_tokens == max + 1) {
+                    in_freetext = TRUE;
+                } else if (copy[i] == '"') {
+                    in_quotes = TRUE;
+                    i++;
+                }
+                token_start = &copy[i];
+                token_size++;
+            }
+        } else {
+            if (in_quotes) {
+                if ((copy[i] == '\0') || (copy[i] == '"')) {
+                    tokens = g_slist_append(tokens, g_strndup(token_start,
+                        token_size));
+                    token_size = 0;
+                    in_token = FALSE;
+                    in_quotes = FALSE;
+                } else {
+                    token_size++;
+                }
+            } else {
+                if ((!in_freetext && copy[i] == ' ') || copy[i] == '\0') {
+                    tokens = g_slist_append(tokens, g_strndup(token_start,
+                        token_size));
+                    token_size = 0;
+                    in_token = FALSE;
+                } else {
+                    token_size++;
+                }
+            }
+        }
+    }
+
+    int num = g_slist_length(tokens) - 1;
+
+    // if num args not valid return NULL
+    if ((num < min) || (num > max)) {
+        g_slist_free_full(tokens, free);
+        free(copy);
+        return NULL;
+
+    // if min allowed is 0 and 0 found, return empty char* array
+    } else if (min == 0 && num == 0) {
+        gchar **args = malloc((num + 1) * sizeof(*args));
+        args[0] = NULL;
+        return args;
+
+    // otherwise return args array
+    } else {
+        gchar **args = malloc((num + 1) * sizeof(*args));
+        GSList *token = tokens;
+        token = g_slist_next(token);
+        int arg_count = 0;
+
+        while (token != NULL) {
+            args[arg_count++] = strdup(token->data);
+            token = g_slist_next(token);
+        }
+
+        args[arg_count] = NULL;
+        g_slist_free_full(tokens, free);
+        free(copy);
+
+        return args;
+    }
+}
diff --git a/src/command/parser.h b/src/command/parser.h
new file mode 100644
index 00000000..f191ed15
--- /dev/null
+++ b/src/command/parser.h
@@ -0,0 +1,31 @@
+/*
+ * parser.h
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef PARSER_H
+#define PARSER_H
+
+#include <glib.h>
+
+gchar** parse_args(const char * const inp, int min, int max);
+gchar** parse_args_with_freetext(const char * const inp, int min, int max);
+
+#endif