From 6940214dff541f2196ce750fdd5bc7195be0cada Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Fri, 10 Jul 2026 23:09:14 +0700
Subject: [PATCH 01/10] feat(gtk): modern theme foundation with adaptive
light/dark tokens
Rework the GTK GUI stylesheet into a token-based theme. Colors derive
from GTK's adaptive named colors so the UI follows the active light or
dark theme automatically, with a fixed Pactus teal accent that stays
legible on both. Adds modern tab styling with an accent underline,
rounded buttons/entries with focus rings, card-styled frames and
assistant pages, hover transitions, and a polished splash screen.
---
cmd/gtk/assets/css/style.css | 239 +++++++++++++++++++++++++++++------
1 file changed, 202 insertions(+), 37 deletions(-)
diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css
index f5b02f43c..931b882d8 100644
--- a/cmd/gtk/assets/css/style.css
+++ b/cmd/gtk/assets/css/style.css
@@ -1,98 +1,263 @@
+/*
+ * Pactus GUI theme foundation.
+ *
+ * Colors are derived from GTK's adaptive named colors (@theme_bg_color,
+ * @theme_fg_color, @theme_base_color ...) so the interface follows the
+ * active light or dark theme automatically. Only the Pactus brand accent
+ * is fixed; it is a mid-tone teal chosen to stay legible on both light
+ * and dark backgrounds.
+ *
+ * Note: GTK CSS variables (@define-color) hold colors only, not lengths,
+ * so radii and spacing are written as literal values kept consistent here.
+ */
+
+/* ---- Brand + token layer ------------------------------------------------ */
+
+@define-color pactus_navy #002235;
+@define-color pactus_accent #12909e;
+@define-color pactus_accent_hi #16a7b8;
+
+/* Route GTK's own accent/selection through the brand teal for cohesion. */
+@define-color accent_color @pactus_accent;
+@define-color accent_bg_color @pactus_accent;
+@define-color theme_selected_bg_color @pactus_accent;
+@define-color theme_selected_fg_color #ffffff;
+
+/* Surfaces + lines derived from the active theme (adapts to light/dark). */
+@define-color surface @theme_bg_color;
+@define-color card_bg mix(@theme_base_color, @theme_fg_color, 0.03);
+@define-color card_border alpha(@theme_fg_color, 0.10);
+@define-color hairline alpha(@theme_fg_color, 0.12);
+@define-color muted_fg alpha(@theme_fg_color, 0.62);
+@define-color hover_bg alpha(@theme_fg_color, 0.06);
+@define-color danger_fg #e0483a;
+
+/* ---- Base --------------------------------------------------------------- */
+
+window {
+ background-color: @surface;
+}
+
+label {
+ /* Gentle default; specific labels override below. */
+}
+
+separator {
+ margin: 8px 4px;
+ background-color: @hairline;
+ min-width: 1px;
+ min-height: 1px;
+}
+
+/* ---- Notebook tabs (main navigation) ------------------------------------ */
+
+notebook > header {
+ background-color: @surface;
+ border-bottom: 1px solid @hairline;
+ padding: 2px 4px 0 4px;
+}
+
+notebook > header tab {
+ margin: 0 2px;
+ padding: 8px 16px;
+ border-radius: 8px 8px 0 0;
+ border: none;
+ color: @muted_fg;
+ font-weight: 500;
+ transition: background-color 150ms ease, color 150ms ease;
+}
+
+notebook > header tab:hover {
+ background-color: @hover_bg;
+ color: @theme_fg_color;
+}
+
+notebook > header tab:checked {
+ color: @pactus_accent;
+ box-shadow: inset 0 -2px 0 0 @pactus_accent;
+}
+
+notebook > header tab:checked label {
+ font-weight: 700;
+}
+
+/* ---- Buttons ------------------------------------------------------------ */
+
+button {
+ border-radius: 8px;
+ padding: 6px 14px;
+ transition: background-color 150ms ease, box-shadow 150ms ease;
+}
+
+button:hover {
+ background-color: @hover_bg;
+}
+
+button.suggested-action,
+button.default {
+ background-image: none;
+ background-color: @pactus_accent;
+ color: #ffffff;
+ border: none;
+}
+
+button.suggested-action:hover,
+button.default:hover {
+ background-color: @pactus_accent_hi;
+}
+
+button.destructive-action {
+ background-image: none;
+ background-color: @danger_fg;
+ color: #ffffff;
+ border: none;
+}
+
.inline_button {
- padding: 2px;
- margin-right: 3px;
+ padding: 4px;
+ margin-right: 4px;
+ border-radius: 6px;
+}
+
+/* ---- Entries ------------------------------------------------------------ */
+
+entry {
+ border-radius: 8px;
+ padding: 6px 10px;
+ border: 1px solid @hairline;
+ transition: border-color 150ms ease, box-shadow 150ms ease;
+}
+
+entry:focus-within {
+ border-color: @pactus_accent;
+ box-shadow: 0 0 0 2px alpha(@pactus_accent, 0.30);
}
.copyable_entry {
padding-right: 36px;
}
-.warning {
- color: red;
+/* ---- Lists + rows ------------------------------------------------------- */
+
+list {
+ padding: 4px;
+ background-color: transparent;
+}
+
+list > row {
+ border-radius: 8px;
+ padding: 6px 8px;
+ margin: 1px 2px;
+ transition: background-color 150ms ease;
}
+list > row:hover {
+ background-color: @hover_bg;
+}
-separator {
- margin: 8px;
+/* ---- Cards / framed sections ------------------------------------------- */
+
+frame {
+ border-radius: 12px;
+ border: 1px solid @card_border;
+ background-color: @card_bg;
+ padding: 4px;
+}
+
+frame > label {
+ font-weight: 600;
+ margin: 2px 6px;
}
.widget-grid {
- margin-top: 12px;
- margin-bottom: 8px;
- margin-right: 4px;
- margin-left: 4px;
+ margin: 12px 6px 8px 6px;
}
.dialog-box {
- margin-top: 12px;
- margin-bottom: 8px;
- margin-right: 4px;
- margin-left: 4px;
+ margin: 12px 6px 8px 6px;
}
.dialog-action-bar {
-
+ padding-top: 8px;
}
.toolbar {
background-image: none;
background-color: @theme_bg_color;
box-shadow: none;
+ border-bottom: 1px solid @hairline;
}
-/*** Styles for Gtk.ListBox ***/
-list {
- padding: 2px;
- margin: 2px;
-}
+/* ---- TextView ----------------------------------------------------------- */
-/*** Styles for Gtk.TextView ***/
textview.view {
- padding: 2px;
- margin: 2px;
- border: 1px solid #000000;
+ padding: 6px;
+ border-radius: 8px;
+ border: 1px solid @hairline;
}
-/* Styles for the assistant page */
+/* ---- Status colors ------------------------------------------------------ */
+
+.warning {
+ color: @danger_fg;
+ font-weight: 600;
+}
+
+/* ---- Startup assistant -------------------------------------------------- */
.assistant-frame {
font-size: 14px;
- margin: 4px;
- padding: 4px;
+ margin: 8px;
+ padding: 14px;
+ border-radius: 12px;
+ border: 1px solid @card_border;
+ background-color: @card_bg;
+ transition: border-color 150ms ease, background-color 150ms ease;
+}
+
+.assistant-frame:hover {
+ border-color: alpha(@pactus_accent, 0.55);
}
.assistant-frame-label {
+ font-weight: 700;
margin-bottom: 12px;
}
.assistant-frame-desc {
- font-size: 14px;
+ font-size: 13px;
font-weight: normal;
+ color: @muted_fg;
margin-bottom: 12px;
- margin-left: 8px;
+ margin-left: 4px;
}
+/* ---- Splash screen ------------------------------------------------------ */
+
.splash {
- padding: 24px;
+ padding: 28px 32px;
+ background-color: @surface;
+ border-radius: 16px;
}
.splash-logo {
- margin-bottom: 8px;
- padding: 0px;
+ margin-bottom: 10px;
+ padding: 0;
}
.splash-spinner {
margin-bottom: 6px;
- padding: 0px;
+ padding: 0;
}
.splash-status {
font-size: 14px;
- font-weight: 500;
- margin-bottom: 6px;
+ font-weight: 600;
+ margin-bottom: 4px;
}
.splash-version {
font-size: 12px;
- opacity: 0.7;
-}
\ No newline at end of file
+ color: @muted_fg;
+ opacity: 0.8;
+}
From 1f3a950117db587bf746aecc8e6ea8b718cbcccb Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Sat, 11 Jul 2026 00:42:14 +0700
Subject: [PATCH 02/10] feat(gtk): title bar with logo and libadwaita
light/dark toggle
Add a custom header bar showing the Pactus logo next to the app name,
and a round moon/sun toggle beside the window buttons that switches the
appearance at runtime. Theme control now goes through the libadwaita
style manager (ColorSchemeForceLight/ForceDark), which works reliably on
Windows where gtk-application-prefer-dark-theme did not. Removes the
hard-coded dark preference at startup so the app follows the system
theme by default. Also brands progress bars, switches, checks and the
sync scale with the accent color.
---
cmd/gtk/app/run.go | 42 +++++++++++++++++++++
cmd/gtk/assets/css/style.css | 67 +++++++++++++++++++++++++++++++++
cmd/gtk/controller/navigator.go | 12 ++++++
cmd/gtk/main.go | 6 ++-
go.mod | 3 +-
go.sum | 4 ++
6 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/cmd/gtk/app/run.go b/cmd/gtk/app/run.go
index 029a87c24..91d3d767f 100644
--- a/cmd/gtk/app/run.go
+++ b/cmd/gtk/app/run.go
@@ -5,6 +5,7 @@ package app
import (
"context"
+ adw "github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/gogpu/systray"
"github.com/pactus-project/pactus/cmd"
@@ -99,6 +100,47 @@ func Run(ctx context.Context, conn grpc.ClientConnInterface,
mwView := gtkutil.IdleAddSyncT(func() *view.MainWindowView {
mwView := view.NewMainWindowView()
+ // Custom title bar: small Pactus logo next to the app name on the left,
+ // and a round light/dark toggle on the right, beside the window buttons.
+ header := gtk.NewHeaderBar()
+
+ brand := gtk.NewBox(gtk.OrientationHorizontal, 8)
+ brand.SetVAlign(gtk.AlignCenter)
+ logo := gtk.NewImageFromPaintable(assets.ImagePactusLogoTexture)
+ logo.SetPixelSize(24)
+ logo.AddCSSClass("app-logo")
+ title := gtk.NewLabel("Pactus GUI")
+ title.AddCSSClass("app-title")
+ brand.Append(logo)
+ brand.Append(title)
+ header.PackStart(brand)
+ // Suppress the centered window title so the brand stays left-aligned.
+ header.SetTitleWidget(gtk.NewLabel(""))
+
+ themeToggle := gtk.NewToggleButton()
+ themeToggle.AddCSSClass("theme-toggle")
+ themeToggle.AddCSSClass("circular")
+ themeToggle.SetVAlign(gtk.AlignCenter)
+ themeToggle.SetTooltipText("Toggle light / dark mode")
+ applyThemeIcon := func(dark bool) {
+ if dark {
+ themeToggle.SetLabel("๐")
+ } else {
+ themeToggle.SetLabel("โ")
+ }
+ }
+ startDark := adw.StyleManagerGetDefault().Dark()
+ themeToggle.SetActive(startDark)
+ applyThemeIcon(startDark)
+ themeToggle.ConnectToggled(func() {
+ active := themeToggle.Active()
+ nav.SetDarkMode(active)
+ applyThemeIcon(active)
+ })
+ header.PackEnd(themeToggle)
+
+ mwView.Window.SetTitlebar(header)
+
walletCtrl.SetupMenu(mwView.Window)
menu := nav.CreateMenu(isLocal)
diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css
index 931b882d8..95ab38d59 100644
--- a/cmd/gtk/assets/css/style.css
+++ b/cmd/gtk/assets/css/style.css
@@ -49,6 +49,34 @@ separator {
min-height: 1px;
}
+/* ---- Header bar (title bar with logo) ----------------------------------- */
+
+headerbar {
+ min-height: 42px;
+ padding: 3px 8px;
+ background-color: @surface;
+ border-bottom: 1px solid @hairline;
+ box-shadow: none;
+}
+
+.app-logo {
+ margin: 0 8px 0 2px;
+}
+
+.app-title {
+ font-weight: 700;
+ font-size: 14px;
+ color: @theme_fg_color;
+}
+
+.theme-toggle {
+ margin-right: 6px;
+ min-width: 28px;
+ min-height: 28px;
+ padding: 0;
+ font-size: 14px;
+}
+
/* ---- Notebook tabs (main navigation) ------------------------------------ */
notebook > header {
@@ -196,6 +224,45 @@ textview.view {
border: 1px solid @hairline;
}
+/* ---- Progress + controls ------------------------------------------------ */
+
+progressbar > trough {
+ border-radius: 6px;
+ min-height: 8px;
+ background-color: @hover_bg;
+}
+
+progressbar > trough > progress {
+ background-image: none;
+ background-color: @pactus_accent;
+ border-radius: 6px;
+}
+
+progressbar text {
+ color: @muted_fg;
+ font-size: 11px;
+}
+
+/* Brand the interactive controls. */
+spinner {
+ color: @pactus_accent;
+}
+
+check:checked,
+radio:checked {
+ background-image: none;
+ background-color: @pactus_accent;
+ border-color: @pactus_accent;
+}
+
+switch:checked {
+ background-color: @pactus_accent;
+}
+
+scale > trough > highlight {
+ background-color: @pactus_accent;
+}
+
/* ---- Status colors ------------------------------------------------------ */
.warning {
diff --git a/cmd/gtk/controller/navigator.go b/cmd/gtk/controller/navigator.go
index 088794981..1efe54d28 100644
--- a/cmd/gtk/controller/navigator.go
+++ b/cmd/gtk/controller/navigator.go
@@ -3,6 +3,7 @@
package controller
import (
+ adw "github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -162,6 +163,17 @@ func (n *Navigator) Quit() {
n.gtkApp.Quit()
}
+// SetDarkMode forces the light or dark appearance at runtime through the
+// libadwaita style manager, independent of the operating system theme.
+// It must be called on the UI thread.
+func (*Navigator) SetDarkMode(enabled bool) {
+ scheme := adw.ColorSchemeForceLight
+ if enabled {
+ scheme = adw.ColorSchemeForceDark
+ }
+ adw.StyleManagerGetDefault().SetColorScheme(scheme)
+}
+
func (n *Navigator) CreateMenu(isLocal bool) *gio.Menu {
// Helper to create an app action that calls a given function
createAppAction := func(name string, callback func()) {
diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go
index 5bd5052e5..6471fe3b8 100644
--- a/cmd/gtk/main.go
+++ b/cmd/gtk/main.go
@@ -13,6 +13,7 @@ import (
"sync"
"time"
+ adw "github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -64,6 +65,9 @@ func init() {
}
gtk.Init()
+ // Initialize libadwaita so its StyleManager controls the light/dark
+ // appearance, independent of the operating system theme.
+ adw.Init()
}
//nolint:gocognit // needs refactoring
@@ -79,8 +83,6 @@ func main() {
// Create a new app.
app := gtk.NewApplication(appID, gio.ApplicationNonUnique)
gtk.WidgetSetDefaultDirection(gtk.TextDirLTR)
- settings := gtk.SettingsGetDefault()
- settings.Object.SetObjectProperty("gtk-application-prefer-dark-theme", true)
// apply custom css
provider := gtk.NewCSSProvider()
diff --git a/go.mod b/go.mod
index e6345db72..d93622812 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
github.com/c-bata/go-prompt v0.2.6
github.com/consensys/gnark-crypto v0.20.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1
- github.com/diamondburned/gotk4/pkg v0.3.1
+ github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a
github.com/fxamacker/cbor/v2 v2.9.2
github.com/glebarez/go-sqlite v1.22.1-0.20250214171204-e6de9fc0c320
github.com/go-zeromq/zmq4 v0.17.0
@@ -69,6 +69,7 @@ require (
github.com/creachadair/mds v0.29.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
+ github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index 2056c682a..5e24eb675 100644
--- a/go.sum
+++ b/go.sum
@@ -55,8 +55,12 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6 h1:WzOC3KtvrC1hJMz3fbJBg0Ye50nt4Tafor+a/bBHNEA=
+github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6/go.mod h1:ZzYiyPe0TqsukfPHi0sK/WwKzm0wIJdSRylLnuvAZNw=
github.com/diamondburned/gotk4/pkg v0.3.1 h1:uhkXSUPUsCyz3yujdvl7DSN8jiLS2BgNTQE95hk6ygg=
github.com/diamondburned/gotk4/pkg v0.3.1/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4=
+github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a h1:dN2jYYZ71hFhoKFSn24pQdKWLZb/XDydBt8pEIkFjJo=
+github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a/go.mod h1:O9K8+PGNFGJpAu8+u5D2Sn5Wae4hxWzHB+AeZNbV/2Q=
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
From 6c44e6b0d26d2c57ddb894a13068b1da8b5d38ed Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Sat, 11 Jul 2026 01:55:04 +0700
Subject: [PATCH 03/10] feat(gtk): redesign Node tab with metric cards and
circular sync gauge
Replace the flat label grid with a responsive GtkFlowBox of grouped
metric cards (Network Health, Validator Network, Local Node Info) that
reflow on narrow windows, plus a dedicated synchronization card. Add a
custom circular progress gauge (DrawingArea + cairo) that fills with the
sync percentage and shows it in the center, replacing the plain progress
bar. All original fields are preserved.
---
cmd/gtk/assets/css/style.css | 57 +
cmd/gtk/assets/ui/widget_node.ui | 1012 +++++++++++-------
cmd/gtk/controller/node_widget_controller.go | 3 +-
cmd/gtk/view/circular_progress.go | 92 ++
cmd/gtk/view/node_widget_view.go | 6 +-
5 files changed, 795 insertions(+), 375 deletions(-)
create mode 100644 cmd/gtk/view/circular_progress.go
diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css
index 95ab38d59..028f267df 100644
--- a/cmd/gtk/assets/css/style.css
+++ b/cmd/gtk/assets/css/style.css
@@ -224,6 +224,63 @@ textview.view {
border: 1px solid @hairline;
}
+/* ---- Node status cards -------------------------------------------------- */
+
+.node-content {
+ margin: 16px;
+}
+
+.card {
+ padding: 14px 16px;
+ min-width: 190px;
+}
+
+.overview-flow > flowboxchild {
+ padding: 0;
+}
+
+.section-title {
+ font-weight: 800;
+ font-size: 12px;
+ color: @muted_fg;
+ margin-bottom: 8px;
+}
+
+.metric {
+ padding: 7px 0;
+ border-bottom: 1px solid @hairline;
+}
+
+.metric-last {
+ border-bottom: none;
+}
+
+.metric-icon {
+ font-size: 15px;
+}
+
+.metric-label {
+ font-size: 11px;
+ color: @muted_fg;
+}
+
+.metric-value {
+ font-size: 14px;
+ font-weight: 700;
+}
+
+.sync-card {
+ padding: 20px 24px;
+}
+
+/* ---- Circular sync gauge ------------------------------------------------ */
+
+.circular-progress-label {
+ font-size: 15px;
+ font-weight: 700;
+ color: @theme_fg_color;
+}
+
/* ---- Progress + controls ------------------------------------------------ */
progressbar > trough {
diff --git a/cmd/gtk/assets/ui/widget_node.ui b/cmd/gtk/assets/ui/widget_node.ui
index c88c7da86..910bc3c79 100644
--- a/cmd/gtk/assets/ui/widget_node.ui
+++ b/cmd/gtk/assets/ui/widget_node.ui
@@ -1,383 +1,653 @@
-
diff --git a/cmd/gtk/assets/ui/widget_wallet.ui b/cmd/gtk/assets/ui/widget_wallet.ui
index 3e0ceae86..377278d5f 100644
--- a/cmd/gtk/assets/ui/widget_wallet.ui
+++ b/cmd/gtk/assets/ui/widget_wallet.ui
@@ -1,241 +1,278 @@
-
- widget-main-box
- True
+ node-content
vertical
+ 12
+
+
-
- left
+
+ page-header
+ 8
+
+
+ vertical
+ True
+ center
+
+
+ Wallet
+ page-title
+ start
+ 0
+
+
+
+
+ page-subtitle
+ start
+ 0
+ True
+
+
+
+
+
+
+ suggested-action
+ center
+
+
+
+
+ center
+
+
+
+
+ center
+
+
+
+
+ center
+
+
+
+
+
+
+
+
+ True
+ 12
+
+
+ card
+ vertical
+ 4
+
+
+ Total Balance
+ section-title
+ start
+
+
+
+
+ stat-value
+ start
+ 0
+ True
+ end
+
+
+
+
-
+
+ card
vertical
+ 4
+
+
+ Total Stake
+ section-title
+ start
+
+
+
+
+ stat-value
+ start
+ 0
+ True
+ end
+
+
+
+
+
+
+ card
+ vertical
+ 4
+
+
+ Default Fee
+ section-title
+ start
+
+
+
+
+ stat-value
+ start
+ 0
+ True
+ end
+
+
+
+
+
+
+
+
+
+
+ card
+ vertical
+ 10
+
+
+ True
+ 24
- 8
- widget-grid
- True
- 8
- True
+ info-grid
+ 16
+ 10
+ Driver
+ info-key
start
- Name:
- start
-
- 0
- 0
-
-
-
-
-
- start
- True
- start
-
- 1
- 0
-
-
-
-
-
- start
- Location:
- start
-
- 0
- 1
-
-
-
-
-
- start
- True
- start
-
- 1
- 1
-
-
-
-
-
- start
- Driver:
- True
- start
-
- 0
- 2
-
+ 00
+ info-val
start
+ 0
+ True
True
- start
-
- 1
- 2
-
+ 10
+ Created At
+ info-key
start
- Created At:
- True
- start
-
- 0
- 3
-
+ 01
+ info-val
start
+ 0
+ True
True
- start
-
- 1
- 3
-
+ 11
+
+
+
+
+ info-grid
+ 16
+ 10
+ Encrypted
+ info-key
start
- Encrypted:
- start
-
- 0
- 4
-
+ 00
+ info-val
start
- start
-
- 1
- 4
-
+ 0
+ True
+ 10
+ Location
+ info-key
start
- Total Balance:
- start
-
- 0
- 5
-
+ 01
-
- start
- start
-
- 1
- 5
-
-
-
-
-
- start
- Total Stake:
- start
-
- 0
- 6
-
-
-
-
-
- start
- start
-
- 1
- 6
-
-
-
-
-
- start
- Default Fee:
- start
-
- 0
- 7
-
-
-
-
-
+
+ info-val
start
- start
-
- 1
- 7
-
+ 0
+ True
+ True
+ end
+ 11
-
-
- Info
-
-
+
+
+
+
+
+
+ wallet-tabs
+ True
vertical
+ 10
- vertical
+ 8
-
-
-
- True
-
-
+
+ Addresses
+ section-title
+ start
+ True
-
- toolbar
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ center
+
+
+
+
+
+
+ True
+ True
+ never
+
+
+ True
+ True
@@ -250,30 +287,44 @@
vertical
+ 10
- vertical
+ 8
+
+
+ Transactions
+ section-title
+ start
+ True
+
+
+
+
+ center
+
+
-
-
-
- True
-
-
+
+ center
-
- toolbar
-
-
-
-
-
-
-
-
-
+
+ center
+
+
+
+
+
+
+ True
+ True
+ never
+
+
+ True
+ True
diff --git a/cmd/gtk/controller/committee_widget_controller.go b/cmd/gtk/controller/committee_widget_controller.go
index 831be70af..7d208fba7 100644
--- a/cmd/gtk/controller/committee_widget_controller.go
+++ b/cmd/gtk/controller/committee_widget_controller.go
@@ -48,27 +48,33 @@ func NewCommitteeWidgetController(
func (c *CommitteeWidgetController) BuildView(ctx context.Context) error {
gtkutil.IdleAddSync(func() {
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "No", func(row committeeRow) string {
- return strconv.Itoa(row.no)
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Address", func(row committeeRow) string {
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "No", 0, false, "cell-dim",
+ func(row committeeRow) string {
+ return strconv.Itoa(row.no)
+ })
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewMembers, "Address", func(row committeeRow) string {
return row.val.GetAddress()
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Stake", func(row committeeRow) string {
- return amount.Amount(row.val.GetStake()).String()
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Bonding Height", func(row committeeRow) string {
- return strconv.Itoa(int(row.val.GetLastBondingHeight()))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Sortition Height", func(row committeeRow) string {
- return strconv.Itoa(int(row.val.GetLastSortitionHeight()))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Protocol Version", func(row committeeRow) string {
- return strconv.Itoa(int(row.val.GetProtocolVersion()))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Availability Score", func(row committeeRow) string {
- return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore())
- })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Stake", 1, false, "cell-num",
+ func(row committeeRow) string {
+ return amount.Amount(row.val.GetStake()).String()
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Bonding Height", 1, false, "cell-num",
+ func(row committeeRow) string {
+ return strconv.Itoa(int(row.val.GetLastBondingHeight()))
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Sortition Height", 1, false, "cell-num",
+ func(row committeeRow) string {
+ return strconv.Itoa(int(row.val.GetLastSortitionHeight()))
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Protocol", 1, false, "cell-num",
+ func(row committeeRow) string {
+ return strconv.Itoa(int(row.val.GetProtocolVersion()))
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Availability", 1, false, "cell-num",
+ func(row committeeRow) string {
+ return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore())
+ })
})
scheduler.Every(refreshCommitteeInterval).Do(ctx, func(ctx context.Context) {
diff --git a/cmd/gtk/controller/network_widget_controller.go b/cmd/gtk/controller/network_widget_controller.go
index b16f0c9b5..88cf160a7 100644
--- a/cmd/gtk/controller/network_widget_controller.go
+++ b/cmd/gtk/controller/network_widget_controller.go
@@ -56,27 +56,31 @@ func NewNetworkWidgetController(
func (c *NetworkWidgetController) BuildView(ctx context.Context) error {
gtkutil.IdleAddSync(func() {
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "No", func(row peerRow) string {
- return strconv.Itoa(row.no)
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Moniker", func(row peerRow) string {
- return row.peer.GetMoniker()
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Address", func(row peerRow) string {
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "No", 0, false, "cell-dim",
+ func(row peerRow) string {
+ return strconv.Itoa(row.no)
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "Moniker", 0, false, "",
+ func(row peerRow) string {
+ return row.peer.GetMoniker()
+ })
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewPeers, "Address", func(row peerRow) string {
return row.peer.GetAddress()
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Peer ID", func(row peerRow) string {
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewPeers, "Peer ID", func(row peerRow) string {
return row.peer.GetPeerId()
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Height", func(row peerRow) string {
- return strconv.Itoa(int(row.peer.GetHeight()))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Agent", func(row peerRow) string {
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "Height", 1, false, "cell-num",
+ func(row peerRow) string {
+ return strconv.Itoa(int(row.peer.GetHeight()))
+ })
+ gtkutil.ColumnViewAppendEllipsizedColumn(c.view.ColViewPeers, "Agent", func(row peerRow) string {
return row.peer.GetAgent()
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Direction", func(row peerRow) string {
- return peerDirectionString(row.peer.GetDirection())
- })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "Direction", 0, false, "",
+ func(row peerRow) string {
+ return peerDirectionString(row.peer.GetDirection())
+ })
})
scheduler.Every(refreshNetworkInterval).Do(ctx, func(ctx context.Context) {
diff --git a/cmd/gtk/controller/node_widget_controller.go b/cmd/gtk/controller/node_widget_controller.go
index 4fb027d50..fd82936a5 100644
--- a/cmd/gtk/controller/node_widget_controller.go
+++ b/cmd/gtk/controller/node_widget_controller.go
@@ -100,11 +100,11 @@ func (c *NodeWidgetController) timeoutProgress() {
func (c *NodeWidgetController) setSyncStatus(percentage float64) {
if percentage >= 1 {
c.view.LabelSyncStatus.SetMarkup(
- "All blocks downloaded")
+ "Fully synced")
return
}
- c.view.LabelSyncStatus.SetText("Downloading blocksโฆ")
+ c.view.LabelSyncStatus.SetText("Syncing blocksโฆ")
}
func (c *NodeWidgetController) timeoutInfo() {
diff --git a/cmd/gtk/controller/validator_widget_controller.go b/cmd/gtk/controller/validator_widget_controller.go
index 654bd6f14..677d334bb 100644
--- a/cmd/gtk/controller/validator_widget_controller.go
+++ b/cmd/gtk/controller/validator_widget_controller.go
@@ -43,27 +43,33 @@ func NewValidatorWidgetController(
func (c *ValidatorWidgetController) BuildView(ctx context.Context) error {
gtkutil.IdleAddSync(func() {
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "No", func(row validatorRow) string {
- return strconv.Itoa(row.no)
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Address", func(row validatorRow) string {
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "No", 0, false, "cell-dim",
+ func(row validatorRow) string {
+ return strconv.Itoa(row.no)
+ })
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewValidators, "Address", func(row validatorRow) string {
return row.val.GetAddress()
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Stake", func(row validatorRow) string {
- return amount.Amount(row.val.GetStake()).String()
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Bonding Height", func(row validatorRow) string {
- return strconv.Itoa(int(row.val.GetLastBondingHeight()))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Sortition Height", func(row validatorRow) string {
- return strconv.Itoa(int(row.val.GetLastSortitionHeight()))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Unbonding Height", func(row validatorRow) string {
- return strconv.Itoa(int(row.val.GetUnbondingHeight()))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Availability Score", func(row validatorRow) string {
- return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore())
- })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Stake", 1, false, "cell-num",
+ func(row validatorRow) string {
+ return amount.Amount(row.val.GetStake()).String()
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Bonding Height", 1, false, "cell-num",
+ func(row validatorRow) string {
+ return strconv.Itoa(int(row.val.GetLastBondingHeight()))
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Sortition Height", 1, false, "cell-num",
+ func(row validatorRow) string {
+ return strconv.Itoa(int(row.val.GetLastSortitionHeight()))
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Unbonding Height", 1, false, "cell-num",
+ func(row validatorRow) string {
+ return strconv.Itoa(int(row.val.GetUnbondingHeight()))
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Availability Score", 1, false, "cell-num",
+ func(row validatorRow) string {
+ return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore())
+ })
})
scheduler.Every(refreshValidatorsInterval).Do(ctx, func(ctx context.Context) {
diff --git a/cmd/gtk/controller/wallet_widget_controller.go b/cmd/gtk/controller/wallet_widget_controller.go
index 56e5bf1bb..4432e8ff8 100644
--- a/cmd/gtk/controller/wallet_widget_controller.go
+++ b/cmd/gtk/controller/wallet_widget_controller.go
@@ -61,10 +61,11 @@ func (c *WalletWidgetController) BuildView(ctx context.Context, nav *Navigator)
gtkutil.ColumnViewSetup(c.view.ColViewTransactions, c.lsTransactions)
// Setup address columns
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "No", func(row addressRow) string {
- return strconv.Itoa(row.no)
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Address", func(row addressRow) string {
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "No", 0, false, "cell-dim",
+ func(row addressRow) string {
+ return strconv.Itoa(row.no)
+ })
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewAddresses, "Address", func(row addressRow) string {
return row.addr.Address
})
gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Type", func(row addressRow) string {
@@ -75,41 +76,49 @@ func (c *WalletWidgetController) BuildView(ctx context.Context, nav *Navigator)
return typ.String()
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Label", func(row addressRow) string {
- return row.addr.Label
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Balance", func(row addressRow) string {
- return amount.Amount(row.addr.Balance).String()
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Stake", func(row addressRow) string {
- return amount.Amount(row.addr.Stake).String()
- })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "Label", 0, false, "",
+ func(row addressRow) string {
+ return row.addr.Label
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "Balance", 1, false, "cell-num",
+ func(row addressRow) string {
+ return amount.Amount(row.addr.Balance).String()
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "Stake", 1, false, "cell-num",
+ func(row addressRow) string {
+ return amount.Amount(row.addr.Stake).String()
+ })
// Setup transaction columns
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "No", func(row transactionRow) string {
- return strconv.Itoa(int(row.trx.No))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "ID", func(row transactionRow) string {
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "No", 0, false, "cell-dim",
+ func(row transactionRow) string {
+ return strconv.Itoa(int(row.trx.No))
+ })
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewTransactions, "ID", func(row transactionRow) string {
return row.trx.TxId
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Sender", func(row transactionRow) string {
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewTransactions, "Sender", func(row transactionRow) string {
return row.trx.Sender
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Receiver", func(row transactionRow) string {
+ gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewTransactions, "Receiver", func(row transactionRow) string {
return row.trx.Receiver
})
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Type", func(row transactionRow) string {
- return payload.Type(row.trx.PayloadType).String()
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Amount", func(row transactionRow) string {
- return amount.Amount(row.trx.Amount).String()
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Direction", func(row transactionRow) string {
- return getDirectionTextWithIcon(types.TxDirection(row.trx.Direction))
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Status", func(row transactionRow) string {
- return types.TransactionStatus(row.trx.Status).String()
- })
- gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Comment", func(row transactionRow) string {
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Type", 0, false, "",
+ func(row transactionRow) string {
+ return payload.Type(row.trx.PayloadType).String()
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Amount", 1, false, "cell-num",
+ func(row transactionRow) string {
+ return amount.Amount(row.trx.Amount).String()
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Direction", 0, false, "",
+ func(row transactionRow) string {
+ return getDirectionTextWithIcon(types.TxDirection(row.trx.Direction))
+ })
+ gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Status", 0, false, "",
+ func(row transactionRow) string {
+ return types.TransactionStatus(row.trx.Status).String()
+ })
+ gtkutil.ColumnViewAppendEllipsizedColumn(c.view.ColViewTransactions, "Comment", func(row transactionRow) string {
return row.trx.Comment
})
})
diff --git a/cmd/gtk/gtkutil/columnview.go b/cmd/gtk/gtkutil/columnview.go
index 51196bc9d..9a2d40bb2 100644
--- a/cmd/gtk/gtkutil/columnview.go
+++ b/cmd/gtk/gtkutil/columnview.go
@@ -6,6 +6,7 @@ import (
"github.com/diamondburned/gotk4/pkg/core/gioutil"
"github.com/diamondburned/gotk4/pkg/core/glib"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
+ "github.com/diamondburned/gotk4/pkg/pango"
)
func ColumnViewAppendTextColumn[T any](colView *gtk.ColumnView, title string, extractor func(T) string) {
@@ -16,6 +17,98 @@ func ColumnViewAppendTextColumn[T any](colView *gtk.ColumnView, title string, ex
colView.AppendColumn(column)
}
+// ColumnViewAppendTextColumnEx appends a text column with control over the cell
+// text alignment (xalign 0 = left, 1 = right), whether the column expands to
+// fill remaining width, and an optional CSS class applied to the cell label.
+func ColumnViewAppendTextColumnEx[T any](colView *gtk.ColumnView, title string,
+ xalign float32, expand bool, cssClass string, extractor func(T) string,
+) {
+ factory := gtk.NewSignalListItemFactory()
+ factory.ConnectSetup(func(obj *glib.Object) {
+ cell := obj.Cast().(*gtk.ColumnViewCell)
+ label := gtk.NewLabel("")
+ label.SetHExpand(true)
+ label.SetXAlign(xalign)
+ if cssClass != "" {
+ label.AddCSSClass(cssClass)
+ }
+ cell.SetChild(label)
+ })
+ factory.ConnectBind(func(obj *glib.Object) {
+ cell := obj.Cast().(*gtk.ColumnViewCell)
+ row := gioutil.ObjectValue[T](cell.Item())
+ label := cell.Child().(*gtk.Label)
+ label.SetText(extractor(row))
+ })
+
+ column := gtk.NewColumnViewColumn(title, &factory.ListItemFactory)
+ column.SetTitle(title)
+ column.SetExpand(expand)
+ column.SetResizable(true)
+
+ colView.AppendColumn(column)
+}
+
+// ColumnViewAppendEllipsizedColumn appends an expanding column whose long text
+// is ellipsized at the end, with the full value available on hover.
+func ColumnViewAppendEllipsizedColumn[T any](colView *gtk.ColumnView, title string, extractor func(T) string) {
+ factory := gtk.NewSignalListItemFactory()
+ factory.ConnectSetup(func(obj *glib.Object) {
+ cell := obj.Cast().(*gtk.ColumnViewCell)
+ label := gtk.NewLabel("")
+ label.SetHExpand(true)
+ label.SetXAlign(0)
+ label.SetEllipsize(pango.EllipsizeEnd)
+ cell.SetChild(label)
+ })
+ factory.ConnectBind(func(obj *glib.Object) {
+ cell := obj.Cast().(*gtk.ColumnViewCell)
+ row := gioutil.ObjectValue[T](cell.Item())
+ label := cell.Child().(*gtk.Label)
+ value := extractor(row)
+ label.SetText(value)
+ label.SetTooltipText(value)
+ })
+
+ column := gtk.NewColumnViewColumn(title, &factory.ListItemFactory)
+ column.SetTitle(title)
+ column.SetExpand(true)
+ column.SetResizable(true)
+
+ colView.AppendColumn(column)
+}
+
+// ColumnViewAppendAddressColumn appends an expanding column that middle-
+// ellipsizes long identifiers such as addresses and shows the full value on
+// hover, so the table never needs horizontal scrolling.
+func ColumnViewAppendAddressColumn[T any](colView *gtk.ColumnView, title string, extractor func(T) string) {
+ factory := gtk.NewSignalListItemFactory()
+ factory.ConnectSetup(func(obj *glib.Object) {
+ cell := obj.Cast().(*gtk.ColumnViewCell)
+ label := gtk.NewLabel("")
+ label.SetHExpand(true)
+ label.SetXAlign(0)
+ label.SetEllipsize(pango.EllipsizeMiddle)
+ label.AddCSSClass("cell-mono")
+ cell.SetChild(label)
+ })
+ factory.ConnectBind(func(obj *glib.Object) {
+ cell := obj.Cast().(*gtk.ColumnViewCell)
+ row := gioutil.ObjectValue[T](cell.Item())
+ label := cell.Child().(*gtk.Label)
+ value := extractor(row)
+ label.SetText(value)
+ label.SetTooltipText(value)
+ })
+
+ column := gtk.NewColumnViewColumn(title, &factory.ListItemFactory)
+ column.SetTitle(title)
+ column.SetExpand(true)
+ column.SetResizable(true)
+
+ colView.AppendColumn(column)
+}
+
func ColumnViewCreateTextColumn[T any](title string, extractor func(T) string) *gtk.ColumnViewColumn {
factory := gtk.NewSignalListItemFactory()
factory.ConnectSetup(func(obj *glib.Object) {
@@ -84,7 +177,8 @@ func ColumnViewGetSelectedItem[T any](colView *gtk.ColumnView, model *gioutil.Li
}
func ColumnViewSetDefaultProperties(colView *gtk.ColumnView) {
- colView.SetShowRowSeparators(true)
- colView.SetShowColumnSeparators(true)
+ colView.SetShowRowSeparators(false)
+ colView.SetShowColumnSeparators(false)
colView.SetSingleClickActivate(true)
+ colView.AddCSSClass("data-table")
}
From 5d256d3456d4798111861a27104c09b2a1b7730a Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Sat, 11 Jul 2026 09:48:08 +0700
Subject: [PATCH 06/10] fix(gtk): force English for GTK built-in widget strings
The interface text is English only, but GTK and libadwaita translate
their built-in widgets (assistant navigation, about dialog, file
choosers) to the operating system locale, which produced a mix of
languages. Set LANGUAGE=en before initialization so those strings stay
English too. This affects message translation only, not number or date
formatting.
---
cmd/gtk/main.go | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go
index 6471fe3b8..38da467dc 100644
--- a/cmd/gtk/main.go
+++ b/cmd/gtk/main.go
@@ -64,6 +64,12 @@ func init() {
_ = os.Setenv("PANGOCAIRO_BACKEND", "fontconfig")
}
+ // The GUI text is English only, so force GTK's built-in widget strings
+ // (assistant navigation, about dialog, file choosers ...) to English too
+ // instead of following the operating system locale. This affects message
+ // translation only, not number or date formatting.
+ _ = os.Setenv("LANGUAGE", "en")
+
gtk.Init()
// Initialize libadwaita so its StyleManager controls the light/dark
// appearance, independent of the operating system theme.
From c35bd0d0c46449d32c0c09e638f9b515d51b2ca1 Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Sat, 11 Jul 2026 09:53:28 +0700
Subject: [PATCH 07/10] fix(gtk): force English UI language on Windows
Setting LANGUAGE=en is not enough on Windows: GTK and libadwaita pick
their translations from the thread UI language, so the built-in
GtkAssistant navigation buttons still appeared in the system language.
Call SetThreadUILanguage(en-US) before initialization so those strings
render in English, matching the rest of the interface.
---
cmd/gtk/locale_other.go | 7 +++++++
cmd/gtk/locale_windows.go | 16 ++++++++++++++++
cmd/gtk/main.go | 1 +
3 files changed, 24 insertions(+)
create mode 100644 cmd/gtk/locale_other.go
create mode 100644 cmd/gtk/locale_windows.go
diff --git a/cmd/gtk/locale_other.go b/cmd/gtk/locale_other.go
new file mode 100644
index 000000000..d81414b21
--- /dev/null
+++ b/cmd/gtk/locale_other.go
@@ -0,0 +1,7 @@
+//go:build gtk && !windows
+
+package main
+
+// forceEnglishUILanguage is a no-op outside Windows, where the LANGUAGE
+// environment variable already controls GTK's message catalogs.
+func forceEnglishUILanguage() {}
diff --git a/cmd/gtk/locale_windows.go b/cmd/gtk/locale_windows.go
new file mode 100644
index 000000000..773c4830d
--- /dev/null
+++ b/cmd/gtk/locale_windows.go
@@ -0,0 +1,16 @@
+//go:build gtk && windows
+
+package main
+
+import "golang.org/x/sys/windows"
+
+// forceEnglishUILanguage sets the thread UI language to English (en-US), so
+// GTK and libadwaita load their English catalogs instead of following the
+// Windows display language. On Windows GTK reads this rather than the LANGUAGE
+// environment variable.
+func forceEnglishUILanguage() {
+ const langEnUS = 0x0409
+
+ proc := windows.NewLazySystemDLL("kernel32.dll").NewProc("SetThreadUILanguage")
+ _, _, _ = proc.Call(uintptr(langEnUS))
+}
diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go
index 38da467dc..db426d796 100644
--- a/cmd/gtk/main.go
+++ b/cmd/gtk/main.go
@@ -69,6 +69,7 @@ func init() {
// instead of following the operating system locale. This affects message
// translation only, not number or date formatting.
_ = os.Setenv("LANGUAGE", "en")
+ forceEnglishUILanguage()
gtk.Init()
// Initialize libadwaita so its StyleManager controls the light/dark
From ededdbaeb9597c494aaa86463580988ecbfbf8d0 Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Sat, 11 Jul 2026 09:58:15 +0700
Subject: [PATCH 08/10] fix(gtk): set locale env via g_setenv so GTK sees
English
os.Setenv writes to the Win32 environment block, which the C runtime
that GTK and libintl link against does not read, so LANGUAGE=en was
invisible to gettext and the built-in widget strings stayed in the
system language. Set the variables through g_setenv instead, which
updates the same C runtime environment GTK uses, so its assistant and
dialog strings render in English.
---
cmd/gtk/main.go | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go
index db426d796..fd293a80d 100644
--- a/cmd/gtk/main.go
+++ b/cmd/gtk/main.go
@@ -16,6 +16,7 @@ import (
adw "github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gio/v2"
+ "github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/gofrs/flock"
"github.com/pactus-project/gopkg/signal"
@@ -68,7 +69,11 @@ func init() {
// (assistant navigation, about dialog, file choosers ...) to English too
// instead of following the operating system locale. This affects message
// translation only, not number or date formatting.
- _ = os.Setenv("LANGUAGE", "en")
+ // Use g_setenv, not os.Setenv: on Windows the latter uses the Win32
+ // environment block, which the C runtime that GTK/libintl links against
+ // does not read, so the change would be invisible to gettext.
+ glib.Setenv("LANGUAGE", "en", true)
+ glib.Setenv("LC_MESSAGES", "en", true)
forceEnglishUILanguage()
gtk.Init()
From 529c2fbbff93d9587c8226319eac3e122e9e8fe0 Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Sat, 11 Jul 2026 10:31:00 +0700
Subject: [PATCH 09/10] fix(gtk): stop the About dialogs highlighting the
program name
GtkAboutDialog gives its selectable program-name label the initial
keyboard focus, which selects the whole label and makes "Pactus" and
"GTK" look highlighted. Walk the dialog tree and make its labels
non-selectable so the name renders plainly.
---
cmd/gtk/gtkutil/gtkutil.go | 23 +++++++++++++++++++++++
cmd/gtk/view/about_dialog_view.go | 2 ++
cmd/gtk/view/about_gtk_dialog_view.go | 2 ++
3 files changed, 27 insertions(+)
diff --git a/cmd/gtk/gtkutil/gtkutil.go b/cmd/gtk/gtkutil/gtkutil.go
index 4440e4ea3..938fb17a1 100644
--- a/cmd/gtk/gtkutil/gtkutil.go
+++ b/cmd/gtk/gtkutil/gtkutil.go
@@ -413,6 +413,29 @@ func IsWidgetShowing(widget *gtk.Widget) bool {
return widget.Mapped()
}
+// DisableLabelSelection walks the widget tree and makes every label
+// non-selectable. GtkAboutDialog otherwise auto-selects its program-name label
+// when it grabs focus on open, which looks like highlighted text.
+func DisableLabelSelection(root gtk.Widgetter) {
+ parent, ok := root.(interface{ FirstChild() gtk.Widgetter })
+ if !ok {
+ return
+ }
+
+ for child := parent.FirstChild(); child != nil; {
+ if label, isLabel := child.(*gtk.Label); isLabel {
+ label.SetSelectable(false)
+ }
+ DisableLabelSelection(child)
+
+ sibling, hasSibling := child.(interface{ NextSibling() gtk.Widgetter })
+ if !hasSibling {
+ break
+ }
+ child = sibling.NextSibling()
+ }
+}
+
func ClearLable(label *gtk.Label) {
label.SetText("")
}
diff --git a/cmd/gtk/view/about_dialog_view.go b/cmd/gtk/view/about_dialog_view.go
index 4b6853b0c..44d651e64 100644
--- a/cmd/gtk/view/about_dialog_view.go
+++ b/cmd/gtk/view/about_dialog_view.go
@@ -15,5 +15,7 @@ func NewAboutDialog() *gtk.AboutDialog {
pic := gtkutil.NewScaledPictureFromTexture(assets.ImagePactusLogoTexture, 128, 128)
dlg.SetLogo(pic.Paintable())
+ gtkutil.DisableLabelSelection(dlg)
+
return dlg
}
diff --git a/cmd/gtk/view/about_gtk_dialog_view.go b/cmd/gtk/view/about_gtk_dialog_view.go
index 81ed0f7f4..c2cf81f52 100644
--- a/cmd/gtk/view/about_gtk_dialog_view.go
+++ b/cmd/gtk/view/about_gtk_dialog_view.go
@@ -15,5 +15,7 @@ func NewAboutGTKDialog() *gtk.AboutDialog {
pic := gtkutil.NewScaledPictureFromTexture(assets.ImageGTKLogoTexture, 128, 128)
dlg.SetLogo(pic.Paintable())
+ gtkutil.DisableLabelSelection(dlg)
+
return dlg
}
From 3948ad37c254ba357e7665667532e344cb3b1b3e Mon Sep 17 00:00:00 2001
From: andrut-ds <82076528+andrut-ds@users.noreply.github.com>
Date: Sat, 11 Jul 2026 10:31:00 +0700
Subject: [PATCH 10/10] feat(gtk): remember the light/dark choice across
restarts
The dark-mode toggle only changed the libadwaita color scheme for the
running session, so the app fell back to the system theme on the next
launch. Persist the choice to the user config directory and restore it
on startup, still following the system theme until the user picks one.
---
cmd/gtk/app/run.go | 6 ++++
cmd/gtk/controller/navigator.go | 1 +
cmd/gtk/gtkutil/settings.go | 55 +++++++++++++++++++++++++++++++++
3 files changed, 62 insertions(+)
create mode 100644 cmd/gtk/gtkutil/settings.go
diff --git a/cmd/gtk/app/run.go b/cmd/gtk/app/run.go
index 77ceda7e5..0b18f1fc8 100644
--- a/cmd/gtk/app/run.go
+++ b/cmd/gtk/app/run.go
@@ -133,7 +133,13 @@ func Run(ctx context.Context, conn grpc.ClientConnInterface,
themeToggle.SetLabel("โ")
}
}
+ // Restore the persisted light/dark choice, falling back to the system
+ // theme when the user has not chosen yet.
startDark := adw.StyleManagerGetDefault().Dark()
+ if saved, ok := gtkutil.LoadDarkMode(); ok {
+ startDark = saved
+ nav.SetDarkMode(saved)
+ }
themeToggle.SetActive(startDark)
applyThemeIcon(startDark)
themeToggle.ConnectToggled(func() {
diff --git a/cmd/gtk/controller/navigator.go b/cmd/gtk/controller/navigator.go
index 1efe54d28..6d7d983fd 100644
--- a/cmd/gtk/controller/navigator.go
+++ b/cmd/gtk/controller/navigator.go
@@ -172,6 +172,7 @@ func (*Navigator) SetDarkMode(enabled bool) {
scheme = adw.ColorSchemeForceDark
}
adw.StyleManagerGetDefault().SetColorScheme(scheme)
+ gtkutil.SaveDarkMode(enabled)
}
func (n *Navigator) CreateMenu(isLocal bool) *gio.Menu {
diff --git a/cmd/gtk/gtkutil/settings.go b/cmd/gtk/gtkutil/settings.go
new file mode 100644
index 000000000..dd360150b
--- /dev/null
+++ b/cmd/gtk/gtkutil/settings.go
@@ -0,0 +1,55 @@
+//go:build gtk
+
+package gtkutil
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// themePrefPath returns the file that stores the user's light/dark choice,
+// kept in the per-user config directory so it is shared across networks.
+func themePrefPath() string {
+ dir, err := os.UserConfigDir()
+ if err != nil {
+ return ""
+ }
+
+ return filepath.Join(dir, "pactus", "gui_dark_mode")
+}
+
+// SaveDarkMode persists the user's dark-mode choice.
+func SaveDarkMode(dark bool) {
+ path := themePrefPath()
+ if path == "" {
+ return
+ }
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return
+ }
+
+ value := "0"
+ if dark {
+ value = "1"
+ }
+
+ _ = os.WriteFile(path, []byte(value), 0o644)
+}
+
+// LoadDarkMode returns the persisted dark-mode choice. ok is false when the
+// user has not made a choice yet, in which case the system theme is followed.
+func LoadDarkMode() (dark, ok bool) {
+ path := themePrefPath()
+ if path == "" {
+ return false, false
+ }
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return false, false
+ }
+
+ return strings.TrimSpace(string(data)) == "1", true
+}