mirror of
https://github.com/kovidgoyal/kitty.git
synced 2024-11-15 07:04:31 +03:00
385 lines
10 KiB
Python
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]))
|