diff --git a/ibis/ibisclient.h b/ibis/ibisclient.h
--- a/ibis/ibisclient.h
+++ b/ibis/ibisclient.h
@@ -30,10 +30,24 @@
 #include <glib.h>
 #include <glib-object.h>
 
+#include <gio/gio.h>
+
+#include "ibisparser.h"
 #include "ibisversion.h"
 
 G_BEGIN_DECLS
 
+/**
+ * IBIS_CLIENT_ERROR:
+ *
+ * A #GError domain for client errors.
+ *
+ * Since: 0.1
+ */
+#define IBIS_CLIENT_ERROR \
+	(g_quark_from_static_string("ibis-client")) \
+	IBIS_AVAILABLE_MACRO_IN_0_1
+
 #define IBIS_TYPE_CLIENT (ibis_client_get_type())
 
 /**
@@ -50,16 +64,32 @@
 #include "ibiscapabilities.h"
 
 /**
- * ibis_client_new:
+ * ibis_client_disconnect:
+ * @client: The instance.
+ * @error: (transfer full) (nullable): A #GError.
+ * @message: (nullable): An option disconnection message.
  *
- * Creates a new client.
+ * Disconnects @client.
  *
- * Returns: (transfer full): The new client.
+ * If @error is not %NULL, it will be assigned to [property@Client:error].
  *
  * Since: 0.1
  */
 IBIS_AVAILABLE_IN_0_1
-IbisClient *ibis_client_new(void);
+void ibis_client_disconnect(IbisClient *client, GError *error, const char *message);
+
+/**
+ * ibis_client_get_cancellable:
+ * @client: The instance.
+ *
+ * Gets the [class@Glib.Cancellable] for @client.
+ *
+ * Returns: (transfer none): The cancellable.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+GCancellable *ibis_client_get_cancellable(IbisClient *client);
 
 /**
  * ibis_client_get_capabilities:
@@ -75,6 +105,72 @@
 IbisCapabilities *ibis_client_get_capabilities(IbisClient *client);
 
 /**
+ * ibis_client_get_connected:
+ * @client: The instance.
+ *
+ * Gets whether or not @client is currently connected.
+ *
+ * Returns: %TRUE if connected, otherwise %FALSE.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+gboolean ibis_client_get_connected(IbisClient *client);
+
+/**
+ * ibis_client_get_error:
+ * @client: The instance.
+ *
+ * Gets the #GError that client encountered.
+ *
+ * Returns: (transfer none): The error or %NULL is no error has been
+ *          encountered.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+GError *ibis_client_get_error(IbisClient *client);
+
+/**
+ * ibis_client_get_parser:
+ * @client: The instance.
+ *
+ * Gets the [class@Parser] that @client is using.
+ *
+ * Returns: (transfer none): The parser.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+IbisParser *ibis_client_get_parser(IbisClient *client);
+
+/**
+ * ibis_client_new:
+ *
+ * Creates a new client.
+ *
+ * Returns: (transfer full): The new client.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+IbisClient *ibis_client_new(void);
+
+/**
+ * ibis_client_start:
+ * @client: The instance.
+ * @stream: The [class@Gio.IOStream] to process.
+ *
+ * Starts @client.
+ *
+ * Starting the @client means that it should start processing @stream.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+void ibis_client_start(IbisClient *client, GIOStream *stream);
+
+/**
  * ibis_client_writef:
  * @client: The instance.
  * @format: The format string.
# HG changeset patch
# Node ID b14f498850cf
# Parent  6910f0d671c6
diff --git a/ibis/ibisclient.c b/ibis/ibisclient.c
--- a/ibis/ibisclient.c
+++ b/ibis/ibisclient.c
@@ -20,11 +20,21 @@
  * along with this library; if not, see <https://www.gnu.org/licenses/>.
  */
 
+#include <glib/gi18n-lib.h>
+
+#include <birb.h>
+
 #include "ibisclient.h"
 
+#include "ibisstring.h"
+
 enum {
 	PROP_0,
+	PROP_CANCELLABLE,
 	PROP_CAPABILITIES,
+	PROP_CONNECTED,
+	PROP_ERROR,
+	PROP_PARSER,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -32,10 +42,126 @@
 struct _IbisClient {
 	GObject parent;
 
+	GCancellable *cancellable;
 	IbisCapabilities *capabilities;
+	gboolean connected;
+	GError *error;
+	IbisParser *parser;
+
+	GIOStream *stream;
+	GDataInputStream *input;
+	GOutputStream *output;
 };
 
 /******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+ibis_client_set_connected(IbisClient *client, gboolean connected) {
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+
+	if(client->connected != connected) {
+		client->connected = connected;
+
+		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_CONNECTED]);
+	}
+}
+
+static void
+ibis_client_set_error(IbisClient *client, GError *error) {
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+
+	if(client->error == NULL && error == NULL) {
+		return;
+	}
+
+	if(error != NULL) {
+		if(!g_error_matches(client->error, error->domain, error->code)) {
+			/* If this is a different error, set it and notify. */
+			g_clear_error(&client->error);
+			g_propagate_error(&client->error, error);
+
+			g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ERROR]);
+		} else {
+			/* The error was the same, but this is transfer full, so we need
+			 * to remove the reference.
+			 */
+			g_error_free(error);
+		}
+	} else if(client->error != NULL) {
+		/* We're clearing the air, so do that and notify. */
+		g_clear_error(&client->error);
+
+		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ERROR]);
+	}
+}
+
+static void
+ibis_client_write_cb(GObject *source, GAsyncResult *result, gpointer data) {
+	IbisClient *client = data;
+	BirbQueuedOutputStream *stream = BIRB_QUEUED_OUTPUT_STREAM(source);
+	GError *error = NULL;
+	gboolean success = FALSE;
+
+	success = birb_queued_output_stream_push_bytes_finish(stream, result,
+	                                                      &error);
+
+	if(!success) {
+		birb_queued_output_stream_clear_queue(stream);
+
+		g_prefix_error(&error, "%s", _("Lost connection: "));
+
+		ibis_client_disconnect(client, error, NULL);
+	}
+}
+
+static void
+ibis_client_read_cb(GObject *source, GAsyncResult *result, gpointer data) {
+	IbisClient *client = data;
+	GDataInputStream *input = G_DATA_INPUT_STREAM(source);
+	GError *error = NULL;
+	gchar *line = NULL;
+	gsize length;
+	gboolean parsed = FALSE;
+
+	line = g_data_input_stream_read_line_finish(input, result, &length,
+	                                            &error);
+	if(line == NULL || error != NULL) {
+		if(IBIS_IS_CLIENT(client)) {
+			if(error == NULL) {
+				g_set_error_literal(&error, IBIS_CLIENT_ERROR, 0,
+				                    _("Server closed the connection"));
+			} else {
+				g_prefix_error(&error, "%s", _("Lost connection with server: "));
+			}
+
+			ibis_client_disconnect(client, error, NULL);
+		}
+
+		/* In the off chance that line was returned, make sure we free it. */
+		g_free(line);
+
+		return;
+	}
+
+	parsed = ibis_parser_parse(client->parser, line, &error, client);
+	if(!parsed) {
+		g_warning("failed to handle '%s': %s", line,
+		          error != NULL ? error->message : "unknown error");
+	}
+	g_clear_error(&error);
+
+	g_free(line);
+
+	/* Call read_line_async again to continue reading lines. */
+	g_data_input_stream_read_line_async(client->input,
+	                                    G_PRIORITY_DEFAULT,
+	                                    client->cancellable,
+	                                    ibis_client_read_cb,
+	                                    client);
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 G_DEFINE_FINAL_TYPE(IbisClient, ibis_client, G_TYPE_OBJECT)
@@ -44,7 +170,14 @@
 ibis_client_finalize(GObject *obj) {
 	IbisClient *client = IBIS_CLIENT(obj);
 
+	if(client->connected) {
+		ibis_client_disconnect(client, NULL, NULL);
+	}
+
+	g_clear_object(&client->cancellable);
 	g_clear_object(&client->capabilities);
+	g_clear_error(&client->error);
+	g_clear_object(&client->parser);
 
 	G_OBJECT_CLASS(ibis_client_parent_class)->finalize(obj);
 }
@@ -56,9 +189,21 @@
 	IbisClient *client = IBIS_CLIENT(obj);
 
 	switch(param_id) {
+	case PROP_CANCELLABLE:
+		g_value_set_object(value, ibis_client_get_cancellable(client));
+		break;
 	case PROP_CAPABILITIES:
 		g_value_set_object(value, ibis_client_get_capabilities(client));
 		break;
+	case PROP_CONNECTED:
+		g_value_set_boolean(value, ibis_client_get_connected(client));
+		break;
+	case PROP_ERROR:
+		g_value_set_boxed(value, ibis_client_get_error(client));
+		break;
+	case PROP_PARSER:
+		g_value_set_object(value, ibis_client_get_parser(client));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -68,6 +213,7 @@
 static void
 ibis_client_init(IbisClient *client) {
 	client->capabilities = ibis_capabilities_new(client);
+	client->parser = ibis_parser_new();
 }
 
 static void
@@ -78,6 +224,19 @@
 	obj_class->get_property = ibis_client_get_property;
 
 	/**
+	 * IbisClient: cancellable:
+	 *
+	 * The [class@Glib.Cancellable] for the client.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_CANCELLABLE] = g_param_spec_object(
+		"cancellable", "cancellable",
+		"The GCancellable for the client.",
+		G_TYPE_CANCELLABLE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * IbisClient:capabilities:
 	 *
 	 * The [class@Capabilities] that this client is using.
@@ -90,15 +249,94 @@
 		IBIS_TYPE_CAPABILITIES,
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * IbisClient:connected:
+	 *
+	 * Set to whether or not the client is currently connected.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_CONNECTED] = g_param_spec_boolean(
+		"connected", "connected",
+		"Whether or not the client is currently connected.",
+		FALSE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * IbisClient:error:
+	 *
+	 * A #GError that the client encountered.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_ERROR] = g_param_spec_boxed(
+		"error", "error",
+		"An error that the client encountered.",
+		G_TYPE_ERROR,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * IbisClient:parser:
+	 *
+	 * The [class@Parser] that this client is using.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_PARSER] = g_param_spec_object(
+		"parser", "parser",
+		"The parser for this client.",
+		IBIS_TYPE_PARSER,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
 /******************************************************************************
  * Public API
  *****************************************************************************/
-IbisClient *
-ibis_client_new(void) {
-	return g_object_new(IBIS_TYPE_CLIENT, NULL);
+void
+ibis_client_disconnect(IbisClient *client, GError *error, const char *message)
+{
+	GError *close_error = NULL;
+
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+	g_return_if_fail(client->connected == TRUE);
+
+	if(error == NULL) {
+		if(!ibis_str_is_empty(message)) {
+			ibis_client_writef(client, "QUIT %s", message);
+		} else {
+			ibis_client_writef(client, "QUIT");
+		}
+	}
+
+	ibis_client_set_error(client, error);
+
+	/* If closing the stream fails, the stream still gets closed, but we can
+	 * update the error message if it wasn't already set.
+	 */
+	if(!g_io_stream_close(client->stream, client->cancellable, &close_error)) {
+		if(close_error != NULL) {
+			g_warning("failed to close stream: %s", close_error->message);
+		} else {
+			g_warning("failed to close stream: unknown error");
+		}
+
+		if(client->error == NULL) {
+			ibis_client_set_error(client, close_error);
+		}
+
+		g_clear_error(&close_error);
+	}
+
+	ibis_client_set_connected(client, FALSE);
+}
+
+GCancellable *
+ibis_client_get_cancellable(IbisClient *client) {
+	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
+
+	return client->cancellable;
 }
 
 IbisCapabilities *
@@ -108,8 +346,96 @@
 	return client->capabilities;
 }
 
+gboolean
+ibis_client_get_connected(IbisClient *client) {
+	g_return_val_if_fail(IBIS_IS_CLIENT(client), FALSE);
+
+	return client->connected;
+}
+
+GError *
+ibis_client_get_error(IbisClient *client) {
+	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
+
+	return client->error;
+}
+
+IbisParser *
+ibis_client_get_parser(IbisClient *client) {
+	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
+
+	return client->parser;
+}
+
+IbisClient *
+ibis_client_new(void) {
+	return g_object_new(IBIS_TYPE_CLIENT, NULL);
+}
+
 void
-ibis_client_writef(IbisClient *client, const char *format, ...)
-{
-#pragma message not implemented
+ibis_client_start(IbisClient *client, GIOStream *stream) {
+	GInputStream *input = NULL;
+	GOutputStream *output = NULL;
+
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+	g_return_if_fail(G_IS_IO_STREAM(stream));
+	g_return_if_fail(client->connected == FALSE);
+
+	/* Clear any previous error. */
+	ibis_client_set_error(client, NULL);
+
+	/* Store the stream for later use in disconnect. */
+	client->stream = g_object_ref(stream);
+
+	/* Wrap the output stream in a Birb QueuedOutputStream. */
+	output = g_io_stream_get_output_stream(stream);
+	client->output = birb_queued_output_stream_new(output);
+
+	/* Setup the input stream. */
+	input = g_io_stream_get_input_stream(G_IO_STREAM(stream));
+	client->input = g_data_input_stream_new(input);
+	g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(client->input),
+	                                     G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
+
+	/* Start chunking the input. */
+	g_data_input_stream_read_line_async(client->input,
+	                                    G_PRIORITY_DEFAULT,
+	                                    client->cancellable,
+	                                    ibis_client_read_cb,
+	                                    client);
+
+	/* Set our connected property. */
+	ibis_client_set_connected(client, TRUE);
 }
+
+void
+ibis_client_writef(IbisClient *client, const char *format, ...) {
+	GBytes *bytes = NULL;
+	GString *msg = NULL;
+	va_list vargs;
+
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+	g_return_if_fail(format != NULL);
+
+	/* Create our string and append our format to it. */
+	msg = g_string_new("");
+
+	va_start(vargs, format);
+	g_string_vprintf(msg, format, vargs);
+	va_end(vargs);
+
+	/* Next add the trailing carriage return line feed. */
+	g_string_append(msg, "\r\n");
+
+	/* Finally turn the string into bytes and send it! */
+	bytes = g_string_free_to_bytes(msg);
+
+	birb_queued_output_stream_push_bytes_async(BIRB_QUEUED_OUTPUT_STREAM(client->output),
+	                                           bytes,
+	                                           G_PRIORITY_DEFAULT,
+	                                           client->cancellable,
+	                                           ibis_client_write_cb,
+	                                           client);
+
+	g_bytes_unref(bytes);
+}
diff --git a/po/POTFILES b/po/POTFILES
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -0,0 +1,7 @@
+ibis/ibiscapabilities.c
+ibis/ibisclient.c
+ibis/ibisformatting.c
+ibis/ibismessage.c
+ibis/ibisparser.c
+ibis/ibissource.c
+ibis/ibisversion.c
