Let users define custom icons and color for files on the config file (#4395)

- **PR Description**

_Yet another icon-related PR_

- Implementation on config file for user defined file icons and colors. 
- Fix for some file icon mappings.

I use [eza](https://github.com/eza-community/eza),
[nvim-wewb-devicons](https://github.com/nvim-tree/nvim-web-devicons) and
lazygit; the first ones already let user to override icons and colors
via a config file, so with this change I can customize based on
filenames and file extensions.

| Before | After  |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/bc747d99-25c1-4b74-868e-a31178d7d22e)
|
![image](https://github.com/user-attachments/assets/9a43eda9-d167-4602-a891-8c7c3e530289)
|

```yaml
gui:
  customIcons:
    filenames:
      "CONTRIBUTING.md": { icon: "\uede2", color: "#FEDDEF" }
      "HACKING.md": { icon: "\uede2", color: "#FEDDEF" }
    extensions:
      ".cat":
        icon: "\U000f011b"
        color: "#BC4009"
      ".dog":
        icon: "\U000f0a43"
        color: "#B6977E"
```
This commit is contained in:
Stefan Haller 2025-03-25 09:55:18 +01:00 committed by GitHub
commit 5038fa2d51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 124 additions and 17 deletions

View file

@ -43,6 +43,15 @@ gui:
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
branchColorPatterns: {}
# Custom icons for filenames and file extensions
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color
customIcons:
# Map of filenames to icon properties (icon and color)
filenames: {}
# Map of file extensions (including the dot) to icon properties (icon and color)
extensions: {}
# The number of lines you scroll by when scrolling the main window
scrollHeight: 2
@ -649,32 +658,39 @@ os:
```
## Custom Command for Opening a Link
```yaml
os:
openLink: 'bash -C /path/to/your/shell-script.sh {{link}}'
```
Specify the external command to invoke when opening URL links (i.e. creating MR/PR in GitLab, BitBucket or GitHub). `{{link}}` will be replaced by the URL to be opened. A simple shell script can be used to further mangle the passed URL.
## Custom Command for Copying to and Pasting from Clipboard
```yaml
os:
copyToClipboardCmd: ''
```
Specify an external command to invoke when copying to clipboard is requested. `{{text}` will be replaced by text to be copied. Default is to copy to system clipboard.
If you are working on a terminal that supports OSC52, the following command will let you take advantage of it:
```yaml
os:
copyToClipboardCmd: printf "\033]52;c;$(printf {{text}} | base64 -w 0)\a" > /dev/tty
```
For tmux you need to wrap it with the [tmux escape sequence](https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it), and enable passthrough in tmux config with `set -g allow-passthrough on`:
```yaml
os:
copyToClipboardCmd: printf "\033Ptmux;\033\033]52;c;$(printf {{text}} | base64 -w 0)\a\033\\" > /dev/tty
```
For the best of both worlds, we can let the command determine if we are running in a tmux session and send the correct sequence:
```yaml
os:
copyToClipboardCmd: >
@ -686,10 +702,12 @@ os:
```
A custom command for reading from the clipboard can be set using
```yaml
os:
readFromClipboardCmd: ''
```
It is used, for example, when pasting a commit message into the commit message panel. The command is supposed to output the clipboard content to stdout.
## Configuring File Editing
@ -832,6 +850,27 @@ gui:
Note that the regular expressions are not implicitly anchored to the beginning/end of the branch name. If you want to do that, add leading `^` and/or trailing `$` as needed.
## Custom Files Icon & Color
You can customize the icon and color of files based on filenames or extensions:
```yaml
gui:
customIcons:
filenames:
"CONTRIBUTING.md": { icon: "\uede2", color: "#FEDDEF" }
"HACKING.md": { icon: "\uede2", color: "#FEDDEF" }
extensions:
".cat":
icon: "\U000f011b"
color: "#BC4009"
".dog":
icon: "\U000f0a43"
color: "#B6977E"
```
Note that there is no support for regular expressions.
## Example Coloring
![border example](../../assets/colored-border-example.png)
@ -960,6 +999,7 @@ In situations where certain naming pattern is used for branches, this can be use
Example:
Some branches:
- jsmith/AB-123
- cwilson/AB-125

View file

@ -57,6 +57,9 @@ type GuiConfig struct {
BranchColors map[string]string `yaml:"branchColors" jsonschema:"deprecated"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
BranchColorPatterns map[string]string `yaml:"branchColorPatterns"`
// Custom icons for filenames and file extensions
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color
CustomIcons CustomIconsConfig `yaml:"customIcons"`
// The number of lines you scroll by when scrolling the main window
ScrollHeight int `yaml:"scrollHeight" jsonschema:"minimum=1"`
// If true, allow scrolling past the bottom of the content in the main window
@ -707,6 +710,18 @@ type CustomCommandMenuOption struct {
Value string `yaml:"value" jsonschema:"example=feature,minLength=1"`
}
type CustomIconsConfig struct {
// Map of filenames to icon properties (icon and color)
Filenames map[string]IconProperties `yaml:"filenames"`
// Map of file extensions (including the dot) to icon properties (icon and color)
Extensions map[string]IconProperties `yaml:"extensions"`
}
type IconProperties struct {
Icon string `yaml:"icon"`
Color string `yaml:"color"`
}
func GetDefaultConfig() *UserConfig {
return &UserConfig{
Gui: GuiConfig{

View file

@ -39,7 +39,7 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
}
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
lines := presentation.RenderCommitFileTree(viewModel, c.Git().Patch.PatchBuilder, showFileIcons)
lines := presentation.RenderCommitFileTree(viewModel, c.Git().Patch.PatchBuilder, showFileIcons, &c.UserConfig().Gui.CustomIcons)
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})

View file

@ -31,7 +31,7 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
getDisplayStrings := func(_ int, _ int) [][]string {
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
showNumstat := c.UserConfig().Gui.ShowNumstatInFilesView
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat)
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat, &c.UserConfig().Gui.CustomIcons)
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})

View file

@ -6,6 +6,7 @@ import (
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
@ -23,12 +24,13 @@ func RenderFileTree(
submoduleConfigs []*models.SubmoduleConfig,
showFileIcons bool,
showNumstat bool,
customIconsConfig *config.CustomIconsConfig,
) []string {
collapsedPaths := tree.CollapsedPaths()
return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.File], treeDepth int, visualDepth int, isCollapsed bool) string {
fileNode := filetree.NewFileNode(node)
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node)
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node, customIconsConfig)
})
}
@ -36,12 +38,13 @@ func RenderCommitFileTree(
tree *filetree.CommitFileTreeViewModel,
patchBuilder *patch.PatchBuilder,
showFileIcons bool,
customIconsConfig *config.CustomIconsConfig,
) []string {
collapsedPaths := tree.CollapsedPaths()
return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.CommitFile], treeDepth int, visualDepth int, isCollapsed bool) string {
status := commitFilePatchStatus(node, tree, patchBuilder)
return getCommitFileLine(isCollapsed, treeDepth, visualDepth, node, status, showFileIcons)
return getCommitFileLine(isCollapsed, treeDepth, visualDepth, node, status, showFileIcons, customIconsConfig)
})
}
@ -116,6 +119,7 @@ func getFileLine(
showFileIcons bool,
submoduleConfigs []*models.SubmoduleConfig,
node *filetree.Node[models.File],
customIconsConfig *config.CustomIconsConfig,
) string {
name := fileNameAtDepth(node, treeDepth)
output := ""
@ -156,7 +160,7 @@ func getFileLine(
isDirectory := file == nil
if showFileIcons {
icon := icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory)
icon := icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory, customIconsConfig)
paint := color.HEX(icon.Color, false)
output += paint.Sprint(icon.Icon) + nameColor.Sprint(" ")
}
@ -218,6 +222,7 @@ func getCommitFileLine(
node *filetree.Node[models.CommitFile],
status patch.PatchStatus,
showFileIcons bool,
customIconsConfig *config.CustomIconsConfig,
) string {
indentation := strings.Repeat(" ", visualDepth)
name := commitFileNameAtDepth(node, treeDepth)
@ -266,7 +271,7 @@ func getCommitFileLine(
isLinkedWorktree := false
if showFileIcons {
icon := icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory)
icon := icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory, customIconsConfig)
paint := color.HEX(icon.Color, false)
output += paint.Sprint(icon.Icon) + " "
}

View file

@ -7,6 +7,7 @@ import (
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
@ -91,7 +92,7 @@ func TestRenderFileTree(t *testing.T) {
for _, path := range s.collapsedPaths {
viewModel.ToggleCollapsed(path)
}
result := RenderFileTree(viewModel, nil, false, s.showLineChanges)
result := RenderFileTree(viewModel, nil, false, s.showLineChanges, &config.CustomIconsConfig{})
assert.EqualValues(t, s.expected, result)
})
}
@ -161,7 +162,7 @@ func TestRenderCommitFileTree(t *testing.T) {
},
)
patchBuilder.Start("from", "to", false, false)
result := RenderCommitFileTree(viewModel, patchBuilder, false)
result := RenderCommitFileTree(viewModel, patchBuilder, false, &config.CustomIconsConfig{})
assert.EqualValues(t, s.expected, result)
})
}

View file

@ -3,11 +3,13 @@ package icons
import (
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/config"
)
// NOTE: Visit next links for inspiration:
// https://github.com/eza-community/eza/blob/main/src/output/icons.rs
// https://github.com/nvim-tree/nvim-web-devicons/blob/master/lua/nvim-web-devicons/icons-default.lua
// https://github.com/nvim-tree/nvim-web-devicons/tree/master/lua/nvim-web-devicons/default
var (
DEFAULT_FILE_ICON = IconProperties{Icon: "\uf15b", Color: "#878787"} // 
@ -15,6 +17,7 @@ var (
DEFAULT_DIRECTORY_ICON = IconProperties{Icon: "\uf07b", Color: "#878787"} // 
)
// NOTE: The filename map is case sensitive.
var nameIconMap = map[string]IconProperties{
".atom": {Icon: "\ue764", Color: "#EED9B7"}, // 
".babelrc": {Icon: "\ue639", Color: "#FED836"}, // 
@ -90,7 +93,7 @@ var nameIconMap = map[string]IconProperties{
"Cargo.lock": {Icon: "\ue7a8", Color: "#DEA584"}, // 
"Cargo.toml": {Icon: "\ue7a8", Color: "#DEA584"}, // 
"checkhealth": {Icon: "\U000f04d9", Color: "#75B4FB"}, // 󰓙
"cmakelists.txt": {Icon: "\ue794", Color: "##DCE3EB"}, // 
"CMakeLists.txt": {Icon: "\ue794", Color: "#DCE3EB"}, // 
"CODE_OF_CONDUCT": {Icon: "\uf4ae", Color: "#E41662"}, // 
"CODE_OF_CONDUCT.md": {Icon: "\uf4ae", Color: "#E41662"}, // 
"CODE-OF-CONDUCT.md": {Icon: "\uf4ae", Color: "#E41662"}, // 
@ -280,7 +283,7 @@ var extIconMap = map[string]IconProperties{
".cljd": {Icon: "\ue76a", Color: "#519ABA"}, // 
".cljs": {Icon: "\ue642", Color: "#2AB6F6"}, // 
".cls": {Icon: "\ue69b", Color: "#4B5163"}, // 
".cmake": {Icon: "\ue794", Color: "##DCE3EB"}, // 
".cmake": {Icon: "\ue794", Color: "#DCE3EB"}, // 
".cmd": {Icon: "\uebc4", Color: "#FF7043"}, // 
".cob": {Icon: "\u2699", Color: "#005CA5"}, // ⚙
".cobol": {Icon: "\u2699", Color: "#005CA5"}, // ⚙
@ -402,14 +405,14 @@ var extIconMap = map[string]IconProperties{
".guardfile": {Icon: "\ue21e", Color: "#626262"}, // 
".gv": {Icon: "\U000f1049", Color: "#005F87"}, // 󱁉
".gz": {Icon: "\uf410", Color: "#ECA517"}, // 
".h": {Icon: "\uf0fd", Color: "##A074C4"}, // 
".h": {Icon: "\uf0fd", Color: "#A074C4"}, // 
".haml": {Icon: "\ue664", Color: "#F4521E"}, // 
".hbs": {Icon: "\U000f15de", Color: "#FF7043"}, // 󱗞
".hc": {Icon: "\U000f00a2", Color: "#FAF743"}, // 󰂢
".heex": {Icon: "\ue62d", Color: "#9575CE"}, // 
".hex": {Icon: "\U000f12a7", Color: "#25A79A"}, // 󱊧
".hh": {Icon: "\uf0fd", Color: "##A074C4"}, // 
".hpp": {Icon: "\uf0fd", Color: "##A074C4"}, // 
".hh": {Icon: "\uf0fd", Color: "#A074C4"}, // 
".hpp": {Icon: "\uf0fd", Color: "#A074C4"}, // 
".hrl": {Icon: "\ue7b1", Color: "#B83998"}, // 
".hs": {Icon: "\ue61f", Color: "#FFA726"}, // 
".htm": {Icon: "\uf13b", Color: "#E44E27"}, // 
@ -417,7 +420,7 @@ var extIconMap = map[string]IconProperties{
".huff": {Icon: "\U000f0858", Color: "#CFD8DD"}, // 󰡘
".hurl": {Icon: "\uf0ec", Color: "#FF0288"}, // 
".hx": {Icon: "\ue666", Color: "#F68713"}, // 
".hxx": {Icon: "\uf0fd", Color: "##A074C4"}, // 
".hxx": {Icon: "\uf0fd", Color: "#A074C4"}, // 
".ical": {Icon: "\uf073", Color: "#2B9EF3"}, // 
".icalendar": {Icon: "\uf073", Color: "#2B9EF3"}, // 
".ico": {Icon: "\U000f021f", Color: "#25A6A0"}, // 󰈟
@ -688,7 +691,7 @@ var extIconMap = map[string]IconProperties{
".tlz": {Icon: "\uf410", Color: "#ECA517"}, // 
".tmux": {Icon: "\uebc8", Color: "#14BA19"}, // 
".toml": {Icon: "\ue6b2", Color: "#9C4221"}, // 
".torrent": {Icon: "\ue275", Color: "##4C90E8"}, // 
".torrent": {Icon: "\ue275", Color: "#4C90E8"}, // 
".tres": {Icon: "\ue65f", Color: "#42A5F5"}, // 
".ts": {Icon: "\U000f06e6", Color: "#0188D1"}, // 󰛦
".tscn": {Icon: "\ue65f", Color: "#42A5F5"}, // 
@ -763,13 +766,19 @@ func patchFileIconsForNerdFontsV2() {
extIconMap[".vue"] = IconProperties{Icon: "\ufd42", Color: "#89e051"} // ﵂
}
func IconForFile(name string, isSubmodule bool, isLinkedWorktree bool, isDirectory bool) IconProperties {
func IconForFile(name string, isSubmodule bool, isLinkedWorktree bool, isDirectory bool, customIconsConfig *config.CustomIconsConfig) IconProperties {
base := filepath.Base(name)
if icon, ok := customIconsConfig.Filenames[base]; ok {
return IconProperties{Color: icon.Color, Icon: icon.Icon}
}
if icon, ok := nameIconMap[base]; ok {
return icon
}
ext := strings.ToLower(filepath.Ext(name))
if icon, ok := customIconsConfig.Extensions[ext]; ok {
return IconProperties{Color: icon.Color, Icon: icon.Icon}
}
if icon, ok := extIconMap[ext]; ok {
return icon
}

View file

@ -263,6 +263,27 @@
"additionalProperties": false,
"type": "object"
},
"CustomIconsConfig": {
"properties": {
"filenames": {
"additionalProperties": {
"$ref": "#/$defs/IconProperties"
},
"type": "object",
"description": "Map of filenames to icon properties (icon and color)"
},
"extensions": {
"additionalProperties": {
"$ref": "#/$defs/IconProperties"
},
"type": "object",
"description": "Map of file extensions (including the dot) to icon properties (icon and color)"
}
},
"additionalProperties": false,
"type": "object",
"description": "Custom icons for filenames and file extensions\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color"
},
"GitConfig": {
"properties": {
"paging": {
@ -404,6 +425,10 @@
"type": "object",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color"
},
"customIcons": {
"$ref": "#/$defs/CustomIconsConfig",
"description": "Custom icons for filenames and file extensions\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color"
},
"scrollHeight": {
"type": "integer",
"minimum": 1,
@ -700,6 +725,18 @@
"type": "object",
"description": "Config relating to the Lazygit UI"
},
"IconProperties": {
"properties": {
"icon": {
"type": "string"
},
"color": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"KeybindingAmendAttributeConfig": {
"properties": {
"resetAuthor": {