kitty/kittens/tui/dircolors.py
2022-01-29 20:23:34 +08:00

385 lines
10 KiB
Python

#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
import stat
from contextlib import suppress
from typing import Dict, Generator, Optional, Tuple, Union
DEFAULT_DIRCOLORS = r"""# {{{
# Configuration file for dircolors, a utility to help you set the
# LS_COLORS environment variable used by GNU ls with the --color option.
# Copyright (C) 1996-2019 Free Software Foundation, Inc.
# Copying and distribution of this file, with or without modification,
# are permitted provided the copyright notice and this notice are preserved.
# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the
# slackware version of dircolors) are recognized but ignored.
# Below are TERM entries, which can be a glob patterns, to match
# against the TERM environment variable to determine if it is colorizable.
TERM Eterm
TERM ansi
TERM *color*
TERM con[0-9]*x[0-9]*
TERM cons25
TERM console
TERM cygwin
TERM dtterm
TERM gnome
TERM hurd
TERM jfbterm
TERM konsole
TERM kterm
TERM linux
TERM linux-c
TERM mlterm
TERM putty
TERM rxvt*
TERM screen*
TERM st
TERM terminator
TERM tmux*
TERM vt100
TERM xterm*
# Below are the color init strings for the basic file types.
# One can use codes for 256 or more colors supported by modern terminals.
# The default color codes use the capabilities of an 8 color terminal
# with some additional attributes as per the following codes:
# Attribute codes:
# 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed
# Text color codes:
# 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white
# Background color codes:
# 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white
#NORMAL 00 # no color code at all
#FILE 00 # regular file: use no color at all
RESET 0 # reset to "normal" color
DIR 01;34 # directory
LINK 01;36 # symbolic link. (If you set this to 'target' instead of a
# numerical value, the color is as for the file pointed to.)
MULTIHARDLINK 00 # regular file with more than one link
FIFO 40;33 # pipe
SOCK 01;35 # socket
DOOR 01;35 # door
BLK 40;33;01 # block device driver
CHR 40;33;01 # character device driver
ORPHAN 40;31;01 # symlink to nonexistent file, or non-stat'able file ...
MISSING 00 # ... and the files they point to
SETUID 37;41 # file that is setuid (u+s)
SETGID 30;43 # file that is setgid (g+s)
CAPABILITY 30;41 # file with capability
STICKY_OTHER_WRITABLE 30;42 # dir that is sticky and other-writable (+t,o+w)
OTHER_WRITABLE 34;42 # dir that is other-writable (o+w) and not sticky
STICKY 37;44 # dir with the sticky bit set (+t) and not other-writable
# This is for files with execute permission:
EXEC 01;32
# List any file extensions like '.gz' or '.tar' that you would like ls
# to colorize below. Put the extension, a space, and the color init string.
# (and any comments you want to add after a '#')
# If you use DOS-style suffixes, you may want to uncomment the following:
#.cmd 01;32 # executables (bright green)
#.exe 01;32
#.com 01;32
#.btm 01;32
#.bat 01;32
# Or if you want to colorize scripts even if they do not have the
# executable bit actually set.
#.sh 01;32
#.csh 01;32
# archives or compressed (bright red)
.tar 01;31
.tgz 01;31
.arc 01;31
.arj 01;31
.taz 01;31
.lha 01;31
.lz4 01;31
.lzh 01;31
.lzma 01;31
.tlz 01;31
.txz 01;31
.tzo 01;31
.t7z 01;31
.zip 01;31
.z 01;31
.dz 01;31
.gz 01;31
.lrz 01;31
.lz 01;31
.lzo 01;31
.xz 01;31
.zst 01;31
.tzst 01;31
.bz2 01;31
.bz 01;31
.tbz 01;31
.tbz2 01;31
.tz 01;31
.deb 01;31
.rpm 01;31
.jar 01;31
.war 01;31
.ear 01;31
.sar 01;31
.rar 01;31
.alz 01;31
.ace 01;31
.zoo 01;31
.cpio 01;31
.7z 01;31
.rz 01;31
.cab 01;31
.wim 01;31
.swm 01;31
.dwm 01;31
.esd 01;31
# image formats
.jpg 01;35
.jpeg 01;35
.mjpg 01;35
.mjpeg 01;35
.gif 01;35
.bmp 01;35
.pbm 01;35
.pgm 01;35
.ppm 01;35
.tga 01;35
.xbm 01;35
.xpm 01;35
.tif 01;35
.tiff 01;35
.png 01;35
.svg 01;35
.svgz 01;35
.mng 01;35
.pcx 01;35
.mov 01;35
.mpg 01;35
.mpeg 01;35
.m2v 01;35
.mkv 01;35
.webm 01;35
.ogm 01;35
.mp4 01;35
.m4v 01;35
.mp4v 01;35
.vob 01;35
.qt 01;35
.nuv 01;35
.wmv 01;35
.asf 01;35
.rm 01;35
.rmvb 01;35
.flc 01;35
.avi 01;35
.fli 01;35
.flv 01;35
.gl 01;35
.dl 01;35
.xcf 01;35
.xwd 01;35
.yuv 01;35
.cgm 01;35
.emf 01;35
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.ogv 01;35
.ogx 01;35
# audio formats
.aac 00;36
.au 00;36
.flac 00;36
.m4a 00;36
.mid 00;36
.midi 00;36
.mka 00;36
.mp3 00;36
.mpc 00;36
.ogg 00;36
.ra 00;36
.wav 00;36
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.oga 00;36
.opus 00;36
.spx 00;36
.xspf 00;36
""" # }}}
# special file?
special_types = (
(stat.S_IFLNK, 'ln'), # symlink
(stat.S_IFIFO, 'pi'), # pipe (FIFO)
(stat.S_IFSOCK, 'so'), # socket
(stat.S_IFBLK, 'bd'), # block device
(stat.S_IFCHR, 'cd'), # character device
(stat.S_ISUID, 'su'), # setuid
(stat.S_ISGID, 'sg'), # setgid
)
CODE_MAP = {
'RESET': 'rs',
'DIR': 'di',
'LINK': 'ln',
'MULTIHARDLINK': 'mh',
'FIFO': 'pi',
'SOCK': 'so',
'DOOR': 'do',
'BLK': 'bd',
'CHR': 'cd',
'ORPHAN': 'or',
'MISSING': 'mi',
'SETUID': 'su',
'SETGID': 'sg',
'CAPABILITY': 'ca',
'STICKY_OTHER_WRITABLE': 'tw',
'OTHER_WRITABLE': 'ow',
'STICKY': 'st',
'EXEC': 'ex',
}
def stat_at(file: str, cwd: Optional[Union[int, str]] = None, follow_symlinks: bool = False) -> os.stat_result:
dirfd: Optional[int] = None
need_to_close = False
if isinstance(cwd, str):
dirfd = os.open(cwd, os.O_RDONLY | getattr(os, 'O_CLOEXEC', 0))
need_to_close = True
elif isinstance(cwd, int):
dirfd = cwd
try:
return os.stat(file, dir_fd=dirfd, follow_symlinks=follow_symlinks)
finally:
if need_to_close and dirfd is not None:
os.close(dirfd)
class Dircolors:
def __init__(self) -> None:
self.codes: Dict[str, str] = {}
self.extensions: Dict[str, str] = {}
if not self.load_from_environ() and not self.load_from_file():
self.load_defaults()
def clear(self) -> None:
self.codes.clear()
self.extensions.clear()
def load_from_file(self) -> bool:
for candidate in (os.path.expanduser('~/.dir_colors'), '/etc/DIR_COLORS'):
with suppress(Exception):
with open(candidate) as f:
return self.load_from_dircolors(f.read())
return False
def load_from_lscolors(self, lscolors: str) -> bool:
self.clear()
if not lscolors:
return False
for item in lscolors.split(':'):
try:
code, color = item.split('=', 1)
except ValueError:
continue
if code.startswith('*.'):
self.extensions[code[1:]] = color
else:
self.codes[code] = color
return bool(self.codes or self.extensions)
def load_from_environ(self, envvar: str = 'LS_COLORS') -> bool:
return self.load_from_lscolors(os.environ.get(envvar) or '')
def load_from_dircolors(self, database: str, strict: bool = False) -> bool:
self.clear()
for line in database.splitlines():
line = line.split('#')[0].strip()
if not line:
continue
split = line.split()
if len(split) != 2:
if strict:
raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
continue
key, val = split
if key == 'TERM':
continue
if key in CODE_MAP:
self.codes[CODE_MAP[key]] = val
elif key.startswith('.'):
self.extensions[key] = val
elif strict:
raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
return bool(self.codes or self.extensions)
def load_defaults(self) -> bool:
self.clear()
return self.load_from_dircolors(DEFAULT_DIRCOLORS, True)
def generate_lscolors(self) -> str:
""" Output the database in the format used by the LS_COLORS environment variable. """
def gen_pairs() -> Generator[Tuple[str, str], None, None]:
for pair in self.codes.items():
yield pair
for pair in self.extensions.items():
# change .xyz to *.xyz
yield '*' + pair[0], pair[1]
return ':'.join('{}={}'.format(*pair) for pair in gen_pairs())
def _format_code(self, text: str, code: str) -> str:
val = self.codes.get(code)
return '\033[{}m{}\033[{}m'.format(val, text, self.codes.get('rs', '0')) if val else text
def _format_ext(self, text: str, ext: str) -> str:
val = self.extensions.get(ext, '0')
return '\033[{}m{}\033[{}m'.format(val, text, self.codes.get('rs', '0')) if val else text
def format_mode(self, text: str, sr: os.stat_result) -> str:
mode = sr.st_mode
if stat.S_ISDIR(mode):
if (mode & (stat.S_ISVTX | stat.S_IWOTH)) == (stat.S_ISVTX | stat.S_IWOTH):
# sticky and world-writable
return self._format_code(text, 'tw')
if mode & stat.S_ISVTX:
# sticky but not world-writable
return self._format_code(text, 'st')
if mode & stat.S_IWOTH:
# world-writable but not sticky
return self._format_code(text, 'ow')
# normal directory
return self._format_code(text, 'di')
for mask, code in special_types:
if (mode & mask) == mask:
return self._format_code(text, code)
# executable file?
if mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
return self._format_code(text, 'ex')
# regular file, format according to its extension
ext = os.path.splitext(text)[1]
if ext:
return self._format_ext(text, ext)
return text
def __call__(self, path: str, text: str, cwd: Optional[Union[int, str]] = None) -> str:
follow_symlinks = self.codes.get('ln') == 'target'
try:
sr = stat_at(path, cwd, follow_symlinks)
except OSError:
return text
return self.format_mode(text, sr)
def develop() -> None:
import sys
print(Dircolors()(sys.argv[-1], sys.argv[-1]))