tenseleyflow/ndotfiles / 9961c30

Browse files

ranger config + fleshed out neovim

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
9961c30634950b760c317a39ae18cba846e74410
Parents
6bcadd8
Tree
cf2a24c

6 changed files

StatusFile+-
M nvim/init.lua 71 36
A ranger/commands.py 261 0
A ranger/commands_full.py 1993 0
A ranger/rc.conf 361 0
A ranger/rifle.conf 42 0
A ranger/scope.sh 350 0
nvim/init.luamodified
@@ -61,7 +61,14 @@ require("lazy").setup({
6161
   -- Files, search, projects ----------------------------------
6262
   { "nvim-telescope/telescope.nvim", dependencies = { "nvim-lua/plenary.nvim" } },
6363
   { "nvim-telescope/telescope-fzf-native.nvim", build = "make", cond = function() return vim.fn.executable("make") == 1 end },
64
-  { "ahmedkhalf/project.nvim", opts = { detection_methods = { "pattern", "lsp" }, patterns = { ".git", "pyproject.toml", "package.json", "Makefile" } } },
64
+  { "ahmedkhalf/project.nvim", 
65
+    config = function()
66
+      require("project_nvim").setup({
67
+        detection_methods = { "pattern", "lsp" },
68
+        patterns = { ".git", "pyproject.toml", "package.json", "Makefile" }
69
+      })
70
+    end
71
+  },
6572
   { "stevearc/oil.nvim", opts = { view_options = { show_hidden = true } } },
6673
 
6774
   -- Git -------------------------------------------------------
@@ -73,15 +80,18 @@ require("lazy").setup({
7380
   { "stevearc/overseer.nvim", opts = {} },
7481
 
7582
   -- Treesitter ------------------------------------------------
76
-  { "nvim-treesitter/nvim-treesitter", build = ":TSUpdate", opts = {
77
-      ensure_installed = {
78
-        "bash", "c", "cpp", "lua", "python", "rust", "json", "yaml", "toml",
79
-        "html", "css", "javascript", "typescript", "markdown", "markdown_inline",
80
-        "make", "fish", "fortran"
81
-      },
82
-      highlight = { enable = true },
83
-      indent = { enable = true },
84
-    }
83
+  { "nvim-treesitter/nvim-treesitter", build = ":TSUpdate",
84
+    config = function()
85
+      require("nvim-treesitter.configs").setup({
86
+        ensure_installed = {
87
+          "bash", "c", "cpp", "lua", "python", "rust", "json", "yaml", "toml",
88
+          "html", "css", "javascript", "typescript", "markdown", "markdown_inline",
89
+          "make", "fish"
90
+        },
91
+        highlight = { enable = true },
92
+        indent = { enable = true },
93
+      })
94
+    end
8595
   },
8696
 
8797
   -- LSP, format, lint ----------------------------------------
@@ -104,14 +114,13 @@ require("lazy").setup({
104114
         javascript = { "prettier" }, typescript = { "prettier" },
105115
         json = { "jq", "prettier" }, yaml = { "prettier" }, toml = { "taplo" },
106116
         html = { "prettier" }, css = { "prettier" }, markdown = { "prettier" },
107
-        fortran = { "fprettify" },
108117
       },
109118
     }
110119
   },
111120
 
112121
   -- Debugging (optional, light defaults) ---------------------
113122
   { "mfussenegger/nvim-dap" },
114
-  { "rcarriga/nvim-dap-ui", dependencies = { "mfussenegger/nvim-dap" } },
123
+  { "rcarriga/nvim-dap-ui", dependencies = { "mfussenegger/nvim-dap", "nvim-neotest/nvim-nio" } },
115124
 }, {
116125
   install = { colorscheme = { "tokyonight" } },
117126
   change_detection = { notify = false },
@@ -128,8 +137,18 @@ require("lualine").setup({})
128137
 
129138
 -- Telescope
130139
 local telescope = require("telescope")
131
-telescope.setup({ defaults = { mappings = { i = { ["<C-j>"] = "move_selection_next", ["<C-k>"] = "move_selection_previous" } } } })
140
+telescope.setup({ 
141
+  defaults = { 
142
+    mappings = { 
143
+      i = { 
144
+        ["<C-j>"] = "move_selection_next", 
145
+        ["<C-k>"] = "move_selection_previous" 
146
+      } 
147
+    } 
148
+  } 
149
+})
132150
 pcall(telescope.load_extension, "fzf")
151
+pcall(telescope.load_extension, "projects")
133152
 
134153
 -- Oil: simple file manager toggle
135154
 vim.keymap.set("n", "-", function() require("oil").toggle_float() end, { desc = "Oil file manager" })
@@ -144,21 +163,20 @@ vim.keymap.set("n", "<leader>tr", ":OverseerRun<CR>", { desc = "Run a task" }
144163
 -- Gitsigns
145164
 require("gitsigns").setup()
146165
 
147
--- Treesitter
148
-require("nvim-treesitter.configs").setup({})
149
-
150166
 -- Mason + LSPConfig
151167
 require("mason").setup()
152
-local mlsp = require("mason-lspconfig")
153
-mlsp.setup({ ensure_installed = {
154
-  "pyright", "ruff_lsp", "lua_ls", "clangd", "rust_analyzer", "bashls",
155
-  "jsonls", "yamlls", "html", "cssls", "ts_ls", "marksman", "taplo", "fortls"
156
-}})
168
+local mason_lspconfig = require("mason-lspconfig")
169
+
170
+-- Ensure these servers are installed
171
+mason_lspconfig.setup({
172
+  ensure_installed = {
173
+    "pyright", "lua_ls", "clangd", "rust_analyzer", "bashls",
174
+    "jsonls", "yamlls", "html", "cssls", "marksman", "taplo"
175
+  }
176
+})
157177
 
158178
 local lspconfig = require("lspconfig")
159179
 local capabilities = vim.lsp.protocol.make_client_capabilities()
160
-local ok_cmp, cmp_lsp = pcall(require, "cmp_nvim_lsp")
161
-if ok_cmp then capabilities = cmp_lsp.default_capabilities(capabilities) end
162180
 
163181
 -- Pretty borders
164182
 local handlers = {
@@ -181,19 +199,35 @@ local on_attach = function(_, bufnr)
181199
   nmap("<leader>fd", function() vim.lsp.buf.format({ async = true }) end, "Format buffer")
182200
 end
183201
 
184
-mlsp.setup_handlers({ function(server)
185
-  lspconfig[server].setup({ capabilities = capabilities, on_attach = on_attach, handlers = handlers })
186
-end })
187
-
188
--- Lua LS: tuned for Neovim config dev
189
-lspconfig.lua_ls.setup({
190
-  capabilities = capabilities,
191
-  on_attach = on_attach,
192
-  settings = { Lua = { diagnostics = { globals = { "vim" } }, workspace = { checkThirdParty = false } } }
193
-})
202
+-- Manual server setup (more compatible)
203
+local servers = {
204
+  pyright = {},
205
+  lua_ls = {
206
+    settings = {
207
+      Lua = {
208
+        diagnostics = { globals = { "vim" } },
209
+        workspace = { checkThirdParty = false }
210
+      }
211
+    }
212
+  },
213
+  clangd = {},
214
+  rust_analyzer = {},
215
+  bashls = {},
216
+  jsonls = {},
217
+  yamlls = {},
218
+  html = {},
219
+  cssls = {},
220
+  marksman = {},
221
+  taplo = {},
222
+}
194223
 
195
--- Ruff: prefer as linter + fixer with Pyright for types
196
-lspconfig.ruff_lsp.setup({ capabilities = capabilities, on_attach = on_attach })
224
+-- Setup each server
225
+for server, config in pairs(servers) do
226
+  config.capabilities = capabilities
227
+  config.on_attach = on_attach
228
+  config.handlers = handlers
229
+  lspconfig[server].setup(config)
230
+end
197231
 
198232
 -- DAP minimal sugar
199233
 local dap_ok, dapui = pcall(require, "dapui")
@@ -206,7 +240,7 @@ if dap_ok then
206240
 end
207241
 
208242
 ---------------------------------------------------------------
209
--- 4) Keymaps you’ll actually use (and remember)
243
+-- 4) Keymaps you'll actually use (and remember)
210244
 ---------------------------------------------------------------
211245
 local map = vim.keymap.set
212246
 -- Save, quit
@@ -223,6 +257,7 @@ map("n", "<leader>ff", function() require("telescope.builtin").find_files() end,
223257
 map("n", "<leader>fg", function() require("telescope.builtin").live_grep()  end, { desc = "Live grep" })
224258
 map("n", "<leader>fb", function() require("telescope.builtin").buffers()    end, { desc = "Buffers" })
225259
 map("n", "<leader>fh", function() require("telescope.builtin").help_tags()  end, { desc = "Help tags" })
260
+map("n", "<leader>fp", function() require("telescope").extensions.projects.projects() end, { desc = "Projects" })
226261
 
227262
 -- Move lines (visual)
228263
 map("v", "J", ":m '>+1<CR>gv=gv")
ranger/commands.pyadded
@@ -0,0 +1,261 @@
1
+# This is a sample commands.py.  You can add your own commands here.
2
+#
3
+# Please refer to commands_full.py for all the default commands and a complete
4
+# documentation.  Do NOT add them all here, or you may end up with defunct
5
+# commands when upgrading ranger.
6
+
7
+# A simple command for demonstration purposes follows.
8
+# -----------------------------------------------------------------------------
9
+
10
+from __future__ import (absolute_import, division, print_function)
11
+
12
+# You can import any python module as needed.
13
+import os
14
+
15
+# You always need to import ranger.api.commands here to get the Command class:
16
+from ranger.api.commands import Command
17
+
18
+
19
+# Any class that is a subclass of "Command" will be integrated into ranger as a
20
+# command.  Try typing ":my_edit<ENTER>" in ranger!
21
+class my_edit(Command):
22
+    # The so-called doc-string of the class will be visible in the built-in
23
+    # help that is accessible by typing "?c" inside ranger.
24
+    """:my_edit <filename>
25
+
26
+    A sample command for demonstration purposes that opens a file in an editor.
27
+    """
28
+
29
+    # The execute method is called when you run this command in ranger.
30
+    def execute(self):
31
+        # self.arg(1) is the first (space-separated) argument to the function.
32
+        # This way you can write ":my_edit somefilename<ENTER>".
33
+        if self.arg(1):
34
+            # self.rest(1) contains self.arg(1) and everything that follows
35
+            target_filename = self.rest(1)
36
+        else:
37
+            # self.fm is a ranger.core.filemanager.FileManager object and gives
38
+            # you access to internals of ranger.
39
+            # self.fm.thisfile is a ranger.container.file.File object and is a
40
+            # reference to the currently selected file.
41
+            target_filename = self.fm.thisfile.path
42
+
43
+        # This is a generic function to print text in ranger.
44
+        self.fm.notify("Let's edit the file " + target_filename + "!")
45
+
46
+        # Using bad=True in fm.notify allows you to print error messages:
47
+        if not os.path.exists(target_filename):
48
+            self.fm.notify("The given file does not exist!", bad=True)
49
+            return
50
+
51
+        # This executes a function from ranger.core.acitons, a module with a
52
+        # variety of subroutines that can help you construct commands.
53
+        # Check out the source, or run "pydoc ranger.core.actions" for a list.
54
+        self.fm.edit_file(target_filename)
55
+
56
+    # The tab method is called when you press tab, and should return a list of
57
+    # suggestions that the user will tab through.
58
+    # tabnum is 1 for <TAB> and -1 for <S-TAB> by default
59
+    def tab(self, tabnum):
60
+        # This is a generic tab-completion function that iterates through the
61
+        # content of the current directory.
62
+        return self._tab_directory_content()
63
+
64
+class fzf_select(Command):
65
+    """
66
+    :fzf_select
67
+    Find a file or directory using fzf.
68
+    """
69
+    def execute(self):
70
+        import subprocess
71
+        import os.path
72
+        from ranger.ext.get_executables import get_executables
73
+        
74
+        if 'fzf' not in get_executables():
75
+            self.fm.notify('Could not find fzf in PATH', bad=True)
76
+            return
77
+        
78
+        env = os.environ.copy()
79
+        env['FZF_DEFAULT_OPTS'] = '--height=40% --layout=reverse --border'
80
+        
81
+        # Use fd if available, else find
82
+        if 'fd' in get_executables():
83
+            fzf = self.fm.execute_command('fd . | fzf', env=env,
84
+                                        universal_newlines=True,
85
+                                        stdout=subprocess.PIPE)
86
+        else:
87
+            fzf = self.fm.execute_command('find . | fzf', env=env,
88
+                                        universal_newlines=True,
89
+                                        stdout=subprocess.PIPE)
90
+        
91
+        stdout, _ = fzf.communicate()
92
+        if fzf.returncode == 0:
93
+            selected = os.path.abspath(stdout.strip())
94
+            if os.path.isdir(selected):
95
+                self.fm.cd(selected)
96
+            else:
97
+                self.fm.select_file(selected)
98
+                
99
+class fzf_locate(Command):
100
+    """
101
+    :fzf_locate
102
+    Find a file in the whole system using locate and fzf.
103
+    """
104
+    def execute(self):
105
+        import subprocess
106
+        import os.path
107
+        from ranger.ext.get_executables import get_executables
108
+        
109
+        if 'fzf' not in get_executables():
110
+            self.fm.notify('Could not find fzf in PATH', bad=True)
111
+            return
112
+            
113
+        if 'locate' not in get_executables():
114
+            self.fm.notify('Could not find locate in PATH', bad=True)
115
+            return
116
+        
117
+        env = os.environ.copy()
118
+        env['FZF_DEFAULT_OPTS'] = '--height=40% --layout=reverse --border'
119
+        
120
+        fzf = self.fm.execute_command('locate / | fzf', env=env,
121
+                                    universal_newlines=True,
122
+                                    stdout=subprocess.PIPE)
123
+        
124
+        stdout, _ = fzf.communicate()
125
+        if fzf.returncode == 0:
126
+            selected = stdout.strip()
127
+            if os.path.exists(selected):
128
+                if os.path.isdir(selected):
129
+                    self.fm.cd(selected)
130
+                else:
131
+                    self.fm.select_file(selected)
132
+
133
+class fzf_grep(Command):
134
+    """
135
+    :fzf_grep [<search_term>]
136
+    Search file contents using ripgrep and open with fzf.
137
+    """
138
+    def execute(self):
139
+        import subprocess
140
+        import os
141
+        from ranger.ext.get_executables import get_executables
142
+        
143
+        if 'rg' not in get_executables():
144
+            self.fm.notify('Could not find ripgrep in PATH', bad=True)
145
+            return
146
+        if 'fzf' not in get_executables():
147
+            self.fm.notify('Could not find fzf in PATH', bad=True)
148
+            return
149
+            
150
+        search_term = self.rest(1)
151
+        if not search_term:
152
+            self.fm.notify('Usage: fzf_grep <search_term>', bad=True)
153
+            return
154
+        
155
+        env = os.environ.copy()
156
+        env['FZF_DEFAULT_OPTS'] = '--height=40% --layout=reverse --border'
157
+        
158
+        command = f"rg --files-with-matches --no-messages '{search_term}' | fzf"
159
+        fzf = self.fm.execute_command(command, env=env,
160
+                                    universal_newlines=True,
161
+                                    stdout=subprocess.PIPE)
162
+        
163
+        stdout, _ = fzf.communicate()
164
+        if fzf.returncode == 0:
165
+            selected = os.path.abspath(stdout.strip())
166
+            self.fm.select_file(selected)
167
+
168
+class fzf_cd(Command):
169
+    """
170
+    :fzf_cd
171
+    Change to a subdirectory using fzf.
172
+    """
173
+    def execute(self):
174
+        import subprocess
175
+        import os.path
176
+        from ranger.ext.get_executables import get_executables
177
+        
178
+        if 'fzf' not in get_executables():
179
+            self.fm.notify('Could not find fzf in PATH', bad=True)
180
+            return
181
+        
182
+        env = os.environ.copy()
183
+        env['FZF_DEFAULT_OPTS'] = '--height=40% --layout=reverse --border'
184
+        
185
+        # Find only directories
186
+        if 'fd' in get_executables():
187
+            command = 'fd --type d . | fzf'
188
+        else:
189
+            command = 'find . -type d | fzf'
190
+        
191
+        fzf = self.fm.execute_command(command, env=env,
192
+                                    universal_newlines=True,
193
+                                    stdout=subprocess.PIPE)
194
+        
195
+        stdout, _ = fzf.communicate()
196
+        if fzf.returncode == 0:
197
+            selected = os.path.abspath(stdout.strip())
198
+            self.fm.cd(selected)
199
+
200
+class mkcd(Command):
201
+    """
202
+    :mkcd <dirname>
203
+    Create a directory and enter it.
204
+    """
205
+    def execute(self):
206
+        from os.path import join, expanduser, lexists
207
+        from os import makedirs
208
+        
209
+        dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
210
+        if not lexists(dirname):
211
+            makedirs(dirname)
212
+            self.fm.cd(dirname)
213
+        else:
214
+            self.fm.notify("Directory already exists!", bad=True)
215
+
216
+class compress(Command):
217
+    """
218
+    :compress [<filename>]
219
+    Compress selected files to archive.
220
+    """
221
+    def execute(self):
222
+        import os
223
+        cwd = self.fm.thisdir
224
+        marked_files = cwd.get_selection()
225
+        
226
+        if not marked_files:
227
+            return
228
+        
229
+        def refresh(_):
230
+            cwd = self.fm.get_directory(original_path)
231
+            cwd.load_content()
232
+        
233
+        original_path = cwd.path
234
+        parts = self.line.split()
235
+        if len(parts) > 1:
236
+            archive_name = parts[1]
237
+        else:
238
+            archive_name = os.path.basename(marked_files[0]) + '.tar.gz'
239
+        
240
+        # Create archive command
241
+        files_list = ' '.join([f.relative_path for f in marked_files])
242
+        self.fm.execute_console(f'shell tar czf {archive_name} {files_list}')
243
+        self.fm.reload_cwd()
244
+
245
+class extract(Command):
246
+    """
247
+    :extract
248
+    Extract selected archives.
249
+    """
250
+    def execute(self):
251
+        cwd = self.fm.thisdir
252
+        marked_files = cwd.get_selection()
253
+        
254
+        if not marked_files:
255
+            return
256
+        
257
+        for f in marked_files:
258
+            if f.is_file:
259
+                self.fm.execute_console(f'shell atool -x {f.relative_path}')
260
+        
261
+        self.fm.reload_cwd()
ranger/commands_full.pyadded
1993 lines changed — click to load
@@ -0,0 +1,1993 @@
1
+# -*- coding: utf-8 -*-
2
+# This file is part of ranger, the console file manager.
3
+# This configuration file is licensed under the same terms as ranger.
4
+# ===================================================================
5
+#
6
+# NOTE: If you copied this file to /etc/ranger/commands_full.py or
7
+# ~/.config/ranger/commands_full.py, then it will NOT be loaded by ranger,
8
+# and only serve as a reference.
9
+#
10
+# ===================================================================
11
+# This file contains ranger's commands.
12
+# It's all in python; lines beginning with # are comments.
13
+#
14
+# Note that additional commands are automatically generated from the methods
15
+# of the class ranger.core.actions.Actions.
16
+#
17
+# You can customize commands in the files /etc/ranger/commands.py (system-wide)
18
+# and ~/.config/ranger/commands.py (per user).
19
+# They have the same syntax as this file.  In fact, you can just copy this
20
+# file to ~/.config/ranger/commands_full.py with
21
+# `ranger --copy-config=commands_full' and make your modifications, don't
22
+# forget to rename it to commands.py.  You can also use
23
+# `ranger --copy-config=commands' to copy a short sample commands.py that
24
+# has everything you need to get started.
25
+# But make sure you update your configs when you update ranger.
26
+#
27
+# ===================================================================
28
+# Every class defined here which is a subclass of `Command' will be used as a
29
+# command in ranger.  Several methods are defined to interface with ranger:
30
+#   execute():   called when the command is executed.
31
+#   cancel():    called when closing the console.
32
+#   tab(tabnum): called when <TAB> is pressed.
33
+#   quick():     called after each keypress.
34
+#
35
+# tab() argument tabnum is 1 for <TAB> and -1 for <S-TAB> by default
36
+#
37
+# The return values for tab() can be either:
38
+#   None: There is no tab completion
39
+#   A string: Change the console to this string
40
+#   A list/tuple/generator: cycle through every item in it
41
+#
42
+# The return value for quick() can be:
43
+#   False: Nothing happens
44
+#   True: Execute the command afterwards
45
+#
46
+# The return value for execute() and cancel() doesn't matter.
47
+#
48
+# ===================================================================
49
+# Commands have certain attributes and methods that facilitate parsing of
50
+# the arguments:
51
+#
52
+# self.line: The whole line that was written in the console.
53
+# self.args: A list of all (space-separated) arguments to the command.
54
+# self.quantifier: If this command was mapped to the key "X" and
55
+#      the user pressed 6X, self.quantifier will be 6.
56
+# self.arg(n): The n-th argument, or an empty string if it doesn't exist.
57
+# self.rest(n): The n-th argument plus everything that followed.  For example,
58
+#      if the command was "search foo bar a b c", rest(2) will be "bar a b c"
59
+# self.start(n): Anything before the n-th argument.  For example, if the
60
+#      command was "search foo bar a b c", start(2) will be "search foo"
61
+#
62
+# ===================================================================
63
+# And this is a little reference for common ranger functions and objects:
64
+#
65
+# self.fm: A reference to the "fm" object which contains most information
66
+#      about ranger.
67
+# self.fm.notify(string): Print the given string on the screen.
68
+# self.fm.notify(string, bad=True): Print the given string in RED.
69
+# self.fm.reload_cwd(): Reload the current working directory.
70
+# self.fm.thisdir: The current working directory. (A File object.)
71
+# self.fm.thisfile: The current file. (A File object too.)
72
+# self.fm.thistab.get_selection(): A list of all selected files.
73
+# self.fm.execute_console(string): Execute the string as a ranger command.
74
+# self.fm.open_console(string): Open the console with the given string
75
+#      already typed in for you.
76
+# self.fm.move(direction): Moves the cursor in the given direction, which
77
+#      can be something like down=3, up=5, right=1, left=1, to=6, ...
78
+#
79
+# File objects (for example self.fm.thisfile) have these useful attributes and
80
+# methods:
81
+#
82
+# tfile.path: The path to the file.
83
+# tfile.basename: The base name only.
84
+# tfile.load_content(): Force a loading of the directories content (which
85
+#      obviously works with directories only)
86
+# tfile.is_directory: True/False depending on whether it's a directory.
87
+#
88
+# For advanced commands it is unavoidable to dive a bit into the source code
89
+# of ranger.
90
+# ===================================================================
91
+
92
+from __future__ import (absolute_import, division, print_function)
93
+
94
+from collections import deque
95
+import os
96
+import re
97
+
98
+from ranger.api.commands import Command
99
+
100
+
101
+class alias(Command):
102
+    """:alias <newcommand> <oldcommand>
103
+
104
+    Copies the oldcommand as newcommand.
105
+    """
106
+
107
+    context = 'browser'
108
+    resolve_macros = False
109
+
110
+    def execute(self):
111
+        if not self.arg(1) or not self.arg(2):
112
+            self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True)
113
+            return
114
+
115
+        self.fm.commands.alias(self.arg(1), self.rest(2))
116
+
117
+
118
+class echo(Command):
119
+    """:echo <text>
120
+
121
+    Display the text in the statusbar.
122
+    """
123
+
124
+    def execute(self):
125
+        self.fm.notify(self.rest(1))
126
+
127
+
128
+class cd(Command):
129
+    """:cd [-r] <path>
130
+
131
+    The cd command changes the directory.
132
+    If the path is a file, selects that file.
133
+    The command 'cd -' is equivalent to typing ``.
134
+    Using the option "-r" will get you to the real path.
135
+    """
136
+
137
+    def execute(self):
138
+        if self.arg(1) == '-r':
139
+            self.shift()
140
+            destination = os.path.realpath(self.rest(1))
141
+            if os.path.isfile(destination):
142
+                self.fm.select_file(destination)
143
+                return
144
+        else:
145
+            destination = self.rest(1)
146
+
147
+        if not destination:
148
+            destination = '~'
149
+
150
+        if destination == '-':
151
+            self.fm.enter_bookmark('`')
152
+        else:
153
+            self.fm.cd(destination)
154
+
155
+    def _tab_args(self):
156
+        # dest must be rest because path could contain spaces
157
+        if self.arg(1) == '-r':
158
+            start = self.start(2)
159
+            dest = self.rest(2)
160
+        else:
161
+            start = self.start(1)
162
+            dest = self.rest(1)
163
+
164
+        if dest:
165
+            head, tail = os.path.split(os.path.expanduser(dest))
166
+            if head:
167
+                dest_exp = os.path.join(os.path.normpath(head), tail)
168
+            else:
169
+                dest_exp = tail
170
+        else:
171
+            dest_exp = ''
172
+        return (start, dest_exp, os.path.join(self.fm.thisdir.path, dest_exp),
173
+                dest.endswith(os.path.sep))
174
+
175
+    @staticmethod
176
+    def _tab_paths(dest, dest_abs, ends_with_sep):
177
+        if not dest:
178
+            try:
179
+                return next(os.walk(dest_abs))[1], dest_abs
180
+            except (OSError, StopIteration):
181
+                return [], ''
182
+
183
+        if ends_with_sep:
184
+            try:
185
+                return [os.path.join(dest, path) for path in next(os.walk(dest_abs))[1]], ''
186
+            except (OSError, StopIteration):
187
+                return [], ''
188
+
189
+        return None, None
190
+
191
+    def _tab_match(self, path_user, path_file):
192
+        if self.fm.settings.cd_tab_case == 'insensitive':
193
+            path_user = path_user.lower()
194
+            path_file = path_file.lower()
195
+        elif self.fm.settings.cd_tab_case == 'smart' and path_user.islower():
196
+            path_file = path_file.lower()
197
+        return path_file.startswith(path_user)
198
+
199
+    def _tab_normal(self, dest, dest_abs):
200
+        dest_dir = os.path.dirname(dest)
201
+        dest_base = os.path.basename(dest)
202
+
203
+        try:
204
+            dirnames = next(os.walk(os.path.dirname(dest_abs)))[1]
205
+        except (OSError, StopIteration):
206
+            return [], ''
207
+
208
+        return [os.path.join(dest_dir, d) for d in dirnames if self._tab_match(dest_base, d)], ''
209
+
210
+    def _tab_fuzzy_match(self, basepath, tokens):
211
+        """ Find directories matching tokens recursively """
212
+        if not tokens:
213
+            tokens = ['']
214
+        paths = [basepath]
215
+        while True:
216
+            token = tokens.pop()
217
+            matches = []
218
+            for path in paths:
219
+                try:
220
+                    directories = next(os.walk(path))[1]
221
+                except (OSError, StopIteration):
222
+                    continue
223
+                matches += [os.path.join(path, d) for d in directories
224
+                            if self._tab_match(token, d)]
225
+            if not tokens or not matches:
226
+                return matches
227
+            paths = matches
228
+
229
+        return None
230
+
231
+    def _tab_fuzzy(self, dest, dest_abs):
232
+        tokens = []
233
+        basepath = dest_abs
234
+        while True:
235
+            basepath_old = basepath
236
+            basepath, token = os.path.split(basepath)
237
+            if basepath == basepath_old:
238
+                break
239
+            if os.path.isdir(basepath_old) and not token.startswith('.'):
240
+                basepath = basepath_old
241
+                break
242
+            tokens.append(token)
243
+
244
+        paths = self._tab_fuzzy_match(basepath, tokens)
245
+        if not os.path.isabs(dest):
246
+            paths_rel = self.fm.thisdir.path
247
+            paths = [os.path.relpath(os.path.join(basepath, path), paths_rel)
248
+                     for path in paths]
249
+        else:
250
+            paths_rel = ''
251
+        return paths, paths_rel
252
+
253
+    def tab(self, tabnum):
254
+        from os.path import sep
255
+
256
+        start, dest, dest_abs, ends_with_sep = self._tab_args()
257
+
258
+        paths, paths_rel = self._tab_paths(dest, dest_abs, ends_with_sep)
259
+        if paths is None:
260
+            if self.fm.settings.cd_tab_fuzzy:
261
+                paths, paths_rel = self._tab_fuzzy(dest, dest_abs)
262
+            else:
263
+                paths, paths_rel = self._tab_normal(dest, dest_abs)
264
+
265
+        paths.sort()
266
+
267
+        if self.fm.settings.cd_bookmarks:
268
+            paths[0:0] = [
269
+                os.path.relpath(v.path, paths_rel) if paths_rel else v.path
270
+                for v in self.fm.bookmarks.dct.values() for path in paths
271
+                if v.path.startswith(os.path.join(paths_rel, path) + sep)
272
+            ]
273
+
274
+        if not paths:
275
+            return None
276
+        if len(paths) == 1:
277
+            return start + paths[0] + sep
278
+        return [start + dirname + sep for dirname in paths]
279
+
280
+
281
+class chain(Command):
282
+    """:chain <command1>; <command2>; ...
283
+
284
+    Calls multiple commands at once, separated by semicolons.
285
+    """
286
+    resolve_macros = False
287
+
288
+    def execute(self):
289
+        if not self.rest(1).strip():
290
+            self.fm.notify('Syntax: chain <command1>; <command2>; ...', bad=True)
291
+            return
292
+        for command in [s.strip() for s in self.rest(1).split(";")]:
293
+            self.fm.execute_console(command)
294
+
295
+
296
+class shell(Command):
297
+    escape_macros_for_shell = True
298
+
299
+    def execute(self):
300
+        if self.arg(1) and self.arg(1)[0] == '-':
301
+            flags = self.arg(1)[1:]
302
+            command = self.rest(2)
303
+        else:
304
+            flags = ''
305
+            command = self.rest(1)
306
+
307
+        if command:
308
+            self.fm.execute_command(command, flags=flags)
309
+
310
+    def tab(self, tabnum):
311
+        from ranger.ext.get_executables import get_executables
312
+        if self.arg(1) and self.arg(1)[0] == '-':
313
+            command = self.rest(2)
314
+        else:
315
+            command = self.rest(1)
316
+        start = self.line[0:len(self.line) - len(command)]
317
+
318
+        try:
319
+            position_of_last_space = command.rindex(" ")
320
+        except ValueError:
321
+            return (start + program + ' ' for program
322
+                    in get_executables() if program.startswith(command))
323
+        if position_of_last_space == len(command) - 1:
324
+            selection = self.fm.thistab.get_selection()
325
+            if len(selection) == 1:
326
+                return self.line + selection[0].shell_escaped_basename + ' '
327
+            return self.line + '%s '
328
+
329
+        before_word, start_of_word = self.line.rsplit(' ', 1)
330
+        return (before_word + ' ' + file.shell_escaped_basename
331
+                for file in self.fm.thisdir.files or []
332
+                if file.shell_escaped_basename.startswith(start_of_word))
333
+
334
+
335
+class open_with(Command):
336
+
337
+    def execute(self):
338
+        app, flags, mode = self._get_app_flags_mode(self.rest(1))
339
+        self.fm.execute_file(
340
+            files=[f for f in self.fm.thistab.get_selection()],
341
+            app=app,
342
+            flags=flags,
343
+            mode=mode)
344
+
345
+    def tab(self, tabnum):
346
+        return self._tab_through_executables()
347
+
348
+    def _get_app_flags_mode(self, string):  # pylint: disable=too-many-branches,too-many-statements
349
+        """Extracts the application, flags and mode from a string.
350
+
351
+        examples:
352
+        "mplayer f 1" => ("mplayer", "f", 1)
353
+        "atool 4" => ("atool", "", 4)
354
+        "p" => ("", "p", 0)
355
+        "" => None
356
+        """
357
+
358
+        app = ''
359
+        flags = ''
360
+        mode = 0
361
+        split = string.split()
362
+
363
+        if len(split) == 1:
364
+            part = split[0]
365
+            if self._is_app(part):
366
+                app = part
367
+            elif self._is_flags(part):
368
+                flags = part
369
+            elif self._is_mode(part):
370
+                mode = part
371
+
372
+        elif len(split) == 2:
373
+            part0 = split[0]
374
+            part1 = split[1]
375
+
376
+            if self._is_app(part0):
377
+                app = part0
378
+                if self._is_flags(part1):
379
+                    flags = part1
380
+                elif self._is_mode(part1):
381
+                    mode = part1
382
+            elif self._is_flags(part0):
383
+                flags = part0
384
+                if self._is_mode(part1):
385
+                    mode = part1
386
+            elif self._is_mode(part0):
387
+                mode = part0
388
+                if self._is_flags(part1):
389
+                    flags = part1
390
+
391
+        elif len(split) >= 3:
392
+            part0 = split[0]
393
+            part1 = split[1]
394
+            part2 = split[2]
395
+
396
+            if self._is_app(part0):
397
+                app = part0
398
+                if self._is_flags(part1):
399
+                    flags = part1
400
+                    if self._is_mode(part2):
401
+                        mode = part2
402
+                elif self._is_mode(part1):
403
+                    mode = part1
404
+                    if self._is_flags(part2):
405
+                        flags = part2
406
+            elif self._is_flags(part0):
407
+                flags = part0
408
+                if self._is_mode(part1):
409
+                    mode = part1
410
+            elif self._is_mode(part0):
411
+                mode = part0
412
+                if self._is_flags(part1):
413
+                    flags = part1
414
+
415
+        return app, flags, int(mode)
416
+
417
+    def _is_app(self, arg):
418
+        return not self._is_flags(arg) and not arg.isdigit()
419
+
420
+    @staticmethod
421
+    def _is_flags(arg):
422
+        from ranger.core.runner import ALLOWED_FLAGS
423
+        return all(x in ALLOWED_FLAGS for x in arg)
424
+
425
+    @staticmethod
426
+    def _is_mode(arg):
427
+        return all(x in '0123456789' for x in arg)
428
+
429
+
430
+class set_(Command):
431
+    """:set <option name>=<python expression>
432
+
433
+    Gives an option a new value.
434
+
435
+    Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!`
436
+    """
437
+    name = 'set'  # don't override the builtin set class
438
+
439
+    def execute(self):
440
+        name = self.arg(1)
441
+        name, value, _, toggle = self.parse_setting_line_v2()
442
+        if toggle:
443
+            self.fm.toggle_option(name)
444
+        else:
445
+            self.fm.set_option_from_string(name, value)
446
+
447
+    def tab(self, tabnum):  # pylint: disable=too-many-return-statements
448
+        from ranger.gui.colorscheme import get_all_colorschemes
449
+        name, value, name_done = self.parse_setting_line()
450
+        settings = self.fm.settings
451
+        if not name:
452
+            return sorted(self.firstpart + setting for setting in settings)
453
+        if not value and not name_done:
454
+            return sorted(self.firstpart + setting for setting in settings
455
+                          if setting.startswith(name))
456
+        if not value:
457
+            value_completers = {
458
+                "colorscheme":
459
+                # Cycle through colorschemes when name, but no value is specified
460
+                lambda: sorted(self.firstpart + colorscheme for colorscheme
461
+                               in get_all_colorschemes(self.fm)),
462
+
463
+                "column_ratios":
464
+                lambda: self.firstpart + ",".join(map(str, settings[name])),
465
+            }
466
+
467
+            def default_value_completer():
468
+                return self.firstpart + str(settings[name])
469
+
470
+            return value_completers.get(name, default_value_completer)()
471
+        if bool in settings.types_of(name):
472
+            if 'true'.startswith(value.lower()):
473
+                return self.firstpart + 'True'
474
+            if 'false'.startswith(value.lower()):
475
+                return self.firstpart + 'False'
476
+        # Tab complete colorscheme values if incomplete value is present
477
+        if name == "colorscheme":
478
+            return sorted(self.firstpart + colorscheme for colorscheme
479
+                          in get_all_colorschemes(self.fm) if colorscheme.startswith(value))
480
+        return None
481
+
482
+
483
+class setlocal(set_):
484
+    """:setlocal path=<regular expression> <option name>=<python expression>
485
+
486
+    Gives an option a new value.
487
+    """
488
+    PATH_RE_DQUOTED = re.compile(r'^setlocal\s+path="(.*?)"')
489
+    PATH_RE_SQUOTED = re.compile(r"^setlocal\s+path='(.*?)'")
490
+    PATH_RE_UNQUOTED = re.compile(r'^path=(.*?)$')
491
+
492
+    def _re_shift(self, match):
493
+        if not match:
494
+            return None
495
+        path = os.path.expanduser(match.group(1))
496
+        for _ in range(len(path.split())):
497
+            self.shift()
498
+        return path
499
+
500
+    def execute(self):
501
+        path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
502
+        if path is None:
503
+            path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
504
+        if path is None:
505
+            path = self._re_shift(self.PATH_RE_UNQUOTED.match(self.arg(1)))
506
+        if path is None and self.fm.thisdir:
507
+            path = self.fm.thisdir.path
508
+        if not path:
509
+            return
510
+
511
+        name, value, _ = self.parse_setting_line()
512
+        self.fm.set_option_from_string(name, value, localpath=path)
513
+
514
+
515
+class setintag(set_):
516
+    """:setintag <tag or tags> <option name>=<option value>
517
+
518
+    Sets an option for directories that are tagged with a specific tag.
519
+    """
520
+
521
+    def execute(self):
522
+        tags = self.arg(1)
523
+        self.shift()
524
+        name, value, _ = self.parse_setting_line()
525
+        self.fm.set_option_from_string(name, value, tags=tags)
526
+
527
+
528
+class default_linemode(Command):
529
+
530
+    def execute(self):
531
+        from ranger.container.fsobject import FileSystemObject
532
+
533
+        if len(self.args) < 2:
534
+            self.fm.notify(
535
+                "Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
536
+
537
+        # Extract options like "path=..." or "tag=..." from the command line
538
+        arg1 = self.arg(1)
539
+        method = "always"
540
+        argument = None
541
+        if arg1.startswith("path="):
542
+            method = "path"
543
+            argument = re.compile(arg1[5:])
544
+            self.shift()
545
+        elif arg1.startswith("tag="):
546
+            method = "tag"
547
+            argument = arg1[4:]
548
+            self.shift()
549
+
550
+        # Extract and validate the line mode from the command line
551
+        lmode = self.rest(1)
552
+        if lmode not in FileSystemObject.linemode_dict:
553
+            self.fm.notify(
554
+                "Invalid linemode: %s; should be %s" % (
555
+                    lmode, "/".join(FileSystemObject.linemode_dict)),
556
+                bad=True,
557
+            )
558
+
559
+        # Add the prepared entry to the fm.default_linemodes
560
+        entry = [method, argument, lmode]
561
+        self.fm.default_linemodes.appendleft(entry)
562
+
563
+        # Redraw the columns
564
+        if self.fm.ui.browser:
565
+            for col in self.fm.ui.browser.columns:
566
+                col.need_redraw = True
567
+
568
+    def tab(self, tabnum):
569
+        return (self.arg(0) + " " + lmode
570
+                for lmode in self.fm.thisfile.linemode_dict.keys()
571
+                if lmode.startswith(self.arg(1)))
572
+
573
+
574
+class quit(Command):  # pylint: disable=redefined-builtin
575
+    """:quit
576
+
577
+    Closes the current tab, if there's more than one tab.
578
+    Otherwise quits if there are no tasks in progress.
579
+    """
580
+    def _exit_no_work(self):
581
+        if self.fm.loader.has_work():
582
+            self.fm.notify('Not quitting: Tasks in progress: Use `quit!` to force quit')
583
+        else:
584
+            self.fm.exit()
585
+
586
+    def execute(self):
587
+        if len(self.fm.tabs) >= 2:
588
+            self.fm.tab_close()
589
+        else:
590
+            self._exit_no_work()
591
+
592
+
593
+class quit_bang(Command):
594
+    """:quit!
595
+
596
+    Closes the current tab, if there's more than one tab.
597
+    Otherwise force quits immediately.
598
+    """
599
+    name = 'quit!'
600
+    allow_abbrev = False
601
+
602
+    def execute(self):
603
+        if len(self.fm.tabs) >= 2:
604
+            self.fm.tab_close()
605
+        else:
606
+            self.fm.exit()
607
+
608
+
609
+class quitall(Command):
610
+    """:quitall
611
+
612
+    Quits if there are no tasks in progress.
613
+    """
614
+    def _exit_no_work(self):
615
+        if self.fm.loader.has_work():
616
+            self.fm.notify('Not quitting: Tasks in progress: Use `quitall!` to force quit')
617
+        else:
618
+            self.fm.exit()
619
+
620
+    def execute(self):
621
+        self._exit_no_work()
622
+
623
+
624
+class quitall_bang(Command):
625
+    """:quitall!
626
+
627
+    Force quits immediately.
628
+    """
629
+    name = 'quitall!'
630
+    allow_abbrev = False
631
+
632
+    def execute(self):
633
+        self.fm.exit()
634
+
635
+
636
+class terminal(Command):
637
+    """:terminal
638
+
639
+    Spawns an "x-terminal-emulator" starting in the current directory.
640
+    """
641
+
642
+    def execute(self):
643
+        from ranger.ext.get_executables import get_term
644
+        self.fm.run(get_term(), flags='f')
645
+
646
+
647
+class delete(Command):
648
+    """:delete
649
+
650
+    Tries to delete the selection or the files passed in arguments (if any).
651
+    The arguments use a shell-like escaping.
652
+
653
+    "Selection" is defined as all the "marked files" (by default, you
654
+    can mark files with space or v). If there are no marked files,
655
+    use the "current file" (where the cursor is)
656
+
657
+    When attempting to delete non-empty directories or multiple
658
+    marked files, it will require a confirmation.
659
+    """
660
+
661
+    allow_abbrev = False
662
+    escape_macros_for_shell = True
663
+
664
+    def execute(self):
665
+        import shlex
666
+        from functools import partial
667
+
668
+        def is_directory_with_files(path):
669
+            return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
670
+
671
+        if self.rest(1):
672
+            files = shlex.split(self.rest(1))
673
+            many_files = (len(files) > 1 or is_directory_with_files(files[0]))
674
+        else:
675
+            cwd = self.fm.thisdir
676
+            tfile = self.fm.thisfile
677
+            if not cwd or not tfile:
678
+                self.fm.notify("Error: no file selected for deletion!", bad=True)
679
+                return
680
+
681
+            # relative_path used for a user-friendly output in the confirmation.
682
+            files = [f.relative_path for f in self.fm.thistab.get_selection()]
683
+            many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
684
+
685
+        confirm = self.fm.settings.confirm_on_delete
686
+        if confirm != 'never' and (confirm != 'multiple' or many_files):
687
+            self.fm.ui.console.ask(
688
+                "Confirm deletion of: %s (y/N)" % ', '.join(files),
689
+                partial(self._question_callback, files),
690
+                ('n', 'N', 'y', 'Y'),
691
+            )
692
+        else:
693
+            # no need for a confirmation, just delete
694
+            self.fm.delete(files)
695
+
696
+    def tab(self, tabnum):
697
+        return self._tab_directory_content()
698
+
699
+    def _question_callback(self, files, answer):
700
+        if answer == 'y' or answer == 'Y':
701
+            self.fm.delete(files)
702
+
703
+
704
+class trash(Command):
705
+    """:trash
706
+
707
+    Tries to move the selection or the files passed in arguments (if any) to
708
+    the trash, using rifle rules with label "trash".
709
+    The arguments use a shell-like escaping.
710
+
711
+    "Selection" is defined as all the "marked files" (by default, you
712
+    can mark files with space or v). If there are no marked files,
713
+    use the "current file" (where the cursor is)
714
+
715
+    When attempting to trash non-empty directories or multiple
716
+    marked files, it will require a confirmation.
717
+    """
718
+
719
+    allow_abbrev = False
720
+    escape_macros_for_shell = True
721
+
722
+    def execute(self):
723
+        import shlex
724
+        from functools import partial
725
+
726
+        def is_directory_with_files(path):
727
+            return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
728
+
729
+        if self.rest(1):
730
+            files = shlex.split(self.rest(1))
731
+            many_files = (len(files) > 1 or is_directory_with_files(files[0]))
732
+        else:
733
+            cwd = self.fm.thisdir
734
+            tfile = self.fm.thisfile
735
+            if not cwd or not tfile:
736
+                self.fm.notify("Error: no file selected for deletion!", bad=True)
737
+                return
738
+
739
+            # relative_path used for a user-friendly output in the confirmation.
740
+            files = [f.relative_path for f in self.fm.thistab.get_selection()]
741
+            many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
742
+
743
+        confirm = self.fm.settings.confirm_on_delete
744
+        if confirm != 'never' and (confirm != 'multiple' or many_files):
745
+            self.fm.ui.console.ask(
746
+                "Confirm deletion of: %s (y/N)" % ', '.join(files),
747
+                partial(self._question_callback, files),
748
+                ('n', 'N', 'y', 'Y'),
749
+            )
750
+        else:
751
+            # no need for a confirmation, just delete
752
+            self.fm.execute_file(files, label='trash')
753
+
754
+    def tab(self, tabnum):
755
+        return self._tab_directory_content()
756
+
757
+    def _question_callback(self, files, answer):
758
+        if answer == 'y' or answer == 'Y':
759
+            self.fm.execute_file(files, label='trash')
760
+
761
+
762
+class jump_non(Command):
763
+    """:jump_non [-FLAGS...]
764
+
765
+    Jumps to first non-directory if highlighted file is a directory and vice versa.
766
+
767
+    Flags:
768
+     -r    Jump in reverse order
769
+     -w    Wrap around if reaching end of filelist
770
+    """
771
+    def __init__(self, *args, **kwargs):
772
+        super(jump_non, self).__init__(*args, **kwargs)
773
+
774
+        flags, _ = self.parse_flags()
775
+        self._flag_reverse = 'r' in flags
776
+        self._flag_wrap = 'w' in flags
777
+
778
+    @staticmethod
779
+    def _non(fobj, is_directory):
780
+        return fobj.is_directory if not is_directory else not fobj.is_directory
781
+
782
+    def execute(self):
783
+        tfile = self.fm.thisfile
784
+        passed = False
785
+        found_before = None
786
+        found_after = None
787
+        for fobj in self.fm.thisdir.files[::-1] if self._flag_reverse else self.fm.thisdir.files:
788
+            if fobj.path == tfile.path:
789
+                passed = True
790
+                continue
791
+
792
+            if passed:
793
+                if self._non(fobj, tfile.is_directory):
794
+                    found_after = fobj.path
795
+                    break
796
+            elif not found_before and self._non(fobj, tfile.is_directory):
797
+                found_before = fobj.path
798
+
799
+        if found_after:
800
+            self.fm.select_file(found_after)
801
+        elif self._flag_wrap and found_before:
802
+            self.fm.select_file(found_before)
803
+
804
+
805
+class mark_tag(Command):
806
+    """:mark_tag [<tags>]
807
+
808
+    Mark all tags that are tagged with either of the given tags.
809
+    When leaving out the tag argument, all tagged files are marked.
810
+    """
811
+    do_mark = True
812
+
813
+    def execute(self):
814
+        cwd = self.fm.thisdir
815
+        tags = self.rest(1).replace(" ", "")
816
+        if not self.fm.tags or not cwd.files:
817
+            return
818
+        for fileobj in cwd.files:
819
+            try:
820
+                tag = self.fm.tags.tags[fileobj.realpath]
821
+            except KeyError:
822
+                continue
823
+            if not tags or tag in tags:
824
+                cwd.mark_item(fileobj, val=self.do_mark)
825
+        self.fm.ui.status.need_redraw = True
826
+        self.fm.ui.need_redraw = True
827
+
828
+
829
+class console(Command):
830
+    """:console <command>
831
+
832
+    Open the console with the given command.
833
+    """
834
+
835
+    def execute(self):
836
+        position = None
837
+        if self.arg(1)[0:2] == '-p':
838
+            try:
839
+                position = int(self.arg(1)[2:])
840
+            except ValueError:
841
+                pass
842
+            else:
843
+                self.shift()
844
+        self.fm.open_console(self.rest(1), position=position)
845
+
846
+
847
+class load_copy_buffer(Command):
848
+    """:load_copy_buffer
849
+
850
+    Load the copy buffer from datadir/copy_buffer
851
+    """
852
+    copy_buffer_filename = 'copy_buffer'
853
+
854
+    def execute(self):
855
+        import sys
856
+        from ranger.container.file import File
857
+        from os.path import exists
858
+        fname = self.fm.datapath(self.copy_buffer_filename)
859
+        unreadable = IOError if sys.version_info[0] < 3 else OSError
860
+        try:
861
+            fobj = open(fname, 'r')
862
+        except unreadable:
863
+            return self.fm.notify(
864
+                "Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
865
+
866
+        self.fm.copy_buffer = set(File(g)
867
+                                  for g in fobj.read().split("\n") if exists(g))
868
+        fobj.close()
869
+        self.fm.ui.redraw_main_column()
870
+        return None
871
+
872
+
873
+class save_copy_buffer(Command):
874
+    """:save_copy_buffer
875
+
876
+    Save the copy buffer to datadir/copy_buffer
877
+    """
878
+    copy_buffer_filename = 'copy_buffer'
879
+
880
+    def execute(self):
881
+        import sys
882
+        fname = None
883
+        fname = self.fm.datapath(self.copy_buffer_filename)
884
+        unwritable = IOError if sys.version_info[0] < 3 else OSError
885
+        try:
886
+            fobj = open(fname, 'w')
887
+        except unwritable:
888
+            return self.fm.notify("Cannot open %s" %
889
+                                  (fname or self.copy_buffer_filename), bad=True)
890
+        fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
891
+        fobj.close()
892
+        return None
893
+
894
+
895
+class unmark_tag(mark_tag):
896
+    """:unmark_tag [<tags>]
897
+
898
+    Unmark all tags that are tagged with either of the given tags.
899
+    When leaving out the tag argument, all tagged files are unmarked.
900
+    """
901
+    do_mark = False
902
+
903
+
904
+class mkdir(Command):
905
+    """:mkdir <dirname>
906
+
907
+    Creates a directory with the name <dirname>.
908
+    """
909
+
910
+    def execute(self):
911
+        from os.path import join, expanduser, lexists
912
+        from os import makedirs
913
+
914
+        dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
915
+        if not lexists(dirname):
916
+            makedirs(dirname)
917
+        else:
918
+            self.fm.notify("file/directory exists!", bad=True)
919
+
920
+    def tab(self, tabnum):
921
+        return self._tab_directory_content()
922
+
923
+
924
+class touch(Command):
925
+    """:touch <fname>
926
+
927
+    Creates a file with the name <fname>.
928
+    """
929
+
930
+    def execute(self):
931
+        from os.path import join, expanduser, lexists
932
+
933
+        fname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
934
+        if not lexists(fname):
935
+            open(fname, 'a').close()
936
+        else:
937
+            self.fm.notify("file/directory exists!", bad=True)
938
+
939
+    def tab(self, tabnum):
940
+        return self._tab_directory_content()
941
+
942
+
943
+class edit(Command):
944
+    """:edit <filename>
945
+
946
+    Opens the specified file in vim
947
+    """
948
+
949
+    def execute(self):
950
+        if not self.arg(1):
951
+            self.fm.edit_file(self.fm.thisfile.path)
952
+        else:
953
+            self.fm.edit_file(self.rest(1))
954
+
955
+    def tab(self, tabnum):
956
+        return self._tab_directory_content()
957
+
958
+
959
+class eval_(Command):
960
+    """:eval [-q] <python code>
961
+
962
+    Evaluates the python code.
963
+    `fm' is a reference to the FM instance.
964
+    To display text, use the function `p'.
965
+
966
+    Examples:
967
+    :eval fm
968
+    :eval len(fm.directories)
969
+    :eval p("Hello World!")
970
+    """
971
+    name = 'eval'
972
+    resolve_macros = False
973
+
974
+    def execute(self):
975
+        # The import is needed so eval() can access the ranger module
976
+        import ranger  # NOQA pylint: disable=unused-import,unused-variable
977
+        if self.arg(1) == '-q':
978
+            code = self.rest(2)
979
+            quiet = True
980
+        else:
981
+            code = self.rest(1)
982
+            quiet = False
983
+        global cmd, fm, p, quantifier  # pylint: disable=invalid-name,global-variable-undefined
984
+        fm = self.fm
985
+        cmd = self.fm.execute_console
986
+        p = fm.notify
987
+        quantifier = self.quantifier
988
+        try:
989
+            try:
990
+                result = eval(code)  # pylint: disable=eval-used
991
+            except SyntaxError:
992
+                exec(code)  # pylint: disable=exec-used
993
+            else:
994
+                if result and not quiet:
995
+                    p(result)
996
+        except Exception as err:  # pylint: disable=broad-except
997
+            fm.notify("The error `%s` was caused by evaluating the "
998
+                      "following code: `%s`" % (err, code), bad=True)
999
+
1000
+
1001
+class rename(Command):
1002
+    """:rename <newname>
1003
+
1004
+    Changes the name of the currently highlighted file to <newname>
1005
+    """
1006
+
1007
+    def execute(self):
1008
+        from ranger.container.file import File
1009
+        from os import access
1010
+
1011
+        new_name = self.rest(1)
1012
+
1013
+        if not new_name:
1014
+            return self.fm.notify('Syntax: rename <newname>', bad=True)
1015
+
1016
+        if new_name == self.fm.thisfile.relative_path:
1017
+            return None
1018
+
1019
+        if access(new_name, os.F_OK):
1020
+            return self.fm.notify("Can't rename: file already exists!", bad=True)
1021
+
1022
+        if self.fm.rename(self.fm.thisfile, new_name):
1023
+            file_new = File(new_name)
1024
+            self.fm.bookmarks.update_path(self.fm.thisfile.path, file_new)
1025
+            self.fm.tags.update_path(self.fm.thisfile.path, file_new.path)
1026
+            self.fm.thisdir.pointed_obj = file_new
1027
+            self.fm.thisfile = file_new
1028
+
1029
+        return None
1030
+
1031
+    def tab(self, tabnum):
1032
+        return self._tab_directory_content()
1033
+
1034
+
1035
+class rename_append(Command):
1036
+    """:rename_append [-FLAGS...]
1037
+
1038
+    Opens the console with ":rename <current file>" with the cursor positioned
1039
+    before the file extension.
1040
+
1041
+    Flags:
1042
+     -a    Position before all extensions
1043
+     -r    Remove everything before extensions
1044
+    """
1045
+    def __init__(self, *args, **kwargs):
1046
+        super(rename_append, self).__init__(*args, **kwargs)
1047
+
1048
+        flags, _ = self.parse_flags()
1049
+        self._flag_ext_all = 'a' in flags
1050
+        self._flag_remove = 'r' in flags
1051
+
1052
+    def execute(self):
1053
+        from ranger import MACRO_DELIMITER, MACRO_DELIMITER_ESC
1054
+
1055
+        tfile = self.fm.thisfile
1056
+        relpath = tfile.relative_path.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
1057
+        basename = tfile.basename.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
1058
+
1059
+        if basename.find('.') <= 0 or os.path.isdir(relpath):
1060
+            self.fm.open_console('rename ' + relpath)
1061
+            return
1062
+
1063
+        if self._flag_ext_all:
1064
+            pos_ext = re.search(r'[^.]+', basename).end(0)
1065
+        else:
1066
+            pos_ext = basename.rindex('.')
1067
+        pos = len(relpath) - len(basename) + pos_ext
1068
+
1069
+        if self._flag_remove:
1070
+            relpath = relpath[:-len(basename)] + basename[pos_ext:]
1071
+            pos -= pos_ext
1072
+
1073
+        self.fm.open_console('rename ' + relpath, position=(7 + pos))
1074
+
1075
+
1076
+class chmod(Command):
1077
+    """:chmod <octal number>
1078
+
1079
+    Sets the permissions of the selection to the octal number.
1080
+
1081
+    The octal number is between 0 and 777. The digits specify the
1082
+    permissions for the user, the group and others.
1083
+
1084
+    A 1 permits execution, a 2 permits writing, a 4 permits reading.
1085
+    Add those numbers to combine them. So a 7 permits everything.
1086
+    """
1087
+
1088
+    def execute(self):
1089
+        mode_str = self.rest(1)
1090
+        if not mode_str:
1091
+            if self.quantifier is None:
1092
+                self.fm.notify("Syntax: chmod <octal number> "
1093
+                               "or specify a quantifier", bad=True)
1094
+                return
1095
+            mode_str = str(self.quantifier)
1096
+
1097
+        try:
1098
+            mode = int(mode_str, 8)
1099
+            if mode < 0 or mode > 0o777:
1100
+                raise ValueError
1101
+        except ValueError:
1102
+            self.fm.notify("Need an octal number between 0 and 777!", bad=True)
1103
+            return
1104
+
1105
+        for fobj in self.fm.thistab.get_selection():
1106
+            try:
1107
+                os.chmod(fobj.path, mode)
1108
+            except OSError as ex:
1109
+                self.fm.notify(ex)
1110
+
1111
+        # reloading directory.  maybe its better to reload the selected
1112
+        # files only.
1113
+        self.fm.thisdir.content_outdated = True
1114
+
1115
+
1116
+class bulkrename(Command):
1117
+    """:bulkrename
1118
+
1119
+    This command opens a list of selected files in an external editor.
1120
+    After you edit and save the file, it will generate a shell script
1121
+    which does bulk renaming according to the changes you did in the file.
1122
+
1123
+    This shell script is opened in an editor for you to review.
1124
+    After you close it, it will be executed.
1125
+    """
1126
+
1127
+    def execute(self):
1128
+        # pylint: disable=too-many-locals,too-many-statements,too-many-branches
1129
+        import sys
1130
+        import tempfile
1131
+        from ranger.container.file import File
1132
+        from ranger.ext.shell_escape import shell_escape as esc
1133
+        py3 = sys.version_info[0] >= 3
1134
+
1135
+        # Create and edit the file list
1136
+        filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
1137
+        with tempfile.NamedTemporaryFile(delete=False) as listfile:
1138
+            listpath = listfile.name
1139
+            if py3:
1140
+                listfile.write("\n".join(filenames).encode(
1141
+                    encoding="utf-8", errors="surrogateescape"))
1142
+            else:
1143
+                listfile.write("\n".join(filenames))
1144
+        self.fm.execute_file([File(listpath)], app='editor')
1145
+        with (open(listpath, 'r', encoding="utf-8", errors="surrogateescape") if
1146
+              py3 else open(listpath, 'r')) as listfile:
1147
+            new_filenames = listfile.read().split("\n")
1148
+        os.unlink(listpath)
1149
+        if all(a == b for a, b in zip(filenames, new_filenames)):
1150
+            self.fm.notify("No renaming to be done!")
1151
+            return
1152
+
1153
+        # Generate script
1154
+        with tempfile.NamedTemporaryFile() as cmdfile:
1155
+            script_lines = []
1156
+            script_lines.append("# This file will be executed when you close"
1157
+                                " the editor.")
1158
+            script_lines.append("# Please double-check everything, clear the"
1159
+                                " file to abort.")
1160
+            new_dirs = []
1161
+            for old, new in zip(filenames, new_filenames):
1162
+                if old != new:
1163
+                    basepath, _ = os.path.split(new)
1164
+                    if (basepath and basepath not in new_dirs
1165
+                            and not os.path.isdir(basepath)):
1166
+                        script_lines.append("mkdir -vp -- {dir}".format(
1167
+                            dir=esc(basepath)))
1168
+                        new_dirs.append(basepath)
1169
+                    script_lines.append("mv -vi -- {old} {new}".format(
1170
+                        old=esc(old), new=esc(new)))
1171
+            # Make sure not to forget the ending newline
1172
+            script_content = "\n".join(script_lines) + "\n"
1173
+            if py3:
1174
+                cmdfile.write(script_content.encode(encoding="utf-8",
1175
+                                                    errors="surrogateescape"))
1176
+            else:
1177
+                cmdfile.write(script_content)
1178
+            cmdfile.flush()
1179
+
1180
+            # Open the script and let the user review it, then check if the
1181
+            # script was modified by the user
1182
+            self.fm.execute_file([File(cmdfile.name)], app='editor')
1183
+            cmdfile.seek(0)
1184
+            script_was_edited = (script_content != cmdfile.read())
1185
+
1186
+            # Do the renaming
1187
+            self.fm.run(['/bin/sh', cmdfile.name], flags='w')
1188
+
1189
+        # Retag the files, but only if the script wasn't changed during review,
1190
+        # because only then we know which are the source and destination files.
1191
+        if not script_was_edited:
1192
+            tags_changed = False
1193
+            for old, new in zip(filenames, new_filenames):
1194
+                if old != new:
1195
+                    oldpath = self.fm.thisdir.path + '/' + old
1196
+                    newpath = self.fm.thisdir.path + '/' + new
1197
+                    if oldpath in self.fm.tags:
1198
+                        old_tag = self.fm.tags.tags[oldpath]
1199
+                        self.fm.tags.remove(oldpath)
1200
+                        self.fm.tags.tags[newpath] = old_tag
1201
+                        tags_changed = True
1202
+            if tags_changed:
1203
+                self.fm.tags.dump()
1204
+        else:
1205
+            fm.notify("files have not been retagged")
1206
+
1207
+
1208
+class relink(Command):
1209
+    """:relink <newpath>
1210
+
1211
+    Changes the linked path of the currently highlighted symlink to <newpath>
1212
+    """
1213
+
1214
+    def execute(self):
1215
+        new_path = self.rest(1)
1216
+        tfile = self.fm.thisfile
1217
+
1218
+        if not new_path:
1219
+            return self.fm.notify('Syntax: relink <newpath>', bad=True)
1220
+
1221
+        if not tfile.is_link:
1222
+            return self.fm.notify('%s is not a symlink!' % tfile.relative_path, bad=True)
1223
+
1224
+        if new_path == os.readlink(tfile.path):
1225
+            return None
1226
+
1227
+        try:
1228
+            os.remove(tfile.path)
1229
+            os.symlink(new_path, tfile.path)
1230
+        except OSError as err:
1231
+            self.fm.notify(err)
1232
+
1233
+        self.fm.reset()
1234
+        self.fm.thisdir.pointed_obj = tfile
1235
+        self.fm.thisfile = tfile
1236
+
1237
+        return None
1238
+
1239
+    def tab(self, tabnum):
1240
+        if not self.rest(1):
1241
+            return self.line + os.readlink(self.fm.thisfile.path)
1242
+        return self._tab_directory_content()
1243
+
1244
+
1245
+class help_(Command):
1246
+    """:help
1247
+
1248
+    Display ranger's manual page.
1249
+    """
1250
+    name = 'help'
1251
+
1252
+    def execute(self):
1253
+        def callback(answer):
1254
+            if answer == "q":
1255
+                return
1256
+            elif answer == "m":
1257
+                self.fm.display_help()
1258
+            elif answer == "c":
1259
+                self.fm.dump_commands()
1260
+            elif answer == "k":
1261
+                self.fm.dump_keybindings()
1262
+            elif answer == "s":
1263
+                self.fm.dump_settings()
1264
+
1265
+        self.fm.ui.console.ask(
1266
+            "View [m]an page, [k]ey bindings, [c]ommands or [s]ettings? (press q to abort)",
1267
+            callback,
1268
+            list("mqkcs")
1269
+        )
1270
+
1271
+
1272
+class copymap(Command):
1273
+    """:copymap <keys> <newkeys1> [<newkeys2>...]
1274
+
1275
+    Copies a "browser" keybinding from <keys> to <newkeys>
1276
+    """
1277
+    context = 'browser'
1278
+
1279
+    def execute(self):
1280
+        if not self.arg(1) or not self.arg(2):
1281
+            return self.fm.notify("Not enough arguments", bad=True)
1282
+
1283
+        for arg in self.args[2:]:
1284
+            self.fm.ui.keymaps.copy(self.context, self.arg(1), arg)
1285
+
1286
+        return None
1287
+
1288
+
1289
+class copypmap(copymap):
1290
+    """:copypmap <keys> <newkeys1> [<newkeys2>...]
1291
+
1292
+    Copies a "pager" keybinding from <keys> to <newkeys>
1293
+    """
1294
+    context = 'pager'
1295
+
1296
+
1297
+class copycmap(copymap):
1298
+    """:copycmap <keys> <newkeys1> [<newkeys2>...]
1299
+
1300
+    Copies a "console" keybinding from <keys> to <newkeys>
1301
+    """
1302
+    context = 'console'
1303
+
1304
+
1305
+class copytmap(copymap):
1306
+    """:copytmap <keys> <newkeys1> [<newkeys2>...]
1307
+
1308
+    Copies a "taskview" keybinding from <keys> to <newkeys>
1309
+    """
1310
+    context = 'taskview'
1311
+
1312
+
1313
+class unmap(Command):
1314
+    """:unmap <keys> [<keys2>, ...]
1315
+
1316
+    Remove the given "browser" mappings
1317
+    """
1318
+    context = 'browser'
1319
+
1320
+    def execute(self):
1321
+        for arg in self.args[1:]:
1322
+            self.fm.ui.keymaps.unbind(self.context, arg)
1323
+
1324
+
1325
+class uncmap(unmap):
1326
+    """:uncmap <keys> [<keys2>, ...]
1327
+
1328
+    Remove the given "console" mappings
1329
+    """
1330
+    context = 'console'
1331
+
1332
+
1333
+class cunmap(uncmap):
1334
+    """:cunmap <keys> [<keys2>, ...]
1335
+
1336
+    Remove the given "console" mappings
1337
+
1338
+    DEPRECATED in favor of uncmap.
1339
+    """
1340
+
1341
+    def execute(self):
1342
+        self.fm.notify("cunmap is deprecated in favor of uncmap!")
1343
+        super(cunmap, self).execute()
1344
+
1345
+
1346
+class unpmap(unmap):
1347
+    """:unpmap <keys> [<keys2>, ...]
1348
+
1349
+    Remove the given "pager" mappings
1350
+    """
1351
+    context = 'pager'
1352
+
1353
+
1354
+class punmap(unpmap):
1355
+    """:punmap <keys> [<keys2>, ...]
1356
+
1357
+    Remove the given "pager" mappings
1358
+
1359
+    DEPRECATED in favor of unpmap.
1360
+    """
1361
+
1362
+    def execute(self):
1363
+        self.fm.notify("punmap is deprecated in favor of unpmap!")
1364
+        super(punmap, self).execute()
1365
+
1366
+
1367
+class untmap(unmap):
1368
+    """:untmap <keys> [<keys2>, ...]
1369
+
1370
+    Remove the given "taskview" mappings
1371
+    """
1372
+    context = 'taskview'
1373
+
1374
+
1375
+class tunmap(untmap):
1376
+    """:tunmap <keys> [<keys2>, ...]
1377
+
1378
+    Remove the given "taskview" mappings
1379
+
1380
+    DEPRECATED in favor of untmap.
1381
+    """
1382
+
1383
+    def execute(self):
1384
+        self.fm.notify("tunmap is deprecated in favor of untmap!")
1385
+        super(tunmap, self).execute()
1386
+
1387
+
1388
+class map_(Command):
1389
+    """:map <keysequence> <command>
1390
+
1391
+    Maps a command to a keysequence in the "browser" context.
1392
+
1393
+    Example:
1394
+    map j move down
1395
+    map J move down 10
1396
+    """
1397
+    name = 'map'
1398
+    context = 'browser'
1399
+    resolve_macros = False
1400
+
1401
+    def execute(self):
1402
+        if not self.arg(1) or not self.arg(2):
1403
+            self.fm.notify("Syntax: {0} <keysequence> <command>".format(self.get_name()), bad=True)
1404
+            return
1405
+
1406
+        self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2))
1407
+
1408
+
1409
+class cmap(map_):
1410
+    """:cmap <keysequence> <command>
1411
+
1412
+    Maps a command to a keysequence in the "console" context.
1413
+
1414
+    Example:
1415
+    cmap <ESC> console_close
1416
+    cmap <C-x> console_type test
1417
+    """
1418
+    context = 'console'
1419
+
1420
+
1421
+class tmap(map_):
1422
+    """:tmap <keysequence> <command>
1423
+
1424
+    Maps a command to a keysequence in the "taskview" context.
1425
+    """
1426
+    context = 'taskview'
1427
+
1428
+
1429
+class pmap(map_):
1430
+    """:pmap <keysequence> <command>
1431
+
1432
+    Maps a command to a keysequence in the "pager" context.
1433
+    """
1434
+    context = 'pager'
1435
+
1436
+
1437
+class scout(Command):
1438
+    """:scout [-FLAGS...] <pattern>
1439
+
1440
+    Swiss army knife command for searching, traveling and filtering files.
1441
+
1442
+    Flags:
1443
+     -a    Automatically open a file on unambiguous match
1444
+     -e    Open the selected file when pressing enter
1445
+     -f    Filter files that match the current search pattern
1446
+     -g    Interpret pattern as a glob pattern
1447
+     -i    Ignore the letter case of the files
1448
+     -k    Keep the console open when changing a directory with the command
1449
+     -l    Letter skipping; e.g. allow "rdme" to match the file "readme"
1450
+     -m    Mark the matching files after pressing enter
1451
+     -M    Unmark the matching files after pressing enter
1452
+     -p    Permanent filter: hide non-matching files after pressing enter
1453
+     -r    Interpret pattern as a regular expression pattern
1454
+     -s    Smart case; like -i unless pattern contains upper case letters
1455
+     -t    Apply filter and search pattern as you type
1456
+     -v    Inverts the match
1457
+
1458
+    Multiple flags can be combined.  For example, ":scout -gpt" would create
1459
+    a :filter-like command using globbing.
1460
+    """
1461
+    # pylint: disable=bad-whitespace
1462
+    AUTO_OPEN     = 'a'
1463
+    OPEN_ON_ENTER = 'e'
1464
+    FILTER        = 'f'
1465
+    SM_GLOB       = 'g'
1466
+    IGNORE_CASE   = 'i'
1467
+    KEEP_OPEN     = 'k'
1468
+    SM_LETTERSKIP = 'l'
1469
+    MARK          = 'm'
1470
+    UNMARK        = 'M'
1471
+    PERM_FILTER   = 'p'
1472
+    SM_REGEX      = 'r'
1473
+    SMART_CASE    = 's'
1474
+    AS_YOU_TYPE   = 't'
1475
+    INVERT        = 'v'
1476
+    # pylint: enable=bad-whitespace
1477
+
1478
+    def __init__(self, *args, **kwargs):
1479
+        super(scout, self).__init__(*args, **kwargs)
1480
+        self._regex = None
1481
+        self.flags, self.pattern = self.parse_flags()
1482
+
1483
+    def execute(self):  # pylint: disable=too-many-branches
1484
+        thisdir = self.fm.thisdir
1485
+        flags = self.flags
1486
+        pattern = self.pattern
1487
+        regex = self._build_regex()
1488
+        count = self._count(move=True)
1489
+
1490
+        self.fm.thistab.last_search = regex
1491
+        self.fm.set_search_method(order="search")
1492
+
1493
+        if (self.MARK in flags or self.UNMARK in flags) and thisdir.files:
1494
+            value = flags.find(self.MARK) > flags.find(self.UNMARK)
1495
+            if self.FILTER in flags:
1496
+                for fobj in thisdir.files:
1497
+                    thisdir.mark_item(fobj, value)
1498
+            else:
1499
+                for fobj in thisdir.files:
1500
+                    if regex.search(fobj.relative_path):
1501
+                        thisdir.mark_item(fobj, value)
1502
+
1503
+        if self.PERM_FILTER in flags:
1504
+            thisdir.filter = regex if pattern else None
1505
+
1506
+        # clean up:
1507
+        self.cancel()
1508
+
1509
+        if self.OPEN_ON_ENTER in flags or \
1510
+                (self.AUTO_OPEN in flags and count == 1):
1511
+            if pattern == '..':
1512
+                self.fm.cd(pattern)
1513
+            else:
1514
+                self.fm.move(right=1)
1515
+                if self.quickly_executed:
1516
+                    self.fm.block_input(0.5)
1517
+
1518
+        if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir:
1519
+            # reopen the console:
1520
+            if not pattern:
1521
+                self.fm.open_console(self.line)
1522
+            else:
1523
+                self.fm.open_console(self.line[0:-len(pattern)])
1524
+
1525
+        if self.quickly_executed and thisdir != self.fm.thisdir and pattern != "..":
1526
+            self.fm.block_input(0.5)
1527
+
1528
+    def cancel(self):
1529
+        self.fm.thisdir.temporary_filter = None
1530
+        self.fm.thisdir.refilter()
1531
+
1532
+    def quick(self):
1533
+        asyoutype = self.AS_YOU_TYPE in self.flags
1534
+        if self.FILTER in self.flags:
1535
+            self.fm.thisdir.temporary_filter = self._build_regex()
1536
+        if self.PERM_FILTER in self.flags and asyoutype:
1537
+            self.fm.thisdir.filter = self._build_regex()
1538
+        if self.FILTER in self.flags or self.PERM_FILTER in self.flags:
1539
+            self.fm.thisdir.refilter()
1540
+        if self._count(move=asyoutype) == 1 and self.AUTO_OPEN in self.flags:
1541
+            return True
1542
+        return False
1543
+
1544
+    def tab(self, tabnum):
1545
+        self._count(move=True, offset=tabnum)
1546
+
1547
+    def _build_regex(self):
1548
+        if self._regex is not None:
1549
+            return self._regex
1550
+
1551
+        frmat = "%s"
1552
+        flags = self.flags
1553
+        pattern = self.pattern
1554
+
1555
+        if pattern == ".":
1556
+            return re.compile("")
1557
+
1558
+        # Handle carets at start and dollar signs at end separately
1559
+        if pattern.startswith('^'):
1560
+            pattern = pattern[1:]
1561
+            frmat = "^" + frmat
1562
+        if pattern.endswith('$'):
1563
+            pattern = pattern[:-1]
1564
+            frmat += "$"
1565
+
1566
+        # Apply one of the search methods
1567
+        if self.SM_REGEX in flags:
1568
+            regex = pattern
1569
+        elif self.SM_GLOB in flags:
1570
+            regex = re.escape(pattern).replace("\\*", ".*").replace("\\?", ".")
1571
+        elif self.SM_LETTERSKIP in flags:
1572
+            regex = ".*".join(re.escape(c) for c in pattern)
1573
+        else:
1574
+            regex = re.escape(pattern)
1575
+
1576
+        regex = frmat % regex
1577
+
1578
+        # Invert regular expression if necessary
1579
+        if self.INVERT in flags:
1580
+            regex = "^(?:(?!%s).)*$" % regex
1581
+
1582
+        # Compile Regular Expression
1583
+        # pylint: disable=no-member
1584
+        options = re.UNICODE
1585
+        if self.IGNORE_CASE in flags or self.SMART_CASE in flags and \
1586
+                pattern.islower():
1587
+            options |= re.IGNORECASE
1588
+        # pylint: enable=no-member
1589
+        try:
1590
+            self._regex = re.compile(regex, options)
1591
+        except re.error:
1592
+            self._regex = re.compile("")
1593
+        return self._regex
1594
+
1595
+    def _count(self, move=False, offset=0):
1596
+        count = 0
1597
+        cwd = self.fm.thisdir
1598
+        pattern = self.pattern
1599
+
1600
+        if not pattern or not cwd.files:
1601
+            return 0
1602
+        if pattern == '.':
1603
+            return 0
1604
+        if pattern == '..':
1605
+            return 1
1606
+
1607
+        deq = deque(cwd.files)
1608
+        deq.rotate(-cwd.pointer - offset)
1609
+        i = offset
1610
+        regex = self._build_regex()
1611
+        for fsobj in deq:
1612
+            if regex.search(fsobj.relative_path):
1613
+                count += 1
1614
+                if move and count == 1:
1615
+                    cwd.move(to=(cwd.pointer + i) % len(cwd.files))
1616
+                    self.fm.thisfile = cwd.pointed_obj
1617
+            if count > 1:
1618
+                return count
1619
+            i += 1
1620
+
1621
+        return count == 1
1622
+
1623
+
1624
+class narrow(Command):
1625
+    """
1626
+    :narrow
1627
+
1628
+    Show only the files selected right now. If no files are selected,
1629
+    disable narrowing.
1630
+    """
1631
+    def execute(self):
1632
+        if self.fm.thisdir.marked_items:
1633
+            selection = [f.basename for f in self.fm.thistab.get_selection()]
1634
+            self.fm.thisdir.narrow_filter = selection
1635
+        else:
1636
+            self.fm.thisdir.narrow_filter = None
1637
+        self.fm.thisdir.refilter()
1638
+
1639
+
1640
+class filter_inode_type(Command):
1641
+    """
1642
+    :filter_inode_type [dfl]
1643
+
1644
+    Displays only the files of specified inode type. Parameters
1645
+    can be combined.
1646
+
1647
+        d display directories
1648
+        f display files
1649
+        l display links
1650
+    """
1651
+
1652
+    def execute(self):
1653
+        if not self.arg(1):
1654
+            self.fm.thisdir.inode_type_filter = ""
1655
+        else:
1656
+            self.fm.thisdir.inode_type_filter = self.arg(1)
1657
+        self.fm.thisdir.refilter()
1658
+
1659
+
1660
+class filter_stack(Command):
1661
+    """
1662
+    :filter_stack ...
1663
+
1664
+    Manages the filter stack.
1665
+
1666
+        filter_stack add FILTER_TYPE ARGS...
1667
+        filter_stack pop
1668
+        filter_stack decompose
1669
+        filter_stack rotate [N=1]
1670
+        filter_stack clear
1671
+        filter_stack show
1672
+    """
1673
+    def execute(self):
1674
+        from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS
1675
+
1676
+        subcommand = self.arg(1)
1677
+
1678
+        if subcommand == "add":
1679
+            try:
1680
+                self.fm.thisdir.filter_stack.append(
1681
+                    SIMPLE_FILTERS[self.arg(2)](self.rest(3))
1682
+                )
1683
+            except KeyError:
1684
+                FILTER_COMBINATORS[self.arg(2)](self.fm.thisdir.filter_stack)
1685
+        elif subcommand == "pop":
1686
+            self.fm.thisdir.filter_stack.pop()
1687
+        elif subcommand == "decompose":
1688
+            inner_filters = self.fm.thisdir.filter_stack.pop().decompose()
1689
+            if inner_filters:
1690
+                self.fm.thisdir.filter_stack.extend(inner_filters)
1691
+        elif subcommand == "clear":
1692
+            self.fm.thisdir.filter_stack = []
1693
+        elif subcommand == "rotate":
1694
+            rotate_by = int(self.arg(2) or self.quantifier or 1)
1695
+            self.fm.thisdir.filter_stack = (
1696
+                self.fm.thisdir.filter_stack[-rotate_by:]
1697
+                + self.fm.thisdir.filter_stack[:-rotate_by]
1698
+            )
1699
+        elif subcommand == "show":
1700
+            stack = list(map(str, self.fm.thisdir.filter_stack))
1701
+            pager = self.fm.ui.open_pager()
1702
+            pager.set_source(["Filter stack: "] + stack)
1703
+            pager.move(to=100, percentage=True)
1704
+            return
1705
+        else:
1706
+            self.fm.notify(
1707
+                "Unknown subcommand: {}".format(subcommand),
1708
+                bad=True
1709
+            )
1710
+            return
1711
+
1712
+        self.fm.thisdir.refilter()
1713
+
1714
+
1715
+class grep(Command):
1716
+    """:grep <string>
1717
+
1718
+    Looks for a string in all marked files or directories
1719
+    """
1720
+
1721
+    def execute(self):
1722
+        if self.rest(1):
1723
+            action = ['grep', '--line-number']
1724
+            action.extend(['-e', self.rest(1), '-r'])
1725
+            action.extend(f.path for f in self.fm.thistab.get_selection())
1726
+            self.fm.execute_command(action, flags='p')
1727
+
1728
+
1729
+class flat(Command):
1730
+    """
1731
+    :flat <level>
1732
+
1733
+    Flattens the directory view up to the specified level.
1734
+
1735
+        -1 fully flattened
1736
+         0 remove flattened view
1737
+    """
1738
+
1739
+    def execute(self):
1740
+        try:
1741
+            level_str = self.rest(1)
1742
+            level = int(level_str)
1743
+        except ValueError:
1744
+            level = self.quantifier
1745
+        if level is None:
1746
+            self.fm.notify("Syntax: flat <level>", bad=True)
1747
+            return
1748
+        if level < -1:
1749
+            self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
1750
+        self.fm.thisdir.unload()
1751
+        self.fm.thisdir.flat = level
1752
+        self.fm.thisdir.load_content()
1753
+
1754
+
1755
+class reset_previews(Command):
1756
+    """:reset_previews
1757
+
1758
+    Reset the file previews.
1759
+    """
1760
+    def execute(self):
1761
+        self.fm.previews = {}
1762
+        self.fm.ui.need_redraw = True
1763
+
1764
+
1765
+# Version control commands
1766
+# --------------------------------
1767
+
1768
+
1769
+class stage(Command):
1770
+    """
1771
+    :stage
1772
+
1773
+    Stage selected files for the corresponding version control system
1774
+    """
1775
+
1776
+    def execute(self):
1777
+        from ranger.ext.vcs import VcsError
1778
+
1779
+        if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1780
+            filelist = [f.path for f in self.fm.thistab.get_selection()]
1781
+            try:
1782
+                self.fm.thisdir.vcs.action_add(filelist)
1783
+            except VcsError as ex:
1784
+                self.fm.notify('Unable to stage files: {0}'.format(ex))
1785
+            self.fm.ui.vcsthread.process(self.fm.thisdir)
1786
+        else:
1787
+            self.fm.notify('Unable to stage files: Not in repository')
1788
+
1789
+
1790
+class unstage(Command):
1791
+    """
1792
+    :unstage
1793
+
1794
+    Unstage selected files for the corresponding version control system
1795
+    """
1796
+
1797
+    def execute(self):
1798
+        from ranger.ext.vcs import VcsError
1799
+
1800
+        if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1801
+            filelist = [f.path for f in self.fm.thistab.get_selection()]
1802
+            try:
1803
+                self.fm.thisdir.vcs.action_reset(filelist)
1804
+            except VcsError as ex:
1805
+                self.fm.notify('Unable to unstage files: {0}'.format(ex))
1806
+            self.fm.ui.vcsthread.process(self.fm.thisdir)
1807
+        else:
1808
+            self.fm.notify('Unable to unstage files: Not in repository')
1809
+
1810
+# Metadata commands
1811
+# --------------------------------
1812
+
1813
+
1814
+class prompt_metadata(Command):
1815
+    """
1816
+    :prompt_metadata <key1> [<key2> [<key3> ...]]
1817
+
1818
+    Prompt the user to input metadata for multiple keys in a row.
1819
+    """
1820
+
1821
+    _command_name = "meta"
1822
+    _console_chain = None
1823
+
1824
+    def execute(self):
1825
+        prompt_metadata._console_chain = self.args[1:]
1826
+        self._process_command_stack()
1827
+
1828
+    def _process_command_stack(self):
1829
+        if prompt_metadata._console_chain:
1830
+            key = prompt_metadata._console_chain.pop()
1831
+            self._fill_console(key)
1832
+        else:
1833
+            for col in self.fm.ui.browser.columns:
1834
+                col.need_redraw = True
1835
+
1836
+    def _fill_console(self, key):
1837
+        metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
1838
+        if key in metadata and metadata[key]:
1839
+            existing_value = metadata[key]
1840
+        else:
1841
+            existing_value = ""
1842
+        text = "%s %s %s" % (self._command_name, key, existing_value)
1843
+        self.fm.open_console(text, position=len(text))
1844
+
1845
+
1846
+class meta(prompt_metadata):
1847
+    """
1848
+    :meta <key> [<value>]
1849
+
1850
+    Change metadata of a file.  Deletes the key if value is empty.
1851
+    """
1852
+
1853
+    def execute(self):
1854
+        key = self.arg(1)
1855
+        update_dict = dict()
1856
+        update_dict[key] = self.rest(2)
1857
+        selection = self.fm.thistab.get_selection()
1858
+        for fobj in selection:
1859
+            self.fm.metadata.set_metadata(fobj.path, update_dict)
1860
+        self._process_command_stack()
1861
+
1862
+    def tab(self, tabnum):
1863
+        key = self.arg(1)
1864
+        metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
1865
+        if key in metadata and metadata[key]:
1866
+            return [" ".join([self.arg(0), self.arg(1), metadata[key]])]
1867
+        return [self.arg(0) + " " + k for k in sorted(metadata)
1868
+                if k.startswith(self.arg(1))]
1869
+
1870
+
1871
+class linemode(default_linemode):
1872
+    """
1873
+    :linemode <mode>
1874
+
1875
+    Change what is displayed as a filename.
1876
+
1877
+    - "mode" may be any of the defined linemodes (see: ranger.core.linemode).
1878
+      "normal" is mapped to "filename".
1879
+    """
1880
+
1881
+    def execute(self):
1882
+        mode = self.arg(1)
1883
+
1884
+        if mode == "normal":
1885
+            from ranger.core.linemode import DEFAULT_LINEMODE
1886
+            mode = DEFAULT_LINEMODE
1887
+
1888
+        if mode not in self.fm.thisfile.linemode_dict:
1889
+            self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
1890
+            return
1891
+
1892
+        self.fm.thisdir.set_linemode_of_children(mode)
1893
+
1894
+        # Ask the browsercolumns to redraw
1895
+        for col in self.fm.ui.browser.columns:
1896
+            col.need_redraw = True
1897
+
1898
+
1899
+class yank(Command):
1900
+    """:yank [name|dir|path]
1901
+
1902
+    Copies the file's name (default), directory or path into both the primary X
1903
+    selection and the clipboard.
1904
+    """
1905
+
1906
+    modes = {
1907
+        '': 'basename',
1908
+        'name_without_extension': 'basename_without_extension',
1909
+        'name': 'basename',
1910
+        'dir': 'dirname',
1911
+        'path': 'path',
1912
+    }
1913
+
1914
+    def execute(self):
1915
+        import subprocess
1916
+
1917
+        def clipboards():
1918
+            from ranger.ext.get_executables import get_executables
1919
+            clipboard_managers = {
1920
+                'xclip': [
1921
+                    ['xclip'],
1922
+                    ['xclip', '-selection', 'clipboard'],
1923
+                ],
1924
+                'xsel': [
1925
+                    ['xsel'],
1926
+                    ['xsel', '-b'],
1927
+                ],
1928
+                'wl-copy': [
1929
+                    ['wl-copy'],
1930
+                ],
1931
+                'pbcopy': [
1932
+                    ['pbcopy'],
1933
+                ],
1934
+            }
1935
+            ordered_managers = ['pbcopy', 'wl-copy', 'xclip', 'xsel']
1936
+            executables = get_executables()
1937
+            for manager in ordered_managers:
1938
+                if manager in executables:
1939
+                    return clipboard_managers[manager]
1940
+            return []
1941
+
1942
+        clipboard_commands = clipboards()
1943
+
1944
+        mode = self.modes[self.arg(1)]
1945
+        selection = self.get_selection_attr(mode)
1946
+
1947
+        new_clipboard_contents = "\n".join(selection)
1948
+        for command in clipboard_commands:
1949
+            process = subprocess.Popen(command, universal_newlines=True,
1950
+                                       stdin=subprocess.PIPE)
1951
+            process.communicate(input=new_clipboard_contents)
1952
+
1953
+    def get_selection_attr(self, attr):
1954
+        return [getattr(item, attr) for item in
1955
+                self.fm.thistab.get_selection()]
1956
+
1957
+    def tab(self, tabnum):
1958
+        return (
1959
+            self.start(1) + mode for mode
1960
+            in sorted(self.modes.keys())
1961
+            if mode
1962
+        )
1963
+
1964
+
1965
+class paste_ext(Command):
1966
+    """
1967
+    :paste_ext
1968
+
1969
+    Like paste but tries to rename conflicting files so that the
1970
+    file extension stays intact (e.g. file_.ext).
1971
+    """
1972
+
1973
+    @staticmethod
1974
+    def make_safe_path(dst):
1975
+        if not os.path.exists(dst):
1976
+            return dst
1977
+
1978
+        dst_name, dst_ext = os.path.splitext(dst)
1979
+
1980
+        if not dst_name.endswith("_"):
1981
+            dst_name += "_"
1982
+            if not os.path.exists(dst_name + dst_ext):
1983
+                return dst_name + dst_ext
1984
+        n = 0
1985
+        test_dst = dst_name + str(n)
1986
+        while os.path.exists(test_dst + dst_ext):
1987
+            n += 1
1988
+            test_dst = dst_name + str(n)
1989
+
1990
+        return test_dst + dst_ext
1991
+
1992
+    def execute(self):
1993
+        return self.fm.paste(make_safe_path=paste_ext.make_safe_path)
ranger/rc.confadded
@@ -0,0 +1,361 @@
1
+# Clean, fast ranger configuration
2
+# Focused on speed and stability
3
+# ===== Options =====
4
+set viewmode miller
5
+set column_ratios 1,3,4
6
+set hidden_filter ^\.|\.(?:pyc|pyo|bak|swp)$|^lost\+found$|^(py)?cache$
7
+set show_hidden false
8
+set confirm_on_delete multiple
9
+set use_preview_script true
10
+set automatically_count_files true
11
+set open_all_images true
12
+set vcs_aware false
13
+set vcs_backend_git disabled
14
+set preview_images true
15
+set preview_images_method w3m
16
+set unicode_ellipsis false
17
+set show_hidden_bookmarks false
18
+set colorscheme default
19
+set preview_files true
20
+set preview_directories true
21
+set collapse_preview true
22
+set save_console_history true
23
+set status_bar_on_top false
24
+set draw_progress_bar_in_status_bar true
25
+set draw_borders both
26
+set mouse_enabled true
27
+set display_size_in_main_column true
28
+set display_size_in_status_bar true
29
+set display_free_space_in_status_bar true
30
+set display_tags_in_all_columns true
31
+set update_title false
32
+set update_tmux_title false
33
+set shorten_title 3
34
+set hostname_in_titlebar false
35
+set tilde_in_titlebar true
36
+set max_history_size 20
37
+set max_console_history_size 50
38
+set scroll_offset 8
39
+set flushinput true
40
+set padding_right true
41
+set autosave_bookmarks false
42
+set autoupdate_cumulative_size false
43
+set show_cursor false
44
+set sort natural
45
+set sort_reverse false
46
+set sort_case_insensitive true
47
+set sort_directories_first true
48
+set sort_unicode false
49
+set xterm_alt_key false
50
+set cd_bookmarks true
51
+set cd_tab_case sensitive
52
+set cd_tab_fuzzy false
53
+set preview_max_size 0
54
+set show_selection_in_titlebar true
55
+set idle_delay 2000
56
+set metadata_deep_search false
57
+set clear_filters_on_dir_change false
58
+set line_numbers false
59
+set one_indexed false
60
+set save_tabs_on_exit false
61
+set wrap_scroll false
62
+set global_inode_type_filter
63
+set freeze_files false
64
+# ===== Local Options =====
65
+setlocal path=~/Downloads sort mtime
66
+# ===== Command Aliases =====
67
+alias e     edit
68
+alias q     quit
69
+alias qa    quit!
70
+alias q!    quit!
71
+alias qall  quit!
72
+alias setl  setlocal
73
+alias filter scout -prts
74
+alias find  scout -aets
75
+alias travel scout -aefklst
76
+# ===== Quit Mappings (Essential) =====
77
+map     q quit
78
+map     Q quit!
79
+map     ZZ quit!
80
+map     ZQ quit!
81
+# ===== Key Mappings =====
82
+# ----- Basic Movement -----
83
+map     <UP>       move up=1
84
+map     <DOWN>     move down=1
85
+map     <LEFT>     move left=1
86
+map     <RIGHT>    move right=1
87
+map     <HOME>     move to=0
88
+map     <END>      move to=-1
89
+map     <PAGEDOWN> move down=1   pages=True
90
+map     <PAGEUP>   move up=1     pages=True
91
+map     <CR>       move right=1
92
+map     <DELETE>   console delete
93
+map     <INSERT>   console touch%space
94
+# ----- VIM-like -----
95
+copymap <UP>       k
96
+copymap <DOWN>     j
97
+copymap <LEFT>     h
98
+copymap <RIGHT>    l
99
+copymap <HOME>     gg
100
+copymap <END>      G
101
+copymap <PAGEDOWN> <C-F>
102
+copymap <PAGEUP>   <C-B>
103
+map J  move down=0.5  pages=True
104
+map K  move up=0.5    pages=True
105
+copymap J <C-D>
106
+copymap K <C-U>
107
+map H  history_go -1
108
+map L  history_go 1
109
+# ----- Jumping around -----
110
+map ]  move_parent 1
111
+map [  move_parent -1
112
+map }  traverse
113
+map {  traverse_backwards
114
+map )  jump_non
115
+map gh cd ~
116
+map ge cd /etc
117
+map gu cd /usr
118
+map gd cd ~/Documents
119
+map gD cd ~/Downloads
120
+map gp cd ~/Pictures
121
+map gc cd ~/.config
122
+map gm cd /mnt
123
+map gs cd ~/.local/share
124
+map gr cd /
125
+map gR eval fm.cd(ranger.RANGERDIR)
126
+map g/ cd /
127
+map g? cd /usr/share/doc/ranger
128
+# ----- Tabs -----
129
+map <C-n>  tab_new
130
+map <C-w>  tab_close
131
+map <TAB>  tab_move 1
132
+map <S-TAB> tab_move -1
133
+map <A-Right> tab_move 1
134
+map <A-Left>  tab_move -1
135
+map gt     tab_move 1
136
+map gT     tab_move -1
137
+map gn     tab_new
138
+map gc     tab_close
139
+map tt     tab_close
140
+map <a-1>  tab_open 1
141
+map <a-2>  tab_open 2
142
+map <a-3>  tab_open 3
143
+map <a-4>  tab_open 4
144
+map <a-5>  tab_open 5
145
+map <a-6>  tab_open 6
146
+map <a-7>  tab_open 7
147
+map <a-8>  tab_open 8
148
+map <a-9>  tab_open 9
149
+map <a-r>  tab_shift 1
150
+map <a-l>  tab_shift -1
151
+# ----- File Operations -----
152
+map yy copy
153
+map ya copy mode=add
154
+map yr copy mode=remove
155
+map yt copy mode=toggle
156
+map dd cut
157
+map da cut mode=add
158
+map dr cut mode=remove
159
+map dt cut mode=toggle
160
+map pp paste
161
+map po paste overwrite=True
162
+map pP paste append=True
163
+map pO paste overwrite=True append=True
164
+map pl paste_symlink relative=False
165
+map pL paste_symlink relative=True
166
+map phl paste_hardlink
167
+map pht paste_hardlinked_subtree
168
+map pd console paste dest=
169
+map p<any> paste dest=%any_path
170
+map p'<any> paste dest=%any_path
171
+map dD console delete
172
+map dT console trash
173
+map <SPACE> mark_files toggle=True
174
+map v       mark_files all=True toggle=True
175
+map uv      mark_files all=True val=False
176
+map V       toggle_visual_mode
177
+map uV      toggle_visual_mode reverse=True
178
+# ----- Searching -----
179
+map /  console search%space
180
+map n  search_next
181
+map N  search_next forward=False
182
+map ct search_next order=tag
183
+map cs search_next order=size
184
+map ci search_next order=mimetype
185
+map cc search_next order=ctime
186
+map cm search_next order=mtime
187
+map ca search_next order=atime
188
+# ----- Sorting -----
189
+map or set sort_reverse!
190
+map oz set sort=random
191
+map os chain set sort=size;      set sort_reverse=False
192
+map ob chain set sort=basename;  set sort_reverse=False
193
+map on chain set sort=natural;   set sort_reverse=False
194
+map om chain set sort=mtime;     set sort_reverse=False
195
+map oc chain set sort=ctime;     set sort_reverse=False
196
+map oa chain set sort=atime;     set sort_reverse=False
197
+map ot chain set sort=type;      set sort_reverse=False
198
+map oe chain set sort=extension; set sort_reverse=False
199
+map oS chain set sort=size;      set sort_reverse=True
200
+map oB chain set sort=basename;  set sort_reverse=True
201
+map oN chain set sort=natural;   set sort_reverse=True
202
+map oM chain set sort=mtime;     set sort_reverse=True
203
+map oC chain set sort=ctime;     set sort_reverse=True
204
+map oA chain set sort=atime;     set sort_reverse=True
205
+map oT chain set sort=type;      set sort_reverse=True
206
+map oE chain set sort=extension; set sort_reverse=True
207
+map dc get_cumulative_size
208
+# ----- Settings -----
209
+map zc    set collapse_preview!
210
+map zd    set sort_directories_first!
211
+map zh    set show_hidden!
212
+map <C-h> set show_hidden!
213
+copymap <C-h> <backspace>
214
+copymap <backspace> <backspace2>
215
+map zI    set flushinput!
216
+map zi    set preview_images!
217
+map zm    set mouse_enabled!
218
+map zp    set preview_files!
219
+map zP    set preview_directories!
220
+map zs    set sort_case_insensitive!
221
+map zu    set autoupdate_cumulative_size!
222
+map zv    set use_preview_script!
223
+map zf    console filter%space
224
+copymap zf zz
225
+# ----- Bookmarks -----
226
+map <any>  enter_bookmark %any
227
+map '<any>  enter_bookmark %any
228
+map m<any>  set_bookmark %any
229
+map um<any> unset_bookmark %any
230
+map m<bg>   draw_bookmarks
231
+copymap m<bg>  um<bg> `<bg> '<bg>
232
+# ----- External Programs -----
233
+map E  edit
234
+map du shell -p du --max-depth=1 -h --apparent-size
235
+map dU shell -p du --max-depth=1 -h --apparent-size | sort -rh
236
+map yp yank path
237
+map yd yank dir
238
+map yn yank name
239
+map y. yank name_without_extension
240
+# ----- Filesystem Operations -----
241
+map =  chmod
242
+map cw console rename%space
243
+map a  rename_append
244
+map A  eval fm.open_console('rename ' + fm.thisfile.relative_path.replace("%", "%%"))
245
+map I  eval fm.open_console('rename ' + fm.thisfile.relative_path.replace("%", "%%"), position=7)
246
+map pp paste
247
+map po paste overwrite=True
248
+map pP paste append=True
249
+map pO paste overwrite=True append=True
250
+map pl paste_symlink relative=False
251
+map pL paste_symlink relative=True
252
+map phl paste_hardlink
253
+map pht paste_hardlinked_subtree
254
+map pd console paste dest=
255
+# ----- Temporary workarounds -----
256
+map dgg eval fm.cut(dirarg=dict(to=0), narg=quantifier)
257
+map dG  eval fm.cut(dirarg=dict(to=-1), narg=quantifier)
258
+map dj  eval fm.cut(dirarg=dict(down=1), narg=quantifier)
259
+map dk  eval fm.cut(dirarg=dict(up=1), narg=quantifier)
260
+map ygg eval fm.copy(dirarg=dict(to=0), narg=quantifier)
261
+map yG  eval fm.copy(dirarg=dict(to=-1), narg=quantifier)
262
+map yj  eval fm.copy(dirarg=dict(down=1), narg=quantifier)
263
+map yk  eval fm.copy(dirarg=dict(up=1), narg=quantifier)
264
+# ----- Searching -----
265
+map /  console search%space
266
+map n  search_next
267
+map N  search_next forward=False
268
+map ct search_next order=tag
269
+map cs search_next order=size
270
+map ci search_next order=mimetype
271
+map cc search_next order=ctime
272
+map cm search_next order=mtime
273
+map ca search_next order=atime
274
+# ----- Console -----
275
+cmap <tab>   eval fm.ui.console.tab()
276
+cmap <s-tab> eval fm.ui.console.tab(-1)
277
+cmap <ESC>   eval fm.ui.console.close()
278
+cmap <CR>    eval fm.ui.console.execute()
279
+cmap <C-l>   redraw_window
280
+copycmap <ESC> <C-c>
281
+copycmap <CR>  <C-j>
282
+# ===== Main Quit Key (add this) =====
283
+map <ESC> quit
284
+# ----- Movement in console -----
285
+cmap <up>    eval fm.ui.console.history_move(-1)
286
+cmap <down>  eval fm.ui.console.history_move(1)
287
+cmap <left>  eval fm.ui.console.move(left=1)
288
+cmap <right> eval fm.ui.console.move(right=1)
289
+cmap <home>  eval fm.ui.console.move(right=0, absolute=True)
290
+cmap <end>   eval fm.ui.console.move(right=-1, absolute=True)
291
+cmap <a-b> eval fm.ui.console.move_word(left=1)
292
+cmap <a-f> eval fm.ui.console.move_word(right=1)
293
+copycmap <a-b> <a-left>
294
+copycmap <a-f> <a-right>
295
+# ----- Line Editing in console -----
296
+cmap <backspace>  eval fm.ui.console.delete(-1)
297
+cmap <delete>     eval fm.ui.console.delete(0)
298
+cmap <C-w>        eval fm.ui.console.delete_word()
299
+cmap <A-d>        eval fm.ui.console.delete_word(backward=False)
300
+cmap <C-k>        eval fm.ui.console.delete_rest(1)
301
+cmap <C-u>        eval fm.ui.console.delete_rest(-1)
302
+cmap <C-y>        eval fm.ui.console.paste()
303
+copycmap <backspace> <backspace2>
304
+cmap <allow_quantifiers> false
305
+# ----- Pager (less-like) -----
306
+pmap  <down>      pager_move  down=1
307
+pmap  <up>        pager_move  up=1
308
+pmap  <left>      pager_move  left=4
309
+pmap  <right>     pager_move  right=4
310
+pmap  <home>      pager_move  to=0
311
+pmap  <end>       pager_move  to=-1
312
+pmap  <pagedown>  pager_move  down=1.0  pages=True
313
+pmap  <pageup>    pager_move  up=1.0    pages=True
314
+pmap  <C-d>       pager_move  down=0.5  pages=True
315
+pmap  <C-u>       pager_move  up=0.5    pages=True
316
+copypmap <UP>       k  <C-p>
317
+copypmap <DOWN>     j  <C-n> <CR>
318
+copypmap <LEFT>     h
319
+copypmap <RIGHT>    l
320
+copypmap <HOME>     g
321
+copypmap <END>      G
322
+copypmap <C-d>      d
323
+copypmap <C-u>      u
324
+copypmap <PAGEDOWN> n  f  <C-F>  <Space>
325
+copypmap <PAGEUP>   p  b  <C-B>
326
+pmap     <C-l> redraw_window
327
+pmap     <ESC> pager_close
328
+copypmap <ESC> q Q i <F3>
329
+pmap E      edit_file
330
+# ----- Taskview -----
331
+tmap <up>        taskview_move up=1
332
+tmap <down>      taskview_move down=1
333
+tmap <home>      taskview_move to=0
334
+tmap <end>       taskview_move to=-1
335
+tmap <pagedown>  taskview_move down=1.0  pages=True
336
+tmap <pageup>    taskview_move up=1.0    pages=True
337
+tmap <C-d>       taskview_move down=0.5  pages=True
338
+tmap <C-u>       taskview_move up=0.5    pages=True
339
+copytmap <UP>       k  <C-p>
340
+copytmap <DOWN>     j  <C-n> <CR>
341
+copytmap <HOME>     g
342
+copytmap <END>      G
343
+copytmap <C-u>      u
344
+copytmap <PAGEDOWN> n  f  <C-F>  <Space>
345
+copytmap <PAGEUP>   p  b  <C-B>
346
+tmap J          eval -q fm.ui.taskview.task_move(-1)
347
+tmap K          eval -q fm.ui.taskview.task_move(0)
348
+tmap dd         eval -q fm.ui.taskview.task_remove()
349
+tmap <pagedown> eval -q fm.ui.taskview.task_move(-1)
350
+tmap <pageup>   eval -q fm.ui.taskview.task_move(0)
351
+tmap <delete>   eval -q fm.ui.taskview.task_remove()
352
+tmap <C-l> redraw_window
353
+tmap <ESC> taskview_close
354
+copytmap <ESC> q Q w <C-c>
355
+# ===== Fix Console Access =====
356
+map : console
357
+map ; console
358
+
359
+# ===== FZF Commands Only =====
360
+map <C-p> fzf_select
361
+map <A-f> console fzf_grep%space
ranger/rifle.confadded
@@ -0,0 +1,42 @@
1
+# Ranger rifle.conf - file opener configuration
2
+
3
+# Text files
4
+mime ^text,  label editor = ${VISUAL:-${EDITOR:-nvim}} -- "$@"
5
+mime ^text,  label pager  = "$PAGER" -- "$@"
6
+!mime ^text, label editor, ext xml|json|csv|tex|py|pl|rb|js|sh|php|rs|go|lua|vim = ${VISUAL:-${EDITOR:-nvim}} -- "$@"
7
+!mime ^text, label pager,  ext xml|json|csv|tex|py|pl|rb|js|sh|php|rs|go|lua|vim = "$PAGER" -- "$@"
8
+
9
+# Code files explicitly
10
+ext py|pl|rb|js|sh|php|lua|vim|rs|go|c|cpp|h|hpp|java = ${VISUAL:-${EDITOR:-nvim}} -- "$@"
11
+ext 1|2|3|4|5|6|7|8 = man "$1"
12
+
13
+# Fallback editor
14
+label editor, !mime ^text = ${VISUAL:-${EDITOR:-nvim}} -- "$@"
15
+
16
+# Images
17
+mime ^image, has imv,     X, flag f = imv -- "$@"
18
+mime ^image, has feh,     X, flag f = feh -- "$@"
19
+mime ^image, has sxiv,    X, flag f = sxiv -- "$@"
20
+
21
+# Documents
22
+ext pdf, has zathura,  X, flag f = zathura -- "$@"
23
+ext pdf, has evince,   X, flag f = evince -- "$@"
24
+
25
+# Audio/Video
26
+mime ^audio|ogg$, terminal, has mpv = mpv -- "$@"
27
+mime ^video,       has mpv,      X, flag f = mpv -- "$@"
28
+
29
+# Archives
30
+ext 7z|zip|tar|gz|bz2|xz|rar, has atool = atool --list -- "$@" | "$PAGER"
31
+ext 7z|zip|tar|gz|bz2|xz|rar, has atool = atool --extract -- "$@"
32
+
33
+# Directories
34
+mime inode/directory = ranger -- "$@"
35
+
36
+# Define the "editor" label
37
+label editor = ${VISUAL:-${EDITOR:-nvim}} -- "$@"
38
+label pager  = ${PAGER:-less} -- "$@"
39
+label open   = xdg-open -- "$@"
40
+
41
+# Fallback
42
+has xdg-open, X, flag f = xdg-open -- "$@"
ranger/scope.shadded
@@ -0,0 +1,350 @@
1
+#!/usr/bin/env bash
2
+
3
+set -o noclobber -o noglob -o nounset -o pipefail
4
+IFS=$'\n'
5
+
6
+## If the option `use_preview_script` is set to `true`,
7
+## then this script will be called and its output will be displayed in ranger.
8
+## ANSI color codes are supported.
9
+## STDIN is disabled, so interactive scripts won't work properly
10
+
11
+## This script is considered a configuration file and must be updated manually.
12
+## It will be left untouched if you upgrade ranger.
13
+
14
+## Because of some automated testing we do on the script #'s for comments need
15
+## to be doubled up. Code that is commented out, because it's an alternative for
16
+## example, gets only one #.
17
+
18
+## Meanings of exit codes:
19
+## code | meaning    | action of ranger
20
+## -----+------------+-------------------------------------------
21
+## 0    | success    | Display stdout as preview
22
+## 1    | no preview | Display no preview at all
23
+## 2    | plain text | Display the plain content of the file
24
+## 3    | fix width  | Don't reload when width changes
25
+## 4    | fix height | Don't reload when height changes
26
+## 5    | fix both   | Don't ever reload
27
+## 6    | image      | Display the image `$IMAGE_CACHE_PATH` points to as an image preview
28
+## 7    | image      | Display the file directly as an image
29
+
30
+## Script arguments
31
+FILE_PATH="${1}"         # Full path of the highlighted file
32
+PV_WIDTH="${2}"          # Width of the preview pane (number of fitting characters)
33
+## shellcheck disable=SC2034 # PV_HEIGHT is provided for convenience and unused
34
+PV_HEIGHT="${3}"         # Height of the preview pane (number of fitting characters)
35
+IMAGE_CACHE_PATH="${4}"  # Full path that should be used to cache image preview
36
+PV_IMAGE_ENABLED="${5}"  # 'True' if image previews are enabled, 'False' otherwise.
37
+
38
+FILE_EXTENSION="${FILE_PATH##*.}"
39
+FILE_EXTENSION_LOWER="$(printf "%s" "${FILE_EXTENSION}" | tr '[:upper:]' '[:lower:]')"
40
+
41
+## Settings
42
+HIGHLIGHT_SIZE_MAX=262143  # 256KiB
43
+HIGHLIGHT_TABWIDTH=${HIGHLIGHT_TABWIDTH:-8}
44
+HIGHLIGHT_STYLE=${HIGHLIGHT_STYLE:-pablo}
45
+HIGHLIGHT_OPTIONS="--replace-tabs=${HIGHLIGHT_TABWIDTH} --style=${HIGHLIGHT_STYLE} ${HIGHLIGHT_OPTIONS:-}"
46
+PYGMENTIZE_STYLE=${PYGMENTIZE_STYLE:-autumn}
47
+OPENSCAD_IMGSIZE=${RNGR_OPENSCAD_IMGSIZE:-1000,1000}
48
+OPENSCAD_COLORSCHEME=${RNGR_OPENSCAD_COLORSCHEME:-Tomorrow Night}
49
+
50
+handle_extension() {
51
+    case "${FILE_EXTENSION_LOWER}" in
52
+        ## Archive
53
+        a|ace|alz|arc|arj|bz|bz2|cab|cpio|deb|gz|jar|lha|lz|lzh|lzma|lzo|\
54
+        rpm|rz|t7z|tar|tbz|tbz2|tgz|tlz|txz|tZ|tzo|war|xpi|xz|Z|zip)
55
+            atool --list -- "${FILE_PATH}" && exit 5
56
+            bsdtar --list --file "${FILE_PATH}" && exit 5
57
+            exit 1;;
58
+        rar)
59
+            ## Avoid password prompt by providing empty password
60
+            unrar lt -p- -- "${FILE_PATH}" && exit 5
61
+            exit 1;;
62
+        7z)
63
+            ## Avoid password prompt by providing empty password
64
+            7z l -p -- "${FILE_PATH}" && exit 5
65
+            exit 1;;
66
+
67
+        ## PDF
68
+        pdf)
69
+            ## Preview as text conversion
70
+            pdftotext -l 10 -nopgbrk -q -- "${FILE_PATH}" - | \
71
+              fmt -w "${PV_WIDTH}" && exit 5
72
+            mutool draw -F txt -i -- "${FILE_PATH}" 1-10 | \
73
+              fmt -w "${PV_WIDTH}" && exit 5
74
+            exiftool "${FILE_PATH}" && exit 5
75
+            exit 1;;
76
+
77
+        ## BitTorrent
78
+        torrent)
79
+            transmission-show -- "${FILE_PATH}" && exit 5
80
+            exit 1;;
81
+
82
+        ## OpenDocument
83
+        odt|ods|odp|sxw)
84
+            ## Preview as text conversion
85
+            odt2txt "${FILE_PATH}" && exit 5
86
+            ## Preview as markdown conversion
87
+            pandoc -s -t markdown -- "${FILE_PATH}" && exit 5
88
+            exit 1;;
89
+
90
+        ## XLSX
91
+        xlsx)
92
+            ## Preview as csv conversion
93
+            ## Uses: https://github.com/dilshod/xlsx2csv
94
+            xlsx2csv -- "${FILE_PATH}" && exit 5
95
+            exit 1;;
96
+
97
+        ## HTML
98
+        htm|html|xhtml)
99
+            ## Preview as text conversion
100
+            w3m -dump "${FILE_PATH}" && exit 5
101
+            lynx -dump -- "${FILE_PATH}" && exit 5
102
+            elinks -dump "${FILE_PATH}" && exit 5
103
+            pandoc -s -t markdown -- "${FILE_PATH}" && exit 5
104
+            ;;
105
+
106
+        ## JSON
107
+        json)
108
+            jq --color-output . "${FILE_PATH}" && exit 5
109
+            python -m json.tool -- "${FILE_PATH}" && exit 5
110
+            ;;
111
+
112
+        ## Direct Stream Digital/Transfer (DSDIFF) and wavpack aren't detected
113
+        ## by file(1).
114
+        dff|dsf|wv|wvc)
115
+            mediainfo "${FILE_PATH}" && exit 5
116
+            exiftool "${FILE_PATH}" && exit 5
117
+            ;; # Continue with next handler on failure
118
+    esac
119
+}
120
+
121
+handle_image() {
122
+    ## Size of the preview if there are multiple options or it has to be
123
+    ## rendered from vector graphics. If the conversion program allows
124
+    ## specifying only one dimension while keeping the aspect ratio, the width
125
+    ## will be used.
126
+    local DEFAULT_SIZE="1920x1080"
127
+
128
+    local mimetype="${1}"
129
+    case "${mimetype}" in
130
+        ## SVG
131
+        # image/svg+xml|image/svg)
132
+        #     convert -- "${FILE_PATH}" "${IMAGE_CACHE_PATH}" && exit 6
133
+        #     exit 1;;
134
+
135
+        ## DjVu
136
+        # image/vnd.djvu)
137
+        #     ddjvu -format=tiff -quality=90 -page=1 -size="${DEFAULT_SIZE}" \
138
+        #           - "${IMAGE_CACHE_PATH}" < "${FILE_PATH}" \
139
+        #           && exit 6 || exit 1;;
140
+
141
+        ## Image
142
+        image/*)
143
+            local orientation
144
+            orientation="$( identify -format '%[EXIF:Orientation]\n' -- "${FILE_PATH}" )"
145
+            ## If orientation data is present and the image actually
146
+            ## needs rotating ("1" means no rotation)...
147
+            if [[ -n "$orientation" && "$orientation" != 1 ]]; then
148
+                ## ...auto-rotate the image according to the EXIF data.
149
+                convert -- "${FILE_PATH}" -auto-orient "${IMAGE_CACHE_PATH}" && exit 6
150
+            fi
151
+
152
+            ## `w3mimgdisplay` will be called for all images (unless overriden
153
+            ## as above), but might fail for unsupported types.
154
+            exit 7;;
155
+
156
+        ## Video
157
+        # video/*)
158
+        #     # Thumbnail
159
+        #     ffmpegthumbnailer -i "${FILE_PATH}" -o "${IMAGE_CACHE_PATH}" -s 0 && exit 6
160
+        #     exit 1;;
161
+
162
+        ## PDF
163
+        # application/pdf)
164
+        #     pdftoppm -f 1 -l 1 \
165
+        #              -scale-to-x "${DEFAULT_SIZE%x*}" \
166
+        #              -scale-to-y -1 \
167
+        #              -singlefile \
168
+        #              -jpeg -tiffcompression jpeg \
169
+        #              -- "${FILE_PATH}" "${IMAGE_CACHE_PATH%.*}" \
170
+        #         && exit 6 || exit 1;;
171
+
172
+
173
+        ## ePub, MOBI, FB2 (using Calibre)
174
+        # application/epub+zip|application/x-mobipocket-ebook|\
175
+        # application/x-fictionbook+xml)
176
+        #     # ePub (using https://github.com/marianosimone/epub-thumbnailer)
177
+        #     epub-thumbnailer "${FILE_PATH}" "${IMAGE_CACHE_PATH}" \
178
+        #         "${DEFAULT_SIZE%x*}" && exit 6
179
+        #     ebook-meta --get-cover="${IMAGE_CACHE_PATH}" -- "${FILE_PATH}" \
180
+        #         >/dev/null && exit 6
181
+        #     exit 1;;
182
+
183
+        ## Font
184
+        application/font*|application/*opentype)
185
+            preview_png="/tmp/$(basename "${IMAGE_CACHE_PATH%.*}").png"
186
+            if fontimage -o "${preview_png}" \
187
+                         --pixelsize "120" \
188
+                         --fontname \
189
+                         --pixelsize "80" \
190
+                         --text "  ABCDEFGHIJKLMNOPQRSTUVWXYZ  " \
191
+                         --text "  abcdefghijklmnopqrstuvwxyz  " \
192
+                         --text "  0123456789.:,;(*!?') ff fl fi ffi ffl  " \
193
+                         --text "  The quick brown fox jumps over the lazy dog.  " \
194
+                         "${FILE_PATH}";
195
+            then
196
+                convert -- "${preview_png}" "${IMAGE_CACHE_PATH}" \
197
+                    && rm "${preview_png}" \
198
+                    && exit 6
199
+            else
200
+                exit 1
201
+            fi
202
+            ;;
203
+
204
+        ## Preview archives using the first image inside.
205
+        ## (Very useful for comic book collections for example.)
206
+        # application/zip|application/x-rar|application/x-7z-compressed|\
207
+        #     application/x-xz|application/x-bzip2|application/x-gzip|application/x-tar)
208
+        #     local fn=""; local fe=""
209
+        #     local zip=""; local rar=""; local tar=""; local bsd=""
210
+        #     case "${mimetype}" in
211
+        #         application/zip) zip=1 ;;
212
+        #         application/x-rar) rar=1 ;;
213
+        #         application/x-7z-compressed) ;;
214
+        #         *) tar=1 ;;
215
+        #     esac
216
+        #     { [ "$tar" ] && fn=$(tar --list --file "${FILE_PATH}"); } || \
217
+        #     { fn=$(bsdtar --list --file "${FILE_PATH}") && bsd=1 && tar=""; } || \
218
+        #     { [ "$rar" ] && fn=$(unrar lb -p- -- "${FILE_PATH}"); } || \
219
+        #     { [ "$zip" ] && fn=$(zipinfo -1 -- "${FILE_PATH}"); } || return
220
+        #
221
+        #     fn=$(echo "$fn" | python -c "import sys; import mimetypes as m; \
222
+        #             [ print(l, end='') for l in sys.stdin if \
223
+        #               (m.guess_type(l[:-1])[0] or '').startswith('image/') ]" |\
224
+        #         sort -V | head -n 1)
225
+        #     [ "$fn" = "" ] && return
226
+        #     [ "$bsd" ] && fn=$(printf '%b' "$fn")
227
+        #
228
+        #     [ "$tar" ] && tar --extract --to-stdout \
229
+        #         --file "${FILE_PATH}" -- "$fn" > "${IMAGE_CACHE_PATH}" && exit 6
230
+        #     fe=$(echo -n "$fn" | sed 's/[][*?\]/\\\0/g')
231
+        #     [ "$bsd" ] && bsdtar --extract --to-stdout \
232
+        #         --file "${FILE_PATH}" -- "$fe" > "${IMAGE_CACHE_PATH}" && exit 6
233
+        #     [ "$bsd" ] || [ "$tar" ] && rm -- "${IMAGE_CACHE_PATH}"
234
+        #     [ "$rar" ] && unrar p -p- -inul -- "${FILE_PATH}" "$fn" > \
235
+        #         "${IMAGE_CACHE_PATH}" && exit 6
236
+        #     [ "$zip" ] && unzip -pP "" -- "${FILE_PATH}" "$fe" > \
237
+        #         "${IMAGE_CACHE_PATH}" && exit 6
238
+        #     [ "$rar" ] || [ "$zip" ] && rm -- "${IMAGE_CACHE_PATH}"
239
+        #     ;;
240
+    esac
241
+
242
+    # openscad_image() {
243
+    #     TMPPNG="$(mktemp -t XXXXXX.png)"
244
+    #     openscad --colorscheme="${OPENSCAD_COLORSCHEME}" \
245
+    #         --imgsize="${OPENSCAD_IMGSIZE/x/,}" \
246
+    #         -o "${TMPPNG}" "${1}"
247
+    #     mv "${TMPPNG}" "${IMAGE_CACHE_PATH}"
248
+    # }
249
+
250
+    # case "${FILE_EXTENSION_LOWER}" in
251
+    #     ## 3D models
252
+    #     ## OpenSCAD only supports png image output, and ${IMAGE_CACHE_PATH}
253
+    #     ## is hardcoded as jpeg. So we make a tempfile.png and just
254
+    #     ## move/rename it to jpg. This works because image libraries are
255
+    #     ## smart enough to handle it.
256
+    #     csg|scad)
257
+    #         openscad_image "${FILE_PATH}" && exit 6
258
+    #         ;;
259
+    #     3mf|amf|dxf|off|stl)
260
+    #         openscad_image <(echo "import(\"${FILE_PATH}\");") && exit 6
261
+    #         ;;
262
+    # esac
263
+}
264
+
265
+handle_mime() {
266
+    local mimetype="${1}"
267
+    case "${mimetype}" in
268
+        ## RTF and DOC
269
+        text/rtf|*msword)
270
+            ## Preview as text conversion
271
+            ## note: catdoc does not always work for .doc files
272
+            ## catdoc: http://www.wagner.pp.ru/~vitus/software/catdoc/
273
+            catdoc -- "${FILE_PATH}" && exit 5
274
+            exit 1;;
275
+
276
+        ## DOCX, ePub, FB2 (using markdown)
277
+        ## You might want to remove "|epub" and/or "|fb2" below if you have
278
+        ## uncommented other methods to preview those formats
279
+        *wordprocessingml.document|*/epub+zip|*/x-fictionbook+xml)
280
+            ## Preview as markdown conversion
281
+            pandoc -s -t markdown -- "${FILE_PATH}" && exit 5
282
+            exit 1;;
283
+
284
+        ## XLS
285
+        *ms-excel)
286
+            ## Preview as csv conversion
287
+            ## xls2csv comes with catdoc:
288
+            ##   http://www.wagner.pp.ru/~vitus/software/catdoc/
289
+            xls2csv -- "${FILE_PATH}" && exit 5
290
+            exit 1;;
291
+
292
+        ## Text
293
+        text/* | */xml)
294
+            ## Syntax highlight
295
+            if [[ "$( stat --printf='%s' -- "${FILE_PATH}" )" -gt "${HIGHLIGHT_SIZE_MAX}" ]]; then
296
+                exit 2
297
+            fi
298
+            if [[ "$( tput colors )" -ge 256 ]]; then
299
+                local pygmentize_format='terminal256'
300
+                local highlight_format='xterm256'
301
+            else
302
+                local pygmentize_format='terminal'
303
+                local highlight_format='ansi'
304
+            fi
305
+            env HIGHLIGHT_OPTIONS="${HIGHLIGHT_OPTIONS}" highlight \
306
+                --out-format="${highlight_format}" \
307
+                --force -- "${FILE_PATH}" && exit 5
308
+            env COLORTERM=8bit bat --color=always --style="plain" \
309
+                -- "${FILE_PATH}" && exit 5
310
+            pygmentize -f "${pygmentize_format}" -O "style=${PYGMENTIZE_STYLE}"\
311
+                -- "${FILE_PATH}" && exit 5
312
+            exit 2;;
313
+
314
+        ## DjVu
315
+        image/vnd.djvu)
316
+            ## Preview as text conversion (requires djvulibre)
317
+            djvutxt "${FILE_PATH}" | fmt -w "${PV_WIDTH}" && exit 5
318
+            exiftool "${FILE_PATH}" && exit 5
319
+            exit 1;;
320
+
321
+        ## Image
322
+        image/*)
323
+            ## Preview as text conversion
324
+            # img2txt --gamma=0.6 --width="${PV_WIDTH}" -- "${FILE_PATH}" && exit 4
325
+            exiftool "${FILE_PATH}" && exit 5
326
+            exit 1;;
327
+
328
+        ## Video and audio
329
+        video/* | audio/*)
330
+            mediainfo "${FILE_PATH}" && exit 5
331
+            exiftool "${FILE_PATH}" && exit 5
332
+            exit 1;;
333
+    esac
334
+}
335
+
336
+handle_fallback() {
337
+    echo '----- File Type Classification -----' && file --dereference --brief -- "${FILE_PATH}" && exit 5
338
+    exit 1
339
+}
340
+
341
+
342
+MIMETYPE="$( file --dereference --brief --mime-type -- "${FILE_PATH}" )"
343
+if [[ "${PV_IMAGE_ENABLED}" == 'True' ]]; then
344
+    handle_image "${MIMETYPE}"
345
+fi
346
+handle_extension
347
+handle_mime "${MIMETYPE}"
348
+handle_fallback
349
+
350
+exit 1