Skip to content

Commit f24d6b2

Browse files
committed
update
1 parent 2ad4575 commit f24d6b2

14 files changed

Lines changed: 593 additions & 85 deletions

File tree

genius

1.53 MB
Binary file not shown.

go.mod

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@ module git-genius
22

33
go 1.25.0
44

5-
require golang.org/x/term v0.42.0
5+
require (
6+
github.com/charmbracelet/bubbletea v1.3.10
7+
github.com/charmbracelet/lipgloss v1.1.0
8+
golang.org/x/term v0.42.0
9+
)
610

7-
require golang.org/x/sys v0.43.0 // indirect
11+
require (
12+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
13+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
14+
github.com/charmbracelet/x/ansi v0.10.1 // indirect
15+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
16+
github.com/charmbracelet/x/term v0.2.1 // indirect
17+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
18+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
19+
github.com/mattn/go-isatty v0.0.20 // indirect
20+
github.com/mattn/go-localereader v0.0.1 // indirect
21+
github.com/mattn/go-runewidth v0.0.16 // indirect
22+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
23+
github.com/muesli/cancelreader v0.2.2 // indirect
24+
github.com/muesli/termenv v0.16.0 // indirect
25+
github.com/rivo/uniseg v0.4.7 // indirect
26+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
27+
golang.org/x/sys v0.43.0 // indirect
28+
golang.org/x/text v0.3.8 // indirect
29+
)

go.sum

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,45 @@
1+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
2+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
3+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
4+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
5+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
6+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
7+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
8+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
9+
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
10+
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
11+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
12+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
13+
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
14+
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
15+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
16+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
17+
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
18+
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
19+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
20+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
21+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
22+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
23+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
24+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
25+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
26+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
27+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
28+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
29+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
30+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
31+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
32+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
33+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
34+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
35+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
36+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
37+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
38+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
140
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
241
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
342
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
443
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
44+
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
45+
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=

internal/gitops/pull_ops.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ func pullCore(rebase bool) bool {
4141
args = append(args, cfg.Remote, branch)
4242

4343
stateBefore := InspectRepoState()
44-
if err := system.RunGitWithRemote(cfg.Remote, args...); err != nil {
44+
err := func() error {
45+
stop := ui.Spinner("Pulling changes")
46+
defer stop()
47+
return system.RunGitWithRemote(cfg.Remote, args...)
48+
}()
49+
if err != nil {
4550
ui.Error("Pull failed")
4651
if rebase {
4752
ui.Info("If there are conflicts, resolve them and run 'git rebase --continue'")

internal/gitops/push_ops.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ func pushCore(msg string, force bool) bool {
6767
args = append(args, "--force-with-lease")
6868
}
6969

70-
stderr, err := system.RunGitWithRemoteBuffered(cfg.Remote, args...)
70+
stderr, err := func() (string, error) {
71+
stop := ui.Spinner("Pushing to remote")
72+
defer stop()
73+
return system.RunGitWithRemoteBuffered(cfg.Remote, args...)
74+
}()
7175
if err != nil {
7276
ui.Error("Push failed")
7377
printPushFailureSummary(cfg.Remote, branch, stderr)

internal/gitops/repo_state.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type RepoState struct {
1919
HasAheadBehind bool
2020
FirstRun bool
2121
NeedsFirstPush bool
22+
HasConflicts bool
2223
}
2324

2425
func InspectRepoState() RepoState {
@@ -32,6 +33,11 @@ func InspectRepoState() RepoState {
3233
FirstRun: !config.HasProjectConfig(cfg.GetWorkDir()) && !config.HasHistoryForWorkDir(cfg.GetWorkDir()),
3334
}
3435

36+
// Check for conflicts
37+
if out, err := system.GitOutput("diff", "--name-only", "--diff-filter=U"); err == nil && out != "" {
38+
state.HasConflicts = true
39+
}
40+
3541
if state.Branch == "-" || state.Branch == "" {
3642
state.Branch = cfg.Branch
3743
}

internal/gitops/timeline.go

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,16 @@ package gitops
33
import (
44
"fmt"
55
"strings"
6+
"time"
67

78
"git-genius/internal/system"
89
"git-genius/internal/ui"
910
)
1011

11-
// ShowActivityTimeline displays a simple ASCII representation of commit activity.
12-
func ShowActivityTimeline() bool {
13-
if !system.EnsureGitRepo() {
14-
return false
15-
}
16-
17-
ui.BoxHeader("7-Day Activity Timeline")
18-
19-
// Get commit counts per day for the last 7 days
20-
// Using --since="7 days ago" and formatting by date
21-
out, err := system.GitOutput("log", "--since='7 days ago'", "--format=%ad", "--date=short")
12+
func GetRecentActivityData(days int) []int {
13+
out, err := system.GitOutput("log", fmt.Sprintf("--since='%d days ago'", days), "--format=%ad", "--date=short")
2214
if err != nil {
23-
ui.Error("Failed to fetch activity data")
24-
return false
15+
return nil
2516
}
2617

2718
counts := make(map[string]int)
@@ -32,19 +23,41 @@ func ShowActivityTimeline() bool {
3223
}
3324
}
3425

35-
if len(lines) == 0 || lines[0] == "" {
36-
ui.Info("No activity in the last 7 days.")
37-
return true
26+
// We want a sorted list for the last X days, including zeros
27+
res := make([]int, days)
28+
now := time.Now()
29+
for i := 0; i < days; i++ {
30+
date := now.AddDate(0, 0, -i).Format("2006-01-02")
31+
res[days-1-i] = counts[date]
3832
}
33+
return res
34+
}
3935

36+
// ShowActivityTimeline displays a simple ASCII representation of commit activity.
37+
func ShowActivityTimeline() bool {
38+
if !system.EnsureGitRepo() {
39+
return false
40+
}
41+
42+
ui.BoxHeader("7-Day Activity Timeline")
43+
data := GetRecentActivityData(7)
44+
if data == nil {
45+
ui.Error("Failed to fetch activity data")
46+
return false
47+
}
48+
49+
ui.Info("Trend: " + ui.CyberSparkline(data))
4050
ui.Info("Commits per day (ASCII Chart):")
4151
fmt.Println()
4252

43-
// Note: In a real app we'd iterate over actual dates,
44-
// but for this CLI we'll just show days with activity for simplicity.
45-
for date, count := range counts {
46-
bar := strings.Repeat("█", count)
47-
fmt.Printf(ui.Cyan+" %10s | "+ui.Yellow+"%-20s"+ui.Reset+" (%d)\n", date, bar, count)
53+
now := time.Now()
54+
for i := 0; i < 7; i++ {
55+
date := now.AddDate(0, 0, -i).Format("2006-01-02")
56+
count := data[7-1-i]
57+
if count > 0 {
58+
bar := strings.Repeat("█", count)
59+
fmt.Printf(ui.Cyan+" %10s | "+ui.Yellow+"%-20s"+ui.Reset+" (%d)\n", date, bar, count)
60+
}
4861
}
4962
fmt.Println()
5063

internal/menu/context.go

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,43 +24,63 @@ func headerTitle(appVersion string, gitAvailable bool) string {
2424
return title
2525
}
2626

27-
func showContext(gitAvailable bool) {
27+
func GetStatusLines(gitAvailable bool) []string {
2828
cfg := config.Load()
2929
projectDir := cfg.GetWorkDir()
3030

31-
width := 42
32-
fmt.Println(ui.Cyan + "┏" + strings.Repeat("━", width-2) + "┓" + ui.Reset)
33-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+ui.Bold+"PROJECT: "+ui.Reset+"%-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-13, filepath.Base(projectDir))
34-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+"Path : %-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-11, projectDir)
35-
31+
var info []string
32+
branch := "-"
33+
remote := system.CurrentRemote()
34+
sync := "0 ahead / 0 behind"
35+
3636
if gitAvailable {
3737
state := gitops.InspectRepoState()
38-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+"Branch : %-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-11, state.Branch)
39-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+"Remote : %-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-11, system.CurrentRemote())
38+
branch = state.Branch
4039
if state.HasAheadBehind {
41-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+"Sync : %-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-11, state.AheadBehindSummary())
40+
sync = state.AheadBehindSummary()
4241
}
43-
} else {
44-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+"Mode : %-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-11, "Limited (Git unavailable)")
4542
}
4643

47-
if cfg.Owner != "" && cfg.Repo != "" {
48-
repoURL := "github.com/" + cfg.Owner + "/" + cfg.Repo
49-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+"GitHub : %-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-11, repoURL)
50-
}
44+
info = append(info, fmt.Sprintf("Project : %s", filepath.Base(projectDir)))
45+
info = append(info, fmt.Sprintf("Branch : %s", branch))
46+
info = append(info, fmt.Sprintf("Remote : %s", remote))
47+
info = append(info, fmt.Sprintf("Sync : %s", sync))
48+
49+
// Dynamic Health Check & Assistant
50+
statusText := "HEALTHY"
51+
nextAction := "Ready for work"
5152

52-
suggestions := config.HistorySuggestions(projectDir)
53-
if len(suggestions) > 0 {
54-
fmt.Println(ui.Cyan + "┣" + strings.Repeat("━", width-2) + "┫" + ui.Reset)
55-
for _, s := range suggestions {
56-
fmt.Printf(ui.Cyan+"┃ "+ui.Reset+ui.Yellow+"󱐌 "+ui.Reset+"%-*s"+ui.Cyan+" ┃\n"+ui.Reset, width-13, s)
53+
if !gitAvailable {
54+
statusText = "LIMITED"
55+
nextAction = "Install Git"
56+
} else {
57+
state := gitops.InspectRepoState()
58+
if state.HasConflicts {
59+
statusText = "CONFLICT"
60+
nextAction = "Fix Merge Conflicts"
61+
} else if state.WorkingTreeDirty {
62+
nextAction = "Commit your changes"
63+
} else if state.Ahead > 0 {
64+
nextAction = fmt.Sprintf("Push %d commits", state.Ahead)
65+
} else if state.Behind > 0 {
66+
nextAction = "Pull latest changes"
67+
} else if state.NeedsFirstPush {
68+
nextAction = "Publish repository"
5769
}
5870
}
59-
fmt.Println(ui.Cyan + "┗" + strings.Repeat("━", width-2) + "┛" + ui.Reset)
60-
}
6171

62-
func normalizeChoice(s string) string {
63-
return strings.ToLower(strings.TrimSpace(s))
72+
info = append(info, "")
73+
info = append(info, fmt.Sprintf("Status : %s", statusText))
74+
info = append(info, fmt.Sprintf("Next : %s", nextAction))
75+
76+
data := gitops.GetRecentActivityData(7)
77+
if data != nil {
78+
spark := ui.CyberSparkline(data)
79+
info = append(info, "")
80+
info = append(info, fmt.Sprintf("Trend : %s", spark))
81+
}
82+
83+
return info
6484
}
6585

6686
func track(section, action string, fn func() bool) {

internal/menu/main_menu.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"git-genius/internal/ui"
88
)
99

10-
func mainMenu() bool {
11-
ui.BoxMenu("Main Menu", []string{
10+
func getMainMenuOptions() []string {
11+
return []string{
1212
"1) Daily Git Operations",
1313
"2) Visual History (Graph)",
1414
"3) Activity Timeline (Chart)",
@@ -19,9 +19,12 @@ func mainMenu() bool {
1919
"8) Help / About",
2020
"9) Exit",
2121
"",
22-
"Tip: press 'h' for help",
23-
})
22+
"Quick Keys: [p]ush, [u]pdate, [s]tatus",
23+
}
24+
}
2425

26+
func mainMenu() bool {
27+
// Interaction logic remains here, but printing is handled by the main loop
2528
switch ui.MenuChoice() {
2629
case "1":
2730
dailyMenu()
@@ -45,25 +48,35 @@ func mainMenu() bool {
4548
case "9", "q":
4649
ui.Info("Goodbye")
4750
return false
51+
case "p":
52+
track("daily", "push", func() bool { return gitops.Push("") })
53+
ui.Pause()
54+
case "u":
55+
track("daily", "pull", gitops.Pull)
56+
ui.Pause()
57+
case "s":
58+
track("daily", "status", gitops.Status)
59+
ui.Pause()
4860
default:
4961
ui.Error("Invalid option")
5062
ui.Pause()
5163
}
5264

5365
return true
5466
}
55-
56-
func limitedMenu() bool {
57-
ui.BoxMenu("Limited Mode", []string{
67+
func getLimitedMenuOptions() []string {
68+
return []string{
5869
"1) Setup / Reconfigure",
5970
"2) Switch Project",
6071
"3) Doctor (health check)",
6172
"4) Help / About",
6273
"5) Exit",
6374
"",
6475
"Tip: install Git to unlock more",
65-
})
76+
}
77+
}
6678

79+
func limitedMenu() bool {
6780
switch ui.MenuChoice() {
6881
case "1":
6982
track("tools", "setup_reconfigure", setup.Run)

0 commit comments

Comments
 (0)