-
-
Save benoit-pierre/5cebb8bf5210b838ddc45f07da32943a to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*- | |
# pylint: disable=consider-using-f-string,invalid-name,line-too-long,super-with-arguments | |
''' | |
Ranger color-scheme using `$LS_COLORS` / `dircolors`. | |
Originally based on: https://github.com/ranger/colorschemes/raw/a250fe866200940eb06d877a274333a2a54c34f3/ls_colors.py | |
Usage: copy this file to `~/.config/ranger/colorschemes'. The base color-scheme | |
used for non file system entries / the unfocused pane is `default`. To change it, | |
rename this file to `ls_colors_BASE_SCHEME_NAME.py` (e.g. change it to | |
`ls_colors_snow.py` to use the `snow` color-scheme as base). | |
''' | |
import curses | |
import fnmatch | |
import importlib | |
import inspect | |
import os | |
import re | |
import shlex | |
import subprocess | |
import sys | |
import ranger.gui.color as STYLE | |
import ranger.gui.widgets.browsercolumn | |
from ranger.gui.colorscheme import ColorScheme, ColorSchemeError | |
from ranger.gui.context import CONTEXT_KEYS, Context | |
PY2 = sys.version_info[0] == 2 | |
SPECIAL_PATTERNS = { | |
'bd': 'device' , # block device | |
'ca': '' , # cap | |
'cd': 'device' , # char device | |
'cl': '' , # clear end of line | |
'di': 'directory' , # directory | |
'do': '' , # Solaris door | |
'ec': '' , # end color, unused | |
'ex': 'executable', # executable | |
'fi': 'file' , # file | |
'fl': '' , # file, default | |
'lc': '' , # left, unused | |
'ln': 'link' , # symlink | |
'mh': '' , # multi hardlink | |
'mi': '' , # missing file | |
'no': '' , # normal | |
'or': 'orphan' , # orphaned symlink | |
'ow': '' , # other-writable | |
'pi': 'fifo' , # pipe | |
'rc': '' , # right, unused | |
'rs': '' , # reset | |
'sg': '' , # setgid | |
'so': 'socket' , # socket | |
'st': '' , # sticky | |
'su': '' , # setuid | |
'tw': '' , # ow with sticky | |
} | |
STYLE_ATTRIBUTES = { | |
1: STYLE.bold, | |
2: STYLE.dim, | |
# Italic, not always supported, and | |
# not exported by `ranger.gui.color`. | |
3: getattr(curses, 'A_ITALIC', 0), | |
4: STYLE.underline, | |
5: STYLE.blink, | |
# Rapid blink. | |
6: STYLE.blink, | |
7: STYLE.reverse, | |
8: STYLE.invisible, | |
} | |
def parse_terminal_attributes(attribute_list): | |
''' | |
Parse a list of ECMA-48 SGR sequence | |
parameters to sets display attributes. | |
''' | |
fg, bg, return_attr = STYLE.default_colors | |
attribute_iter = iter(attribute_list) | |
for attr in attribute_iter: | |
if attr == 0: | |
# Reset to default. | |
fg, bg, return_attr = STYLE.default_colors | |
elif attr in STYLE_ATTRIBUTES: | |
return_attr |= STYLE_ATTRIBUTES[attr] | |
# Foreground: basic colours. | |
elif 30 <= attr <= 37: | |
fg = attr - 30 | |
# Foreground: 256 colors. | |
elif attr == 38: | |
attr = next(attribute_iter) | |
assert attr == 5 | |
fg = next(attribute_iter) | |
# Background: Basic colours. | |
elif 40 <= attr <= 47: | |
bg = attr - 40 | |
# Background: 256 colors. | |
elif attr == 48: | |
attr = next(attribute_iter) | |
assert attr == 5 | |
bg = next(attribute_iter) | |
# Foreground: bright version of basic colours. | |
elif 90 <= attr <= 97: | |
fg = attr - 90 + STYLE.BRIGHT | |
# Background: bright version of basic colours. | |
elif 100 <= attr <= 107: | |
bg = attr - 100 + STYLE.BRIGHT | |
else: | |
raise ValueError('invalid/unsupported terminal attribute: {}'.format(attr)) | |
return fg, bg, return_attr | |
def get_ls_colors(): | |
''' | |
Return `$LS_COLORS` or call `dircolors` | |
to get the list colors to used. | |
''' | |
spec = os.getenv('LS_COLORS') | |
if spec is None: | |
try: | |
spec = subprocess.check_output(('dircolors', '--csh')) | |
except (subprocess.CalledProcessError, FileNotFoundError): | |
pass | |
else: | |
spec = shlex.split(spec.decode()) | |
assert len(spec) == 3 and spec[0:2] == ['setenv', 'LS_COLORS'], spec | |
spec = spec[-1] | |
if spec is None: | |
spec = '' | |
return spec | |
def parse_ls_colors(spec): | |
''' | |
Parse a `$LS_COLORS` spec. | |
''' | |
ls_colors = [] | |
for entry in spec.strip(':').split(':'): | |
if not entry: | |
continue | |
pattern, t_attributes = entry.split('=', 1) | |
if pattern != 'ln' or t_attributes != 'target': | |
t_attributes = list(map(int, t_attributes.split(';'))) | |
t_attributes = parse_terminal_attributes(t_attributes) | |
ls_colors.append((pattern, t_attributes)) | |
return ls_colors | |
def get_base_colorscheme_name(): | |
''' | |
Find out the name of the base color-scheme to use. | |
''' | |
basename = __name__.rsplit('.', 1)[-1] | |
if basename.startswith('ls_colors_'): | |
return basename[10:] | |
return 'default' | |
def get_colorscheme_class(name): | |
''' | |
Return a color-scheme class by (module) | |
name, checking for a relative one first. | |
''' | |
for module_name in ( | |
(__package__ + '.' if __package__ else '') + name, | |
'ranger.colorschemes.' + name | |
): | |
try: | |
scheme_module = importlib.import_module(module_name) | |
except ImportError: | |
continue | |
for var in scheme_module.__dict__.values(): | |
if var != ColorScheme and \ | |
inspect.isclass(var) and \ | |
issubclass(var, ColorScheme): | |
return var | |
break | |
raise ColorSchemeError("Cannot locate colorscheme `{}'".format(name)) | |
class LsColors(get_colorscheme_class(get_base_colorscheme_name())): | |
__doc__ = __doc__ | |
def __init__(self): | |
super(LsColors, self).__init__() | |
rx_parts = [] | |
self.ls_colors = [] | |
self.pattern_ls_color = [] | |
self.special_ls_color = {} | |
for n, (pattern, t_attributes) in enumerate(parse_ls_colors(get_ls_colors())): | |
self.ls_colors.append(t_attributes) | |
target = SPECIAL_PATTERNS.get(pattern) | |
if target is not None: | |
# Special directives. | |
self.special_ls_color[target] = n | |
continue | |
# Other pattern. | |
pattern = fnmatch.translate(pattern) | |
if PY2: | |
assert pattern.endswith(r'\Z(?ms)') | |
pattern = pattern[:-7] | |
else: | |
assert pattern.startswith('(?s:') and pattern.endswith(r')\Z') | |
pattern = pattern[4:-3] | |
rx_parts.append('(' + pattern + ')') | |
self.pattern_ls_color.append(n) | |
if PY2: | |
# “sorry, but this version only supports 100 named groups”… | |
rx_batch_size = 99 | |
else: | |
rx_batch_size = None | |
self.pattern_rx_list = [] | |
ls_color_offset = -1 | |
while rx_parts: | |
rx_batch = rx_parts[:rx_batch_size] | |
rx_parts = rx_parts[len(rx_batch):] | |
pattern_rx = re.compile('^(?:' + '|'.join(rx_batch) + ')$', | |
re.DOTALL | re.IGNORECASE) | |
self.pattern_rx_list.append((pattern_rx, ls_color_offset)) | |
ls_color_offset += len(rx_batch) | |
self.old_hook_before_drawing = ranger.gui.widgets.browsercolumn.hook_before_drawing | |
ranger.gui.widgets.browsercolumn.hook_before_drawing = self.new_hook_before_drawing | |
def new_hook_before_drawing(self, fsobject, color_list): | |
''' | |
Add an `ls_colorN` tag to `color_list` | |
if there's an applicable LS colors entry. | |
''' | |
color_set = frozenset(color_list) | |
ls_color = None | |
for kind in ( | |
# Check for symlinks first. | |
'link', | |
# Then special files. | |
'device', | |
'fifo', | |
'socket', | |
# Directories before executables (execute bit). | |
'directory', | |
'executable', | |
# And finally standard files. | |
'file', | |
): | |
if kind not in color_set: | |
continue | |
ls_color = self.special_ls_color.get(kind) | |
if kind == 'link': | |
# Orphaned link? | |
if 'bad' in color_set: | |
ls_color = self.special_ls_color.get('orphan', ls_color) | |
# Should we look at the link target instead? | |
if ls_color is not None and self.ls_colors[ls_color] == 'target': | |
ls_color = None | |
elif kind == 'file': | |
# Check for a matching pattern. | |
for pattern_rx, ls_color_offset in self.pattern_rx_list: | |
m = pattern_rx.match(fsobject.basename) | |
if m is not None: | |
ls_color = self.pattern_ls_color[m.lastindex + ls_color_offset] | |
break | |
if ls_color is not None: | |
color_list.append('ls_color' + str(ls_color)) | |
break | |
return self.old_hook_before_drawing(fsobject, color_list) | |
BASE_IGNORED_TAGS = frozenset(( | |
'audio', | |
'container', | |
'document', | |
'image', | |
'media', | |
'video', | |
)) | |
BASE_DECORATION_TAGS = frozenset(( | |
'copied', | |
'cut', | |
'line_number', | |
'marked', | |
'tag_marker', | |
) + tuple(k for k in CONTEXT_KEYS if k.startswith('vcs'))) | |
def use(self, context): | |
''' | |
Return a `(fg, bg, attr)` tuple for colorizing according | |
to `context`: if there's an `ls_colorN` attribute, use that, | |
otherwise, use the base color-scheme. | |
''' | |
tags = set() | |
style = None | |
for k, v in context.__dict__.items(): | |
if k.startswith('ls_color'): | |
style = self.ls_colors[int(k[8:])] | |
elif v and k not in self.BASE_IGNORED_TAGS: | |
tags.add(k) | |
if style is None: | |
# No LS color, use base scheme. | |
return super(LsColors, self).use(context) | |
if not context.in_browser or context.inactive_pane or \ | |
not context.selected and tags & self.BASE_DECORATION_TAGS: | |
# Inactive, or (unselected) decorations, use simplified base scheme. | |
tags -= self.BASE_IGNORED_TAGS | |
return super(LsColors, self).use(Context(tags)) | |
# LS color style, unselected. | |
if 'selected' not in tags: | |
return style | |
# LS color style, selected. | |
fg, bg, attr = style | |
if attr & STYLE.reverse: | |
attr &= ~STYLE.reverse | |
else: | |
attr |= STYLE.reverse | |
if 'main_column' in tags: | |
attr |= STYLE.bold | |
return fg, bg, attr | |
def test(): | |
''' | |
Test the color-scheme: create a temporary directory | |
with a bunch of entries matching the LS colors spec | |
and open ranger on it. | |
''' | |
# pylint: disable=import-outside-toplevel,too-many-locals | |
import random | |
import shutil | |
import socket | |
import stat | |
import tempfile | |
# Randomize a string case. | |
def randcase(s): | |
nchars = len(s) | |
randbits = random.getrandbits(nchars) | |
return ''.join( | |
c.upper() if b == '1' else c.lower() | |
for c, b in zip(s, format(randbits, '0{}b'.format(nchars))) | |
) | |
def mknod(path, kind): | |
os.mknod(path, kind | 0o500) | |
def touch(path, mode=0o644): | |
os.close(os.open(path, os.O_CREAT, mode)) | |
def mklink(path, target): | |
os.symlink(target, path) | |
def mksocket(path): | |
so = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) | |
try: | |
so.bind(path) | |
finally: | |
so.close() | |
tmpdir = tempfile.mkdtemp() | |
try: | |
entry_list = [ | |
(os.mkdir, 'directory'), | |
(os.mkdir, '.hidden_directory'), | |
(os.mkdir, '.git'), | |
(os.mkdir, 'ceci.n\'est.pas.un.jpeg'), | |
(os.mkfifo, 'fifo'), | |
(mksocket, 'socket'), | |
(mklink, 'symlink_to_dir', 'directory'), | |
(mklink, 'symlink_to_executable', 'executable'), | |
(mklink, 'symlink_to_file', 'file'), | |
(mklink, 'symlink_to_missing', 'missing'), | |
(touch, 'executable', 0o755), | |
(touch, 'python_script.py', 0o755), | |
(touch, 'shell_script.sh', 0o755), | |
(touch, '.hidden_file'), | |
(touch, '.hidden.jpeg'), | |
(touch, 'file'), | |
] | |
if os.getuid() == 0: | |
entry_list.extend(( | |
(mknod, 'block_device', stat.S_IFBLK), | |
(mknod, 'char_device', stat.S_IFCHR), | |
)) | |
for entry in filter(None, get_ls_colors().split(':')): | |
pattern = entry.split('=', 1)[0] | |
if pattern not in SPECIAL_PATTERNS: | |
entry_list.append((touch, pattern)) | |
for args in entry_list: | |
fn, name = args[0:2] | |
args = args[2:] | |
fn(os.path.join(tmpdir, name), *args) | |
for unused_retry in range(3): | |
alt_name = randcase(name) | |
if alt_name != name: | |
fn(os.path.join(tmpdir, alt_name), *args) | |
break | |
subprocess.call((sys.executable, '-c', '__import__("ranger").main()', tmpdir)) | |
finally: | |
shutil.rmtree(tmpdir) | |
if __name__ == '__main__': | |
test() |
Copy the file in ~/.config/ranger/colorschemes
. If you want to use another base colorscheme, instead of default
, rename the file accordingly. For example, if you normally use solarized
theme, and want to add ls_colors
support to it, rename the file to ls_colors_solarized.py
. Then change you config to use the theme in question: set colorscheme ls_colors_solarized
.
Running the file directly with Python is for testing how your current ranger render different file types: it wont change your current theme, just create a temporary directory with a bunch of file and open it in ranger.
I stupidly had named the directory colourscheme
with a u
. Working fine now. Just what I was looking for. Excellent, thanks.
Hi… i tried to use your colorscheme with generated colors from https://github.com/sharkdp/vivid but it failing, when i tried to use true-color (24bit) mode of terminal.
Is it possible to support it in ranger?
@Tokariew: ranger themes don't support true-color (see ranger/ranger#690), so no, you'll have to use vivid 8-bit colors mode (vivid -m 8-bit …
).
Hi. How do I use this? I have copied this file to
~/.config/ranger/colorschemes
, but not sure what to do next.~/.config/ranger$ python colourschemes/ls_colors.py
doesn't seem to make any difference to the files I view, including files which I have coloured in mydircolors
file.