Merge pull request #6 from oug-t/feat/empty-state
feat: add contents for empty-state
This commit is contained in:
commit
54842591f5
27
README.md
27
README.md
|
|
@ -6,7 +6,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>The pixel-perfect terminal diff viewer.</strong><br />
|
<strong>A calm, focused way to review Git diffs.</strong><br />
|
||||||
Review code with clarity. Polish before you push.
|
Review code with clarity. Polish before you push.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -14,14 +14,13 @@
|
||||||
<img src="https://via.placeholder.com/800x450.png?text=Showcase+Your+UI+Here" alt="difi demo" width="100%" />
|
<img src="https://via.placeholder.com/800x450.png?text=Showcase+Your+UI+Here" alt="difi demo" width="100%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## Why difi?
|
## Why difi?
|
||||||
|
|
||||||
- ⚡️ **Blazing Fast** — Built in Go. Starts instantly.
|
- ⚡️ **Blazing Fast** — Built in Go. Starts instantly.
|
||||||
- 🎨 **Semantic UI** — Split-pane layout with syntax highlighting and Nerd Font icons.
|
- 🎨 **Semantic UI** — Split-pane layout with syntax highlighting and Nerd Font icons.
|
||||||
- 🧠 **Context Aware** — Opens your editor (nvim/vim) at the exact line you are reviewing.
|
- 🧠 **Context Aware** — Opens your editor (nvim/vim) at the exact line you are reviewing.
|
||||||
- ⌨️ **Vim Native** — Navigate with `h j k l`. Zero mouse required.
|
- ⌨️ **Vim Native** — Navigate with `h j k l`. Zero mouse required.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Homebrew (macOS & Linux)
|
### Homebrew (macOS & Linux)
|
||||||
|
|
@ -44,8 +43,10 @@ go install github.com/oug-t/difi/cmd/difi@latest
|
||||||
- Download the binary from Releases and add it to your $PATH.
|
- Download the binary from Releases and add it to your $PATH.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
- Run difi in any Git repository.
|
- Run difi in any Git repository.
|
||||||
- By default, it compares your current branch against main.
|
- By default, it compares your current branch against main.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd my-project
|
cd my-project
|
||||||
difi
|
difi
|
||||||
|
|
@ -53,14 +54,14 @@ difi
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|-----|--------|
|
| ------------- | -------------------------------------------- |
|
||||||
| `Tab` | Toggle focus between File Tree and Diff View |
|
| `Tab` | Toggle focus between File Tree and Diff View |
|
||||||
| `j / k` | Move cursor down / up |
|
| `j / k` | Move cursor down / up |
|
||||||
| `h / l` | Focus Left (Tree) / Focus Right (Diff) |
|
| `h / l` | Focus Left (Tree) / Focus Right (Diff) |
|
||||||
| `e` / `Enter` | Edit file (opens editor at selected line) |
|
| `e` / `Enter` | Edit file (opens editor at selected line) |
|
||||||
| `?` | Toggle help drawer |
|
| `?` | Toggle help drawer |
|
||||||
| `q` | Quit |
|
| `q` | Quit |
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
@ -72,5 +73,7 @@ git clone https://github.com/oug-t/difi
|
||||||
cd difi
|
cd difi
|
||||||
go run cmd/difi/main.go
|
go run cmd/difi/main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
<p align="center"> Made with ❤️ by <a href="https://github.com/oug-t">oug-t</a> </p>
|
|
||||||
|
<p align="center"> Made with ❤️ by <a href="https://github.com/oug-t">oug-t</a> </p>
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,20 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Colors struct {
|
UI UIConfig
|
||||||
Border string `yaml:"border"`
|
|
||||||
Focus string `yaml:"focus"`
|
|
||||||
LineNumber string `yaml:"line_number"`
|
|
||||||
DiffSelectionBg string `yaml:"diff_selection_bg"` // New config
|
|
||||||
} `yaml:"colors"`
|
|
||||||
UI struct {
|
|
||||||
LineNumbers string `yaml:"line_numbers"`
|
|
||||||
ShowGuide bool `yaml:"show_guide"`
|
|
||||||
} `yaml:"ui"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
type UIConfig struct {
|
||||||
var c Config
|
LineNumbers string // "relative", "absolute", "hybrid", "hidden"
|
||||||
c.Colors.Border = "#D9DCCF"
|
Theme string
|
||||||
c.Colors.Focus = "#6e7781"
|
|
||||||
c.Colors.LineNumber = "#808080"
|
|
||||||
|
|
||||||
// Default: "Neutral Light Transparent Blue"
|
|
||||||
// Dark Mode: Deep subtle blue-grey | Light Mode: Very faint blue
|
|
||||||
// We only set one default here, but AdaptiveColor handles the split in styles.go
|
|
||||||
c.Colors.DiffSelectionBg = "" // Empty means use internal defaults
|
|
||||||
|
|
||||||
c.UI.LineNumbers = "hybrid"
|
|
||||||
c.UI.ShowGuide = true
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
cfg := DefaultConfig()
|
// Default configuration
|
||||||
home, err := os.UserHomeDir()
|
return Config{
|
||||||
if err != nil {
|
UI: UIConfig{
|
||||||
return cfg
|
LineNumbers: "relative", // Default to relative numbers (vim style)
|
||||||
|
Theme: "default",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath := filepath.Join(home, ".config", "difi", "config.yml")
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = yaml.Unmarshal(data, &cfg)
|
|
||||||
return cfg
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,9 @@ package ui
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/oug-t/difi/internal/tree"
|
"github.com/oug-t/difi/internal/tree"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -19,95 +16,25 @@ type TreeDelegate struct {
|
||||||
func (d TreeDelegate) Height() int { return 1 }
|
func (d TreeDelegate) Height() int { return 1 }
|
||||||
func (d TreeDelegate) Spacing() int { return 0 }
|
func (d TreeDelegate) Spacing() int { return 0 }
|
||||||
func (d TreeDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
|
func (d TreeDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
|
||||||
|
|
||||||
func (d TreeDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
|
func (d TreeDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
|
||||||
i, ok := item.(tree.TreeItem)
|
i, ok := item.(tree.TreeItem)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Setup Indentation
|
title := i.Title()
|
||||||
indentSize := i.Depth * 2
|
|
||||||
indent := strings.Repeat(" ", indentSize)
|
|
||||||
|
|
||||||
// 2. Get Icon and Raw Name
|
|
||||||
iconStr, iconStyle := getIconInfo(i.Path, i.IsDir)
|
|
||||||
|
|
||||||
// 3. Truncation (Safety)
|
|
||||||
availableWidth := m.Width() - indentSize - 4
|
|
||||||
displayName := i.Path
|
|
||||||
if availableWidth > 0 && len(displayName) > availableWidth {
|
|
||||||
displayName = displayName[:max(0, availableWidth-1)] + "…"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Render Logic ("Oil" Block Cursor)
|
|
||||||
var row string
|
|
||||||
isSelected := index == m.Index()
|
|
||||||
|
|
||||||
if isSelected && d.Focused {
|
|
||||||
// -- SELECTED STATE (Oil Style) --
|
|
||||||
// We do NOT use iconStyle here. We want the icon to inherit the
|
|
||||||
// selection text color so the background block is unbroken.
|
|
||||||
// Content: Icon + Space + Name
|
|
||||||
content := fmt.Sprintf("%s %s", iconStr, displayName)
|
|
||||||
|
|
||||||
// Apply the solid block style to the whole content
|
|
||||||
renderedContent := SelectedBlockStyle.Render(content)
|
|
||||||
|
|
||||||
// Combine: Indent (unhighlighted) + Block (highlighted)
|
|
||||||
row = fmt.Sprintf("%s%s", indent, renderedContent)
|
|
||||||
|
|
||||||
|
// If this item is selected
|
||||||
|
if index == m.Index() {
|
||||||
|
if d.Focused {
|
||||||
|
// Render the whole line (including indent) with the selection background
|
||||||
|
fmt.Fprint(w, SelectedBlockStyle.Render(title))
|
||||||
|
} else {
|
||||||
|
// Dimmed selection if focus is on the other panel
|
||||||
|
fmt.Fprint(w, SelectedBlockStyle.Copy().Foreground(ColorSubtle).Render(title))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// -- NORMAL / INACTIVE STATE --
|
// Normal Item (No icons added, just the text)
|
||||||
// Render icon with its specific color
|
fmt.Fprint(w, ItemStyle.Render(title))
|
||||||
renderedIcon := iconStyle.Render(iconStr)
|
|
||||||
|
|
||||||
// Combine
|
|
||||||
row = fmt.Sprintf("%s%s %s", indent, renderedIcon, displayName)
|
|
||||||
|
|
||||||
// Apply generic padding/style
|
|
||||||
row = ItemStyle.Render(row)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprint(w, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Returns raw icon string and its preferred style
|
|
||||||
func getIconInfo(name string, isDir bool) (string, lipgloss.Style) {
|
|
||||||
if isDir {
|
|
||||||
return "", FolderIconStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(name)
|
|
||||||
icon := ""
|
|
||||||
|
|
||||||
switch strings.ToLower(ext) {
|
|
||||||
case ".go":
|
|
||||||
icon = ""
|
|
||||||
case ".js", ".ts", ".tsx", ".jsx":
|
|
||||||
icon = ""
|
|
||||||
case ".md":
|
|
||||||
icon = ""
|
|
||||||
case ".json", ".yml", ".yaml", ".toml":
|
|
||||||
icon = ""
|
|
||||||
case ".css", ".scss":
|
|
||||||
icon = ""
|
|
||||||
case ".html":
|
|
||||||
icon = ""
|
|
||||||
case ".git":
|
|
||||||
icon = ""
|
|
||||||
case ".dockerfile":
|
|
||||||
icon = ""
|
|
||||||
case ".svelte":
|
|
||||||
icon = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon, FileIconStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
func max(a, b int) int {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ import (
|
||||||
"github.com/oug-t/difi/internal/tree"
|
"github.com/oug-t/difi/internal/tree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// REMOVED: const TargetBranch = "main" (Now dynamic)
|
|
||||||
|
|
||||||
type Focus int
|
type Focus int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -33,7 +31,7 @@ type Model struct {
|
||||||
|
|
||||||
selectedPath string
|
selectedPath string
|
||||||
currentBranch string
|
currentBranch string
|
||||||
targetBranch string // Added field for dynamic target
|
targetBranch string
|
||||||
repoName string
|
repoName string
|
||||||
|
|
||||||
diffContent string
|
diffContent string
|
||||||
|
|
@ -48,11 +46,9 @@ type Model struct {
|
||||||
width, height int
|
width, height int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated Signature: Accepts targetBranch string
|
|
||||||
func NewModel(cfg config.Config, targetBranch string) Model {
|
func NewModel(cfg config.Config, targetBranch string) Model {
|
||||||
InitStyles(cfg)
|
InitStyles(cfg)
|
||||||
|
|
||||||
// Use the dynamic targetBranch variable
|
|
||||||
files, _ := git.ListChangedFiles(targetBranch)
|
files, _ := git.ListChangedFiles(targetBranch)
|
||||||
items := tree.Build(files)
|
items := tree.Build(files)
|
||||||
|
|
||||||
|
|
@ -72,7 +68,7 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
||||||
diffViewport: viewport.New(0, 0),
|
diffViewport: viewport.New(0, 0),
|
||||||
focus: FocusTree,
|
focus: FocusTree,
|
||||||
currentBranch: git.GetCurrentBranch(),
|
currentBranch: git.GetCurrentBranch(),
|
||||||
targetBranch: targetBranch, // Store it
|
targetBranch: targetBranch,
|
||||||
repoName: git.GetRepoName(),
|
repoName: git.GetRepoName(),
|
||||||
showHelp: false,
|
showHelp: false,
|
||||||
inputBuffer: "",
|
inputBuffer: "",
|
||||||
|
|
@ -88,7 +84,6 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
if m.selectedPath != "" {
|
if m.selectedPath != "" {
|
||||||
// Use m.targetBranch instead of constant
|
|
||||||
return git.DiffCmd(m.targetBranch, m.selectedPath)
|
return git.DiffCmd(m.targetBranch, m.selectedPath)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -119,6 +114,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.updateSizes()
|
m.updateSizes()
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if msg.String() == "q" || msg.String() == "ctrl+c" {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
// If list is empty, ignore other keys
|
||||||
|
if len(m.fileTree.Items()) == 0 {
|
||||||
|
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
|
||||||
|
|
@ -130,10 +134,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.String() == "q" || msg.String() == "ctrl+c" {
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "tab":
|
case "tab":
|
||||||
if m.focus == FocusTree {
|
if m.focus == FocusTree {
|
||||||
|
|
@ -205,7 +205,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.focus == FocusTree {
|
if len(m.fileTree.Items()) > 0 && m.focus == FocusTree {
|
||||||
if !keyHandled {
|
if !keyHandled {
|
||||||
m.fileTree, cmd = m.fileTree.Update(msg)
|
m.fileTree, cmd = m.fileTree.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
@ -216,7 +216,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.selectedPath = item.FullPath
|
m.selectedPath = item.FullPath
|
||||||
m.diffCursor = 0
|
m.diffCursor = 0
|
||||||
m.diffViewport.GotoTop()
|
m.diffViewport.GotoTop()
|
||||||
// Use m.targetBranch
|
|
||||||
cmds = append(cmds, git.DiffCmd(m.targetBranch, m.selectedPath))
|
cmds = append(cmds, git.DiffCmd(m.targetBranch, m.selectedPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +228,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.diffViewport.SetContent(msg.Content)
|
m.diffViewport.SetContent(msg.Content)
|
||||||
|
|
||||||
case git.EditorFinishedMsg:
|
case git.EditorFinishedMsg:
|
||||||
// Use m.targetBranch
|
|
||||||
return m, git.DiffCmd(m.targetBranch, m.selectedPath)
|
return m, git.DiffCmd(m.targetBranch, m.selectedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,6 +265,12 @@ func (m Model) View() string {
|
||||||
return "Loading..."
|
return "Loading..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EMPTY STATE CHECK
|
||||||
|
if len(m.fileTree.Items()) == 0 {
|
||||||
|
return m.viewEmptyState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. PANES
|
||||||
treeStyle := PaneStyle
|
treeStyle := PaneStyle
|
||||||
if m.focus == FocusTree {
|
if m.focus == FocusTree {
|
||||||
treeStyle = FocusedPaneStyle
|
treeStyle = FocusedPaneStyle
|
||||||
|
|
@ -331,10 +335,10 @@ 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("│")
|
||||||
|
|
||||||
// Use m.targetBranch in status bar
|
|
||||||
statusText := fmt.Sprintf(" %s ↔ %s", m.currentBranch, m.targetBranch)
|
statusText := fmt.Sprintf(" %s ↔ %s", m.currentBranch, m.targetBranch)
|
||||||
if m.inputBuffer != "" {
|
if m.inputBuffer != "" {
|
||||||
statusText += fmt.Sprintf(" [Cmd: %s]", m.inputBuffer)
|
statusText += fmt.Sprintf(" [Cmd: %s]", m.inputBuffer)
|
||||||
|
|
@ -390,6 +394,82 @@ func (m Model) View() string {
|
||||||
return finalView
|
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")
|
||||||
|
desc1 := EmptyCodeStyle.Render("Diff against main")
|
||||||
|
|
||||||
|
cmd2 := lipgloss.NewStyle().Foreground(ColorText).Render("difi develop")
|
||||||
|
desc2 := EmptyCodeStyle.Render("Diff against target branch")
|
||||||
|
|
||||||
|
cmd3 := lipgloss.NewStyle().Foreground(ColorText).Render("difi HEAD~1")
|
||||||
|
desc3 := EmptyCodeStyle.Render("Diff against previous commit")
|
||||||
|
|
||||||
|
usageBlock := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
|
usageHeader,
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Left, cmd1, desc1),
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Left, cmd2, desc2),
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Left, cmd3, desc3),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 4. Navigation Guide
|
||||||
|
navHeader := EmptyHeaderStyle.Render("Navigation")
|
||||||
|
|
||||||
|
key1 := lipgloss.NewStyle().Foreground(ColorText).Render("Tab")
|
||||||
|
keyDesc1 := EmptyCodeStyle.Render("Switch panels")
|
||||||
|
|
||||||
|
key2 := lipgloss.NewStyle().Foreground(ColorText).Render("j / k")
|
||||||
|
keyDesc2 := EmptyCodeStyle.Render("Move cursor")
|
||||||
|
|
||||||
|
key3 := lipgloss.NewStyle().Foreground(ColorText).Render("?")
|
||||||
|
keyDesc3 := EmptyCodeStyle.Render("Toggle help")
|
||||||
|
|
||||||
|
navBlock := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
|
navHeader,
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Left, key1, keyDesc1),
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Left, key2, keyDesc2),
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Left, key3, keyDesc3),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Combine blocks
|
||||||
|
guides := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
|
usageBlock,
|
||||||
|
lipgloss.NewStyle().Width(8).Render(""), // Spacer
|
||||||
|
navBlock,
|
||||||
|
)
|
||||||
|
|
||||||
|
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
logo,
|
||||||
|
desc,
|
||||||
|
status,
|
||||||
|
lipgloss.NewStyle().Height(1).Render(""),
|
||||||
|
guides,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Center vertically
|
||||||
|
var verticalPad string
|
||||||
|
if m.height > lipgloss.Height(content) {
|
||||||
|
lines := (m.height - lipgloss.Height(content)) / 2
|
||||||
|
verticalPad = strings.Repeat("\n", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Top,
|
||||||
|
verticalPad,
|
||||||
|
lipgloss.PlaceHorizontal(m.width, lipgloss.Center, content),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func stripAnsi(str string) string {
|
func stripAnsi(str string) string {
|
||||||
re := regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
|
re := regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
|
||||||
return re.ReplaceAllString(str, "")
|
return re.ReplaceAllString(str, "")
|
||||||
|
|
|
||||||
|
|
@ -6,105 +6,112 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// -- Colors --
|
// Global Config
|
||||||
ColorText = lipgloss.AdaptiveColor{Light: "#24292f", Dark: "#c9d1d9"}
|
CurrentConfig config.Config
|
||||||
ColorSubtle = lipgloss.AdaptiveColor{Light: "#6e7781", Dark: "#8b949e"}
|
|
||||||
|
|
||||||
// UNIFIED SELECTION COLOR (The "Neutral Light Transparent Blue")
|
// -- THEME COLORS --
|
||||||
// This is used for BOTH the file tree and the diff panel background.
|
ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
|
||||||
// Dark: Deep subtle slate blue | Light: Pale selection blue
|
ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"}
|
||||||
ColorVisualBg = lipgloss.AdaptiveColor{Light: "#daeaff", Dark: "#3a4b5c"}
|
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
|
||||||
|
|
||||||
// Tree Text Color (High Contrast for the block cursor)
|
// -- PANE STYLES --
|
||||||
ColorVisualFg = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#ffffff"}
|
PaneStyle = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder(), false, true, false, false).
|
||||||
|
BorderForeground(ColorBorder)
|
||||||
|
|
||||||
ColorFolder = lipgloss.AdaptiveColor{Light: "#0969da", Dark: "#83a598"}
|
FocusedPaneStyle = PaneStyle.Copy().
|
||||||
ColorFile = lipgloss.AdaptiveColor{Light: "#24292f", Dark: "#ebdbb2"}
|
BorderForeground(ColorFocus)
|
||||||
|
|
||||||
|
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
||||||
|
ItemStyle = lipgloss.NewStyle().PaddingLeft(2)
|
||||||
|
|
||||||
|
// -- LIST DELEGATE STYLES --
|
||||||
|
SelectedItemStyle = lipgloss.NewStyle().
|
||||||
|
PaddingLeft(1).
|
||||||
|
Background(ColorCursorBg).
|
||||||
|
Foreground(ColorText).
|
||||||
|
Bold(true).
|
||||||
|
Width(1000)
|
||||||
|
|
||||||
|
SelectedBlockStyle = lipgloss.NewStyle().
|
||||||
|
Background(ColorCursorBg).
|
||||||
|
Foreground(ColorText).
|
||||||
|
Bold(true).
|
||||||
|
PaddingLeft(1)
|
||||||
|
|
||||||
|
// -- 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 --
|
||||||
|
LineNumberStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ColorSubtle).
|
||||||
|
PaddingRight(1).
|
||||||
|
Width(4)
|
||||||
|
|
||||||
|
DiffSelectionStyle = lipgloss.NewStyle().
|
||||||
|
Background(ColorCursorBg).
|
||||||
|
Width(1000)
|
||||||
|
|
||||||
|
// -- STATUS BAR STYLES --
|
||||||
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"}
|
||||||
|
|
||||||
// -- Styles --
|
StatusBarStyle = lipgloss.NewStyle().
|
||||||
PaneStyle lipgloss.Style
|
Foreground(ColorBarFg).
|
||||||
FocusedPaneStyle lipgloss.Style
|
Background(ColorBarBg).
|
||||||
DiffStyle lipgloss.Style
|
Padding(0, 1)
|
||||||
|
|
||||||
ItemStyle lipgloss.Style
|
StatusKeyStyle = lipgloss.NewStyle().
|
||||||
SelectedBlockStyle lipgloss.Style // Tree (Opaque)
|
Foreground(ColorText).
|
||||||
DiffSelectionStyle lipgloss.Style // Diff (Transparent/BG only)
|
Background(ColorBarBg).
|
||||||
|
Bold(true).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
FolderIconStyle lipgloss.Style
|
StatusDividerStyle = lipgloss.NewStyle().
|
||||||
FileIconStyle lipgloss.Style
|
Foreground(ColorSubtle).
|
||||||
LineNumberStyle lipgloss.Style
|
Background(ColorBarBg).
|
||||||
|
Padding(0, 0)
|
||||||
|
|
||||||
StatusBarStyle lipgloss.Style
|
// -- HELP STYLES --
|
||||||
StatusKeyStyle lipgloss.Style
|
HelpTextStyle = lipgloss.NewStyle().
|
||||||
StatusDividerStyle lipgloss.Style
|
Foreground(ColorSubtle).
|
||||||
HelpTextStyle lipgloss.Style
|
Padding(0, 1)
|
||||||
HelpDrawerStyle lipgloss.Style
|
|
||||||
|
|
||||||
CurrentConfig config.Config
|
HelpDrawerStyle = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder(), true, false, false, false).
|
||||||
|
BorderForeground(ColorBorder).
|
||||||
|
Padding(1, 2)
|
||||||
|
|
||||||
|
// -- EMPTY STATE / LANDING PAGE STYLES --
|
||||||
|
EmptyLogoStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ColorAccent).
|
||||||
|
Bold(true).
|
||||||
|
PaddingBottom(1)
|
||||||
|
|
||||||
|
EmptyDescStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ColorSubtle).
|
||||||
|
PaddingBottom(2)
|
||||||
|
|
||||||
|
EmptyStatusStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ColorText).
|
||||||
|
Background(ColorCursorBg).
|
||||||
|
Padding(0, 2).
|
||||||
|
MarginBottom(2)
|
||||||
|
|
||||||
|
EmptyCodeStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ColorSubtle).
|
||||||
|
MarginLeft(2)
|
||||||
|
|
||||||
|
EmptyHeaderStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ColorText).
|
||||||
|
Bold(true).
|
||||||
|
MarginBottom(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitStyles(cfg config.Config) {
|
func InitStyles(cfg config.Config) {
|
||||||
CurrentConfig = cfg
|
CurrentConfig = cfg
|
||||||
|
|
||||||
ColorBorder := lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: cfg.Colors.Border}
|
|
||||||
ColorFocus := lipgloss.AdaptiveColor{Light: "#6e7781", Dark: cfg.Colors.Focus}
|
|
||||||
|
|
||||||
// Allow user override for the selection background
|
|
||||||
var selectionBg lipgloss.TerminalColor
|
|
||||||
if cfg.Colors.DiffSelectionBg != "" {
|
|
||||||
selectionBg = lipgloss.Color(cfg.Colors.DiffSelectionBg)
|
|
||||||
} else {
|
|
||||||
selectionBg = ColorVisualBg
|
|
||||||
}
|
|
||||||
|
|
||||||
PaneStyle = lipgloss.NewStyle().
|
|
||||||
Border(lipgloss.NormalBorder(), false, cfg.UI.ShowGuide, false, false).
|
|
||||||
BorderForeground(ColorBorder)
|
|
||||||
|
|
||||||
FocusedPaneStyle = PaneStyle.Copy().
|
|
||||||
BorderForeground(ColorFocus)
|
|
||||||
|
|
||||||
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
|
||||||
|
|
||||||
// Base Row
|
|
||||||
ItemStyle = lipgloss.NewStyle().
|
|
||||||
PaddingLeft(1).
|
|
||||||
PaddingRight(1).
|
|
||||||
Foreground(ColorText)
|
|
||||||
|
|
||||||
// 1. LEFT PANE STYLE (Tree)
|
|
||||||
// Uses the shared background + forces a foreground color for readability
|
|
||||||
SelectedBlockStyle = lipgloss.NewStyle().
|
|
||||||
Background(selectionBg).
|
|
||||||
Foreground(ColorVisualFg).
|
|
||||||
PaddingLeft(1).
|
|
||||||
PaddingRight(1).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
// 2. RIGHT PANE STYLE (Diff)
|
|
||||||
// Uses the SAME shared background, but NO foreground.
|
|
||||||
// This makes it "transparent" so Green(+)/Red(-) text colors show through.
|
|
||||||
DiffSelectionStyle = lipgloss.NewStyle().
|
|
||||||
Background(selectionBg)
|
|
||||||
|
|
||||||
FolderIconStyle = lipgloss.NewStyle().Foreground(ColorFolder)
|
|
||||||
FileIconStyle = lipgloss.NewStyle().Foreground(ColorFile)
|
|
||||||
|
|
||||||
LineNumberStyle = lipgloss.NewStyle().
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user