@@ -4,6 +4,7 @@ package cmd
44import (
55 "bufio"
66 "fmt"
7+ "io"
78 "math"
89 "os"
910 "strings"
@@ -17,6 +18,40 @@ type CommandInfo struct {
1718 Description string
1819}
1920
21+ // UI represents the interface for terminal UI operations
22+ type UI struct {
23+ stdin io.Reader
24+ stdout io.Writer
25+ stderr io.Writer
26+ term terminal
27+ }
28+
29+ // terminal represents terminal operations
30+ type terminal interface {
31+ makeRaw (fd int ) (* term.State , error )
32+ restore (fd int , state * term.State ) error
33+ }
34+
35+ type defaultTerminal struct {}
36+
37+ func (t * defaultTerminal ) makeRaw (fd int ) (* term.State , error ) {
38+ return term .MakeRaw (fd )
39+ }
40+
41+ func (t * defaultTerminal ) restore (fd int , state * term.State ) error {
42+ return term .Restore (fd , state )
43+ }
44+
45+ // NewUI creates a new UI with default settings
46+ func NewUI () * UI {
47+ return & UI {
48+ stdin : os .Stdin ,
49+ stdout : os .Stdout ,
50+ stderr : os .Stderr ,
51+ term : & defaultTerminal {},
52+ }
53+ }
54+
2055var commands = []CommandInfo {
2156 {"add <file>" , "Add a specific file to the index" },
2257 {"add ." , "Add all changes to index" },
@@ -74,28 +109,34 @@ var commands = []CommandInfo{
74109// InteractiveUI provides an incremental search interactive UI for command selection.
75110// Returns the selected command as []string (nil if nothing selected)
76111func InteractiveUI () []string {
112+ ui := NewUI ()
113+ return ui .Run ()
114+ }
115+
116+ // Run executes the interactive UI
117+ func (ui * UI ) Run () []string {
77118 fd := int (os .Stdin .Fd ())
78- oldState , err := term .MakeRaw (fd )
119+ oldState , err := ui . term .makeRaw (fd )
79120 if err != nil {
80- fmt .Println ( "Failed to set terminal to raw mode:" , err )
121+ fmt .Fprintln ( ui . stderr , "Failed to set terminal to raw mode:" , err )
81122 return nil
82123 }
83124 defer func () {
84- if err := term .Restore (fd , oldState ); err != nil {
85- fmt .Fprintln (os . Stderr , "failed to restore terminal state:" , err )
125+ if err := ui . term .restore (fd , oldState ); err != nil {
126+ fmt .Fprintln (ui . stderr , "failed to restore terminal state:" , err )
86127 }
87128 }()
88129
89- reader := bufio .NewReader (os . Stdin )
130+ reader := bufio .NewReader (ui . stdin )
90131 selected := 0
91132 input := ""
92133
93134 for {
94- if _ , err := os . Stdout . Write ([] byte ( "\033 [H\033 [2J\033 [H" ) ); err != nil {
95- fmt .Fprintln (os . Stderr , "failed to write clear screen sequence:" , err )
135+ if _ , err := fmt . Fprint ( ui . stdout , "\033 [H\033 [2J\033 [H" ); err != nil {
136+ fmt .Fprintln (ui . stderr , "failed to write clear screen sequence:" , err )
96137 }
97- fmt .Printf ( "Select a command (incremental search: type to filter, ctrl+n: down, ctrl+p: up, enter: execute, ctrl+c: quit)\n " )
98- fmt .Printf ( "\r Search: %s\n \n " , input )
138+ fmt .Fprintf ( ui . stdout , "Select a command (incremental search: type to filter, ctrl+n: down, ctrl+p: up, enter: execute, ctrl+c: quit)\n " )
139+ fmt .Fprintf ( ui . stdout , "\r Search: %s\n \n " , input )
99140
100141 // Filtering
101142 filtered := []CommandInfo {}
@@ -105,10 +146,10 @@ func InteractiveUI() []string {
105146 }
106147 }
107148 if input == "" {
108- fmt .Println ( "(Type to filter commands...)" )
149+ fmt .Fprintln ( ui . stdout , "(Type to filter commands...)" )
109150 } else {
110151 if len (filtered ) == 0 {
111- fmt .Println ( " (No matching command)" )
152+ fmt .Fprintln ( ui . stdout , " (No matching command)" )
112153 }
113154 if selected >= len (filtered ) {
114155 selected = len (filtered ) - 1
@@ -131,41 +172,41 @@ func InteractiveUI() []string {
131172 paddingLen := int (math .Max (0 , float64 (maxCmdLen - len (cmd .Command ))))
132173 padding := strings .Repeat (" " , paddingLen )
133174 if i == selected {
134- fmt .Printf ( "\r > %s%s %s\n " , cmd .Command , padding , desc )
175+ fmt .Fprintf ( ui . stdout , "\r > %s%s %s\n " , cmd .Command , padding , desc )
135176 } else {
136- fmt .Printf ( "\r %s%s %s\n " , cmd .Command , padding , desc )
177+ fmt .Fprintf ( ui . stdout , "\r %s%s %s\n " , cmd .Command , padding , desc )
137178 }
138179 }
139180 }
140- fmt .Print ( "\n \r " ) // Ensure next output starts at left edge
181+ fmt .Fprint ( ui . stdout , "\n \r " ) // Ensure next output starts at left edge
141182
142183 b , err := reader .ReadByte ()
143184 if err != nil {
144185 continue
145186 }
146187 if b == 3 { // Ctrl+C in raw mode
147- if err := term .Restore (fd , oldState ); err != nil {
148- fmt .Fprintln (os . Stderr , "failed to restore terminal state:" , err )
188+ if err := ui . term .restore (fd , oldState ); err != nil {
189+ fmt .Fprintln (ui . stderr , "failed to restore terminal state:" , err )
149190 }
150- fmt .Println ( "\n Exiting..." )
191+ fmt .Fprintln ( ui . stdout , "\n Exiting..." )
151192 os .Exit (0 )
152193 } else if b == 13 { // Enter
153194 if input == "" {
154195 continue
155196 }
156197 if len (filtered ) > 0 {
157- fmt .Printf ( "\n Execute: %s\n " , filtered [selected ].Command )
158- if err := term .Restore (fd , oldState ); err != nil {
159- fmt .Fprintln (os . Stderr , "failed to restore terminal state:" , err )
198+ fmt .Fprintf ( ui . stdout , "\n Execute: %s\n " , filtered [selected ].Command )
199+ if err := ui . term .restore (fd , oldState ); err != nil {
200+ fmt .Fprintln (ui . stderr , "failed to restore terminal state:" , err )
160201 }
161202 // Placeholder detection
162203 cmdTemplate := filtered [selected ].Command
163204 placeholders := extractPlaceholders (cmdTemplate )
164205 inputs := make (map [string ]string )
165- readerStdin := bufio .NewReader (os . Stdin )
206+ readerStdin := bufio .NewReader (ui . stdin )
166207 for _ , ph := range placeholders {
167- fmt .Print ( "\n \r " ) // Newline + carriage return
168- fmt .Printf ( "Enter value for %s: " , ph )
208+ fmt .Fprint ( ui . stdout , "\n \r " ) // Newline + carriage return
209+ fmt .Fprintf ( ui . stdout , "Enter value for %s: " , ph )
169210 val , _ := readerStdin .ReadString ('\n' )
170211 val = strings .TrimSpace (val )
171212 inputs [ph ] = val
0 commit comments