1package main
2
3import (
4	"log"
5	"os"
6	"path/filepath"
7	"strconv"
8	"strings"
9
10	"github.com/gdamore/tcell/v2"
11)
12
13type styleMap map[string]tcell.Style
14
15func parseStyles() styleMap {
16	sm := make(styleMap)
17
18	// Default values from dircolors
19	//
20	// no*  NORMAL                 00
21	// fi   FILE                   00
22	// rs*  RESET                  0
23	// di   DIR                    01;34
24	// ln   LINK                   01;36
25	// mh*  MULTIHARDLINK          00
26	// pi   FIFO                   40;33
27	// so   SOCK                   01;35
28	// do*  DOOR                   01;35
29	// bd   BLK                    40;33;01
30	// cd   CHR                    40;33;01
31	// or   ORPHAN                 40;31;01
32	// mi*  MISSING                00
33	// su   SETUID                 37;41
34	// sg   SETGID                 30;43
35	// ca*  CAPABILITY             30;41
36	// tw   STICKY_OTHER_WRITABLE  30;42
37	// ow   OTHER_WRITABLE         34;42
38	// st   STICKY                 37;44
39	// ex   EXEC                   01;32
40	//
41	// (Entries marked with * are not implemented in lf)
42
43	// default values from dircolors with background colors removed
44	defaultColors := []string{
45		"fi=00",
46		"di=01;34",
47		"ln=01;36",
48		"pi=33",
49		"so=01;35",
50		"bd=33;01",
51		"cd=33;01",
52		"or=31;01",
53		"su=01;32",
54		"sg=01;32",
55		"tw=01;34",
56		"ow=01;34",
57		"st=01;34",
58		"ex=01;32",
59	}
60
61	sm.parseGNU(strings.Join(defaultColors, ":"))
62
63	if env := os.Getenv("LSCOLORS"); env != "" {
64		sm.parseBSD(env)
65	}
66
67	if env := os.Getenv("LS_COLORS"); env != "" {
68		sm.parseGNU(env)
69	}
70
71	if env := os.Getenv("LF_COLORS"); env != "" {
72		sm.parseGNU(env)
73	}
74
75	return sm
76}
77
78func applyAnsiCodes(s string, st tcell.Style) tcell.Style {
79	toks := strings.Split(s, ";")
80
81	var nums []int
82	for _, tok := range toks {
83		if tok == "" {
84			nums = append(nums, 0)
85			continue
86		}
87		n, err := strconv.Atoi(tok)
88		if err != nil {
89			log.Printf("converting escape code: %s", err)
90			continue
91		}
92		nums = append(nums, n)
93	}
94
95	// ECMA-48 details the standard
96	// TODO: should we support turning off attributes?
97	//    Probably because this is used for previewers too
98	for i := 0; i < len(nums); i++ {
99		n := nums[i]
100		switch {
101		case n == 0:
102			st = tcell.StyleDefault
103		case n == 1:
104			st = st.Bold(true)
105		case n == 2:
106			st = st.Dim(true)
107		case n == 4:
108			st = st.Underline(true)
109		case n == 5 || n == 6:
110			st = st.Blink(true)
111		case n == 7:
112			st = st.Reverse(true)
113		case n == 8:
114			// TODO: tcell PR for proper conceal
115			_, bg, _ := st.Decompose()
116			st = st.Foreground(bg)
117		case n == 9:
118			st = st.StrikeThrough(true)
119		case n >= 30 && n <= 37:
120			st = st.Foreground(tcell.PaletteColor(n - 30))
121		case n >= 90 && n <= 97:
122			st = st.Foreground(tcell.PaletteColor(n - 82))
123		case n == 38:
124			if i+3 <= len(nums) && nums[i+1] == 5 {
125				st = st.Foreground(tcell.PaletteColor(nums[i+2]))
126				i += 2
127			} else if i+5 <= len(nums) && nums[i+1] == 2 {
128				st = st.Foreground(tcell.NewRGBColor(
129					int32(nums[i+2]),
130					int32(nums[i+3]),
131					int32(nums[i+4])))
132				i += 4
133			} else {
134				log.Printf("unknown ansi code or incorrect form: %d", n)
135			}
136		case n >= 40 && n <= 47:
137			st = st.Background(tcell.PaletteColor(n - 40))
138		case n >= 100 && n <= 107:
139			st = st.Background(tcell.PaletteColor(n - 92))
140		case n == 48:
141			if i+3 <= len(nums) && nums[i+1] == 5 {
142				st = st.Background(tcell.PaletteColor(nums[i+2]))
143				i += 2
144			} else if i+5 <= len(nums) && nums[i+1] == 2 {
145				st = st.Background(tcell.NewRGBColor(
146					int32(nums[i+2]),
147					int32(nums[i+3]),
148					int32(nums[i+4])))
149				i += 4
150			} else {
151				log.Printf("unknown ansi code or incorrect form: %d", n)
152			}
153		default:
154			log.Printf("unknown ansi code: %d", n)
155		}
156	}
157
158	return st
159}
160
161// This function parses $LS_COLORS environment variable.
162func (sm styleMap) parseGNU(env string) {
163	for _, entry := range strings.Split(env, ":") {
164		if entry == "" {
165			continue
166		}
167
168		pair := strings.Split(entry, "=")
169
170		if len(pair) != 2 {
171			log.Printf("invalid $LS_COLORS entry: %s", entry)
172			return
173		}
174
175		key, val := pair[0], pair[1]
176
177		key = replaceTilde(key)
178
179		if filepath.IsAbs(key) {
180			key = filepath.Clean(key)
181		}
182
183		sm[key] = applyAnsiCodes(val, tcell.StyleDefault)
184	}
185}
186
187// This function parses $LSCOLORS environment variable.
188func (sm styleMap) parseBSD(env string) {
189	if len(env) != 22 {
190		log.Printf("invalid $LSCOLORS variable: %s", env)
191		return
192	}
193
194	colorNames := []string{"di", "ln", "so", "pi", "ex", "bd", "cd", "su", "sg", "tw", "ow"}
195
196	getStyle := func(r1, r2 byte) tcell.Style {
197		st := tcell.StyleDefault
198
199		switch {
200		case r1 == 'x':
201			st = st.Foreground(tcell.ColorDefault)
202		case 'A' <= r1 && r1 <= 'H':
203			st = st.Foreground(tcell.PaletteColor(int(r1 - 'A'))).Bold(true)
204		case 'a' <= r1 && r1 <= 'h':
205			st = st.Foreground(tcell.PaletteColor(int(r1 - 'a')))
206		default:
207			log.Printf("invalid $LSCOLORS entry: %c", r1)
208			return tcell.StyleDefault
209		}
210
211		switch {
212		case r2 == 'x':
213			st = st.Background(tcell.ColorDefault)
214		case 'a' <= r2 && r2 <= 'h':
215			st = st.Background(tcell.PaletteColor(int(r2 - 'a')))
216		default:
217			log.Printf("invalid $LSCOLORS entry: %c", r2)
218			return tcell.StyleDefault
219		}
220
221		return st
222	}
223
224	for i, key := range colorNames {
225		sm[key] = getStyle(env[i*2], env[i*2+1])
226	}
227}
228
229func (sm styleMap) get(f *file) tcell.Style {
230	if val, ok := sm[f.path]; ok {
231		return val
232	}
233
234	if f.IsDir() {
235		if val, ok := sm[f.Name()+"/"]; ok {
236			return val
237		}
238	}
239
240	var key string
241
242	switch {
243	case f.linkState == working:
244		key = "ln"
245	case f.linkState == broken:
246		key = "or"
247	case f.IsDir() && f.Mode()&os.ModeSticky != 0 && f.Mode()&0002 != 0:
248		key = "tw"
249	case f.IsDir() && f.Mode()&0002 != 0:
250		key = "ow"
251	case f.IsDir() && f.Mode()&os.ModeSticky != 0:
252		key = "st"
253	case f.IsDir():
254		key = "di"
255	case f.Mode()&os.ModeNamedPipe != 0:
256		key = "pi"
257	case f.Mode()&os.ModeSocket != 0:
258		key = "so"
259	case f.Mode()&os.ModeDevice != 0:
260		key = "bd"
261	case f.Mode()&os.ModeCharDevice != 0:
262		key = "cd"
263	case f.Mode()&os.ModeSetuid != 0:
264		key = "su"
265	case f.Mode()&os.ModeSetgid != 0:
266		key = "sg"
267	case f.Mode()&0111 != 0:
268		key = "ex"
269	}
270
271	if val, ok := sm[key]; ok {
272		return val
273	}
274
275	if val, ok := sm[f.Name()+"*"]; ok {
276		return val
277	}
278
279	if val, ok := sm[filepath.Base(f.Name())+".*"]; ok {
280		return val
281	}
282
283	if val, ok := sm["*"+f.ext]; ok {
284		return val
285	}
286
287	if val, ok := sm["fi"]; ok {
288		return val
289	}
290
291	return tcell.StyleDefault
292}
293