From cff906922ad280eb36af12d86e61c659c666889e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 28 Dec 2023 01:09:49 +0100 Subject: [PATCH] Improve UI --- agent/actions/vault.go | 2 ++ cmd/vault.go | 4 ++- ipc/messages/vault.go | 2 ++ ui/components.py | 62 +++++++++++++++++++++++++++++++++ ui/main.py | 12 ++++--- ui/settings.py | 79 ++++++++++++++++++++++++++++++++++++++---- ui/style.css | 26 ++++++++++++++ 7 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 ui/components.py create mode 100644 ui/style.css diff --git a/agent/actions/vault.go b/agent/actions/vault.go index 84e18b4..59b1ef0 100644 --- a/agent/actions/vault.go +++ b/agent/actions/vault.go @@ -192,6 +192,8 @@ func handleVaultStatus(request messages.IPCMessage, cfg *config.Config, vault *v vaultStatus.NumberOfNotes = len(vault.GetNotes()) vaultStatus.LastSynced = vault.GetLastSynced() vaultStatus.WebsockedConnected = vault.IsWebsocketConnected() + vaultStatus.PinSet = cfg.HasPin() + vaultStatus.LoggedIn = cfg.IsLoggedIn() response, err = messages.IPCMessageFromPayload(vaultStatus) return } diff --git a/cmd/vault.go b/cmd/vault.go index e207599..0da79b3 100644 --- a/cmd/vault.go +++ b/cmd/vault.go @@ -113,7 +113,9 @@ var statusCmd = &cobra.Command{ fmt.Println(" \"loginEntries\":", status.NumberOfLogins, ",") fmt.Println(" \"noteEntries\":", status.NumberOfNotes, ",") fmt.Println(" \"lastSynced\": \"" + time.Unix(status.LastSynced, 0).String() + "\",") - fmt.Println(" \"websocketConnected\":", status.WebsockedConnected) + fmt.Println(" \"websocketConnected\":", status.WebsockedConnected, ",") + fmt.Println(" \"pinSet\":", status.PinSet, ",") + fmt.Println(" \"loggedIn\":", status.LoggedIn) fmt.Println("}") default: println("Wrong response type") diff --git a/ipc/messages/vault.go b/ipc/messages/vault.go index 9a38f6f..9873518 100644 --- a/ipc/messages/vault.go +++ b/ipc/messages/vault.go @@ -22,6 +22,8 @@ type VaultStatusRequest struct { type VaultStatusResponse struct { Locked bool + LoggedIn bool + PinSet bool NumberOfLogins int NumberOfNotes int LastSynced int64 diff --git a/ui/components.py b/ui/components.py new file mode 100644 index 0000000..1461ad2 --- /dev/null +++ b/ui/components.py @@ -0,0 +1,62 @@ +from gi.repository import Gtk + +def status_icon_ok(icon_name): + imagebox = Gtk.Box() + imagebox.set_orientation(Gtk.Orientation.VERTICAL) + imagebox.set_halign(Gtk.Align.CENTER) + imagebox.set_valign(Gtk.Align.CENTER) + image = Gtk.Image() + image.get_style_context().add_class("status-icon") + image.get_style_context().add_class("ok-icon") + image.set_from_icon_name(icon_name) + imagebox.append(image) + return imagebox + +def status_icon_error(icon_name): + imagebox = Gtk.Box() + imagebox.set_orientation(Gtk.Orientation.VERTICAL) + imagebox.set_halign(Gtk.Align.CENTER) + imagebox.set_valign(Gtk.Align.CENTER) + image = Gtk.Image() + image.get_style_context().add_class("status-icon") + image.get_style_context().add_class("error-icon") + image.set_from_icon_name(icon_name) + imagebox.append(image) + return imagebox + +def status_icon_warning(icon_name): + imagebox = Gtk.Box() + imagebox.set_orientation(Gtk.Orientation.VERTICAL) + imagebox.set_halign(Gtk.Align.CENTER) + imagebox.set_valign(Gtk.Align.CENTER) + image = Gtk.Image() + image.get_style_context().add_class("status-icon") + image.get_style_context().add_class("warning-icon") + image.set_from_icon_name(icon_name) + imagebox.append(image) + + return imagebox + +class StatusIcon(Gtk.Box): + def __init__(self): + super().__init__() + self.icon_name = None + self.status = None + + def set_icon(self, icon_name, status): + if self.icon_name == icon_name and self.status == status: + return + self.icon_name = icon_name + self.status = status + + while self.get_first_child() != None: + self.remove(self.get_first_child()) + + if status == "ok": + self.append(status_icon_ok(icon_name)) + elif status == "error": + self.append(status_icon_error(icon_name)) + elif status == "warning": + self.append(status_icon_warning(icon_name)) + else: + raise Exception("Invalid status", status) \ No newline at end of file diff --git a/ui/main.py b/ui/main.py index b5742dd..978722f 100644 --- a/ui/main.py +++ b/ui/main.py @@ -6,9 +6,13 @@ import sys import goldwarden from threading import Thread +import os + +isflatpak = os.path.exists("/.flatpak-info") +pathprefix = "/app/bin/" if isflatpak else "./" try: - subprocess.Popen(["python3", "/app/bin/background.py"], start_new_session=True) + subprocess.Popen(["python3", f'{pathprefix}background.py'], start_new_session=True) except: pass @@ -16,9 +20,9 @@ if not is_hidden: try: - subprocess.Popen(["python3", "/app/bin/settings.py"], start_new_session=True) + subprocess.Popen(["python3", f'{pathprefix}settings.py'], start_new_session=True) except: - subprocess.Popen(["python3", "./settings.py"], start_new_session=True) + subprocess.Popen(["python3", f'{pathprefix}settings.py'], start_new_session=True) pass try: @@ -40,7 +44,7 @@ def run_daemon(): thread.start() def on_autofill(): - subprocess.Popen(["python3", "/app/bin/autofill.py"], start_new_session=True) + subprocess.Popen(["python3", f'{pathprefix}autofill.py'], start_new_session=True) monitors.dbus_autofill_monitor.on_autofill = lambda: on_autofill() monitors.dbus_autofill_monitor.run_daemon() diff --git a/ui/settings.py b/ui/settings.py index dcd1947..00c45ed 100644 --- a/ui/settings.py +++ b/ui/settings.py @@ -5,10 +5,11 @@ gi.require_version('Adw', '1') import gc -from gi.repository import Gtk, Adw, GLib +from gi.repository import Gtk, Adw, GLib, Gdk import goldwarden from threading import Thread import subprocess +import components class SettingsWinvdow(Gtk.ApplicationWindow): def __init__(self, *args, **kwargs): @@ -31,6 +32,9 @@ def __init__(self, *args, **kwargs): self.ssh_row.set_subtitle("Listening at ~/.goldwarden-ssh-agent.sock") self.preferences_group.add(self.ssh_row) + self.icon = components.status_icon_ok("emblem-default") + self.ssh_row.add_prefix(self.icon) + self.login_with_device = Adw.ActionRow() self.login_with_device.set_title("Login with device") self.login_with_device.set_subtitle("Waiting for requests...") @@ -50,6 +54,10 @@ def __init__(self, *args, **kwargs): self.autofill_row.set_subtitle("Unavailable, please set up a shortcut in your desktop environment (README)") self.shortcut_preferences_group.add(self.autofill_row) + self.autofill_icon = components.StatusIcon() + self.autofill_icon.set_icon("dialog-warning", "warning") + self.autofill_row.add_prefix(self.autofill_icon) + self.copy_username_shortcut_row = Adw.ActionRow() self.copy_username_shortcut_row.set_title("Copy Username Shortcut") self.copy_username_shortcut_row.set_subtitle("U") @@ -69,9 +77,14 @@ def __init__(self, *args, **kwargs): self.status_row.set_subtitle("Locked") self.vault_status_preferences_group.add(self.status_row) + self.vault_status_icon = components.StatusIcon() + self.vault_status_icon.set_icon("dialog-error", "error") + self.status_row.add_prefix(self.vault_status_icon) + self.last_sync_row = Adw.ActionRow() self.last_sync_row.set_title("Last Sync") self.last_sync_row.set_subtitle("Never") + self.last_sync_row.set_icon_name("emblem-synchronizing-symbolic") self.vault_status_preferences_group.add(self.last_sync_row) self.websocket_connected_row = Adw.ActionRow() @@ -79,14 +92,20 @@ def __init__(self, *args, **kwargs): self.websocket_connected_row.set_subtitle("False") self.vault_status_preferences_group.add(self.websocket_connected_row) + self.websocket_connected_status_icon = components.StatusIcon() + self.websocket_connected_status_icon.set_icon("dialog-error", "error") + self.websocket_connected_row.add_prefix(self.websocket_connected_status_icon) + self.login_row = Adw.ActionRow() self.login_row.set_title("Vault Login Entries") self.login_row.set_subtitle("0") + self.login_row.set_icon_name("dialog-password-symbolic") self.vault_status_preferences_group.add(self.login_row) self.notes_row = Adw.ActionRow() self.notes_row.set_title("Vault Notes") self.notes_row.set_subtitle("0") + self.notes_row.set_icon_name("emblem-documents-symbolic") self.vault_status_preferences_group.add(self.notes_row) self.action_preferences_group = Adw.PreferencesGroup() @@ -142,21 +161,55 @@ def update_labels(): pin_set = goldwarden.is_pin_enabled() status = goldwarden.get_vault_status() if status != None: + if pin_set: + self.unlock_button.set_sensitive(True) + else: + self.unlock_button.set_sensitive(False) + logged_in = status["loggedIn"] + if logged_in: + self.preferences_group.set_visible(True) + self.shortcut_preferences_group.set_visible(True) + self.autotype_button.set_visible(True) + self.login_row.set_sensitive(True) + self.notes_row.set_sensitive(True) + self.websocket_connected_row.set_sensitive(True) + else: + self.preferences_group.set_visible(False) + self.shortcut_preferences_group.set_visible(False) + self.autotype_button.set_visible(False) + self.websocket_connected_row.set_sensitive(False) + self.login_row.set_sensitive(False) + self.notes_row.set_sensitive(False) + locked = status["locked"] self.login_button.set_sensitive(pin_set and not locked) self.set_pin_button.set_sensitive(not pin_set or not locked) self.autotype_button.set_sensitive(not locked) - self.status_row.set_subtitle(str("Unlocked" if not locked else "Locked")) + self.status_row.set_subtitle(str("Logged in" if (logged_in and not locked) else "Logged out") if not locked else "Locked") + if locked or not logged_in: + self.vault_status_icon.set_icon("dialog-warning", "warning") + else: + self.vault_status_icon.set_icon("emblem-default", "ok") + if not logged_in: + self.logout_button.set_sensitive(False) + else: + self.logout_button.set_sensitive(True) self.login_row.set_subtitle(str(status["loginEntries"])) self.notes_row.set_subtitle(str(status["noteEntries"])) self.websocket_connected_row.set_subtitle("Connected" if status["websocketConnected"] else "Disconnected") + if status["websocketConnected"]: + self.websocket_connected_status_icon.set_icon("emblem-default", "ok") + else: + self.websocket_connected_status_icon.set_icon("dialog-error", "error") self.last_sync_row.set_subtitle(str(status["lastSynced"])) - self.unlock_button.set_sensitive(True) + if status["lastSynced"].startswith("1970"): + self.last_sync_row.set_subtitle("Never") self.unlock_button.set_label("Unlock" if locked else "Lock") else: is_daemon_running = goldwarden.is_daemon_running() if not is_daemon_running: self.status_row.set_subtitle("Daemon not running") + self.vault_status_icon.set_icon("dialog-error", "error") GLib.timeout_add(1000, update_labels) GLib.timeout_add(1000, update_labels) @@ -177,23 +230,26 @@ def on_activate(self, app): self.settings_win = SettingsWinvdow(application=app) self.settings_win.present() -app = MyApp(application_id="com.quexten.Goldwarden") - def show_login(): dialog = Gtk.Dialog(title="Goldwarden") preference_group = Adw.PreferencesGroup() preference_group.set_title("Config") + preference_group.set_margin_top(10) + preference_group.set_margin_bottom(10) + preference_group.set_margin_start(10) + preference_group.set_margin_end(10) + dialog.get_content_area().append(preference_group) api_url_entry = Adw.EntryRow() api_url_entry.set_title("API Url") # set value - api_url_entry.set_text("https://api.bitwarden.com/") + api_url_entry.set_text("https://vault.bitwarden.com/api") preference_group.add(api_url_entry) identity_url_entry = Adw.EntryRow() identity_url_entry.set_title("Identity Url") - identity_url_entry.set_text("https://identity.bitwarden.com/") + identity_url_entry.set_text("https://vault.bitwarden.com/identity") preference_group.add(identity_url_entry) notification_url_entry = Adw.EntryRow() @@ -239,5 +295,14 @@ def handle_res(): dialog.set_modal(True) dialog.present() +css_provider = Gtk.CssProvider() +css_provider.load_from_path("style.css") +Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION +) + + app = MyApp(application_id="com.quexten.Goldwarden.settings") app.run(sys.argv) \ No newline at end of file diff --git a/ui/style.css b/ui/style.css new file mode 100644 index 0000000..f409e44 --- /dev/null +++ b/ui/style.css @@ -0,0 +1,26 @@ +.status-icon { + min-width: 32px; + min-height: 32px; + border-radius: 9999px; +} + +.ok-icon { + color: @green_5; + background-color: alpha(@green_3, .25); +} + +.warning-icon { + color: #ae7b03; + background: alpha(@yellow_5, .25); +} + +.error-icon { + color: @red_4; + background-color: alpha(@red_2, .25); +} + +.accent-icon { + padding: 9px; + color: @blue_4; + background-color: alpha(@blue_3, .25); +} \ No newline at end of file