Skip to content

Commit bd65f40

Browse files
committed
feat: TUI improvements
- Make --tui default for scan command (use --no-tui for text output) - Rescan items after deletion instead of exiting (press q to quit) - Remember cursor position when navigating tree (cursorStack) - Update help text to show rescan/quit options
1 parent 1516aca commit bd65f40

2 files changed

Lines changed: 90 additions & 12 deletions

File tree

cmd/root/scan.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ var scanCmd = &cobra.Command{
2525
Short: "Scan for development artifacts",
2626
Long: `Scan your system for development artifacts that can be cleaned.
2727
28-
By default, scans all categories (iOS, Android, Node.js).
29-
Use flags to scan specific categories only.
28+
By default, opens interactive TUI for selection.
29+
Use --no-tui for simple text output.
3030
3131
Examples:
32-
dev-cleaner scan # Scan all categories
33-
dev-cleaner scan --ios # Scan iOS/Xcode only
34-
dev-cleaner scan --android # Scan Android/Gradle only
35-
dev-cleaner scan --node # Scan Node.js only`,
32+
dev-cleaner scan # Scan + TUI (default)
33+
dev-cleaner scan --no-tui # Scan + text output
34+
dev-cleaner scan --ios # Scan iOS/Xcode only`,
3635
Run: runScan,
3736
}
3837

@@ -43,7 +42,8 @@ func init() {
4342
scanCmd.Flags().BoolVar(&scanAndroid, "android", false, "Scan Android/Gradle artifacts only")
4443
scanCmd.Flags().BoolVar(&scanNode, "node", false, "Scan Node.js artifacts only")
4544
scanCmd.Flags().BoolVar(&scanAll, "all", true, "Scan all categories (default)")
46-
scanCmd.Flags().BoolVar(&scanTUI, "tui", false, "Launch interactive TUI for selection")
45+
scanCmd.Flags().BoolVar(&scanTUI, "tui", true, "Launch interactive TUI (default)")
46+
scanCmd.Flags().BoolP("no-tui", "T", false, "Disable TUI, show text output")
4747
}
4848

4949
func runScan(cmd *cobra.Command, args []string) {
@@ -86,7 +86,13 @@ func runScan(cmd *cobra.Command, args []string) {
8686
// Sort by size (largest first)
8787
sortBySize(results)
8888

89-
// Launch TUI if --tui flag is set
89+
// Check for --no-tui flag
90+
noTUI, _ := cmd.Flags().GetBool("no-tui")
91+
if noTUI {
92+
scanTUI = false
93+
}
94+
95+
// Launch TUI by default
9096
if scanTUI {
9197
if err := tui.Run(results, false); err != nil {
9298
fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err)

internal/tui/tui.go

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ type Model struct {
155155
treeMode bool // True when in tree view
156156
currentNode *types.TreeNode // Current tree node
157157
nodeStack []*types.TreeNode // Breadcrumb trail
158+
cursorStack []int // Cursor positions for each level
158159
maxDepth int // Max depth limit
159160
treeSelected map[string]bool // Selected items in tree
160161
scanning bool // True while scanning
@@ -217,7 +218,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
217218
// Handle based on current state
218219
switch m.state {
219220
case StateDone:
220-
return m, tea.Quit
221+
// 'q' to quit, any other key to rescan and continue
222+
if msg.String() == "q" || msg.String() == "ctrl+c" {
223+
m.quitting = true
224+
return m, tea.Quit
225+
}
226+
// Rescan and return to selection
227+
return m, m.rescanItems()
221228

222229
case StateConfirming:
223230
switch msg.String() {
@@ -346,6 +353,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
346353
}
347354
m.cursor = 0
348355
return m, nil
356+
357+
case rescanItemsMsg:
358+
if msg.err != nil {
359+
m.err = msg.err
360+
return m, nil
361+
}
362+
// Reset state and show new items
363+
m.items = msg.items
364+
m.selected = make(map[int]bool)
365+
m.cursor = 0
366+
m.state = StateSelecting
367+
m.results = nil
368+
m.err = nil
369+
return m, nil
349370
}
350371

351372
return m, nil
@@ -368,6 +389,45 @@ type scanNodeMsg struct {
368389
err error
369390
}
370391

392+
// rescanItemsMsg is sent when items rescan completes
393+
type rescanItemsMsg struct {
394+
items []types.ScanResult
395+
err error
396+
}
397+
398+
// rescanItems rescans all items and returns to selection
399+
func (m Model) rescanItems() tea.Cmd {
400+
return func() tea.Msg {
401+
s, err := scanner.New()
402+
if err != nil {
403+
return rescanItemsMsg{err: err}
404+
}
405+
406+
opts := types.ScanOptions{
407+
MaxDepth: 3,
408+
IncludeXcode: true,
409+
IncludeAndroid: true,
410+
IncludeNode: true,
411+
}
412+
413+
results, err := s.ScanAll(opts)
414+
if err != nil {
415+
return rescanItemsMsg{err: err}
416+
}
417+
418+
// Sort by size
419+
for i := 0; i < len(results)-1; i++ {
420+
for j := i + 1; j < len(results); j++ {
421+
if results[j].Size > results[i].Size {
422+
results[i], results[j] = results[j], results[i]
423+
}
424+
}
425+
}
426+
427+
return rescanItemsMsg{items: results}
428+
}
429+
}
430+
371431
// enterTreeMode transitions from flat list to tree view
372432
func (m Model) enterTreeMode() tea.Cmd {
373433
return func() tea.Msg {
@@ -409,9 +469,17 @@ func (m *Model) goBackInTree() {
409469
return
410470
}
411471

472+
// Pop node and cursor from stacks
412473
m.currentNode = m.nodeStack[len(m.nodeStack)-1]
413474
m.nodeStack = m.nodeStack[:len(m.nodeStack)-1]
414-
m.cursor = 0
475+
476+
// Restore cursor position
477+
if len(m.cursorStack) > 0 {
478+
m.cursor = m.cursorStack[len(m.cursorStack)-1]
479+
m.cursorStack = m.cursorStack[:len(m.cursorStack)-1]
480+
} else {
481+
m.cursor = 0
482+
}
415483
}
416484

417485
// drillDownInTree navigates into child node
@@ -436,9 +504,13 @@ func (m *Model) drillDownInTree() tea.Cmd {
436504

437505
if selectedNode.NeedsScanning() {
438506
m.scanning = true
507+
// Save cursor position before scanning
508+
m.cursorStack = append(m.cursorStack, m.cursor)
439509
return m.scanNode(selectedNode)
440510
}
441511

512+
// Save cursor position before navigating
513+
m.cursorStack = append(m.cursorStack, m.cursor)
442514
m.nodeStack = append(m.nodeStack, m.currentNode)
443515
m.currentNode = selectedNode
444516
m.cursor = 0
@@ -759,7 +831,7 @@ func (m Model) renderSelection(b *strings.Builder) string {
759831
func (m Model) renderResults(b *strings.Builder) string {
760832
if m.err != nil {
761833
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %v", m.err)))
762-
b.WriteString("\n\nPress any key to exit.")
834+
b.WriteString("\n\nPress any key to rescan, q to quit.")
763835
return b.String()
764836
}
765837

@@ -786,7 +858,7 @@ func (m Model) renderResults(b *strings.Builder) string {
786858
summary += fmt.Sprintf(" (%s freed)", ui.FormatSize(freedSize))
787859
}
788860
b.WriteString(successStyle.Render(summary))
789-
b.WriteString("\n\nPress any key to exit.")
861+
b.WriteString("\n\nPress any key to rescan, q to quit.")
790862

791863
return b.String()
792864
}

0 commit comments

Comments
 (0)