Skip to content

Commit 208b2d5

Browse files
authored
Fsck add key to UI (#772)
Adds a reusable FsckPanel which acts as a container for all the UI elements required to show information about an ongoing fsck operations, and adds a simple key to the fsck UI for the progress bars.
1 parent 84fc932 commit 208b2d5

5 files changed

Lines changed: 352 additions & 136 deletions

File tree

cmd/fsck/internal/tui/app.go

Lines changed: 23 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"bufio"
2020
"context"
2121
"flag"
22-
"fmt"
2322
"io"
2423
"time"
2524

@@ -28,13 +27,12 @@ import (
2827
"k8s.io/klog/v2"
2928

3029
tea "github.com/charmbracelet/bubbletea"
31-
"github.com/charmbracelet/lipgloss"
3230
"github.com/charmbracelet/x/ansi"
3331
)
3432

3533
// RunApp runs the TUI app, using the provided fsck instance to fetch updates from to populate the UI.
3634
func RunApp(ctx context.Context, f *fsck.Fsck) error {
37-
m := NewFsckAppModel()
35+
m := newAppModel()
3836
p := tea.NewProgram(m)
3937

4038
// Redirect logging so as to appear above the UI
@@ -58,13 +56,13 @@ func RunApp(ctx context.Context, f *fsck.Fsck) error {
5856
case <-ctx.Done():
5957
// Have the UI update one last time to show where we got to (this helps ensure we see 100%
6058
// on the progress bars if we're exiting because the fsck has completed).
61-
p.Send(UpdateCmd(f.Status())())
59+
p.Send(tui.FsckPanelUpdateCmd(f.Status())())
6260
// Give the UI a bit of time to render...
6361
<-time.After(100 * time.Millisecond)
6462
// And then we're out.
6563
p.Send(tea.Quit())
6664
case <-time.After(100 * time.Millisecond):
67-
p.Send(UpdateCmd(f.Status())())
65+
p.Send(tui.FsckPanelUpdateCmd(f.Status())())
6866
}
6967
}
7068
}()
@@ -75,31 +73,29 @@ func RunApp(ctx context.Context, f *fsck.Fsck) error {
7573
return nil
7674
}
7775

78-
// NewFsckAppModel creates a new BubbleTea model for the TUI.
79-
func NewFsckAppModel() *FsckAppModel {
80-
r := &FsckAppModel{}
76+
// newAppModel creates a new BubbleTea model for the TUI.
77+
func newAppModel() *appModel {
78+
r := &appModel{
79+
fsckPanel: tui.NewFsckPanel(),
80+
}
8181
return r
8282
}
8383

84-
// FsckAppModel represents the UI model for the FSCK TUI.
85-
type FsckAppModel struct {
86-
// entriesBar is the status/progress bar representing progress through the entry bundles.
87-
entriesBar *tui.LayerProgressModel
88-
// titlesBars is the list status/progress bars representing progress through the various levels of tiles in the log.
89-
// The zeroth entry corresponds to the tiles on level zero.
90-
tilesBars []*tui.LayerProgressModel
91-
84+
// appModel represents the UI model for the FSCK TUI.
85+
type appModel struct {
86+
// fsckPanel displays information about the fsck operation.
87+
fsckPanel *tui.FsckPanel
9288
// width is the width of the app window
9389
width int
9490
}
9591

9692
// Init is called by Bubbleteam early on to set up the app.
97-
func (m *FsckAppModel) Init() tea.Cmd {
93+
func (m *appModel) Init() tea.Cmd {
9894
return nil
9995
}
10096

10197
// Update is called by Bubbletea to handle events.
102-
func (m *FsckAppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
98+
func (m *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
10399
switch msg := msg.(type) {
104100
case tea.KeyMsg:
105101
// Handle user input.
@@ -111,82 +107,24 @@ func (m *FsckAppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
111107
return m, nil
112108
case tea.WindowSizeMsg:
113109
m.width = msg.Width
114-
115110
var cmd tea.Cmd
116-
cmds := []tea.Cmd{}
117-
118-
if m.entriesBar != nil {
119-
_, cmd = m.entriesBar.Update(msg)
120-
cmds = append(cmds, cmd)
121-
}
122-
123-
for _, t := range m.tilesBars {
124-
_, cmd = t.Update(msg)
125-
cmds = append(cmds, cmd)
126-
}
127-
return m, tea.Batch(cmds...)
128-
case updateMsg:
111+
_, cmd = m.fsckPanel.Update(msg)
112+
return m, cmd
113+
case tui.FsckPanelUpdateMsg:
129114
// Ignore empty updates
130-
if len(msg.s.TileRanges) == 0 {
115+
if len(msg.Status.TileRanges) == 0 {
131116
return m, nil
132117
}
133118

134-
// Create the range progress bars now that we know how details about the tree.
135-
if len(m.tilesBars) != len(msg.s.TileRanges) {
136-
m.entriesBar = tui.NewLayerProgressBar("Entry bundles", m.width, 0)
137-
138-
bs := make([]*tui.LayerProgressModel, 0, len(msg.s.TileRanges))
139-
for i := range msg.s.TileRanges {
140-
bs = append(bs, tui.NewLayerProgressBar(fmt.Sprintf("Tiles level %02d", i), m.width, i))
141-
}
142-
m.tilesBars = bs
143-
}
144-
145-
// Update all the range progress bars with the latest state.
146-
_, cmd := m.entriesBar.Update(tui.LayerUpdateMsg{Ranges: msg.s.EntryRanges})
147-
cmds := []tea.Cmd{cmd}
148-
for i := range m.tilesBars {
149-
_, cmd = m.tilesBars[i].Update(tui.LayerUpdateMsg{Ranges: msg.s.TileRanges[i]})
150-
cmds = append(cmds, cmd)
151-
}
152-
153-
return m, tea.Batch(cmds...)
119+
var cmd tea.Cmd
120+
_, cmd = m.fsckPanel.Update(msg)
121+
return m, cmd
154122
default:
155123
return m, nil
156124
}
157125
}
158126

159127
// View is called by Bubbletea to render the UI components.
160-
func (m *FsckAppModel) View() string {
161-
// Build the progress bars, we'll use this below.
162-
bars := []string{}
163-
for i := len(m.tilesBars) - 1; i >= 0; i-- {
164-
t := m.tilesBars[i]
165-
bars = append(bars, t.View())
166-
}
167-
if m.entriesBar != nil {
168-
bars = append(bars, m.entriesBar.View())
169-
}
170-
barsView := lipgloss.JoinVertical(lipgloss.Bottom, bars...)
171-
172-
content := lipgloss.NewStyle().
173-
Width(m.width).
174-
Height(lipgloss.Height(barsView)+1).
175-
Align(lipgloss.Center, lipgloss.Top).
176-
Border(lipgloss.NormalBorder(), true, false, false, false).
177-
Render(barsView)
178-
179-
return lipgloss.JoinVertical(lipgloss.Top, content)
180-
}
181-
182-
// updateMsg is used to tell the model about updated status from the fsck library.
183-
type updateMsg struct {
184-
s fsck.Status
185-
}
186-
187-
// UpdateCmd returns a Cmd which Bubbletea can execute in order to retrieve and updateMsg.
188-
func UpdateCmd(status fsck.Status) tea.Cmd {
189-
return func() tea.Msg {
190-
return updateMsg{s: status}
191-
}
128+
func (m *appModel) View() string {
129+
return m.fsckPanel.View()
192130
}

cmd/fsck/tui/fsck_panel.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2025 The Tessera authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package tui
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/transparency-dev/tessera/fsck"
21+
22+
tea "github.com/charmbracelet/bubbletea"
23+
"github.com/charmbracelet/lipgloss"
24+
)
25+
26+
// NewFsckPanel creates a new TUI panel showing information about an ongoing fsck operation.
27+
func NewFsckPanel() *FsckPanel {
28+
r := &FsckPanel{}
29+
return r
30+
}
31+
32+
// FsckPanel represents the UI model for the FSCK TUI.
33+
type FsckPanel struct {
34+
// entriesBar is the status/progress bar representing progress through the entry bundles.
35+
entriesBar *LayerProgressModel
36+
// titlesBars is the list status/progress bars representing progress through the various levels of tiles in the log.
37+
// The zeroth entry corresponds to the tiles on level zero.
38+
tilesBars []*LayerProgressModel
39+
40+
// width is the width of the app window
41+
width int
42+
}
43+
44+
// Init is called by Bubbleteam early on to set up the app.
45+
func (m *FsckPanel) Init() tea.Cmd {
46+
return nil
47+
}
48+
49+
// Update is called by Bubbletea to handle events.
50+
func (m *FsckPanel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
51+
switch msg := msg.(type) {
52+
case tea.WindowSizeMsg:
53+
m.width = msg.Width
54+
55+
var cmd tea.Cmd
56+
cmds := []tea.Cmd{}
57+
58+
if m.entriesBar != nil {
59+
_, cmd = m.entriesBar.Update(msg)
60+
cmds = append(cmds, cmd)
61+
}
62+
63+
for _, t := range m.tilesBars {
64+
_, cmd = t.Update(msg)
65+
cmds = append(cmds, cmd)
66+
}
67+
return m, tea.Batch(cmds...)
68+
case FsckPanelUpdateMsg:
69+
// Ignore empty updates
70+
if len(msg.Status.TileRanges) == 0 {
71+
return m, nil
72+
}
73+
74+
// Create the range progress bars now that we know how details about the tree.
75+
if len(m.tilesBars) != len(msg.Status.TileRanges) {
76+
m.entriesBar = NewLayerProgressBar("Entry bundles", m.width, 0)
77+
78+
bs := make([]*LayerProgressModel, 0, len(msg.Status.TileRanges))
79+
for i := range msg.Status.TileRanges {
80+
bs = append(bs, NewLayerProgressBar(fmt.Sprintf("Tiles level %02d", i), m.width, i))
81+
}
82+
m.tilesBars = bs
83+
}
84+
85+
// Update all the range progress bars with the latest state.
86+
_, cmd := m.entriesBar.Update(LayerUpdateMsg{Ranges: msg.Status.EntryRanges})
87+
cmds := []tea.Cmd{cmd}
88+
for i := range m.tilesBars {
89+
_, cmd = m.tilesBars[i].Update(LayerUpdateMsg{Ranges: msg.Status.TileRanges[i]})
90+
cmds = append(cmds, cmd)
91+
}
92+
93+
return m, tea.Batch(cmds...)
94+
default:
95+
return m, nil
96+
}
97+
}
98+
99+
// View is called by Bubbletea to render the UI components.
100+
func (m *FsckPanel) View() string {
101+
// Build the progress bars, we'll use this below.
102+
bars := []string{}
103+
for i := len(m.tilesBars) - 1; i >= 0; i-- {
104+
t := m.tilesBars[i]
105+
bars = append(bars, t.View())
106+
}
107+
if m.entriesBar != nil {
108+
bars = append(bars, m.entriesBar.View())
109+
}
110+
bars = append(bars, LayerProgressKey())
111+
barsView := lipgloss.JoinVertical(lipgloss.Bottom, bars...)
112+
113+
content := lipgloss.NewStyle().
114+
Width(m.width).
115+
Height(lipgloss.Height(barsView)+1).
116+
Align(lipgloss.Center, lipgloss.Top).
117+
Border(lipgloss.NormalBorder(), true, false, false, false).
118+
Render(barsView)
119+
120+
return lipgloss.JoinVertical(lipgloss.Top, content)
121+
}
122+
123+
// FsckPanelUpdateMsg is used to tell the FsckPanel about updated status from the fsck operation.
124+
type FsckPanelUpdateMsg struct {
125+
Status fsck.Status
126+
}
127+
128+
// FsckPanelUpdateCmd returns a Cmd which Bubbletea can execute in order to retrieve and updateMsg.
129+
func FsckPanelUpdateCmd(status fsck.Status) tea.Cmd {
130+
return func() tea.Msg {
131+
return FsckPanelUpdateMsg{Status: status}
132+
}
133+
}

0 commit comments

Comments
 (0)