From 7f68d13676ba4d9205827d8128c1dc5552ba3506 Mon Sep 17 00:00:00 2001 From: Tommy Guo Date: Fri, 30 Jan 2026 12:46:02 -0500 Subject: [PATCH] feat: config yaml support --- cmd/difi/main.go | 10 ++++- go.mod | 1 + go.sum | 4 ++ internal/config/config.go | 49 +++++++++++++++++++++ internal/ui/model.go | 60 ++++++++++++++++++------- internal/ui/styles.go | 92 ++++++++++++++++++++++++++++++--------- 6 files changed, 179 insertions(+), 37 deletions(-) create mode 100644 internal/config/config.go diff --git a/cmd/difi/main.go b/cmd/difi/main.go index 2c581fe..01c60dc 100644 --- a/cmd/difi/main.go +++ b/cmd/difi/main.go @@ -5,13 +5,19 @@ import ( "os" tea "github.com/charmbracelet/bubbletea" + "github.com/oug-t/difi/internal/config" "github.com/oug-t/difi/internal/ui" ) func main() { - p := tea.NewProgram(ui.NewModel(), tea.WithAltScreen()) + // Load config (defaults used if file missing) + cfg := config.Load() + + // Pass config to model + p := tea.NewProgram(ui.NewModel(cfg), tea.WithAltScreen()) + if _, err := p.Run(); err != nil { - fmt.Printf("Error: %v\n", err) + fmt.Printf("Error running difi: %v", err) os.Exit(1) } } diff --git a/go.mod b/go.mod index 9e26151..ca84197 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index a5aaf28..e9ed947 100644 --- a/go.sum +++ b/go.sum @@ -53,3 +53,7 @@ golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..51023db --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,49 @@ +package config + +import ( + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Colors struct { + Border string `yaml:"border"` + Focus string `yaml:"focus"` + LineNumber string `yaml:"line_number"` + } `yaml:"colors"` + UI struct { + LineNumbers string `yaml:"line_numbers"` // "absolute", "relative", "hybrid", "hidden" + ShowGuide bool `yaml:"show_guide"` // The vertical separation line + } `yaml:"ui"` +} + +func DefaultConfig() Config { + var c Config + c.Colors.Border = "#D9DCCF" + c.Colors.Focus = "#000000" // Default neutral focus + c.Colors.LineNumber = "#808080" + c.UI.LineNumbers = "hybrid" + c.UI.ShowGuide = true + return c +} + +func Load() Config { + cfg := DefaultConfig() + + home, err := os.UserHomeDir() + if err != nil { + return cfg + } + + configPath := filepath.Join(home, ".config", "difi", "config.yml") + data, err := os.ReadFile(configPath) + if err != nil { + return cfg + } + + // Parse YAML + _ = yaml.Unmarshal(data, &cfg) + return cfg +} diff --git a/internal/ui/model.go b/internal/ui/model.go index 448b907..324d3b7 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -12,6 +12,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/oug-t/difi/internal/config" "github.com/oug-t/difi/internal/git" "github.com/oug-t/difi/internal/tree" ) @@ -29,27 +30,26 @@ type Model struct { fileTree list.Model diffViewport viewport.Model - // Data selectedPath string currentBranch string repoName string - // Diff State diffContent string diffLines []string diffCursor int - // Input State for Vim Motions inputBuffer string - // UI State focus Focus showHelp bool width, height int } -func NewModel() Model { +func NewModel(cfg config.Config) Model { + // Initialize styles with the loaded config + InitStyles(cfg) + files, _ := git.ListChangedFiles(TargetBranch) items := tree.Build(files) @@ -101,7 +101,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd - // Flag to track if we manually handled navigation keyHandled := false switch msg := msg.(type) { @@ -155,9 +154,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, git.OpenEditorCmd(m.selectedPath, line) } - // Vim Motions case "j", "down": - keyHandled = true // Mark as handled so we don't pass to list.Update() + keyHandled = true count := m.getRepeatCount() for i := 0; i < count; i++ { if m.focus == FocusDiff { @@ -174,7 +172,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.inputBuffer = "" case "k", "up": - keyHandled = true // Mark as handled + keyHandled = true count := m.getRepeatCount() for i := 0; i < count; i++ { if m.focus == FocusDiff { @@ -195,7 +193,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - // Update Components if m.focus == FocusTree { if !keyHandled { m.fileTree, cmd = m.fileTree.Update(msg) @@ -251,6 +248,7 @@ func (m Model) View() string { return "Loading..." } + // 1. PANES treeStyle := PaneStyle if m.focus == FocusTree { treeStyle = FocusedPaneStyle @@ -270,13 +268,43 @@ func (m Model) View() string { end = len(m.diffLines) } + // RENDER LOOP for i := start; i < end; i++ { line := m.diffLines[i] - // Relative Numbers - distance := int(math.Abs(float64(i - m.diffCursor))) - relNum := fmt.Sprintf("%d", distance) - lineNumStr := LineNumberStyle.Render(relNum) + // --- LINE NUMBER LOGIC --- + var numStr string + mode := CurrentConfig.UI.LineNumbers + + if mode == "hidden" { + numStr = "" + } else { + // Is this the cursor line? + isCursor := (i == m.diffCursor) + + if isCursor && mode == "hybrid" { + // HYBRID: Show Real File Line Number + realLine := git.CalculateFileLine(m.diffContent, m.diffCursor) + numStr = fmt.Sprintf("%d", realLine) + } else if isCursor && mode == "relative" { + numStr = "0" + } else if mode == "absolute" { + // Note: Calculating absolute for every line is expensive, + // usually absolute view shows Diff Line Index or File Line. + // For simple 'absolute' view, we often show viewport index + 1 + numStr = fmt.Sprintf("%d", i+1) + } else { + // Default / Hybrid-non-cursor: Show Relative Distance + dist := int(math.Abs(float64(i - m.diffCursor))) + numStr = fmt.Sprintf("%d", dist) + } + } + + lineNumRendered := "" + if numStr != "" { + lineNumRendered = LineNumberStyle.Render(numStr) + } + // ------------------------- if m.focus == FocusDiff && i == m.diffCursor { line = SelectedItemStyle.Render(line) @@ -284,7 +312,7 @@ func (m Model) View() string { line = " " + line } - renderedDiff.WriteString(lineNumStr + line + "\n") + renderedDiff.WriteString(lineNumRendered + line + "\n") } diffView := DiffStyle.Copy(). @@ -294,6 +322,7 @@ func (m Model) View() string { mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView) + // 2. BOTTOM AREA repoSection := StatusKeyStyle.Render(" " + m.repoName) divider := StatusDividerStyle.Render("│") @@ -352,6 +381,7 @@ func (m Model) View() string { return finalView } +// -- Delegates (unchanged) -- type listDelegate struct{} func (d listDelegate) Height() int { return 1 } diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 9b5a017..8b04fec 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -1,10 +1,15 @@ package ui -import "github.com/charmbracelet/lipgloss" +import ( + "github.com/charmbracelet/lipgloss" + "github.com/oug-t/difi/internal/config" +) +// Global UI colors and styles. +// Initialized once at startup via InitStyles. var ( - ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} - ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"} + ColorBorder lipgloss.AdaptiveColor + ColorFocus lipgloss.AdaptiveColor ColorText = lipgloss.AdaptiveColor{Light: "#1F1F1F", Dark: "#F8F8F2"} ColorSubtle = lipgloss.AdaptiveColor{Light: "#A8A8A8", Dark: "#626262"} ColorCursorBg = lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#3E3E3E"} @@ -12,31 +17,78 @@ var ( ColorBarBg = lipgloss.AdaptiveColor{Light: "#F2F2F2", Dark: "#1F1F1F"} ColorBarFg = lipgloss.AdaptiveColor{Light: "#6E6E6E", Dark: "#9E9E9E"} + PaneStyle lipgloss.Style + FocusedPaneStyle lipgloss.Style + DiffStyle lipgloss.Style + ItemStyle lipgloss.Style + SelectedItemStyle lipgloss.Style + LineNumberStyle lipgloss.Style + StatusBarStyle lipgloss.Style + StatusKeyStyle lipgloss.Style + StatusDividerStyle lipgloss.Style + HelpTextStyle lipgloss.Style + HelpDrawerStyle lipgloss.Style + + CurrentConfig config.Config +) + +// InitStyles initializes global styles based on the provided config. +// This should be called once during application startup. +func InitStyles(cfg config.Config) { + CurrentConfig = cfg + + // Colors derived from user config + ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: cfg.Colors.Border} + ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: cfg.Colors.Focus} + + // Pane styles PaneStyle = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder(), false, true, false, false). - BorderForeground(ColorBorder) + Border(lipgloss.NormalBorder(), false, cfg.UI.ShowGuide, false, false). + BorderForeground(ColorBorder) FocusedPaneStyle = PaneStyle.Copy(). - BorderForeground(ColorFocus) + BorderForeground(ColorFocus) + // Diff and list item styles DiffStyle = lipgloss.NewStyle().Padding(0, 0) ItemStyle = lipgloss.NewStyle().PaddingLeft(2) SelectedItemStyle = lipgloss.NewStyle(). - PaddingLeft(1). - Background(ColorCursorBg). - Foreground(ColorText). - Bold(true). - Width(1000) + PaddingLeft(1). + Background(ColorCursorBg). + Foreground(ColorText). + Bold(true). + Width(1000) + // Line numbers LineNumberStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#707070")). // Solid gray, easy to read - PaddingRight(1). - Width(4) + Foreground(lipgloss.Color(cfg.Colors.LineNumber)). + PaddingRight(1). + Width(4) - StatusBarStyle = lipgloss.NewStyle().Foreground(ColorBarFg).Background(ColorBarBg).Padding(0, 1) - StatusKeyStyle = lipgloss.NewStyle().Foreground(ColorText).Background(ColorBarBg).Bold(true).Padding(0, 1) - StatusDividerStyle = lipgloss.NewStyle().Foreground(ColorSubtle).Background(ColorBarBg).Padding(0, 0) - HelpTextStyle = lipgloss.NewStyle().Foreground(ColorSubtle).Padding(0, 1) - HelpDrawerStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false, false, false).BorderForeground(ColorBorder).Padding(1, 2) -) + // Status bar + StatusBarStyle = lipgloss.NewStyle(). + Foreground(ColorBarFg). + Background(ColorBarBg). + Padding(0, 1) + + StatusKeyStyle = lipgloss.NewStyle(). + Foreground(ColorText). + Background(ColorBarBg). + Bold(true). + Padding(0, 1) + + StatusDividerStyle = lipgloss.NewStyle(). + Foreground(ColorSubtle). + Background(ColorBarBg) + + // Help drawer + HelpTextStyle = lipgloss.NewStyle(). + Foreground(ColorSubtle). + Padding(0, 1) + + HelpDrawerStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), true, false, false, false). + BorderForeground(ColorBorder). + Padding(1, 2) +}