Add colors to the path completer
This commit is contained in:
parent
ee5bc8523b
commit
71c942d290
386
kittens/tui/dircolors.py
Normal file
386
kittens/tui/dircolors.py
Normal file
@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# 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
|
||||
""" # }}}
|
||||
|
||||
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)
|
||||
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('%s=%s' % pair for pair in gen_pairs())
|
||||
|
||||
def _format_code(self, text: str, code: str) -> str:
|
||||
val = self.codes.get(code)
|
||||
return '\033[%sm%s\033[%sm' % (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[%sm%s\033[%sm' % (val, text, self.codes.get('rs', '0')) if val else text
|
||||
|
||||
def format_mode(self, text: str, mode: Union[int, os.stat_result]) -> str:
|
||||
if isinstance(mode, os.stat_result):
|
||||
mode = mode.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')
|
||||
|
||||
# special file?
|
||||
# pylint: disable=bad-whitespace
|
||||
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
|
||||
)
|
||||
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, follow_symlinks: bool = False) -> str:
|
||||
try:
|
||||
statbuf = stat_at(path, cwd, follow_symlinks)
|
||||
except OSError:
|
||||
return text
|
||||
return self.format_mode(text, statbuf.st_mode)
|
||||
|
||||
|
||||
def develop() -> None:
|
||||
import sys
|
||||
print(Dircolors()(sys.argv[-1], sys.argv[-1]))
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, Generator, Optional, Sequence, Tuple
|
||||
from typing import Any, Callable, Dict, Generator, Optional, Sequence, Tuple
|
||||
|
||||
from kitty.fast_data_types import wcswidth
|
||||
from kitty.utils import ScreenSize, screen_size_function
|
||||
@ -42,7 +42,7 @@ def find_completions(path: str) -> Generator[str, None, None]:
|
||||
yield from directory_completions(os.path.dirname(path), os.path.dirname(qpath), os.path.basename(qpath))
|
||||
|
||||
|
||||
def print_table(items: Sequence[str], screen_size: ScreenSize) -> None:
|
||||
def print_table(items: Sequence[str], screen_size: ScreenSize, dir_colors: Callable[[str, str], str]) -> None:
|
||||
max_width = 0
|
||||
item_widths = {}
|
||||
for item in items:
|
||||
@ -55,7 +55,7 @@ def print_table(items: Sequence[str], screen_size: ScreenSize) -> None:
|
||||
for item in items:
|
||||
w = item_widths[item]
|
||||
left = col_width - w
|
||||
print(item, ' ' * left, sep='', end='')
|
||||
print(dir_colors(expand_path(item), item), ' ' * left, sep='', end='')
|
||||
at_start = False
|
||||
cr = (cr + 1) % num_of_cols
|
||||
if not cr:
|
||||
@ -73,6 +73,8 @@ class PathCompleter:
|
||||
|
||||
def __enter__(self) -> 'PathCompleter':
|
||||
import readline
|
||||
|
||||
from .dircolors import Dircolors
|
||||
if 'libedit' in readline.__doc__:
|
||||
readline.parse_and_bind("bind -e")
|
||||
readline.parse_and_bind("bind '\t' rl_complete")
|
||||
@ -84,6 +86,7 @@ class PathCompleter:
|
||||
self.original_completer = readline.get_completer()
|
||||
readline.set_completer(self)
|
||||
self.cache: Dict[str, Tuple[str, ...]] = {}
|
||||
self.dircolors = Dircolors()
|
||||
return self
|
||||
|
||||
def format_completions(self, substitution: str, matches: Sequence[str], longest_match_length: int) -> None:
|
||||
@ -101,10 +104,10 @@ class PathCompleter:
|
||||
ss = screen_size_function()()
|
||||
if dirs:
|
||||
print(styled('Directories', bold=True, fg_intense=True))
|
||||
print_table(dirs, ss)
|
||||
print_table(dirs, ss, self.dircolors)
|
||||
if files:
|
||||
print(styled('Files', bold=True, fg_intense=True))
|
||||
print_table(files, ss)
|
||||
print_table(files, ss, self.dircolors)
|
||||
|
||||
buf = readline.get_line_buffer()
|
||||
x = readline.get_endidx()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user