diff --git a/colors.go b/colors.go new file mode 100644 index 0000000..a1db69f --- /dev/null +++ b/colors.go @@ -0,0 +1,215 @@ +package main + +import ( + "log" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/nsf/termbox-go" +) + +type colorEntry struct { + fg termbox.Attribute + bg termbox.Attribute +} + +type colorMap map[string]colorEntry + +func parseColors() colorMap { + if env := os.Getenv("LS_COLORS"); env != "" { + return parseColorsGNU(env) + } + + if env := os.Getenv("LSCOLORS"); env != "" { + return parseColorsBSD(env) + } + + // default values from dircolors + defaultColors := []string{ + // "rs=0", + "di=01;34", + "ln=01;36", + // "mh=00", + "pi=40;33", + "so=01;35", + "do=01;35", + "bd=40;33;01", + "cd=40;33;01", + "or=40;31;01", + // "mi=00", + "su=37;41", + "sg=30;43", + "ca=30;41", + "tw=30;42", + "ow=34;42", + "st=37;44", + "ex=01;32", + } + + return parseColorsGNU(strings.Join(defaultColors, ":")) +} + +func applyAnsiCodes(s string, fg, bg termbox.Attribute) (termbox.Attribute, termbox.Attribute) { + toks := strings.Split(s, ";") + + var nums []int + for _, t := range toks { + if t == "" { + fg = termbox.ColorDefault + bg = termbox.ColorDefault + break + } + n, err := strconv.Atoi(t) + if err != nil { + log.Printf("converting escape code: %s", err) + continue + } + nums = append(nums, n) + } + + // parse 256 color terminal ansi codes + if len(nums) == 3 { + if nums[0] == 48 && nums[1] == 5 { + bg = termbox.Attribute(nums[2] + 1) + } + if nums[0] == 38 && nums[1] == 5 { + fg = termbox.Attribute(nums[2] + 1) + } + } + + for _, n := range nums { + attr, ok := gAnsiCodes[n] + if !ok { + log.Printf("unknown ansi code: %d", n) + continue + } + switch { + case n == 0: + fg, bg = attr, attr + case n == 1 || n == 4 || n == 7: + fg |= attr + case 30 <= n && n <= 37: + fg &= gAnsiColorResetMask + fg |= attr + case 40 <= n && n <= 47: + bg = attr + } + } + + return fg, bg +} + +// This function parses $LS_COLORS environment variable. +func parseColorsGNU(env string) colorMap { + colors := make(colorMap) + + entries := strings.Split(env, ":") + for _, entry := range entries { + if entry == "" { + continue + } + pair := strings.Split(entry, "=") + if len(pair) != 2 { + log.Printf("invalid $LS_COLORS entry: %s", entry) + return colors + } + key, val := pair[0], pair[1] + fg, bg := applyAnsiCodes(val, termbox.ColorDefault, termbox.ColorDefault) + colors[key] = colorEntry{fg: fg, bg: bg} + } + + return colors +} + +// This function parses $LSCOLORS environment variable. +func parseColorsBSD(env string) colorMap { + colors := make(colorMap) + + if len(env) != 22 { + log.Printf("invalid $LSCOLORS variable: %s", env) + return colors + } + + colorNames := []string{"di", "ln", "so", "pi", "ex", "bd", "cd", "su", "sg", "tw", "ow"} + colorCodes := map[byte]termbox.Attribute{ + 'a': termbox.ColorBlack, + 'b': termbox.ColorRed, + 'c': termbox.ColorGreen, + 'd': termbox.ColorYellow, // brown + 'e': termbox.ColorBlue, + 'f': termbox.ColorMagenta, + 'g': termbox.ColorCyan, + 'h': termbox.ColorWhite, // light grey + 'A': termbox.AttrBold | termbox.ColorBlack, + 'B': termbox.AttrBold | termbox.ColorRed, + 'C': termbox.AttrBold | termbox.ColorGreen, + 'D': termbox.AttrBold | termbox.ColorYellow, // brown + 'E': termbox.AttrBold | termbox.ColorBlue, + 'F': termbox.AttrBold | termbox.ColorMagenta, + 'G': termbox.AttrBold | termbox.ColorCyan, + 'H': termbox.AttrBold | termbox.ColorWhite, // light grey + 'x': termbox.ColorDefault, + } + + getColor := func(r byte) termbox.Attribute { + if color, ok := colorCodes[r]; ok { + return color + } + + log.Printf("invalid $LSCOLORS entry: %c", r) + return termbox.ColorDefault + } + + for i, key := range colorNames { + colors[key] = colorEntry{fg: getColor(env[i*2]), bg: getColor(env[i*2+1])} + } + + return colors +} + +func (cm colorMap) get(f *file) (termbox.Attribute, termbox.Attribute) { + var key string + + switch { + case f.IsDir() && f.Mode()&os.ModeSticky != 0 && f.Mode()&0002 != 0: + key = "tw" + case f.IsDir() && f.Mode()&os.ModeSticky != 0: + key = "st" + case f.IsDir() && f.Mode()&0002 != 0: + key = "ow" + case f.IsDir(): + key = "di" + case f.linkState == working: + key = "ln" + case f.linkState == broken: + key = "or" + case f.Mode()&os.ModeNamedPipe != 0: + key = "pi" + case f.Mode()&os.ModeSocket != 0: + key = "so" + case f.Mode()&os.ModeCharDevice != 0: + key = "cd" + case f.Mode()&os.ModeDevice != 0: + key = "bd" + case f.Mode()&os.ModeSetuid != 0: + key = "su" + case f.Mode()&os.ModeSetgid != 0: + key = "sg" + case f.Mode().IsRegular() && f.Mode()&0111 != 0: + key = "ex" + default: + key = "*" + filepath.Ext(f.Name()) + } + + if val, ok := cm[key]; ok { + return val.fg, val.bg + } + + if val, ok := cm["fi"]; ok { + return val.fg, val.bg + } + + return termbox.ColorDefault, termbox.ColorDefault +} diff --git a/doc.go b/doc.go index 96631ed..f0198b6 100644 --- a/doc.go +++ b/doc.go @@ -79,7 +79,7 @@ The following options can be used to customize the behavior of lf: globsearch boolean (default off) hidden boolean (default off) ignorecase boolean (default on) - lscolors boolean (default on if LSCOLORS or LS_COLORS environment variable is set) + lscolors boolean (default on) preview boolean (default on) reverse boolean (default off) smartcase boolean (default on) diff --git a/docstring.go b/docstring.go index 87094d7..9825eac 100644 --- a/docstring.go +++ b/docstring.go @@ -83,7 +83,7 @@ The following options can be used to customize the behavior of lf: globsearch boolean (default off) hidden boolean (default off) ignorecase boolean (default on) - lscolors boolean (default on if LSCOLORS or LS_COLORS environment variable is set) + lscolors boolean (default on) preview boolean (default on) reverse boolean (default off) smartcase boolean (default on) diff --git a/eval.go b/eval.go index 9cc1050..50a6a69 100644 --- a/eval.go +++ b/eval.go @@ -49,10 +49,13 @@ func (e *setExpr) eval(app *app, args []string) { gOpts.ignorecase = !gOpts.ignorecase case "lscolors": gOpts.lscolors = true + app.ui.colors = parseColors() case "nolscolors": gOpts.lscolors = false + app.ui.colors = parseColors() case "lscolors!": gOpts.lscolors = !gOpts.lscolors + app.ui.colors = parseColors() case "preview": gOpts.preview = true case "nopreview": diff --git a/lscolors.go b/lscolors.go deleted file mode 100644 index 5889579..0000000 --- a/lscolors.go +++ /dev/null @@ -1,148 +0,0 @@ -package main - -import ( - "log" - "os" - "strconv" - "strings" - - "github.com/nsf/termbox-go" -) - -func init() { - if lscolors := os.Getenv("LSCOLORS"); lscolors != "" { - gColors.parseBSD(lscolors) - gOpts.lscolors = true - } - if ls_colors := os.Getenv("LS_COLORS"); ls_colors != "" { - gColors.parseLinux(ls_colors) - gOpts.lscolors = true - } -} - -type lsColorsEntry struct { - fg termbox.Attribute - bg termbox.Attribute -} - -type lsColorsT map[string]lsColorsEntry - -var gColors = make(lsColorsT) - -// This function parses LS_COLORS environment variable -func (lsc lsColorsT) parseLinux(env string) { - e := strings.Split(env, ":") - for _, e := range e { - i := strings.IndexRune(e, '=') - if i >= 0 { - key := e[:i] - values := strings.Split(e[i+1:], ";") - var fg, bg termbox.Attribute - for _, a := range values { - i, _ := strconv.Atoi(a) // strconv.Atoi() will returns 0 on error which is termbox.ColorDefault. No need to check the error. - - switch { - case i == 1: - fg = fg | termbox.AttrBold - case i == 4: - fg = fg | termbox.AttrUnderline - case i == 5: - // Flashing text - case i == 7: - fg = fg | termbox.AttrReverse - case i == 8: - // Concealed - case 30 <= i && i <= 37: - fg = fg | termbox.Attribute(i-29) - case 40 <= i && i <= 47: - bg = bg | termbox.Attribute(i-39) - } - } - lsc[key] = lsColorsEntry{fg: fg, bg: bg} - } - } -} - -// This function parses LSCOLORS variable. See http://www.manpages.info/freebsd/ls.1.html -func (lsc lsColorsT) parseBSD(env string) { - if len(env) != 22 { - log.Print("LSCOLORS variable invalid") - return - } - - unixLsColors := []string{"di", "so", "ln", "pi", "ex", "bd", "cd", "su", "sg", "tw", "ow"} - unixColors := map[byte]termbox.Attribute{ - 'a': termbox.ColorBlack, - 'b': termbox.ColorRed, - 'c': termbox.ColorGreen, - 'd': termbox.ColorYellow, // should be brown - 'e': termbox.ColorBlue, - 'f': termbox.ColorMagenta, - 'g': termbox.ColorCyan, - 'h': termbox.ColorWhite, // should be light grey - 'A': termbox.AttrBold | termbox.ColorBlack, - 'B': termbox.AttrBold | termbox.ColorRed, - 'C': termbox.AttrBold | termbox.ColorGreen, - 'D': termbox.AttrBold | termbox.ColorWhite, // brown - 'E': termbox.AttrBold | termbox.ColorBlue, - 'F': termbox.AttrBold | termbox.ColorMagenta, - 'G': termbox.AttrBold | termbox.ColorCyan, - 'H': termbox.AttrBold | termbox.ColorWhite, // light grey - } - - getColor := func(r byte) termbox.Attribute { - if color, ok := unixColors[r]; ok { - return color - } else { - return termbox.ColorDefault - } - } - - for i, key := range unixLsColors { - lsc[key] = lsColorsEntry{fg: getColor(env[i*2]), bg: getColor(env[i*2+1])} - } -} - -// This function returns foreground and background colors for given file -func (lsc lsColorsT) getColors(f *file) (termbox.Attribute, termbox.Attribute) { - var key = "" - - switch { - case f.Mode()&os.ModeSticky != 0: - key = "st" - case f.Mode()&os.ModeSetuid != 0: - key = "su" - case f.Mode()&os.ModeSetgid != 0: - key = "sg" - case f.IsDir(): - key = "di" - case f.linkState == working: - key = "ln" - case f.Mode()&os.ModeNamedPipe != 0: - key = "pi" - case f.Mode()&os.ModeSocket != 0: - key = "so" - case f.Mode()&os.ModeCharDevice != 0: - key = "cd" - case f.Mode()&os.ModeDevice != 0: - key = "bd" - case f.linkState == broken: - key = "or" - case f.Mode().IsRegular() && f.Mode()&0111 != 0: - key = "ex" - default: - if extI := strings.LastIndexByte(f.Name(), '.'); extI > 0 { - key = "*" + f.Name()[extI:] - } - } - - if val, ok := lsc[key]; ok { - return val.fg, val.bg - } else { - if val, ok = lsc["no"]; ok { - return val.fg, val.bg - } else { - return termbox.ColorDefault, termbox.ColorDefault - } - } -} diff --git a/opts.go b/opts.go index aff1548..f04226c 100644 --- a/opts.go +++ b/opts.go @@ -35,7 +35,7 @@ func init() { gOpts.globsearch = false gOpts.hidden = false gOpts.ignorecase = true - // gOpts.lscolors = true this option is initialized in lscolors.go init() depending on whether the corresponding environment variable is set + gOpts.lscolors = true gOpts.preview = true gOpts.reverse = false gOpts.smartcase = true diff --git a/ui.go b/ui.go index bf6e871..909c01c 100644 --- a/ui.go +++ b/ui.go @@ -126,60 +126,6 @@ func (win *win) renew(w, h, x, y int) { win.w, win.h, win.x, win.y = w, h, x, y } -func applyAnsiCodes(s string, fg, bg termbox.Attribute) (termbox.Attribute, termbox.Attribute) { - toks := strings.Split(s, ";") - - var nums []int - for _, t := range toks { - if t == "" { - fg = termbox.ColorDefault - bg = termbox.ColorDefault - break - } - n, err := strconv.Atoi(t) - if err != nil { - log.Printf("converting escape code: %s", err) - continue - } - nums = append(nums, n) - } - - // Parse 256 color terminal ansi codes - // termbox-go has a color offset of one, because of attributes - if len(nums) == 3 { - if nums[0] == 48 && nums[1] == 5 { - bg = termbox.Attribute(nums[2]) - bg++ - } - if nums[0] == 38 && nums[1] == 5 { - fg = termbox.Attribute(nums[2]) - fg++ - } - return fg, bg - } - - for _, n := range nums { - attr, ok := gAnsiCodes[n] - if !ok { - log.Printf("unknown ansi code: %d", n) - continue - } - switch { - case n == 0: - fg, bg = attr, attr - case n == 1 || n == 4 || n == 7: - fg |= attr - case 30 <= n && n <= 37: - fg &= gAnsiColorResetMask - fg |= attr - case 40 <= n && n <= 47: - bg = attr - } - } - - return fg, bg -} - func printLength(s string) int { ind := 0 off := 0 @@ -267,7 +213,7 @@ func (win *win) printReg(reg *reg) { return } -func (win *win) printDir(dir *dir, marks map[string]int, saves map[string]bool) { +func (win *win) printDir(dir *dir, marks map[string]int, saves map[string]bool, colors colorMap) { if win.w < 3 { return } @@ -296,33 +242,7 @@ func (win *win) printDir(dir *dir, marks map[string]int, saves map[string]bool) end := min(beg+win.h, maxind+1) for i, f := range dir.fi[beg:end] { - if gOpts.lscolors { - fg, bg = gColors.getColors(f) - } else { - switch { - case f.linkState == working: - fg = termbox.ColorCyan - if f.Mode().IsDir() { - fg |= termbox.AttrBold - } - case f.linkState == broken: - fg = termbox.ColorMagenta - case f.Mode().IsRegular(): - if f.Mode()&0111 != 0 { - fg = termbox.AttrBold | termbox.ColorGreen - } else { - fg = termbox.ColorDefault - } - case f.Mode().IsDir(): - fg = termbox.AttrBold | termbox.ColorBlue - case f.Mode()&os.ModeNamedPipe != 0: - fg = termbox.ColorRed - case f.Mode()&os.ModeSocket != 0: - fg = termbox.ColorYellow - case f.Mode()&os.ModeDevice != 0: - fg = termbox.ColorWhite - } - } + fg, bg = colors.get(f) path := filepath.Join(dir.path, f.Name()) @@ -429,6 +349,7 @@ type ui struct { cmdBuf []rune keyAcc []rune keyCount []rune + colors colorMap } func getWidths(wtot int) []int { @@ -485,6 +406,7 @@ func newUI() *ui { menuWin: newWin(wtot, 1, 0, htot-2), keyChan: make(chan string, 1000), evChan: evChan, + colors: parseColors(), } } @@ -614,7 +536,7 @@ func (ui *ui) draw(nav *nav) { doff := len(nav.dirs) - length for i := 0; i < length; i++ { - ui.wins[woff+i].printDir(nav.dirs[doff+i], nav.marks, nav.saves) + ui.wins[woff+i].printDir(nav.dirs[doff+i], nav.marks, nav.saves, ui.colors) } switch ui.cmdPrefix { @@ -640,7 +562,7 @@ func (ui *ui) draw(nav *nav) { preview := ui.wins[len(ui.wins)-1] if f.IsDir() { - preview.printDir(ui.dirPrev, nav.marks, nav.saves) + preview.printDir(ui.dirPrev, nav.marks, nav.saves, ui.colors) } else if f.Mode().IsRegular() { preview.printReg(ui.regPrev) }