From e8e2e2a6d92c5cefd4b0228d5cc06969e5ef010f Mon Sep 17 00:00:00 2001 From: Tommy Guo Date: Fri, 30 Jan 2026 22:38:34 -0500 Subject: [PATCH] chore: clean the project --- internal/config/config.go | 3 +- internal/tree/builder.go | 13 +---- internal/ui/model.go | 114 ++++++++++++++++++++++++++++++++------ internal/ui/styles.go | 21 +++---- 4 files changed, 110 insertions(+), 41 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index d087fd5..563cc55 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,10 +10,9 @@ type UIConfig struct { } func Load() Config { - // Default configuration return Config{ UI: UIConfig{ - LineNumbers: "relative", // Default to relative numbers (vim style) + LineNumbers: "hybrid", Theme: "default", }, } diff --git a/internal/tree/builder.go b/internal/tree/builder.go index 2bbd1bd..a22f294 100644 --- a/internal/tree/builder.go +++ b/internal/tree/builder.go @@ -9,7 +9,7 @@ import ( "github.com/charmbracelet/bubbles/list" ) -// TreeItem represents a file or folder in the UI list. +// TreeItem represents a file or folder type TreeItem struct { Path string FullPath string @@ -25,16 +25,12 @@ func (i TreeItem) Title() string { 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 { - // Initialize root root := &node{ children: make(map[string]*node), isDir: true, } - // 1. Build the raw tree structure for _, path := range paths { parts := strings.Split(path, "/") current := root @@ -53,14 +49,11 @@ func Build(paths []string) []list.Item { } } - // 2. Flatten to list items (Sorting happens here) var items []list.Item flatten(root, 0, &items) return items } -// -- Helpers -- - type node struct { name string fullPath string @@ -75,10 +68,8 @@ func flatten(n *node, depth int, items *[]list.Item) { keys = append(keys, k) } - // Sort: Directories first, then alphabetical sort.Slice(keys, func(i, j int) bool { a, b := n.children[keys[i]], n.children[keys[j]] - // Folders first if a.isDir && !b.isDir { return true } @@ -90,7 +81,6 @@ func flatten(n *node, depth int, items *[]list.Item) { for _, k := range keys { child := n.children[k] - // Add current node *items = append(*items, TreeItem{ Path: child.name, FullPath: child.fullPath, @@ -98,7 +88,6 @@ func flatten(n *node, depth int, items *[]list.Item) { Depth: depth, }) - // Recurse if directory if child.isDir { flatten(child, depth+1, items) } diff --git a/internal/ui/model.go b/internal/ui/model.go index 58e64eb..456af98 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -39,6 +39,7 @@ type Model struct { diffCursor int inputBuffer string + pendingZ bool focus Focus showHelp bool @@ -72,6 +73,7 @@ func NewModel(cfg config.Config, targetBranch string) Model { repoName: git.GetRepoName(), showHelp: false, inputBuffer: "", + pendingZ: false, } if len(items) > 0 { @@ -118,11 +120,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } - // If list is empty, ignore other keys if len(m.fileTree.Items()) == 0 { 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") { m.inputBuffer += msg.String() 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) } + // 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": keyHandled = true count := m.getRepeatCount() @@ -173,6 +250,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.focus == FocusDiff { if m.diffCursor < len(m.diffLines)-1 { m.diffCursor++ + // Scroll if hitting bottom edge if m.diffCursor >= m.diffViewport.YOffset+m.diffViewport.Height { 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.diffCursor > 0 { m.diffCursor-- + // Scroll if hitting top edge if m.diffCursor < m.diffViewport.YOffset { m.diffViewport.LineUp(1) } @@ -234,6 +313,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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() { reservedHeight := 1 if m.showHelp { @@ -265,12 +353,11 @@ func (m Model) View() string { return "Loading..." } - // EMPTY STATE CHECK if len(m.fileTree.Items()) == 0 { return m.viewEmptyState() } - // 1. PANES + // Panes treeStyle := PaneStyle if m.focus == FocusTree { treeStyle = FocusedPaneStyle @@ -335,7 +422,7 @@ func (m Model) View() string { mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView) - // 2. BOTTOM AREA + // Bottom area repoSection := StatusKeyStyle.Render(" " + m.repoName) divider := StatusDividerStyle.Render("│") @@ -366,12 +453,12 @@ func (m Model) View() string { HelpTextStyle.Render("→/l Right Panel"), ) col3 := lipgloss.JoinVertical(lipgloss.Left, - HelpTextStyle.Render("Tab Switch Panel"), - HelpTextStyle.Render("Num Motion Count"), + HelpTextStyle.Render("C-d/u Page Dn/Up"), + HelpTextStyle.Render("zz/zt Scroll View"), ) col4 := lipgloss.JoinVertical(lipgloss.Left, + HelpTextStyle.Render("H/M/L Move Cursor"), HelpTextStyle.Render("e Edit File"), - HelpTextStyle.Render("? Close Help"), ) helpDrawer := HelpDrawerStyle.Copy(). @@ -394,17 +481,13 @@ func (m Model) View() string { return finalView } -// viewEmptyState renders a "Landing Page" when there are no changes func (m Model) viewEmptyState() string { - // 1. Logo & Tagline logo := EmptyLogoStyle.Render("difi") desc := EmptyDescStyle.Render("A calm, focused way to review Git diffs.") - // 2. Status Message statusMsg := fmt.Sprintf("✓ No changes found against '%s'", m.targetBranch) status := EmptyStatusStyle.Render(statusMsg) - // 3. Usage Guide usageHeader := EmptyHeaderStyle.Render("Usage Patterns") cmd1 := lipgloss.NewStyle().Foreground(ColorText).Render("difi") @@ -423,7 +506,6 @@ func (m Model) viewEmptyState() string { lipgloss.JoinHorizontal(lipgloss.Left, cmd3, desc3), ) - // 4. Navigation Guide navHeader := EmptyHeaderStyle.Render("Navigation") key1 := lipgloss.NewStyle().Foreground(ColorText).Render("Tab") @@ -432,8 +514,8 @@ func (m Model) viewEmptyState() string { key2 := lipgloss.NewStyle().Foreground(ColorText).Render("j / k") keyDesc2 := EmptyCodeStyle.Render("Move cursor") - key3 := lipgloss.NewStyle().Foreground(ColorText).Render("?") - keyDesc3 := EmptyCodeStyle.Render("Toggle help") + key3 := lipgloss.NewStyle().Foreground(ColorText).Render("zz/zt") + keyDesc3 := EmptyCodeStyle.Render("Center/Top") navBlock := lipgloss.JoinVertical(lipgloss.Left, navHeader, @@ -442,10 +524,9 @@ func (m Model) viewEmptyState() string { lipgloss.JoinHorizontal(lipgloss.Left, key3, keyDesc3), ) - // Combine blocks guides := lipgloss.JoinHorizontal(lipgloss.Top, usageBlock, - lipgloss.NewStyle().Width(8).Render(""), // Spacer + lipgloss.NewStyle().Width(8).Render(""), navBlock, ) @@ -457,7 +538,6 @@ func (m Model) viewEmptyState() string { guides, ) - // Center vertically var verticalPad string if m.height > lipgloss.Height(content) { lines := (m.height - lipgloss.Height(content)) / 2 diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 9a4e562..0670bec 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -6,18 +6,18 @@ import ( ) var ( - // Global Config + // Config CurrentConfig config.Config - // -- THEME COLORS -- + // Theme colors ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"} ColorText = lipgloss.AdaptiveColor{Light: "#1F1F1F", Dark: "#F8F8F2"} ColorSubtle = lipgloss.AdaptiveColor{Light: "#A8A8A8", Dark: "#626262"} 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(). Border(lipgloss.NormalBorder(), false, true, false, false). BorderForeground(ColorBorder) @@ -28,7 +28,7 @@ var ( DiffStyle = lipgloss.NewStyle().Padding(0, 0) ItemStyle = lipgloss.NewStyle().PaddingLeft(2) - // -- LIST DELEGATE STYLES -- + // List styles SelectedItemStyle = lipgloss.NewStyle(). PaddingLeft(1). Background(ColorCursorBg). @@ -42,11 +42,11 @@ var ( Bold(true). PaddingLeft(1) - // -- ICON STYLES -- + // Icon styles FolderIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F7B96E", Dark: "#E5C07B"}) FileIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#969696", Dark: "#ABB2BF"}) - // -- DIFF VIEW STYLES -- + // Diff view styles LineNumberStyle = lipgloss.NewStyle(). Foreground(ColorSubtle). PaddingRight(1). @@ -56,10 +56,11 @@ var ( Background(ColorCursorBg). Width(1000) - // -- STATUS BAR STYLES -- + // Status bar colors ColorBarBg = lipgloss.AdaptiveColor{Light: "#F2F2F2", Dark: "#1F1F1F"} ColorBarFg = lipgloss.AdaptiveColor{Light: "#6E6E6E", Dark: "#9E9E9E"} + // Status bar styles StatusBarStyle = lipgloss.NewStyle(). Foreground(ColorBarFg). Background(ColorBarBg). @@ -76,7 +77,7 @@ var ( Background(ColorBarBg). Padding(0, 0) - // -- HELP STYLES -- + // Help styles HelpTextStyle = lipgloss.NewStyle(). Foreground(ColorSubtle). Padding(0, 1) @@ -86,7 +87,7 @@ var ( BorderForeground(ColorBorder). Padding(1, 2) - // -- EMPTY STATE / LANDING PAGE STYLES -- + // Empty/landing styles EmptyLogoStyle = lipgloss.NewStyle(). Foreground(ColorAccent). Bold(true).