Skip to content

Commit 46b9b6d

Browse files
author
JkLondon
committed
commit
1 parent 0db420a commit 46b9b6d

File tree

7 files changed

+755
-3
lines changed

7 files changed

+755
-3
lines changed

cmd/etui/app/app.go

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type App struct {
2929
logTailer *datasource.LogTailer
3030
nodeMgr *datasource.NodeManager
3131
log *datasource.TUILog
32+
tuiCfg config.TUIConfig
3233
datadir string
3334
}
3435

@@ -37,18 +38,33 @@ func New(datadir string) *App {
3738
logPath := filepath.Join(datadir, "logs", "erigon.log")
3839
tuiLog := datasource.NewTUILog(datadir)
3940
tuiLog.Info("etui starting, datadir=%s", datadir)
41+
42+
// Load persisted TUI configuration (or defaults on first run).
43+
tuiCfg, err := config.Load(datadir)
44+
if err != nil {
45+
tuiLog.Warn("loading config: %v (using defaults)", err)
46+
tuiCfg = config.Defaults()
47+
tuiCfg.DataDir = datadir
48+
}
49+
50+
diagURL := tuiCfg.DiagnosticsURL
51+
if diagURL == "" {
52+
diagURL = config.DefaultDownloaderURL
53+
}
54+
4055
return &App{
4156
datadir: datadir,
4257
tview: tview.NewApplication(),
43-
dp: datasource.NewDownloaderPinger(config.DefaultDownloaderURL),
58+
dp: datasource.NewDownloaderPinger(diagURL),
4459
dlTracker: datasource.NewDownloaderTracker(),
4560
sysColl: datasource.NewSystemCollector(datadir),
4661
iopsTrack: datasource.NewDiskIOPSTracker(),
4762
syncTracker: datasource.NewSyncTracker(),
4863
alertMgr: datasource.NewAlertManager(),
4964
logTailer: datasource.NewLogTailer(logPath),
50-
nodeMgr: datasource.NewNodeManager(datadir, ""),
65+
nodeMgr: datasource.NewNodeManager(datadir, tuiCfg.Chain),
5166
log: tuiLog,
67+
tuiCfg: tuiCfg,
5268
}
5369
}
5470

@@ -57,6 +73,8 @@ const (
5773
pageStart = "start"
5874
pageNodeInfo = "nodeInfo"
5975
pageLogs = "logs"
76+
pageConfig = "config"
77+
pageWizard = "wizard"
6078
)
6179

6280
// Run starts the TUI event loop. It blocks until the user quits or the parent
@@ -135,10 +153,97 @@ func (a *App) Run(parent context.Context, infoCh <-chan *commands.StagesInfo, er
135153
pages.AddPage(pageNodeInfo, nodeInfoPage, true, false)
136154
pages.AddPage(pageLogs, logsPage, true, false)
137155

156+
// Track whether a modal overlay (config/wizard) is showing.
157+
// When true, global keybindings are suppressed to avoid conflicts with form input.
158+
modalActive := false
159+
160+
// openConfigModal opens the config editor as an overlay page.
161+
openConfigModal := func() {
162+
if modalActive {
163+
return
164+
}
165+
modalActive = true
166+
a.log.Info("opening config modal")
167+
configModal := widgets.NewConfigureModal(a.tuiCfg,
168+
func(newCfg config.TUIConfig) {
169+
// Save callback.
170+
if err := newCfg.Validate(); err != nil {
171+
a.log.Error("config validation: %v", err)
172+
// Stay in the modal — don't close on error.
173+
return
174+
}
175+
if err := newCfg.Save(); err != nil {
176+
a.log.Error("config save: %v", err)
177+
} else {
178+
a.log.Info("config saved to %s", config.ConfigPath(newCfg.DataDir))
179+
a.tuiCfg = newCfg
180+
}
181+
pages.RemovePage(pageConfig)
182+
pages.SwitchToPage(dashPages[currentPage])
183+
a.tview.SetFocus(pages)
184+
modalActive = false
185+
},
186+
func() {
187+
// Cancel callback.
188+
pages.RemovePage(pageConfig)
189+
pages.SwitchToPage(dashPages[currentPage])
190+
a.tview.SetFocus(pages)
191+
modalActive = false
192+
},
193+
)
194+
pages.AddPage(pageConfig, configModal.Root, true, true)
195+
a.tview.SetFocus(configModal.Form())
196+
}
197+
198+
// Check for first-run: if no etui.toml exists, show the install wizard.
199+
if _, err := os.Stat(config.ConfigPath(a.datadir)); os.IsNotExist(err) {
200+
modalActive = true
201+
a.log.Info("first run detected — launching install wizard")
202+
wizard := widgets.NewInstallWizard(a.datadir,
203+
func(newCfg config.TUIConfig) {
204+
// Wizard complete.
205+
if err := newCfg.Save(); err != nil {
206+
a.log.Error("wizard config save: %v", err)
207+
} else {
208+
a.log.Info("wizard config saved to %s", config.ConfigPath(newCfg.DataDir))
209+
a.tuiCfg = newCfg
210+
}
211+
pages.RemovePage(pageWizard)
212+
pages.SwitchToPage(pageStart)
213+
a.tview.SetFocus(pages)
214+
modalActive = false
215+
},
216+
func() {
217+
// Wizard cancelled — proceed with defaults.
218+
a.log.Info("wizard cancelled, using defaults")
219+
pages.RemovePage(pageWizard)
220+
pages.SwitchToPage(pageStart)
221+
a.tview.SetFocus(pages)
222+
modalActive = false
223+
},
224+
)
225+
pages.AddPage(pageWizard, wizard.Root, true, true)
226+
// SetFocus for the wizard must happen after SetRoot, handled below.
227+
defer func() {
228+
a.tview.SetFocus(wizard.Focusable())
229+
}()
230+
}
231+
138232
if err := a.tview.SetRoot(pages, true).EnableMouse(true).SetInputCapture(
139233
func(event *tcell.EventKey) *tcell.EventKey {
140234
currentFront, _ := pages.GetFrontPage()
141235

236+
// --- Modal overlay active: only allow Ctrl+C/q to quit ---
237+
// Config and wizard pages handle their own Escape/Enter/Tab internally.
238+
if modalActive {
239+
if event.Key() == tcell.KeyCtrlC {
240+
cancel()
241+
a.tview.Stop()
242+
return nil
243+
}
244+
return event // let the modal form handle all other input
245+
}
246+
142247
// --- Log viewer search mode: capture all input ---
143248
if currentFront == pageLogs && logViewer.IsSearching() {
144249
if event.Key() == tcell.KeyEscape {
@@ -174,6 +279,11 @@ func (a *App) Run(parent context.Context, infoCh <-chan *commands.StagesInfo, er
174279
navigateToPage(logsPageIdx)
175280
return nil
176281

282+
// Open configuration modal.
283+
case event.Rune() == 'C':
284+
openConfigModal()
285+
return nil
286+
177287
// Node toggle (works from any page).
178288
case event.Rune() == 'R':
179289
a.handleNodeToggle(ctx, pages, a.nodeMgr, nodeView.NodeControl, dashPages[currentPage])

cmd/etui/config/tuiconfig.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strconv"
8+
9+
"github.com/pelletier/go-toml/v2"
10+
)
11+
12+
// TUIConfig holds all user-configurable settings for the Erigon TUI.
13+
// Persisted as TOML in <datadir>/etui.toml.
14+
type TUIConfig struct {
15+
DataDir string `toml:"datadir"`
16+
Chain string `toml:"chain"`
17+
PruneMode string `toml:"prune_mode"` // "full", "archive", "minimal"
18+
RPCEnabled bool `toml:"rpc_enabled"` // whether to pass --http flag
19+
RPCPort int `toml:"rpc_port"` // --http.port value
20+
PrivateAPIAddr string `toml:"private_api_addr"` // --private.api.addr value
21+
DiagnosticsURL string `toml:"diagnostics_url"` // URL for diagnostics/downloader
22+
}
23+
24+
// Defaults returns a TUIConfig with sane default values.
25+
func Defaults() TUIConfig {
26+
return TUIConfig{
27+
Chain: "mainnet",
28+
PruneMode: "full",
29+
RPCEnabled: false,
30+
RPCPort: 8545,
31+
PrivateAPIAddr: "127.0.0.1:9090",
32+
DiagnosticsURL: DefaultDownloaderURL,
33+
}
34+
}
35+
36+
// ConfigPath returns the path to etui.toml inside the given datadir.
37+
func ConfigPath(datadir string) string {
38+
return filepath.Join(datadir, "etui.toml")
39+
}
40+
41+
// Load reads etui.toml from the datadir. If the file doesn't exist,
42+
// it returns Defaults() with DataDir set to the given path.
43+
func Load(datadir string) (TUIConfig, error) {
44+
cfg := Defaults()
45+
cfg.DataDir = datadir
46+
47+
path := ConfigPath(datadir)
48+
data, err := os.ReadFile(path)
49+
if err != nil {
50+
if os.IsNotExist(err) {
51+
return cfg, nil // first run — use defaults
52+
}
53+
return cfg, fmt.Errorf("reading %s: %w", path, err)
54+
}
55+
56+
if err := toml.Unmarshal(data, &cfg); err != nil {
57+
return cfg, fmt.Errorf("parsing %s: %w", path, err)
58+
}
59+
// Always override DataDir with the actual directory we loaded from.
60+
cfg.DataDir = datadir
61+
return cfg, nil
62+
}
63+
64+
// Save writes the config to etui.toml in the configured datadir.
65+
func (c *TUIConfig) Save() error {
66+
if c.DataDir == "" {
67+
return fmt.Errorf("datadir is empty")
68+
}
69+
if err := os.MkdirAll(c.DataDir, 0755); err != nil {
70+
return fmt.Errorf("creating datadir: %w", err)
71+
}
72+
73+
data, err := toml.Marshal(c)
74+
if err != nil {
75+
return fmt.Errorf("marshalling config: %w", err)
76+
}
77+
78+
path := ConfigPath(c.DataDir)
79+
if err := os.WriteFile(path, data, 0644); err != nil {
80+
return fmt.Errorf("writing %s: %w", path, err)
81+
}
82+
return nil
83+
}
84+
85+
// Validate checks that all config values are within acceptable ranges.
86+
func (c *TUIConfig) Validate() error {
87+
switch c.PruneMode {
88+
case "full", "archive", "minimal":
89+
// ok
90+
default:
91+
return fmt.Errorf("invalid prune_mode %q (must be full, archive, or minimal)", c.PruneMode)
92+
}
93+
if c.RPCPort < 1 || c.RPCPort > 65535 {
94+
return fmt.Errorf("rpc_port %d out of range (1-65535)", c.RPCPort)
95+
}
96+
if c.Chain == "" {
97+
return fmt.Errorf("chain cannot be empty")
98+
}
99+
return nil
100+
}
101+
102+
// ValidChains returns the list of supported network chains.
103+
func ValidChains() []string {
104+
return []string{"mainnet", "hoodi", "sepolia"}
105+
}
106+
107+
// ValidPruneModes returns the list of supported prune modes.
108+
func ValidPruneModes() []string {
109+
return []string{"full", "archive", "minimal"}
110+
}
111+
112+
// PruneModeDescription returns a short description of each prune mode.
113+
func PruneModeDescription(mode string) string {
114+
switch mode {
115+
case "full":
116+
return "Keeps recent state, prunes old history (~2TB)"
117+
case "archive":
118+
return "Keeps all history for full archive queries (~3TB+)"
119+
case "minimal":
120+
return "Aggressive pruning for minimum storage (~800GB)"
121+
default:
122+
return ""
123+
}
124+
}
125+
126+
// FormatRPCPort returns the RPC port as a string.
127+
func FormatRPCPort(port int) string {
128+
return strconv.Itoa(port)
129+
}

cmd/etui/widgets/common.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ func Header() tview.Primitive {
1919
func Footer() *tview.TextView {
2020
return tview.NewTextView().SetDynamicColors(true).
2121
SetTextAlign(tview.AlignCenter).
22-
SetText("[green]◄ ► [-]navigate [green]L [-]logs [green]R [-]node [yellow]Press [red]q [yellow]or [red]Ctrl+C [yellow]to quit")
22+
SetText("[green]◄ ► [-]navigate [green]L [-]logs [green]R [-]node [green]C [-]config [yellow]Press [red]q [yellow]or [red]Ctrl+C [yellow]to quit")
2323
}

0 commit comments

Comments
 (0)