Skip to content

Commit da43d0b

Browse files
authored
Merge pull request #211 from hugoh/display
2 parents df1746c + 69a4f5b commit da43d0b

8 files changed

Lines changed: 267 additions & 106 deletions

File tree

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ module github.com/hugoh/tmhi-cli
33
go 1.25.0
44

55
require (
6-
atomicgo.dev/keyboard v0.2.9
76
github.com/go-playground/validator/v10 v10.30.2
8-
github.com/hugoh/cellular-signal v1.1.2
9-
github.com/hugoh/tmhi-gateway v1.1.0
7+
github.com/hugoh/cellular-signal v1.1.3
8+
github.com/hugoh/tmhi-gateway v1.1.1
109
github.com/pterm/pterm v0.12.83
1110
github.com/stretchr/testify v1.11.1
1211
github.com/urfave/cli-altsrc/v3 v3.1.0
@@ -17,6 +16,7 @@ require (
1716

1817
require (
1918
atomicgo.dev/cursor v0.2.0 // indirect
19+
atomicgo.dev/keyboard v0.2.9 // indirect
2020
atomicgo.dev/schedule v0.1.0 // indirect
2121
github.com/BurntSushi/toml v1.6.0 // indirect
2222
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ
4545
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
4646
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
4747
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
48-
github.com/hugoh/cellular-signal v1.1.2 h1:olGnf0ZyDf0dUMr8BI4wjFVClOvRzP6uc9JHilHiJHc=
49-
github.com/hugoh/cellular-signal v1.1.2/go.mod h1:UX0FKsxcPzK7YFM6Glu7LjZQA8m8TxXcRV64/N1rgZk=
50-
github.com/hugoh/tmhi-gateway v1.1.0 h1:cXwcF1nlRTeVEwq6BQ6xhZogerun62zbSQEO8iqCnMo=
51-
github.com/hugoh/tmhi-gateway v1.1.0/go.mod h1:QuYWc8UMULHcn6ZOe5zYiykdPs0uQKDHbao6fWOMzbo=
48+
github.com/hugoh/cellular-signal v1.1.3 h1:RBreCLAgY5zoeYbmUPVxMKVZVSuiirnrRHo6a6Dqgz4=
49+
github.com/hugoh/cellular-signal v1.1.3/go.mod h1:UX0FKsxcPzK7YFM6Glu7LjZQA8m8TxXcRV64/N1rgZk=
50+
github.com/hugoh/tmhi-gateway v1.1.1 h1:EKQ/Nx6H57G68gHylzUEyE6DKEjiGpvGhgt7N3cyYF8=
51+
github.com/hugoh/tmhi-gateway v1.1.1/go.mod h1:32TqX2vp5E4YdTe5ckfTiFc9onf+RaGE4+pIJaJpf5E=
5252
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
5353
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
5454
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=

internal/cmd.go

Lines changed: 108 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,60 @@ import (
1414
"golang.org/x/term"
1515
)
1616

17+
type spinner interface {
18+
Fail(message ...any)
19+
Success(message ...any)
20+
Stop() error
21+
}
22+
23+
// spinnerFunc creates a new spinner. Overridable for testing.
24+
//
25+
//nolint:gochecknoglobals
26+
var spinnerFunc = func(message string) (spinner, error) {
27+
sp, err := pterm.DefaultSpinner.Start(message)
28+
if err != nil {
29+
return nil, fmt.Errorf("failed to start spinner: %w", err)
30+
}
31+
32+
return &spinnerWrapper{spinnerPrinter: sp}, nil
33+
}
34+
35+
// spinnerWrapper wraps pterm.SpinnerPrinter to implement the spinner interface.
36+
type spinnerWrapper struct {
37+
spinnerPrinter *pterm.SpinnerPrinter
38+
}
39+
40+
func (w *spinnerWrapper) Fail(message ...any) {
41+
w.spinnerPrinter.Fail(message...)
42+
}
43+
44+
func (w *spinnerWrapper) Success(message ...any) {
45+
if message == nil {
46+
_ = w.spinnerPrinter.WithRemoveWhenDone().Stop()
47+
48+
return
49+
}
50+
51+
w.spinnerPrinter.Success(message...)
52+
}
53+
54+
func (w *spinnerWrapper) Stop() error {
55+
if err := w.spinnerPrinter.Stop(); err != nil {
56+
return fmt.Errorf("failed to stop spinner: %w", err)
57+
}
58+
59+
return nil
60+
}
61+
62+
// confirmDialog prompts the user for confirmation. Overridable for testing.
63+
//
64+
//nolint:gochecknoglobals
65+
var confirmDialog = func(msg string, defaultVal bool) (bool, error) {
66+
return pterm.DefaultInteractiveConfirm.
67+
WithDefaultValue(defaultVal).
68+
Show(msg)
69+
}
70+
1771
// Gateway model constants.
1872
const (
1973
ARCADYAN string = "ARCADYAN"
@@ -24,22 +78,34 @@ const (
2478
//nolint:gochecknoglobals
2579
var initGatewayFunc = initGateway
2680

27-
// withSpinner runs an operation with a spinner, handling success/failure.
28-
// It starts a spinner with the given message, executes the function,
29-
// and properly stops the spinner on success or failure.
81+
// fetchWithFeedback runs an operation with a spinner, handling success/failure.
82+
// It starts a spinner with the given message, executes the fetch function,
83+
// displays the result using the display function, and properly stops the spinner.
3084
//
3185
//nolint:ireturn
32-
func withSpinner[T any](message string, fn func() (T, error)) (T, error) {
33-
spinner, _ := pterm.DefaultSpinner.Start(message)
34-
35-
result, err := fn()
86+
func fetchWithFeedback[T any](
87+
message string,
88+
fetch func() (T, error),
89+
display func(T),
90+
successMessage ...any,
91+
) (T, error) {
92+
spinnerInstance, err := spinnerFunc(message)
3693
if err != nil {
37-
spinner.Fail(fmt.Sprintf("%s: %v", message, err))
94+
return *new(T), err
95+
}
96+
97+
result, opErr := fetch()
98+
if opErr != nil {
99+
spinnerInstance.Fail(fmt.Sprintf("%s: %v", message, opErr))
38100

39-
return result, fmt.Errorf("%s: %w", message, err)
101+
return result, fmt.Errorf("%s: %w", message, opErr)
40102
}
41103

42-
_ = spinner.WithRemoveWhenDone().Stop()
104+
spinnerInstance.Success(successMessage...)
105+
106+
if display != nil {
107+
display(result)
108+
}
43109

44110
return result, nil
45111
}
@@ -62,16 +128,9 @@ func login(_ context.Context, _ *cli.Command) error {
62128
return err
63129
}
64130

65-
result, err := withSpinner("Checking logging in...", func() (*tmhi.LoginResult, error) {
66-
return gateway.Login()
67-
})
68-
if err != nil {
69-
return err
70-
}
71-
72-
displayLoginResult(result)
131+
_, err = fetchWithFeedback("Checking logging in...", gateway.Login, displayLoginResult)
73132

74-
return nil
133+
return err
75134
}
76135

77136
func req(_ context.Context, cmd *cli.Command) error {
@@ -111,14 +170,9 @@ func info(_ context.Context, _ *cli.Command) error {
111170
return err
112171
}
113172

114-
result, err := gateway.Info()
115-
if err != nil {
116-
return fmt.Errorf("info command failed: %w", err)
117-
}
118-
119-
displayInfoResult(result)
173+
_, err = fetchWithFeedback("Fetching gateway info...", gateway.Info, displayInfoResult)
120174

121-
return nil
175+
return err
122176
}
123177

124178
func status(_ context.Context, _ *cli.Command) error {
@@ -127,16 +181,9 @@ func status(_ context.Context, _ *cli.Command) error {
127181
return err
128182
}
129183

130-
result, err := withSpinner("Checking gateway status...", func() (*tmhi.StatusResult, error) {
131-
return gateway.Status()
132-
})
133-
if err != nil {
134-
return err
135-
}
136-
137-
displayStatusResult(result)
184+
_, err = fetchWithFeedback("Checking gateway status...", gateway.Status, displayStatusResult)
138185

139-
return nil
186+
return err
140187
}
141188

142189
func signalCmd(_ context.Context, _ *cli.Command) error {
@@ -145,19 +192,13 @@ func signalCmd(_ context.Context, _ *cli.Command) error {
145192
return err
146193
}
147194

148-
result, err := withSpinner(
195+
_, err = fetchWithFeedback(
149196
"Fetching signal information...",
150-
func() (*tmhi.SignalResult, error) {
151-
return gateway.Signal()
152-
},
197+
gateway.Signal,
198+
displaySignalResult,
153199
)
154-
if err != nil {
155-
return err
156-
}
157200

158-
displaySignalResult(result)
159-
160-
return nil
201+
return err
161202
}
162203

163204
func reboot(_ context.Context, cmd *cli.Command) error {
@@ -167,10 +208,15 @@ func reboot(_ context.Context, cmd *cli.Command) error {
167208
}
168209

169210
if !cmd.Bool(ConfigAutoConfirm) {
170-
confirm, _ := pterm.DefaultInteractiveConfirm.
171-
WithDefaultValue(false).
172-
Show("Are you sure you want to reboot the gateway?")
173-
if !confirm {
211+
confirmed, confirmErr := confirmDialog(
212+
"Are you sure you want to reboot the gateway?",
213+
false,
214+
)
215+
if confirmErr != nil {
216+
return fmt.Errorf("confirmation failed: %w", confirmErr)
217+
}
218+
219+
if !confirmed {
174220
pterm.Warning.Println("Reboot cancelled")
175221

176222
return nil
@@ -183,18 +229,21 @@ func reboot(_ context.Context, cmd *cli.Command) error {
183229
return nil
184230
}
185231

186-
spinner, _ := pterm.DefaultSpinner.Start("Rebooting gateway...")
187-
188-
err = gateway.Reboot()
189-
if err != nil {
190-
spinner.Fail("Reboot failed")
191-
192-
return fmt.Errorf("could not reboot gateway: %w", err)
193-
}
232+
_, ret := fetchWithFeedback(
233+
"Rebooting gateway...",
234+
func() (*tmhi.SignalResult, error) {
235+
rebootErr := gateway.Reboot()
236+
if rebootErr != nil {
237+
return nil, fmt.Errorf("Reboot failed: %w", rebootErr)
238+
}
194239

195-
spinner.Success("Reboot command sent successfully")
240+
return nil, nil //nolint:nilnil // no error to report
241+
},
242+
nil,
243+
"Reboot command sent successfully",
244+
)
196245

197-
return nil
246+
return ret
198247
}
199248

200249
func defaultConfigPath() string {

internal/cmd_handlers_test.go

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import (
66
"testing"
77
"time"
88

9-
"atomicgo.dev/keyboard"
10-
"atomicgo.dev/keyboard/keys"
119
tmhi "github.com/hugoh/tmhi-gateway"
1210
"github.com/stretchr/testify/assert"
1311
"github.com/stretchr/testify/require"
@@ -115,7 +113,6 @@ func TestLogin_SuccessAndFailure(t *testing.T) {
115113
})
116114
}
117115

118-
//nolint:dupl
119116
func TestInfo_SuccessAndFailure(t *testing.T) {
120117
original := initGatewayFunc
121118

@@ -134,7 +131,8 @@ func TestInfo_SuccessAndFailure(t *testing.T) {
134131
initGatewayFunc = func(_ *Config) (tmhi.Gateway, error) { return mg, nil }
135132
err := info(context.Background(), nil)
136133
require.Error(t, err)
137-
assert.Contains(t, err.Error(), "info command failed")
134+
assert.Contains(t, err.Error(), "Fetching gateway info")
135+
assert.Contains(t, err.Error(), "info boom")
138136
assert.True(t, mg.infoCalled)
139137
})
140138
}
@@ -203,15 +201,15 @@ func TestReboot_ConfirmationDefaultsToNo(t *testing.T) {
203201
}()
204202

205203
t.Run("enter accepts default no", func(t *testing.T) {
206-
testRebootConfirmCancel(t, keys.Enter)
204+
testRebootConfirmCancel(t)
207205
})
208206

209207
t.Run("y confirms reboot", func(t *testing.T) {
210-
testRebootConfirmProceed(t, 'y')
208+
testRebootConfirmProceed(t)
211209
})
212210

213211
t.Run("n cancels reboot", func(t *testing.T) {
214-
testRebootConfirmCancel(t, 'n')
212+
testRebootConfirmCancel(t)
215213
})
216214

217215
t.Run("auto confirm skips prompt", func(t *testing.T) {
@@ -232,66 +230,64 @@ func TestReboot_ConfirmationDefaultsToNo(t *testing.T) {
232230
}
233231

234232
//nolint:dupl
235-
func testRebootConfirmCancel(t *testing.T, key any) {
233+
func testRebootConfirmCancel(t *testing.T) {
236234
t.Helper()
237235

238236
original := initGatewayFunc
239237
originalConfig := appConfig
238+
originalConfirm := confirmDialog
240239

241240
defer func() {
242241
initGatewayFunc = original
243242
appConfig = originalConfig
243+
confirmDialog = originalConfirm
244244
}()
245245

246246
appConfig = &Config{DryRun: false}
247247
mg := &mockGateway{}
248248
initGatewayFunc = func(_ *Config) (tmhi.Gateway, error) { return mg, nil }
249+
confirmDialog = func(_ string, _ bool) (bool, error) {
250+
return false, nil // Simulate user cancelling
251+
}
249252
cmd := &cli.Command{
250253
Flags: []cli.Flag{
251254
&cli.BoolFlag{Name: ConfigDryRun, Value: false},
252255
&cli.BoolFlag{Name: ConfigAutoConfirm, Value: false},
253256
},
254257
}
255258

256-
go func() {
257-
time.Sleep(50 * time.Millisecond)
258-
259-
_ = keyboard.SimulateKeyPress(key)
260-
}()
261-
262259
err := reboot(context.Background(), cmd)
263260
require.NoError(t, err)
264261
assert.False(t, mg.rebootCalled, "reboot should be cancelled")
265262
}
266263

267264
//nolint:dupl
268-
func testRebootConfirmProceed(t *testing.T, key any) {
265+
func testRebootConfirmProceed(t *testing.T) {
269266
t.Helper()
270267

271268
original := initGatewayFunc
272269
originalConfig := appConfig
270+
originalConfirm := confirmDialog
273271

274272
defer func() {
275273
initGatewayFunc = original
276274
appConfig = originalConfig
275+
confirmDialog = originalConfirm
277276
}()
278277

279278
appConfig = &Config{DryRun: false}
280279
mg := &mockGateway{}
281280
initGatewayFunc = func(_ *Config) (tmhi.Gateway, error) { return mg, nil }
281+
confirmDialog = func(_ string, _ bool) (bool, error) {
282+
return true, nil // Simulate user confirming
283+
}
282284
cmd := &cli.Command{
283285
Flags: []cli.Flag{
284286
&cli.BoolFlag{Name: ConfigDryRun, Value: false},
285287
&cli.BoolFlag{Name: ConfigAutoConfirm, Value: false},
286288
},
287289
}
288290

289-
go func() {
290-
time.Sleep(50 * time.Millisecond)
291-
292-
_ = keyboard.SimulateKeyPress(key)
293-
}()
294-
295291
err := reboot(context.Background(), cmd)
296292
require.NoError(t, err)
297293
assert.True(t, mg.rebootCalled, "reboot should proceed")

0 commit comments

Comments
 (0)