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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ git-scope -h

## ✨ Features

* **📁 Workspace Switch** — Switch root directories without quitting (`w`). Supports `~`, relative paths, and **symlinks**.
* **🔍 Fuzzy Search** — Find any repo by name, path, or branch (`/`).
* **🛡️ Dirty Filter** — Instantly show only repos with uncommitted changes (`f`).
* **🚀 Editor Jump** — Open the selected repo in VSCode, Neovim, Vim, or Helix (`Enter`).
Expand All @@ -81,13 +82,15 @@ git-scope -h
* **🌿 Contribution Graph** — GitHub-style local heatmap for your activity (`g`).
* **💾 Disk Usage** — Visualize `.git` vs `node_modules` size (`d`).
* **⏰ Timeline** — View recent activity across all projects (`t`).
* **🔗 Symlink Support** — Symlinked directories resolve transparently (great for Codespaces/devcontainers).

-----

## ⌨️ Keyboard Shortcuts

| Key | Action |
| :--- | :--- |
| `w` | **Switch Workspace** (with Tab completion) |
| `/` | **Search** repositories (Fuzzy) |
| `f` | **Filter** (Cycle: All / Dirty / Clean) |
| `s` | Cycle **Sort** Mode |
Expand Down Expand Up @@ -142,6 +145,8 @@ I built `git-scope` to solve the **"Multi-Repo Blindness"** problem. It gives me

## 🗺️ Roadmap

- [x] In-app workspace switching with Tab completion
- [x] Symlink resolution for devcontainers/Codespaces
- [ ] Background file watcher (real-time updates)
- [ ] Quick actions (bulk pull/fetch)
- [ ] Repo grouping (Service / Team / Stack)
Expand Down
115 changes: 115 additions & 0 deletions internal/nudge/nudge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package nudge

import (
"encoding/json"
"os"
"path/filepath"
)

// Version is the current app version - used to track per-version nudge
const Version = "1.3.0"

// GitHubRepoURL is the URL to open when user presses S
const GitHubRepoURL = "https://github.com/Bharath-code/git-scope"

// NudgeState represents the persistent state of the star nudge
type NudgeState struct {
SeenVersion string `json:"seenVersion"`
Dismissed bool `json:"dismissed"`
Completed bool `json:"completed"`
}

// getNudgePath returns the path to the nudge state file
func getNudgePath() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".cache", "git-scope", "nudge.json")
}

// loadState loads the nudge state from disk
func loadState() *NudgeState {
path := getNudgePath()
if path == "" {
return &NudgeState{}
}

data, err := os.ReadFile(path)
if err != nil {
return &NudgeState{}
}

var state NudgeState
if err := json.Unmarshal(data, &state); err != nil {
return &NudgeState{}
}

return &state
}

// saveState saves the nudge state to disk
func saveState(state *NudgeState) error {
path := getNudgePath()
if path == "" {
return nil
}

// Ensure directory exists
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}

data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}

return os.WriteFile(path, data, 0644)
}

// ShouldShowNudge checks if the star nudge should be shown
// Returns true only if:
// - Not already seen for this version
// - Not dismissed
// - Not completed (user already starred)
func ShouldShowNudge() bool {
state := loadState()

// Already seen for this version
if state.SeenVersion == Version {
return false
}

// User already completed (pressed S)
if state.Completed {
return false
}

return true
}

// MarkShown marks the nudge as shown for the current version
func MarkShown() {
state := loadState()
state.SeenVersion = Version
state.Dismissed = false
_ = saveState(state)
}

// MarkDismissed marks the nudge as dismissed (any key pressed)
func MarkDismissed() {
state := loadState()
state.SeenVersion = Version
state.Dismissed = true
_ = saveState(state)
}

// MarkCompleted marks the nudge as completed (S pressed, GitHub opened)
func MarkCompleted() {
state := loadState()
state.SeenVersion = Version
state.Completed = true
_ = saveState(state)
}
29 changes: 22 additions & 7 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
StateReady
StateError
StateSearching
StateWorkspaceSwitch
)

// SortMode represents different sorting options
Expand Down Expand Up @@ -66,6 +67,13 @@ type Model struct {
grassData *stats.ContributionData
diskData *stats.DiskUsageData
timelineData *stats.TimelineData
// Workspace switch state
workspaceInput textinput.Model
workspaceError string
activeWorkspace string
// Star nudge state
showStarNudge bool
nudgeShownThisSession bool
}

// NewModel creates a new TUI model
Expand Down Expand Up @@ -115,19 +123,26 @@ func NewModel(cfg *config.Config) Model {
ti.CharLimit = 50
ti.Width = 30

// Create text input for workspace switch
wi := textinput.New()
wi.Placeholder = "~/projects or /path/to/dir"
wi.CharLimit = 200
wi.Width = 40

// Create spinner with Braille pattern
sp := spinner.New()
sp.Spinner = spinner.Dot
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#7C3AED"))

return Model{
cfg: cfg,
table: t,
textInput: ti,
spinner: sp,
state: StateLoading,
sortMode: SortByDirty,
filterMode: FilterAll,
cfg: cfg,
table: t,
textInput: ti,
workspaceInput: wi,
spinner: sp,
state: StateLoading,
sortMode: SortByDirty,
filterMode: FilterAll,
}
}

Expand Down
Loading