[hotssh] Use a list view for connections



commit 2b26e332b8f69d290a6234f7869dfab3e55e391f
Author: Colin Walters <walters verbum org>
Date:   Tue Dec 31 12:48:55 2013 -0500

    Use a list view for connections
    
    Make the primary view a list of known connections, and rework the
    HostDB so that our config file is primary, and the OpenSSH known hosts
    is secondary.

 src/hotssh-hostdb.c |  496 +++++++++++++++++++++++++++++----------------------
 src/hotssh-hostdb.h |   35 +++-
 src/hotssh-tab.c    |  188 ++++++++++++++++++--
 src/tab.ui          |  147 ++++++++++++++--
 4 files changed, 621 insertions(+), 245 deletions(-)
---
diff --git a/src/hotssh-hostdb.c b/src/hotssh-hostdb.c
index 560fc8a..c67cc53 100644
--- a/src/hotssh-hostdb.c
+++ b/src/hotssh-hostdb.c
@@ -28,13 +28,6 @@
 
 #include "libgsystem.h"
 
-enum {
-  HOTSSH_HOSTDB_COLUMN_HOSTNAME,
-  HOTSSH_HOSTDB_COLUMN_LAST_USED,
-  HOTSSH_HOSTDB_COLUMN_IS_KNOWN,
-  HOTSSH_HOSTDB_COLUMN_USERNAME
-};
-
 struct _HotSshHostDB
 {
   GObject parent;
@@ -50,145 +43,83 @@ typedef struct _HotSshHostDBPrivate HotSshHostDBPrivate;
 struct _HotSshHostDBPrivate
 {
   GtkListStore *model;
-  GKeyFile *extradb;
+  GHashTable *openssh_knownhosts; /* str -> str */
+  GKeyFile *hostdb;
   GFile *openssh_dir;
   GFile *openssh_knownhosts_path;
-  GFile *hotssh_extradb;
+  GFile *hotssh_hostdb_path;
   GFileMonitor *knownhosts_monitor;
-  GFileMonitor *hotssh_extradb_monitor;
 
-  guint idle_save_extradb_id;
-  char *new_extradb_contents;
+  guint idle_save_hostdb_id;
+  char *new_hostdb_contents;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE(HotSshHostDB, hotssh_hostdb, G_TYPE_OBJECT)
 
 static char *
-host_group_key_to_host (const char *group)
+address_to_string (GNetworkAddress    *address)
 {
-  char *hostname = NULL;
-  const char *host_group_prefix = "host \"";
-  const char *lastquote;
-  const char *hoststart;
-  
-  if (!g_str_has_prefix (group, host_group_prefix))
-    return NULL;
-
-  hoststart = group + strlen (host_group_prefix);
-  lastquote = strchr (hoststart, '"');
-  if (!lastquote)
-    return NULL;
-      
-  hostname = g_strndup (hoststart, lastquote - hoststart);
-  if (!(hostname && hostname[0]))
-    {
-      g_free (hostname);
-      return NULL;
-    }
-
-  return hostname;
+  const char *hostname = g_network_address_get_hostname (address);
+  guint port = g_network_address_get_port (address);
+  if (port == 22)
+    return g_strdup (g_network_address_get_hostname (address));
+  return g_strdup_printf ("[%s]:%u", hostname, port);
 }
 
-static gboolean
-sync_extradb_to_model (HotSshHostDB    *self,
-                       const char      *groupname,
-                       const char      *hostname,
-                       GtkTreeIter     *iter)
+static void
+mark_all_entries_unknown (HotSshHostDB    *self)
 {
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
-  guint64 last_used;
-  gs_free char *username = NULL;
-  GError *temp_error = NULL;
+  GtkTreeIter iter;
 
-  last_used = g_key_file_get_uint64 (priv->extradb, groupname, "last-used", &temp_error);
-  if (temp_error)
-    {
-      g_clear_error (&temp_error);
-      return FALSE;
-    }
-  else
+  if (!gtk_tree_model_get_iter_first ((GtkTreeModel*)priv->model, &iter))
+    return;
+  while (TRUE)
     {
-      gtk_list_store_set (priv->model, iter,
-                          HOTSSH_HOSTDB_COLUMN_LAST_USED, last_used,
+      gtk_list_store_set (priv->model, &iter,
+                          HOTSSH_HOSTDB_COLUMN_IS_KNOWN, FALSE,
                           -1);
-    }
 
-  username = g_key_file_get_string (priv->extradb, groupname, "username", NULL);
-  if (username && username[0])
-    {
-      gtk_list_store_set (priv->model, iter,
-                          HOTSSH_HOSTDB_COLUMN_USERNAME, username,
-                          -1);
+      if (!gtk_tree_model_iter_next ((GtkTreeModel*)priv->model, &iter))
+        break;
     }
-  return TRUE;
 }
 
 static void
-merge_databases (HotSshHostDB *self)
+mark_all_entries_known_by_address (HotSshHostDB    *self,
+                                   const char      *hostname,
+                                   guint            port,
+                                   guint            lineno)
 {
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
-  gs_strfreev char **extradb_groups = NULL;
-  gs_unref_hashtable GHashTable *known_hosts
-    = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
-  char **strviter;
   GtkTreeIter iter;
-  
+
   if (!gtk_tree_model_get_iter_first ((GtkTreeModel*)priv->model, &iter))
     return;
   while (TRUE)
     {
-      gs_free char *hostname = NULL;
-      gs_free char *section = NULL;
-      gs_free char *username = NULL;
+      gs_free char *model_hostname = NULL;
+      guint model_port;
       gboolean is_known = FALSE;
-      gboolean remove = FALSE;
 
       gtk_tree_model_get ((GtkTreeModel*)priv->model, &iter,
-                          HOTSSH_HOSTDB_COLUMN_HOSTNAME, &hostname,
+                          HOTSSH_HOSTDB_COLUMN_HOSTNAME, &model_hostname,
+                          HOTSSH_HOSTDB_COLUMN_PORT, &model_port,
                           HOTSSH_HOSTDB_COLUMN_IS_KNOWN, &is_known,
                           -1);
 
-      if (is_known)
-        g_hash_table_add (known_hosts, g_strdup (hostname));
-
-      section = g_strdup_printf ("host \"%s\"", hostname);
-      if (!sync_extradb_to_model (self, section, hostname, &iter))
-        {
-          if (!is_known)
-            remove = TRUE;
-        }
-
-      if (remove)
+      if (!is_known &&
+          g_ascii_strcasecmp (hostname, model_hostname) == 0 &&
+          port == model_port)
         {
-          if (!gtk_list_store_remove (priv->model, &iter))
-            break;
-        }
-      else
-        {
-          if (!gtk_tree_model_iter_next ((GtkTreeModel*)priv->model, &iter))
-            break;
-        }
-    }
-
-  extradb_groups = g_key_file_get_groups (priv->extradb, NULL);
-  for (strviter = extradb_groups; strviter && *strviter; strviter++)
-    {
-      const char *group = *strviter;
-      gs_free char *hostname = NULL;
-
-      hostname = host_group_key_to_host (group);
-      if (!hostname)
-        continue;
-      
-      if (!g_hash_table_contains (known_hosts, hostname))
-        {
-          gtk_list_store_append (priv->model, &iter);
           gtk_list_store_set (priv->model, &iter,
-                              HOTSSH_HOSTDB_COLUMN_HOSTNAME, hostname,
-                              HOTSSH_HOSTDB_COLUMN_IS_KNOWN, FALSE,
+                              HOTSSH_HOSTDB_COLUMN_IS_KNOWN, TRUE,
+                              HOTSSH_HOSTDB_COLUMN_OPENSSH_KNOWNHOST_LINE, lineno,
                               -1);
-          (void) sync_extradb_to_model (self, group, hostname, &iter);
         }
+
+      if (!gtk_tree_model_iter_next ((GtkTreeModel*)priv->model, &iter))
+        break;
     }
 }
 
@@ -203,23 +134,27 @@ on_knownhosts_changed (GFileMonitor        *monitor,
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
   GError *local_error = NULL;
   gs_free char *contents = NULL;
-  GtkTreeIter modeliter;
   gsize len;
   char *iter;
   char *eol;
   char *carriage;
+  guint lineno = 0;
 
   if (!g_file_load_contents (priv->openssh_knownhosts_path, NULL,
                              &contents, &len, NULL,
                              &local_error))
     goto out;
 
-  gtk_list_store_clear (priv->model);
-  
+  mark_all_entries_unknown (self);
+
+  g_hash_table_remove_all (priv->openssh_knownhosts);
+
   iter = contents;
   while (TRUE)
     {
       gs_strfreev char **parts = NULL;
+      GNetworkAddress *address_obj = NULL;
+      gs_free char *address_str = NULL;
       char *comma;
 
       eol = strchr (iter, '\n');
@@ -240,14 +175,23 @@ on_knownhosts_changed (GFileMonitor        *monitor,
       comma = strchr (parts[0], ',');
       if (comma)
         *comma = '\0';
-      gtk_list_store_append (priv->model, &modeliter);
-      gtk_list_store_set (priv->model, &modeliter,
-                          HOTSSH_HOSTDB_COLUMN_HOSTNAME, parts[0],
-                          HOTSSH_HOSTDB_COLUMN_LAST_USED, 0,
-                          HOTSSH_HOSTDB_COLUMN_IS_KNOWN, TRUE,
-                          -1);
+
+      address_obj = (GNetworkAddress*)g_network_address_parse (parts[0], 22, NULL);
+      if (!address_obj)
+        goto next;
+      address_str = address_to_string (address_obj);
+
+      mark_all_entries_known_by_address (self,
+                                         g_network_address_get_hostname (address_obj),
+                                         g_network_address_get_port (address_obj),
+                                         lineno);
+
+      g_hash_table_insert (priv->openssh_knownhosts, address_str, parts);
+      address_str = NULL;
+      parts = NULL;
 
     next:
+      lineno++;
       if (eol)
         iter = eol + 1;
       else
@@ -256,9 +200,6 @@ on_knownhosts_changed (GFileMonitor        *monitor,
 
   g_debug ("Read %d known hosts", gtk_tree_model_iter_n_children ((GtkTreeModel*)priv->model, NULL));
 
-  if (priv->extradb)
-    merge_databases (self);
-  
  out:
   if (local_error)
     {
@@ -268,21 +209,58 @@ on_knownhosts_changed (GFileMonitor        *monitor,
     }
 }
 
+static gboolean
+set_row_from_group (HotSshHostDB         *self,
+                    const char           *groupname,
+                    GtkTreeIter          *iter)
+{
+  G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
+  gs_free char *hostname = NULL;
+  gs_free char *username = NULL;
+  gs_free char *port_str = NULL;
+  gint64 last_used;
+  int port;
+  gboolean is_known;
+
+  last_used = g_key_file_get_uint64 (priv->hostdb, groupname, "last-used", NULL);
+
+  username = g_key_file_get_string (priv->hostdb, groupname, "username", NULL);
+  if (!(username && username[0]))
+    return FALSE;
+
+  hostname = g_key_file_get_string (priv->hostdb, groupname, "hostname", NULL);
+  if (!(hostname && hostname[0]))
+    return FALSE;
+
+  port = g_key_file_get_integer (priv->hostdb, groupname, "port", NULL);
+  if (port <= 0 || port > G_MAXUINT16)
+    port = 22;
+
+  is_known = g_hash_table_contains (priv->openssh_knownhosts, hostname);
+
+  gtk_list_store_set (priv->model, iter,
+                      HOTSSH_HOSTDB_COLUMN_ID, groupname,
+                      HOTSSH_HOSTDB_COLUMN_USERNAME, username,
+                      HOTSSH_HOSTDB_COLUMN_HOSTNAME, hostname,
+                      HOTSSH_HOSTDB_COLUMN_PORT, (guint)port,
+                      HOTSSH_HOSTDB_COLUMN_LAST_USED, last_used,
+                      HOTSSH_HOSTDB_COLUMN_IS_KNOWN, is_known,
+                      -1);
+  return TRUE;
+}
+
 static void
-on_extradb_changed (GFileMonitor        *monitor,
-                    GFile               *src,
-                    GFile               *other,
-                    GFileMonitorEvent    event,
-                    gpointer             user_data)
+load_hostdb (HotSshHostDB         *self)
 {
-  HotSshHostDB *self = user_data;
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
+  gs_strfreev char **hostdb_groups = NULL;
+  char **strviter;
   GError *local_error = NULL;
 
-  g_clear_pointer (&priv->extradb, g_key_file_unref);
-  priv->extradb = g_key_file_new ();
+  g_clear_pointer (&priv->hostdb, g_key_file_unref);
+  priv->hostdb = g_key_file_new ();
   
-  if (!g_key_file_load_from_file (priv->extradb, gs_file_get_path_cached (priv->hotssh_extradb), 0, 
&local_error))
+  if (!g_key_file_load_from_file (priv->hostdb, gs_file_get_path_cached (priv->hotssh_hostdb_path), 0, 
&local_error))
     {
       if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
         g_clear_error (&local_error);
@@ -290,8 +268,21 @@ on_extradb_changed (GFileMonitor        *monitor,
         goto out;
     }
 
-  if (priv->extradb)
-    merge_databases (self);
+  hostdb_groups = g_key_file_get_groups (priv->hostdb, NULL);
+  for (strviter = hostdb_groups; strviter && *strviter; strviter++)
+    {
+      const char *group = *strviter;
+      GtkTreeIter iter;
+
+      if (!g_str_has_prefix (group, "entry "))
+        continue;
+
+      gtk_list_store_append (priv->model, &iter);
+      if (!set_row_from_group (self, group, &iter))
+        {
+          gtk_list_store_remove (priv->model, &iter);
+        }
+    }
   
  out:
   if (local_error)
@@ -302,53 +293,8 @@ on_extradb_changed (GFileMonitor        *monitor,
     }
 }
 
-static gboolean
-hostname_to_iter (HotSshHostDB    *self,
-                  const char      *hostname,
-                  GtkTreeIter     *iter)
-{
-  G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
-
-  if (!gtk_tree_model_get_iter_first ((GtkTreeModel*)priv->model, iter))
-    return FALSE;
-
-  while (TRUE)
-    {
-      gs_free char *model_hostname = NULL;
-
-      gtk_tree_model_get ((GtkTreeModel*)priv->model, iter,
-                          HOTSSH_HOSTDB_COLUMN_HOSTNAME, &model_hostname,
-                          -1);
-
-      if (g_ascii_strcasecmp (hostname, model_hostname) == 0)
-        return TRUE;
-
-      if (!gtk_tree_model_iter_next ((GtkTreeModel*)priv->model, iter))
-        break;
-    }
-
-  return FALSE;
-}
-
 static void
-get_or_create_host_in_db (HotSshHostDB        *self,
-                          const char          *hostname,
-                          GtkTreeIter         *iter)
-{
-  G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
-
-  if (hostname_to_iter (self, hostname, iter))
-    return;
-
-  gtk_list_store_append (priv->model, iter);
-  gtk_list_store_set (priv->model, iter,
-                      HOTSSH_HOSTDB_COLUMN_HOSTNAME, hostname,
-                      HOTSSH_HOSTDB_COLUMN_IS_KNOWN, FALSE,
-                      -1);
-}
-
-static void
-on_replace_extradb_contents_complete (GObject                *src,
+on_replace_hostdb_contents_complete (GObject                *src,
                                       GAsyncResult           *result,
                                       gpointer                user_data)
 {
@@ -356,75 +302,195 @@ on_replace_extradb_contents_complete (GObject                *src,
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
   GError *local_error = NULL;
 
-  priv->idle_save_extradb_id = 0;
+  priv->idle_save_hostdb_id = 0;
 
   if (!g_file_replace_contents_finish ((GFile*)src, result, NULL, &local_error))
     goto out;
 
  out:
-  g_clear_pointer (&priv->new_extradb_contents, g_free);
+  g_clear_pointer (&priv->new_hostdb_contents, g_free);
   if (local_error)
     {
       g_warning ("Failed to save '%s': %s",
-                 gs_file_get_path_cached (priv->hotssh_extradb),
+                 gs_file_get_path_cached (priv->hotssh_hostdb_path),
                  local_error->message);
       g_clear_error (&local_error);
     }
 }
 
 static gboolean
-idle_save_extradb (gpointer user_data)
+idle_save_hostdb (gpointer user_data)
 {
   HotSshHostDB *self = user_data;
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
   gsize len;
 
-  priv->new_extradb_contents = g_key_file_to_data (priv->extradb, &len, NULL);
-  g_assert (priv->new_extradb_contents);
+  priv->new_hostdb_contents = g_key_file_to_data (priv->hostdb, &len, NULL);
+  g_assert (priv->new_hostdb_contents);
 
-  g_file_replace_contents_async (priv->hotssh_extradb, priv->new_extradb_contents, len, NULL,
+  g_file_replace_contents_async (priv->hotssh_hostdb_path, priv->new_hostdb_contents, len, NULL,
                                  FALSE, 0, NULL,
-                                 on_replace_extradb_contents_complete, self);
+                                 on_replace_hostdb_contents_complete, self);
   return FALSE;
 }
 
 static void
-queue_save_extradb (HotSshHostDB    *self)
+queue_save_hostdb (HotSshHostDB    *self)
+{
+  G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
+  if (priv->idle_save_hostdb_id > 0)
+    return;
+  priv->idle_save_hostdb_id = g_timeout_add_seconds (5, idle_save_hostdb, self);
+}
+
+static void
+append_randword (GString *buf)
+{
+  guint i;
+  for (i = 0; i < 2; i++)
+    g_string_append_printf (buf, "%02X", (guint8) g_random_int_range (0, 256));
+}
+
+static char *
+allocate_groupname (HotSshHostDB       *self)
+{
+  GString *buf = g_string_new ("entry ");
+  guint i;
+
+  for (i = 0; i < 2; i++)
+    append_randword (buf);
+  g_string_append_c (buf, '-');
+  for (i = 0; i < 3; i++)
+    {
+      append_randword (buf);
+      g_string_append_c (buf, '-');
+    }
+  for (i = 0; i < 3; i++)
+    append_randword (buf);
+  
+  return g_string_free (buf, FALSE);
+}
+
+static void
+set_group_values (HotSshHostDB     *self,
+                  const char       *groupname,
+                  const char       *username,
+                  GNetworkAddress  *address)
+{
+  G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
+  guint port = g_network_address_get_port (address);
+  g_key_file_set_string (priv->hostdb, groupname, "username",
+                         username ? username : g_get_user_name ());
+  g_key_file_set_string (priv->hostdb, groupname, "hostname",
+                         g_network_address_get_hostname (address));
+  if (port != 22)
+    g_key_file_set_integer (priv->hostdb, groupname, "port", port);
+}
+
+void
+hotssh_hostdb_add_entry (HotSshHostDB     *self,
+                         const char       *username,
+                         GNetworkAddress  *address,
+                         char            **out_id)
+{
+  G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
+  GtkTreeIter iter;
+  gs_free char *groupname = allocate_groupname (self);
+
+  set_group_values (self, groupname, username, address);
+
+  gtk_list_store_append (priv->model, &iter);
+  (void) set_row_from_group (self, groupname, &iter);
+  queue_save_hostdb (self);
+  *out_id = groupname;
+  groupname = NULL;
+}
+
+gboolean
+hotssh_hostdb_lookup_by_id (HotSshHostDB   *self,
+                            const char     *id,
+                            GtkTreeIter    *out_iter)
 {
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
-  if (priv->idle_save_extradb_id > 0)
+  if (!gtk_tree_model_get_iter_first ((GtkTreeModel*)priv->model, out_iter))
+    return FALSE;
+  while (TRUE)
+    {
+      gs_free char *model_id = NULL;
+
+      gtk_tree_model_get ((GtkTreeModel*)priv->model, out_iter,
+                          HOTSSH_HOSTDB_COLUMN_ID, &model_id,
+                          -1);
+
+      if (model_id != NULL && strcmp (id, model_id) == 0)
+        return TRUE;
+
+      if (!gtk_tree_model_iter_next ((GtkTreeModel*)priv->model, out_iter))
+        break;
+    }
+  return FALSE;
+}
+
+void
+hotssh_hostdb_set_entry_basic (HotSshHostDB    *self,
+                               const char      *id,
+                               const char      *username,
+                               GNetworkAddress *address)
+{
+  G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
+  GtkTreeIter iter;
+
+  if (!hotssh_hostdb_lookup_by_id (self, id, &iter))
     return;
-  priv->idle_save_extradb_id = g_timeout_add_seconds (5, idle_save_extradb, self);
+
+  set_group_values (self, id, username, address);
+  (void) set_row_from_group (self, id, &iter);
+  queue_save_hostdb (self);
 }
 
 void
-hotssh_hostdb_host_used (HotSshHostDB *self,
-                         const char   *hostname)
+hotssh_hostdb_update_last_used (HotSshHostDB    *self,
+                                const char      *id)
 {
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
   GtkTreeIter iter;
-  gs_free char *groupname = g_strdup_printf ("host \"%s\"", hostname);
 
-  get_or_create_host_in_db (self, hostname, &iter);
+  if (!hotssh_hostdb_lookup_by_id (self, id, &iter))
+    return;
+
+  g_key_file_set_uint64 (priv->hostdb, id, "last-used", g_get_real_time () / G_USEC_PER_SEC);
 
-  g_key_file_set_uint64 (priv->extradb, groupname, "last-used", g_get_real_time () / G_USEC_PER_SEC);
-  queue_save_extradb (self);
+  (void) set_row_from_group (self, id, &iter);
+  queue_save_hostdb (self);
 }
 
 void
-hotssh_hostdb_set_username (HotSshHostDB *self,
-                            const char   *hostname,
-                            const char   *username)
+hotssh_hostdb_set_entry_known (HotSshHostDB    *self,
+                               const char      *id,
+                               gboolean         make_known)
 {
   G_GNUC_UNUSED HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private (self);
+  gboolean is_known;
   GtkTreeIter iter;
-  gs_free char *groupname = g_strdup_printf ("host \"%s\"", hostname);
 
-  get_or_create_host_in_db (self, hostname, &iter);
+  if (!hotssh_hostdb_lookup_by_id (self, id, &iter))
+    return;
+
+  gtk_tree_model_get ((GtkTreeModel*)priv->model, &iter,
+                      HOTSSH_HOSTDB_COLUMN_IS_KNOWN, &is_known,
+                      -1);
 
-  g_key_file_set_string (priv->extradb, groupname, "username", username);
+  if (is_known == make_known)
+    return;
 
-  queue_save_extradb (self);
+  if (make_known)
+    {
+      g_debug ("not implemented");
+    }
+  else
+    {
+      g_debug ("not implemented");
+    }
 }
 
 static GFileMonitor *
@@ -458,13 +524,23 @@ hotssh_hostdb_init (HotSshHostDB *self)
   gs_free char *knownhosts_path = NULL;
   gs_free char *openssh_path = NULL;
   gs_free char *hotssh_config_dir = NULL;
-  gs_free char *hotssh_extradb_path = NULL;
-
-  priv->model = gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_UINT64, G_TYPE_BOOLEAN,
-                                    G_TYPE_STRING);
+  gs_free char *hotssh_hostdb_path = NULL;
+
+  priv->model = gtk_list_store_new (HOTSSH_HOSTDB_N_COLUMNS,
+                                    G_TYPE_STRING, /* id */
+                                    G_TYPE_STRING, /* hostname */
+                                    G_TYPE_UINT,   /* port */
+                                    G_TYPE_STRING, /* username */
+                                    G_TYPE_UINT64, /* last-used */
+                                    G_TYPE_BOOLEAN, /* is-known */
+                                    G_TYPE_UINT64  /* openssh-knownhost-line */
+                                    );
   homedir = g_get_home_dir ();
   g_assert (homedir);
 
+  priv->openssh_knownhosts = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                    g_free, (GDestroyNotify)g_strfreev);
+
   openssh_path = g_build_filename (homedir, ".ssh", NULL);
   knownhosts_path = g_build_filename (openssh_path, "known_hosts", NULL);
 
@@ -473,8 +549,8 @@ hotssh_hostdb_init (HotSshHostDB *self)
 
   hotssh_config_dir = g_build_filename (g_get_user_config_dir (), "hotssh", NULL);
   (void) g_mkdir_with_parents (hotssh_config_dir, 0700);
-  hotssh_extradb_path = g_build_filename (hotssh_config_dir, "hostdb.ini", NULL);
-  priv->hotssh_extradb = g_file_new_for_path (hotssh_extradb_path);
+  hotssh_hostdb_path = g_build_filename (hotssh_config_dir, "hostdb.ini", NULL);
+  priv->hotssh_hostdb_path = g_file_new_for_path (hotssh_hostdb_path);
 
   if (!g_file_query_exists (priv->openssh_dir, NULL))
     {
@@ -486,11 +562,10 @@ hotssh_hostdb_init (HotSshHostDB *self)
         }
     }
   
+  load_hostdb (self);
+
   priv->knownhosts_monitor = monitor_file_bind_noerror (self, priv->openssh_knownhosts_path,
                                                         on_knownhosts_changed);
-
-  priv->hotssh_extradb_monitor = monitor_file_bind_noerror (self, priv->hotssh_extradb,
-                                                            on_extradb_changed);
 }
 
 static void
@@ -498,7 +573,8 @@ hotssh_hostdb_dispose (GObject *object)
 {
   HotSshHostDBPrivate *priv = hotssh_hostdb_get_instance_private ((HotSshHostDB*)object);
 
-  g_clear_pointer (&priv->extradb, g_key_file_unref);
+  g_clear_pointer (&priv->hostdb, g_key_file_unref);
+  g_clear_pointer (&priv->openssh_knownhosts_path, g_hash_table_unref);
   g_clear_object (&priv->model);
 
   G_OBJECT_CLASS (hotssh_hostdb_parent_class)->dispose (object);
diff --git a/src/hotssh-hostdb.h b/src/hotssh-hostdb.h
index 217834d..459491d 100644
--- a/src/hotssh-hostdb.h
+++ b/src/hotssh-hostdb.h
@@ -28,14 +28,39 @@
 typedef struct _HotSshHostDB          HotSshHostDB;
 typedef struct _HotSshHostDBClass     HotSshHostDBClass;
 
+enum {
+  HOTSSH_HOSTDB_COLUMN_ID,
+  HOTSSH_HOSTDB_COLUMN_HOSTNAME,
+  HOTSSH_HOSTDB_COLUMN_PORT,
+  HOTSSH_HOSTDB_COLUMN_USERNAME,
+  HOTSSH_HOSTDB_COLUMN_LAST_USED,
+  HOTSSH_HOSTDB_COLUMN_IS_KNOWN,
+  HOTSSH_HOSTDB_COLUMN_OPENSSH_KNOWNHOST_LINE
+};
+#define HOTSSH_HOSTDB_N_COLUMNS (HOTSSH_HOSTDB_COLUMN_OPENSSH_KNOWNHOST_LINE+1)
+
 GType                   hotssh_hostdb_get_type     (void);
 HotSshHostDB           *hotssh_hostdb_get_instance (void);
 
 GtkTreeModel           *hotssh_hostdb_get_model    (HotSshHostDB *self);
 
-void                    hotssh_hostdb_host_used    (HotSshHostDB *self,
-                                                    const char   *hostname);
-void                    hotssh_hostdb_set_username    (HotSshHostDB *self,
-                                                       const char   *hostname,
-                                                       const char   *username);
+void                    hotssh_hostdb_add_entry    (HotSshHostDB    *self,
+                                                    const char      *username,
+                                                    GNetworkAddress *address,
+                                                    char            **out_id);
+
+gboolean               hotssh_hostdb_lookup_by_id (HotSshHostDB   *self,
+                                                   const char     *id,
+                                                   GtkTreeIter    *out_iter);
+
+void                   hotssh_hostdb_set_entry_basic (HotSshHostDB    *self,
+                                                      const char      *id,
+                                                      const char      *username,
+                                                      GNetworkAddress *address);
+
+void                   hotssh_hostdb_update_last_used    (HotSshHostDB    *self,
+                                                          const char      *id);
 
+void                   hotssh_hostdb_set_entry_known    (HotSshHostDB    *self,
+                                                         const char      *id,
+                                                         gboolean         known);
diff --git a/src/hotssh-tab.c b/src/hotssh-tab.c
index d2eb717..e00d875 100644
--- a/src/hotssh-tab.c
+++ b/src/hotssh-tab.c
@@ -56,6 +56,7 @@ typedef struct _HotSshTabPrivate HotSshTabPrivate;
 
 typedef enum {
   HOTSSH_TAB_PAGE_NEW_CONNECTION,
+  HOTSSH_TAB_PAGE_LIST_CONNECTIONS,
   HOTSSH_TAB_PAGE_CONNECTING,
   HOTSSH_TAB_PAGE_ERROR,
   HOTSSH_TAB_PAGE_HOSTKEY,
@@ -72,7 +73,8 @@ struct _HotSshTabPrivate
   /* Bound via template */
   GtkWidget *host_entry;
   GtkWidget *username_entry;
-  GtkWidget *connect_button;
+  GtkWidget *create_and_connect_button;
+  GtkWidget *add_new_connection_button;
   GtkWidget *connection_text_container;
   GtkWidget *connection_text;
   GtkWidget *error_text;
@@ -87,13 +89,20 @@ struct _HotSshTabPrivate
   GtkWidget *approve_hostkey_button;
   GtkWidget *disapprove_hostkey_button;
   GtkWidget *terminal_box;
+  GtkWidget *connections_treeview;
+  GtkWidget *hostname_column;
+  GtkWidget *hostname_renderer;
+  GtkWidget *lastused_column;
+  GtkWidget *lastused_renderer;
 
   /* State */
   HotSshTabPage active_page;
   guint authmechanism_index;
 
   gboolean indisposed;
+  char *connection_id;
   char *hostname;
+  char *username;
   GtkEntryCompletion *host_completion;
   GSocketConnectable *address;
   GSshConnection *connection;
@@ -157,7 +166,7 @@ state_reset_for_new_connection (HotSshTab                *self)
 {
   HotSshTabPrivate *priv = hotssh_tab_get_instance_private (self);
   g_debug ("reset state");
-  g_clear_pointer (&priv->hostname, g_free);
+  g_clear_pointer (&priv->connection_id, g_free);
   g_clear_object (&priv->address);
   g_clear_object (&priv->connection);
   g_clear_object (&priv->cancellable);
@@ -189,6 +198,7 @@ page_transition (HotSshTab        *self,
   priv->active_page = new_page;
 
   if (priv->active_page == HOTSSH_TAB_PAGE_NEW_CONNECTION
+      || priv->active_page == HOTSSH_TAB_PAGE_LIST_CONNECTIONS
       || priv->active_page == HOTSSH_TAB_PAGE_ERROR)
     state_reset_for_new_connection (self);
 
@@ -502,6 +512,13 @@ on_socket_client_event (GSocketClient      *client,
       break;
     }
 }
+
+static void
+on_add_new_connection (GtkButton     *button,
+                       HotSshTab  *self)
+{
+  page_transition (self, HOTSSH_TAB_PAGE_NEW_CONNECTION);
+}
                         
 static void
 on_connect (GtkButton     *button,
@@ -510,11 +527,11 @@ on_connect (GtkButton     *button,
   HotSshTabPrivate *priv = hotssh_tab_get_instance_private (self);
   GError *local_error = NULL;
   GError **error = &local_error;
+  GtkTreeIter iter;
+  gs_unref_object GNetworkAddress *address = NULL;
   const char *hostname;
   const char *username;
 
-  page_transition (self, HOTSSH_TAB_PAGE_CONNECTING);
-
   hostname = gtk_entry_get_text (GTK_ENTRY (priv->host_entry));
   username = gtk_entry_get_text (GTK_ENTRY (priv->username_entry));
 
@@ -522,18 +539,67 @@ on_connect (GtkButton     *button,
   priv->cancellable = g_cancellable_new ();
 
   g_clear_object (&priv->address);
-  priv->address = g_network_address_parse (hostname, 22, error);
-  if (!priv->address)
+  address = (GNetworkAddress*)g_network_address_parse (hostname, 22, error);
+  if (!address)
     {
       page_transition_take_error (self, local_error);
       return;
     }
 
-  priv->hostname = g_strdup (hostname);
+  g_clear_pointer (&priv->connection_id, g_free);
+  hotssh_hostdb_add_entry (hotssh_hostdb_get_instance (),
+                           username,
+                           (GNetworkAddress*)address,
+                           &priv->connection_id);
+  hotssh_hostdb_lookup_by_id (hotssh_hostdb_get_instance (),
+                              priv->connection_id, &iter);
+  /*
+  gtk_tree_selection_select_iter (gtk_tree_view_get_selection ((GtkTreeView*)priv->connections_treeview),
+                                  &iter);
+  */
+
+  page_transition (self, HOTSSH_TAB_PAGE_LIST_CONNECTIONS);
+}
+
+static void
+on_connection_row_activated (GtkTreeView       *tree_view,
+                             GtkTreePath       *path,
+                             GtkTreeViewColumn *column,
+                             gpointer           user_data)
+{
+  HotSshTab *self = user_data;
+  HotSshTabPrivate *priv = hotssh_tab_get_instance_private (self);
+  GtkTreeIter iter;
+  guint port;
+  gs_unref_object GSocketConnectable *address = NULL;
+  gs_unref_object GtkTreeModel *model = NULL;
+
+  model = hotssh_hostdb_get_model (hotssh_hostdb_get_instance ());
+
+  g_assert (gtk_tree_model_get_iter (model, &iter, path));
+
+  g_clear_pointer (&priv->connection_id, g_free);
+  g_clear_pointer (&priv->hostname, g_free);
+  g_clear_pointer (&priv->username, g_free);
+  gtk_tree_model_get (model, &iter,
+                      HOTSSH_HOSTDB_COLUMN_ID,
+                      &priv->connection_id,
+                      HOTSSH_HOSTDB_COLUMN_HOSTNAME,
+                      &priv->hostname,
+                      HOTSSH_HOSTDB_COLUMN_PORT,
+                      &port,
+                      HOTSSH_HOSTDB_COLUMN_USERNAME,
+                      &priv->username,
+                      -1);
+
+  g_clear_object (&priv->address);
+  priv->address = g_network_address_new (priv->hostname, port);
+
   g_object_notify ((GObject*)self, "hostname");
 
+  page_transition (self, HOTSSH_TAB_PAGE_CONNECTING);
   g_clear_object (&priv->connection);
-  priv->connection = gssh_connection_new (priv->address, username); 
+  priv->connection = gssh_connection_new (priv->address, priv->username); 
   g_signal_connect (gssh_connection_get_socket_client (priv->connection),
                     "event", G_CALLBACK (on_socket_client_event), self);
   gssh_connection_set_interaction (priv->connection, (GTlsInteraction*)priv->password_interaction);
@@ -543,6 +609,9 @@ on_connect (GtkButton     *button,
   g_debug ("connected, beginning handshake");
   gssh_connection_handshake_async (priv->connection, priv->cancellable,
                                   on_connection_handshake, self);
+  
+  hotssh_hostdb_update_last_used (hotssh_hostdb_get_instance (),
+                                  priv->connection_id);
 }
 
 static void
@@ -633,7 +702,7 @@ on_connect_cancel (GtkButton     *button,
                   gpointer       user_data)
 {
   HotSshTab *self = user_data;
-  page_transition (self, HOTSSH_TAB_PAGE_NEW_CONNECTION);
+  page_transition (self, HOTSSH_TAB_PAGE_LIST_CONNECTIONS);
 }
 
 static void
@@ -663,8 +732,8 @@ on_approve_hostkey_clicked (GtkButton     *button,
   HotSshTab *self = user_data;
   HotSshTabPrivate *priv = hotssh_tab_get_instance_private (self);
 
-  hotssh_hostdb_host_used (hotssh_hostdb_get_instance (),
-                           priv->hostname);
+  hotssh_hostdb_set_entry_known (hotssh_hostdb_get_instance (),
+                                 priv->connection_id, TRUE);
 
   gssh_connection_negotiate_async (priv->connection, priv->cancellable,
                                    on_negotiate_complete, self);
@@ -810,7 +879,8 @@ hotssh_tab_init (HotSshTab *self)
 
   gtk_notebook_set_show_tabs ((GtkNotebook*)self, FALSE);
 
-  g_signal_connect (priv->connect_button, "clicked", G_CALLBACK (on_connect), self);
+  g_signal_connect (priv->create_and_connect_button, "clicked", G_CALLBACK (on_connect), self);
+  g_signal_connect (priv->add_new_connection_button, "clicked", G_CALLBACK (on_add_new_connection), self);
   g_signal_connect (priv->connect_cancel_button, "clicked", G_CALLBACK (on_connect_cancel), self);
   g_signal_connect (priv->error_disconnect, "clicked", G_CALLBACK (on_connect_cancel), self);
   g_signal_connect (priv->auth_cancel_button, "clicked", G_CALLBACK (on_connect_cancel), self);
@@ -818,6 +888,7 @@ hotssh_tab_init (HotSshTab *self)
   g_signal_connect (priv->disapprove_hostkey_button, "clicked", G_CALLBACK (on_connect_cancel), self);
   g_signal_connect_swapped (priv->password_entry, "activate", G_CALLBACK (submit_password), self);
   g_signal_connect_swapped (priv->password_submit, "clicked", G_CALLBACK (submit_password), self);
+  g_signal_connect (priv->connections_treeview, "row-activated", G_CALLBACK (on_connection_row_activated), 
self);
 
   priv->password_interaction = hotssh_password_interaction_new ((GtkEntry*)priv->password_entry);
 
@@ -851,25 +922,112 @@ hotssh_tab_dispose (GObject *object)
 
   priv->indisposed = TRUE;
 
-  page_transition (self, HOTSSH_TAB_PAGE_NEW_CONNECTION);
+  page_transition (self, HOTSSH_TAB_PAGE_LIST_CONNECTIONS);
 
   g_clear_object (&priv->host_completion);
 
   G_OBJECT_CLASS (hotssh_tab_parent_class)->dispose (object);
 }
 
+static const char *
+seconds_to_time_ago_format (gulong *seconds)
+{
+  
+  if (*seconds < 60)
+    return _("Less than 1 minute ago");
+  *seconds /= 60;
+  if (*seconds < 60)
+    return ngettext ("%d minute ago", "%d minutes ago",
+                     *seconds);
+  *seconds /= 60;
+  if (*seconds < 24)
+    return ngettext ("%d hour ago", "%d hours ago",
+                     *seconds);
+  *seconds /= 24;
+  return ngettext ("%d day ago", "%d days ago",
+                   *seconds);
+}
+
+static void
+render_last_used (GtkTreeViewColumn *tree_column,
+                  GtkCellRenderer *cell,
+                  GtkTreeModel *tree_model,
+                  GtkTreeIter *iter,
+                  gpointer data)
+{
+  guint64 last_used;
+  guint64 current_time;
+  gs_free char *formatted_text = NULL;
+  const char *text;
+
+  gtk_tree_model_get (tree_model, iter,
+                      HOTSSH_HOSTDB_COLUMN_LAST_USED,
+                      &last_used,
+                      -1);
+  if (last_used == 0)
+    {
+      text = _("Never");
+    }
+  else
+    {
+      current_time = g_get_real_time () / G_USEC_PER_SEC;
+      if (current_time < last_used)
+        text = _("In the future");
+      else
+        {
+          gulong diff = (gulong)(current_time - last_used);
+          const char *fmt = seconds_to_time_ago_format (&diff);
+
+          text = formatted_text = g_strdup_printf (fmt, diff);
+        }
+    }
+
+  g_object_set (cell, "text", text, NULL);
+}
+
+static void
+hotssh_tab_constructed (GObject *object)
+{
+  HotSshTab *self = HOTSSH_TAB (object);
+  HotSshTabPrivate *priv = hotssh_tab_get_instance_private (self);
+  gs_unref_object GtkTreeModel *model = NULL;
+
+  if (G_OBJECT_CLASS (hotssh_tab_parent_class)->constructed)
+    G_OBJECT_CLASS (hotssh_tab_parent_class)->constructed (object);
+
+  page_transition (self, HOTSSH_TAB_PAGE_LIST_CONNECTIONS);
+
+  model = hotssh_hostdb_get_model (hotssh_hostdb_get_instance ());
+  gtk_tree_view_set_model ((GtkTreeView*)priv->connections_treeview, model);
+
+  gtk_tree_view_column_add_attribute ((GtkTreeViewColumn*)priv->hostname_column,
+                                      (GtkCellRenderer*)priv->hostname_renderer,
+                                      "text", 1);
+  gtk_tree_view_column_set_cell_data_func ((GtkTreeViewColumn*)priv->lastused_column,
+                                           (GtkCellRenderer*)priv->lastused_renderer,
+                                           render_last_used,
+                                           self, NULL);
+}
+
 static void
 hotssh_tab_class_init (HotSshTabClass *class)
 {
   G_OBJECT_CLASS (class)->get_property = hotssh_tab_get_property;
   G_OBJECT_CLASS (class)->dispose = hotssh_tab_dispose;
+  G_OBJECT_CLASS (class)->constructed = hotssh_tab_constructed;
 
   gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
                                                "/org/gnome/hotssh/tab.ui");
 
+  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, connections_treeview);
+  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, hostname_column);
+  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, hostname_renderer);
+  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, lastused_column);
+  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, lastused_renderer);
   gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, host_entry);
   gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, username_entry);
-  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, connect_button);
+  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, 
create_and_connect_button);
+  gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, 
add_new_connection_button);
   gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, 
connection_text_container);
   gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, connection_text);
   gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), HotSshTab, error_text);
@@ -919,7 +1077,7 @@ hotssh_tab_new_channel  (HotSshTab *source)
 void
 hotssh_tab_disconnect  (HotSshTab *self)
 {
-  page_transition (self, HOTSSH_TAB_PAGE_NEW_CONNECTION);
+  page_transition (self, HOTSSH_TAB_PAGE_LIST_CONNECTIONS);
 }
 
 const char *
diff --git a/src/tab.ui b/src/tab.ui
index 04c50b7..b467397 100644
--- a/src/tab.ui
+++ b/src/tab.ui
@@ -2,6 +2,12 @@
 <!-- Generated with glade 3.16.0 -->
 <interface>
   <requires lib="gtk+" version="3.10"/>
+  <object class="GtkListStore" id="liststore1">
+    <columns>
+      <!-- column-name hostname -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
   <template class="HotSshTab" parent="GtkNotebook">
     <property name="can_focus">False</property>
     <child>
@@ -93,17 +99,47 @@
                     <property name="halign">center</property>
                     <property name="margin_top">8</property>
                     <child>
-                      <object class="GtkButton" id="connect_button">
-                        <property name="label" translatable="yes">_Connect</property>
+                      <object class="GtkBox" id="box12">
                         <property name="visible">True</property>
-                        <property name="can_focus">True</property>
-                        <property name="can_default">True</property>
-                        <property name="receives_default">True</property>
-                        <property name="halign">end</property>
-                        <property name="use_underline">True</property>
-                        <style>
-                          <class name="suggested-action"/>
-                        </style>
+                        <property name="can_focus">False</property>
+                        <child>
+                          <object class="GtkButton" id="connect_cancel_button1">
+                            <property name="label">gtk-cancel</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">True</property>
+                            <property name="halign">end</property>
+                            <property name="valign">center</property>
+                            <property name="use_stock">True</property>
+                            <property name="image_position">right</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="create_and_connect_button">
+                            <property name="label" translatable="yes">_Create and Connect</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="can_default">True</property>
+                            <property name="receives_default">True</property>
+                            <property name="halign">start</property>
+                            <property name="valign">center</property>
+                            <property name="margin_left">8</property>
+                            <property name="use_underline">True</property>
+                            <style>
+                              <class name="suggested-action"/>
+                            </style>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
                       </object>
                     </child>
                   </object>
@@ -124,6 +160,72 @@
         </child>
       </object>
     </child>
+    <child type="tab">
+      <placeholder/>
+    </child>
+    <child>
+      <object class="GtkBox" id="box9">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkTreeView" id="connections_treeview">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="model">liststore1</property>
+            <child internal-child="selection">
+              <object class="GtkTreeSelection" id="treeview-selection"/>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn" id="hostname_column">
+                <property name="title" translatable="yes">Hostname</property>
+                <child>
+                  <object class="GtkCellRendererText" id="hostname_renderer"/>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn" id="lastused_column">
+                <property name="title" translatable="yes">Last Used</property>
+                <property name="sort_indicator">True</property>
+                <property name="sort_column_id">4</property>
+                <child>
+                  <object class="GtkCellRendererText" id="lastused_renderer"/>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="add_new_connection_button">
+            <property name="label" translatable="yes">Add New Connection</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="halign">end</property>
+            <property name="valign">center</property>
+            <property name="margin_right">8</property>
+            <property name="margin_bottom">8</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child type="tab">
+      <placeholder/>
+    </child>
     <child>
       <object class="GtkAlignment" id="connection_text_container">
         <property name="visible">True</property>
@@ -169,9 +271,12 @@
         </child>
       </object>
       <packing>
-        <property name="position">1</property>
+        <property name="position">2</property>
       </packing>
     </child>
+    <child type="tab">
+      <placeholder/>
+    </child>
     <child>
       <object class="GtkBox" id="box2">
         <property name="visible">True</property>
@@ -236,9 +341,12 @@
         </child>
       </object>
       <packing>
-        <property name="position">2</property>
+        <property name="position">3</property>
       </packing>
     </child>
+    <child type="tab">
+      <placeholder/>
+    </child>
     <child>
       <object class="GtkBox" id="box4">
         <property name="visible">True</property>
@@ -343,9 +451,12 @@
         </child>
       </object>
       <packing>
-        <property name="position">3</property>
+        <property name="position">4</property>
       </packing>
     </child>
+    <child type="tab">
+      <placeholder/>
+    </child>
     <child>
       <object class="GtkBox" id="box1">
         <property name="visible">True</property>
@@ -443,9 +554,12 @@
         </child>
       </object>
       <packing>
-        <property name="position">4</property>
+        <property name="position">5</property>
       </packing>
     </child>
+    <child type="tab">
+      <placeholder/>
+    </child>
     <child>
       <object class="GtkBox" id="terminal_outer_vbox">
         <property name="visible">True</property>
@@ -468,9 +582,12 @@
         </child>
       </object>
       <packing>
-        <property name="position">5</property>
+        <property name="position">6</property>
       </packing>
     </child>
+    <child type="tab">
+      <placeholder/>
+    </child>
   </template>
   <object class="GtkSizeGroup" id="password_entry_submit_size_group">
     <property name="mode">vertical</property>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]