diff options
-rw-r--r-- | README | 6 | ||||
-rw-r--r-- | ranger/container/settingobject.py | 1 | ||||
-rw-r--r-- | ranger/core/actions.py | 9 | ||||
-rw-r--r-- | ranger/core/fm.py | 6 | ||||
-rw-r--r-- | ranger/core/main.py | 1 | ||||
-rw-r--r-- | ranger/core/shared.py | 6 | ||||
-rwxr-xr-x | ranger/data/scope.sh | 49 | ||||
-rw-r--r-- | ranger/defaults/options.py | 7 | ||||
-rw-r--r-- | ranger/fsobject/file.py | 27 | ||||
-rw-r--r-- | ranger/gui/ansi.py | 94 | ||||
-rw-r--r-- | ranger/gui/curses_shortcuts.py | 9 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 2 | ||||
-rw-r--r-- | ranger/gui/widgets/pager.py | 28 | ||||
-rw-r--r-- | test/tc_ansi.py | 40 |
14 files changed, 261 insertions, 24 deletions
diff --git a/README b/README index 5534a26a..44c03513 100644 --- a/README +++ b/README @@ -59,6 +59,12 @@ Optional: * The "file" program * A pager ("less" by default) +For scope.sh: (enhanced file previews) +* img2txt (from caca-utils) for previewing images +* highlight for syntax highlighting of code +* atool for previews of archives +* lynx or elinks for previews of html pages + Getting Started --------------- diff --git a/ranger/container/settingobject.py b/ranger/container/settingobject.py index 008b846d..c8bd8b49 100644 --- a/ranger/container/settingobject.py +++ b/ranger/container/settingobject.py @@ -35,6 +35,7 @@ ALLOWED_SETTINGS = { 'mouse_enabled': bool, 'preview_directories': bool, 'preview_files': bool, + 'preview_script': (str, type(None)), 'save_console_history': bool, 'scroll_offset': int, 'shorten_title': int, # Note: False is an instance of int diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 2e775848..1dc51502 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -568,13 +568,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): if not hasattr(self.ui, 'open_embedded_pager'): return - try: - f = open(self.env.cf.path, 'r') - except: - pass - else: - pager = self.ui.open_embedded_pager() - pager.set_source(f) + pager = self.ui.open_embedded_pager() + pager.set_source(self.env.cf.get_preview_source(pager)) # -------------------------- # -- Tabs diff --git a/ranger/core/fm.py b/ranger/core/fm.py index cd1c73b6..84fea956 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -121,6 +121,12 @@ class FM(Actions, SignalDispatcher): self.input_blocked = False return self.input_blocked + def copy_config_files(self): + if not (ranger.arg.clean or os.path.exists(self.confpath('scope.sh'))): + import shutil + shutil.copy(self.relpath('data/scope.sh'), + self.confpath('scope.sh')) + def confpath(self, *paths): """returns the path relative to rangers configuration directory""" if ranger.arg.clean: diff --git a/ranger/core/main.py b/ranger/core/main.py index 42118516..ed555a8d 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -65,6 +65,7 @@ def main(): # Initialize objects EnvironmentAware.env = Environment(target) fm = FM() + fm.copy_config_files() fm.tabs = dict((n+1, os.path.abspath(path)) for n, path \ in enumerate(targets[:9])) load_settings(fm, arg.clean) diff --git a/ranger/core/shared.py b/ranger/core/shared.py index b91445a3..175395ec 100644 --- a/ranger/core/shared.py +++ b/ranger/core/shared.py @@ -54,6 +54,12 @@ class SettingsAware(Awareness): settings.signal_bind('setopt.colorscheme', _colorscheme_name_to_class, priority=1) + def postprocess_paths(signal): + import os + signal.value = os.path.expanduser(signal.value) + settings.signal_bind('setopt.preview_script', + postprocess_paths, priority=1) + if not clean: # add the custom options to the list of setting sources sys.path[0:0] = [ranger.arg.confdir] diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh new file mode 100755 index 00000000..80fd2ea7 --- /dev/null +++ b/ranger/data/scope.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# This script is called whenever you preview a file. +# Its output is used as the preview. ANSI color codes are supported. + +# NOTE: This is considered to be a configuration file. If you upgrade +# ranger, it will be left untouched. (You must update it yourself) + +# Meanings of arguments: +# name | meaning +# -----+-------------------------------------------------------- +# $1 | Full filename of the selected file +# $2 | Width of the preview pane (number of fitting characters) +# $3 | Height of the preview pane (number of fitting characters) + +# Meanings of exit codes: +# code | meaning | action of ranger +# -----+------------+------------------------------------------- +# 0 | success | display stdout as preview +# 1 | no preview | display no preview at all +# 2 | plain text | display the plain content of the file + +mimetype=$(file --mime-type -Lb "$1") +extension=$(echo "$1" | grep '\.' | grep -o '[^.]\+$') + +case "$extension" in + # Archive extensions: + tar|gz|tgz|bz|tbz|bz2|tbz2|Z|tZ|lzo|tzo|lz|tlz|xz|txz|7z|t7z|\ + zip|jar|war|rar|lha|lzh|alz|ace|a|arj|arc|rpm|cab|lzma|rz|cpio) + atool -l "$1" || exit 1 + exit 0;; + # HTML Pages: + htm|html|xhtml) + lynx -dump "$1" || elinks -dump "$1" || exit 1 + exit 0;; +esac + +case "$mimetype" in + # Syntax highlight for text files: + text/* | */xml) + highlight --ansi "$1" || cat "$1" || exit 1 + exit 0;; + # Ascii-previews of images: + image/*) + img2txt -W "$2" "$1" || exit 1 + exit 0;; +esac + +exit 1 diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index 126d4bad..3c20c6fb 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -39,6 +39,13 @@ hidden_filter = regexp( r'^\.|\.(?:pyc|pyo|bak|swp)$|~$|lost\+found') show_hidden = False +# Which script is used to generate file previews? +#preview_script = None + +# Ranger ships with scope.sh, a script that calls external programs (see +# README for dependencies) to preview images, archives, etc. +preview_script = '~/.config/ranger/scope.sh' + # Show dotfiles in the bookmark preview box? show_hidden_bookmarks = True diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py index 5e79c2d1..57d82b31 100644 --- a/ranger/fsobject/file.py +++ b/ranger/fsobject/file.py @@ -14,8 +14,9 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import re -import zipfile from ranger.fsobject import FileSystemObject +from subprocess import Popen, PIPE +from ranger.core.runner import devnull N_FIRST_BYTES = 20 control_characters = set(chr(n) for n in @@ -28,12 +29,10 @@ PREVIEW_BLACKLIST = re.compile(r""" # one character extensions: [oa] # media formats: - | avi | [mj]pe?g | mp\d | og[gmv] | wm[av] | mkv | flv - | png | bmp | vob | wav | mpc | flac | divx? | xcf | pdf + | avi | mpe?g | mp\d | og[gmv] | wm[av] | mkv | flv + | vob | wav | mpc | flac | divx? | xcf | pdf # binary files: | torrent | class | so | img | py[co] | dmg - # containers: - | iso | rar | 7z | tar | gz | bz2 | tgz ) # ignore filetype-independent suffixes: (\.part|\.bak|~)? @@ -80,17 +79,27 @@ class File(FileSystemObject): return False if not self.accessible: return False + if self.image or self.container: + return True if PREVIEW_WHITELIST.search(self.basename): return True if PREVIEW_BLACKLIST.search(self.basename): return False if self.path == '/dev/core' or self.path == '/proc/kcore': return False - if self.extension not in ('zip',) and self.is_binary(): + if self.is_binary(): return False return True - def get_preview_source(self): - if self.extension == 'zip': - return '\n'.join(zipfile.ZipFile(self.path).namelist()) + def get_preview_source(self, widget): + if self.fm.settings.preview_script: + try: + p = Popen([self.fm.settings.preview_script, self.path, + str(widget.wid), str(widget.hei)], + stdout=PIPE, stderr=devnull) + if p.poll(): # nonzero exit code + return None + return p.stdout + except: + pass return open(self.path, 'r') diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py new file mode 100644 index 00000000..a9b37665 --- /dev/null +++ b/ranger/gui/ansi.py @@ -0,0 +1,94 @@ +# Copyright (C) 2010 David Barnett <davidbarnett2@gmail.com> +# Copyright (C) 2010 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from ranger.gui import color +import re + +ansi_re = re.compile('(\033' + r'\[\d*(?:;\d+)*?[a-zA-Z])') +reset = '\033[0m' + +def split_ansi_from_text(ansi_text): + return ansi_re.split(ansi_text) + +def text_with_fg_bg_attr(ansi_text): + for chunk in split_ansi_from_text(ansi_text): + if chunk and chunk[0] == '\033': + if chunk[-1] != 'm': + continue + match = re.match(r'^.\[(.*).$', chunk) + attr_args = match.group(1) + fg, bg, attr = -1, -1, 0 + + # Convert arguments to attributes/colors + for arg in attr_args.split(';'): + try: + n = int(arg) + except: + if arg == '': + n = 0 + else: + continue + if n == 0: + fg, bg, attr = -1, -1, 0 + elif n == 1: + attr |= color.bold + elif n == 4: + attr |= color.underline + elif n == 7: + attr |= color.reverse + elif n == 8: + attr |= color.invisible + elif n >= 30 and n <= 37: + fg = n - 30 + elif n == 39: + fg = -1 + elif n >= 40 and n <= 47: + bg = n - 40 + elif n == 49: + bg = -1 + yield (fg, bg, attr) + else: + yield chunk + +def char_len(ansi_text): + return len(ansi_re.sub('', ansi_text)) + +def char_slice(ansi_text, start, end): + slice_chunks = [] + # skip to start + last_color = None + skip_len_left = start + len_left = end - start + for chunk in split_ansi_from_text(ansi_text): + m = ansi_re.match(chunk) + if m: + if chunk[-1] == 'm': + last_color = chunk + else: + if skip_len_left > len(chunk): + skip_len_left -= len(chunk) + else: # finished skipping to start + if skip_len_left > 0: + chunk = chunk[skip_len_left:] + chunk_left = chunk[:len_left] + if len(chunk_left): + if last_color is not None: + slice_chunks.append(last_color) + slice_chunks.append(chunk_left) + len_left -= len(chunk_left) + if len_left == 0: + break + return ''.join(slice_chunks) diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py index 4ed348fd..bae03adc 100644 --- a/ranger/gui/curses_shortcuts.py +++ b/ranger/gui/curses_shortcuts.py @@ -1,4 +1,5 @@ # Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# Copyright (C) 2010 David Barnett <davidbarnett2@gmail.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,9 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import curses import _curses from ranger.ext.iter_tools import flatten +from ranger.gui.color import get_color from ranger.core.shared import SettingsAware def ascii_only(string): @@ -95,6 +98,12 @@ class CursesShortcuts(SettingsAware): except _curses.error: pass + def set_fg_bg_attr(self, fg, bg, attr): + try: + self.win.attrset(curses.color_pair(get_color(fg, bg)) | attr) + except _curses.error: + pass + def color_reset(self): """Change the colors to the default colors""" CursesShortcuts.color(self, 'reset') diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index dacc3920..63323f65 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -156,7 +156,7 @@ class BrowserColumn(Pager): return try: - f = self.target.get_preview_source() + f = self.target.get_preview_source(self) except: Pager.close(self) else: diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index c0bc98b4..5e761a0f 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -1,4 +1,5 @@ # Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# Copyright (C) 2010 David Barnett <davidbarnett2@gmail.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,6 +19,7 @@ The pager displays text and allows you to scroll inside it. """ import re from . import Widget +from ranger.gui import ansi from ranger.ext.direction import Direction from ranger.container.keymap import CommandArgs @@ -33,6 +35,7 @@ class Pager(Widget): old_source = None old_scroll_begin = 0 old_startx = 0 + max_width = None def __init__(self, win, embedded=False): Widget.__init__(self, win) self.embedded = embedded @@ -106,6 +109,13 @@ class Pager(Widget): if TITLE_REGEXP.match(line): self.color_at(i, 0, -1, 'title', *baseclr) + elif self.markup == 'ansi': + self.win.move(i, 0) + for chunk in ansi.text_with_fg_bg_attr(line): + if isinstance(chunk, tuple): + self.set_fg_bg_attr(*chunk) + else: + self.addstr(chunk) def move(self, narg=None, **kw): direction = Direction(kw) @@ -113,7 +123,7 @@ class Pager(Widget): self.startx = direction.move( direction=direction.right(), override=narg, - maximum=self._get_max_width(), + maximum=self.max_width, current=self.startx, pagesize=self.wid, offset=-self.wid + 1) @@ -158,12 +168,13 @@ class Pager(Widget): if isinstance(source, str): self.source_is_stream = False - self.lines = source.split('\n') + self.lines = source.splitlines() elif hasattr(source, '__getitem__'): self.source_is_stream = False self.lines = source elif hasattr(source, 'readline'): self.source_is_stream = True + self.markup = 'ansi' self.lines = [] else: self.source = None @@ -191,6 +202,8 @@ class Pager(Widget): if attempt_to_read and self.source_is_stream: try: for l in self.source: + if len(l) > self.max_width: + self.max_width = len(l) self.lines.append(l) if len(self.lines) > n: break @@ -206,11 +219,12 @@ class Pager(Widget): while True: try: line = self._get_line(i).expandtabs(4) - line = line[startx:self.wid + startx].rstrip() - yield line + if self.markup is 'ansi': + line = ansi.char_slice(line, startx, self.wid + startx) \ + + ansi.reset + else: + line = line[startx:self.wid + startx] + yield line.rstrip() except IndexError: raise StopIteration i += 1 - - def _get_max_width(self): - return max(len(line) for line in self.lines) diff --git a/test/tc_ansi.py b/test/tc_ansi.py new file mode 100644 index 00000000..0a6ad8b1 --- /dev/null +++ b/test/tc_ansi.py @@ -0,0 +1,40 @@ +# Copyright (C) 2010 David Barnett <davidbarnett2@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +if __name__ == '__main__': from __init__ import init; init() + +import unittest +from ranger.gui import ansi + +class TestDisplayable(unittest.TestCase): + def test_char_len(self): + ansi_string = "[0;30;40mX[0m" + self.assertEqual(ansi.char_len(ansi_string), 1) + + def test_char_len2(self): + ansi_string = "[0;30;40mXY[0m" + self.assertEqual(ansi.char_len(ansi_string), 2) + + def test_char_len3(self): + ansi_string = "[0;30;40mX[0;31;41mY" + self.assertEqual(ansi.char_len(ansi_string), 2) + + def test_char_slice(self): + ansi_string = "[0;30;40mX[0;31;41mY[0m" + expected = "[0;30;40mX" + self.assertEqual(ansi.char_slice(ansi_string, 0, 1), expected) + +if __name__ == '__main__': + unittest.main() |