diff --git a/src/Makefile.am b/src/Makefile.am
index 16766d3..73c78e7 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -6,6 +6,8 @@ bin_PROGRAMS = gnome-terminal
 libexec_PROGRAMS = gnome-terminal-server
 noinst_PROGRAMS =
 
+check_PROGRAMS = terminal-screen
+
 if WITH_NAUTILUS_EXTENSION
 nautilusextension_LTLIBRARIES = libterminal-nautilus.la
 endif # WITH_NAUTILUS_EXTENSION
@@ -149,6 +151,77 @@ terminal-gdbus-generated.c terminal-gdbus-generated.h: org.gnome.Terminal.xml Ma
 terminal-resources.h terminal-resources.c: terminal.gresource.xml Makefile $(shell $(GLIB_COMPILE_RESOURCES) --generate-dependencies --sourcedir $(srcdir) $(srcdir)/terminal.gresource.xml)
 	$(AM_V_GEN) XMLLINT=$(XMLLINT) $(GLIB_COMPILE_RESOURCES) --target $@ --sourcedir $(srcdir) --generate --c-name terminal $<
 
+terminal_screen_CPPFLAGS = \
+	-DTERMINAL_COMPILATION \
+	-DTERM_LOCALEDIR="\"$(datadir)/locale\"" \
+	-DTERM_HELPDIR="\"$(HELP_DIR)\"" \
+	-DTERM_LIBEXECDIR="\"$(libexecdir)\"" \
+	$(AM_CPPFLAGS)
+terminal_screen_SOURCES = \
+	eggshell.c \
+	eggshell.h \
+	profile-editor.c \
+	profile-editor.h \
+	terminal-accels.c \
+	terminal-accels.h \
+	terminal-app.c \
+	terminal-app.h \
+	terminal-debug.c \
+	terminal-debug.h \
+	terminal-encoding.c \
+	terminal-encoding.h \
+	terminal-gdbus.c \
+	terminal-gdbus.h \
+	terminal-gdbus-generated.c \
+	terminal-gdbus-generated.h \
+	terminal-icon-button.h \
+	terminal-icon-button.c \
+	terminal-info-bar.c \
+	terminal-info-bar.h \
+	terminal-marshal.c \
+	terminal-marshal.h \
+	terminal-mdi-container.c \
+	terminal-mdi-container.h \
+	terminal-notebook.c \
+	terminal-notebook.h \
+	terminal-prefs.c \
+	terminal-prefs.h \
+	terminal-profiles-list.c \
+	terminal-profiles-list.h \
+	terminal-schemas.h \
+	terminal-settings-list.c \
+	terminal-settings-list.h \
+	terminal-screen.c \
+	terminal-screen.h \
+	terminal-screen-container.c \
+	terminal-screen-container.h \
+	terminal-search-popover.c \
+	terminal-search-popover.h \
+	terminal-search-provider.c \
+	terminal-search-provider.h \
+	terminal-search-provider-gdbus-generated.c \
+	terminal-search-provider-gdbus-generated.h \
+	terminal-tab-label.c \
+	terminal-tab-label.h \
+	terminal-tabs-menu.c \
+	terminal-tabs-menu.h \
+	terminal-type-builtins.c \
+	terminal-type-builtins.h \
+	terminal-util.c \
+	terminal-util.h \
+	terminal-window.c \
+	terminal-window.h \
+	$(NULL)
+terminal_screen_CFLAGS = \
+	-DTERMINAL_SCREEN_MAIN \
+	$(TERM_CFLAGS) \
+	$(WARN_CFLAGS) \
+	$(AM_CFLAGS)
+terminal_screen_LDFLAGS = \
+	$(AM_LDFLAGS)
+terminal_screen_LDADD = \
+	$(TERM_LIBS)
+
 # Terminal client
 
 if ENABLE_GTERMINAL
diff --git a/src/terminal-screen.c b/src/terminal-screen.c
index 9aa7bbe..4ca137c 100644
--- a/src/terminal-screen.c
+++ b/src/terminal-screen.c
@@ -165,17 +165,42 @@ static void terminal_screen_set_override_command (TerminalScreen  *screen,
 
 static guint signals[LAST_SIGNAL];
 
-#define USERCHARS "-[:alnum:]"
-#define USERCHARS_CLASS "[" USERCHARS "]"
+#define SCHEME "(?:news:|telnet:|nntp:|file:\\/|https?:|ftps?:|sftp:|webcal:)"
+
+#define USERCHARS "-.[:alnum:]"
+/* Nonempty username, e.g. "john.smith" */
+#define USER "[" USERCHARS "]+"
+
 #define PASSCHARS_CLASS "[-[:alnum:]\\Q,?;.:/!%$^*&~\"#'\\E]"
-#define HOSTCHARS_CLASS "[-[:alnum:]]"
-#define HOST HOSTCHARS_CLASS "+(\\." HOSTCHARS_CLASS "+)*"
+/* Optional colon-prefixed password. I guess empty password should be allowed, right? E.g. ":secret", ":", "" */
+#define PASS "(?::" PASSCHARS_CLASS "*)?"
+
+/* Optional at-terminated username (with perhaps a password too), e.g. "joe@", "pete:secret@", "" */
+/* TODO: move '.' to USERCHARS, adjust e-mail pattern */
+/* TODO: why is @ backslashed? */
+#define USERPASS "(?:" USER PASS "\\@)?"
+
+/* Decimal IPv4, e.g. "1.2.3.4" */
+#define IPV4 "(?:[[:digit:]]{1,3}\\.){3}[[:digit:]]{1,3}"
+/* IPv6, including embedded IPv4, e.g. "::1", "dead:beef::1.2.3.4"
+ * TODO: add zone_id: RFC 4007 section 11, RFC 6874 */
+#define IPV6 "[:[:xdigit:]]+(?::" IPV4 ")?"
+
+#define HOSTNAMECHARS_CLASS "[-[:alnum:]]"
+/* Note: currently HOSTNAME matches IPv4 addresses too. No problem */
+#define HOSTNAME HOSTNAMECHARS_CLASS "+(\\." HOSTNAMECHARS_CLASS "+)*"
+
+/* Hostname, IPv4, or bracket-enclosed IPv6, e.g. "example.com", "1.2.3.4", "[::1]" */
+#define HOST "(?:" HOSTNAME "|" IPV4 "|\\[" IPV6 "\\])"
+
+/* Optional colon-prefixed port, e.g. ":1080", "" */
 #define PORT "(?:\\:[[:digit:]]{1,5})?"
+
 #define PATHCHARS_CLASS "[-[:alnum:]\\Q_$.+!*,:;@&=?/~#%\\E]"
-#define PATHTERM_CLASS "[^\\Q]'.:}>) \t\r\n,\"\\E]"
-#define SCHEME "(?:news:|telnet:|nntp:|file:\\/|https?:|ftps?:|sftp:|webcal:)"
-#define USERPASS USERCHARS_CLASS "+(?:" PASSCHARS_CLASS "+)?"
-#define URLPATH   "(?:(/"PATHCHARS_CLASS"+(?:[(]"PATHCHARS_CLASS"*[)])*"PATHCHARS_CLASS"*)*"PATHTERM_CLASS")?"
+/* Chars preferably not at the end of a URL */
+#define PATHTERM_CLASS "[\\Q.!,?\\E]"
+
+#define URLPATH "(:?/" PATHCHARS_CLASS "*)?(?<!" PATHTERM_CLASS ")"
 
 typedef struct {
   const char *pattern;
@@ -184,10 +209,10 @@ typedef struct {
 } TerminalRegexPattern;
 
 static const TerminalRegexPattern url_regex_patterns[] = {
-  { SCHEME "//(?:" USERPASS "\\@)?" HOST PORT URLPATH, FLAVOR_AS_IS, TRUE },
-  { "(?:www|ftp)" HOSTCHARS_CLASS "*\\." HOST PORT URLPATH , FLAVOR_DEFAULT_TO_HTTP, TRUE  },
-  { "(?:callto:|h323:|sip:)" USERCHARS_CLASS "[" USERCHARS ".]*(?:" PORT "/[a-z0-9]+)?\\@" HOST, FLAVOR_VOIP_CALL, TRUE },
-  { "(?:mailto:)?" USERCHARS_CLASS "[" USERCHARS ".]*\\@" HOSTCHARS_CLASS "+\\." HOST, FLAVOR_EMAIL, TRUE },
+  { SCHEME "//" USERPASS HOST PORT URLPATH, FLAVOR_AS_IS, TRUE },
+  { "(?:www|ftp)" HOSTNAMECHARS_CLASS "*\\." HOST PORT URLPATH , FLAVOR_DEFAULT_TO_HTTP, TRUE  },
+  { "(?:callto:|h323:|sip:)" USER "(?:" PORT "/[a-z0-9]+)?\\@" HOST, FLAVOR_VOIP_CALL, TRUE },
+  { "(?:mailto:)?" USER "\\@" HOSTNAMECHARS_CLASS "+\\." HOST, FLAVOR_EMAIL, TRUE },
   { "(?:news:|man:|info:)[-[:alnum:]\\Q^_{|}~!\"#$%&'()*+,./;:=?`\\E]+", FLAVOR_AS_IS, TRUE },
 };
 
@@ -2107,3 +2132,117 @@ terminal_screen_get_uuid (TerminalScreen *screen)
 
   return screen->priv->uuid;
 }
+
+/******************************************************************************/
+
+#ifdef TERMINAL_SCREEN_MAIN
+
+#define ENTIRE ((char *) 1)
+
+static const char* regex_checks[] = {
+  "There's no URL here http:/foo",          NULL,
+  "Visit http://example.com for details",   "http://example.com",
+  "Trailing dot http://foo/bar.html.",      "http://foo/bar.html",
+  "Trailing ellipsis http://foo/bar.html...",  "http://foo/bar.html",
+  "See <http://foo/bar>",                   "http://foo/bar",
+  "<http://foo.bar/asdf.qwer.html>",        "http://foo.bar/asdf.qwer.html",
+
+  "http://aa.bb/c",                         ENTIRE,
+
+  /* Valid IP addresses */
+  "http://111.222.33.44/foo",               ENTIRE,
+  "http://1.2.3.4:5555/xyz",                ENTIRE,
+  "http://[::1]/ipv6",                      ENTIRE,
+  "http://[dead::be:ef]",                   ENTIRE,
+  "http://[dead:beef::192.168.1.1]:53/",    ENTIRE,
+
+  /* Invalid IP addresses (note: our regexes give quite a few false positive matches,
+   * so we cannot test most of the invalid addresses) */
+  "http://dead::beef/nope",                 "http://dead",
+  "http://[living:beef::]/",                NULL,
+  "http://[192.168.1.1]/",                  NULL,
+
+  /* Username, password */
+  "http://joe@example.com",                 ENTIRE,
+  "http://user.name:sec.ret@host.name",     ENTIRE,
+  "http://joe:secret@[::1]",                ENTIRE,
+  "http://dudewithnopassword:@example.com", ENTIRE,
+  "http://safeguy:!#$%^&*@host",            ENTIRE,
+  "http://invalidusername!@host",           "http://invalidusername",
+
+  /* Email */
+  "Write to foo@bar.com.",                  "foo@bar.com",
+  "Write to <foo@bar.com>",                 "foo@bar.com",
+  "Write to mailto:foo@bar.com.",           "mailto:foo@bar.com",
+};
+
+#ifdef WITH_PCRE2
+  XXX
+#else
+static char *
+check_regex_one (GRegex *regex,
+                 const gchar *input)
+{
+  GMatchInfo *match_info;
+  char *ret = NULL;
+
+  if (g_regex_match (regex, input, 0, &match_info))
+    {
+      ret = g_strdup (g_match_info_fetch (match_info, 0));
+    }
+  g_match_info_free (match_info);
+  return ret;
+}
+#endif
+
+static char *
+check_regex_all (
+#ifdef WITH_PCRE2
+                 VteRegex **regexes,
+#else
+                 GRegex **regexes,
+#endif
+                 int len,
+                 const gchar *input)
+{
+  int i;
+  char *ret;
+
+  for (i = 0; i < len; i++)
+    {
+      ret = check_regex_one (regexes[i], input);
+      if (ret != NULL)
+        return ret;
+    }
+  return NULL;
+}
+
+int
+main (int argc, char **argv)
+{
+  guint i;
+  const char *input, *expected_match;
+  char *match;
+
+  n_url_regexes = G_N_ELEMENTS (url_regex_patterns);
+  precompile_regexes (url_regex_patterns, n_url_regexes, &url_regexes, &url_regex_flavors);
+
+  for (i = 0; i < G_N_ELEMENTS (regex_checks); i += 2)
+    {
+      input = regex_checks[i];
+      expected_match = regex_checks[i + 1];
+      if (expected_match == ENTIRE)
+        expected_match = input;
+
+      match = check_regex_all (url_regexes, n_url_regexes, input);
+
+      g_assert_cmpstr (match, ==, expected_match);
+
+      g_free (match);
+    }
+
+  printf("terminal-screen regex tests passed :)\n");
+  return 0;
+}
+
+#endif /* TERMINAL_SCREEN_MAIN */
