Skip to content

Commit 9aee406

Browse files
authored
Merge pull request #2 from jr-k/fix/stability
Fix stability
2 parents 45b1794 + d6c3ebb commit 9aee406

16 files changed

Lines changed: 359 additions & 93 deletions

File tree

internal/dao/swarm/service/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (s Service) GetStatusColor() (tcell.Color, tcell.Color) {
5656
} else if running > desired {
5757
return tcell.ColorMediumPurple, styles.ColorBlack
5858
} else if desired > 0 {
59-
return styles.ColorStatusGreen, styles.ColorBlack
59+
return styles.ColorIdle, styles.ColorBlack
6060
}
6161
}
6262
}

internal/ui/app.go

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ui
22

33
import (
44
"fmt"
5+
"sync"
56
"time"
67

78
"runtime/debug"
@@ -27,6 +28,7 @@ import (
2728

2829
type App struct {
2930
TviewApp *tview.Application
31+
Screen tcell.Screen
3032
Docker *dao.DockerClient
3133

3234
// Components
@@ -44,6 +46,11 @@ type App struct {
4446
ActiveFilter string
4547
ActiveScope *common.Scope
4648
ActiveInspector common.Inspector
49+
50+
// Concurrency
51+
pauseMx sync.RWMutex
52+
paused bool
53+
stopTicker chan struct{}
4754
}
4855

4956
// Ensure App implements AppController interface
@@ -76,8 +83,19 @@ func NewApp() *App {
7683
panic(err)
7784
}
7885

86+
screen, err := tcell.NewScreen()
87+
if err != nil {
88+
panic(err)
89+
}
90+
// We don't Init() here, tview does it on Run() if we set it?
91+
// Actually tview.Application.Run() calls screen.Init() if not initialized.
92+
93+
tviewApp := tview.NewApplication()
94+
tviewApp.SetScreen(screen)
95+
7996
app := &App{
80-
TviewApp: tview.NewApplication(),
97+
TviewApp: tviewApp,
98+
Screen: screen,
8199
Docker: docker,
82100
Views: make(map[string]*view.ResourceView),
83101
Pages: tview.NewPages(),
@@ -95,21 +113,48 @@ func (a *App) Run() error {
95113
}
96114
}()
97115

116+
// Start auto-refresh
117+
a.StartAutoRefresh()
118+
119+
return a.TviewApp.SetRoot(a.Layout, true).Run()
120+
}
121+
122+
func (a *App) StartAutoRefresh() {
123+
if a.stopTicker != nil {
124+
return
125+
}
126+
a.stopTicker = make(chan struct{})
127+
98128
go func() {
99-
// Initial Delay for UI setup
100-
time.Sleep(100 * time.Millisecond)
129+
// Initial update
130+
// We use a small delay on first run to let UI settle if needed, but only for the first ever run
131+
// subsequent restarts of auto-refresh might want immediate effect or wait for next tick.
132+
// Let's rely on tick.
133+
134+
ticker := time.NewTicker(2 * time.Second)
135+
defer ticker.Stop()
136+
137+
// Immediate update on start
101138
a.RefreshCurrentView()
102139
a.updateHeader()
103140

104-
ticker := time.NewTicker(2 * time.Second)
105-
defer ticker.Stop()
106-
for range ticker.C {
107-
a.RefreshCurrentView()
108-
a.updateHeader()
141+
for {
142+
select {
143+
case <-ticker.C:
144+
a.RefreshCurrentView()
145+
a.updateHeader()
146+
case <-a.stopTicker:
147+
return
148+
}
109149
}
110150
}()
151+
}
111152

112-
return a.TviewApp.SetRoot(a.Layout, true).Run()
153+
func (a *App) StopAutoRefresh() {
154+
if a.stopTicker != nil {
155+
close(a.stopTicker)
156+
a.stopTicker = nil
157+
}
113158
}
114159

115160
func (a *App) initUI() {
@@ -308,6 +353,10 @@ func (a *App) GetTviewApp() *tview.Application {
308353
return a.TviewApp
309354
}
310355

356+
func (a *App) GetScreen() tcell.Screen {
357+
return a.Screen
358+
}
359+
311360
func (a *App) GetDocker() *dao.DockerClient {
312361
return a.Docker
313362
}
@@ -419,3 +468,60 @@ func (a *App) CloseInspector() {
419468
a.RestoreFocus()
420469
a.UpdateShortcuts()
421470
}
471+
472+
func (a *App) ActionPause() {
473+
a.SetPaused(true)
474+
}
475+
476+
func (a *App) ActionResume() {
477+
a.SetPaused(false)
478+
a.TviewApp.Draw() // Force redraw
479+
}
480+
481+
func (a *App) SetPaused(paused bool) {
482+
a.pauseMx.Lock()
483+
defer a.pauseMx.Unlock()
484+
a.paused = paused
485+
}
486+
487+
func (a *App) IsPaused() bool {
488+
a.pauseMx.RLock()
489+
defer a.pauseMx.RUnlock()
490+
return a.paused
491+
}
492+
493+
func (a *App) SafeQueueUpdateDraw(f func()) {
494+
a.pauseMx.RLock()
495+
isPaused := a.paused
496+
a.pauseMx.RUnlock()
497+
498+
if isPaused {
499+
return
500+
}
501+
502+
a.TviewApp.QueueUpdateDraw(func() {
503+
a.pauseMx.RLock()
504+
isPausedNow := a.paused
505+
a.pauseMx.RUnlock()
506+
507+
if isPausedNow {
508+
return
509+
}
510+
f()
511+
})
512+
}
513+
514+
func (a *App) RunInBackground(task func()) {
515+
go func() {
516+
defer func() {
517+
if r := recover(); r != nil {
518+
a.TviewApp.QueueUpdateDraw(func() {
519+
a.Flash.SetText(fmt.Sprintf("[red]Background task panic: %v", r))
520+
// Also print to stdout for debugging if app is still running or logs are captured
521+
fmt.Printf("Background task panic: %v\nStack trace:\n%s\n", r, string(debug.Stack()))
522+
})
523+
}
524+
}()
525+
task()
526+
}()
527+
}

internal/ui/app_actions.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (a *App) PerformAction(action func(id string) error, actionName string, col
3232

3333
a.Flash.SetText(fmt.Sprintf("[yellow]%s %d items...", actionName, len(ids)))
3434

35-
go func() {
35+
a.RunInBackground(func() {
3636
var errs []string
3737
for _, id := range ids {
3838
if err := action(id); err != nil {
@@ -63,7 +63,7 @@ func (a *App) PerformAction(action func(id string) error, actionName string, col
6363
}
6464
a.UpdateShortcuts()
6565
})
66-
}()
66+
})
6767
}
6868

6969
// Helper to get target IDs (Multi or Single)
@@ -156,7 +156,7 @@ func (a *App) PerformPrune() {
156156

157157
dialogs.ShowConfirmation(a, "PRUNE", name, func(force bool) {
158158
a.Flash.SetText(fmt.Sprintf("[yellow]Pruning %s...", name))
159-
go func() {
159+
a.RunInBackground(func() {
160160
err := action(a)
161161
a.TviewApp.QueueUpdateDraw(func() {
162162
if err != nil {
@@ -166,7 +166,7 @@ func (a *App) PerformPrune() {
166166
a.RefreshCurrentView()
167167
}
168168
})
169-
}()
169+
})
170170
})
171171
a.UpdateShortcuts()
172172
}
@@ -237,8 +237,8 @@ func (a *App) PerformCopy() {
237237
a.AppendFlash(fmt.Sprintf("[red]Copy error: %v", err))
238238
} else {
239239
preview := value
240-
if len(preview) > 20 {
241-
preview = preview[:20] + "..."
240+
if len(preview) > 60 {
241+
preview = preview[:60] + "..."
242242
}
243243
a.AppendFlash(fmt.Sprintf("[black:#50fa7b] <copied: %s>[-]", preview))
244244
}

internal/ui/app_view.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,15 @@ func (a *App) RefreshCurrentView() {
3333
// 1. Immediate Updates (Optimistic UI) - Inside Queue not needed if Ticker calls this?
3434
// Ticker runs in BG. `UpdateShortcuts` accesses UI. Should be queued.
3535
// But let's revert to "working but racy" to fix the "broken views" regression first.
36+
// a.UpdateShortcuts() -> Moved inside SafeQueueUpdateDraw if needed, but keeping as is for now unless asked.
37+
// NOTE: UpdateShortcuts() calls existing UI methods which might not be thread safe from ticker.
3638
a.UpdateShortcuts()
3739

38-
go func() {
40+
a.RunInBackground(func() {
41+
if a.IsPaused() {
42+
return
43+
}
44+
3945
var err error
4046
var data []dao.Resource
4147
var headers []string
@@ -45,7 +51,12 @@ func (a *App) RefreshCurrentView() {
4551
data, err = v.FetchFunc(a)
4652
}
4753

48-
a.TviewApp.QueueUpdateDraw(func() {
54+
// Check pause again after fetching (fetching can take time)
55+
if a.IsPaused() {
56+
return
57+
}
58+
59+
a.SafeQueueUpdateDraw(func() {
4960
// Check if page changed while fetching?
5061
currentPage, _ := a.Pages.GetFrontPage()
5162
if currentPage != page {
@@ -95,7 +106,7 @@ func (a *App) RefreshCurrentView() {
95106
a.Flash.SetText(status)
96107
}
97108
})
98-
}()
109+
})
99110
}
100111

101112
func (a *App) formatViewTitle(viewName string, countStr string, filter string) string {

internal/ui/common/common.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type AppController interface {
2626
// Accessors
2727
GetPages() *tview.Pages
2828
GetTviewApp() *tview.Application
29+
GetScreen() tcell.Screen
2930
GetDocker() *dao.DockerClient
3031

3132
// Actions
@@ -53,6 +54,14 @@ type AppController interface {
5354
// Inspector Management
5455
OpenInspector(inspector Inspector)
5556
CloseInspector()
57+
58+
// Async Task Management
59+
RunInBackground(task func())
60+
SetPaused(paused bool)
61+
62+
// Refactoring: Auto Refresh Control
63+
StartAutoRefresh()
64+
StopAutoRefresh()
5665
}
5766

5867
func FormatSCHeader(key, action string) string {

internal/ui/components/footer/footer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ func (f *FlashComponent) Append(text string) {
3232
// Assuming the tag starts with " [black:#50fa7b:b] <copied:" and ends with "[-] "
3333

3434
// Strategy: Split by the start of our known copy tag style
35-
// Tag format from app_actions: [black:#50fa7b:b] <copied:
35+
// Tag format from app_actions: [black:#50fa7b] <copied:
3636

37-
const tagStart = " [black:#50fa7b:b] <copied:"
37+
const tagStart = " [black:#50fa7b] <copied:"
3838

3939
if idx := strings.Index(current, tagStart); idx != -1 {
4040
current = current[:idx]

internal/ui/components/header/header.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ func (h *HeaderComponent) Update(stats dao.HostStats, shortcuts []string) {
9494
fmt.Sprintf("[orange]Engine: [white]%s [dim](%s)", stats.Name, stats.Version),
9595
fmt.Sprintf("[orange]CPU: [white]%s", cpuDisplay),
9696
fmt.Sprintf("[orange]Mem: [white]%s", memDisplay),
97-
"",
9897
}
9998

10099
statsWidth := 0
@@ -126,9 +125,9 @@ func (h *HeaderComponent) Update(stats dao.HostStats, shortcuts []string) {
126125
logoWidth += 2 // Padding
127126

128127
// 3. Shortcuts View
129-
// Max 7 per column (matches header height)
128+
// Max 6 per column (matches header height)
130129
// Each shortcut uses 2 columns: alias (fixed width) and label
131-
const maxPerCol = 7
130+
const maxPerCol = 6
132131
const groupSpacer = " " // Spacer between shortcut groups
133132

134133
// Organize shortcuts into columns, respecting color changes

internal/ui/components/inspect/log_inspector.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ func NewLogInspector(id, subject, resourceType string) *LogInspector {
4949
AutoScroll: true,
5050
Wrap: false,
5151
Timestamps: false,
52-
since: "5m",
53-
tail: "all",
54-
sinceLabel: "5m",
52+
since: "",
53+
tail: "200",
54+
sinceLabel: "Tail",
5555
}
5656
}
5757

0 commit comments

Comments
 (0)