Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.claude
mdc
dist/
QWEN.md
prompts/
.gitignore
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitnoreception :-)

Probably a mistake.

.qwen
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Midday Commander (mdc) brings the classic dual-panel file management paradigm in
- **Live theme picker** - browse and preview themes with Ctrl-T
- **Multi-file selection** - tag files with Insert or Shift+Arrow for batch operations
- **Quick search** - start typing to jump to matching files instantly
- **Nerd Font icons** - file type icons with per-extension customization
- **External editor/viewer** - opens files in `$EDITOR` and `$PAGER`
- **Mouse support** - clickable menu bar and panel interaction
- **Go to path** - quickly jump to any directory with `~` expansion
Expand Down Expand Up @@ -236,6 +237,68 @@ fkey_label_bg = "blue"

Colors can be hex values (`"#89b4fa"`), ANSI color numbers (`"4"`), or palette references (`"blue"`). Any missing values fall back to the built-in default theme.

## Nerd Font Icons

Display Nerd Font icons next to file and folder names in the panel for a more visual file browsing experience.

### Prerequisites

You need a [Nerd Font](https://www.nerdfonts.com/) installed and set as your terminal font. Popular choices:

- **Meslo Nerd Font** — monospaced, great for file managers
- **JetBrains Mono Nerd Font** — developer-focused
- **FiraCode Nerd Font** — clean and readable

### Enabling Icons

Icons are **disabled by default**. Set `enabled = true` in your config:

```toml
[icons]
enabled = true
```

### Icon Configuration

```toml
[icons]
enabled = true
folder = "" # Icon for directories (default: nf-fa-folder)
file = "" # Default icon for files (default: nf-fa-file)

[icons.extensions]
txt = "" # nf-fa-file_text
pdf = "" # nf-fa-file_pdf
png = "" # nf-fa-file_image
jpg = ""
go = ""
py = ""
js = ""
ts = "" # nf-seti-typescript
zip = "" # nf-fa-file_archive
```

### Icon Resolution Behavior

1. **Directories** — always use the `folder` icon, regardless of extension config.
2. **Files with matching extension** — look up the extension (case-insensitive) in `[icons.extensions]`.
3. **Files without a match** — fall back to the `file` icon.
4. **Files without an extension** (e.g. `Makefile`) — also fall back to the `file` icon.

### Built-in Defaults

The icon system ships with defaults for 50+ file extensions, so icons work immediately when enabled without any config needed. Defaults cover:

- **Documents** — txt, md, pdf, doc, docx, xls, xlsx
- **Images** — png, jpg, jpeg, gif, svg, ico, bmp, webp
- **Archives** — zip, tar, gz, bz2, 7z, rar, xz
- **Code** — go, py, js, jsx, ts, tsx, rs, rb, java, c, cpp, h, hpp, cs, php, sh, bash
- **Config/Markup** — json, yaml, yml, toml, xml, html, css
- **Media** — mp3, wav, mp4, avi, mkv
- **Go** — mod, sum

You only need to add entries to `[icons.extensions]` if you want to override the defaults or add custom extensions.

## Contributing

Contributions are welcome. Please open an issue to discuss significant changes before submitting a pull request.
Expand Down
27 changes: 27 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,30 @@ select_down = "shift+down"

# Search
quick_search = "ctrl+s"

# ─── Nerd Font Icons ─────────────────────────
# Display Nerd Font icons next to file/folder names in the panel.
# Requires a Nerd Font installed in your terminal (e.g. Meslo Nerd Font,
# JetBrains Mono Nerd Font, FiraCode Nerd Font).
[icons]
# Set to true to enable icons (default: false).
enabled = false
# Icon for directories (default: nf-fa-folder).
folder = ""
# Default icon for files without a matching extension (default: nf-fa-file).
file = ""

# Extension → icon mapping. Only a few examples shown; add any you need.
[icons.extensions]
txt = "" # nf-fa-file_text
pdf = "" # nf-fa-file_pdf
png = "" # nf-fa-file_image
jpg = ""
jpeg = ""
go = ""
py = ""
js = ""
ts = "" # nf-seti-typescript
zip = "" # nf-fa-file_archive
tar = ""
gz = ""
5 changes: 3 additions & 2 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,12 @@ func New() Model {
lfs := local.New(string(filepath.Separator))

panelKM := panelKeyMapFromConfig(cfg.Keys)
iconResolver := panel.NewIconResolver(cfg.Icons)

left := panel.New(lfs, cwd, panelKM)
left := panel.New(lfs, cwd, panelKM, iconResolver)
left.SetActive(true)

right := panel.New(lfs, home, panelKM)
right := panel.New(lfs, home, panelKM, iconResolver)

th := theme.Default()
if cfg.Theme != "" {
Expand Down
138 changes: 126 additions & 12 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,96 @@ type Config struct {
Theme string `toml:"theme"`
Keys KeyBindings `toml:"keys"`
Behavior BehaviorConfig `toml:"behavior"`
Icons IconsConfig `toml:"icons"`
}

// IconsConfig controls the display of Nerd Font icons in the file panel.
type IconsConfig struct {
// Enabled controls whether icons are displayed (default: false).
Enabled bool `toml:"enabled"`
// Icon shown for directories.
Folder string `toml:"folder"`
// Icon shown for files without a matching extension.
File string `toml:"file"`
// Mapping of file extension (without dot) to icon.
Extensions map[string]string `toml:"extensions"`
}

// DefaultIconsConfig returns the default icon configuration.
func DefaultIconsConfig() IconsConfig {
return IconsConfig{
Enabled: false,
Folder: "", // nf-fa-folder
File: "", // nf-fa-file
Extensions: map[string]string{
// Documents
"txt": "", // nf-fa-file_text
"md": "",
"pdf": "", // nf-fa-file_pdf
"doc": "", // nf-fa-file_word
"docx": "",
"xls": "", // nf-fa-file_excel
"xlsx": "",

// Images
"png": "", // nf-fa-file_image
"jpg": "",
"jpeg": "",
"gif": "",
"svg": "",
"ico": "",
"bmp": "",
"webp": "",

// Archives
"zip": "", // nf-fa-file_archive
"tar": "",
"gz": "",
"bz2": "",
"7z": "",
"rar": "",
"xz": "",

// Code
"go": "", // nf-seti-go
"py": "", // nf-dev-python
"js": "", // nf-seti-javascript
"jsx": "",
"ts": "", // nf-seti-typescript
"tsx": "",
"rs": "", // nf-custom-rust
"rb": "", // nf-dev-ruby
"java": "", // nf-custom-java
"c": "", // nf-seti-c
"cpp": "", // nf-seti-cpp
"h": "",
"hpp": "",
"cs": "󰌛", // nf-custom-csharp
"php": "", // nf-custom-php
"sh": "", // nf-oct-terminal
"bash": "",

// Config / Markup
"json": "", // nf-oct-file_json
"yaml": "", // nf-dev-config
"yml": "",
"toml": "", // nf-md-file_toml
"xml": "󰗀", // nf-md-xml
"html": "", // nf-dev-html5
"css": "", // nf-dev-css3

// Media
"mp3": "", // nf-fa-volume_up (audio)
"wav": "",
"mp4": "", // nf-fa-film (video)
"avi": "",
"mkv": "",

// Go-specific
"mod": "", // nf-md-go (module)
"sum": "",
},
}
}

// BehaviorConfig controls configurable behaviors.
Expand Down Expand Up @@ -58,9 +148,9 @@ type KeyBindings struct {
QuickSearch StringOrList `toml:"quick_search"`

// Go to path
GoTo StringOrList `toml:"goto"`
FuzzyFind StringOrList `toml:"fuzzy_find"`
Bookmarks StringOrList `toml:"bookmarks"`
GoTo StringOrList `toml:"goto"`
FuzzyFind StringOrList `toml:"fuzzy_find"`
Bookmarks StringOrList `toml:"bookmarks"`
Help StringOrList `toml:"help"`
ThemePicker StringOrList `toml:"theme_picker"`
CmdExec StringOrList `toml:"cmd_exec"`
Expand Down Expand Up @@ -88,12 +178,10 @@ func Default() Config {
keys := DefaultKeyBindings()
normalizeAllKeys(&keys)
return Config{
Theme: "",
Behavior: BehaviorConfig{
EnterAction: "edit",
SpaceAction: "preview",
},
Keys: keys,
Theme: "",
Behavior: BehaviorConfig{EnterAction: "edit", SpaceAction: "preview"},
Icons: DefaultIconsConfig(),
Keys: keys,
}
}

Expand Down Expand Up @@ -125,9 +213,9 @@ func DefaultKeyBindings() KeyBindings {

QuickSearch: StringOrList{"ctrl+s"},

GoTo: StringOrList{"ctrl+g"},
FuzzyFind: StringOrList{"f9", "ctrl+p"},
Bookmarks: StringOrList{"f2", "ctrl+b"},
GoTo: StringOrList{"ctrl+g"},
FuzzyFind: StringOrList{"f9", "ctrl+p"},
Bookmarks: StringOrList{"f2", "ctrl+b"},
Help: StringOrList{"f1"},
ThemePicker: StringOrList{"ctrl+t"},
CmdExec: StringOrList{"ctrl+r"},
Expand Down Expand Up @@ -163,6 +251,9 @@ func Load() Config {
mergeKeys(&cfg.Keys, &fileCfg.Keys)
normalizeAllKeys(&cfg.Keys)

// Merge icons config: start with defaults, overlay file config.
mergeIcons(&cfg.Icons, &fileCfg.Icons)

return cfg
}

Expand Down Expand Up @@ -202,6 +293,29 @@ func mergeKey(dst *StringOrList, src StringOrList) {
}
}

// mergeIcons merges src IconsConfig into dst. Non-empty fields from src override dst.
// Extensions are merged (src overrides dst on conflict).
func mergeIcons(dst, src *IconsConfig) {
if src.Folder != "" {
dst.Folder = src.Folder
}
if src.File != "" {
dst.File = src.File
}
// Enabled is only overridden if explicitly set in file config.
// Since the default is false and we want false to mean "use default",
// we always use the file value (false or true).
dst.Enabled = src.Enabled
if src.Extensions != nil {
if dst.Extensions == nil {
dst.Extensions = make(map[string]string)
}
for ext, icon := range src.Extensions {
dst.Extensions[ext] = icon
}
}
}

// normalizeKey converts user-friendly "shift+fN" (N=1..8) to the BubbleTea
// key string "f(N+12)". BubbleTea v1.x reports Shift+F1..F8 as F13..F20.
func normalizeKey(k string) string {
Expand Down
72 changes: 72 additions & 0 deletions internal/ui/panel/icons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package panel

import (
"path/filepath"
"strings"
"unicode/utf8"

"github.com/kooler/MiddayCommander/internal/config"
)

// IconResolver resolves file/directory names to Nerd Font icons based on
// configuration. It is immutable after creation and safe for concurrent use.
type IconResolver struct {
enabled bool
folder string
file string
extIcons map[string]string
}

// NewIconResolver creates a resolver from the given icon configuration.
// If cfg.Enabled is false, the resolver will return empty strings.
func NewIconResolver(cfg config.IconsConfig) IconResolver {
if !cfg.Enabled {
return IconResolver{}
}
r := IconResolver{
enabled: true,
folder: cfg.Folder,
file: cfg.File,
extIcons: cfg.Extensions,
}
if r.folder == "" {
r.folder = "\uF07B" // nf-fa-folder
}
if r.file == "" {
r.file = "\uF016" // nf-fa-file
}
if r.extIcons == nil {
r.extIcons = make(map[string]string)
}
return r
}

// ResolveIcon returns the Nerd Font icon for the given entry.
// Returns an empty string if icons are disabled or no match is found.
// isDir determines whether to use the folder icon or extension-based lookup.
func (r IconResolver) ResolveIcon(name string, isDir bool) string {
if !r.enabled {
return ""
}
if isDir {
return r.folder
}
ext := strings.TrimPrefix(filepath.Ext(name), ".")
if ext == "" {
return r.file
}
if icon, ok := r.extIcons[strings.ToLower(ext)]; ok {
return icon
}
Comment on lines +58 to +60
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting a user extension to "" produces a broken row.

A user who wants to disable the default icon for .go by writing go = "" will get:

Lets pick one of:

  • Treat empty as "fall back to default file icon" (if icon, ok := ...; ok && icon != "" { return icon }), OR
  • Document explicitly that empty strings are reserved / not allowed in [icons.extensions].

return r.file
}

// IconWidth returns the display width (in characters/rune count) of the icon
// that would be rendered for the given entry. Returns 0 if icons are disabled.
func (r IconResolver) IconWidth(name string, isDir bool) int {
icon := r.ResolveIcon(name, isDir)
if icon == "" {
return 0
}
return utf8.RuneCountInString(icon)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many Nerd Font glyphs render as 2 terminal cells (East-Asian Wide / ambiguous-width in some terminals, and several nf-md-* / nf-custom-* glyphs are definitively 2 cells). utf8.RuneCountInString always returns 1 for a single-rune icon and will miscount those, causing the name column to be off by one.

Probably better to use runewidth.StringWidth(icon).

}
Loading