Skip to content

Commit 3ec221f

Browse files
feat(tui, ci): enhance UX and add static Linux builds
1 parent 22544fa commit 3ec221f

File tree

13 files changed

+270
-19
lines changed

13 files changed

+270
-19
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ jobs:
3232
env:
3333
GOOS: ${{ matrix.os }}
3434
GOARCH: ${{ matrix.arch }}
35+
CGO_ENABLED: 0
3536
run: |
3637
mkdir -p dist
3738
BIN_NAME="froggit-${{ matrix.os }}-${{ matrix.arch }}"
3839
[[ "${{ matrix.os }}" == "windows" ]] && BIN_NAME="$BIN_NAME.exe"
39-
go build -o "$BIN_NAME" .
40+
go build -ldflags="-s -w" -o "$BIN_NAME" .
4041
zip -j "dist/${{ matrix.os }}-${{ matrix.arch }}.zip" "$BIN_NAME"
4142
4243
- name: Upload artifact for release

internal/git/actions.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,28 @@ func HasCommitsToush() (bool, error) {
229229
return NewGitClient("").HasCommitsToush()
230230
}
231231

232+
func GetFileDiff(filename string, staged bool) (string, error) {
233+
return NewGitClient("").GetFileDiff(filename, staged)
234+
}
235+
236+
func (g *GitClient) GetFileDiff(filename string, staged bool) (string, error) {
237+
var args []string
238+
if staged {
239+
args = []string{"diff", "--cached", "--", filename}
240+
} else {
241+
args = []string{"diff", "--", filename}
242+
}
243+
output, err := g.runGitCommand(args...)
244+
if err != nil {
245+
return "", fmt.Errorf("failed to get diff for %s: %w", filename, err)
246+
}
247+
result := strings.TrimSpace(string(output))
248+
if result == "" {
249+
return "No changes to display", nil
250+
}
251+
return result, nil
252+
}
253+
232254
func GetStagedDiff() (string, error) {
233255
return NewGitClient("").GetStagedDiff()
234256
}

internal/tui/controls/controls.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,18 +152,23 @@ func NewFileViewControls(staged bool, hasFiles bool, advancedMode bool) *Control
152152
if !advancedMode {
153153
if hasFiles {
154154
cs.Add("space", "stage/unstage", "files")
155+
cs.Add("d", "diff", "files")
155156
cs.Add("x", "discard changes", "files")
156157
}
157158
if staged {
158159
cs.Add("c", "commit", "files")
159160
}
160161
cs.Add("a", "stage all", "files")
162+
if staged {
163+
cs.Add("u", "unstage all", "files")
164+
}
161165
cs.Add("r", "refresh", "files")
162166
cs.Add("f", "fetch", "git")
163167
cs.Add("l", "pull", "git")
164168
cs.Add("p", "push", "git")
165169
cs.Add("b", "branches", "nav")
166170
cs.Add("m", "remotes", "nav")
171+
cs.Add("A", "advanced", "mode")
167172
cs.Add("?", "help", "general")
168173
} else {
169174
cs.Add("L", "log graph", "advanced")
@@ -316,6 +321,13 @@ func NewHelpViewControls() *ControlSet {
316321
return cs
317322
}
318323

324+
func NewDiffViewControls() *ControlSet {
325+
cs := NewControlSet()
326+
cs.Add("↑/↓", "scroll", "navigation")
327+
cs.Add("esc", "back", "navigation")
328+
return cs
329+
}
330+
319331
func NewLogGraphViewControls() *ControlSet {
320332
cs := NewControlSet()
321333
cs.Add("↑/↓", "navigate", "navigation")

internal/tui/model/model.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
RebaseView
2828
StashView
2929
StashMessageView
30+
DiffView
3031
)
3132

3233
type Model struct {
@@ -75,12 +76,16 @@ type Model struct {
7576
FileViewOffset int
7677
FileViewHeight int
7778

79+
DiffLines []string
80+
DiffViewOffset int
81+
7882
IsGeneratingAI bool
7983
CopilotAvailable bool
8084

8185
DialogType string
8286
DialogTarget string
8387

88+
MessageID int
8489
AwaitingPush bool
8590

8691
QuickStartOptions []string

internal/tui/render.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ func Render(m model.Model, cfg config.Config) string {
6262
sb.WriteString(view.RenderStashView(m))
6363
case model.StashMessageView:
6464
sb.WriteString(view.RenderStashMessageView(m))
65+
case model.DiffView:
66+
sb.WriteString(view.RenderDiffView(m))
6567
}
6668

6769
if m.Message != "" {

internal/tui/update/async/async.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ func PerformRemoteChangesCheck(branch string) tea.Cmd {
9393
}
9494
}
9595

96+
// PerformMessageClear returns a Cmd that clears the message after 3 seconds.
97+
func PerformMessageClear(messageID int) tea.Cmd {
98+
return tea.Tick(time.Second*3, func(t time.Time) tea.Msg {
99+
return messages.MessageClearMsg{MessageID: messageID}
100+
})
101+
}
102+
96103
// PerformAICommitGeneration generates a commit message using Copilot
97104
func PerformAICommitGeneration() tea.Cmd {
98105
return func() tea.Msg {

internal/tui/update/messages/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ type SwitchBranchMsg struct {
66
NextAction string
77
SourceBranch string
88
}
9+
10+
type MessageClearMsg struct {
11+
MessageID int
12+
}

internal/tui/update/update.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,28 @@ func Update(m model.Model, cfg config.Config, msg tea.Msg) (model.Model, tea.Cmd
163163
return HandleLogGraphKey(m, msg)
164164
}
165165

166+
if m.CurrentView == model.DiffView {
167+
switch msg.String() {
168+
case "esc":
169+
m.CurrentView = model.FileView
170+
m.DiffLines = nil
171+
m.DiffViewOffset = 0
172+
return m, nil
173+
case "up", "k":
174+
if m.DiffViewOffset > 0 {
175+
m.DiffViewOffset--
176+
}
177+
return m, nil
178+
case "down", "j":
179+
if m.DiffViewOffset < len(m.DiffLines)-1 {
180+
m.DiffViewOffset++
181+
}
182+
return m, nil
183+
default:
184+
return m, nil
185+
}
186+
}
187+
166188
if m.CurrentView == model.MergeView {
167189
return handlers.HandleMergeView(m, msg)
168190
}
@@ -602,6 +624,44 @@ func Update(m model.Model, cfg config.Config, msg tea.Msg) (model.Model, tea.Cmd
602624
m.Message = "⚠ No files to stage"
603625
m.MessageType = "warning"
604626
}
627+
case "u":
628+
hasStaged := false
629+
for _, f := range m.Files {
630+
if f.Staged {
631+
hasStaged = true
632+
break
633+
}
634+
}
635+
if hasStaged {
636+
currentFileName := ""
637+
if m.Cursor < len(m.Files) {
638+
currentFileName = m.Files[m.Cursor].Name
639+
}
640+
for i := range m.Files {
641+
if m.Files[i].Staged {
642+
m.Files[i].Staged = false
643+
git.Reset(m.Files[i].Name)
644+
}
645+
}
646+
m.Message = "✓ All files unstaged"
647+
m.MessageType = "success"
648+
files, _ := git.GetModifiedFiles()
649+
m.Files = files
650+
if currentFileName != "" {
651+
for i, file := range m.Files {
652+
if file.Name == currentFileName {
653+
m.Cursor = i
654+
break
655+
}
656+
}
657+
}
658+
if m.Cursor >= len(m.Files) && len(m.Files) > 0 {
659+
m.Cursor = len(m.Files) - 1
660+
}
661+
} else {
662+
m.Message = "⚠ No staged files to unstage"
663+
m.MessageType = "warning"
664+
}
605665
case "c":
606666
ok := false
607667
for _, f := range m.Files {
@@ -680,6 +740,20 @@ func Update(m model.Model, cfg config.Config, msg tea.Msg) (model.Model, tea.Cmd
680740
m.Message = "⚠ No file selected or no files available"
681741
m.MessageType = "warning"
682742
}
743+
case "d":
744+
if len(m.Files) > 0 && m.Cursor < len(m.Files) {
745+
file := m.Files[m.Cursor]
746+
diff, err := git.GetFileDiff(file.Name, file.Staged)
747+
if err != nil {
748+
m.Message = fmt.Sprintf("✗ Error getting diff: %s", err)
749+
m.MessageType = "error"
750+
} else {
751+
m.DiffLines = strings.Split(diff, "\n")
752+
m.DiffViewOffset = 0
753+
m.CurrentView = model.DiffView
754+
}
755+
return m, nil
756+
}
683757
case "?":
684758
if m.CurrentView == model.FileView {
685759
m.CurrentView = model.HelpView
@@ -753,6 +827,19 @@ func Update(m model.Model, cfg config.Config, msg tea.Msg) (model.Model, tea.Cmd
753827
if msg.Err == nil {
754828
m.HasRemoteChanges = msg.HasChanges
755829
}
830+
831+
case messages.MessageClearMsg:
832+
if msg.MessageID == m.MessageID {
833+
m.Message = ""
834+
m.MessageType = ""
835+
}
836+
return m, nil
837+
}
838+
839+
// Auto-clear messages after 3 seconds
840+
if m.Message != "" {
841+
m.MessageID++
842+
cmds = append(cmds, async.PerformMessageClear(m.MessageID))
756843
}
757844

758845
if len(cmds) > 0 {

internal/tui/views/commit_view.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package view
22

33
import (
4+
"fmt"
45
"strings"
56

67
"froggit/internal/tui/controls"
@@ -16,7 +17,18 @@ func RenderCommitView(m model.Model) string {
1617
if m.IsGeneratingAI {
1718
s.WriteString(styles.InputStyle.Render(m.SpinnerFrames[m.SpinnerIndex]+" Generating...") + "\n\n")
1819
} else {
19-
s.WriteString(styles.InputStyle.Render(m.CommitMsg+"_") + "\n\n")
20+
s.WriteString(styles.InputStyle.Render(m.CommitMsg+"_") + "\n")
21+
22+
charCount := len(m.CommitMsg)
23+
countText := fmt.Sprintf(" %d/72", charCount)
24+
if charCount > 72 {
25+
s.WriteString(styles.ErrorStyle.Render(countText))
26+
} else if charCount > 50 {
27+
s.WriteString(styles.WarningStyle.Render(countText))
28+
} else {
29+
s.WriteString(styles.HelpStyle.Render(countText))
30+
}
31+
s.WriteString("\n")
2032
}
2133

2234
controlsWidget := controls.NewCommitViewControls(m.CopilotAvailable, m.IsGeneratingAI)

internal/tui/views/diff_view.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package view
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"froggit/internal/tui/controls"
8+
"froggit/internal/tui/model"
9+
"froggit/internal/tui/styles"
10+
11+
"github.com/charmbracelet/lipgloss"
12+
)
13+
14+
var (
15+
diffAddStyle = lipgloss.NewStyle().
16+
Foreground(lipgloss.Color(styles.Green))
17+
diffRemoveStyle = lipgloss.NewStyle().
18+
Foreground(lipgloss.Color(styles.Red))
19+
diffHunkStyle = lipgloss.NewStyle().
20+
Foreground(lipgloss.Color(styles.Cyan))
21+
)
22+
23+
func RenderDiffView(m model.Model) string {
24+
const viewport = 20
25+
26+
var sb strings.Builder
27+
28+
sb.WriteString(styles.HeaderStyle.Render(" Diff Preview:") + "\n\n")
29+
30+
total := len(m.DiffLines)
31+
if total == 0 {
32+
sb.WriteString(styles.HelpStyle.Render("No diff to display\n"))
33+
} else {
34+
start := m.DiffViewOffset
35+
if start > total-viewport {
36+
start = max(0, total-viewport)
37+
}
38+
end := min(total, start+viewport)
39+
40+
for i := start; i < end; i++ {
41+
line := m.DiffLines[i]
42+
if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
43+
sb.WriteString(diffAddStyle.Render(line) + "\n")
44+
} else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
45+
sb.WriteString(diffRemoveStyle.Render(line) + "\n")
46+
} else if strings.HasPrefix(line, "@@") {
47+
sb.WriteString(diffHunkStyle.Render(line) + "\n")
48+
} else {
49+
sb.WriteString(styles.NormalStyle.Render(line) + "\n")
50+
}
51+
}
52+
}
53+
54+
position := fmt.Sprintf("%d/%d", min(m.DiffViewOffset+1, total), total)
55+
sb.WriteString("\n" + styles.HelpStyle.Render(position))
56+
57+
controlsWidget := controls.NewDiffViewControls()
58+
sb.WriteString("\n" + controlsWidget.Render())
59+
60+
return sb.String()
61+
}

0 commit comments

Comments
 (0)