lf/colors.go
2020-12-24 15:42:36 +03:00

265 lines
5.6 KiB
Go

package main
import (
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
)
type styleMap map[string]tcell.Style
func parseStyles() styleMap {
if env := os.Getenv("LS_COLORS"); env != "" {
return parseStylesGNU(env)
}
if env := os.Getenv("LSCOLORS"); env != "" {
return parseStylesBSD(env)
}
// default values from dircolors with removed background colors
defaultColors := []string{
// "rs=0",
"di=01;34",
"ln=01;36",
// "mh=00",
"pi=33", // "pi=40;33",
"so=01;35",
"do=01;35",
"bd=33;01", // "bd=40;33;01",
"cd=33;01", // "cd=40;33;01",
"or=31;01", // "or=40;31;01",
// "mi=00",
"su=01;32", // "su=37;41",
"sg=01;32", // "sg=30;43",
// "ca=30;41",
"tw=01;34", // "tw=30;42",
"ow=01;34", // "ow=34;42",
"st=01;34", // "st=37;44",
"ex=01;32",
}
return parseStylesGNU(strings.Join(defaultColors, ":"))
}
func applyAnsiCodes(s string, st tcell.Style) tcell.Style {
toks := strings.Split(s, ";")
var nums []int
for _, tok := range toks {
if tok == "" {
nums = append(nums, 0)
continue
}
n, err := strconv.Atoi(tok)
if err != nil {
log.Printf("converting escape code: %s", err)
continue
}
nums = append(nums, n)
}
// ECMA-48 details the standard
// TODO: should we support turning off attributes?
// Probably because this is used for previewers too
for i := 0; i < len(nums); i++ {
n := nums[i]
switch {
case n == 0:
st = tcell.StyleDefault
case n == 1:
st = st.Bold(true)
case n == 2:
st = st.Dim(true)
case n == 4:
st = st.Underline(true)
case n == 5 || n == 6:
st = st.Blink(true)
case n == 7:
st = st.Reverse(true)
case n == 8:
// TODO: tcell PR for proper conceal
_, bg, _ := st.Decompose()
st = st.Foreground(bg)
case n == 9:
st = st.StrikeThrough(true)
case n >= 30 && n <= 37:
st = st.Foreground(tcell.PaletteColor(n - 30))
case n == 38:
if i+3 <= len(nums) && nums[i+1] == 5 {
st = st.Foreground(tcell.PaletteColor(nums[i+2]))
i += 2
} else if i+5 <= len(nums) && nums[i+1] == 2 {
st = st.Foreground(tcell.NewRGBColor(
int32(nums[i+2]),
int32(nums[i+3]),
int32(nums[i+4])))
i += 4
} else {
log.Printf("unknown ansi code or incorrect form: %d", n)
}
case n >= 40 && n <= 47:
st = st.Background(tcell.PaletteColor(n - 40))
case n == 48:
if i+3 <= len(nums) && nums[i+1] == 5 {
st = st.Background(tcell.PaletteColor(nums[i+2]))
i += 2
} else if i+5 <= len(nums) && nums[i+1] == 2 {
st = st.Background(tcell.NewRGBColor(
int32(nums[i+2]),
int32(nums[i+3]),
int32(nums[i+4])))
i += 4
} else {
log.Printf("unknown ansi code or incorrect form: %d", n)
}
default:
log.Printf("unknown ansi code: %d", n)
}
}
return st
}
// This function parses $LS_COLORS environment variable.
func parseStylesGNU(env string) styleMap {
styles := make(styleMap)
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 styles
}
key, val := pair[0], pair[1]
key = replaceTilde(key)
if filepath.IsAbs(key) {
key = filepath.Clean(key)
}
styles[key] = applyAnsiCodes(val, tcell.StyleDefault)
}
return styles
}
// This function parses $LSCOLORS environment variable.
func parseStylesBSD(env string) styleMap {
styles := make(styleMap)
if len(env) != 22 {
log.Printf("invalid $LSCOLORS variable: %s", env)
return styles
}
colorNames := []string{"di", "ln", "so", "pi", "ex", "bd", "cd", "su", "sg", "tw", "ow"}
getStyle := func(r1, r2 byte) tcell.Style {
st := tcell.StyleDefault
switch {
case r1 == 'x':
st = st.Foreground(tcell.ColorDefault)
case 'A' <= r1 && r1 <= 'H':
st = st.Foreground(tcell.PaletteColor(int(r1 - 'A'))).Bold(true)
case 'a' <= r1 && r1 <= 'h':
st = st.Foreground(tcell.PaletteColor(int(r1 - 'a')))
default:
log.Printf("invalid $LSCOLORS entry: %c", r1)
return tcell.StyleDefault
}
switch {
case r2 == 'x':
st = st.Background(tcell.ColorDefault)
case 'a' <= r2 && r2 <= 'h':
st = st.Background(tcell.PaletteColor(int(r2 - 'a')))
default:
log.Printf("invalid $LSCOLORS entry: %c", r2)
return tcell.StyleDefault
}
return st
}
for i, key := range colorNames {
styles[key] = getStyle(env[i*2], env[i*2+1])
}
return styles
}
func (cm styleMap) get(f *file) tcell.Style {
if val, ok := cm[f.path]; ok {
return val
}
if f.IsDir() {
if val, ok := cm[f.Name()+"/"]; ok {
return val
}
}
if val, ok := cm[f.Name()]; ok {
return val
}
if val, ok := cm[f.Name()]; ok {
return val
}
if val, ok := cm[filepath.Base(f.Name())+".*"]; ok {
return val
}
var key string
switch {
case f.linkState == working:
key = "ln"
case f.linkState == broken:
key = "or"
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.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
}
if val, ok := cm["fi"]; ok {
return val
}
return tcell.StyleDefault
}