Skip to content

Commit 9b70de6

Browse files
committed
Single select for switch command to select a worktree to switch to
1 parent 9918b49 commit 9b70de6

2 files changed

Lines changed: 157 additions & 4 deletions

File tree

cmd/input.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,110 @@ func redrawMultiSelect(title string, items []MultiSelectItem, cursor int, totalL
370370
}
371371
fmt.Print(color.New(color.Faint).Sprint("↑/k up ↓/j down space select enter confirm q cancel"))
372372
}
373+
374+
// promptSingleSelect displays a navigable single-select list
375+
// Navigation: arrow keys or hjkl, enter to select, q to cancel
376+
// Returns selected item, whether user confirmed (false if cancelled), error
377+
func promptSingleSelect(title string, items []string) (string, bool, error) {
378+
if len(items) == 0 {
379+
return "", false, fmt.Errorf("no items provided")
380+
}
381+
382+
cursor := 0
383+
totalLines := len(items) + 2 // title + items + help line
384+
385+
// Print initial display
386+
printSingleSelect(title, items, cursor)
387+
388+
for {
389+
// Get terminal into raw mode for reading input
390+
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
391+
if err != nil {
392+
return "", false, fmt.Errorf("failed to set raw mode: %w", err)
393+
}
394+
395+
// Read input
396+
b := make([]byte, 3) // Up to 3 bytes for arrow keys
397+
n, err := os.Stdin.Read(b)
398+
399+
// Restore terminal before processing
400+
term.Restore(int(os.Stdin.Fd()), oldState)
401+
402+
if err != nil {
403+
return "", false, fmt.Errorf("failed to read input: %w", err)
404+
}
405+
406+
needsRedraw := false
407+
408+
// Handle input
409+
if n == 1 {
410+
switch b[0] {
411+
case 'q', 27, 3: // q, ESC, or Ctrl+C
412+
fmt.Println()
413+
return "", false, nil
414+
case '\r', '\n': // Enter - select current item
415+
fmt.Println()
416+
return items[cursor], true, nil
417+
case 'k': // Vim up
418+
if cursor > 0 {
419+
cursor--
420+
needsRedraw = true
421+
}
422+
case 'j': // Vim down
423+
if cursor < len(items)-1 {
424+
cursor++
425+
needsRedraw = true
426+
}
427+
}
428+
} else if n == 3 && b[0] == 27 && b[1] == 91 {
429+
// Arrow keys: ESC [ A/B/C/D
430+
switch b[2] {
431+
case 65: // Up arrow
432+
if cursor > 0 {
433+
cursor--
434+
needsRedraw = true
435+
}
436+
case 66: // Down arrow
437+
if cursor < len(items)-1 {
438+
cursor++
439+
needsRedraw = true
440+
}
441+
}
442+
}
443+
444+
if needsRedraw {
445+
redrawSingleSelect(title, items, cursor, totalLines)
446+
}
447+
}
448+
}
449+
450+
// printSingleSelect prints the initial single-select display
451+
func printSingleSelect(title string, items []string, cursor int) {
452+
fmt.Println(title)
453+
for i, item := range items {
454+
prefix := " "
455+
if i == cursor {
456+
prefix = color.CyanString("> ")
457+
}
458+
fmt.Printf("%s%s\n", prefix, item)
459+
}
460+
fmt.Print(color.New(color.Faint).Sprint("↑/k up ↓/j down enter select q cancel"))
461+
}
462+
463+
// redrawSingleSelect redraws the single-select display (called with terminal in normal mode)
464+
func redrawSingleSelect(title string, items []string, cursor int, totalLines int) {
465+
// Move cursor up to the title line and clear to end of screen
466+
fmt.Printf("\033[%dF", totalLines-1)
467+
fmt.Print("\033[J")
468+
469+
// Redraw everything
470+
fmt.Println(title)
471+
for i, item := range items {
472+
prefix := " "
473+
if i == cursor {
474+
prefix = color.CyanString("> ")
475+
}
476+
fmt.Printf("%s%s\n", prefix, item)
477+
}
478+
fmt.Print(color.New(color.Faint).Sprint("↑/k up ↓/j down enter select q cancel"))
479+
}

cmd/switch.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,62 @@ var (
1414
)
1515

1616
var switchCmd = &cobra.Command{
17-
Use: "switch <branch>",
17+
Use: "switch [branch]",
1818
Short: "Switch to a worktree",
19-
Long: `Changes to the worktree directory for the specified branch.`,
20-
Args: cobra.ExactArgs(1),
19+
Long: `Changes to the worktree directory for the specified branch. If no branch specified, shows interactive selection.`,
20+
Args: cobra.MaximumNArgs(1),
2121
RunE: func(cmd *cobra.Command, args []string) error {
2222
if err := ensureGitRepo(); err != nil {
2323
return err
2424
}
2525

26-
branch := args[0]
26+
var branch string
27+
28+
if len(args) == 0 {
29+
// Interactive mode - show single-select
30+
worktrees, err := getWorktrees()
31+
if err != nil {
32+
return fmt.Errorf("failed to get worktrees: %w", err)
33+
}
34+
35+
// Get git root to filter out main worktree
36+
gitRoot, err := getGitRoot()
37+
if err != nil {
38+
return fmt.Errorf("failed to get git root: %w", err)
39+
}
40+
41+
// Build list of selectable worktrees (exclude main)
42+
var items []string
43+
for _, wt := range worktrees {
44+
if wt.Path == gitRoot {
45+
continue // Skip main worktree
46+
}
47+
if wt.Branch == "" {
48+
continue // Skip detached worktrees
49+
}
50+
items = append(items, wt.Branch)
51+
}
52+
53+
if len(items) == 0 {
54+
fmt.Println("No worktrees available to switch to.")
55+
return nil
56+
}
57+
58+
// Show single-select
59+
selected, confirmed, err := promptSingleSelect("Select worktree:", items)
60+
if err != nil {
61+
return fmt.Errorf("failed to get selection: %w", err)
62+
}
63+
64+
if !confirmed {
65+
fmt.Println("Cancelled.")
66+
return nil
67+
}
68+
69+
branch = selected
70+
} else {
71+
branch = args[0]
72+
}
2773

2874
// Find worktree
2975
wt, err := findWorktreeByBranch(branch)

0 commit comments

Comments
 (0)