Skip to content

Commit c8a8f8e

Browse files
authored
feat: add configurable icon styles (#58)
Add `icons` config option to customize file tree icons. **Styles:** - `ascii`: +, x, * (default) - `unicode`: +, ⛌, ● - `nerd-fonts`: different icons per status - `nerd-fonts-alt`: simple with file icons **Features:** - Press `i` to cycle through styles - Icons colored by git status (green/red/yellow) - The highlighted selection is of the color of the change (same as icon color) - Selection highlight uses status color - Folder icons also change per style | ascii | `unicode` | `nerd-fonts` | `nerd-fonts-alt` | | :------------------: | :-----: | :------: | :------------------------------------: | | <img width="130" height="664" alt="image" src="https://github.com/user-attachments/assets/fb22360e-0811-4030-9b06-0bf7a82aaba2" /> | <img width="130" height="664" alt="image" src="https://github.com/user-attachments/assets/81c56616-21f7-425f-ae6c-26f54e36a822" /> | <img width="130" height="664" alt="image" src="https://github.com/user-attachments/assets/4a8fc6a4-2b5d-409a-a52e-f716cade2c25" /> | <img width="130" height="664" alt="image" src="https://github.com/user-attachments/assets/4da8053f-e184-40a8-afb1-a6d87665b015" /> | **Config file:** | Option | Type | Default | Description | | :------------------ | :----- | :------ | :------------------------------------ | | `ui.icons` | string | `ascii` | Icon style: `ascii`, `unicode`, `nerd-fonts`, or `nerd-fonts-alt` |
1 parent dd8fd3d commit c8a8f8e

File tree

5 files changed

+187
-65
lines changed

5 files changed

+187
-65
lines changed

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,23 @@ ui:
7070

7171
# Customize the search panel width (default: 50)
7272
searchTreeWidth: 60
73+
74+
# Icon style: "nerd-fonts" (default), "nerd-fonts-alt", "unicode", or "ascii"
75+
icons: nerd-fonts
76+
77+
# Color filenames by git status (default: true)
78+
colorFileNames: false
7379
```
7480
75-
| Option | Type | Default | Description |
76-
| :------------------ | :--- | :------ | :------------------------------------ |
77-
| `ui.hideHeader` | bool | `false` | Hide the "DIFFNAV" header |
78-
| `ui.hideFooter` | bool | `false` | Hide the footer with keybindings help |
79-
| `ui.showFileTree` | bool | `true` | Show file tree on startup |
80-
| `ui.fileTreeWidth` | int | `26` | Width of the file tree sidebar |
81-
| `ui.searchTreeWidth`| int | `50` | Width of the search panel |
81+
| Option | Type | Default | Description |
82+
| :------------------ | :----- | :------ | :------------------------------------ |
83+
| `ui.hideHeader` | bool | `false` | Hide the "DIFFNAV" header |
84+
| `ui.hideFooter` | bool | `false` | Hide the footer with keybindings help |
85+
| `ui.showFileTree` | bool | `true` | Show file tree on startup |
86+
| `ui.fileTreeWidth` | int | `26` | Width of the file tree sidebar |
87+
| `ui.searchTreeWidth`| int | `50` | Width of the search panel |
88+
| `ui.icons` | string | `nerd-fonts` | Icon style: `nerd-fonts`, `nerd-fonts-alt`, `unicode`, or `ascii` |
89+
| `ui.colorFileNames` | bool | `true` | Color filenames by git status |
8290

8391
### Delta
8492

@@ -97,8 +105,9 @@ If you want the exact delta configuration I'm using - [it can be found here](htt
97105
| <kbd>e</kbd> | Toggle the file tree |
98106
| <kbd>t</kbd> | Search/go-to file |
99107
| <kbd>y</kbd> | Copy file path |
108+
| <kbd>i</kbd> | Cycle icon style |
100109
| <kbd>o</kbd> | Open file in $EDITOR |
101-
| <kbd>Tab</kbd> | Switch focus between the panes |
110+
| <kbd>Tab</kbd> | Switch focus between the panes |
102111
| <kbd>q</kbd> | Quit |
103112

104113
## Under the hood

pkg/config/config.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
)
1010

1111
type UIConfig struct {
12-
HideHeader bool `yaml:"hideHeader"`
13-
HideFooter bool `yaml:"hideFooter"`
14-
ShowFileTree bool `yaml:"showFileTree"`
15-
FileTreeWidth int `yaml:"fileTreeWidth"`
16-
SearchTreeWidth int `yaml:"searchTreeWidth"`
12+
HideHeader bool `yaml:"hideHeader"`
13+
HideFooter bool `yaml:"hideFooter"`
14+
ShowFileTree bool `yaml:"showFileTree"`
15+
FileTreeWidth int `yaml:"fileTreeWidth"`
16+
SearchTreeWidth int `yaml:"searchTreeWidth"`
17+
Icons string `yaml:"icons"` // "nerd-fonts" (default), "nerd-fonts-alt", "unicode", "ascii"
18+
ColorFileNames bool `yaml:"colorFileNames"` // Color filenames by git status (default: true)
1719
}
1820

1921
type Config struct {
@@ -28,6 +30,8 @@ func DefaultConfig() Config {
2830
ShowFileTree: true,
2931
FileTreeWidth: 26,
3032
SearchTreeWidth: 50,
33+
Icons: "nerd-fonts",
34+
ColorFileNames: true,
3135
},
3236
}
3337
}

pkg/filenode/file_node.go

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,101 @@ package filenode
22

33
import (
44
"path/filepath"
5-
"strings"
65

76
"github.com/bluekeyes/go-gitdiff/gitdiff"
87
"github.com/charmbracelet/lipgloss"
98
"github.com/charmbracelet/lipgloss/tree"
9+
)
1010

11-
"github.com/dlvhdr/diffnav/pkg/constants"
12-
"github.com/dlvhdr/diffnav/pkg/utils"
11+
// Icon style constants.
12+
const (
13+
IconsNerdFonts = "nerd-fonts"
14+
IconsNerdFontsAlt = "nerd-fonts-alt"
15+
IconsUnicode = "unicode"
16+
IconsASCII = "ascii"
1317
)
1418

1519
type FileNode struct {
16-
File *gitdiff.File
17-
Depth int
18-
YOffset int
20+
File *gitdiff.File
21+
Depth int
22+
YOffset int
23+
IconStyle string
24+
Selected bool
25+
ColorFileNames bool
26+
PanelWidth int
1927
}
2028

2129
func (f FileNode) Path() string {
2230
return GetFileName(f.File)
2331
}
2432

2533
func (f FileNode) Value() string {
26-
icon := " "
27-
status := " "
28-
if f.File.IsNew {
29-
status += lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Render("")
30-
} else if f.File.IsDelete {
31-
status += lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render("")
32-
} else {
33-
status += lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Render("")
34-
}
34+
icon := f.getIcon()
35+
name := filepath.Base(f.Path())
36+
// Icon is always colored by status
37+
coloredIcon := lipgloss.NewStyle().Foreground(f.StatusColor()).Render(icon)
3538

36-
depthWidth := f.Depth * 2
37-
iconsWidth := lipgloss.Width(icon) + lipgloss.Width(status)
38-
nameMaxWidth := constants.OpenFileTreeWidth - depthWidth - iconsWidth
39-
base := filepath.Base(f.Path())
40-
name := utils.TruncateString(base, nameMaxWidth)
39+
if f.Selected {
40+
// Apply background with fixed width to extend to panel edge
41+
bgStyle := lipgloss.NewStyle().
42+
Bold(true).
43+
Foreground(f.StatusColor()).
44+
Background(lipgloss.Color("#3a3a3a"))
45+
if f.PanelWidth > 0 {
46+
iconWidth := lipgloss.Width(coloredIcon) + 1
47+
// Subtract tree indentation
48+
availableWidth := f.PanelWidth - iconWidth - (f.Depth * 2)
49+
if availableWidth > 0 {
50+
bgStyle = bgStyle.Width(availableWidth)
51+
}
52+
}
53+
return coloredIcon + " " + bgStyle.Render(name)
54+
}
4155

42-
spacerWidth := constants.OpenFileTreeWidth - lipgloss.Width(name) - iconsWidth - depthWidth
43-
if len(name) < len(base) {
44-
spacerWidth = spacerWidth - 1
56+
if f.ColorFileNames {
57+
styledName := lipgloss.NewStyle().Foreground(f.StatusColor()).Render(name)
58+
return coloredIcon + " " + styledName
4559
}
46-
spacer := ""
47-
if spacerWidth > 0 {
48-
spacer = strings.Repeat(" ", spacerWidth)
60+
61+
return coloredIcon + " " + name
62+
}
63+
64+
func (f FileNode) getIcon() string {
65+
switch f.IconStyle {
66+
case IconsNerdFonts:
67+
if f.File.IsNew {
68+
return ""
69+
} else if f.File.IsDelete {
70+
return ""
71+
}
72+
return ""
73+
case IconsNerdFontsAlt:
74+
return ""
75+
case IconsUnicode:
76+
if f.File.IsNew {
77+
return "+"
78+
} else if f.File.IsDelete {
79+
return "⛌"
80+
}
81+
return "●"
82+
default: // ascii (fallback for unknown values)
83+
if f.File.IsNew {
84+
return "+"
85+
} else if f.File.IsDelete {
86+
return "x"
87+
}
88+
return "*"
4989
}
90+
}
5091

51-
return lipgloss.JoinHorizontal(lipgloss.Top, icon, name, spacer, status)
92+
// StatusColor returns the color for this file based on its git status.
93+
func (f FileNode) StatusColor() lipgloss.Color {
94+
if f.File.IsNew {
95+
return lipgloss.Color("2") // green
96+
} else if f.File.IsDelete {
97+
return lipgloss.Color("1") // red
98+
}
99+
return lipgloss.Color("3") // yellow/orange
52100
}
53101

54102
func (f FileNode) String() string {

pkg/ui/mainModel.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,12 @@ type mainModel struct {
7272
config config.Config
7373
draggingSidebar bool
7474
customSidebarWidth int
75+
iconStyle string
7576
}
7677

7778
func New(input string, cfg config.Config) mainModel {
78-
m := mainModel{input: input, isShowingFileTree: cfg.UI.ShowFileTree, activePanel: FileTreePanel, config: cfg}
79-
m.fileTree = filetree.New()
79+
m := mainModel{input: input, isShowingFileTree: cfg.UI.ShowFileTree, activePanel: FileTreePanel, config: cfg, iconStyle: cfg.UI.Icons}
80+
m.fileTree = filetree.New(cfg.UI.Icons, cfg.UI.ColorFileNames)
8081
m.diffViewer = diffviewer.New()
8182

8283
m.help = help.New()
@@ -93,7 +94,7 @@ func New(input string, cfg config.Config) mainModel {
9394
m.search = textinput.New()
9495
m.search.ShowSuggestions = true
9596
m.search.KeyMap.AcceptSuggestion = key.NewBinding(key.WithKeys("tab"))
96-
m.search.Prompt = " "
97+
m.search.Prompt = " "
9798
m.search.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
9899
m.search.Placeholder = "Filter files 󰬛 "
99100
m.search.PlaceholderStyle = lipgloss.NewStyle().MaxWidth(lipgloss.Width(m.search.Placeholder)).Foreground(lipgloss.Color("8"))
@@ -145,6 +146,8 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145146
}
146147
dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-m.footerHeight()-m.headerHeight())
147148
cmds = append(cmds, dfCmd)
149+
case "i":
150+
m.cycleIconStyle()
148151
case "tab":
149152
if m.isShowingFileTree {
150153
if m.activePanel == FileTreePanel {
@@ -240,6 +243,20 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
240243
return m, tea.Batch(cmds...)
241244
}
242245

246+
func (m *mainModel) cycleIconStyle() {
247+
switch m.iconStyle {
248+
case filenode.IconsASCII:
249+
m.iconStyle = filenode.IconsUnicode
250+
case filenode.IconsUnicode:
251+
m.iconStyle = filenode.IconsNerdFonts
252+
case filenode.IconsNerdFonts:
253+
m.iconStyle = filenode.IconsNerdFontsAlt
254+
default:
255+
m.iconStyle = filenode.IconsASCII
256+
}
257+
m.fileTree = m.fileTree.SetIconStyle(m.iconStyle)
258+
}
259+
243260
func (m mainModel) searchUpdate(msg tea.Msg) (mainModel, []tea.Cmd) {
244261
var cmd tea.Cmd
245262
var cmds []tea.Cmd
@@ -413,7 +430,7 @@ func (m mainModel) footerView() string {
413430
func (m mainModel) resultsView() string {
414431
sb := strings.Builder{}
415432
for i, f := range m.filtered {
416-
fName := utils.TruncateString(" "+f, m.config.UI.SearchTreeWidth-2)
433+
fName := utils.TruncateString(" "+f, m.config.UI.SearchTreeWidth-2)
417434
if i == m.resultsCursor {
418435
sb.WriteString(lipgloss.NewStyle().Background(lipgloss.Color("#1b1b33")).Bold(true).Render(fName) + "\n")
419436
} else {

0 commit comments

Comments
 (0)