Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a new colour scheme to parse $LS_COLORS #2

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
168 changes: 168 additions & 0 deletions ls_colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from ranger.gui.colorscheme import ColorScheme
import ranger.gui.color as style
import ranger.gui.context
import ranger.gui.widgets.browsercolumn
from os import getenv

ls_colors = getenv('LS_COLORS').split(':')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I unset LS_COLORS this throws an exception that's uncaught. (getenv('LS_COLORS') is None, so getenv('LS_COLORS').split(':') raises an AttributeError.)

I think you want to check if getenv('LS_COLORS') is None instead of ls_colors. Or catch the exception.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a default to getenv? Otherwise you can use os.environ.get('LS_COLORS', default value). Imo that looks like the proper way to solve this problem.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to look it up myself, getenv('LS_COLORS', default='') looks good. (NB: in two places in the file)

What I didn't realise earlier. It seems the uncaught exception makes ranger fall back to the default colors. Once I add the default value or exception handling in class base(ColorScheme): and when LS_COLORS is not set, then ranger becomes black&white.

I don't think this is what we want here (ls --color falls back to default colors when LS_COLORS is not set).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if we want the default LS_COLORS, we should either let ranger automatically fall back to its default scheme, or we should manually read the default dircolors file or run the dircolors command without arguments to get the default colouring. Also, are we sure that is what ranger really does? (falling back to default LS_COLORS) This could simply be a matter of catching the exception.

I've had trouble however finding the system wide config file that dircolors automatically reads. The man page (man dir_colors) points to either ~/.dircolors (user-defined) or /etc/DIR_COLORS (system-wide). I, however, do not have the system-wide one and yet dircolors is still able to read the colours from somewhere.

Have you had any luck finding whence it gets the colours?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's the best to do. I guess given the name 'ls_colors' it should be as close as reasonable to "what ls does".

ranger gets its default colors from https://github.com/ranger/ranger/blob/master/ranger/colorschemes/default.py which happens to be the same as (my?) ls defaults when LS_COLORS is unset.

I also don't see where my dircolors gets its defaults from.

Given we don't seem to see where dircolors gets its config from (and one would need to check user file, system file, fallback) I think calling dircolors if LS_COLORS is unset is a good handling.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a commit to handle the case where LS_COLORS isn't set.
Scenario cases:

  1. LS_COLORS is defined and parsed
  2. LS_COLORS isn't defined, dircolors is called and we use its output to define the LS_COLORS for the colorscheme
  3. LS_COLORS isn't defined and the dircolors command can't be found. We use a black & white colorscheme.

the 3) is probably not what the user want, but it might be a good idea to let them know something is wrong if neither a LS_COLORS env variable or a dircolors command can be found while using the ls_colors.py scheme.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ranger probably "falls back", actually doesn't change to ls_colors.py, because that throws an error.
The proper behavior would be to do what ls does. The default.py colorscheme doesn't completely match what ls --color does without LS_COLORS set.

I meant to use whatever actually is the default value for LS_COLORS as the default but calling dircolors every time is probably fine too.

Do you really need to duplicate the try/except block for the getenv?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@toonn

Do you really need to duplicate the try/except block for the getenv?

Can you elaborate? I don't see duplication here. Or should this thread be resolved?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refers to an old version. the old file I still have from September has two calls getenv('LS_COLORS'). The current version in this PR does not have that duplication.

if ls_colors is None:
ls_colors = []
ls_colors_keys = [k.split('=')[0] for k in ls_colors if k != '']
ls_colors_keys = [k.split('*.')[1] for k in ls_colors_keys if '*.' in k]

# Add your key names
for key in ls_colors_keys:
ranger.gui.context.CONTEXT_KEYS.append(key)
setattr(ranger.gui.context.Context, key, False)

OLD_HOOK_BEFORE_DRAWING = ranger.gui.widgets.browsercolumn.hook_before_drawing


def new_hook_before_drawing(fsobject, color_list):
for key in ls_colors_keys:
if fsobject.basename.endswith(key):
color_list.append(key)

return OLD_HOOK_BEFORE_DRAWING(fsobject, color_list)


ranger.gui.widgets.browsercolumn.hook_before_drawing = new_hook_before_drawing


class base(ColorScheme):
progress_bar_color = 1
ls_colors = getenv('LS_COLORS').split(':')
if ls_colors is None:
ls_colors = []

ls_colors_keys = [k.split('=') for k in ls_colors if k != '']
tup_ls_colors = []

# Not considering file extensions
# The order of these two block matters, as extensions colouring should take
# precedence over file type
for key in [k for k in ls_colors_keys if '.*' not in k]:
if key[0] == 'fi':
tup_ls_colors += [('file', key[1])]

# Considering files extensions
tup_ls_colors += [(k[0].split('*.')[1], k[1]) for k in ls_colors_keys
if '*.' in k[0]]

for key in [k for k in ls_colors_keys if '.*' not in k]:
if key[0] == 'ex':
tup_ls_colors += [('executable', key[1])]
elif key[0] == 'pi':
tup_ls_colors += [('fifo', key[1])]
elif key[0] == 'ln':
tup_ls_colors += [('link', key[1])]
elif key[0] == 'bd' or key[0] == 'cd':
tup_ls_colors += [('device', key[1])]
elif key[0] == 'so':
tup_ls_colors += [('socket', key[1])]
elif key[0] == 'di':
tup_ls_colors += [('directory', key[1])]

def get_attr_from_lscolors(self, attribute_list):
return_attr = 0
to_del = []

for i, attr in enumerate(attribute_list):
attr = int(attr)
to_del.append(i)
if attr == 1:
return_attr |= style.bold
elif attr == 4:
return_attr |= style.underline
elif attr == 5:
return_attr |= style.blink
elif attr == 7:
return_attr |= style.reverse
elif attr == 8:
return_attr |= style.invisible
else:
to_del.pop(-1)

return return_attr

def get_256_background_color_if_exists(self, attribute_list):
colour256 = False
for i, key in enumerate(attribute_list):
if key == '48' and attribute_list[i + 1] == '5':
colour256 = True
break
if colour256 and len(attribute_list) >= i + 3:
return_colour = int(attribute_list[i + 2])
del attribute_list[i:i + 3]
return return_colour
else:
return None

def get_256_foreground_color_if_exists(self, attribute_list):
colour256 = False
for i, key in enumerate(attribute_list):
if key == '38' and attribute_list[i + 1] == '5':
colour256 = True
break
if colour256 and len(attribute_list) >= i + 3:
return_colour = int(attribute_list[i + 2])
del attribute_list[i:i + 3]
return return_colour
else:
return None

def use(self, context):
fg, bg, attr = style.default_colors

if context.reset:
return style.default_colors

elif context.in_browser:
if context.selected:
attr = style.reverse

# Values found from
# http://www.bigsoft.co.uk/blog/2008/04/11/configuring-ls_colors
for key, t_attributes in self.tup_ls_colors:
if getattr(context, key):
if key == 'executable' and (context.directory or context.link):
continue
t_attributes = t_attributes.split(';')
colour256_fg = self.get_256_foreground_color_if_exists(
t_attributes)
colour256_bg = self.get_256_background_color_if_exists(
t_attributes)
new_attr = self.get_attr_from_lscolors(t_attributes)
if new_attr is not None:
attr |= new_attr

# Now only the non-256 colours should be left.
# Let's fetch them
colour16_fg, colour16_bg = None, None
for colour_val in t_attributes:
colour_val = int(colour_val)
# This is an attribute
if (colour_val >= 30
and colour_val <= 37): # Basic colours
colour16_fg = colour_val - 30
# eight more basic colours
elif (colour_val >= 90 and colour_val <= 97):
colour16_fg = colour_val - 82

elif (colour_val >= 40 and colour_val <= 47):
colour16_bg = colour_val
# eight more basic colours
elif (colour_val >= 90 and colour_val <= 97):
colour16_bg = colour_val

if colour256_fg is not None:
fg = colour256_fg
elif colour16_fg is not None:
fg = colour16_fg

if colour256_bg is not None:
bg = colour256_bg
elif colour16_bg is not None:
bg = colour16_bg

return fg, bg, attr