@@ -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 ])
0 commit comments