Merge pull request #8 from oug-t/chore/clean

chore: clean the project
This commit is contained in:
Tommy Guo 2026-01-30 22:38:55 -05:00 committed by GitHub
commit f318e7f880
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 110 additions and 41 deletions

View File

@ -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",
},
}

View File

@ -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)
}

View File

@ -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

View File

@ -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).