diff options
76 files changed, 172 insertions, 15 deletions
diff --git a/doc/tools/convert_papermode_to_metadata.py b/doc/tools/convert_papermode_to_metadata.py index a1d6372d..61427c83 100755 --- a/doc/tools/convert_papermode_to_metadata.py +++ b/doc/tools/convert_papermode_to_metadata.py @@ -21,6 +21,7 @@ else: FIELDS = ["name", "year", "title", "authors", "url"] + def replace(source, target): if not os.path.exists(source): print("Source file `%s' doesn't exist, skipping." % source) diff --git a/doc/tools/print_colors.py b/doc/tools/print_colors.py index 4bc12f91..feae8e35 100755 --- a/doc/tools/print_colors.py +++ b/doc/tools/print_colors.py @@ -7,6 +7,7 @@ It will exit after a keypress. import curses from curses import * + @wrapper def main(win): def print_all_colors(attr): diff --git a/doc/tools/print_keys.py b/doc/tools/print_keys.py index e1cea81e..73091db4 100755 --- a/doc/tools/print_keys.py +++ b/doc/tools/print_keys.py @@ -7,6 +7,7 @@ from curses import * sep = '; ' + @wrapper def main(w): mousemask(ALL_MOUSE_EVENTS) diff --git a/examples/plugin_chmod_keybindings.py b/examples/plugin_chmod_keybindings.py index 1c9558f7..63f42b0e 100644 --- a/examples/plugin_chmod_keybindings.py +++ b/examples/plugin_chmod_keybindings.py @@ -7,6 +7,7 @@ import ranger.api old_hook_init = ranger.api.hook_init + def hook_init(fm): old_hook_init(fm) diff --git a/examples/plugin_file_filter.py b/examples/plugin_file_filter.py index d5ea2d2d..5d5f1466 100644 --- a/examples/plugin_file_filter.py +++ b/examples/plugin_file_filter.py @@ -10,6 +10,8 @@ old_accept_file = ranger.container.directory.accept_file HIDE_FILES = ("/boot", "/sbin", "/proc", "/sys") # Define a new one + + def custom_accept_file(file, filters): if not file.fm.settings.show_hidden and file.path in HIDE_FILES: return False diff --git a/examples/plugin_hello_world.py b/examples/plugin_hello_world.py index b64916d4..0158a653 100644 --- a/examples/plugin_hello_world.py +++ b/examples/plugin_hello_world.py @@ -12,6 +12,8 @@ import ranger.api old_hook_ready = ranger.api.hook_ready # Create a replacement for the hook that... + + def hook_ready(fm): # ...does the desired action... fm.notify("Hello World") diff --git a/examples/plugin_ipc.py b/examples/plugin_ipc.py index a9e79205..47fb1c84 100644 --- a/examples/plugin_ipc.py +++ b/examples/plugin_ipc.py @@ -10,6 +10,8 @@ import ranger.api old_hook_init = ranger.api.hook_init + + def hook_init(fm): try: # Create a FIFO. @@ -22,6 +24,7 @@ def hook_init(fm): import thread except ImportError: import _thread as thread + def ipc_reader(filepath): while True: with open(filepath, 'r') as fifo: diff --git a/examples/plugin_linemode.py b/examples/plugin_linemode.py index 8a92552e..851d6213 100644 --- a/examples/plugin_linemode.py +++ b/examples/plugin_linemode.py @@ -9,8 +9,10 @@ import codecs import ranger.api from ranger.core.linemode import LinemodeBase + @ranger.api.register_linemode class MyLinemode(LinemodeBase): name = "rot13" + def filetitle(self, file, metadata): return codecs.encode(file.relative_path, "rot_13") diff --git a/examples/plugin_new_macro.py b/examples/plugin_new_macro.py index 31350387..8dbe435d 100644 --- a/examples/plugin_new_macro.py +++ b/examples/plugin_new_macro.py @@ -10,6 +10,8 @@ old_get_macros = ranger.core.actions.Actions._get_macros # Define a new macro function import time + + def get_macros_with_date(self): macros = old_get_macros(self) macros['date'] = time.strftime('%m/%d/%Y') diff --git a/examples/plugin_pmount.py b/examples/plugin_pmount.py index 61ee0dbd..ba61b0e5 100644 --- a/examples/plugin_pmount.py +++ b/examples/plugin_pmount.py @@ -16,6 +16,8 @@ UMOUNT_KEY = '<alt>M' LIST_MOUNTS_KEY = '<alt>N' old_hook_init = ranger.api.hook_init + + def hook_init(fm): try: fm.execute_console("map {key} shell -p lsblk".format(key=LIST_MOUNTS_KEY)) diff --git a/ranger/__init__.py b/ranger/__init__.py index b562ebf5..093a01d5 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -46,6 +46,8 @@ CONFDIR = '~/.config/ranger' # Debugging functions. These will be activated when run with --debug. # Example usage in the code: # import ranger; ranger.log("hello world") + + def log(*objects, **keywords): """Writes objects to a logfile (for the purpose of debugging only.) Has the same arguments as print() in python3. diff --git a/ranger/api/__init__.py b/ranger/api/__init__.py index 7baa661d..5981c31a 100644 --- a/ranger/api/__init__.py +++ b/ranger/api/__init__.py @@ -5,6 +5,7 @@ # Hooks for use in plugins: + def hook_init(fm): """A hook that is called when ranger starts up. @@ -18,6 +19,7 @@ def hook_init(fm): keybindings and such. """ + def hook_ready(fm): """A hook that is called after the ranger UI is initialized. @@ -32,6 +34,7 @@ def hook_ready(fm): from ranger.core.linemode import LinemodeBase + def register_linemode(linemode_class): """Add a custom linemode class. See ranger.core.linemode""" from ranger.container.fsobject import FileSystemObject diff --git a/ranger/api/commands.py b/ranger/api/commands.py index c3e0a59a..f42fe9f7 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -14,6 +14,7 @@ from ranger.ext.lazy_property import lazy_property _SETTINGS_RE = re.compile(r'^\s*([^\s]+?)=(.*)$') + class CommandContainer(object): def __init__(self): self.commands = {} @@ -377,6 +378,7 @@ class FunctionCommand(Command): _based_function = None _object_name = "" _function_name = "unknown" + def execute(self): if not self._based_function: return @@ -426,11 +428,13 @@ class FunctionCommand(Command): (self._object_name, self._function_name, repr(args), repr(keywords)), bad=True) + class AliasCommand(Command): _based_function = None _object_name = "" _function_name = "unknown" _line = "" + def execute(self): return self._make_cmd().execute() diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index 37b372a3..d04e5f6a 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -4,6 +4,7 @@ from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * + class Default(ColorScheme): progress_bar_color = blue @@ -107,7 +108,6 @@ class Default(ColorScheme): fg = cyan attr &= ~bold - if context.text: if context.highlight: attr |= reverse @@ -125,7 +125,6 @@ class Default(ColorScheme): else: bg = self.progress_bar_color - if context.vcsfile and not context.selected: attr &= ~bold if context.vcsconflict: diff --git a/ranger/colorschemes/jungle.py b/ranger/colorschemes/jungle.py index 6a9c3c52..fbcfab7d 100644 --- a/ranger/colorschemes/jungle.py +++ b/ranger/colorschemes/jungle.py @@ -4,8 +4,10 @@ from ranger.gui.color import * from ranger.colorschemes.default import Default + class Scheme(Default): progress_bar_color = green + def use(self, context): fg, bg, attr = Default.use(self, context) diff --git a/ranger/colorschemes/snow.py b/ranger/colorschemes/snow.py index 849f15e4..90090854 100644 --- a/ranger/colorschemes/snow.py +++ b/ranger/colorschemes/snow.py @@ -4,6 +4,7 @@ from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * + class Snow(ColorScheme): def use(self, context): fg, bg, attr = default_colors diff --git a/ranger/colorschemes/solarized.py b/ranger/colorschemes/solarized.py index 2177ca3d..8cdfb5ce 100644 --- a/ranger/colorschemes/solarized.py +++ b/ranger/colorschemes/solarized.py @@ -8,6 +8,7 @@ from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * + class Solarized(ColorScheme): progress_bar_color = 33 diff --git a/ranger/config/commands.py b/ranger/config/commands.py index e09e1f10..cb4fd3a8 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -85,6 +85,7 @@ from ranger.api.commands import * + class alias(Command): """:alias <newcommand> <oldcommand> @@ -237,6 +238,7 @@ class shell(Command): for file in self.fm.thisdir.files or [] \ if file.shell_escaped_basename.startswith(start_of_word)) + class open_with(Command): def execute(self): app, flags, mode = self._get_app_flags_mode(self.rest(1)) @@ -340,6 +342,7 @@ class set_(Command): Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!` """ name = 'set' # don't override the builtin set class + def execute(self): name = self.arg(1) name, value, _, toggle = self.parse_setting_line_v2() @@ -380,6 +383,7 @@ class setlocal(set_): Gives an option a new value. """ PATH_RE = re.compile(r'^\s*path="?(.*?)"?\s*$') + def execute(self): import os.path match = self.PATH_RE.match(self.arg(1)) @@ -605,6 +609,7 @@ class load_copy_buffer(Command): Load the copy buffer from confdir/copy_buffer """ copy_buffer_filename = 'copy_buffer' + def execute(self): from ranger.container.file import File from os.path import exists @@ -626,6 +631,7 @@ class save_copy_buffer(Command): Save the copy buffer to confdir/copy_buffer """ copy_buffer_filename = 'copy_buffer' + def execute(self): fname = None try: @@ -789,6 +795,7 @@ class rename(Command): def tab(self, tabnum): return self._tab_directory_content() + class rename_append(Command): """:rename_append @@ -803,6 +810,7 @@ class rename_append(Command): else: self.fm.open_console('rename ' + path) + class chmod(Command): """:chmod <octal number> @@ -920,6 +928,7 @@ class bulkrename(Command): else: fm.notify("files have not been retagged") + class relink(Command): """:relink <newpath> @@ -964,6 +973,7 @@ class help_(Command): Display ranger's manual page. """ name = 'help' + def execute(self): def callback(answer): if answer == "q": @@ -1325,6 +1335,7 @@ class grep(Command): action.extend(f.path for f in self.fm.thistab.get_selection()) self.fm.execute_command(action, flags='p') + class flat(Command): """ :flat <level> @@ -1350,6 +1361,7 @@ class flat(Command): # Version control commands # -------------------------------- + class stage(Command): """ :stage @@ -1369,6 +1381,7 @@ class stage(Command): else: self.fm.notify('Unable to stage files: Not in repository') + class unstage(Command): """ :unstage @@ -1391,6 +1404,7 @@ class unstage(Command): # Metadata commands # -------------------------------- + class prompt_metadata(Command): """ :prompt_metadata <key1> [<key2> [<key3> ...]] @@ -1400,6 +1414,7 @@ class prompt_metadata(Command): _command_name = "meta" _console_chain = None + def execute(self): prompt_metadata._console_chain = self.args[1:] self._process_command_stack() diff --git a/ranger/config/commands_sample.py b/ranger/config/commands_sample.py index ce48eb37..ea74b7d6 100644 --- a/ranger/config/commands_sample.py +++ b/ranger/config/commands_sample.py @@ -15,6 +15,8 @@ import os # Any class that is a subclass of "Command" will be integrated into ranger as a # command. Try typing ":my_edit<ENTER>" in ranger! + + class my_edit(Command): # The so-called doc-string of the class will be visible in the built-in # help that is accessible by typing "?c" inside ranger. diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py index 02f3e3bc..b5e7dc72 100644 --- a/ranger/container/bookmarks.py +++ b/ranger/container/bookmarks.py @@ -6,6 +6,7 @@ import re import os ALLOWED_KEYS = string.ascii_letters + string.digits + "`'" + class Bookmarks(object): """Bookmarks is a container which associates keys with bookmarks. @@ -71,7 +72,6 @@ class Bookmarks(object): del self.dct[key] if self.autosave: self.save() - def __iter__(self): return iter(self.dct.items()) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index e7c2b4b6..4ffd9bc2 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -20,29 +20,36 @@ from ranger.ext.human_readable import human_readable from ranger.container.settings import LocalSettings from ranger.ext.vcs import Vcs + def sort_by_basename(path): """returns path.relative_path (for sorting)""" return path.relative_path + def sort_by_basename_icase(path): """returns case-insensitive path.relative_path (for sorting)""" return path.relative_path_lower + def sort_by_directory(path): """returns 0 if path is a directory, otherwise 1 (for sorting)""" return 1 - path.is_directory + def sort_naturally(path): return path.basename_natural + def sort_naturally_icase(path): return path.basename_natural_lower + def sort_unicode_wrapper_string(old_sort_func): def sort_unicode(path): return locale.strxfrm(old_sort_func(path)) return sort_unicode + def sort_unicode_wrapper_list(old_sort_func): def sort_unicode(path): return [locale.strxfrm(str(c)) for c in old_sort_func(path)] @@ -63,6 +70,7 @@ def accept_file(file, filters): return False return True + def walklevel(some_dir, level): some_dir = some_dir.rstrip(os.path.sep) followlinks = True if level > 0 else False @@ -74,6 +82,7 @@ def walklevel(some_dir, level): if level != -1 and num_sep + level <= num_sep_this: del dirs[:] + def mtimelevel(path, level): mtime = os.stat(path).st_mtime for dirpath, dirnames, filenames in walklevel(path, level): @@ -82,6 +91,7 @@ def mtimelevel(path, level): mtime = max(mtime, max([-1] + [os.stat(d).st_mtime for d in dirlist])) return mtime + class Directory(FileSystemObject, Accumulator, Loadable): is_directory = True enterable = False diff --git a/ranger/container/file.py b/ranger/container/file.py index 2c6d8315..40a4f73d 100644 --- a/ranger/container/file.py +++ b/ranger/container/file.py @@ -37,6 +37,7 @@ PREVIEW_WHITELIST = re.compile(r""" $ """, re.VERBOSE | re.IGNORECASE) + class File(FileSystemObject): is_file = True preview_data = None diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index fff29c36..2fd6ad26 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -33,9 +33,11 @@ _safe_string_table = maketrans(_unsafe_chars, '?' * len(_unsafe_chars)) _extract_number_re = re.compile(r'(\d+|\D)') _integers = set("0123456789") + def safe_path(path): return path.translate(_safe_string_table) + class FileSystemObject(FileManagerAware, SettingsAware): (basename, relative_path, @@ -119,7 +121,6 @@ class FileSystemObject(FileManagerAware, SettingsAware): self._linemode = linemode break - def __repr__(self): return "<{0} {1}>".format(self.__class__.__name__, self.path) diff --git a/ranger/container/history.py b/ranger/container/history.py index 69c3da57..d92f2910 100644 --- a/ranger/container/history.py +++ b/ranger/container/history.py @@ -3,9 +3,11 @@ # TODO: rewrite to use deque instead of list + class HistoryEmptyException(Exception): pass + class History(object): def __init__(self, maxlen=None, unique=True): assert maxlen is not None, "maxlen cannot be None" diff --git a/ranger/container/settings.py b/ranger/container/settings.py index 85729907..402dd806 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -87,6 +87,7 @@ DEFAULT_VALUES = { tuple: tuple([]), } + class Settings(SignalDispatcher, FileManagerAware): def __init__(self): SignalDispatcher.__init__(self) @@ -200,7 +201,6 @@ class Settings(SignalDispatcher, FileManagerAware): else: return (typ, ) - def _check_type(self, name, value): typ = ALLOWED_SETTINGS[name] if isfunction(typ): diff --git a/ranger/container/tags.py b/ranger/container/tags.py index 555ef8b0..cf2f359d 100644 --- a/ranger/container/tags.py +++ b/ranger/container/tags.py @@ -9,6 +9,7 @@ import sys ALLOWED_KEYS = string.ascii_letters + string.digits + string.punctuation + class Tags(object): default_tag = '*' diff --git a/ranger/core/actions.py b/ranger/core/actions.py index a39cef21..a1beddc8 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -31,11 +31,13 @@ from ranger.core.linemode import DEFAULT_LINEMODE MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>" + class _MacroTemplate(string.Template): """A template for substituting macros in commands""" delimiter = ranger.MACRO_DELIMITER idpattern = r"[_a-z0-9]*" + class Actions(FileManagerAware, SettingsAware): # -------------------------- # -- Basic Commands @@ -92,7 +94,6 @@ class Actions(FileManagerAware, SettingsAware): self.settings.set(option_name, self._parse_option_value(option_name, value), localpath, tags) - def _parse_option_value(self, name, value): types = self.fm.settings.types_of(name) if bool in types: @@ -479,7 +480,6 @@ class Actions(FileManagerAware, SettingsAware): if self.ui.pager.visible: self.display_file() - def move_parent(self, n, narg=None): self.change_mode('normal') if narg is not None: @@ -930,7 +930,6 @@ class Actions(FileManagerAware, SettingsAware): if data['loading']: return None - found = data.get((-1, -1), data.get((width, -1), data.get((-1, height), data.get((width, height), False)))) if found == False: @@ -967,6 +966,7 @@ class Actions(FileManagerAware, SettingsAware): path, str(width), str(height), cacheimg, str(self.settings.preview_images)], read=True, silent=True, descr="Getting preview of %s" % path) + def on_after(signal): exit = signal.process.poll() content = signal.loader.stdout_buffer @@ -1012,6 +1012,7 @@ class Actions(FileManagerAware, SettingsAware): else: pager.set_source(self.thisfile.get_preview_source( pager.wid, pager.hei)) + def on_destroy(signal): try: del self.previews[path] @@ -1164,6 +1165,7 @@ class Actions(FileManagerAware, SettingsAware): contexts = 'browser', 'console', 'pager', 'taskview' temporary_file = tempfile.NamedTemporaryFile() + def write(string): temporary_file.write(string.encode('utf-8')) @@ -1189,6 +1191,7 @@ class Actions(FileManagerAware, SettingsAware): def dump_commands(self): temporary_file = tempfile.NamedTemporaryFile() + def write(string): temporary_file.write(string.encode('utf-8')) @@ -1214,6 +1217,7 @@ class Actions(FileManagerAware, SettingsAware): def dump_settings(self): temporary_file = tempfile.NamedTemporaryFile() + def write(string): temporary_file.write(string.encode('utf-8')) diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 052584a6..d4a24bb0 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -27,6 +27,7 @@ from ranger.ext.signals import SignalDispatcher from ranger import __version__ from ranger.core.loader import Loader + class FM(Actions, SignalDispatcher): input_blocked = False input_blocked_until = 0 @@ -235,6 +236,7 @@ class FM(Actions, SignalDispatcher): return import shutil from errno import EEXIST + def copy(_from, to): if os.path.exists(self.confpath(to)): sys.stderr.write("already exists: %s\n" % self.confpath(to)) diff --git a/ranger/core/linemode.py b/ranger/core/linemode.py index be559578..96557515 100644 --- a/ranger/core/linemode.py +++ b/ranger/core/linemode.py @@ -108,6 +108,7 @@ class FileInfoLinemode(LinemodeBase): else: raise NotImplementedError + class MtimeLinemode(LinemodeBase): name = "mtime" @@ -117,6 +118,7 @@ class MtimeLinemode(LinemodeBase): def infostring(self, file, metadata): return datetime.fromtimestamp(file.stat.st_mtime).strftime("%Y-%m-%d %H:%M") + class SizeMtimeLinemode(LinemodeBase): name = "sizemtime" diff --git a/ranger/core/loader.py b/ranger/core/loader.py index b1aabb53..aebd6efe 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -17,9 +17,11 @@ try: except: HAVE_CHARDET = False + class Loadable(object): paused = False progressbar_supported = False + def __init__(self, gen, descr): self.load_generator = gen self.description = descr @@ -43,6 +45,7 @@ class Loadable(object): class CopyLoader(Loadable, FileManagerAware): progressbar_supported = True + def __init__(self, copy_buffer, do_cut=False, overwrite=False): self.copy_buffer = tuple(copy_buffer) self.do_cut = do_cut @@ -137,6 +140,7 @@ class CommandLoader(Loadable, SignalDispatcher, FileManagerAware): """ finished = False process = None + def __init__(self, args, descr, silent=False, read=False, input=None, kill_on_pause=False, popenArgs=None): SignalDispatcher.__init__(self) diff --git a/ranger/core/main.py b/ranger/core/main.py index 2972a1eb..fed4a231 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -8,6 +8,7 @@ import sys import tempfile import importlib + def main(): """initialize objects and run the filemanager""" import locale @@ -72,6 +73,7 @@ def main(): sys.stderr.write("Warning: Using ranger as a file launcher is " "deprecated.\nPlease use the standalone file launcher " "'rifle' instead.\n") + def print_function(string): print(string) from ranger.ext.rifle import Rifle diff --git a/ranger/core/metadata.py b/ranger/core/metadata.py index e7f90efd..299d2b85 100644 --- a/ranger/core/metadata.py +++ b/ranger/core/metadata.py @@ -18,6 +18,7 @@ import copy from os.path import join, dirname, exists, basename from ranger.ext.openstruct import DefaultOpenStruct as ostruct + class MetadataManager(object): def __init__(self): # metadata_cache maps filenames to dicts containing their metadata diff --git a/ranger/core/shared.py b/ranger/core/shared.py index 177ba15d..38b0d35a 100644 --- a/ranger/core/shared.py +++ b/ranger/core/shared.py @@ -5,12 +5,14 @@ from ranger.ext.lazy_property import lazy_property + class FileManagerAware(object): """Subclass this to gain access to the global "FM" object.""" @staticmethod def _setup(fm): FileManagerAware.fm = fm + class SettingsAware(object): """Subclass this to gain access to the global "SettingObject" object.""" @staticmethod diff --git a/ranger/core/tab.py b/ranger/core/tab.py index f8de953f..dc76568b 100644 --- a/ranger/core/tab.py +++ b/ranger/core/tab.py @@ -9,6 +9,7 @@ from ranger.container.history import History from ranger.core.shared import FileManagerAware, SettingsAware from ranger.ext.signals import SignalDispatcher + class Tab(FileManagerAware, SettingsAware): def __init__(self, path): self.thisdir = None # Current Working Directory diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py index 278600c7..863a59df 100644 --- a/ranger/ext/accumulator.py +++ b/ranger/ext/accumulator.py @@ -3,6 +3,7 @@ from ranger.ext.direction import Direction + class Accumulator(object): def __init__(self): self.pointer = 0 diff --git a/ranger/ext/cached_function.py b/ranger/ext/cached_function.py index cbc0d591..48461a8d 100644 --- a/ranger/ext/cached_function.py +++ b/ranger/ext/cached_function.py @@ -1,8 +1,10 @@ # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. + def cached_function(fnc): cache = {} + def inner_cached_function(*args): try: return cache[args] diff --git a/ranger/ext/curses_interrupt_handler.py b/ranger/ext/curses_interrupt_handler.py index 9ee12846..606cbd62 100644 --- a/ranger/ext/curses_interrupt_handler.py +++ b/ranger/ext/curses_interrupt_handler.py @@ -13,6 +13,7 @@ import signal _do_catch_interrupt = True + def catch_interrupt(boolean=True): """Should interrupts be caught and simulate a ^C press in curses?""" global _do_catch_interrupt @@ -21,6 +22,8 @@ def catch_interrupt(boolean=True): return old_value # The handler which will be used in signal.signal() + + def _interrupt_handler(a1, a2): global _do_catch_interrupt # if a keyboard-interrupt occurs... @@ -31,10 +34,12 @@ def _interrupt_handler(a1, a2): # use the default handler signal.default_int_handler(a1, a2) + def install_interrupt_handler(): """Install the custom interrupt_handler""" signal.signal(signal.SIGINT, _interrupt_handler) + def restore_interrupt_handler(): """Restore the default_int_handler""" signal.signal(signal.SIGINT, signal.default_int_handler) diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py index 572ce271..f236e7e3 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -18,6 +18,7 @@ has been defined. False """ + class Direction(dict): def __init__(self, dictionary=None, **keywords): if dictionary is not None: diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index af371370..31d5d279 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -1,6 +1,7 @@ # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. + def human_readable(byte, separator=' '): """Convert a large number of bytes to an easily readable format. diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 0c6a11d4..61967b02 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -31,9 +31,11 @@ W3MIMGDISPLAY_PATHS = [ '/usr/libexec64/w3m/w3mimgdisplay' ] + class ImgDisplayUnsupportedException(Exception): pass + class ImageDisplayer(object): """Image display provider functions for drawing images in the terminal""" def draw(self, path, start_x, start_y, width, height): @@ -48,6 +50,7 @@ class ImageDisplayer(object): """Cleanup and close""" pass + class W3MImageDisplayer(ImageDisplayer): """Implementation of ImageDisplayer using w3mimgdisplay, an utilitary program from w3m (a text-based web browser). w3mimgdisplay can display @@ -178,6 +181,8 @@ class W3MImageDisplayer(ImageDisplayer): # TODO: remove FileManagerAwareness, as stuff in ranger.ext should be # ranger-independent libraries. + + class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer using iTerm2 image display support (http://iterm2.com/images.html). @@ -242,7 +247,6 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): else: return width - def _encode_image_content(self, path): """Read and encode the contents of path""" file = open(path, 'rb') diff --git a/ranger/ext/iter_tools.py b/ranger/ext/iter_tools.py index d583d060..838d8aff 100644 --- a/ranger/ext/iter_tools.py +++ b/ranger/ext/iter_tools.py @@ -3,6 +3,7 @@ from collections import deque + def flatten(lst): """Flatten an iterable. @@ -22,6 +23,7 @@ def flatten(lst): else: yield elem + def unique(iterable): """Return an iterable of the same type which contains unique items. diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py index 8efb337f..4e9375bf 100644 --- a/ranger/ext/keybinding_parser.py +++ b/ranger/ext/keybinding_parser.py @@ -149,6 +149,7 @@ def _unbind_traverse(pointer, keys, pos=0): except: pass + class KeyMaps(dict): def __init__(self, keybuffer=None): dict.__init__(self) diff --git a/ranger/ext/lazy_property.py b/ranger/ext/lazy_property.py index 9e3454b0..4bdcda1e 100644 --- a/ranger/ext/lazy_property.py +++ b/ranger/ext/lazy_property.py @@ -1,5 +1,6 @@ # From http://blog.pythonisito.com/2008/08/lazy-descriptors.html + class lazy_property(object): """A @property-like decorator with lazy evaluation diff --git a/ranger/ext/mount_path.py b/ranger/ext/mount_path.py index fdf11883..69a277af 100644 --- a/ranger/ext/mount_path.py +++ b/ranger/ext/mount_path.py @@ -3,6 +3,7 @@ from os.path import realpath, abspath, dirname, ismount + def mount_path(path): """Get the mount root of a directory""" path = abspath(realpath(path)) diff --git a/ranger/ext/next_available_filename.py b/ranger/ext/next_available_filename.py index 77a73c00..c408b35d 100644 --- a/ranger/ext/next_available_filename.py +++ b/ranger/ext/next_available_filename.py @@ -3,6 +3,7 @@ import os.path + def next_available_filename(fname, directory="."): existing_files = os.listdir(directory) diff --git a/ranger/ext/openstruct.py b/ranger/ext/openstruct.py index 96a75f49..6479d158 100644 --- a/ranger/ext/openstruct.py +++ b/ranger/ext/openstruct.py @@ -3,6 +3,7 @@ import collections + class OpenStruct(dict): """The fusion of dict and struct""" def __init__(self, *args, **keywords): diff --git a/ranger/ext/popen_forked.py b/ranger/ext/popen_forked.py index ffe8438e..d4a75a48 100644 --- a/ranger/ext/popen_forked.py +++ b/ranger/ext/popen_forked.py @@ -4,6 +4,7 @@ import os import subprocess + def Popen_forked(*args, **kwargs): """Forks process and runs Popen with the given args and kwargs. diff --git a/ranger/ext/relative_symlink.py b/ranger/ext/relative_symlink.py index fe15a98a..247d1318 100644 --- a/ranger/ext/relative_symlink.py +++ b/ranger/ext/relative_symlink.py @@ -3,15 +3,18 @@ from os import symlink, sep + def relative_symlink(src, dst): common_base = get_common_base(src, dst) symlink(get_relative_source_file(src, dst, common_base), dst) + def get_relative_source_file(src, dst, common_base=None): if common_base is None: common_base = get_common_base(src, dst) return '../' * dst.count('/', len(common_base)) + src[len(common_base):] + def get_common_base(src, dst): if not src or not dst: return '/' diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index eadbc110..b1467c34 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -398,7 +398,6 @@ def main(): else: conf_path = os.path.join(ranger.__path__[0], "config", "rifle.conf") - # Evaluate arguments from optparse import OptionParser parser = OptionParser(usage="%prog [-fhlpw] [files]", version=__version__) @@ -448,8 +447,6 @@ def main(): # TODO: implement interactive asking for file type? print("Unknown file type: %s" % rifle._get_mimetype(positional[0])) - - if __name__ == '__main__': if 'RANGER_DOCTEST' in os.environ: import doctest diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py index a7be2068..44984405 100644 --- a/ranger/ext/shell_escape.py +++ b/ranger/ext/shell_escape.py @@ -9,10 +9,12 @@ UNESCAPABLE = set(map(chr, list(range(9)) + list(range(10, 32)) \ + list(range(127, 256)))) META_DICT = dict([(mc, '\\' + mc) for mc in META_CHARS]) + def shell_quote(string): """Escapes by quoting""" return "'" + str(string).replace("'", "'\\''") + "'" + def shell_escape(arg): """Escapes by adding backslashes""" arg = str(arg) diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py index 7958b048..1bf95842 100644 --- a/ranger/ext/shutil_generatorized.py +++ b/ranger/ext/shutil_generatorized.py @@ -16,9 +16,11 @@ __all__ = ["copyfileobj", "copyfile", "copystat", "copy2", "BLOCK_SIZE", APPENDIX = '_' BLOCK_SIZE = 16 * 1024 + class Error(EnvironmentError): pass + class SpecialFileError(EnvironmentError): """Raised when trying to do a kind of operation (e.g. copying) which is not supported on a special file (e.g. a named pipe)""" @@ -28,6 +30,7 @@ try: except NameError: WindowsError = None + def copyfileobj(fsrc, fdst, length=BLOCK_SIZE): """copy data from file-like object fsrc to file-like object fdst""" done = 0 @@ -39,6 +42,7 @@ def copyfileobj(fsrc, fdst, length=BLOCK_SIZE): done += len(buf) yield done + def _samefile(src, dst): # Macintosh, Unix. if hasattr(os.path, 'samefile'): @@ -51,6 +55,7 @@ def _samefile(src, dst): return (os.path.normcase(abspath(src)) == os.path.normcase(abspath(dst))) + def copyfile(src, dst): """Copy data from src to dst""" if _samefile(src, dst): @@ -79,6 +84,7 @@ def copyfile(src, dst): if fsrc: fsrc.close() + def copystat(src, dst): """Copy all stat info (mode bits, atime, mtime, flags) from src to dst""" st = os.lstat(src) @@ -93,6 +99,7 @@ def copystat(src, dst): try: os.chflags(dst, st.st_flags) except: pass + def copy2(src, dst, overwrite=False, symlinks=False): """Copy data and all stat info ("cp -p src dst"). @@ -113,6 +120,7 @@ def copy2(src, dst, overwrite=False, symlinks=False): yield done copystat(src, dst) + def get_safe_path(dst): if not os.path.exists(dst): return dst @@ -128,6 +136,7 @@ def get_safe_path(dst): return test_dst + def copytree(src, dst, symlinks=False, ignore=None, overwrite=False): """Recursively copy a directory tree using copy2(). @@ -210,6 +219,7 @@ def copytree(src, dst, symlinks=False, ignore=None, overwrite=False): if errors: raise Error(errors) + def rmtree(path, ignore_errors=False, onerror=None): """Recursively delete a directory tree. @@ -264,6 +274,7 @@ def _basename(path): # Thus we always get the last component of the path, even for directories. return os.path.basename(path.rstrip(os.path.sep)) + def move(src, dst, overwrite=False): """Recursively move a file or directory to another location. This is similar to the Unix "mv" command. @@ -298,6 +309,7 @@ def move(src, dst, overwrite=False): yield done os.unlink(src) + def _destinsrc(src, dst): src = abspath(src) dst = abspath(dst) diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py index bcac66fe..1e907b70 100644 --- a/ranger/ext/signals.py +++ b/ranger/ext/signals.py @@ -60,6 +60,7 @@ True import weakref from types import MethodType + class Signal(dict): """Signals are passed to the bound functions as an argument. @@ -71,6 +72,7 @@ class Signal(dict): To delete a signal handler from inside a signal, raise a ReferenceError. """ stopped = False + def __init__(self, **keywords): dict.__init__(self, keywords) self.__dict__ = self @@ -90,6 +92,7 @@ class SignalHandler: "active" to False. """ active = True + def __init__(self, signal_name, function, priority, pass_signal): self._priority = max(0, min(1, priority)) self._signal_name = signal_name diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py index 6c1b17b4..7c5c921c 100644 --- a/ranger/ext/spawn.py +++ b/ranger/ext/spawn.py @@ -4,6 +4,7 @@ from subprocess import Popen, PIPE ENCODING = 'utf-8' + def spawn(*args): """Runs a program, waits for its termination and returns its stdout""" if len(args) == 1: diff --git a/ranger/ext/widestring.py b/ranger/ext/widestring.py index 06d32830..b042c45b 100644 --- a/ranger/ext/widestring.py +++ b/ranger/ext/widestring.py @@ -11,6 +11,7 @@ NARROW = 1 WIDE = 2 WIDE_SYMBOLS = set('WF') + def uwid(string): """Return the width of a string""" if not PY3: diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py index 601d6004..57a69442 100644 --- a/ranger/gui/ansi.py +++ b/ranger/gui/ansi.py @@ -11,11 +11,14 @@ ansi_re = re.compile('(\x1b' + r'\[\d*(?:;\d+)*?[a-zA-Z])') codesplit_re = re.compile('38;5;(\d+);|48;5;(\d+);|(\d*);') reset = '\x1b[0m' + def split_ansi_from_text(ansi_text): return ansi_re.split(ansi_text) # For information on the ANSI codes see # githttp://en.wikipedia.org/wiki/ANSI_escape_code + + def text_with_fg_bg_attr(ansi_text): fg, bg, attr = -1, -1, 0 for chunk in split_ansi_from_text(ansi_text): @@ -93,6 +96,7 @@ def text_with_fg_bg_attr(ansi_text): else: yield chunk + def char_len(ansi_text): """Count the number of visible characters. @@ -109,6 +113,7 @@ def char_len(ansi_text): """ return len(ansi_re.sub('', ansi_text)) + def char_slice(ansi_text, start, length): """Slices a string with respect to ansi code sequences diff --git a/ranger/gui/bar.py b/ranger/gui/bar.py index 27462e54..12c44488 100644 --- a/ranger/gui/bar.py +++ b/ranger/gui/bar.py @@ -5,6 +5,7 @@ from ranger.ext.widestring import WideString, utf_char_width import sys PY3 = sys.version_info[0] >= 3 + class Bar(object): left = None right = None diff --git a/ranger/gui/color.py b/ranger/gui/color.py index 89b58092..78f0ccda 100644 --- a/ranger/gui/color.py +++ b/ranger/gui/color.py @@ -19,6 +19,7 @@ DEFAULT_FOREGROUND = curses.COLOR_WHITE DEFAULT_BACKGROUND = curses.COLOR_BLACK COLOR_PAIRS = {10: 0} + def get_color(fg, bg): """Returns the curses color pair for the given fg/bg combination.""" diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py index d2b3b2d2..fa553aea 100644 --- a/ranger/gui/colorscheme.py +++ b/ranger/gui/colorscheme.py @@ -34,6 +34,7 @@ from ranger.core.main import allow_access_to_confdir from ranger.ext.cached_function import cached_function from ranger.ext.iter_tools import flatten + class ColorScheme(object): """This is the class that colorschemes must inherit from. @@ -72,6 +73,7 @@ class ColorScheme(object): """ return (-1, -1, 0) + def _colorscheme_name_to_class(signal): # Find the colorscheme. First look in ~/.config/ranger/colorschemes, # then at RANGERDIR/colorschemes. If the file contains a class @@ -132,6 +134,7 @@ def _colorscheme_name_to_class(signal): else: raise Exception("The module contains no valid colorscheme!") + def get_all_colorschemes(): colorschemes = set() # Load colorscheme names from main ranger/colorschemes dir diff --git a/ranger/gui/context.py b/ranger/gui/context.py index 2d23d4f1..d5352a0a 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -22,6 +22,7 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo', 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', 'vcsstaged', 'vcssync', 'vcsnone', 'vcsbehind', 'vcsahead', 'vcsdiverged'] + class Context(object): def __init__(self, keys): # set all given keys to True diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py index e7573f17..7078897a 100644 --- a/ranger/gui/curses_shortcuts.py +++ b/ranger/gui/curses_shortcuts.py @@ -10,10 +10,12 @@ from ranger.core.shared import SettingsAware REVERSE_ADDCH_ARGS = sys.version[0:5] == '3.4.0' + def _fix_surrogates(args): return [isinstance(arg, str) and arg.encode('utf-8', 'surrogateescape') .decode('utf-8', 'replace') or arg for arg in args] + class CursesShortcuts(SettingsAware): """This class defines shortcuts to faciliate operations with curses. diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py index c6e21d54..e8b627e8 100644 --- a/ranger/gui/displayable.py +++ b/ranger/gui/displayable.py @@ -4,6 +4,7 @@ from ranger.core.shared import FileManagerAware from ranger.gui.curses_shortcuts import CursesShortcuts + class Displayable(FileManagerAware, CursesShortcuts): """Displayables are objects which are displayed on the screen. @@ -199,6 +200,7 @@ class Displayable(FileManagerAware, CursesShortcuts): def __str__(self): return self.__class__.__name__ + class DisplayableContainer(Displayable): """DisplayableContainers are Displayables which contain other Displayables. diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py index 0de3eea2..6931648f 100644 --- a/ranger/gui/mouse_event.py +++ b/ranger/gui/mouse_event.py @@ -3,6 +3,7 @@ import curses + class MouseEvent(object): PRESSED = [0, curses.BUTTON1_PRESSED, diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 5c4e2ee0..200f3625 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -16,9 +16,12 @@ from ranger.ext.signals import Signal MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION _ASCII = ''.join(chr(c) for c in range(32, 127)) + + def ascii_only(string): return ''.join(c if c in _ASCII else '?' for c in string) + def _setup_mouse(signal): if signal['value']: curses.mousemask(MOUSEMASK) @@ -34,6 +37,7 @@ def _setup_mouse(signal): else: curses.mousemask(0) + class UI(DisplayableContainer): ALLOWED_VIEWMODES = 'miller', 'multipane' diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index f61e18eb..9a95509c 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -2,6 +2,7 @@ from ranger.gui.displayable import Displayable + class Widget(Displayable): """A class for classification of widgets.""" diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 919a8f19..0a73e013 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -16,6 +16,7 @@ from ranger.core import linemode from ranger.gui.color import * + class BrowserColumn(Pager): main_column = False display_infostring = False diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 5383fc21..64474ac7 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -13,6 +13,7 @@ from ranger.ext.widestring import uwid, WideString from ranger.container.history import History, HistoryEmptyException import ranger + class Console(Widget): visible = False last_cursor_mode = None diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index 0d185f98..e950ebf9 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -10,6 +10,8 @@ from ranger.ext.direction import Direction from ranger.ext.img_display import ImgDisplayUnsupportedException # TODO: Scrolling in embedded pager + + class Pager(Widget): source = None source_is_stream = False @@ -20,6 +22,7 @@ class Pager(Widget): need_clear_image = False need_redraw_image = False max_width = None + def __init__(self, win, embedded=False): Widget.__init__(self, win) self.embedded = embedded diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 8786af06..c0c937de 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -18,6 +18,7 @@ from ranger.ext.human_readable import human_readable from . import Widget from ranger.gui.bar import Bar + class StatusBar(Widget): __doc__ = __doc__ owners = {} @@ -229,8 +230,6 @@ class StatusBar(Widget): except KeyError: return str(gid) - - def _get_right_part(self, bar): right = bar.right if self.column is None: @@ -314,10 +313,12 @@ class StatusBar(Widget): self.color_at(0, 0, int(barwidth), ("in_statusbar", "loaded")) self.color_reset() + def get_free_space(path): stat = os.statvfs(path) return stat.f_bavail * stat.f_frsize + class Message(object): elapse = None text = None diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py index 04fdfe0e..654c63be 100644 --- a/ranger/gui/widgets/taskview.py +++ b/ranger/gui/widgets/taskview.py @@ -6,6 +6,7 @@ from . import Widget from ranger.ext.accumulator import Accumulator + class TaskView(Widget, Accumulator): old_lst = None @@ -71,7 +72,6 @@ class TaskView(Widget, Accumulator): y = self.y + 1 + self.pointer - self.scroll_begin self.fm.ui.win.move(y, self.x) - def task_remove(self, i=None): if i is None: i = self.pointer diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py index dbd08981..a4e091b2 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -8,9 +8,11 @@ It displays the current path among other things. from os.path import basename + from . import Widget from ranger.gui.bar import Bar + class TitleBar(Widget): old_thisfile = None old_keybuffer = None diff --git a/ranger/gui/widgets/view_base.py b/ranger/gui/widgets/view_base.py index 9fbd9586..613353c3 100644 --- a/ranger/gui/widgets/view_base.py +++ b/ranger/gui/widgets/view_base.py @@ -8,6 +8,7 @@ from ranger.ext.keybinding_parser import key_to_string from . import Widget from ..displayable import DisplayableContainer + class ViewBase(Widget, DisplayableContainer): draw_bookmarks = False need_clear = False diff --git a/ranger/gui/widgets/view_miller.py b/ranger/gui/widgets/view_miller.py index dbf59ff8..2c0bc300 100644 --- a/ranger/gui/widgets/view_miller.py +++ b/ranger/gui/widgets/view_miller.py @@ -10,6 +10,7 @@ from .pager import Pager from ..displayable import DisplayableContainer from ranger.gui.widgets.view_base import ViewBase + class ViewMiller(ViewBase): ratios = None preview = True diff --git a/ranger/gui/widgets/view_multipane.py b/ranger/gui/widgets/view_multipane.py index 1899687e..7b77b3db 100644 --- a/ranger/gui/widgets/view_multipane.py +++ b/ranger/gui/widgets/view_multipane.py @@ -4,6 +4,7 @@ from ranger.gui.widgets.view_base import ViewBase from ranger.gui.widgets.browsercolumn import BrowserColumn + class ViewMultipane(ViewBase): def __init__(self, win): ViewBase.__init__(self, win) diff --git a/setup.py b/setup.py index 234bba19..c2b6da17 100755 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ import distutils.core import os.path import ranger + def _findall(directory): return [os.path.join(directory, f) for f in os.listdir(directory) \ if os.path.isfile(os.path.join(directory, f))] diff --git a/tests/ranger/container/test_bookmarks.py b/tests/ranger/container/test_bookmarks.py index 46c615c6..cfaf3ba1 100644 --- a/tests/ranger/container/test_bookmarks.py +++ b/tests/ranger/container/test_bookmarks.py @@ -4,6 +4,7 @@ import pytest from ranger.container.bookmarks import Bookmarks + def testbookmarks(tmpdir): # Bookmarks point to directory location and allow fast access to # 'favorite' directories. They are persisted to a bookmark file, plain text. @@ -36,8 +37,10 @@ def testbookmarks(tmpdir): # We don't uneccesary update when the file on disk does not change origupdate = secondstore.update + class OutOfDateException(Exception): pass + def crash(): raise OutOfDateException("Don't access me") secondstore.update = crash diff --git a/tests/ranger/container/test_container.py b/tests/ranger/container/test_container.py index 140186d0..2b823912 100644 --- a/tests/ranger/container/test_container.py +++ b/tests/ranger/container/test_container.py @@ -4,6 +4,7 @@ from ranger.container import history HISTORY_TEST_ENTRIES = [str(k) for k in range(20)] OTHER_TEST_ENTRIES = [str(k) for k in range(40, 45)] + def testhistorybasic(): # A history is a buffer of limited size that stores the last `maxlen` # item added to it. It has a `current` index that serves as a cursor. |