diff options
-rw-r--r-- | doc/examples/plugin_file_filter.py | 18 | ||||
-rw-r--r-- | doc/ranger.1 | 61 | ||||
-rw-r--r-- | doc/ranger.pod | 81 | ||||
-rw-r--r-- | ranger/config/commands.py | 156 | ||||
-rw-r--r-- | ranger/config/rc.conf | 10 | ||||
-rw-r--r-- | ranger/container/directory.py | 104 | ||||
-rw-r--r-- | ranger/container/file.py | 2 | ||||
-rw-r--r-- | ranger/container/fsobject.py | 54 | ||||
-rw-r--r-- | ranger/container/settings.py | 1 | ||||
-rw-r--r-- | ranger/core/actions.py | 47 | ||||
-rw-r--r-- | ranger/core/fm.py | 16 | ||||
-rw-r--r-- | ranger/ext/papermanager.py | 171 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 46 | ||||
-rw-r--r-- | ranger/gui/widgets/titlebar.py | 2 |
14 files changed, 698 insertions, 71 deletions
diff --git a/doc/examples/plugin_file_filter.py b/doc/examples/plugin_file_filter.py index bad5a368..b9bea1f3 100644 --- a/doc/examples/plugin_file_filter.py +++ b/doc/examples/plugin_file_filter.py @@ -1,18 +1,20 @@ -# Compatible with ranger 1.6.* +# Compatible since ranger 1.6.1, git commit c82a8a76989c # -# This plugin hides the directories "boot", "sbin", "proc" and "sys" in the -# root directory. +# This plugin hides the directories "/boot", "/sbin", "/proc" and "/sys" unless +# the "show_hidden" option is activated. # Save the original filter function import ranger.container.directory old_accept_file = ranger.container.directory.accept_file +HIDE_FILES = ("/boot", "/sbin", "/proc", "/sys") + # Define a new one -def custom_accept_file(fname, directory, hidden_filter, name_filter): - if hidden_filter and directory.path == '/' and fname in ('boot', 'sbin', 'proc', 'sys'): - return False - else: - return old_accept_file(fname, directory, hidden_filter, name_filter) +def custom_accept_file(file, filters): + if not file.fm.settings.show_hidden and file.path in HIDE_FILES: + return False + else: + return old_accept_file(file, filters) # Overwrite the old function import ranger.container.directory diff --git a/doc/ranger.1 b/doc/ranger.1 index 9aa4e318..27de2aa9 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.6.1" "10/31/2014" "ranger manual" +.TH RANGER 1 "ranger-1.6.1" "12/05/2014" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -539,6 +539,11 @@ Go to the next or previous tab. You can also use \s-1TAB\s0 and \s-1SHIFT+TAB\s0 .IP "gc, ^W" 14 .IX Item "gc, ^W" Close the current tab. The last tab cannot be closed this way. +.IP "M" 14 +.IX Item "M" +A key chain that allows you to quickly change the line mode of all the files of +the current directory. For a more permanent solution, use the command +\&\*(L"default_linemode\*(R" in your rc.conf. .SS "READLINE-LIKE \s-1BINDINGS IN THE CONSOLE\s0" .IX Subsection "READLINE-LIKE BINDINGS IN THE CONSOLE" .IP "^B, ^F" 14 @@ -684,6 +689,11 @@ How many directory changes should be kept in history? .IP "mouse_enabled [bool] <zm>" 4 .IX Item "mouse_enabled [bool] <zm>" Enable mouse input? +.IP "papermanager_deep_search [bool]" 4 +.IX Item "papermanager_deep_search [bool]" +When the paper manager module looks for metadata, should it only look for a +\&\*(L".paperinfo\*(R" file in the current directory, or do a deep search and check all +directories above the current one as well? .IP "padding_right [bool]" 4 .IX Item "padding_right [bool]" When collapse_preview is on and there is no preview, should there remain a @@ -798,18 +808,25 @@ ranger. For your convenience, this is a list of the \*(L"public\*(R" commands i \& copypmap key newkey [newkey2...] \& copytmap key newkey [newkey2...] \& cunmap keys... +\& default_linemode [path=regexp | tag=tags] linemodename \& delete \& edit [filename] \& eval [\-q] python_code \& filter [string] \& find pattern \& grep pattern +\& linemode linemodename \& load_copy_buffer \& map key command \& mark pattern \& mark_tag [tags] \& mkdir dirname \& open_with [application] [flags] [mode] +\& paper +\& paper_authors [authors] +\& paper_title [title] +\& paper_url [url] +\& paper_year [year] \& pmap key command \& punmap keys... \& quit @@ -898,6 +915,20 @@ See \f(CW\*(C`copymap\*(C'\fR .IP "cunmap [\fIkeys...\fR]" 2 .IX Item "cunmap [keys...]" Removes key mappings of the console. Works like the \f(CW\*(C`unmap\*(C'\fR command. +.IP "default_linemode [\fIpath=regexp\fR | \fItag=tags\fR] \fIlinemodename\fR" 2 +.IX Item "default_linemode [path=regexp | tag=tags] linemodename" +Sets the default linemode. See \fIlinemode\fR command. +.Sp +Examples: +.Sp +Set the global default linemode to \*(L"permissions\*(R": + :default_linemode permissions +.Sp +Set the default linemode to \*(L"permissions\*(R" for all files tagged with \*(L"p\*(R" or \*(L"P\*(R": + :default_linemode tag=pP permissions +.Sp +Set the default linemode for all files in ~/books/ to \*(L"papertitle\*(R": + :default_linemode path=/home/.*?/books/.* papertitle .IP "delete" 2 .IX Item "delete" Destroy all files in the selection with a roundhouse kick. ranger will ask for @@ -932,6 +963,16 @@ This command is based on the \fIscout\fR command and supports all of its options .IP "grep \fIpattern\fR" 2 .IX Item "grep pattern" Looks for a string in all marked files or directories. +.IP "linemode \fIlinemodename\fR" 2 +.IX Item "linemode linemodename" +Sets the linemode of all files in the current directory. The linemode may be: +.Sp +.Vb 4 +\& "filename": display each line as "<basename>...<size>" +\& "permissions": display each line as "<permissions> <owner> <group> <basename>" +\& "papertitle": display metadata from .paperinfo files if available, fall back +\& to the "filename" linemode if no metadata was found. See :paper commands. +.Ve .IP "load_copy_buffer" 2 .IX Item "load_copy_buffer" Load the copy buffer from \fI~/.config/ranger/copy_buffer\fR. This can be used to @@ -968,6 +1009,24 @@ of applications is generated by the external file opener \*(L"rifle\*(R" and can displayed when pressing \*(L"r\*(R" in ranger. .Sp Note that if you specify an application, the mode is ignored. +.IP "paper" 2 +.IX Item "paper" +This command opens a series of commands on the console that will ask the user +to input metadata about the current file. This is used by the paper manager +module of ranger and can be later displayed in ranger, for example by setting +the option \*(L"linemode\*(R" to \*(L"papertitle\*(R". +.IP "paper_authors \fIauthors\fR" 2 +.IX Item "paper_authors authors" +Tells the paper manager to set/update the authors of the current file +.IP "paper_title \fItitle\fR" 2 +.IX Item "paper_title title" +Tells the paper manager to set/update the title of the current file +.IP "paper_url \fIurl\fR" 2 +.IX Item "paper_url url" +Tells the paper manager to set/update the url of the current file +.IP "paper_year \fIyear\fR" 2 +.IX Item "paper_year year" +Tells the paper manager to set/update the year of the current file .IP "pmap \fIkey\fR \fIcommand\fR" 2 .IX Item "pmap key command" Binds keys for the pager. Works like the \f(CW\*(C`map\*(C'\fR command. diff --git a/doc/ranger.pod b/doc/ranger.pod index 370bbb85..211aea11 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -490,6 +490,12 @@ Go to the next or previous tab. You can also use TAB and SHIFT+TAB instead. Close the current tab. The last tab cannot be closed this way. +=item M + +A key chain that allows you to quickly change the line mode of all the files of +the current directory. For a more permanent solution, use the command +"default_linemode" in your rc.conf. + =back =head2 READLINE-LIKE BINDINGS IN THE CONSOLE @@ -674,6 +680,12 @@ How many directory changes should be kept in history? Enable mouse input? +=item papermanager_deep_search [bool] + +When the paper manager module looks for metadata, should it only look for a +".paperinfo" file in the current directory, or do a deep search and check all +directories above the current one as well? + =item padding_right [bool] When collapse_preview is on and there is no preview, should there remain a @@ -813,18 +825,27 @@ ranger. For your convenience, this is a list of the "public" commands including copypmap key newkey [newkey2...] copytmap key newkey [newkey2...] cunmap keys... + default_linemode [path=regexp | tag=tags] linemodename delete edit [filename] eval [-q] python_code filter [string] + filter_inode_type [dfl] find pattern + flat level grep pattern + linemode linemodename load_copy_buffer map key command mark pattern mark_tag [tags] mkdir dirname open_with [application] [flags] [mode] + paper + paper_authors [authors] + paper_title [title] + paper_url [url] + paper_year [year] pmap key command punmap keys... quit @@ -927,6 +948,21 @@ See C<copymap> Removes key mappings of the console. Works like the C<unmap> command. +=item default_linemode [I<path=regexp> | I<tag=tags>] I<linemodename> + +Sets the default linemode. See I<linemode> command. + +Examples: + +Set the global default linemode to "permissions": + :default_linemode permissions + +Set the default linemode to "permissions" for all files tagged with "p" or "P": + :default_linemode tag=pP permissions + +Set the default linemode for all files in ~/books/ to "papertitle": + :default_linemode path=/home/.*?/books/.* papertitle + =item delete Destroy all files in the selection with a roundhouse kick. ranger will ask for @@ -955,6 +991,13 @@ this command without any parameter will reset the fitler. This command is based on the I<scout> command and supports all of its options. +=item filter_inode_type [dfl] + +Displays only the files of specified inode type. To display only directories, +use the 'd' parameter. To display only files, use the 'f' parameter. To display +only links, use the 'l' parameter. Parameters can be combined. To remove this +filter, use no parameter. + =item find I<pattern> Search files in the current directory that contain the given (case-insensitive) @@ -963,10 +1006,25 @@ be run immediately. (Or entered, if it's a directory.) This command is based on the I<scout> command and supports all of its options. +=item flat level + +Flattens the directory view up to the specified level. Level -1 means infinite +level. Level 0 means standard view without flattened directory view. Level +values -2 and less are invalid. + =item grep I<pattern> Looks for a string in all marked files or directories. +=item linemode I<linemodename> + +Sets the linemode of all files in the current directory. The linemode may be: + + "filename": display each line as "<basename>...<size>" + "permissions": display each line as "<permissions> <owner> <group> <basename>" + "papertitle": display metadata from .paperinfo files if available, fall back + to the "filename" linemode if no metadata was found. See :paper commands. + =item load_copy_buffer Load the copy buffer from F<~/.config/ranger/copy_buffer>. This can be used to @@ -1009,6 +1067,29 @@ displayed when pressing "r" in ranger. Note that if you specify an application, the mode is ignored. +=item paper + +This command opens a series of commands on the console that will ask the user +to input metadata about the current file. This is used by the paper manager +module of ranger and can be later displayed in ranger, for example by setting +the option "linemode" to "papertitle". + +=item paper_authors I<authors> + +Tells the paper manager to set/update the authors of the current file + +=item paper_title I<title> + +Tells the paper manager to set/update the title of the current file + +=item paper_url I<url> + +Tells the paper manager to set/update the url of the current file + +=item paper_year I<year> + +Tells the paper manager to set/update the year of the current file + =item pmap I<key> I<command> Binds keys for the pager. Works like the C<map> command. diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 953e2cc3..1f59ee90 100644 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -381,6 +381,43 @@ class setintag(setlocal): self.fm.set_option_from_string(name, value, tags=tags) +class default_linemode(Command): + def execute(self): + import re + from ranger.container.fsobject import POSSIBLE_LINEMODES + + if len(self.args) < 2: + self.fm.notify("Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True) + + # Extract options like "path=..." or "tag=..." from the command line + arg1 = self.arg(1) + method = "always" + argument = None + if arg1.startswith("path="): + method = "path" + argument = re.compile(arg1[5:]) + self.shift() + elif arg1.startswith("tag="): + method = "tag" + argument = arg1[4:] + self.shift() + + # Extract and validate the line mode from the command line + linemode = self.rest(1) + if linemode not in POSSIBLE_LINEMODES: + self.fm.notify("Invalid linemode: %s; should be %s" % + (linemode, "/".join(POSSIBLE_LINEMODES)), bad=True) + + # Add the prepared entry to the fm.default_linemodes + entry = [method, argument, linemode] + self.fm.default_linemodes.appendleft(entry) + + # Redraw the columns + if hasattr(self.fm.ui, "browser"): + for col in self.fm.ui.browser.columns: + col.need_redraw = True + + class quit(Command): """:quit @@ -1150,6 +1187,33 @@ class scout(Command): return count == 1 +class filter_inode_type(Command): + """ + :filter_inode_type [dfl] + + Displays only the files of specified inode type. Parameters + can be combined. + + d display directories + f display files + l display links + """ + + FILTER_DIRS = 'd' + FILTER_FILES = 'f' + FILTER_LINKS = 'l' + + def execute(self): + if not self.arg(1): + self.fm.thisdir.inode_type_filter = None + else: + self.fm.thisdir.inode_type_filter = lambda file: ( + True if ((self.FILTER_DIRS in self.arg(1) and file.is_directory) or + (self.FILTER_FILES in self.arg(1) and file.is_file and not file.is_link) or + (self.FILTER_LINKS in self.arg(1) and file.is_link)) else False) + self.fm.thisdir.refilter() + + class grep(Command): """:grep <string> @@ -1266,9 +1330,10 @@ class flat(Command): """ :flat <level> - Flattens the directory view up to level specified. + Flattens the directory view up to the specified level. + -1 fully flattened - 0 remove flattened view + 0 remove flattened view """ def execute(self): @@ -1277,7 +1342,94 @@ class flat(Command): level = int(level) except ValueError: level = self.quantifier + if level < -1: + self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True) self.fm.thisdir.unload() self.fm.thisdir.flat = level self.fm.thisdir.load_content() + +# Papermanager commands +# -------------------------------- +class paper(Command): + """ + :paper + + This command opens a series of commands on the console that will ask the + user to input metadata about the current file. This is used by the paper + manager module of ranger and can be later displayed in ranger, for example + by setting the option "linemode" to "papertitle". + """ + _paper_console_chain = None + def execute(self): + # TODO: This sets a pseudo-global variable containing a stack of + # commands that should be opened in the console next. It's a + # work-around for ranger's lack of inherent console command chaining + # and will hopefully be implemented properly in the future. + paper._paper_console_chain = ["url", "year", "authors", "title"] + + self._process_command_stack() + + def _process_command_stack(self): + if paper._paper_console_chain: + key = paper._paper_console_chain.pop() + self._paper_fill_console(key) + else: + for col in self.fm.ui.browser.columns: + col.need_redraw = True + + def _paper_fill_console(self, key): + paperinfo = self.fm.papermanager.get_paper_info(self.fm.thisfile.path) + if key in paperinfo and paperinfo[key]: + existing_value = paperinfo[key] + else: + existing_value = "" + text = "paper_%s %s" % (key, existing_value) + self.fm.open_console(text, position=len(text)) + + +class paper_title(paper): + """ + :paper_title <title> + + Tells the paper manager to set/update the title of the current file + """ + _key = "title" + + def execute(self): + update_dict = dict() + update_dict[self._key] = self.rest(1) + self.fm.papermanager.set_paper_info(self.fm.thisfile.path, update_dict) + self._process_command_stack() + + def tab(self): + paperinfo = self.fm.papermanager.get_paper_info(self.fm.thisfile.path) + if paperinfo[self._key]: + return self.arg(0) + " " + paperinfo[self._key] + + +class paper_authors(paper_title): + """ + :paper_authors <authors> + + Tells the paper manager to set/update the authors of the current file + """ + _key = "authors" + + +class paper_url(paper_title): + """ + :paper_url <authors> + + Tells the paper manager to set/update the url of the current file + """ + _key = "url" + + +class paper_year(paper_title): + """ + :paper_year <authors> + + Tells the paper manager to set/update the year of the current file + """ + _key = "year" diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 23cd2e10..12fd2a32 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -174,6 +174,11 @@ set show_selection_in_titlebar true # increases CPU load. set idle_delay 2000 +# When the paper manager module looks for metadata, should it only look for a +# ".paperinfo" file in the current directory, or do a deep search and check all +# directories above the current one as well? +set papermanager_deep_search false + # =================================================================== # == Local Options # =================================================================== @@ -232,6 +237,11 @@ map r chain draw_possible_programs; console open_with map f console find map cd console cd +# Change the line mode +map Mf linemode filename +map Mp linemode permissions +map Mt linemode papertitle + # Tagging / Marking map t tag_toggle map ut tag_remove diff --git a/ranger/container/directory.py b/ranger/container/directory.py index c6987a76..7d39daaf 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -20,7 +20,7 @@ from ranger.container.settings import LocalSettings def sort_by_basename(path): """returns path.basename (for sorting)""" - return path.basename + return path.drawn_basename def sort_by_basename_icase(path): """returns case-insensitive path.basename (for sorting)""" @@ -36,15 +36,39 @@ def sort_naturally(path): def sort_naturally_icase(path): return path.basename_natural_lower -def accept_file(fname, directory, hidden_filter, name_filter): - if hidden_filter and hidden_filter.search(fname): - return False - if name_filter and not name_filter.search(fname): - return False - if directory.temporary_filter and not directory.temporary_filter.search(fname): - return False +def accept_file(file, filters): + """ + Returns True if file shall be shown, otherwise False. + Parameters: + file - an instance of FileSystemObject + filters - an array of lambdas, each expects a file and + returns True if file shall be shown, + otherwise False. + """ + for filter in filters: + if filter and not filter(file): + return False return True +def walklevel(some_dir, level): + some_dir = some_dir.rstrip(os.path.sep) + followlinks = True if level > 0 else False + assert os.path.isdir(some_dir) + num_sep = some_dir.count(os.path.sep) + for root, dirs, files in os.walk(some_dir, followlinks=followlinks): + yield root, dirs, files + num_sep_this = root.count(os.path.sep) + 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): + dirlist = [os.path.join("/", dirpath, d) for d in dirnames + if level == -1 or dirpath.count(os.path.sep) - path.count(os.path.sep) <= 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 @@ -59,6 +83,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): files_all = None filter = None temporary_filter = None + inode_type_filter = None marked_items = None scroll_begin = 0 @@ -178,12 +203,14 @@ class Directory(FileSystemObject, Accumulator, Loadable): else: hidden_filter = None - self.files = [f for f in self.files_all if accept_file( - f.basename, self, hidden_filter, self.filter)] + filters = [(lambda file: not hidden_filter.search(file.basename)) if hidden_filter else None, + (lambda file: self.filter.search(file.basename)) if self.filter else None, + self.inode_type_filter] + self.files = [f for f in self.files_all if accept_file(f, filters)] self.move_to_obj(self.pointed_obj) # XXX: Check for possible race conditions - def load_bit_by_bit(self, flat=0): + def load_bit_by_bit(self): """An iterator that loads a part on every next() call Returns a generator which load a part of the directory @@ -194,17 +221,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): self.percent = 0 self.load_if_outdated() - basename_is_rel = True if flat else False - - def walklevel(some_dir, level): - some_dir = some_dir.rstrip(os.path.sep) - assert os.path.isdir(some_dir) - num_sep = some_dir.count(os.path.sep) - for root, dirs, files in os.walk(some_dir): - yield root, dirs, files - num_sep_this = root.count(os.path.sep) - if level != -1 and num_sep + level <= num_sep_this: - del dirs[:] + basename_is_rel_to = self.path if self.flat else None try: if self.runnable: @@ -213,17 +230,20 @@ class Directory(FileSystemObject, Accumulator, Loadable): self.mount_path = mount_path(mypath) - if flat: + if self.flat: filelist = [] - for dirpath, dirnames, filenames in walklevel(mypath, flat): - filelist += [os.path.join("/", dirpath, d) for d in dirnames - if dirpath.count(os.path.sep) - mypath.count(os.path.sep) == flat] + for dirpath, dirnames, filenames in walklevel(mypath, self.flat): + dirlist = [os.path.join("/", dirpath, d) for d in dirnames + if self.flat == -1 or dirpath.count(os.path.sep) - mypath.count(os.path.sep) <= self.flat] + filelist += dirlist filelist += [os.path.join("/", dirpath, f) for f in filenames] - filenames = [os.path.relpath(name, mypath) for name in filelist] + filenames = filelist + self.load_content_mtime = mtimelevel(mypath, self.flat) else: filelist = os.listdir(mypath) filenames = [mypath + (mypath == '/' and fname or '/' + fname) for fname in filelist] + self.load_content_mtime = os.stat(mypath).st_mtime if self._cumulative_size_calculated: # If self.content_loaded is true, this is not the first @@ -245,8 +265,6 @@ class Directory(FileSystemObject, Accumulator, Loadable): yield - self.load_content_mtime = os.stat(mypath).st_mtime - marked_paths = [obj.path for obj in self.marked_items] files = [] @@ -269,17 +287,20 @@ class Directory(FileSystemObject, Accumulator, Loadable): stats = None is_a_dir = False if is_a_dir: - try: - item = self.fm.get_directory(name, - basename_is_rel=basename_is_rel) - item.load_if_outdated() - except: + if self.flat: item = Directory(name, preload=stats, path_is_abs=True, - basename_is_rel=basename_is_rel) + basename_is_rel_to=basename_is_rel_to) item.load() + else: + try: + item = self.fm.get_directory(name) + item.load_if_outdated() + except: + item = Directory(name, preload=stats, path_is_abs=True) + item.load() else: item = File(name, preload=stats, path_is_abs=True, - basename_is_rel=basename_is_rel) + basename_is_rel_to=basename_is_rel_to) item.load() disk_usage += item.size @@ -350,7 +371,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): schedule = True # was: self.size > 30 if self.load_generator is None: - self.load_generator = self.load_bit_by_bit(flat=self.flat) + self.load_generator = self.load_bit_by_bit() if schedule and self.fm: self.fm.loader.add(self) @@ -530,7 +551,10 @@ class Directory(FileSystemObject, Accumulator, Loadable): return True try: - real_mtime = os.stat(self.path).st_mtime + if self.flat: + real_mtime = mtimelevel(self.path, self.flat) + else: + real_mtime = os.stat(self.path).st_mtime except OSError: real_mtime = None return False @@ -567,6 +591,10 @@ class Directory(FileSystemObject, Accumulator, Loadable): """Is the directory empty?""" return self.files is None or len(self.files) == 0 + def _set_linemode_of_children(self, mode): + for f in self.files: + f._set_linemode(mode) + def __nonzero__(self): """Always True""" return True diff --git a/ranger/container/file.py b/ranger/container/file.py index ab677125..7297dc30 100644 --- a/ranger/container/file.py +++ b/ranger/container/file.py @@ -43,6 +43,8 @@ class File(FileSystemObject): preview_known = False preview_loading = False + _linemode = "filename" + @property def firstbytes(self): try: diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index 6f206d0d..020721d8 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -12,9 +12,14 @@ DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits', BAD_INFO = '?' +POSSIBLE_LINEMODES = ("filename", "papertitle", "permissions") +DEFAULT_LINEMODE = "filename" + import re +from grp import getgrgid from os import lstat, stat, getcwd from os.path import abspath, basename, dirname, realpath, splitext, extsep, relpath +from pwd import getpwuid from ranger.core.shared import FileManagerAware, SettingsAware from ranger.ext.shell_escape import shell_escape from ranger.ext.spawn import spawn @@ -78,18 +83,22 @@ class FileSystemObject(FileManagerAware, SettingsAware): vcs_outdated = False vcs_enabled = False - basename_is_rel = False + basename_is_rel_to = None + + _linemode = DEFAULT_LINEMODE - def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel=False): + def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel_to=None): if not path_is_abs: path = abspath(path) self.path = path - self.basename_is_rel = basename_is_rel - if not basename_is_rel: + self.basename_is_rel_to = basename_is_rel_to + if basename_is_rel_to == None: self.basename = basename(path) + self.drawn_basename = self.basename else: - self.basename = relpath(path, getcwd()) - self.basename_lower = self.basename.lower() + self.basename = basename(path) + self.drawn_basename = relpath(path, basename_is_rel_to) + self.basename_lower = self.drawn_basename.lower() self.extension = splitext(self.basename)[1].lstrip(extsep) or None self.dirname = dirname(path) self.preload = preload @@ -101,6 +110,21 @@ class FileSystemObject(FileManagerAware, SettingsAware): except ValueError: self.extension = None + # Set the line mode from fm.default_linemodes + for method, argument, linemode in self.fm.default_linemodes: + if linemode in POSSIBLE_LINEMODES: + if method == "always": + self._linemode = linemode + break + if method == "path" and argument.search(path): + self._linemode = linemode + break + if method == "tag" and self.realpath in self.fm.tags and \ + self.fm.tags.marker(self.realpath) in argument: + self._linemode = linemode + break + + def __repr__(self): return "<{0} {1}>".format(self.__class__.__name__, self.path) @@ -118,7 +142,7 @@ class FileSystemObject(FileManagerAware, SettingsAware): @lazy_property def basename_natural(self): return [c if i % 3 == 1 else (int(c) if c else 0) for i, c in \ - enumerate(_extract_number_re.split(self.basename))] + enumerate(_extract_number_re.split(self.drawn_basename))] @lazy_property def basename_natural_lower(self): @@ -129,6 +153,19 @@ class FileSystemObject(FileManagerAware, SettingsAware): def safe_basename(self): return self.basename.translate(_safe_string_table) + @lazy_property + def user(self): + try: + return getpwuid(self.stat.st_uid)[0] + except: + return str(self.stat.st_uid) + + @lazy_property + def group(self): + try: + return getgrgid(self.stat.st_gid)[0] + except: + return str(self.stat.st_gid) for attr in ('video', 'audio', 'image', 'media', 'document', 'container'): exec("%s = lazy_property(" @@ -373,3 +410,6 @@ class FileSystemObject(FileManagerAware, SettingsAware): self.vcs_outdated = True return True return False + + def _set_linemode(self, mode): + self._linemode = mode diff --git a/ranger/container/settings.py b/ranger/container/settings.py index f75c274f..ad42b13f 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -31,6 +31,7 @@ ALLOWED_SETTINGS = { 'mouse_enabled': bool, 'open_all_images': bool, 'padding_right': bool, + 'papermanager_deep_search': bool, 'preview_directories': bool, 'preview_files': bool, 'preview_images': bool, diff --git a/ranger/core/actions.py b/ranger/core/actions.py index ba6b3658..879d1f68 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -27,6 +27,7 @@ from ranger.core.tab import Tab from ranger.container.file import File from ranger.core.loader import CommandLoader, CopyLoader from ranger.container.settings import ALLOWED_SETTINGS +from ranger.container.fsobject import POSSIBLE_LINEMODES, DEFAULT_LINEMODE MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>" @@ -51,6 +52,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.garbage_collect(-1) self.enter_dir(old_path) self.change_mode('normal') + if self.papermanager: + self.papermanager.reset() def change_mode(self, mode): if mode == self.mode: @@ -151,6 +154,50 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): """Redraw the window""" self.ui.redraw_window() + def linemode(self, mode, directory=None, depth=0): + """ + Change what is displayed as a filename. + + - "mode" may be: "filename", "permissions", "papertitle", the mode "normal" + is mapped to "filename". + - "directory" specifies the directory. None means the current directory + - "depth" specifies the recursion depth + """ + + assert mode == "normal" or mode in POSSIBLE_LINEMODES + + if mode == "normal": + mode = DEFAULT_LINEMODE + + if directory is None: + directory = self.fm.thisdir + + directories = set([directory]) + bucket = set() + + current_depth = 0 + + while True: + if current_depth >= depth: + for direct in directories: + direct._set_linemode_of_children(mode) + break + + else: + for direct in directories: + direct._set_linemode_of_children(mode) + for file_ in direct.files: + if file_.is_directory: + bucket.add(file_) + + directories, bucket = bucket, directories + bucket.clear() + current_depth += 1 + + # Ask the browsercolumns to redraw + for col in self.ui.browser.columns: + col.need_redraw = True + def open_console(self, string='', prompt=None, position=None): """Open the console""" self.change_mode('normal') diff --git a/ranger/core/fm.py b/ranger/core/fm.py index d567bf24..6bb4fd22 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -20,6 +20,7 @@ from ranger.gui.ui import UI from ranger.container.bookmarks import Bookmarks from ranger.core.runner import Runner from ranger.ext.img_display import ImageDisplayer +from ranger.ext.papermanager import PaperManager from ranger.ext.rifle import Rifle from ranger.container.directory import Directory from ranger.ext.signals import SignalDispatcher @@ -56,9 +57,14 @@ class FM(Actions, SignalDispatcher): self.restorable_tabs = deque([], ranger.MAX_RESTORABLE_TABS) self.py3 = sys.version_info >= (3, ) self.previews = {} + self.default_linemodes = deque() self.loader = Loader() self.copy_buffer = set() self.do_cut = False + self.papermanager = PaperManager() + self.settings.signal_bind('setopt.papermanager_deep_search', + lambda signal: setattr(signal.fm.papermanager, 'deep_search', + signal.value)) try: self.username = pwd.getpwuid(os.geteuid()).pw_name @@ -252,17 +258,13 @@ class FM(Actions, SignalDispatcher): """returns the path relative to rangers library directory""" return os.path.join(ranger.RANGERDIR, *paths) - def get_directory(self, path, basename_is_rel = False): + def get_directory(self, path): """Get the directory object at the given path""" path = os.path.abspath(path) try: - directory = self.directories[path] - if directory.basename_is_rel != basename_is_rel: - del self.directories[path] - raise KeyError - return directory + return self.directories[path] except KeyError: - obj = Directory(path, basename_is_rel = basename_is_rel) + obj = Directory(path) self.directories[path] = obj return obj diff --git a/ranger/ext/papermanager.py b/ranger/ext/papermanager.py new file mode 100644 index 00000000..a6292c49 --- /dev/null +++ b/ranger/ext/papermanager.py @@ -0,0 +1,171 @@ +# Copyright (C) 2014 Roman Zimbelmann <hut@lepus.uberspace.de> +# This software is distributed under the terms of the GNU GPL version 3. + +""" +A Paper Manager that reads metadata information about papers from a file. + +The file is named .paperinfo and is formatted as comma-separated values. + +The columns are: +1. Filename +2. Date +3. Title +4. Authors +5. URL +""" + +PAPERINFO_FILE_NAME = ".paperinfo" +DEEP_SEARCH_DEFAULT = False + +import csv +from os.path import join, dirname, exists, basename + +from ranger.ext.openstruct import OpenStruct + +class PaperManager(object): + def __init__(self): + self.metadata_cache = dict() + self.metafile_cache = dict() + self.deep_search = DEEP_SEARCH_DEFAULT + + def reset(self): + self.metadata_cache.clear() + self.metafile_cache.clear() + + def get_paper_info(self, filename): + try: + return self.metadata_cache[filename] + except KeyError: + result = OpenStruct(filename=filename, title=None, year=None, + authors=None, url=None) + + valid = (filename, basename(filename)) + for metafile in self._get_metafile_names(filename): + for entry in self._get_metafile_content(metafile): + if entry[0] in valid: + self._fill_ostruct_with_data(result, entry) + self.metadata_cache[filename] = result + return result + + # Cache the value + self.metadata_cache[filename] = result + return result + + def set_paper_info(self, filename, update_dict): + result = None + found = False + valid = (filename, basename(filename)) + first_metafile = None + + if not self.deep_search: + metafile = next(self._get_metafile_names(filename)) + return self._set_paper_info_raw(filename, update_dict, metafile) + + for i, metafile in enumerate(self._get_metafile_names(filename)): + if i == 0: + first_metafile = metafile + + csvfile = None + try: + csvfile = open(metafile, "r") + except: + # .paperinfo file doesn't exist... look for another one. + pass + else: + reader = csv.reader(csvfile, skipinitialspace=True) + for row in reader: + name, year, title, authors, url = row + if name in valid: + return self._set_paper_info_raw(filename, update_dict, + metafile) + self.metadata_cache[filename] = result + finally: + if csvfile: + csvfile.close() + + # No .paperinfo file found, so let's create a new one in the same path + # as the given file. + if first_metafile: + return self._set_paper_info_raw(filename, update_dict, first_metafile) + + def _set_paper_info_raw(self, filename, update_dict, metafile): + valid = (filename, basename(filename)) + paperinfo = OpenStruct(filename=filename, title=None, year=None, + authors=None, url=None) + + try: + with open(metafile, "r") as infile: + reader = csv.reader(infile, skipinitialspace=True) + rows = list(reader) + except IOError: + rows = [] + + with open(metafile, "w") as outfile: + writer = csv.writer(outfile) + found = False + + # Iterate through all rows and write them back to the file. + for row in rows: + if not found and row[0] in valid: + # When finding the row that corresponds to the given filename, + # update the items with the information from update_dict. + self._fill_row_with_ostruct(row, update_dict) + self._fill_ostruct_with_data(paperinfo, row) + self.metadata_cache[filename] = paperinfo + found = True + writer.writerow(row) + + # If the row was never found, create a new one. + if not found: + row = [basename(filename), None, None, None, None] + self._fill_row_with_ostruct(row, update_dict) + self._fill_ostruct_with_data(paperinfo, row) + self.metadata_cache[filename] = paperinfo + writer.writerow(row) + + def _get_metafile_content(self, metafile): + if metafile in self.metafile_cache: + return self.metafile_cache[metafile] + else: + if exists(metafile): + reader = csv.reader(open(metafile, "r"), skipinitialspace=True) + + entries = list(entry for entry in reader if len(entry) == 5) + self.metafile_cache[metafile] = entries + return entries + else: + return [] + + def _get_metafile_names(self, path): + # Iterates through the paths of all .paperinfo files that could + # influence the metadata of the given file. + # When deep_search is deactivated, this only yields the .paperinfo file + # in the same directory as the given file. + + base = dirname(path) + yield join(base, PAPERINFO_FILE_NAME) + if self.deep_search: + dirs = base.split("/")[1:] + for i in reversed(range(len(dirs))): + yield join("/" + "/".join(dirs[0:i]), PAPERINFO_FILE_NAME) + + def _fill_ostruct_with_data(self, ostruct, dataset): + # Copy data from a CSV row to a dict/ostruct + + filename, year, title, authors, url = dataset + if year: ostruct['year'] = year + if title: ostruct['title'] = title + if authors: ostruct['authors'] = authors + if url: ostruct['url'] = url + + def _fill_row_with_ostruct(self, row, update_dict): + # Copy data from a dict/ostruct into a CSV row + for key, value in update_dict.items(): + if key == "year": + row[1] = value + elif key == "title": + row[2] = value + elif key == "authors": + row[3] = value + elif key == "url": + row[4] = value diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 0edb9c8d..5eabe2c6 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -248,16 +248,38 @@ class BrowserColumn(Pager): else: tagged_marker = " " + # Extract linemode-related information from the drawn object + paperinfo = None + use_linemode = drawn._linemode + if use_linemode == "papertitle": + paperinfo = self.fm.papermanager.get_paper_info(drawn.path) + if not paperinfo.title: + use_linemode = "filename" + key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, - drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut) + drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut, + use_linemode) if key in drawn.display_data: self.execute_curses_batch(line, drawn.display_data[key]) self.color_reset() continue - text = drawn.basename + + # Deal with the line mode + if use_linemode == "papertitle": + if paperinfo.year: + text = "%s - %s" % (paperinfo.year, paperinfo.title) + else: + text = paperinfo.title + if use_linemode == "filename": + text = drawn.drawn_basename + elif use_linemode == "permissions": + text = "%s %s %s %s" % (drawn.get_permission_string(), + drawn.user, drawn.group, drawn.drawn_basename) + + if drawn.marked and (self.main_column or \ self.settings.display_tags_in_all_columns): text = " " + text @@ -285,11 +307,21 @@ class BrowserColumn(Pager): space -= vcsstringlen # info string - infostring = self._draw_infostring_display(drawn, space) - infostringlen = self._total_len(infostring) - if space - infostringlen > 2: - predisplay_right = infostring + predisplay_right - space -= infostringlen + infostring = [] + infostringlen = 0 + if use_linemode == "filename": + infostring = self._draw_infostring_display(drawn, space) + elif use_linemode == "papertitle": + if paperinfo and paperinfo.authors: + authorstring = paperinfo.authors + if ',' in authorstring: + authorstring = authorstring[0:authorstring.find(",")] + infostring.append([" " + authorstring + " ", ["infostring"]]) + if infostring: + infostringlen = self._total_len(infostring) + if space - infostringlen > 2: + predisplay_right = infostring + predisplay_right + space -= infostringlen textstring = self._draw_text_display(text, space) textstringlen = self._total_len(textstring) diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py index fa10a744..25edd5e4 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -117,7 +117,7 @@ class TitleBar(Widget): if self.fm.thisfile is not None and \ self.settings.show_selection_in_titlebar: - bar.add(self.fm.thisfile.basename, 'file') + bar.add(self.fm.thisfile.drawn_basename, 'file') def _get_right_part(self, bar): # TODO: fix that pressed keys are cut off when chaining CTRL keys |