commit
d4ddcc6e98
|
|
@ -5,13 +5,19 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/oug-t/difi/internal/config"
|
||||||
"github.com/oug-t/difi/internal/ui"
|
"github.com/oug-t/difi/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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 {
|
if _, err := p.Run(); err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error running difi: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -6,6 +6,7 @@ require (
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
|
||||||
4
go.sum
4
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/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 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
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=
|
||||||
|
|
|
||||||
49
internal/config/config.go
Normal file
49
internal/config/config.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
"github.com/oug-t/difi/internal/config"
|
||||||
"github.com/oug-t/difi/internal/git"
|
"github.com/oug-t/difi/internal/git"
|
||||||
"github.com/oug-t/difi/internal/tree"
|
"github.com/oug-t/difi/internal/tree"
|
||||||
)
|
)
|
||||||
|
|
@ -29,27 +30,26 @@ type Model struct {
|
||||||
fileTree list.Model
|
fileTree list.Model
|
||||||
diffViewport viewport.Model
|
diffViewport viewport.Model
|
||||||
|
|
||||||
// Data
|
|
||||||
selectedPath string
|
selectedPath string
|
||||||
currentBranch string
|
currentBranch string
|
||||||
repoName string
|
repoName string
|
||||||
|
|
||||||
// Diff State
|
|
||||||
diffContent string
|
diffContent string
|
||||||
diffLines []string
|
diffLines []string
|
||||||
diffCursor int
|
diffCursor int
|
||||||
|
|
||||||
// Input State for Vim Motions
|
|
||||||
inputBuffer string
|
inputBuffer string
|
||||||
|
|
||||||
// UI State
|
|
||||||
focus Focus
|
focus Focus
|
||||||
showHelp bool
|
showHelp bool
|
||||||
|
|
||||||
width, height int
|
width, height int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel() Model {
|
func NewModel(cfg config.Config) Model {
|
||||||
|
// Initialize styles with the loaded config
|
||||||
|
InitStyles(cfg)
|
||||||
|
|
||||||
files, _ := git.ListChangedFiles(TargetBranch)
|
files, _ := git.ListChangedFiles(TargetBranch)
|
||||||
items := tree.Build(files)
|
items := tree.Build(files)
|
||||||
|
|
||||||
|
|
@ -101,7 +101,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
// Flag to track if we manually handled navigation
|
|
||||||
keyHandled := false
|
keyHandled := false
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
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)
|
return m, git.OpenEditorCmd(m.selectedPath, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vim Motions
|
|
||||||
case "j", "down":
|
case "j", "down":
|
||||||
keyHandled = true // Mark as handled so we don't pass to list.Update()
|
keyHandled = true
|
||||||
count := m.getRepeatCount()
|
count := m.getRepeatCount()
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
if m.focus == FocusDiff {
|
if m.focus == FocusDiff {
|
||||||
|
|
@ -174,7 +172,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.inputBuffer = ""
|
m.inputBuffer = ""
|
||||||
|
|
||||||
case "k", "up":
|
case "k", "up":
|
||||||
keyHandled = true // Mark as handled
|
keyHandled = true
|
||||||
count := m.getRepeatCount()
|
count := m.getRepeatCount()
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
if m.focus == FocusDiff {
|
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 m.focus == FocusTree {
|
||||||
if !keyHandled {
|
if !keyHandled {
|
||||||
m.fileTree, cmd = m.fileTree.Update(msg)
|
m.fileTree, cmd = m.fileTree.Update(msg)
|
||||||
|
|
@ -251,6 +248,7 @@ func (m Model) View() string {
|
||||||
return "Loading..."
|
return "Loading..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. PANES
|
||||||
treeStyle := PaneStyle
|
treeStyle := PaneStyle
|
||||||
if m.focus == FocusTree {
|
if m.focus == FocusTree {
|
||||||
treeStyle = FocusedPaneStyle
|
treeStyle = FocusedPaneStyle
|
||||||
|
|
@ -270,13 +268,43 @@ func (m Model) View() string {
|
||||||
end = len(m.diffLines)
|
end = len(m.diffLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RENDER LOOP
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
line := m.diffLines[i]
|
line := m.diffLines[i]
|
||||||
|
|
||||||
// Relative Numbers
|
// --- LINE NUMBER LOGIC ---
|
||||||
distance := int(math.Abs(float64(i - m.diffCursor)))
|
var numStr string
|
||||||
relNum := fmt.Sprintf("%d", distance)
|
mode := CurrentConfig.UI.LineNumbers
|
||||||
lineNumStr := LineNumberStyle.Render(relNum)
|
|
||||||
|
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 {
|
if m.focus == FocusDiff && i == m.diffCursor {
|
||||||
line = SelectedItemStyle.Render(line)
|
line = SelectedItemStyle.Render(line)
|
||||||
|
|
@ -284,7 +312,7 @@ func (m Model) View() string {
|
||||||
line = " " + line
|
line = " " + line
|
||||||
}
|
}
|
||||||
|
|
||||||
renderedDiff.WriteString(lineNumStr + line + "\n")
|
renderedDiff.WriteString(lineNumRendered + line + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
diffView := DiffStyle.Copy().
|
diffView := DiffStyle.Copy().
|
||||||
|
|
@ -294,6 +322,7 @@ func (m Model) View() string {
|
||||||
|
|
||||||
mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView)
|
mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView)
|
||||||
|
|
||||||
|
// 2. BOTTOM AREA
|
||||||
repoSection := StatusKeyStyle.Render(" " + m.repoName)
|
repoSection := StatusKeyStyle.Render(" " + m.repoName)
|
||||||
divider := StatusDividerStyle.Render("│")
|
divider := StatusDividerStyle.Render("│")
|
||||||
|
|
||||||
|
|
@ -352,6 +381,7 @@ func (m Model) View() string {
|
||||||
return finalView
|
return finalView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Delegates (unchanged) --
|
||||||
type listDelegate struct{}
|
type listDelegate struct{}
|
||||||
|
|
||||||
func (d listDelegate) Height() int { return 1 }
|
func (d listDelegate) Height() int { return 1 }
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
package ui
|
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 (
|
var (
|
||||||
ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
|
ColorBorder lipgloss.AdaptiveColor
|
||||||
ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"}
|
ColorFocus lipgloss.AdaptiveColor
|
||||||
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"}
|
||||||
|
|
@ -12,31 +17,78 @@ var (
|
||||||
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"}
|
||||||
|
|
||||||
|
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().
|
PaneStyle = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder(), false, true, false, false).
|
Border(lipgloss.NormalBorder(), false, cfg.UI.ShowGuide, false, false).
|
||||||
BorderForeground(ColorBorder)
|
BorderForeground(ColorBorder)
|
||||||
|
|
||||||
FocusedPaneStyle = PaneStyle.Copy().
|
FocusedPaneStyle = PaneStyle.Copy().
|
||||||
BorderForeground(ColorFocus)
|
BorderForeground(ColorFocus)
|
||||||
|
|
||||||
|
// Diff and list item styles
|
||||||
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
||||||
ItemStyle = lipgloss.NewStyle().PaddingLeft(2)
|
ItemStyle = lipgloss.NewStyle().PaddingLeft(2)
|
||||||
|
|
||||||
SelectedItemStyle = lipgloss.NewStyle().
|
SelectedItemStyle = lipgloss.NewStyle().
|
||||||
PaddingLeft(1).
|
PaddingLeft(1).
|
||||||
Background(ColorCursorBg).
|
Background(ColorCursorBg).
|
||||||
Foreground(ColorText).
|
Foreground(ColorText).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Width(1000)
|
Width(1000)
|
||||||
|
|
||||||
|
// Line numbers
|
||||||
LineNumberStyle = lipgloss.NewStyle().
|
LineNumberStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#707070")). // Solid gray, easy to read
|
Foreground(lipgloss.Color(cfg.Colors.LineNumber)).
|
||||||
PaddingRight(1).
|
PaddingRight(1).
|
||||||
Width(4)
|
Width(4)
|
||||||
|
|
||||||
StatusBarStyle = lipgloss.NewStyle().Foreground(ColorBarFg).Background(ColorBarBg).Padding(0, 1)
|
// Status bar
|
||||||
StatusKeyStyle = lipgloss.NewStyle().Foreground(ColorText).Background(ColorBarBg).Bold(true).Padding(0, 1)
|
StatusBarStyle = lipgloss.NewStyle().
|
||||||
StatusDividerStyle = lipgloss.NewStyle().Foreground(ColorSubtle).Background(ColorBarBg).Padding(0, 0)
|
Foreground(ColorBarFg).
|
||||||
HelpTextStyle = lipgloss.NewStyle().Foreground(ColorSubtle).Padding(0, 1)
|
Background(ColorBarBg).
|
||||||
HelpDrawerStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false, false, false).BorderForeground(ColorBorder).Padding(1, 2)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user