aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2019-02-25 18:48:31 +0100
committerJason A. Donenfeld <Jason@zx2c4.com>2019-02-28 08:05:02 +0100
commit8ddde70f2d354c9179e7434bc4a630d8acc1b370 (patch)
tree4651f4fe9984b671cfb95f2274ece6ef885d8332 /ui
parentipc: add base of IPC (diff)
downloadwireguard-windows-8ddde70f2d354c9179e7434bc4a630d8acc1b370.tar.xz
wireguard-windows-8ddde70f2d354c9179e7434bc4a630d8acc1b370.zip
ui: add initial sample UI for testing
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'ui')
-rw-r--r--ui/highlighter.c620
-rw-r--r--ui/highlighter.h37
-rw-r--r--ui/icon/128.pngbin0 -> 8419 bytes
-rw-r--r--ui/icon/16.pngbin0 -> 869 bytes
-rw-r--r--ui/icon/256.pngbin0 -> 17786 bytes
-rw-r--r--ui/icon/32.pngbin0 -> 1951 bytes
-rw-r--r--ui/icon/48.pngbin0 -> 2986 bytes
-rw-r--r--ui/icon/64.pngbin0 -> 3922 bytes
-rw-r--r--ui/icon/icon.icobin0 -> 117355 bytes
-rw-r--r--ui/manifest.xml14
-rw-r--r--ui/raise.go54
-rw-r--r--ui/syntaxedit.c311
-rw-r--r--ui/syntaxedit.go153
-rw-r--r--ui/syntaxedit.h23
-rw-r--r--ui/ui.go140
15 files changed, 1352 insertions, 0 deletions
diff --git a/ui/highlighter.c b/ui/highlighter.c
new file mode 100644
index 00000000..1913e359
--- /dev/null
+++ b/ui/highlighter.c
@@ -0,0 +1,620 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "highlighter.h"
+
+typedef struct {
+ const char *s;
+ size_t len;
+} string_span_t;
+
+static bool is_decimal(char c)
+{
+ return c >= '0' && c <= '9';
+}
+
+static bool is_hexadecimal(char c)
+{
+ return is_decimal(c) || ((c | 32) >= 'a' && (c | 32) <= 'f');
+}
+
+static bool is_alphabet(char c)
+{
+ return (c | 32) >= 'a' && (c | 32) <= 'z';
+}
+
+static bool is_same(string_span_t s, const char *c)
+{
+ size_t len = strlen(c);
+
+ if (len != s.len)
+ return false;
+ return !memcmp(s.s, c, len);
+}
+
+static bool is_caseless_same(string_span_t s, const char *c)
+{
+ size_t len = strlen(c);
+
+ if (len != s.len)
+ return false;
+ for (size_t i = 0; i < len; ++i) {
+ char a = c[i], b = s.s[i];
+ if ((unsigned)a - 'a' < 26)
+ a &= 95;
+ if ((unsigned)b - 'a' < 26)
+ b &= 95;
+ if (a != b)
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_key(string_span_t s)
+{
+ if (s.len != 44 || s.s[43] != '=')
+ return false;
+
+ for (size_t i = 0; i < 43; ++i) {
+ if (!is_decimal(s.s[i]) && !is_alphabet(s.s[i]) &&
+ s.s[i] != '/' && s.s[i] != '+')
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_hostname(string_span_t s)
+{
+ size_t num_digit = 0, num_entity = s.len;
+
+ if (s.len > 63 || !s.len)
+ return false;
+ if (s.s[0] == '-' || s.s[s.len - 1] == '-')
+ return false;
+ if (s.s[0] == '.' || s.s[s.len - 1] == '.')
+ return false;
+
+ for (size_t i = 0; i < s.len; ++i) {
+ if (is_decimal(s.s[i])) {
+ ++num_digit;
+ continue;
+ }
+ if (s.s[i] == '.') {
+ --num_entity;
+ continue;
+ }
+
+ if (!is_alphabet(s.s[i]) && s.s[i] != '-')
+ return false;
+
+ if (i && s.s[i] == '.' && s.s[i - 1] == '.')
+ return false;
+ }
+ return num_digit != num_entity;
+}
+
+static bool is_valid_ipv4(string_span_t s)
+{
+ for (size_t j, i = 0, pos = 0; i < 4 && pos < s.len; ++i) {
+ uint32_t val = 0;
+
+ for (j = 0; j < 3 && pos + j < s.len && is_decimal(s.s[pos + j]); ++j)
+ val = 10 * val + s.s[pos + j] - '0';
+ if (j == 0 || (j > 1 && s.s[pos] == '0') || val > 255)
+ return false;
+ if (pos + j == s.len && i == 3)
+ return true;
+ if (s.s[pos + j] != '.')
+ return false;
+ pos += j + 1;
+ }
+ return false;
+}
+
+static bool is_valid_ipv6(string_span_t s)
+{
+ size_t pos = 0;
+ bool seen_colon = false;
+
+ if (s.len < 2)
+ return false;
+ if (s.s[pos] == ':' && s.s[++pos] != ':')
+ return false;
+ if (s.s[s.len - 1] == ':' && s.s[s.len - 2] != ':')
+ return false;
+
+ for (size_t j, i = 0; pos < s.len; ++i) {
+ if (s.s[pos] == ':' && !seen_colon) {
+ seen_colon = true;
+ if (++pos == s.len)
+ break;
+ if (i == 7)
+ return false;
+ continue;
+ }
+ for (j = 0; j < 4 && pos + j < s.len && is_hexadecimal(s.s[pos + j]); ++j);
+ if (j == 0)
+ return false;
+ if (pos + j == s.len && (seen_colon || i == 7))
+ break;
+ if (i == 7)
+ return false;
+ if (s.s[pos + j] != ':') {
+ if (s.s[pos + j] != '.' || (i < 6 && !seen_colon))
+ return false;
+ return is_valid_ipv4((string_span_t){ s.s + pos, s.len - pos });
+ }
+ pos += j + 1;
+ }
+ return true;
+}
+
+static bool is_valid_uint(string_span_t s, bool support_hex, uint64_t min, uint64_t max)
+{
+ uint64_t val = 0;
+
+ /* Bound this around 32 bits, so that we don't have to write overflow logic. */
+ if (s.len > 10 || !s.len)
+ return false;
+
+ if (support_hex && s.len > 2 && s.s[0] == '0' && s.s[1] == 'x') {
+ for (size_t i = 2; i < s.len; ++i) {
+ if ((unsigned)s.s[i] - '0' < 10)
+ val = 16 * val + (s.s[i] - '0');
+ else if (((unsigned)s.s[i] | 32) - 'a' < 6)
+ val = 16 * val + (s.s[i] | 32) - 'a' + 10;
+ else
+ return false;
+ }
+ } else {
+ for (size_t i = 0; i < s.len; ++i) {
+ if (!is_decimal(s.s[i]))
+ return false;
+ val = 10 * val + s.s[i] - '0';
+ }
+ }
+ return val <= max && val >= min;
+}
+
+static bool is_valid_port(string_span_t s)
+{
+ return is_valid_uint(s, false, 0, 65535);
+}
+
+static bool is_valid_mtu(string_span_t s)
+{
+ return is_valid_uint(s, false, 576, 65535);
+}
+
+static bool is_valid_persistentkeepalive(string_span_t s)
+{
+ if (is_same(s, "off"))
+ return true;
+ return is_valid_uint(s, false, 0, 65535);
+}
+
+#ifndef MOBILE_WGQUICK_SUBSET
+
+static bool is_valid_fwmark(string_span_t s)
+{
+ if (is_same(s, "off"))
+ return true;
+ return is_valid_uint(s, true, 0, 4294967295);
+}
+
+static bool is_valid_table(string_span_t s)
+{
+ if (is_same(s, "auto"))
+ return true;
+ if (is_same(s, "off"))
+ return true;
+ /* This pretty much invalidates the other checks, but rt_names.c's
+ * fread_id_name does no validation aside from this. */
+ if (s.len < 512)
+ return true;
+ return is_valid_uint(s, false, 0, 4294967295);
+}
+
+static bool is_valid_saveconfig(string_span_t s)
+{
+ return is_same(s, "true") || is_same(s, "false");
+}
+
+static bool is_valid_prepostupdown(string_span_t s)
+{
+ /* It's probably not worthwhile to try to validate a bash expression.
+ * So instead we just demand non-zero length. */
+ return s.len;
+}
+#endif
+
+static bool is_valid_scope(string_span_t s)
+{
+ if (s.len > 64 || !s.len)
+ return false;
+ for (size_t i = 0; i < s.len; ++i) {
+ if (!is_alphabet(s.s[i]) && !is_decimal(s.s[i]) &&
+ s.s[i] != '_' && s.s[i] != '=' && s.s[i] != '+' &&
+ s.s[i] != '.' && s.s[i] != '-')
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_endpoint(string_span_t s)
+{
+
+ if (!s.len)
+ return false;
+
+ if (s.s[0] == '[') {
+ bool seen_scope = false;
+ string_span_t hostspan = { s.s + 1, 0 };
+
+ for (size_t i = 1; i < s.len; ++i) {
+ if (s.s[i] == '%') {
+ if (seen_scope)
+ return false;
+ seen_scope = true;
+ if (!is_valid_ipv6(hostspan))
+ return false;
+ hostspan = (string_span_t){ s.s + i + 1, 0 };
+ } else if (s.s[i] == ']') {
+ if (seen_scope) {
+ if (!is_valid_scope(hostspan))
+ return false;
+ } else if (!is_valid_ipv6(hostspan)) {
+ return false;
+ }
+ if (i == s.len - 1 || s.s[i + 1] != ':')
+ return false;
+ return is_valid_port((string_span_t){ s.s + i + 2, s.len - i - 2 });
+ } else {
+ ++hostspan.len;
+ }
+ }
+ return false;
+ }
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == ':') {
+ string_span_t host = { s.s, i }, port = { s.s + i + 1, s.len - i - 1};
+ return is_valid_port(port) && (is_valid_ipv4(host) || is_valid_hostname(host));
+ }
+ }
+ return false;
+}
+
+static bool is_valid_network(string_span_t s)
+{
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == '/') {
+ string_span_t ip = { s.s, i }, cidr = { s.s + i + 1, s.len - i - 1};
+ uint16_t cidrval = 0;
+
+ if (cidr.len > 3 || !cidr.len)
+ return false;
+
+ for (size_t j = 0; j < cidr.len; ++j) {
+ if (!is_decimal(cidr.s[j]))
+ return false;
+ cidrval = 10 * cidrval + cidr.s[j] - '0';
+ }
+ if (is_valid_ipv4(ip))
+ return cidrval <= 32;
+ else if (is_valid_ipv6(ip))
+ return cidrval <= 128;
+ return false;
+ }
+ }
+ return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+static bool is_valid_dns(string_span_t s)
+{
+ return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+enum field {
+ InterfaceSection,
+ PrivateKey,
+ ListenPort,
+ Address,
+ DNS,
+ MTU,
+#ifndef MOBILE_WGQUICK_SUBSET
+ FwMark,
+ Table,
+ PreUp, PostUp, PreDown, PostDown,
+ SaveConfig,
+#endif
+
+ PeerSection,
+ PublicKey,
+ PresharedKey,
+ AllowedIPs,
+ Endpoint,
+ PersistentKeepalive,
+
+ Invalid
+};
+
+static enum field section_for_field(enum field t)
+{
+ if (t > InterfaceSection && t < PeerSection)
+ return InterfaceSection;
+ if (t > PeerSection && t < Invalid)
+ return PeerSection;
+ return Invalid;
+}
+
+static enum field get_field(string_span_t s)
+{
+#define check_enum(t) do { if (is_caseless_same(s, #t)) return t; } while (0)
+ check_enum(PrivateKey);
+ check_enum(ListenPort);
+ check_enum(Address);
+ check_enum(DNS);
+ check_enum(MTU);
+ check_enum(PublicKey);
+ check_enum(PresharedKey);
+ check_enum(AllowedIPs);
+ check_enum(Endpoint);
+ check_enum(PersistentKeepalive);
+#ifndef MOBILE_WGQUICK_SUBSET
+ check_enum(FwMark);
+ check_enum(Table);
+ check_enum(PreUp);
+ check_enum(PostUp);
+ check_enum(PreDown);
+ check_enum(PostDown);
+ check_enum(SaveConfig);
+#endif
+ return Invalid;
+#undef check_enum
+}
+
+static enum field get_sectiontype(string_span_t s)
+{
+ if (is_caseless_same(s, "[Peer]"))
+ return PeerSection;
+ if (is_caseless_same(s, "[Interface]"))
+ return InterfaceSection;
+ return Invalid;
+}
+
+struct highlight_span_array {
+ size_t len, capacity;
+ struct highlight_span *spans;
+};
+
+/* A useful OpenBSD-ism. */
+static void *realloc_array(void *optr, size_t nmemb, size_t size)
+{
+ if ((nmemb >= (size_t)1 << (sizeof(size_t) * 4) ||
+ size >= (size_t)1 << (sizeof(size_t) * 4)) &&
+ nmemb > 0 && SIZE_MAX / nmemb < size) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ return realloc(optr, size * nmemb);
+}
+
+static bool append_highlight_span(struct highlight_span_array *a, const char *o, string_span_t s, enum highlight_type t)
+{
+ if (!s.len)
+ return true;
+ if (a->len >= a->capacity) {
+ struct highlight_span *resized;
+
+ a->capacity = a->capacity ? a->capacity * 2 : 64;
+ resized = realloc_array(a->spans, a->capacity, sizeof(*resized));
+ if (!resized) {
+ free(a->spans);
+ memset(a, 0, sizeof(*a));
+ return false;
+ }
+ a->spans = resized;
+ }
+ a->spans[a->len++] = (struct highlight_span){ t, s.s - o, s.len };
+ return true;
+}
+
+static void highlight_multivalue_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ switch (section) {
+ case DNS:
+ append_highlight_span(ret, parent.s, s, is_valid_dns(s) ? HighlightIP : HighlightError);
+ break;
+ case Address:
+ case AllowedIPs: {
+ size_t slash;
+
+ if (!is_valid_network(s)) {
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ break;
+ }
+ for (slash = 0; slash < s.len; ++slash) {
+ if (s.s[slash] == '/')
+ break;
+ }
+ if (slash == s.len) {
+ append_highlight_span(ret, parent.s, s, HighlightIP);
+ } else {
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s, slash }, HighlightIP);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash, 1 }, HighlightDelimiter);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash + 1, s.len - slash - 1 }, HighlightCidr);
+ }
+ break;
+ }
+ default:
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ }
+}
+
+static void highlight_multivalue(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ string_span_t current_span = { s.s, 0 };
+ size_t len_at_last_space = 0;
+
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == ',') {
+ current_span.len = len_at_last_space;
+ highlight_multivalue_value(ret, parent, current_span, section);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + i, 1 }, HighlightDelimiter);
+ len_at_last_space = 0;
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+ if (&s.s[i] == current_span.s && !current_span.len)
+ ++current_span.s;
+ else
+ ++current_span.len;
+ } else {
+ len_at_last_space = ++current_span.len;
+ }
+ }
+ current_span.len = len_at_last_space;
+ if (current_span.len)
+ highlight_multivalue_value(ret, parent, current_span, section);
+ else if (ret->spans[ret->len - 1].type == HighlightDelimiter)
+ ret->spans[ret->len - 1].type = HighlightError;
+}
+
+static void highlight_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ switch (section) {
+ case PrivateKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPrivateKey : HighlightError);
+ break;
+ case PublicKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPublicKey : HighlightError);
+ break;
+ case PresharedKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPresharedKey : HighlightError);
+ break;
+ case MTU:
+ append_highlight_span(ret, parent.s, s, is_valid_mtu(s) ? HighlightMTU : HighlightError);
+ break;
+#ifndef MOBILE_WGQUICK_SUBSET
+ case SaveConfig:
+ append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? HighlightSaveConfig : HighlightError);
+ break;
+ case FwMark:
+ append_highlight_span(ret, parent.s, s, is_valid_fwmark(s) ? HighlightFwMark : HighlightError);
+ break;
+ case Table:
+ append_highlight_span(ret, parent.s, s, is_valid_table(s) ? HighlightTable : HighlightError);
+ break;
+ case PreUp:
+ case PostUp:
+ case PreDown:
+ case PostDown:
+ append_highlight_span(ret, parent.s, s, is_valid_prepostupdown(s) ? HighlightCmd : HighlightError);
+ break;
+#endif
+ case ListenPort:
+ append_highlight_span(ret, parent.s, s, is_valid_port(s) ? HighlightPort : HighlightError);
+ break;
+ case PersistentKeepalive:
+ append_highlight_span(ret, parent.s, s, is_valid_persistentkeepalive(s) ? HighlightKeepalive : HighlightError);
+ break;
+ case Endpoint: {
+ size_t colon;
+
+ if (!is_valid_endpoint(s)) {
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ break;
+ }
+ for (colon = s.len; colon --> 0;) {
+ if (s.s[colon] == ':')
+ break;
+ }
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s, colon }, HighlightHost);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon, 1 }, HighlightDelimiter);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon + 1, s.len - colon - 1 }, HighlightPort);
+ break;
+ }
+ case Address:
+ case DNS:
+ case AllowedIPs:
+ highlight_multivalue(ret, parent, s, section);
+ break;
+ default:
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ }
+}
+
+struct highlight_span *highlight_config(const char *config)
+{
+ struct highlight_span_array ret = { 0 };
+ const string_span_t s = { config, strlen(config) };
+ string_span_t current_span = { s.s, 0 };
+ enum field current_section = Invalid, current_field = Invalid;
+ enum { OnNone, OnKey, OnValue, OnComment, OnSection } state = OnNone;
+ size_t len_at_last_space = 0, equals_location = 0;
+
+ for (size_t i = 0; i <= s.len; ++i) {
+ if (i == s.len || s.s[i] == '\n' || (state != OnComment && s.s[i] == '#')) {
+ if (state == OnKey) {
+ current_span.len = len_at_last_space;
+ append_highlight_span(&ret, s.s, current_span, HighlightError);
+ } else if (state == OnValue) {
+ if (current_span.len) {
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightDelimiter);
+ current_span.len = len_at_last_space;
+ highlight_value(&ret, s, current_span, current_field);
+ } else {
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightError);
+ }
+ } else if (state == OnSection) {
+ current_span.len = len_at_last_space;
+ current_section = get_sectiontype(current_span);
+ append_highlight_span(&ret, s.s, current_span, current_section == Invalid ? HighlightError : HighlightSection);
+ } else if (state == OnComment) {
+ append_highlight_span(&ret, s.s, current_span, HighlightComment);
+ }
+ if (i == s.len)
+ break;
+ len_at_last_space = 0;
+ current_field = Invalid;
+ if (s.s[i] == '#') {
+ current_span = (string_span_t){ s.s + i, 1 };
+ state = OnComment;
+ } else {
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ state = OnNone;
+ }
+ } else if (state == OnComment) {
+ ++current_span.len;
+ } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+ if (&s.s[i] == current_span.s && !current_span.len)
+ ++current_span.s;
+ else
+ ++current_span.len;
+ } else if (s.s[i] == '=' && state == OnKey) {
+ current_span.len = len_at_last_space;
+ current_field = get_field(current_span);
+ enum field section = section_for_field(current_field);
+ if (section == Invalid || current_field == Invalid || section != current_section)
+ append_highlight_span(&ret, s.s, current_span, HighlightError);
+ else
+ append_highlight_span(&ret, s.s, current_span, HighlightField);
+ equals_location = i;
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ state = OnValue;
+ } else {
+ if (state == OnNone)
+ state = s.s[i] == '[' ? OnSection : OnKey;
+ len_at_last_space = ++current_span.len;
+ }
+ }
+
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s, -1 }, HighlightEnd);
+ return ret.spans;
+}
diff --git a/ui/highlighter.h b/ui/highlighter.h
new file mode 100644
index 00000000..c004e127
--- /dev/null
+++ b/ui/highlighter.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <sys/types.h>
+
+enum highlight_type {
+ HighlightSection,
+ HighlightField,
+ HighlightPrivateKey,
+ HighlightPublicKey,
+ HighlightPresharedKey,
+ HighlightIP,
+ HighlightCidr,
+ HighlightHost,
+ HighlightPort,
+ HighlightMTU,
+ HighlightKeepalive,
+ HighlightComment,
+ HighlightDelimiter,
+#ifndef MOBILE_WGQUICK_SUBSET
+ HighlightTable,
+ HighlightFwMark,
+ HighlightSaveConfig,
+ HighlightCmd,
+#endif
+ HighlightError,
+ HighlightEnd
+};
+
+struct highlight_span {
+ enum highlight_type type;
+ size_t start, len;
+};
+
+struct highlight_span *highlight_config(const char *config);
diff --git a/ui/icon/128.png b/ui/icon/128.png
new file mode 100644
index 00000000..a3ba1ca0
--- /dev/null
+++ b/ui/icon/128.png
Binary files differ
diff --git a/ui/icon/16.png b/ui/icon/16.png
new file mode 100644
index 00000000..51c71e0e
--- /dev/null
+++ b/ui/icon/16.png
Binary files differ
diff --git a/ui/icon/256.png b/ui/icon/256.png
new file mode 100644
index 00000000..7b5f63e5
--- /dev/null
+++ b/ui/icon/256.png
Binary files differ
diff --git a/ui/icon/32.png b/ui/icon/32.png
new file mode 100644
index 00000000..ee4acd97
--- /dev/null
+++ b/ui/icon/32.png
Binary files differ
diff --git a/ui/icon/48.png b/ui/icon/48.png
new file mode 100644
index 00000000..24ab1cb2
--- /dev/null
+++ b/ui/icon/48.png
Binary files differ
diff --git a/ui/icon/64.png b/ui/icon/64.png
new file mode 100644
index 00000000..e0295697
--- /dev/null
+++ b/ui/icon/64.png
Binary files differ
diff --git a/ui/icon/icon.ico b/ui/icon/icon.ico
new file mode 100644
index 00000000..9fd9f330
--- /dev/null
+++ b/ui/icon/icon.ico
Binary files differ
diff --git a/ui/manifest.xml b/ui/manifest.xml
new file mode 100644
index 00000000..a14c4be4
--- /dev/null
+++ b/ui/manifest.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+ <assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="wireguard-manager" type="win32"/>
+ <dependency>
+ <dependentAssembly>
+ <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
+ </dependentAssembly>
+ </dependency>
+ <asmv3:application>
+ <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+ <dpiAware>true</dpiAware>
+ </asmv3:windowsSettings>
+ </asmv3:application>
+</assembly>
diff --git a/ui/raise.go b/ui/raise.go
new file mode 100644
index 00000000..b53757c7
--- /dev/null
+++ b/ui/raise.go
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
+ */
+
+package ui
+
+import (
+ "fmt"
+ "golang.org/x/sys/windows"
+ "golang.zx2c4.com/wireguard/windows/ui/internal/walk"
+ "golang.zx2c4.com/wireguard/windows/ui/internal/walk/win"
+ "os"
+ "runtime"
+)
+
+const wireguardUIClass = "WireGuard UI - MainWindow"
+
+func RaiseUI() bool {
+ hwnd := win.FindWindow(windows.StringToUTF16Ptr(wireguardUIClass), windows.StringToUTF16Ptr("WireGuard"))
+ if hwnd == 0 {
+ return false
+ }
+ win.ShowWindow(hwnd, win.SW_NORMAL)
+ win.SetForegroundWindow(hwnd)
+ return true
+}
+
+func WaitForRaiseUIThenQuit() {
+ var handle win.HWINEVENTHOOK
+ runtime.LockOSThread()
+ handle, err := win.SetWinEventHook(win.EVENT_OBJECT_CREATE, win.EVENT_OBJECT_CREATE, 0, func(hWinEventHook win.HWINEVENTHOOK, event uint32, hwnd win.HWND, idObject int32, idChild int32, idEventThread uint32, dwmsEventTime uint32) uintptr {
+ class := make([]uint16, len(wireguardUIClass)+2) /* Plus 2, one for the null terminator, and one to see if this is only a prefix */
+ n, err := win.GetClassName(hwnd, &class[0], len(class))
+ if err != nil || n != len(wireguardUIClass) || windows.UTF16ToString(class) != wireguardUIClass {
+ return 0
+ }
+ win.UnhookWinEvent(handle)
+ win.ShowWindow(hwnd, win.SW_NORMAL)
+ win.SetForegroundWindow(hwnd)
+ os.Exit(0)
+ return 0
+ }, 0, 0, win.WINEVENT_SKIPOWNPROCESS|win.WINEVENT_OUTOFCONTEXT)
+ if err != nil {
+ walk.MsgBox(nil, "WireGuard Detection Error", fmt.Sprintf("Unable to wait for WireGuard window to appear: %v", err), walk.MsgBoxIconError)
+ }
+ for {
+ var msg win.MSG
+ if m := win.GetMessage(&msg, 0, 0, 0); m != 0 {
+ win.TranslateMessage(&msg)
+ win.DispatchMessage(&msg)
+ }
+ }
+}
diff --git a/ui/syntaxedit.c b/ui/syntaxedit.c
new file mode 100644
index 00000000..5586e9d1
--- /dev/null
+++ b/ui/syntaxedit.c
@@ -0,0 +1,311 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <assert.h>
+#include <windows.h>
+#include <richedit.h>
+#include <richole.h>
+#include <tom.h>
+
+#include "syntaxedit.h"
+#include "highlighter.h"
+
+const GUID CDECL IID_ITextDocument = { 0x8CC497C0, 0xA1DF, 0x11CE, { 0x80, 0x98, 0x00, 0xAA, 0x00, 0x47, 0xBE, 0x5D } };
+
+struct syntaxedit_data {
+ IRichEditOle *irich;
+ ITextDocument *idoc;
+};
+
+static WNDPROC parent_proc;
+
+struct span_style {
+ COLORREF color;
+ DWORD effects;
+};
+
+static const struct span_style stylemap[] = {
+ [HighlightSection] = { .color = RGB(0x32, 0x6D, 0x74), .effects = CFE_BOLD },
+ [HighlightField] = { .color = RGB(0x9B, 0x23, 0x93), .effects = CFE_BOLD },
+ [HighlightPrivateKey] = { .color = RGB(0x64, 0x38, 0x20) },
+ [HighlightPublicKey] = { .color = RGB(0x64, 0x38, 0x20) },
+ [HighlightPresharedKey] = { .color = RGB(0x64, 0x38, 0x20) },
+ [HighlightIP] = { .color = RGB(0x0E, 0x0E, 0xFF) },
+ [HighlightCidr] = { .color = RGB(0x81, 0x5F, 0x03) },
+ [HighlightHost] = { .color = RGB(0x0E, 0x0E, 0xFF) },
+ [HighlightPort] = { .color = RGB(0x81, 0x5F, 0x03) },
+ [HighlightMTU] = { .color = RGB(0x1C, 0x00, 0xCF) },
+ [HighlightKeepalive] = { .color = RGB(0x1C, 0x00, 0xCF) },
+ [HighlightComment] = { .color = RGB(0x53, 0x65, 0x79), .effects = CFE_ITALIC },
+ [HighlightDelimiter] = { .color = RGB(0x00, 0x00, 0x00) },
+#ifndef MOBILE_WGQUICK_SUBSET
+ [HighlightTable] = { .color = RGB(0x1C, 0x00, 0xCF) },
+ [HighlightFwMark] = { .color = RGB(0x1C, 0x00, 0xCF) },
+ [HighlightSaveConfig] = { .color = RGB(0x81, 0x5F, 0x03) },
+ [HighlightCmd] = { .color = RGB(0x63, 0x75, 0x89) },
+#endif
+ [HighlightError] = { .color = RGB(0xC4, 0x1A, 0x16), .effects = CFE_UNDERLINE }
+};
+
+static void highlight_text(HWND hWnd)
+{
+ GETTEXTLENGTHEX gettextlengthex = {
+ .flags = GTL_NUMBYTES,
+ .codepage = CP_ACP /* Probably CP_UTF8 would be better, but (wine at least) returns utf32 sizes. */
+ };
+ GETTEXTEX gettextex = {
+ .flags = GT_NOHIDDENTEXT,
+ .codepage = gettextlengthex.codepage
+ };
+ CHARFORMAT2 format = {
+ .cbSize = sizeof(CHARFORMAT2),
+ .dwMask = CFM_COLOR | CFM_CHARSET | CFM_SIZE | CFM_BOLD | CFM_ITALIC | CFM_UNDERLINE,
+ .dwEffects = CFE_AUTOCOLOR,
+ .yHeight = 20 * 10,
+ .bCharSet = ANSI_CHARSET
+ };
+ struct syntaxedit_data *this = (struct syntaxedit_data *)GetWindowLongPtr(hWnd, GWLP_USERDATA);
+ LRESULT msg_size;
+ char *msg;
+ struct highlight_span *spans;
+ CHARRANGE orig_selection;
+ POINT original_scroll;
+ bool found_private_key = false;
+
+ msg_size = SendMessage(hWnd, EM_GETTEXTLENGTHEX, (WPARAM)&gettextlengthex, 0);
+ if (msg_size == E_INVALIDARG)
+ return;
+ gettextex.cb = msg_size + 1;
+
+ msg = malloc(msg_size + 1);
+ if (!msg)
+ return;
+ if (SendMessage(hWnd, EM_GETTEXTEX, (WPARAM)&gettextex, (LPARAM)msg) <= 0) {
+ free(msg);
+ return;
+ }
+
+ /* By default we get CR not CRLF, so just convert to LF. */
+ for (size_t i = 0; i < msg_size; ++i) {
+ if (msg[i] == '\r')
+ msg[i] = '\n';
+ }
+
+ spans = highlight_config(msg);
+ if (!spans) {
+ free(msg);
+ return;
+ }
+
+ this->idoc->lpVtbl->Undo(this->idoc, tomSuspend, NULL);
+ SendMessage(hWnd, WM_SETREDRAW, FALSE, 0);
+ SendMessage(hWnd, EM_EXGETSEL, 0, (LPARAM)&orig_selection);
+ SendMessage(hWnd, EM_GETSCROLLPOS, 0, (LPARAM)&original_scroll);
+ SendMessage(hWnd, EM_HIDESELECTION, TRUE, 0);
+ SendMessage(hWnd, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&format);
+ for (struct highlight_span *span = spans; span->type != HighlightEnd; ++span) {
+ CHARRANGE selection = { span->start, span->len + span->start };
+ SendMessage(hWnd, EM_EXSETSEL, 0, (LPARAM)&selection);
+ format.crTextColor = stylemap[span->type].color;
+ format.dwEffects = stylemap[span->type].effects;
+ SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&format);
+ if (span->type == HighlightPrivateKey && !found_private_key) {
+ /* Rather than allocating a new string, we mangle this one, since (for now) we don't use msg again. */
+ msg[span->start + span->len] = '\0';
+ SendMessage(hWnd, SE_PRIVATE_KEY, 0, (LPARAM)&msg[span->start]);
+ found_private_key = true;
+ }
+ }
+ SendMessage(hWnd, EM_SETSCROLLPOS, 0, (LPARAM)&original_scroll);
+ SendMessage(hWnd, EM_EXSETSEL, 0, (LPARAM)&orig_selection);
+ SendMessage(hWnd, EM_HIDESELECTION, FALSE, 0);
+ SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
+ RedrawWindow(hWnd, NULL, NULL, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN);
+ this->idoc->lpVtbl->Undo(this->idoc, tomResume, NULL);
+ free(spans);
+ free(msg);
+ if (!found_private_key)
+ SendMessage(hWnd, SE_PRIVATE_KEY, 0, 0);
+}
+
+static void context_menu(HWND hWnd, INT x, INT y)
+{
+ GETTEXTLENGTHEX gettextlengthex = {
+ .flags = GTL_DEFAULT,
+ .codepage = CP_ACP
+ };
+ /* This disturbing hack grabs the system edit menu normally used for the EDIT control. */
+ HMENU popup, menu = LoadMenuW(GetModuleHandleW(L"comctl32.dll"), MAKEINTRESOURCEW(1));
+ CHARRANGE selection = { 0 };
+ bool has_selection, can_selectall, can_undo, can_paste;
+ UINT cmd;
+
+ if (!menu)
+ return;
+
+ SendMessage(hWnd, EM_EXGETSEL, 0, (LPARAM)&selection);
+ has_selection = selection.cpMax - selection.cpMin;
+ can_selectall = selection.cpMin || (selection.cpMax < SendMessage(hWnd, EM_GETTEXTLENGTHEX, (WPARAM)&gettextlengthex, 0));
+ can_undo = SendMessage(hWnd, EM_CANUNDO, 0, 0);
+ can_paste = SendMessage(hWnd, EM_CANPASTE, CF_TEXT, 0);
+
+ popup = GetSubMenu(menu, 0);
+ EnableMenuItem(popup, WM_UNDO, MF_BYCOMMAND | (can_undo ? MF_ENABLED : MF_GRAYED));
+ EnableMenuItem(popup, WM_CUT, MF_BYCOMMAND | (has_selection ? MF_ENABLED : MF_GRAYED));
+ EnableMenuItem(popup, WM_COPY, MF_BYCOMMAND | (has_selection ? MF_ENABLED : MF_GRAYED));
+ EnableMenuItem(popup, WM_PASTE, MF_BYCOMMAND | (can_paste ? MF_ENABLED : MF_GRAYED));
+ EnableMenuItem(popup, WM_CLEAR, MF_BYCOMMAND | (has_selection ? MF_ENABLED : MF_GRAYED));
+ EnableMenuItem(popup, EM_SETSEL, MF_BYCOMMAND | (can_selectall ? MF_ENABLED : MF_GRAYED));
+
+ /* Delete items that we don't handle. */
+ for (int ctl = GetMenuItemCount(popup) - 1; ctl >= 0; --ctl) {
+ MENUITEMINFOW menu_item = {
+ .cbSize = sizeof(MENUITEMINFOW),
+ .fMask = MIIM_FTYPE | MIIM_ID
+ };
+ if (!GetMenuItemInfoW(popup, ctl, MF_BYPOSITION, &menu_item))
+ continue;
+ if (menu_item.fType & MFT_SEPARATOR)
+ continue;
+ switch (menu_item.wID) {
+ case WM_UNDO:
+ case WM_CUT:
+ case WM_COPY:
+ case WM_PASTE:
+ case WM_CLEAR:
+ case EM_SETSEL:
+ continue;
+ }
+ DeleteMenu(popup, ctl, MF_BYPOSITION);
+ }
+ /* Delete trailing and adjacent separators. */
+ for (int ctl = GetMenuItemCount(popup) - 1, end = true; ctl >= 0; --ctl) {
+ MENUITEMINFOW menu_item = {
+ .cbSize = sizeof(MENUITEMINFOW),
+ .fMask = MIIM_FTYPE
+ };
+ if (!GetMenuItemInfoW(popup, ctl, MF_BYPOSITION, &menu_item))
+ continue;
+ if (!(menu_item.fType & MFT_SEPARATOR)) {
+ end = false;
+ continue;
+ }
+ if (!end && ctl) {
+ if (!GetMenuItemInfoW(popup, ctl - 1, MF_BYPOSITION, &menu_item))
+ continue;
+ if (!(menu_item.fType & MFT_SEPARATOR))
+ continue;
+ }
+ DeleteMenu(popup, ctl, MF_BYPOSITION);
+ }
+
+ if (x == -1 && y == -1) {
+ RECT rect;
+ GetWindowRect(hWnd, &rect);
+ x = rect.left + (rect.right - rect.left) / 2;
+ y = rect.top + (rect.bottom - rect.top) / 2;
+ }
+
+ if (GetFocus() != hWnd)
+ SetFocus(hWnd);
+
+ cmd = TrackPopupMenu(popup, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY, x, y, 0, hWnd, NULL);
+ if (cmd)
+ SendMessage(hWnd, cmd, 0, cmd == EM_SETSEL ? -1 : 0);
+
+ DestroyMenu(menu);
+}
+
+static LRESULT CALLBACK child_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
+{
+ switch (Msg) {
+ case WM_CREATE: {
+ struct syntaxedit_data *this = calloc(1, sizeof(*this));
+ assert(this);
+ SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this);
+ SendMessage(hWnd, EM_GETOLEINTERFACE, 0, (LPARAM)&this->irich);
+ assert(this->irich);
+ this->irich->lpVtbl->QueryInterface(this->irich, &IID_ITextDocument, (void **)&this->idoc);
+ assert(this->idoc);
+ SendMessage(hWnd, EM_SETEVENTMASK, 0, ENM_CHANGE);
+ SendMessage(hWnd, EM_SETTEXTMODE, TM_SINGLECODEPAGE, 0);
+ break;
+ }
+ case WM_DESTROY: {
+ struct syntaxedit_data *this = (struct syntaxedit_data *)GetWindowLongPtr(hWnd, GWLP_USERDATA);
+ this->idoc->lpVtbl->Release(this->idoc);
+ this->irich->lpVtbl->Release(this->irich);
+ free(this);
+ }
+ case WM_SETTEXT: {
+ LRESULT ret = parent_proc(hWnd, Msg, wParam, lParam);
+ highlight_text(hWnd);
+ SendMessage(hWnd, EM_EMPTYUNDOBUFFER, 0, 0);
+ return ret;
+ }
+ case WM_REFLECT + WM_COMMAND:
+ case WM_COMMAND:
+ case WM_REFLECT + WM_NOTIFY:
+ case WM_NOTIFY:
+ switch (HIWORD(wParam)) {
+ case EN_CHANGE:
+ highlight_text(hWnd);
+ break;
+ }
+ break;
+ case WM_PASTE:
+ SendMessage(hWnd, EM_PASTESPECIAL, CF_TEXT, 0);
+ return 0;
+ case WM_KEYDOWN:
+ if (!(GetKeyState(VK_CONTROL) & 0x8000))
+ break;
+ switch (LOWORD(wParam)) {
+ case 'V':
+ SendMessage(hWnd, EM_PASTESPECIAL, CF_TEXT, 0);
+ return 0;
+ }
+ break;
+ case WM_CONTEXTMENU:
+ context_menu(hWnd, LOWORD(lParam), HIWORD(lParam));
+ return 0;
+ }
+ return parent_proc(hWnd, Msg, wParam, lParam);
+}
+
+bool register_syntax_edit(void)
+{
+ WNDCLASSEXW class = { .cbSize = sizeof(WNDCLASSEXW) };
+ WNDPROC pp;
+ HANDLE lib;
+
+ if (parent_proc)
+ return true;
+
+ lib = LoadLibraryW(L"msftedit.dll");
+ if (!lib)
+ return false;
+
+ if (!GetClassInfoExW(NULL, L"RICHEDIT50W", &class))
+ goto err;
+ pp = class.lpfnWndProc;
+ if (!pp)
+ goto err;
+ class.cbSize = sizeof(WNDCLASSEXW);
+ class.hInstance = GetModuleHandleW(NULL);
+ class.lpszClassName = L"WgQuickSyntaxEdit";
+ class.lpfnWndProc = child_proc;
+ if (!RegisterClassExW(&class))
+ goto err;
+ parent_proc = pp;
+ return true;
+
+err:
+ FreeLibrary(lib);
+ return false;
+}
diff --git a/ui/syntaxedit.go b/ui/syntaxedit.go
new file mode 100644
index 00000000..34d36305
--- /dev/null
+++ b/ui/syntaxedit.go
@@ -0,0 +1,153 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
+ */
+
+package ui
+
+import (
+ "errors"
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "golang.zx2c4.com/wireguard/windows/ui/internal/walk"
+ "golang.zx2c4.com/wireguard/windows/ui/internal/walk/win"
+)
+
+// #include "syntaxedit.h"
+import "C"
+
+type PrivateKeyHandler func(privateKey string)
+type PrivateKeyEvent struct {
+ handlers []PrivateKeyHandler
+}
+
+func (e *PrivateKeyEvent) Attach(handler PrivateKeyHandler) int {
+ for i, h := range e.handlers {
+ if h == nil {
+ e.handlers[i] = handler
+ return i
+ }
+ }
+
+ e.handlers = append(e.handlers, handler)
+ return len(e.handlers) - 1
+}
+func (e *PrivateKeyEvent) Detach(handle int) {
+ e.handlers[handle] = nil
+}
+
+type PrivateKeyPublisher struct {
+ event PrivateKeyEvent
+}
+
+func (p *PrivateKeyPublisher) Event() *PrivateKeyEvent {
+ return &p.event
+}
+func (p *PrivateKeyPublisher) Publish(privateKey string) {
+ for _, handler := range p.event.handlers {
+ if handler != nil {
+ handler(privateKey)
+ }
+ }
+}
+
+type SyntaxEdit struct {
+ walk.WidgetBase
+ textChangedPublisher walk.EventPublisher
+ privateKeyPublisher PrivateKeyPublisher
+}
+
+func init() {
+ C.register_syntax_edit()
+}
+
+func (se *SyntaxEdit) LayoutFlags() walk.LayoutFlags {
+ return walk.GrowableHorz | walk.GrowableVert | walk.GreedyHorz | walk.GreedyVert
+}
+
+func (se *SyntaxEdit) MinSizeHint() walk.Size {
+ return walk.Size{20, 12}
+}
+
+func (se *SyntaxEdit) SizeHint() walk.Size {
+ return walk.Size{200, 100}
+}
+
+func (se *SyntaxEdit) Text() string {
+ textLength := se.SendMessage(win.WM_GETTEXTLENGTH, 0, 0)
+ buf := make([]uint16, textLength+1)
+ se.SendMessage(win.WM_GETTEXT, uintptr(textLength+1), uintptr(unsafe.Pointer(&buf[0])))
+ return strings.Replace(syscall.UTF16ToString(buf), "\r\n", "\n", -1)
+}
+
+func (se *SyntaxEdit) SetText(text string) (err error) {
+ if text == se.Text() {
+ return nil
+ }
+ text = strings.Replace(text, "\n", "\r\n", -1)
+ if win.TRUE != se.SendMessage(win.WM_SETTEXT, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text)))) {
+ err = errors.New("WM_SETTEXT failed")
+ }
+ se.textChangedPublisher.Publish()
+ return
+}
+
+func (se *SyntaxEdit) TextChanged() *walk.Event {
+ return se.textChangedPublisher.Event()
+}
+
+func (se *SyntaxEdit) PrivateKeyChanged() *PrivateKeyEvent {
+ return se.privateKeyPublisher.Event()
+}
+
+func (se *SyntaxEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
+ switch msg {
+ case win.WM_NOTIFY, win.WM_COMMAND:
+ switch win.HIWORD(uint32(wParam)) {
+ case win.EN_CHANGE:
+ se.textChangedPublisher.Publish()
+ }
+ // This is a horrible trick from MFC where we reflect the event back to the child.
+ se.SendMessage(msg+C.WM_REFLECT, wParam, lParam)
+ case C.SE_PRIVATE_KEY:
+ if lParam == 0 {
+ se.privateKeyPublisher.Publish("")
+ } else {
+ se.privateKeyPublisher.Publish(C.GoString((*C.char)(unsafe.Pointer(lParam))))
+ }
+ }
+ return se.WidgetBase.WndProc(hwnd, msg, wParam, lParam)
+}
+
+func NewSyntaxEdit(parent walk.Container) (*SyntaxEdit, error) {
+ se := &SyntaxEdit{}
+ err := walk.InitWidget(
+ se,
+ parent,
+ "WgQuickSyntaxEdit",
+ C.SYNTAXEDIT_STYLE,
+ C.SYNTAXEDIT_EXTSTYLE,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ se.GraphicsEffects().Add(walk.InteractionEffect)
+ se.GraphicsEffects().Add(walk.FocusEffect)
+ se.MustRegisterProperty("Text", walk.NewProperty(
+ func() interface{} {
+ return se.Text()
+ },
+ func(v interface{}) error {
+ if s, ok := v.(string); ok {
+ return se.SetText(s)
+ } else {
+ return se.SetText("")
+ }
+ },
+ se.textChangedPublisher.Event()))
+
+ return se, nil
+}
diff --git a/ui/syntaxedit.h b/ui/syntaxedit.h
new file mode 100644
index 00000000..12815278
--- /dev/null
+++ b/ui/syntaxedit.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
+ */
+
+#ifndef SYNTAXEDIT_H
+#define SYNTAXEDIT_H
+
+#include <stdbool.h>
+#include <windows.h>
+#include <richedit.h>
+
+#define SYNTAXEDIT_STYLE (WS_CHILD | WS_CLIPSIBLINGS | ES_MULTILINE | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_BORDER | WS_TABSTOP | ES_WANTRETURN | ES_NOOLEDRAGDROP)
+#define SYNTAXEDIT_EXTSTYLE (WS_EX_CLIENTEDGE)
+
+/* The old MFC reflection trick. */
+#define WM_REFLECT (WM_USER + 0x1C00)
+
+#define SE_PRIVATE_KEY (WM_USER + 0x3100)
+
+extern bool register_syntax_edit(void);
+
+#endif
diff --git a/ui/ui.go b/ui/ui.go
new file mode 100644
index 00000000..f1c5f7c6
--- /dev/null
+++ b/ui/ui.go
@@ -0,0 +1,140 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
+ */
+
+package ui
+
+import (
+ "encoding/base64"
+ "fmt"
+ "golang.org/x/crypto/curve25519"
+ "golang.zx2c4.com/wireguard/windows/conf"
+ "golang.zx2c4.com/wireguard/windows/service"
+ "golang.zx2c4.com/wireguard/windows/ui/internal/walk"
+ "os"
+)
+
+const demoConfig = `[Interface]
+PrivateKey = 6KpcbNFK4tKBciKBT2Rj6Z/sHBqxdV+p+nuNA5AlWGI=
+Address = 192.168.4.84/24
+DNS = 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1
+
+[Peer]
+PublicKey = JRI8Xc0zKP9kXk8qP84NdUQA04h6DLfFbwJn4g+/PFs=
+Endpoint = demo.wireguard.com:12912
+AllowedIPs = 0.0.0.0/0
+`
+
+func RunUI() {
+ icon, _ := walk.NewIconFromResourceId(8)
+
+ mw, _ := walk.NewMainWindowWithName("WireGuard")
+ tray, _ := walk.NewNotifyIcon(mw)
+ defer tray.Dispose()
+ tray.SetIcon(icon)
+ tray.SetToolTip("WireGuard: Disconnected")
+ tray.SetVisible(true)
+
+ mw.SetSize(walk.Size{900, 800})
+ mw.SetLayout(walk.NewVBoxLayout())
+ mw.SetIcon(icon)
+ mw.Closing().Attach(func(canceled *bool, reason walk.CloseReason) {
+ *canceled = true
+ mw.Hide()
+ })
+
+ tl, _ := walk.NewTextLabel(mw)
+ tl.SetText("Public key: (unknown)")
+
+ se, _ := NewSyntaxEdit(mw)
+ lastPrivate := ""
+ se.PrivateKeyChanged().Attach(func(privateKey string) {
+ if privateKey == lastPrivate {
+ return
+ }
+ lastPrivate = privateKey
+ key := func() string {
+ if privateKey == "" {
+ return ""
+ }
+ decoded, err := base64.StdEncoding.DecodeString(privateKey)
+ if err != nil {
+ return ""
+ }
+ if len(decoded) != 32 {
+ return ""
+ }
+ var p [32]byte
+ var s [32]byte
+ copy(s[:], decoded[:32])
+ curve25519.ScalarBaseMult(&p, &s)
+ return base64.StdEncoding.EncodeToString(p[:])
+ }()
+ if key != "" {
+ tl.SetText("Public key: " + key)
+ } else {
+ tl.SetText("Public key: (unknown)")
+ }
+ })
+ se.SetText(demoConfig)
+
+ pb, _ := walk.NewPushButton(mw)
+ pb.SetText("Start")
+ var runningTunnel *service.Tunnel
+ pb.Clicked().Attach(func() {
+ if runningTunnel != nil {
+ _, err := runningTunnel.Stop()
+ if err != nil {
+ walk.MsgBox(mw, "Unable to stop tunnel", err.Error(), walk.MsgBoxIconError)
+ return
+ }
+ runningTunnel = nil
+ pb.SetText("Start")
+ tray.SetToolTip("WireGuard: Disconnected")
+ return
+ }
+ c, err := conf.FromWgQuick(se.Text(), "test")
+ if err != nil {
+ walk.MsgBox(mw, "Invalid configuration", err.Error(), walk.MsgBoxIconError)
+ return
+ }
+ tunnel, err := service.IPCClientNewTunnel(c)
+ if err != nil {
+ walk.MsgBox(mw, "Unable to create tunnel", err.Error(), walk.MsgBoxIconError)
+ return
+ }
+ _, err = tunnel.Start()
+ if err != nil {
+ walk.MsgBox(mw, "Unable to start tunnel", err.Error(), walk.MsgBoxIconError)
+ return
+ }
+ runningTunnel = &tunnel
+ pb.SetText("Stop")
+ tray.SetToolTip("WireGuard: Connected")
+ })
+
+ quitAction := walk.NewAction()
+ quitAction.SetText("Exit")
+ quitAction.Triggered().Attach(func() {
+ tray.Dispose()
+ _, err := service.IPCClientQuit(true)
+ if err != nil {
+ walk.MsgBox(nil, "Error Exiting WireGuard", fmt.Sprintf("Unable to exit service due to: %s. You may want to stop WireGuard from the service manager.", err), walk.MsgBoxIconError)
+ os.Exit(1)
+ }
+ })
+ tray.ContextMenu().Actions().Add(quitAction)
+ tray.MouseDown().Attach(func(x, y int, button walk.MouseButton) {
+ if button == walk.LeftButton {
+ mw.Show()
+ }
+ })
+
+ err := service.IPCClientRegisterAsNotificationThread()
+ if err != nil {
+ walk.MsgBox(mw, "Unable to register for notifications", err.Error(), walk.MsgBoxIconError)
+ os.Exit(1)
+ }
+ mw.Run()
+}