Skip to content

Commit c65e058

Browse files
committed
Address review: elapsed time as prefix, initialized at construction time
- Show elapsed time as prefix (e.g., "00:15 ⣾ Starting SSH server...") so it doesn't jump around as the message changes - Replace TrackElapsedTime() method with WithElapsedTime() construction option - Remove elapsedTimeMsg in favor of setting startTime directly in the model Co-authored-by: Isaac
1 parent 8dd8208 commit c65e058

File tree

3 files changed

+37
-36
lines changed

3 files changed

+37
-36
lines changed

experimental/ssh/internal/client/client.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,7 @@ func Run(ctx context.Context, client *databricks.WorkspaceClient, opts ClientOpt
279279

280280
if opts.ServerMetadata == "" {
281281
cmdio.LogString(ctx, "Uploading binaries...")
282-
sp := cmdio.NewSpinner(ctx)
283-
sp.TrackElapsedTime()
282+
sp := cmdio.NewSpinner(ctx, cmdio.WithElapsedTime())
284283
sp.Update("Uploading binaries...")
285284
err := UploadTunnelReleases(ctx, client, version, opts.ReleasesDir)
286285
sp.Close()
@@ -582,8 +581,7 @@ func runSSHProxy(ctx context.Context, client *databricks.WorkspaceClient, server
582581
}
583582

584583
func checkClusterState(ctx context.Context, client *databricks.WorkspaceClient, clusterID string, autoStart bool) error {
585-
sp := cmdio.NewSpinner(ctx)
586-
sp.TrackElapsedTime()
584+
sp := cmdio.NewSpinner(ctx, cmdio.WithElapsedTime())
587585
defer sp.Close()
588586
if autoStart {
589587
sp.Update("Ensuring the cluster is running...")
@@ -607,8 +605,7 @@ func checkClusterState(ctx context.Context, client *databricks.WorkspaceClient,
607605
// waitForJobToStart polls the task status until the SSH server task is in RUNNING state or terminates.
608606
// Returns an error if the task fails to start or if polling times out.
609607
func waitForJobToStart(ctx context.Context, client *databricks.WorkspaceClient, runID int64, taskStartupTimeout time.Duration) error {
610-
sp := cmdio.NewSpinner(ctx)
611-
sp.TrackElapsedTime()
608+
sp := cmdio.NewSpinner(ctx, cmdio.WithElapsedTime())
612609
defer sp.Close()
613610
sp.Update("Starting SSH server...")
614611
var prevState jobs.RunLifecycleStateV2State
@@ -677,9 +674,8 @@ func ensureSSHServerIsRunning(ctx context.Context, client *databricks.WorkspaceC
677674
return "", 0, "", fmt.Errorf("failed to submit and start ssh server job: %w", err)
678675
}
679676

680-
sp := cmdio.NewSpinner(ctx)
677+
sp := cmdio.NewSpinner(ctx, cmdio.WithElapsedTime())
681678
defer sp.Close()
682-
sp.TrackElapsedTime()
683679
sp.Update("Waiting for the SSH server to start...")
684680
maxRetries := 30
685681
for retries := range maxRetries {

libs/cmdio/io.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ func RunSelect(ctx context.Context, prompt *promptui.Select) (int, string, error
195195
//
196196
// The spinner automatically degrades in non-interactive terminals (no output).
197197
// Context cancellation will automatically close the spinner.
198-
func NewSpinner(ctx context.Context) *spinner {
198+
func NewSpinner(ctx context.Context, opts ...SpinnerOption) *spinner {
199199
c := fromContext(ctx)
200-
return c.NewSpinner(ctx)
200+
return c.NewSpinner(ctx, opts...)
201201
}
202202

203203
type cmdIOType int

libs/cmdio/spinner.go

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,22 @@ type spinnerModel struct {
2121

2222
// Message types for spinner updates.
2323
type (
24-
suffixMsg string
25-
quitMsg struct{}
26-
elapsedTimeMsg struct{ startTime time.Time }
24+
suffixMsg string
25+
quitMsg struct{}
2726
)
2827

28+
// SpinnerOption configures spinner behavior.
29+
type SpinnerOption func(*spinnerModel)
30+
31+
// WithElapsedTime enables an elapsed time prefix (MM:SS) on the spinner.
32+
func WithElapsedTime() SpinnerOption {
33+
return func(m *spinnerModel) {
34+
m.startTime = time.Now()
35+
}
36+
}
37+
2938
// newSpinnerModel creates a new spinner model.
30-
func newSpinnerModel() spinnerModel {
39+
func newSpinnerModel(opts ...SpinnerOption) spinnerModel {
3140
s := bubblespinner.New()
3241
// Braille spinner frames with 200ms timing
3342
s.Spinner = bubblespinner.Spinner{
@@ -36,11 +45,13 @@ func newSpinnerModel() spinnerModel {
3645
}
3746
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // Green
3847

39-
return spinnerModel{
40-
spinner: s,
41-
suffix: "",
42-
quitting: false,
48+
m := spinnerModel{
49+
spinner: s,
50+
}
51+
for _, opt := range opts {
52+
opt(&m)
4353
}
54+
return m
4455
}
4556

4657
func (m spinnerModel) Init() tea.Cmd {
@@ -53,10 +64,6 @@ func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5364
m.suffix = string(msg)
5465
return m, nil
5566

56-
case elapsedTimeMsg:
57-
m.startTime = msg.startTime
58-
return m, nil
59-
6067
case quitMsg:
6168
m.quitting = true
6269
return m, tea.Quit
@@ -76,13 +83,14 @@ func (m spinnerModel) View() string {
7683
return ""
7784
}
7885

79-
result := m.spinner.View()
80-
if m.suffix != "" {
81-
result += " " + m.suffix
82-
}
86+
var result string
8387
if !m.startTime.IsZero() {
8488
elapsed := time.Since(m.startTime)
85-
result += fmt.Sprintf(" %02d:%02d", int(elapsed.Minutes()), int(elapsed.Seconds())%60)
89+
result += fmt.Sprintf("%02d:%02d ", int(elapsed.Minutes()), int(elapsed.Seconds())%60)
90+
}
91+
result += m.spinner.View()
92+
if m.suffix != "" {
93+
result += " " + m.suffix
8694
}
8795
return result
8896
}
@@ -101,13 +109,6 @@ type spinner struct {
101109
done chan struct{} // Closed when tea.Program finishes
102110
}
103111

104-
// TrackElapsedTime enables an elapsed time display (MM:SS) next to the spinner message.
105-
func (sp *spinner) TrackElapsedTime() {
106-
if sp.p != nil {
107-
sp.p.Send(elapsedTimeMsg{startTime: time.Now()})
108-
}
109-
}
110-
111112
// Update sends a status message to the spinner.
112113
// This operation sends directly to the tea.Program.
113114
func (sp *spinner) Update(msg string) {
@@ -139,14 +140,18 @@ func (sp *spinner) Close() {
139140
// sp := cmdio.NewSpinner(ctx)
140141
// defer sp.Close()
141142
// sp.Update("processing files")
142-
func (c *cmdIO) NewSpinner(ctx context.Context) *spinner {
143+
//
144+
// Use WithElapsedTime() to show a running MM:SS prefix:
145+
//
146+
// sp := cmdio.NewSpinner(ctx, cmdio.WithElapsedTime())
147+
func (c *cmdIO) NewSpinner(ctx context.Context, opts ...SpinnerOption) *spinner {
143148
// Don't show spinner if not interactive
144149
if !c.capabilities.SupportsInteractive() {
145150
return &spinner{p: nil, c: c, ctx: ctx}
146151
}
147152

148153
// Create model and program
149-
m := newSpinnerModel()
154+
m := newSpinnerModel(opts...)
150155
p := tea.NewProgram(
151156
m,
152157
tea.WithInput(nil),

0 commit comments

Comments
 (0)