diff --git a/app/ui/status/status.go b/app/ui/status/status.go index df964c7..220108c 100644 --- a/app/ui/status/status.go +++ b/app/ui/status/status.go @@ -12,10 +12,13 @@ import ( type Status struct { *focus.Focusable *views.Text - screen commander.ScreenHandler - events chan *tcell.EventKey + mu sync.Mutex + screen commander.ScreenHandler + events chan *tcell.EventKey + + once sync.Once + clearMu sync.Mutex clearIn time.Time - mu sync.Mutex } func (s *Status) watch() { @@ -23,48 +26,41 @@ func (s *Status) watch() { for { t := <-ticker.C s.mu.Lock() - if !s.clearIn.IsZero() && s.clearIn.Before(t) { + clear := !s.clearIn.IsZero() && s.clearIn.Before(t) + s.mu.Unlock() + if clear { s.Clear() } - s.mu.Unlock() } } -func (s *Status) Clear() { - s.clearIn = time.Time{} - s.SetText("") - s.SetStyle(s.screen.Theme().GetStyle("status-bar")) +func (s *Status) setMessage(text string, style tcell.Style, clearIn time.Duration) { + s.mu.Lock() + if clearIn == 0 { + s.clearIn = time.Time{} + } else { + s.clearIn = time.Now().Add(clearIn) + } + s.SetText(text) + s.SetStyle(style) + s.mu.Unlock() s.screen.UpdateScreen() } -func (s *Status) ClearIn(duration time.Duration) { - s.mu.Lock() - s.clearIn = time.Now().Add(duration) - s.mu.Unlock() +func (s *Status) Clear() { + s.setMessage("", s.screen.Theme().GetStyle("status-bar"), 0) } func (s *Status) Warning(msg string) { - s.Clear() - s.SetText(msg) - s.SetStyle(s.screen.Theme().GetStyle("status-warning")) - s.screen.UpdateScreen() - s.ClearIn(time.Second * 5) + s.setMessage(msg, s.screen.Theme().GetStyle("status-warning"), time.Second*5) } func (s *Status) Info(msg string) { - s.Clear() - s.SetText(msg) - s.SetStyle(s.screen.Theme().GetStyle("status-info")) - s.screen.UpdateScreen() - s.ClearIn(time.Second * 2) + s.setMessage(msg, s.screen.Theme().GetStyle("status-info"), time.Second*2) } func (s *Status) Error(err error) { - s.Clear() - s.SetText(err.Error()) - s.SetStyle(s.screen.Theme().GetStyle("status-error")) - s.screen.UpdateScreen() - s.ClearIn(time.Second * 10) + s.setMessage(err.Error(), s.screen.Theme().GetStyle("status-error"), time.Second*10) } func (s *Status) HandleEvent(ev tcell.Event) bool { @@ -79,10 +75,7 @@ func (s *Status) HandleEvent(ev tcell.Event) bool { } func (s *Status) Confirm(msg string) bool { - s.Clear() - s.SetText(msg) - s.SetStyle(s.screen.Theme().GetStyle("status-confirm")) - s.screen.UpdateScreen() + s.setMessage(msg, s.screen.Theme().GetStyle("status-confirm"), 0) ev := <-s.events switch ev.Rune() { case 'y', 'Y': @@ -92,10 +85,14 @@ func (s *Status) Confirm(msg string) bool { } func (s *Status) Draw() { - if s.Style() == tcell.StyleDefault { + s.once.Do(func() { + s.mu.Lock() s.SetStyle(s.screen.Theme().GetStyle("status-bar")) - } + s.mu.Unlock() + }) + s.mu.Lock() s.Text.Draw() + s.mu.Unlock() } func (s *Status) Size() (int, int) { diff --git a/app/ui/widgets/listTable/listTable.go b/app/ui/widgets/listTable/listTable.go index b445190..f928866 100644 --- a/app/ui/widgets/listTable/listTable.go +++ b/app/ui/widgets/listTable/listTable.go @@ -56,6 +56,7 @@ type ListTable struct { views.WidgetWatchers *focus.Focusable + viewMu sync.RWMutex view views.View ageCol int @@ -67,9 +68,11 @@ type ListTable struct { selectedId string format TableFormat // internal representation of table values - table table - // Currently selected row + + tableMu sync.RWMutex + table table selectedRowIndex int + // Row to start rendering from (vertical scrolling) topRow int // Left cell to start rendering from (horizontal scrolling) @@ -80,7 +83,6 @@ type ListTable struct { onInitStart InitFunc onInitFinish InitFunc - preloader *preloader rowProvider commander.RowProvider screen commander.ScreenHandler stopCh chan struct{} @@ -100,7 +102,6 @@ func NewListTable(prov commander.RowProvider, format TableFormat, screen command onChange: DefaultRowFunc, onInitStart: DefaultInit, onInitFinish: DefaultInit, - preloader: NewPreloader(screen), rowProvider: prov, screen: screen, filterMode: format.Has(AlwaysFilter), @@ -211,10 +212,8 @@ func (lt *ListTable) watch() { } lt.rowsMu.Unlock() case *commander.OpInitStart: - lt.preloader.Start() lt.onInitStart() case *commander.OpInitFinished: - lt.preloader.Stop() lt.onInitFinish() } } @@ -255,21 +254,26 @@ func (lt *ListTable) SelectedRowIndex() int { } func (lt *ListTable) SelectedRowId() string { + lt.tableMu.RLock() + defer lt.tableMu.RUnlock() return lt.selectedId } func (lt *ListTable) SelectedRow() commander.Row { - if len(lt.table.rows) == 0 { + lt.tableMu.RLock() + table := lt.table + lt.tableMu.RUnlock() + if len(table.rows) == 0 { return nil } - if lt.selectedRowIndex < len(lt.table.rows) { - return lt.table.rows[lt.selectedRowIndex] + if lt.selectedRowIndex < len(table.rows) { + return table.rows[lt.selectedRowIndex] } return nil } -func (lt *ListTable) rowStyle(row commander.Row) commander.Style { - if row.Id() == lt.SelectedRowId() { +func (lt *ListTable) rowStyle(row commander.Row, selectedId string) commander.Style { + if row.Id() == selectedId { if lt.IsFocused() { return lt.screen.Theme().GetStyle("row-selected-focused") } else { @@ -336,16 +340,22 @@ func (lt *ListTable) BindOnKeyPress(rowKeyEventFunc RowKeyEventFunc) { } func (lt *ListTable) columnSeparatorsWidth() int { + lt.rowsMu.RLock() + defer lt.rowsMu.RUnlock() return (len(lt.columns) - 1) * columnSeparatorLen } func (lt *ListTable) viewWidth() int { + lt.viewMu.RLock() width, _ := lt.view.Size() + lt.viewMu.RUnlock() return width } func (lt *ListTable) tableHeight() int { + lt.viewMu.RLock() _, height := lt.view.Size() + lt.viewMu.RUnlock() if lt.format.Has(WithHeaders) { height -= 1 } @@ -356,7 +366,10 @@ func (lt *ListTable) tableHeight() int { } func (lt *ListTable) MaxSize() (w int, h int) { - w = lt.table.dataWidth + len(lt.table.columnDataWidths) + lt.tableMu.RLock() + table := lt.table + lt.tableMu.RUnlock() + w = table.dataWidth + len(table.columnDataWidths) if lt.filterMode { filterLen := len(lt.filter) + 1 if w < filterLen { @@ -364,7 +377,7 @@ func (lt *ListTable) MaxSize() (w int, h int) { } } - h = lt.table.dataHeight + h = table.dataHeight if lt.format.Has(WithHeaders) { h++ } @@ -438,13 +451,12 @@ func (lt *ListTable) renderTable() table { return t } -func (lt *ListTable) getColumnSizes() []int { - t := lt.table +func (lt *ListTable) getColumnSizes(t table) []int { sizes := make([]int, len(t.columnDataWidths)) copy(sizes, t.columnDataWidths) // If there is some additional horizontal space available - spread it in a rational way - viewWidth, _ := lt.view.Size() + viewWidth := lt.viewWidth() viewWidth -= lt.columnSeparatorsWidth() unusedWidth := viewWidth - t.dataWidth addedWidth := 0 @@ -468,35 +480,43 @@ func (lt *ListTable) getColumnSizes() []int { } func (lt *ListTable) Draw() { + lt.tableMu.RLock() + table := lt.table + lt.tableMu.RUnlock() style := lt.defaultStyle() + sizes := lt.getColumnSizes(table) + tableHeight := lt.tableHeight() + viewWidth := lt.viewWidth() + selectedId := lt.SelectedRowId() + + lt.viewMu.Lock() + defer lt.viewMu.Unlock() lt.view.Fill(' ', style) index := 0 if lt.filterMode || lt.filter != "" { lt.drawFilter(index) index++ } - sizes := lt.getColumnSizes() if lt.format.Has(WithHeaders) { - lt.drawRow(index, lt.table.headers, sizes, lt.screen.Theme().GetStyle("row-header")) + lt.drawRow(index, table.headers, sizes, lt.screen.Theme().GetStyle("row-header")) index++ } rowIndex := 0 - for rowId := lt.topRow; rowId < lt.topRow+lt.tableHeight() && rowId < len(lt.table.rows); rowId++ { - lt.drawRow(index, lt.table.values[rowId], sizes, lt.rowStyle(lt.table.rows[rowId])) + for rowId := lt.topRow; rowId < lt.topRow+tableHeight && rowId < len(table.rows); rowId++ { + lt.drawRow(index, table.values[rowId], sizes, lt.rowStyle(table.rows[rowId], selectedId)) var suffix *rune if rowIndex == 0 && lt.topRow != 0 { suffix = &arrowUp } - if rowIndex == lt.tableHeight()-1 && rowId < len(lt.table.rows)-1 { + if rowIndex == tableHeight-1 && rowId < len(table.rows)-1 { suffix = &arrowDown } if suffix != nil { - lt.view.SetContent(lt.viewWidth()-1, index, *suffix, nil, lt.rowStyle(lt.table.rows[rowId])) + lt.view.SetContent(viewWidth-1, index, *suffix, nil, lt.rowStyle(table.rows[rowId], selectedId)) } index++ rowIndex++ } - lt.preloader.Draw() } func (lt *ListTable) drawFilter(y int) { @@ -545,7 +565,9 @@ func (lt *ListTable) drawRow(y int, row []string, sizes []int, style tcell.Style } func (lt *ListTable) Render() { + lt.tableMu.Lock() lt.table = lt.renderTable() + lt.tableMu.Unlock() } func (lt *ListTable) Resize() { @@ -628,20 +650,27 @@ func (lt *ListTable) HandleEvent(ev tcell.Event) bool { }) } +func (lt *ListTable) selectIndexRelative(offset int) { + lt.tableMu.RLock() + selected := lt.selectedRowIndex + lt.tableMu.RUnlock() + lt.SelectIndex(selected + offset) +} + func (lt *ListTable) Next() { - lt.SelectIndex(lt.selectedRowIndex + 1) + lt.selectIndexRelative(1) } func (lt *ListTable) Prev() { - lt.SelectIndex(lt.selectedRowIndex - 1) + lt.selectIndexRelative(-1) } func (lt *ListTable) NextPage() { - lt.SelectIndex(lt.selectedRowIndex + lt.tableHeight()) + lt.selectIndexRelative(lt.tableHeight()) } func (lt *ListTable) PrevPage() { - lt.SelectIndex(lt.selectedRowIndex - lt.tableHeight()) + lt.selectIndexRelative(-lt.tableHeight()) } func (lt *ListTable) Home() { @@ -649,7 +678,10 @@ func (lt *ListTable) Home() { } func (lt *ListTable) End() { - lt.SelectIndex(len(lt.table.rows) - 1) + lt.tableMu.RLock() + rowsLen := len(lt.table.rows) + lt.tableMu.RUnlock() + lt.SelectIndex(rowsLen - 1) } func (lt *ListTable) Right() { @@ -661,41 +693,51 @@ func (lt *ListTable) Left() { } func (lt *ListTable) SelectIndex(index int) { - if len(lt.table.rows) == 0 { + lt.tableMu.RLock() + table := lt.table + selected := lt.selectedRowIndex + lt.tableMu.RUnlock() + + if len(table.rows) == 0 { return } - if index > len(lt.table.rows)-1 { - index = len(lt.table.rows) - 1 + if index > len(table.rows)-1 { + index = len(table.rows) - 1 } if index < 0 { index = 0 } // Determine direction to skip disabled rows var delta int - if lt.selectedRowIndex <= index { + if selected <= index { delta = 1 } else { delta = -1 } - row := lt.table.rows[index] + row := table.rows[index] for !row.Enabled() { index += delta - if index < 0 || index >= len(lt.table.rows) { + if index < 0 || index >= len(table.rows) { return } - row = lt.table.rows[index] + row = table.rows[index] } + changed := selected != index + lt.tableMu.Lock() lt.selectedId = row.Id() - changed := lt.selectedRowIndex != index lt.selectedRowIndex = index + lt.tableMu.Unlock() if changed { lt.onChange(row) } + lt.viewMu.RLock() if lt.view == nil { + lt.viewMu.RUnlock() return } + lt.viewMu.RUnlock() height := lt.tableHeight() scrollThreshold := lt.topRow + height - 1 @@ -714,7 +756,11 @@ func (lt *ListTable) SelectId(id string) { } func (lt *ListTable) reindexSelection() { - if index, ok := lt.table.rowIndex[lt.selectedId]; ok { + lt.tableMu.RLock() + table := lt.table + selectedId := lt.selectedId + lt.tableMu.RUnlock() + if index, ok := table.rowIndex[selectedId]; ok { lt.SelectIndex(index) } else { lt.SelectIndex(0) @@ -725,7 +771,9 @@ func (lt *ListTable) SetLeft(index int) { if index < 0 { index = 0 } + lt.tableMu.RLock() maxLeft := lt.table.dataWidth + lt.columnSeparatorsWidth() - lt.viewWidth() + lt.tableMu.RUnlock() if maxLeft < 0 { index = 0 } else if index > maxLeft { @@ -735,14 +783,18 @@ func (lt *ListTable) SetLeft(index int) { } func (lt *ListTable) SetView(view views.View) { + lt.viewMu.Lock() lt.view = view - lt.preloader.SetView(view) + lt.viewMu.Unlock() lt.Resize() } // This is the minimum required size of ListTable func (lt *ListTable) Size() (int, int) { - if lt.table.dataWidth == 0 { + lt.tableMu.RLock() + table := lt.table + lt.tableMu.RUnlock() + if table.dataWidth == 0 { return 1, 1 } w, h := lt.MaxSize() diff --git a/app/ui/widgets/listTable/preloader.go b/app/ui/widgets/listTable/preloader.go deleted file mode 100644 index a8a2db1..0000000 --- a/app/ui/widgets/listTable/preloader.go +++ /dev/null @@ -1,84 +0,0 @@ -package listTable - -import ( - "github.com/AnatolyRugalev/kube-commander/app/focus" - "github.com/AnatolyRugalev/kube-commander/commander" - "github.com/gdamore/tcell" - "github.com/gdamore/tcell/views" - "sync" - "time" -) - -var phases = []rune{ - '|', - '\\', - '-', - '/', -} - -type preloader struct { - views.WidgetWatchers - focus.Focusable - sync.Mutex - phase int - ticker *time.Ticker - view views.View - style tcell.Style - screen commander.ScreenHandler - preloader *preloader -} - -func NewPreloader(screen commander.ScreenHandler) *preloader { - return &preloader{ - screen: screen, - phase: -1, - } -} - -func (p *preloader) Start() { - p.phase = 0 - p.Lock() - p.ticker = time.NewTicker(time.Millisecond * 200) - p.Unlock() - go func() { - for range p.ticker.C { - p.phase++ - if p.phase >= len(phases) { - p.phase = 0 - } - p.screen.UpdateScreen() - } - }() -} - -func (p *preloader) Stop() { - p.Lock() - if p.ticker != nil { - p.ticker.Stop() - } - p.Unlock() - p.phase = -1 - p.screen.UpdateScreen() -} - -func (p *preloader) Draw() { - if p.phase == -1 { - return - } - p.view.SetContent(0, 0, phases[p.phase], nil, p.screen.Theme().GetStyle("loader")) -} - -func (p *preloader) Resize() { -} - -func (p *preloader) HandleEvent(_ tcell.Event) bool { - return false -} - -func (p *preloader) SetView(view views.View) { - p.view = view -} - -func (p *preloader) Size() (int, int) { - return 1, 1 -} diff --git a/go.mod b/go.mod index b322193..01b2d38 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/atotto/clipboard v0.1.2 github.com/creack/pty v1.1.11 github.com/fsnotify/fsnotify v1.4.7 - github.com/gdamore/tcell v1.3.1-0.20200315173632-8ec73b6fa6c5 + github.com/gdamore/tcell v1.4.0 github.com/golang/protobuf v1.4.1 github.com/googleapis/gnostic v0.2.0 // indirect github.com/imdario/mergo v0.3.7 // indirect diff --git a/go.sum b/go.sum index 9ef02b5..ca1f358 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell v1.3.1-0.20200315173632-8ec73b6fa6c5 h1:VUIItIAoV7OjWSaBWdOAVetSSfas7GiFvO3Mq8lS6so= github.com/gdamore/tcell v1.3.1-0.20200315173632-8ec73b6fa6c5/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= +github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= +github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=