package tui import ( "context" "errors" "fmt" "math" "sync" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) type updateType int const ( other updateType = iota moveToPrevPage moveToNextPage moveUp moveDown moveHome moveEnd ) type RecordsView struct { *tview.Box mu sync.RWMutex onUnmount func() bucket *Bucket records []*Record buffer chan *Record firstRecordIndex int lastRecordIndex int selectedRecordIndex int updateType updateType ui *UI filter *Filter } func NewRecordsView(ui *UI, bucket *Bucket, filter *Filter) *RecordsView { return &RecordsView{ Box: tview.NewBox(), bucket: bucket, ui: ui, filter: filter, } } func (v *RecordsView) Mount(ctx context.Context) error { if v.onUnmount != nil { return errors.New("try to mount already mounted component") } ctx, v.onUnmount = context.WithCancel(ctx) tempBuffer, err := LoadRecords(ctx, v.ui.db, v.bucket.Path, v.ui.loadBufferSize) if err != nil { return err } v.buffer = make(chan *Record, v.ui.loadBufferSize) go func() { defer close(v.buffer) for item := range tempBuffer { if item.err != nil { v.ui.stopOnError(err) break } record := item.val record.Entry, _, err = v.bucket.NextParser(record.Key, record.Value) if err != nil { v.ui.stopOnError(err) break } if v.filter.Apply(record.Entry).Result() != common.Yes { continue } v.buffer <- record } }() return nil } func (v *RecordsView) Unmount() { if v.onUnmount == nil { panic("try to unmount not mounted component") } v.onUnmount() v.onUnmount = nil } func (v *RecordsView) Update(ctx context.Context) error { _, _, _, recordsPerPage := v.GetInnerRect() firstRecordIndex, lastRecordIndex, selectedRecordIndex := v.getNewIndexes() loop: for len(v.records) < lastRecordIndex { select { case <-ctx.Done(): return nil case record, ok := <-v.buffer: if !ok { break loop } v.records = append(v.records, record) } } // Set the update type to its default value after some specific key event // has been handled. v.updateType = other firstRecordIndex = max(0, min(firstRecordIndex, len(v.records)-recordsPerPage)) lastRecordIndex = min(firstRecordIndex+recordsPerPage, len(v.records)) selectedRecordIndex = min(selectedRecordIndex, lastRecordIndex-1) v.mu.Lock() v.firstRecordIndex = firstRecordIndex v.lastRecordIndex = lastRecordIndex v.selectedRecordIndex = selectedRecordIndex v.mu.Unlock() return nil } func (v *RecordsView) getNewIndexes() (int, int, int) { v.mu.RLock() firstRecordIndex := v.firstRecordIndex lastRecordIndex := v.lastRecordIndex selectedRecordIndex := v.selectedRecordIndex v.mu.RUnlock() _, _, _, recordsPerPage := v.GetInnerRect() switch v.updateType { case moveUp: if selectedRecordIndex != firstRecordIndex { selectedRecordIndex-- break } firstRecordIndex = max(0, firstRecordIndex-1) lastRecordIndex = min(firstRecordIndex+recordsPerPage, len(v.records)) selectedRecordIndex = firstRecordIndex case moveToPrevPage: if selectedRecordIndex != firstRecordIndex { selectedRecordIndex = firstRecordIndex break } firstRecordIndex = max(0, firstRecordIndex-recordsPerPage) lastRecordIndex = firstRecordIndex + recordsPerPage selectedRecordIndex = firstRecordIndex case moveDown: if selectedRecordIndex != lastRecordIndex-1 { selectedRecordIndex++ break } firstRecordIndex++ lastRecordIndex++ selectedRecordIndex++ case moveToNextPage: if selectedRecordIndex != lastRecordIndex-1 { selectedRecordIndex = lastRecordIndex - 1 break } firstRecordIndex += recordsPerPage lastRecordIndex = firstRecordIndex + recordsPerPage selectedRecordIndex = lastRecordIndex - 1 case moveHome: firstRecordIndex = 0 lastRecordIndex = firstRecordIndex + recordsPerPage selectedRecordIndex = 0 case moveEnd: lastRecordIndex = math.MaxInt32 firstRecordIndex = lastRecordIndex - recordsPerPage selectedRecordIndex = lastRecordIndex - 1 default: lastRecordIndex = firstRecordIndex + recordsPerPage } return firstRecordIndex, lastRecordIndex, selectedRecordIndex } func (v *RecordsView) GetInnerRect() (int, int, int, int) { x, y, width, height := v.Box.GetInnerRect() // Left padding. x = min(x+3, x+width-1) width = max(width-3, 0) return x, y, width, height } func (v *RecordsView) Draw(screen tcell.Screen) { v.mu.RLock() firstRecordIndex := v.firstRecordIndex lastRecordIndex := v.lastRecordIndex selectedRecordIndex := v.selectedRecordIndex records := v.records v.mu.RUnlock() v.DrawForSubclass(screen, v) x, y, width, height := v.GetInnerRect() if height == 0 { return } // No records in that bucket. if firstRecordIndex == lastRecordIndex { tview.Print( screen, "Empty Bucket", x, y, width, tview.AlignCenter, tview.Styles.PrimaryTextColor, ) return } for index := firstRecordIndex; index < lastRecordIndex; index++ { result := records[index].Entry text := result.String() if index == selectedRecordIndex { text = fmt.Sprintf("[:white]%s[:-]", text) tview.Print(screen, text, x, y, width, tview.AlignLeft, tview.Styles.PrimitiveBackgroundColor) } else { tview.Print(screen, text, x, y, width, tview.AlignLeft, tview.Styles.PrimaryTextColor) } y++ } } func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.Primitive)) { return v.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) { switch m, k := event.Modifiers(), event.Key(); { case m == 0 && k == tcell.KeyPgUp: v.updateType = moveToPrevPage case m == 0 && k == tcell.KeyPgDn: v.updateType = moveToNextPage case m == 0 && k == tcell.KeyUp: v.updateType = moveUp case m == 0 && k == tcell.KeyDown: v.updateType = moveDown case m == 0 && k == tcell.KeyHome: v.updateType = moveHome case m == 0 && k == tcell.KeyEnd: v.updateType = moveEnd case k == tcell.KeyEnter: v.mu.RLock() selectedRecordIndex := v.selectedRecordIndex records := v.records v.mu.RUnlock() if len(records) != 0 { current := records[selectedRecordIndex] v.ui.moveNextPage(NewDetailedView(current.Entry.DetailedString())) } } }) }