chore: clean the project

This commit is contained in:
Tommy Guo 2026-01-30 22:38:34 -05:00
parent 67b51de266
commit e8e2e2a6d9
4 changed files with 110 additions and 41 deletions

View File

@ -10,10 +10,9 @@ type UIConfig struct {
} }
func Load() Config { func Load() Config {
// Default configuration
return Config{ return Config{
UI: UIConfig{ UI: UIConfig{
LineNumbers: "relative", // Default to relative numbers (vim style) LineNumbers: "hybrid",
Theme: "default", Theme: "default",
}, },
} }

View File

@ -9,7 +9,7 @@ import (
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
) )
// TreeItem represents a file or folder in the UI list. // TreeItem represents a file or folder
type TreeItem struct { type TreeItem struct {
Path string Path string
FullPath string FullPath string
@ -25,16 +25,12 @@ func (i TreeItem) Title() string {
return fmt.Sprintf("%s%s %s", indent, icon, i.Path) return fmt.Sprintf("%s%s %s", indent, icon, i.Path)
} }
// Build converts a list of file paths into a sorted tree list.
// Compaction is disabled to ensure tree stability.
func Build(paths []string) []list.Item { func Build(paths []string) []list.Item {
// Initialize root
root := &node{ root := &node{
children: make(map[string]*node), children: make(map[string]*node),
isDir: true, isDir: true,
} }
// 1. Build the raw tree structure
for _, path := range paths { for _, path := range paths {
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
current := root current := root
@ -53,14 +49,11 @@ func Build(paths []string) []list.Item {
} }
} }
// 2. Flatten to list items (Sorting happens here)
var items []list.Item var items []list.Item
flatten(root, 0, &items) flatten(root, 0, &items)
return items return items
} }
// -- Helpers --
type node struct { type node struct {
name string name string
fullPath string fullPath string
@ -75,10 +68,8 @@ func flatten(n *node, depth int, items *[]list.Item) {
keys = append(keys, k) keys = append(keys, k)
} }
// Sort: Directories first, then alphabetical
sort.Slice(keys, func(i, j int) bool { sort.Slice(keys, func(i, j int) bool {
a, b := n.children[keys[i]], n.children[keys[j]] a, b := n.children[keys[i]], n.children[keys[j]]
// Folders first
if a.isDir && !b.isDir { if a.isDir && !b.isDir {
return true return true
} }
@ -90,7 +81,6 @@ func flatten(n *node, depth int, items *[]list.Item) {
for _, k := range keys { for _, k := range keys {
child := n.children[k] child := n.children[k]
// Add current node
*items = append(*items, TreeItem{ *items = append(*items, TreeItem{
Path: child.name, Path: child.name,
FullPath: child.fullPath, FullPath: child.fullPath,
@ -98,7 +88,6 @@ func flatten(n *node, depth int, items *[]list.Item) {
Depth: depth, Depth: depth,
}) })
// Recurse if directory
if child.isDir { if child.isDir {
flatten(child, depth+1, items) flatten(child, depth+1, items)
} }

View File

@ -39,6 +39,7 @@ type Model struct {
diffCursor int diffCursor int
inputBuffer string inputBuffer string
pendingZ bool
focus Focus focus Focus
showHelp bool showHelp bool
@ -72,6 +73,7 @@ func NewModel(cfg config.Config, targetBranch string) Model {
repoName: git.GetRepoName(), repoName: git.GetRepoName(),
showHelp: false, showHelp: false,
inputBuffer: "", inputBuffer: "",
pendingZ: false,
} }
if len(items) > 0 { if len(items) > 0 {
@ -118,11 +120,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit return m, tea.Quit
} }
// If list is empty, ignore other keys
if len(m.fileTree.Items()) == 0 { if len(m.fileTree.Items()) == 0 {
return m, nil return m, nil
} }
// Handle z-prefix commands (zz, zt, zb)
if m.pendingZ {
m.pendingZ = false
if m.focus == FocusDiff {
switch msg.String() {
case "z", ".":
m.centerDiffCursor()
case "t":
m.diffViewport.SetYOffset(m.diffCursor)
case "b":
offset := m.diffCursor - m.diffViewport.Height + 1
if offset < 0 {
offset = 0
}
m.diffViewport.SetYOffset(offset)
}
}
return m, nil
}
if len(msg.String()) == 1 && strings.ContainsAny(msg.String(), "0123456789") { if len(msg.String()) == 1 && strings.ContainsAny(msg.String(), "0123456789") {
m.inputBuffer += msg.String() m.inputBuffer += msg.String()
return m, nil return m, nil
@ -166,6 +187,62 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, git.OpenEditorCmd(m.selectedPath, line) return m, git.OpenEditorCmd(m.selectedPath, line)
} }
// Viewport trigger
case "z":
if m.focus == FocusDiff {
m.pendingZ = true
return m, nil
}
// Cursor screen positioning (H, M, L)
case "H":
if m.focus == FocusDiff {
m.diffCursor = m.diffViewport.YOffset
if m.diffCursor >= len(m.diffLines) {
m.diffCursor = len(m.diffLines) - 1
}
}
case "M":
if m.focus == FocusDiff {
half := m.diffViewport.Height / 2
m.diffCursor = m.diffViewport.YOffset + half
if m.diffCursor >= len(m.diffLines) {
m.diffCursor = len(m.diffLines) - 1
}
}
case "L":
if m.focus == FocusDiff {
m.diffCursor = m.diffViewport.YOffset + m.diffViewport.Height - 1
if m.diffCursor >= len(m.diffLines) {
m.diffCursor = len(m.diffLines) - 1
}
}
// Page navigation
case "ctrl+d":
if m.focus == FocusDiff {
halfScreen := m.diffViewport.Height / 2
m.diffCursor += halfScreen
if m.diffCursor >= len(m.diffLines) {
m.diffCursor = len(m.diffLines) - 1
}
m.centerDiffCursor()
}
m.inputBuffer = ""
case "ctrl+u":
if m.focus == FocusDiff {
halfScreen := m.diffViewport.Height / 2
m.diffCursor -= halfScreen
if m.diffCursor < 0 {
m.diffCursor = 0
}
m.centerDiffCursor()
}
m.inputBuffer = ""
case "j", "down": case "j", "down":
keyHandled = true keyHandled = true
count := m.getRepeatCount() count := m.getRepeatCount()
@ -173,6 +250,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.focus == FocusDiff { if m.focus == FocusDiff {
if m.diffCursor < len(m.diffLines)-1 { if m.diffCursor < len(m.diffLines)-1 {
m.diffCursor++ m.diffCursor++
// Scroll if hitting bottom edge
if m.diffCursor >= m.diffViewport.YOffset+m.diffViewport.Height { if m.diffCursor >= m.diffViewport.YOffset+m.diffViewport.Height {
m.diffViewport.LineDown(1) m.diffViewport.LineDown(1)
} }
@ -190,6 +268,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.focus == FocusDiff { if m.focus == FocusDiff {
if m.diffCursor > 0 { if m.diffCursor > 0 {
m.diffCursor-- m.diffCursor--
// Scroll if hitting top edge
if m.diffCursor < m.diffViewport.YOffset { if m.diffCursor < m.diffViewport.YOffset {
m.diffViewport.LineUp(1) m.diffViewport.LineUp(1)
} }
@ -234,6 +313,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
func (m *Model) centerDiffCursor() {
halfScreen := m.diffViewport.Height / 2
targetOffset := m.diffCursor - halfScreen
if targetOffset < 0 {
targetOffset = 0
}
m.diffViewport.SetYOffset(targetOffset)
}
func (m *Model) updateSizes() { func (m *Model) updateSizes() {
reservedHeight := 1 reservedHeight := 1
if m.showHelp { if m.showHelp {
@ -265,12 +353,11 @@ func (m Model) View() string {
return "Loading..." return "Loading..."
} }
// EMPTY STATE CHECK
if len(m.fileTree.Items()) == 0 { if len(m.fileTree.Items()) == 0 {
return m.viewEmptyState() return m.viewEmptyState()
} }
// 1. PANES // Panes
treeStyle := PaneStyle treeStyle := PaneStyle
if m.focus == FocusTree { if m.focus == FocusTree {
treeStyle = FocusedPaneStyle treeStyle = FocusedPaneStyle
@ -335,7 +422,7 @@ func (m Model) View() string {
mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView) mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView)
// 2. BOTTOM AREA // Bottom area
repoSection := StatusKeyStyle.Render(" " + m.repoName) repoSection := StatusKeyStyle.Render(" " + m.repoName)
divider := StatusDividerStyle.Render("│") divider := StatusDividerStyle.Render("│")
@ -366,12 +453,12 @@ func (m Model) View() string {
HelpTextStyle.Render("→/l Right Panel"), HelpTextStyle.Render("→/l Right Panel"),
) )
col3 := lipgloss.JoinVertical(lipgloss.Left, col3 := lipgloss.JoinVertical(lipgloss.Left,
HelpTextStyle.Render("Tab Switch Panel"), HelpTextStyle.Render("C-d/u Page Dn/Up"),
HelpTextStyle.Render("Num Motion Count"), HelpTextStyle.Render("zz/zt Scroll View"),
) )
col4 := lipgloss.JoinVertical(lipgloss.Left, col4 := lipgloss.JoinVertical(lipgloss.Left,
HelpTextStyle.Render("H/M/L Move Cursor"),
HelpTextStyle.Render("e Edit File"), HelpTextStyle.Render("e Edit File"),
HelpTextStyle.Render("? Close Help"),
) )
helpDrawer := HelpDrawerStyle.Copy(). helpDrawer := HelpDrawerStyle.Copy().
@ -394,17 +481,13 @@ func (m Model) View() string {
return finalView return finalView
} }
// viewEmptyState renders a "Landing Page" when there are no changes
func (m Model) viewEmptyState() string { func (m Model) viewEmptyState() string {
// 1. Logo & Tagline
logo := EmptyLogoStyle.Render("difi") logo := EmptyLogoStyle.Render("difi")
desc := EmptyDescStyle.Render("A calm, focused way to review Git diffs.") desc := EmptyDescStyle.Render("A calm, focused way to review Git diffs.")
// 2. Status Message
statusMsg := fmt.Sprintf("✓ No changes found against '%s'", m.targetBranch) statusMsg := fmt.Sprintf("✓ No changes found against '%s'", m.targetBranch)
status := EmptyStatusStyle.Render(statusMsg) status := EmptyStatusStyle.Render(statusMsg)
// 3. Usage Guide
usageHeader := EmptyHeaderStyle.Render("Usage Patterns") usageHeader := EmptyHeaderStyle.Render("Usage Patterns")
cmd1 := lipgloss.NewStyle().Foreground(ColorText).Render("difi") cmd1 := lipgloss.NewStyle().Foreground(ColorText).Render("difi")
@ -423,7 +506,6 @@ func (m Model) viewEmptyState() string {
lipgloss.JoinHorizontal(lipgloss.Left, cmd3, desc3), lipgloss.JoinHorizontal(lipgloss.Left, cmd3, desc3),
) )
// 4. Navigation Guide
navHeader := EmptyHeaderStyle.Render("Navigation") navHeader := EmptyHeaderStyle.Render("Navigation")
key1 := lipgloss.NewStyle().Foreground(ColorText).Render("Tab") key1 := lipgloss.NewStyle().Foreground(ColorText).Render("Tab")
@ -432,8 +514,8 @@ func (m Model) viewEmptyState() string {
key2 := lipgloss.NewStyle().Foreground(ColorText).Render("j / k") key2 := lipgloss.NewStyle().Foreground(ColorText).Render("j / k")
keyDesc2 := EmptyCodeStyle.Render("Move cursor") keyDesc2 := EmptyCodeStyle.Render("Move cursor")
key3 := lipgloss.NewStyle().Foreground(ColorText).Render("?") key3 := lipgloss.NewStyle().Foreground(ColorText).Render("zz/zt")
keyDesc3 := EmptyCodeStyle.Render("Toggle help") keyDesc3 := EmptyCodeStyle.Render("Center/Top")
navBlock := lipgloss.JoinVertical(lipgloss.Left, navBlock := lipgloss.JoinVertical(lipgloss.Left,
navHeader, navHeader,
@ -442,10 +524,9 @@ func (m Model) viewEmptyState() string {
lipgloss.JoinHorizontal(lipgloss.Left, key3, keyDesc3), lipgloss.JoinHorizontal(lipgloss.Left, key3, keyDesc3),
) )
// Combine blocks
guides := lipgloss.JoinHorizontal(lipgloss.Top, guides := lipgloss.JoinHorizontal(lipgloss.Top,
usageBlock, usageBlock,
lipgloss.NewStyle().Width(8).Render(""), // Spacer lipgloss.NewStyle().Width(8).Render(""),
navBlock, navBlock,
) )
@ -457,7 +538,6 @@ func (m Model) viewEmptyState() string {
guides, guides,
) )
// Center vertically
var verticalPad string var verticalPad string
if m.height > lipgloss.Height(content) { if m.height > lipgloss.Height(content) {
lines := (m.height - lipgloss.Height(content)) / 2 lines := (m.height - lipgloss.Height(content)) / 2

View File

@ -6,18 +6,18 @@ import (
) )
var ( var (
// Global Config // Config
CurrentConfig config.Config CurrentConfig config.Config
// -- THEME COLORS -- // Theme colors
ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"} ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"}
ColorText = lipgloss.AdaptiveColor{Light: "#1F1F1F", Dark: "#F8F8F2"} ColorText = lipgloss.AdaptiveColor{Light: "#1F1F1F", Dark: "#F8F8F2"}
ColorSubtle = lipgloss.AdaptiveColor{Light: "#A8A8A8", Dark: "#626262"} ColorSubtle = lipgloss.AdaptiveColor{Light: "#A8A8A8", Dark: "#626262"}
ColorCursorBg = lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#3E3E3E"} ColorCursorBg = lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#3E3E3E"}
ColorAccent = lipgloss.AdaptiveColor{Light: "#00ADD8", Dark: "#00ADD8"} // Go Blue ColorAccent = lipgloss.AdaptiveColor{Light: "#00ADD8", Dark: "#00ADD8"} // Go blue
// -- PANE STYLES -- // Pane styles
PaneStyle = lipgloss.NewStyle(). PaneStyle = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, true, false, false). Border(lipgloss.NormalBorder(), false, true, false, false).
BorderForeground(ColorBorder) BorderForeground(ColorBorder)
@ -28,7 +28,7 @@ var (
DiffStyle = lipgloss.NewStyle().Padding(0, 0) DiffStyle = lipgloss.NewStyle().Padding(0, 0)
ItemStyle = lipgloss.NewStyle().PaddingLeft(2) ItemStyle = lipgloss.NewStyle().PaddingLeft(2)
// -- LIST DELEGATE STYLES -- // List styles
SelectedItemStyle = lipgloss.NewStyle(). SelectedItemStyle = lipgloss.NewStyle().
PaddingLeft(1). PaddingLeft(1).
Background(ColorCursorBg). Background(ColorCursorBg).
@ -42,11 +42,11 @@ var (
Bold(true). Bold(true).
PaddingLeft(1) PaddingLeft(1)
// -- ICON STYLES -- // Icon styles
FolderIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F7B96E", Dark: "#E5C07B"}) FolderIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F7B96E", Dark: "#E5C07B"})
FileIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#969696", Dark: "#ABB2BF"}) FileIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#969696", Dark: "#ABB2BF"})
// -- DIFF VIEW STYLES -- // Diff view styles
LineNumberStyle = lipgloss.NewStyle(). LineNumberStyle = lipgloss.NewStyle().
Foreground(ColorSubtle). Foreground(ColorSubtle).
PaddingRight(1). PaddingRight(1).
@ -56,10 +56,11 @@ var (
Background(ColorCursorBg). Background(ColorCursorBg).
Width(1000) Width(1000)
// -- STATUS BAR STYLES -- // Status bar colors
ColorBarBg = lipgloss.AdaptiveColor{Light: "#F2F2F2", Dark: "#1F1F1F"} ColorBarBg = lipgloss.AdaptiveColor{Light: "#F2F2F2", Dark: "#1F1F1F"}
ColorBarFg = lipgloss.AdaptiveColor{Light: "#6E6E6E", Dark: "#9E9E9E"} ColorBarFg = lipgloss.AdaptiveColor{Light: "#6E6E6E", Dark: "#9E9E9E"}
// Status bar styles
StatusBarStyle = lipgloss.NewStyle(). StatusBarStyle = lipgloss.NewStyle().
Foreground(ColorBarFg). Foreground(ColorBarFg).
Background(ColorBarBg). Background(ColorBarBg).
@ -76,7 +77,7 @@ var (
Background(ColorBarBg). Background(ColorBarBg).
Padding(0, 0) Padding(0, 0)
// -- HELP STYLES -- // Help styles
HelpTextStyle = lipgloss.NewStyle(). HelpTextStyle = lipgloss.NewStyle().
Foreground(ColorSubtle). Foreground(ColorSubtle).
Padding(0, 1) Padding(0, 1)
@ -86,7 +87,7 @@ var (
BorderForeground(ColorBorder). BorderForeground(ColorBorder).
Padding(1, 2) Padding(1, 2)
// -- EMPTY STATE / LANDING PAGE STYLES -- // Empty/landing styles
EmptyLogoStyle = lipgloss.NewStyle(). EmptyLogoStyle = lipgloss.NewStyle().
Foreground(ColorAccent). Foreground(ColorAccent).
Bold(true). Bold(true).