Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ It outputs results in both **human-friendly tables** and **machine-friendly JSON

🖼 Demo

https://github.com/user-attachments/assets/ba7fbb95-6566-41eb-bc1c-d67fb5adab5e


https://github.com/user-attachments/assets/1c8370c6-3b94-4490-bc96-fc179ef14f1d




---
Expand Down Expand Up @@ -122,7 +126,7 @@ Each step overrides the one before it
- [x] Read user customizable `config.toml` file
- [x] Export Report to json file
- [x] Support dirignore
- [ ] Worker pool for speed
- [x] Worker pool for speed
- [ ] Support git worktrees
- [ ] Perform git push/pull/fetch on repos
- [ ] Show branches with their states on each repo
Expand Down
60 changes: 3 additions & 57 deletions cmd/reposcan/rootCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/mabd-dev/reposcan/internal"
"github.com/mabd-dev/reposcan/internal/config"
"github.com/mabd-dev/reposcan/internal/gitx"
"github.com/mabd-dev/reposcan/internal/logger"
"github.com/mabd-dev/reposcan/internal/render/file"
"github.com/mabd-dev/reposcan/internal/render/stdout"
"github.com/mabd-dev/reposcan/internal/render/tui"
"github.com/mabd-dev/reposcan/internal/scan"
"github.com/mabd-dev/reposcan/pkg/report"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -145,31 +142,7 @@ func readFlags(cmd *cobra.Command, configs *config.Config) error {
}

func run(configs config.Config) error {
reportWarnings := []string{}

// Find git repos at defined configs.Roots
gitReposPaths, warnings := scan.FindGitRepos(configs.Roots, configs.DirIgnore)

reportWarnings = append(reportWarnings, warnings...)

repoStates := make([]report.RepoState, 0, len(gitReposPaths))

allRepoStates, warnings := gitx.GetGitRepoStatesConcurrent(gitReposPaths, configs.MaxWorkers)
reportWarnings = append(reportWarnings, warnings...)

// filter repo states based on config OnlyFilter
for _, repoState := range allRepoStates {
if filter(configs.Only, repoState) {
repoStates = append(repoStates, repoState)
}
}

report := report.ScanReport{
Version: configs.Version,
GeneratedAt: time.Now(),
RepoStates: repoStates,
Warnings: reportWarnings,
}
report := internal.GenerateScanReport(configs)

switch configs.Output.Type {
case config.OutputJson:
Expand All @@ -180,7 +153,7 @@ func run(configs config.Config) error {
case config.OutputTable:
stdout.RenderScanReportAsTable(report)
case config.OutputInteractive:
if err := tui.ShowReportTUI(report, configs.Output.ColorSchemeName); err != nil {
if err := tui.Render(report, configs); err != nil {
fmt.Fprintf(os.Stderr, "tui error: %v\n", err)
os.Exit(1)
}
Expand All @@ -204,30 +177,3 @@ func run(configs config.Config) error {

return nil
}

// Filter repoState based on config only filter
// Returns true if repoState should be in output, false otherwise
func filter(f config.OnlyFilter, repoState report.RepoState) bool {
switch f {
case config.OnlyAll:
return true
case config.OnlyDirty:
if repoState.IsDirty() {
return true
}
case config.OnlyUncommitted:
if len(repoState.UncommitedFiles) > 0 {
return true
}
case config.OnlyUnpushed:
if repoState.Ahead > 0 {
return true
}
case config.OnlyUnpulled:
if repoState.Behind > 0 {
return true
}
}

return false
}
3 changes: 2 additions & 1 deletion cmd/reposcan/versionCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package reposcan
import (
"fmt"

"github.com/mabd-dev/reposcan/internal"
"github.com/spf13/cobra"
)

var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of reposcan",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("reposcan 1.3.5\n")
fmt.Printf("reposcan " + internal.VERSION + "\n")
},
}
44 changes: 44 additions & 0 deletions docs/release-notes/v1.3.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# v1.3.6 - UI Polish & Architecture Improvements

This release focuses on making the interactive TUI more intuitive and responsive, while significantly improving code organization for better maintainability.

## ✨ New Features

### Refresh on Demand
- **New `r` keybinding**: Rescan your filesystem for repositories without restarting the app
- Perfect for when you've just cloned new repos or want to refresh the current state

## 🎨 UI/UX Improvements

### Automatic Repository Details
- Repository details now appear **automatically** when you select a repo - no more pressing Enter to toggle visibility
- Shows maximum possible lines of changed files to give you the full picture at a glance

### Smarter Layout
- **Repositioned filter input**: Moved to the footer when active
- **Maximized table width**: Repository table now expands to fill the full terminal width
- **Loop scrolling**: Navigate seamlessly - scrolling past the last item wraps to the first (and vice versa)

## 🏗️ Technical & Architecture Changes

### Focus Management System
- Introduced **focus model stack** pattern for managing multiple interactive components
- Cleaner state transitions between table navigation, text input, and popup overlays
- Foundation for future interactive features

### Package Restructuring
- **Dedicated TUI overlay package**: Centralized popup and overlay management
- **Standalone repository details package**: Self-contained component that renders based on provided data
- **Consistent TUI package structure**: Every custom Bubble Tea model now follows a standard layout:
- `main.go` - initialization and exports
- `types.go` - model definitions
- `update.go` - message handling
- `view.go` - rendering logic
- Removed unused properties from main model for a cleaner codebase

### Benefits
These architectural improvements make the codebase more maintainable, testable, and easier for contributors to navigate.

## 🐛 Bug Fixes

- **Filter cursor preservation**: Fixed issue where cursor position was lost when filtering repositories - now maintains your selection when possible
3 changes: 3 additions & 0 deletions internal/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package internal

var VERSION string = "v1.3.6"
7 changes: 7 additions & 0 deletions internal/render/tui/common/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package common

type Keybinding struct {
Key string
Description string
ShortDesc string
}
69 changes: 0 additions & 69 deletions internal/render/tui/defaultUpdate.go

This file was deleted.

89 changes: 89 additions & 0 deletions internal/render/tui/focusModel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package tui

import (
"github.com/mabd-dev/reposcan/internal/render/tui/common"
)

type FocusState int

const (
FocusReposTable FocusState = iota
FocusReposFilter
FocusHelpPopup
)

func (m Model) currentFocus() FocusState {
if len(m.focusStack) == 0 {
return FocusReposTable
}
return m.focusStack[len(m.focusStack)-1]
}

func (m *Model) pushFocus(state FocusState) {
m.blurCurrentModel()
m.focusStack = append(m.focusStack, state)
m.focusCurrentModel()
}

func (m *Model) popFocus(reset bool) Model {
m.blurCurrentModel()
if reset {
m.resetCurrentModel()
}

if len(m.focusStack) > 1 {
m.focusStack = m.focusStack[:len(m.focusStack)-1]
}

m.focusCurrentModel()
if reset {
m.resetCurrentModel()
}

return *m
}

func (m *Model) focusCurrentModel() {
switch m.currentFocus() {
case FocusReposTable:
m.reposTable.Focus()
case FocusReposFilter:
m.reposFilter.Focus()
case FocusHelpPopup:
break
}
}

func (m *Model) blurCurrentModel() {
switch m.currentFocus() {
case FocusReposTable:
m.reposTable.Blur()
case FocusReposFilter:
m.reposFilter.Blur()
case FocusHelpPopup:
break
}
}

func (m *Model) resetCurrentModel() {
switch m.currentFocus() {
case FocusReposTable:
m.reposTable.Filter("")
case FocusReposFilter:
m.reposFilter.SetValue("")
case FocusHelpPopup:
break
}
}

func (m *Model) keybindings() []common.Keybinding {
switch m.currentFocus() {
case FocusReposTable:
return reposTableKeybindings
case FocusReposFilter:
return reposTableFilterKeybindings
case FocusHelpPopup:
return helpPopupKeybindings
}
return []common.Keybinding{}
}
Loading
Loading