diff options
44 files changed, 1042 insertions, 263 deletions
diff --git a/.flake8 b/.flake8 index f5072c08..b77e4e6c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 99 -ignore = E221 +ignore = E221,W503 diff --git a/.gitignore b/.gitignore index 88c75b90..73ca85e6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ /ranger_fm.egg-info /stuff/* + +.idea +.pytest_cache diff --git a/.travis.yml b/.travis.yml index f9e31256..0efd09fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,14 @@ -dist: 'trusty' +dist: 'xenial' language: 'python' python: - - '2.7' - - '3.4' - - '3.5' + - '2.7' + - '3.4' + - '3.5' install: - - 'pip install pytest pylint flake8' + - 'pip install pipenv' + - 'pipenv update --dev' script: - - 'make test' + - 'make test' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..36ad0a95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +# Usage instructions: +# 1. "docker build -t ranger/ranger:latest ." +# 2. "docker run -it ranger/ranger" + +FROM debian + +RUN apt-get update && apt-get install -y ranger +ENTRYPOINT ["ranger"] diff --git a/Makefile b/Makefile index 47e2fa01..6cd1ec8f 100644 --- a/Makefile +++ b/Makefile @@ -98,7 +98,11 @@ test_pytest: @echo "Running py.test tests..." py.test tests -test: test_pylint test_flake8 test_doctest test_pytest +test_other: + @echo "Checking completeness of man page..." + @tests/manpage_completion_test.py + +test: test_pylint test_flake8 test_doctest test_pytest test_other @echo "Finished testing: All tests passed!" man: @@ -122,4 +126,5 @@ todo: @grep --color -Ion '\(TODO\|XXX\).*' -r ranger .PHONY: clean cleandoc compile default dist doc help install man manhtml \ - options snapshot test test_pylint test_flake8 test_doctest test_pytest todo pypi_sdist + options snapshot test test_pylint test_flake8 test_doctest test_pytest \ + test_other todo pypi_sdist diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..a927408c --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[requires] +python_version = "3.5" + +[dev-packages] +pytest = "*" +"flake8" = "*" +pylint = "<2.0.0" +"enum34" = "*" + +[packages] diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..be0d4a62 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,175 @@ +{ + "_meta": { + "hash": { + "sha256": "ccd51f0d238502cb3001a4b709d4455134eeaebb96800ebaad364567ba1ba784" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.5" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "astroid": { + "hashes": [ + "sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a", + "sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a" + ], + "version": "==1.6.5" + }, + "atomicwrites": { + "hashes": [ + "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", + "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" + ], + "version": "==1.1.5" + }, + "attrs": { + "hashes": [ + "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", + "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" + ], + "version": "==18.1.0" + }, + "enum34": { + "hashes": [ + "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", + "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", + "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", + "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" + ], + "index": "pypi", + "version": "==1.1.6" + }, + "flake8": { + "hashes": [ + "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", + "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37" + ], + "index": "pypi", + "version": "==3.5.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", + "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", + "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" + ], + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", + "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", + "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", + "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" + ], + "version": "==1.5.4" + }, + "pycodestyle": { + "hashes": [ + "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", + "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9" + ], + "version": "==2.3.1" + }, + "pyflakes": { + "hashes": [ + "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", + "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" + ], + "version": "==1.6.0" + }, + "pylint": { + "hashes": [ + "sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c", + "sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3" + ], + "index": "pypi", + "version": "==1.9.2" + }, + "pytest": { + "hashes": [ + "sha256:0453c8676c2bee6feb0434748b068d5510273a916295fd61d306c4f22fbfd752", + "sha256:4b208614ae6d98195430ad6bde03641c78553acee7c83cec2e85d613c0cd383d" + ], + "index": "pypi", + "version": "==3.6.3" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} diff --git a/README.md b/README.md index ef644ae6..e14bab46 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Optional: * The python module `chardet`, in case of encoding detection problems * `sudo` to use the "run as root"-feature * `w3m` for the `w3mimgdisplay` program to preview images +* `python-bidi` for correct display of RTL file names (Hebrew, Arabic) Optional, for enhanced file previews (with `scope.sh`): @@ -75,10 +76,11 @@ Optional, for enhanced file previews (with `scope.sh`): * `highlight` or `pygmentize` for syntax highlighting of code * `atool`, `bsdtar` and/or `unrar` for previews of archives * `lynx`, `w3m` or `elinks` for previews of html pages -* `pdftotext` for pdf previews +* `pdftotext` or `mutool` for pdf previews * `transmission-show` for viewing bit-torrent information * `mediainfo` or `exiftool` for viewing information about media files * `odt2txt` for OpenDocument text files (`odt`, `ods`, `odp` and `sxw`) +* `chardet` (Python package) for improved encoding detection of text files Installing diff --git a/doc/colorschemes.md b/doc/colorschemes.md new file mode 100644 index 00000000..458cfc53 --- /dev/null +++ b/doc/colorschemes.md @@ -0,0 +1,140 @@ +Colorschemes +============ + +This text explains colorschemes and how they work. + +Context Tags +------------ + +Context tags provide information about the context and are Boolean values (`True` +or `False`). For example, if the tag `in_titlebar` is set, you probably want to +know about the color of a part of the titlebar now. + +The default context tags are specified in `/ranger/gui/context.py` in the +constant `CONTEXT_KEYS`. Custom tags can be specified in a custom plugin file in +`~/.config/ranger/plugins/`. The code to use follows. + +```python +# Import the class +import ranger.gui.context + +# Add your key names +ranger.gui.context.CONTEXT_KEYS.append('my_key') + +# Set it to False (the default value) +ranger.gui.context.Context.my_key = False + +# Or use an array for multiple names +my_keys = ['key_one', 'key_two'] +ranger.gui.context.CONTEXT_KEYS.append(my_keys) + +# Set them to False +for key in my_keys: + code = 'ranger.gui.context.Context.' + key + ' = False' + exec(code) +``` + +As you may or may not have guessed, this only tells ranger that they exist, not +what they mean. To do this, you'll have to dig around in the source code. As an +example, let's walk through adding a key that highlights `README.md` files +differently. All the following code will be written in a standalone plugin file. + +First, from above, we'll add the key `readme` and set it to `False`. + +```python +import ranger.gui.context + +ranger.gui.context.CONTEXT_KEYS.append('readme') +ranger.gui.context.Context.readme = False +``` + +Then we'll use the hook `hook_before_drawing` to tell ranger that our key is +talking about `README.md` files. + +```python +import ranger.gui.widgets.browsercolumn + +OLD_HOOK_BEFORE_DRAWING = ranger.gui.widgets.browsercolumn.hook_before_drawing + +def new_hook_before_drawing(fsobject, color_list): + if fsobject.basename === 'README.md': + color_list.append('readme') + + return OLD_HOOK_BEFORE_DRAWING(fsobject, color_list) + +ranger.gui.widgets.browsercolumn.hook_before_drawing = new_hook_before_drawing +``` + +Notice we call the old `hook_before_drawing`. This makes sure that we don't +overwrite another plugin's code, we just append our own to it. + +To highlight it a different color, just [add it to your colorscheme][1] + +[1]:#adapt-a-colorscheme + +Implementation in the GUI Classes +--------------------------------- + +The class `CursesShortcuts` in the file `/ranger/gui/curses_shortcuts.py` defines +the methods `color(*tags)`, `color_at(y, x, wid, *tags)` and `color_reset()`. +This class is a superclass of `Displayable`, so these methods are available almost +everywhere. + +Something like `color("in_titlebar", "directory")` will be called to get the +color of directories in the titlebar. This creates a `ranger.gui.context.Context` +object, sets its attributes `in_titlebar` and `directory` to True, leaves the +others as `False`, and passes it to the colorscheme's `use(context)` method. + +The Color Scheme +---------------- + +A colorscheme should be a subclass of `ranger.gui.ColorScheme` and define the +method `use(context)`. By looking at the context, this use-method has to +determine a 3-tuple of integers: `(foreground, background, attribute)` and return +it. + +`foreground` and `background` are integers representing colors, `attribute` is +another integer with each bit representing one attribute. These integers are +interpreted by the terminal emulator in use. + +Abbreviations for colors and attributes are defined in `ranger.gui.color`. Two +attributes can be combined via bitwise OR: `bold | reverse` + +Once the color for a set of tags is determined, it will be cached by default. If +you want more dynamic colorschemes (such as a different color for very large +files), you will need to dig into the source code, perhaps add a custom tag and +modify the draw-method of the widget to use that tag. + +Run `tc_colorscheme` to check if your colorschemes are valid. + +Specify a Colorscheme +--------------------- + +Colorschemes are searched for in these directories: + +- `~/.config/ranger/colorschemes/` +- `/path/to/ranger/colorschemes/` + +To specify which colorscheme to use, change the option `colorscheme` in your +rc.conf: `set colorscheme default`. + +This means, use the colorscheme contained in either +`~/.config/ranger/colorschemes/default.py` or +`/path/to/ranger/colorschemes/default.py`. + +Adapt a colorscheme +------------------- + +You may want to adapt a colorscheme to your needs without having a complete copy +of it, but rather the changes only. Say, you want the exact same colors as in +the default colorscheme, but the directories to be green rather than blue, +because you find the blue hard to read. + +This is done in the jungle colorscheme `ranger/colorschemes/jungle`, check it +out for implementation details. In short, I made a subclass of the default +scheme, set the initial colors to the result of the default `use()` method and +modified the colors how I wanted. + +This has the obvious advantage that you need to write less, which results in +less maintenance work and a greater chance that your colorscheme will work with +future versions of ranger. diff --git a/doc/colorschemes.txt b/doc/colorschemes.txt deleted file mode 100644 index 145cc94e..00000000 --- a/doc/colorschemes.txt +++ /dev/null @@ -1,92 +0,0 @@ -Colorschemes -============ - -This text explains colorschemes and how they work. - - -Context Tags ------------- - -Context Tags provide information about the context. If the tag -"in_titlebar" is set, you probably want to know about the color -of a part of the titlebar now. - -There are a number of context tags, specified in /ranger/gui/context.py -in the constant CONTEXT_KEYS. - -A Context object, defined in the same file, contains attributes with -the names of all tags, whose values are either True or False. - - -Implementation in the GUI Classes ---------------------------------- - -The class CursesShortcuts in the file /ranger/gui/curses_shortcuts.py -defines the methods color(*tags), color_at(y, x, wid, *tags) and -color_reset(). This class is a superclass of Displayable, so these -methods are available almost everywhere. - -Something like color("in_titlebar", "directory") will be called to -get the color of directories in the titlebar. This creates a -ranger.gui.context.Context object, sets its attributes "in_titlebar" and -"directory" to True, leaves the others as False, and passes it to the -colorscheme's use(context) method. - - -The Color Scheme ----------------- - -A colorscheme should be a subclass of ranger.gui.ColorScheme and -define the method use(context). By looking at the context, this use-method -has to determine a 3-tuple of integers: (foreground, background, attribute) -and return it. - -foreground and background are integers representing colors, -attribute is another integer with each bit representing one attribute. -These integers are interpreted by the used terminal emulator. - -Abbreviations for colors and attributes are defined in ranger.gui.color. -Two attributes can be combined via bitwise OR: bold | reverse - -Once the color for a set of tags is determined, it will be cached by -default. If you want more dynamic colorschemes (such as a different -color for very large files), you will need to dig into the source code, -perhaps add an own tag and modify the draw-method of the widget to use -that tag. - -Run tc_colorscheme to check if your colorschemes are valid. - - -Specify a Colorscheme ---------------------- - -Colorschemes are searched for in these directories: -~/.config/ranger/colorschemes/ -/path/to/ranger/colorschemes/ - -To specify which colorscheme to use, change the option "colorscheme" -in your rc.conf: -set colorscheme default - -This means, use the colorscheme contained in -either ~/.config/ranger/colorschemes/default.py or -/path/to/ranger/colorschemes/default.py. - - -Adapt a colorscheme -------------------- - -You may want to adapt a colorscheme to your needs without having -a complete copy of it, but rather the changes only. Say, you -want the exact same colors as in the default colorscheme, but -the directories to be green rather than blue, because you find the -blue hard to read. - -This is done in the jungle colorscheme ranger/colorschemes/jungle, -check it out for implementation details. In short, I made a subclass -of the default scheme, set the initial colors to the result of the -default use() method and modified the colors how I wanted. - -This has the obvious advantage that you need to write less, which -results in less maintenance work and a greater chance that your colorscheme -will work with future versions of ranger. diff --git a/doc/ranger.1 b/doc/ranger.1 index bd6116d1..e141011f 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -129,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.9.1" "2018-04-03" "ranger manual" +.TH RANGER 1 "ranger-1.9.1" "2018-07-15" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -274,10 +274,10 @@ typing \fI"<tagname>\fR. By default, only text files are previewed, but you can enable external preview scripts by setting the option \f(CW\*(C`use_preview_script\*(C'\fR and \f(CW\*(C`preview_files\*(C'\fR to true. .PP -This default script is \fI~/.config/ranger/scope.sh\fR. It contains more +This default script is \fI\f(CI%rangerdir\fI/data/scope.sh\fR. It contains more documentation and calls to the programs \fIlynx\fR and \fIelinks\fR for html, \&\fIhighlight\fR for text/code, \fIimg2txt\fR for images, \fIatool\fR for archives, -\&\fIpdftotext\fR for PDFs and \fImediainfo\fR for video and audio files. +\&\fIpdftotext\fR or \fImutool\fR for PDFs and \fImediainfo\fR for video and audio files. .PP Install these programs (just the ones you need) and scope.sh will automatically use them. @@ -307,6 +307,13 @@ This feature relies on the dimensions of the terminal's font. By default, a width of 8 and height of 11 are used. To use other values, set the options \&\f(CW\*(C`iterm2_font_width\*(C'\fR and \f(CW\*(C`iterm2_font_height\*(C'\fR to the desired values. .PP +\fIterminology\fR +.IX Subsection "terminology" +.PP +This only works in terminology. It can render vector graphics, but works only locally. +.PP +To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to terminology. +.PP \fIurxvt\fR .IX Subsection "urxvt" .PP @@ -324,6 +331,14 @@ The same as urxvt but utilizing not only the preview pane but the whole terminal window. .PP To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to urxvt-full. +.PP +\fIkitty\fR +.IX Subsection "kitty" +.PP +This only works on Kitty. It requires \s-1PIL\s0 (or pillow) to work. +Allows remote image previews, for example in an ssh session. +.PP +To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to kitty. .SS "\s-1SELECTION\s0" .IX Subsection "SELECTION" The \fIselection\fR is defined as \*(L"All marked files \s-1IF THERE ARE ANY,\s0 otherwise @@ -470,7 +485,7 @@ sample plugins in the \fI/usr/share/doc/ranger/examples/\fR directory, including hello-world plugin that describes this procedure. .SH "KEY BINDINGS" .IX Header "KEY BINDINGS" -Key bindings are defined in the file \fIranger/config/rc.conf\fR. Check this +Key bindings are defined in the file \fI\f(CI%rangerdir\fI/config/rc.conf\fR. Check this file for a list of all key bindings. You can copy it to your local configuration directory with the \-\-copy\-config=rc option. .PP @@ -675,7 +690,7 @@ scrolling to switch directories. .SH "SETTINGS" .IX Header "SETTINGS" This section lists all built-in settings of ranger. The valid types for the -value are in [brackets]. The hotkey to toggle the setting is in <brokets>, if +value are in [brackets]. The hotkey to toggle the setting is in <brakets>, if a hotkey exists. .PP Settings can be changed in the file \fI~/.config/ranger/rc.conf\fR or on the @@ -709,7 +724,7 @@ in ranger. .IP "automatically_count_files [bool]" 4 .IX Item "automatically_count_files [bool]" Should ranger count and display the number of files in each directory -as soon as it's visible? This gets slow with remote file sytems. Turning it +as soon as it's visible? This gets slow with remote file systems. Turning it off will still allow you to see the number of files after entering the directory. .IP "autosave_bookmarks [bool]" 4 @@ -771,6 +786,9 @@ Display the file size in the main column? .IP "display_size_in_status_bar [bool]" 4 .IX Item "display_size_in_status_bar [bool]" Display the file size in the status bar? +.IP "display_free_space_in_status_bar [bool]" 4 +.IX Item "display_free_space_in_status_bar [bool]" +Display the free disk space in the status bar? .IP "display_tags_in_all_columns [bool]" 4 .IX Item "display_tags_in_all_columns [bool]" Display tags in all columns? @@ -890,6 +908,10 @@ to disable this feature. Which script should handle generating previews? If the file doesn't exist, or use_preview_script is off, ranger will handle previews itself by just printing the content. +.IP "relative_current_zero [bool]" 4 +.IX Item "relative_current_zero [bool]" +When line_numbers is set to relative, show 0 on the current line if +true or show the absolute number of the current line when false. .IP "save_backtick_bookmark [bool]" 4 .IX Item "save_backtick_bookmark [bool]" Save the \f(CW\*(C`\`\*(C'\fR bookmark to disk. This bookmark is used to switch to the last @@ -952,6 +974,11 @@ Abbreviate \f(CW$HOME\fR with ~ in the titlebar (first line) of ranger? .IP "unicode_ellipsis [bool]" 4 .IX Item "unicode_ellipsis [bool]" Use a unicode \*(L"...\*(R" character instead of \*(L"~\*(R" to mark cut-off filenames? +.IP "bidi_support [bool]" 4 +.IX Item "bidi_support [bool]" +Try to properly display file names in \s-1RTL\s0 languages (Hebrew, Arabic) by using +a \s-1BIDI\s0 algorithm to reverse the relevant parts of the text. +Requires the python-bidi pip package. .IP "update_title [bool]" 4 .IX Item "update_title [bool]" Set a window title? @@ -978,6 +1005,10 @@ Sets the state for the version control backend. The possible values are: Sets the view mode, which can be \fBmiller\fR to display the files in the traditional miller column view that shows multiple levels of the hierarchy, or \&\fBmultipane\fR to use multiple panes (one per tab) similar to midnight-commander. +.IP "w3m_delay [float]" 4 +.IX Item "w3m_delay [float]" +Delay in seconds before displaying an image with the w3m method. +Increase it in case of experiencing display corruption. .IP "wrap_scroll [bool]" 4 .IX Item "wrap_scroll [bool]" Enable scroll wrapping \- moving down while on the last item will wrap around to @@ -1432,6 +1463,9 @@ being bound despite the corresponding line being removed from the user's copy of the configuration file. This behavior may be disabled with an environment variable (see also: \fB\s-1ENVIRONMENT\s0\fR). Note: All other configuration files only read from one source; i.e. default \s-1OR\s0 user, not both. +\&\fIrc.conf\fR and \fIcommands.py\fR are additionally read from \fI/etc/ranger\fR if they +exist for system-wide configuration, user configuration overrides system +configuration which overrides the default configuration. .PP When starting ranger with the \fB\-\-clean\fR option, it will not access or create any of these files. diff --git a/doc/ranger.pod b/doc/ranger.pod index 4b0e90b1..3062fef5 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -189,10 +189,10 @@ typing I<"<tagnameE<gt>>. By default, only text files are previewed, but you can enable external preview scripts by setting the option C<use_preview_script> and C<preview_files> to true. -This default script is F<~/.config/ranger/scope.sh>. It contains more +This default script is F<%rangerdir/data/scope.sh>. It contains more documentation and calls to the programs I<lynx> and I<elinks> for html, I<highlight> for text/code, I<img2txt> for images, I<atool> for archives, -I<pdftotext> for PDFs and I<mediainfo> for video and audio files. +I<pdftotext> or I<mutool> for PDFs and I<mediainfo> for video and audio files. Install these programs (just the ones you need) and scope.sh will automatically use them. @@ -220,6 +220,12 @@ This feature relies on the dimensions of the terminal's font. By default, a width of 8 and height of 11 are used. To use other values, set the options C<iterm2_font_width> and C<iterm2_font_height> to the desired values. +=head3 terminology + +This only works in terminology. It can render vector graphics, but works only locally. + +To enable this feature, set the option C<preview_images_method> to terminology. + =head3 urxvt This only works in urxvt compiled with pixbuf support. Does not work over ssh. @@ -236,6 +242,13 @@ window. To enable this feature, set the option C<preview_images_method> to urxvt-full. +=head3 kitty + +This only works on Kitty. It requires PIL (or pillow) to work. +Allows remote image previews, for example in an ssh session. + +To enable this feature, set the option C<preview_images_method> to kitty. + =head2 SELECTION The I<selection> is defined as "All marked files IF THERE ARE ANY, otherwise @@ -379,7 +392,7 @@ hello-world plugin that describes this procedure. =head1 KEY BINDINGS -Key bindings are defined in the file F<ranger/config/rc.conf>. Check this +Key bindings are defined in the file F<%rangerdir/config/rc.conf>. Check this file for a list of all key bindings. You can copy it to your local configuration directory with the --copy-config=rc option. @@ -662,7 +675,7 @@ scrolling to switch directories. =head1 SETTINGS This section lists all built-in settings of ranger. The valid types for the -value are in [brackets]. The hotkey to toggle the setting is in <brokets>, if +value are in [brackets]. The hotkey to toggle the setting is in <brakets>, if a hotkey exists. Settings can be changed in the file F<~/.config/ranger/rc.conf> or on the @@ -693,7 +706,7 @@ in ranger. =item automatically_count_files [bool] Should ranger count and display the number of files in each directory -as soon as it's visible? This gets slow with remote file sytems. Turning it +as soon as it's visible? This gets slow with remote file systems. Turning it off will still allow you to see the number of files after entering the directory. @@ -767,6 +780,10 @@ Display the file size in the main column? Display the file size in the status bar? +=item display_free_space_in_status_bar [bool] + +Display the free disk space in the status bar? + =item display_tags_in_all_columns [bool] Display tags in all columns? @@ -904,6 +921,11 @@ Which script should handle generating previews? If the file doesn't exist, or use_preview_script is off, ranger will handle previews itself by just printing the content. +=item relative_current_zero [bool] + +When line_numbers is set to relative, show 0 on the current line if +true or show the absolute number of the current line when false. + =item save_backtick_bookmark [bool] Save the C<`> bookmark to disk. This bookmark is used to switch to the last @@ -983,6 +1005,12 @@ Abbreviate $HOME with ~ in the titlebar (first line) of ranger? Use a unicode "..." character instead of "~" to mark cut-off filenames? +=item bidi_support [bool] + +Try to properly display file names in RTL languages (Hebrew, Arabic) by using +a BIDI algorithm to reverse the relevant parts of the text. +Requires the python-bidi pip package. + =item update_title [bool] Set a window title? @@ -1013,6 +1041,11 @@ Sets the view mode, which can be B<miller> to display the files in the traditional miller column view that shows multiple levels of the hierarchy, or B<multipane> to use multiple panes (one per tab) similar to midnight-commander. +=item w3m_delay [float] + +Delay in seconds before displaying an image with the w3m method. +Increase it in case of experiencing display corruption. + =item wrap_scroll [bool] Enable scroll wrapping - moving down while on the last item will wrap around to @@ -1527,6 +1560,9 @@ being bound despite the corresponding line being removed from the user's copy of the configuration file. This behavior may be disabled with an environment variable (see also: B<ENVIRONMENT>). Note: All other configuration files only read from one source; i.e. default OR user, not both. +F<rc.conf> and F<commands.py> are additionally read from F</etc/ranger> if they +exist for system-wide configuration, user configuration overrides system +configuration which overrides the default configuration. When starting ranger with the B<--clean> option, it will not access or create any of these files. diff --git a/doc/rifle.1 b/doc/rifle.1 index ad32e4f4..c8c679ec 100644 --- a/doc/rifle.1 +++ b/doc/rifle.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.07 (Pod::Simple 3.32) +.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -129,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RIFLE 1" -.TH RIFLE 1 "rifle-1.9.1" "05.03.2018" "rifle manual" +.TH RIFLE 1 "rifle-1.9.1" "2018-06-07" "rifle manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l diff --git a/examples/bash_automatic_cd.sh b/examples/bash_automatic_cd.sh index bdac5757..04b58e24 100644 --- a/examples/bash_automatic_cd.sh +++ b/examples/bash_automatic_cd.sh @@ -6,12 +6,10 @@ # the last visited one after ranger quits. # To undo the effect of this function, you can type "cd -" to return to the # original directory. -# -# On OS X 10 or later, replace `usr/bin/ranger` with `/usr/local/bin/ranger`. function ranger-cd { tempfile="$(mktemp -t tmp.XXXXXX)" - /usr/bin/ranger --choosedir="$tempfile" "${@:-$(pwd)}" + ranger --choosedir="$tempfile" "${@:-$(pwd)}" test -f "$tempfile" && if [ "$(cat -- "$tempfile")" != "$(echo -n `pwd`)" ]; then cd -- "$(cat "$tempfile")" diff --git a/examples/plugin_avfs.py b/examples/plugin_avfs.py new file mode 100644 index 00000000..07968a03 --- /dev/null +++ b/examples/plugin_avfs.py @@ -0,0 +1,33 @@ +# Tested with ranger 1.9.1 +# +# A very simple and possibly buggy support for AVFS +# (http://avf.sourceforge.net/), that allows ranger to handle +# archives. +# +# Run `:avfs' to browse the selected archive. + +from __future__ import (absolute_import, division, print_function) + +import os +import os.path + +from ranger.api.commands import Command + + +class avfs(Command): # pylint: disable=invalid-name + avfs_root = os.path.join(os.environ["HOME"], ".avfs") + avfs_suffix = "#" + + def execute(self): + if os.path.isdir(self.avfs_root): + archive_directory = "".join([ + self.avfs_root, + self.fm.thisfile.path, + self.avfs_suffix, + ]) + if os.path.isdir(archive_directory): + self.fm.cd(archive_directory) + else: + self.fm.notify("This file cannot be handled by avfs.", bad=True) + else: + self.fm.notify("Install `avfs' and run `mountavfs' first.", bad=True) diff --git a/examples/rc_emacs.conf b/examples/rc_emacs.conf index 26074a42..0462282e 100644 --- a/examples/rc_emacs.conf +++ b/examples/rc_emacs.conf @@ -122,6 +122,9 @@ set mouse_enabled true set display_size_in_main_column true set display_size_in_status_bar true +# Display the free disk space in the status bar? +set display_free_space_in_status_bar true + # Display files tags in all columns or only in main column? set display_tags_in_all_columns true @@ -129,7 +132,7 @@ set display_tags_in_all_columns true set update_title false # Set the title to "ranger" in the tmux program? -set update_tmux_title false +set update_tmux_title true # Shorten the title if it gets long? The number defines how many # directories are displayed at once, 0 turns off this feature. diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index c88cdc7c..350c8359 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -138,6 +138,8 @@ class Default(ColorScheme): attr &= ~bold if context.vcsconflict: fg = magenta + elif context.vcsuntracked: + fg = cyan elif context.vcschanged: fg = red elif context.vcsunknown: diff --git a/ranger/config/__init__.py b/ranger/config/__init__.py index 71df3cb3..0facbdf8 100644 --- a/ranger/config/__init__.py +++ b/ranger/config/__init__.py @@ -1 +1 @@ -"""Default options and configration files""" +"""Default options and configuration files""" diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 2f1480b9..7f5fc764 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -3,8 +3,9 @@ # This configuration file is licensed under the same terms as ranger. # =================================================================== # -# NOTE: If you copied this file to ~/.config/ranger/commands_full.py, -# then it will NOT be loaded by ranger, and only serve as a reference. +# NOTE: If you copied this file to /etc/ranger/commands_full.py or +# ~/.config/ranger/commands_full.py, then it will NOT be loaded by ranger, +# and only serve as a reference. # # =================================================================== # This file contains ranger's commands. @@ -13,9 +14,14 @@ # Note that additional commands are automatically generated from the methods # of the class ranger.core.actions.Actions. # -# You can customize commands in the file ~/.config/ranger/commands.py. -# It has the same syntax as this file. In fact, you can just copy this -# file there with `ranger --copy-config=commands' and make your modifications. +# You can customize commands in the files /etc/ranger/commands.py (system-wide) +# and ~/.config/ranger/commands.py (per user). +# They have the same syntax as this file. In fact, you can just copy this +# file to ~/.config/ranger/commands_full.py with +# `ranger --copy-config=commands_full' and make your modifications, don't +# forget to rename it to commands.py. You can also use +# `ranger --copy-config=commands' to copy a short sample commands.py that +# has everything you need to get started. # But make sure you update your configs when you update ranger. # # =================================================================== @@ -1394,6 +1400,8 @@ class scout(Command): self.fm.cd(pattern) else: self.fm.move(right=1) + if self.quickly_executed: + self.fm.block_input(0.5) if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir: # reopen the console: @@ -1719,6 +1727,7 @@ class yank(Command): modes = { '': 'basename', + 'name_without_extension': 'basename_without_extension', 'name': 'basename', 'dir': 'dirname', 'path': 'path', @@ -1751,7 +1760,9 @@ class yank(Command): clipboard_commands = clipboards() - selection = self.get_selection_attr(self.modes[self.arg(1)]) + mode = self.modes[self.arg(1)] + selection = self.get_selection_attr(mode) + new_clipboard_contents = "\n".join(selection) for command in clipboard_commands: process = subprocess.Popen(command, universal_newlines=True, diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index afe9ea42..62331e22 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -1,7 +1,8 @@ # =================================================================== # This file contains the default startup commands for ranger. -# To change them, it is recommended to create the file -# ~/.config/ranger/rc.conf and add your custom commands there. +# To change them, it is recommended to create either /etc/ranger/rc.conf +# (system-wide) or ~/.config/ranger/rc.conf (per user) and add your custom +# commands there. # # If you copy this whole file there, you may want to set the environment # variable RANGER_LOAD_DEFAULT_RC to FALSE to avoid loading it twice. @@ -85,6 +86,10 @@ set preview_images false # width of 8 and height of 11 are used. To use other values, set the options # iterm2_font_width and iterm2_font_height to the desired values. # +# * terminology: +# Previews images in full color in the terminology terminal emulator. +# Supports a wide variety of formats, even vector graphics like svg. +# # * urxvt: # Preview images in full color using urxvt image backgrounds. This # requires using urxvt compiled with pixbuf support. @@ -92,8 +97,21 @@ set preview_images false # * urxvt-full: # The same as urxvt but utilizing not only the preview pane but the # whole terminal window. +# +# * kitty: +# Preview images in full color using kitty image protocol. +# Requires python PIL or pillow library. +# If ranger does not share the local filesystem with kitty +# the transfer method is changed to encode the whole image; +# while slower, this allows remote previews, +# for example during an ssh session. +# Tmux is unsupported. set preview_images_method w3m +# Delay in seconds before displaying an image with the w3m method. +# Increase it in case of experiencing display corruption. +set w3m_delay 0.02 + # Default iTerm2 font size (see: preview_images_method: iterm2) set iterm2_font_width 8 set iterm2_font_height 11 @@ -101,6 +119,10 @@ set iterm2_font_height 11 # Use a unicode "..." character to mark cut-off filenames? set unicode_ellipsis false +# BIDI support - try to properly display file names in RTL languages (Hebrew, Arabic). +# Requires the python-bidi pip package +set bidi_support false + # Show dotfiles in the bookmark preview box? set show_hidden_bookmarks true @@ -137,6 +159,9 @@ set mouse_enabled true set display_size_in_main_column true set display_size_in_status_bar true +# Display the free disk space in the status bar? +set display_free_space_in_status_bar true + # Display files tags in all columns or only in main column? set display_tags_in_all_columns true @@ -144,7 +169,7 @@ set display_tags_in_all_columns true set update_title false # Set the title to "ranger" in the tmux program? -set update_tmux_title false +set update_tmux_title true # Shorten the title if it gets long? The number defines how many # directories are displayed at once, 0 turns off this feature. @@ -236,9 +261,14 @@ set metadata_deep_search false # Clear all existing filters when leaving a directory set clear_filters_on_dir_change false -# Disable displaying line numbers in main column +# Disable displaying line numbers in main column. +# Possible values: false, absolute, relative. set line_numbers false +# When line_numbers=relative show the absolute line number in the +# current line. +set relative_current_zero false + # Start line numbers from 1 instead of 0 set one_indexed false @@ -253,6 +283,10 @@ set wrap_scroll false # directories, files and symlinks respectively. set global_inode_type_filter +# This setting allows to freeze the list of files to save I/O bandwidth. It +# should be 'false' during start-up, but you can toggle it by pressing F. +set freeze_files false + # =================================================================== # == Local Options # =================================================================== @@ -378,6 +412,7 @@ map L history_go 1 map ] move_parent 1 map [ move_parent -1 map } traverse +map { traverse_backwards map ) jump_non map gh cd ~ @@ -389,6 +424,7 @@ map gL cd -r %f map go cd /opt map gv cd /var map gm cd /media +map gi eval fm.cd('/run/media/' + os.getenv('USER')) map gM cd /mnt map gs cd /srv map gp cd /tmp @@ -404,6 +440,7 @@ map dU shell -p du --max-depth=1 -h --apparent-size | sort -rh map yp yank path map yd yank dir map yn yank name +map y. yank name_without_extension # Filesystem Operations map = chmod @@ -507,6 +544,8 @@ map zc set collapse_preview! map zd set sort_directories_first! map zh set show_hidden! map <C-h> set show_hidden! +copymap <C-h> <backspace> +copymap <backspace> <backspace2> map zI set flushinput! map zi set preview_images! map zm set mouse_enabled! diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index e2653a76..5a87c1a5 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -151,7 +151,7 @@ ext pdf, has atril, X, flag f = atril -- "$@" ext pdf, has okular, X, flag f = okular -- "$@" ext pdf, has epdfview, X, flag f = epdfview -- "$@" ext pdf, has qpdfview, X, flag f = qpdfview "$@" -ext pdf, has open, X, flat f = open "$@" +ext pdf, has open, X, flag f = open "$@" ext docx?, has catdoc, terminal = catdoc -- "$@" | "$PAGER" @@ -164,6 +164,7 @@ ext pptx?|od[dfgpst]|docx?|sxc|xlsx?|xlt|xlw|gnm|gnumeric, has ooffice, X, f ext djvu, has zathura,X, flag f = zathura -- "$@" ext djvu, has evince, X, flag f = evince -- "$@" ext djvu, has atril, X, flag f = atril -- "$@" +ext djvu, has djview, X, flag f = djview -- "$@" ext epub, has ebook-viewer, X, flag f = ebook-viewer -- "$@" ext mobi, has ebook-viewer, X, flag f = ebook-viewer -- "$@" @@ -183,6 +184,7 @@ mime ^image, has eog, X, flag f = eog -- "$@" mime ^image, has eom, X, flag f = eom -- "$@" mime ^image, has nomacs, X, flag f = nomacs -- "$@" mime ^image, has geeqie, X, flag f = geeqie -- "$@" +mime ^image, has gwenview, X, flag f = gwenview -- "$@" mime ^image, has gimp, X, flag f = gimp -- "$@" ext xcf, X, flag f = gimp -- "$@" diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 18b1687c..e281e6c9 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -338,8 +338,9 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public dirlist = [ os.path.join("/", dirpath, d) for d in dirnames - if self.flat == -1 or - (dirpath.count(os.path.sep) - mypath.count(os.path.sep)) <= self.flat + if self.flat == -1 + or (dirpath.count(os.path.sep) + - mypath.count(os.path.sep)) <= self.flat ] filelist += dirlist filelist += [os.path.join("/", dirpath, f) for f in filenames] diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index 0c9f70f6..37151ecf 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -6,7 +6,7 @@ from __future__ import (absolute_import, division, print_function) import re from grp import getgrgid from os import lstat, stat -from os.path import abspath, basename, dirname, realpath, relpath +from os.path import abspath, basename, dirname, realpath, relpath, splitext from pwd import getpwuid from time import time @@ -171,6 +171,10 @@ class FileSystemObject( # pylint: disable=too-many-instance-attributes,too-many return basename_list @lazy_property + def basename_without_extension(self): + return splitext(self.basename)[0] + + @lazy_property def safe_basename(self): return self.basename.translate(_SAFE_STRING_TABLE) diff --git a/ranger/container/settings.py b/ranger/container/settings.py index 9f54f24d..94e455a5 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -27,6 +27,7 @@ ALLOWED_SETTINGS = { 'automatically_count_files': bool, 'autosave_bookmarks': bool, 'autoupdate_cumulative_size': bool, + 'bidi_support': bool, 'cd_bookmarks': bool, 'cd_tab_case': str, 'cd_tab_fuzzy': bool, @@ -38,6 +39,7 @@ ALLOWED_SETTINGS = { 'dirname_in_tabs': bool, 'display_size_in_main_column': bool, 'display_size_in_status_bar': bool, + "display_free_space_in_status_bar": bool, 'display_tags_in_all_columns': bool, 'draw_borders': bool, 'draw_progress_bar_in_status_bar': bool, @@ -64,6 +66,7 @@ ALLOWED_SETTINGS = { 'preview_images_method': str, 'preview_max_size': int, 'preview_script': (str, type(None)), + 'relative_current_zero': bool, 'save_backtick_bookmark': bool, 'save_console_history': bool, 'save_tabs_on_exit': bool, @@ -90,6 +93,7 @@ ALLOWED_SETTINGS = { 'vcs_backend_hg': str, 'vcs_backend_svn': str, 'viewmode': str, + 'w3m_delay': float, 'wrap_scroll': bool, 'xterm_alt_key': bool, } @@ -99,7 +103,8 @@ ALLOWED_VALUES = { 'confirm_on_delete': ['multiple', 'always', 'never'], 'line_numbers': ['false', 'absolute', 'relative'], 'one_indexed': [False, True], - 'preview_images_method': ['w3m', 'iterm2', 'urxvt', 'urxvt-full'], + 'preview_images_method': ['w3m', 'iterm2', 'terminology', + 'urxvt', 'urxvt-full', 'kitty'], 'vcs_backend_bzr': ['disabled', 'local', 'enabled'], 'vcs_backend_git': ['enabled', 'disabled', 'local'], 'vcs_backend_hg': ['disabled', 'local', 'enabled'], @@ -112,6 +117,7 @@ DEFAULT_VALUES = { type(None): None, str: "", int: 0, + float: 0.0, list: [], tuple: tuple([]), } diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 6ddcdd86..86927a6e 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -110,7 +110,8 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.settings.set(option_name, self._parse_option_value(option_name, value), localpath, tags) - def _parse_option_value(self, name, value): + def _parse_option_value( # pylint: disable=too-many-return-statements + self, name, value): types = self.fm.settings.types_of(name) if bool in types: if value.lower() in ('false', 'off', '0'): @@ -124,6 +125,11 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m return int(value) except ValueError: pass + if float in types: + try: + return float(value) + except ValueError: + pass if str in types: return value if list in types: @@ -409,7 +415,8 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m # ranger can act as a file chooser when running with --choosefile=... if mode == 0 and 'label' not in kw: if ranger.args.choosefile: - open(ranger.args.choosefile, 'w').write(self.fm.thisfile.path) + with open(ranger.args.choosefile, 'w') as fobj: + fobj.write(self.fm.thisfile.path) if ranger.args.choosefiles: paths = [] @@ -608,6 +615,23 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m self.move(down=1) self.traverse() + def traverse_backwards(self): + self.change_mode('normal') + if self.thisdir.pointer == 0: + self.move(left=1) + if self.thisdir.pointer != 0: + self.traverse_backwards() + else: + self.move(up=1) + while True: + if self.thisfile is not None and self.thisfile.is_directory: + self.enter_dir(self.thisfile.path) + self.move(to=100, percentage=True) + elif self.thisdir.pointer == 0: + break + else: + self.move(up=1) + # -------------------------- # -- Shortcuts / Wrappers # -------------------------- @@ -981,6 +1005,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m if not self.settings.preview_script or not self.settings.use_preview_script: try: + # XXX: properly determine file's encoding return codecs.open(path, 'r', errors='ignore') # IOError for Python2, OSError for Python3 except (IOError, OSError): @@ -1066,14 +1091,11 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m data[(-1, -1)] = None data['foundpreview'] = False elif rcode == 2: - fobj = codecs.open(path, 'r', errors='ignore') - try: - data[(-1, -1)] = fobj.read(1024 * 32) - except UnicodeDecodeError: - fobj.close() - fobj = codecs.open(path, 'r', encoding='latin-1', errors='ignore') - data[(-1, -1)] = fobj.read(1024 * 32) - fobj.close() + text = self.read_text_file(path, 1024 * 32) + if not isinstance(text, str): + # Convert 'unicode' to 'str' in Python 2 + text = text.encode('utf-8') + data[(-1, -1)] = text else: data[(-1, -1)] = None @@ -1114,6 +1136,35 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m return None + @staticmethod + def read_text_file(path, count=None): + """Encoding-aware reading of a text file.""" + try: + import chardet + except ImportError: + # Guess encoding ourselves. These should be the most frequently used ones. + encodings = ('utf-8', 'utf-16') + for encoding in encodings: + try: + with codecs.open(path, 'r', encoding=encoding) as fobj: + text = fobj.read(count) + except UnicodeDecodeError: + pass + else: + LOG.debug("guessed encoding of '%s' as %r", path, encoding) + return text + else: + with open(path, 'rb') as fobj: + data = fobj.read(count) + result = chardet.detect(data) + LOG.debug("chardet guess for '%s': %s", path, result) + guessed_encoding = result['encoding'] + return codecs.decode(data, guessed_encoding, 'replace') + + # latin-1 as the last resort + with codecs.open(path, 'r', encoding='latin-1', errors='replace') as fobj: + return fobj.read(count) + # -------------------------- # -- Tabs # -------------------------- @@ -1189,10 +1240,10 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m def tab_new(self, path=None, narg=None): if narg: return self.tab_open(narg, path) - for i in range(1, 10): - if i not in self.tabs: - return self.tab_open(i, path) - return None + i = 1 + while i in self.tabs: + i += 1 + return self.tab_open(i, path) def tab_switch(self, path, create_directory=False): """Switches to tab of given path, opening a new tab as necessary. @@ -1240,7 +1291,18 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m def get_tab_list(self): assert self.tabs, "There must be at least 1 tab at all times" - return sorted(self.tabs) + + class NaturalOrder(object): # pylint: disable=too-few-public-methods + def __init__(self, obj): + self.obj = obj + + def __lt__(self, other): + try: + return self.obj < other.obj + except TypeError: + return str(self.obj) < str(other.obj) + + return sorted(self.tabs, key=NaturalOrder) # -------------------------- # -- Overview of internals diff --git a/ranger/core/fm.py b/ranger/core/fm.py index c55a3922..61b3cb11 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -24,7 +24,9 @@ from ranger.container.bookmarks import Bookmarks from ranger.core.runner import Runner from ranger.ext.img_display import (W3MImageDisplayer, ITerm2ImageDisplayer, TerminologyImageDisplayer, - URXVTImageDisplayer, URXVTImageFSDisplayer, ImageDisplayer) + URXVTImageDisplayer, URXVTImageFSDisplayer, + KittyImageDisplayer, + ImageDisplayer) from ranger.core.metadata import MetadataManager from ranger.ext.rifle import Rifle from ranger.container.directory import Directory @@ -223,7 +225,7 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes for line in entry.splitlines(): yield line - def _get_image_displayer(self): + def _get_image_displayer(self): # pylint: disable=too-many-return-statements if self.settings.preview_images_method == "w3m": return W3MImageDisplayer() elif self.settings.preview_images_method == "iterm2": @@ -234,6 +236,8 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes return URXVTImageDisplayer() elif self.settings.preview_images_method == "urxvt-full": return URXVTImageFSDisplayer() + elif self.settings.preview_images_method == "kitty": + return KittyImageDisplayer() return ImageDisplayer() def _get_thisfile(self): @@ -424,5 +428,5 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes if not ranger.args.clean and self.settings.save_tabs_on_exit and len(self.tabs) > 1: with open(self.datapath('tabs'), 'a') as fobj: # Don't save active tab since launching ranger changes the active tab - fobj.write('\0'.join(v.path for t, v in self.tabs.items() - if t != self.current_tab) + '\0\0') + fobj.write('\0'.join(v.path for t, v in self.tabs.items()) + + '\0\0') diff --git a/ranger/core/main.py b/ranger/core/main.py index 2ae4d16b..598ce243 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -94,10 +94,7 @@ def main( args.selectfile = os.path.abspath(args.selectfile) args.paths.insert(0, os.path.dirname(args.selectfile)) - if args.paths: - paths = [p[7:] if p.startswith('file:///') else p for p in args.paths] - else: - paths = [os.environ.get('PWD', os.getcwd())] + paths = get_paths(args) paths_inaccessible = [] for path in paths: try: @@ -183,6 +180,7 @@ def main( fm.select_file(args.selectfile) if args.cmd: + fm.enter_dir(fm.thistab.path) for command in args.cmd: fm.execute_console(command) @@ -236,6 +234,24 @@ https://github.com/ranger/ranger/issues return exit_code # pylint: disable=lost-exception +def get_paths(args): + if args.paths: + prefix = 'file:///' + prefix_length = len(prefix) + paths = [path[prefix_length:] if path.startswith(prefix) else path for path in args.paths] + else: + start_directory = os.environ.get('PWD') + is_valid_start_directory = start_directory and os.path.exists(start_directory) + if not is_valid_start_directory: + start_directory = __get_home_directory() + paths = [start_directory] + return paths + + +def __get_home_directory(): + return os.path.expanduser('~') + + def xdg_path(env_var): path = os.environ.get(env_var) if path and os.path.isabs(path): @@ -340,23 +356,50 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many fm.commands.load_commands_from_module(commands_default) if not clean: + system_confdir = os.path.join(os.sep, 'etc', 'ranger') + if os.path.exists(system_confdir): + sys.path.append(system_confdir) allow_access_to_confdir(ranger.args.confdir, True) # Load custom commands - custom_comm_path = fm.confpath('commands.py') - if os.path.exists(custom_comm_path): + def import_file(name, path): # From https://stackoverflow.com/a/67692 + # pragma pylint: disable=no-name-in-module,import-error,no-member, deprecated-method + if sys.version_info >= (3, 5): + import importlib.util as util + spec = util.spec_from_file_location(name, path) + module = util.module_from_spec(spec) + spec.loader.exec_module(module) + elif (3, 3) <= sys.version_info < (3, 5): + from importlib.machinery import SourceFileLoader + module = SourceFileLoader(name, path).load_module() + else: + import imp + module = imp.load_source(name, path) + # pragma pylint: enable=no-name-in-module,import-error,no-member + return module + + def load_custom_commands(*paths): old_bytecode_setting = sys.dont_write_bytecode sys.dont_write_bytecode = True - try: - import commands as commands_custom - fm.commands.load_commands_from_module(commands_custom) - except ImportError as ex: - LOG.debug("Failed to import custom commands from '%s'", custom_comm_path) - LOG.exception(ex) - else: - LOG.debug("Loaded custom commands from '%s'", custom_comm_path) + for custom_comm_path in paths: + if os.path.exists(custom_comm_path): + try: + commands_custom = import_file('commands', + custom_comm_path) + fm.commands.load_commands_from_module(commands_custom) + except ImportError as ex: + LOG.debug("Failed to import custom commands from '%s'", + custom_comm_path) + LOG.exception(ex) + else: + LOG.debug("Loaded custom commands from '%s'", + custom_comm_path) sys.dont_write_bytecode = old_bytecode_setting + system_comm_path = os.path.join(system_confdir, 'commands.py') + custom_comm_path = fm.confpath('commands.py') + load_custom_commands(system_comm_path, custom_comm_path) + # XXX Load plugins (experimental) plugindir = fm.confpath('plugins') try: @@ -395,12 +438,16 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many allow_access_to_confdir(ranger.args.confdir, False) # Load rc.conf custom_conf = fm.confpath('rc.conf') + system_conf = os.path.join(system_confdir, 'rc.conf') default_conf = fm.relpath('config', 'rc.conf') custom_conf_is_readable = os.access(custom_conf, os.R_OK) - if (os.environ.get('RANGER_LOAD_DEFAULT_RC', 'TRUE').upper() != 'FALSE' or - not custom_conf_is_readable): + system_conf_is_readable = os.access(system_conf, os.R_OK) + if (os.environ.get('RANGER_LOAD_DEFAULT_RC', 'TRUE').upper() != 'FALSE' + or not (custom_conf_is_readable or system_conf_is_readable)): fm.source(default_conf) + if system_conf_is_readable: + fm.source(system_conf) if custom_conf_is_readable: fm.source(custom_conf) diff --git a/ranger/core/runner.py b/ranger/core/runner.py index bb4e512a..f38b026a 100644 --- a/ranger/core/runner.py +++ b/ranger/core/runner.py @@ -235,7 +235,7 @@ class Runner(object): # pylint: disable=too-few-public-methods self.fm.signal_emit('runner.execute.before', popen_kws=popen_kws, context=context) try: - if 'f' in context.flags: + if 'f' in context.flags and 'r' not in context.flags: # This can fail and return False if os.fork() is not # supported, but we assume it is, since curses is used. Popen_forked(**popen_kws) diff --git a/ranger/core/tab.py b/ranger/core/tab.py index 7bb45d75..1d5e69d4 100644 --- a/ranger/core/tab.py +++ b/ranger/core/tab.py @@ -4,7 +4,7 @@ from __future__ import (absolute_import, division, print_function) import os -from os.path import abspath, normpath, join, expanduser, isdir, dirname +from os.path import abspath, normpath, join, expanduser, isdir import sys from ranger.container import settings @@ -123,11 +123,9 @@ class Tab(FileManagerAware, SettingsAware): # pylint: disable=too-many-instance # get the absolute path path = normpath(join(self.path, expanduser(path))) - selectfile = None if not isdir(path): - selectfile = path - path = dirname(path) + return False new_thisdir = self.fm.get_directory(path) try: @@ -157,8 +155,6 @@ class Tab(FileManagerAware, SettingsAware): # pylint: disable=too-many-instance self.thisdir.sort_directories_first = self.fm.settings.sort_directories_first self.thisdir.sort_reverse = self.fm.settings.sort_reverse self.thisdir.sort_if_outdated() - if selectfile: - self.thisdir.move_to_obj(selectfile) if previous and previous.path != path: self.thisfile = self.thisdir.pointed_obj else: diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh index 540a910e..25251533 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -60,7 +60,8 @@ handle_extension() { # PDF pdf) # Preview as text conversion - pdftotext -l 10 -nopgbrk -q -- "${FILE_PATH}" - && exit 5 + pdftotext -l 10 -nopgbrk -q -- "${FILE_PATH}" - | fmt -w ${PV_WIDTH} && exit 5 + mutool draw -F txt -i -- "${FILE_PATH}" 1-10 | fmt -w ${PV_WIDTH} && exit 5 exiftool "${FILE_PATH}" && exit 5 exit 1;; diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py index 7df45556..33ebb604 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -97,8 +97,8 @@ class Direction(dict): return self.get('cycle') in (True, 'true', 'on', 'yes') def one_indexed(self): - return ('one_indexed' in self and - self.get('one_indexed') in (True, 'true', 'on', 'yes')) + return ('one_indexed' in self + and self.get('one_indexed') in (True, 'true', 'on', 'yes')) def multiply(self, n): for key in ('up', 'right', 'down', 'left'): diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index df74eabf..f365e594 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -15,6 +15,10 @@ def human_readable(byte, separator=' '): # pylint: disable=too-many-return-stat '1023 M' """ + # handle automatically_count_files false + if byte is None: + return '' + # I know this can be written much shorter, but this long version # performs much better than what I had before. If you attempt to # shorten this code, take performance into consideration. diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 67941e27..f78e170b 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -19,9 +19,13 @@ import imghdr import os import struct import sys +import warnings from subprocess import Popen, PIPE import termios +from contextlib import contextmanager +import codecs +from tempfile import NamedTemporaryFile from ranger.core.shared import FileManagerAware @@ -35,6 +39,28 @@ W3MIMGDISPLAY_PATHS = [ '/usr/local/libexec/w3m/w3mimgdisplay', ] +# Helper functions shared between the previewers (make them static methods of the base class?) + + +@contextmanager +def temporarily_moved_cursor(to_y, to_x): + """Common boilerplate code to move the cursor to a drawing area. Use it as: + with temporarily_moved_cursor(dest_y, dest_x): + your_func_here()""" + curses.putp(curses.tigetstr("sc")) + move_cur(to_y, to_x) + yield + curses.putp(curses.tigetstr("rc")) + sys.stdout.flush() + + +# this is excised since Terminology needs to move the cursor multiple times +def move_cur(to_y, to_x): + tparm = curses.tparm(curses.tigetstr("cup"), to_y, to_x) + # on python2 stdout is already in binary mode, in python3 is accessed via buffer + bin_stdout = getattr(sys.stdout, 'buffer', sys.stdout) + bin_stdout.write(tparm) + class ImageDisplayError(Exception): pass @@ -60,7 +86,7 @@ class ImageDisplayer(object): pass -class W3MImageDisplayer(ImageDisplayer): +class W3MImageDisplayer(ImageDisplayer, FileManagerAware): """Implementation of ImageDisplayer using w3mimgdisplay, an utilitary program from w3m (a text-based web browser). w3mimgdisplay can display images either in virtual tty (using linux framebuffer) or in a Xorg session. @@ -119,6 +145,14 @@ class W3MImageDisplayer(ImageDisplayer): input_gen = self._generate_w3m_input(path, start_x, start_y, width, height) except ImageDisplayError: raise + + # Mitigate the issue with the horizontal black bars when + # selecting some images on some systems. 2 milliseconds seems + # enough. Adjust as necessary. + if self.fm.settings.w3m_delay > 0: + from time import sleep + sleep(self.fm.settings.w3m_delay) + self.process.stdin.write(input_gen) self.process.stdin.flush() self.process.stdout.readline() @@ -210,15 +244,8 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): """ def draw(self, path, start_x, start_y, width, height): - curses.putp(curses.tigetstr("sc")) - tparm = curses.tparm(curses.tigetstr("cup"), start_y, start_x) - if sys.version_info[0] < 3: - sys.stdout.write(tparm) - else: - sys.stdout.buffer.write(tparm) # pylint: disable=no-member - sys.stdout.write(self._generate_iterm2_input(path, width, height)) - curses.putp(curses.tigetstr("rc")) - sys.stdout.flush() + with temporarily_moved_cursor(start_y, start_x): + sys.stdout.write(self._generate_iterm2_input(path, width, height)) def clear(self, start_x, start_y, width, height): self.fm.ui.win.redrawwin() @@ -327,44 +354,23 @@ class TerminologyImageDisplayer(ImageDisplayer, FileManagerAware): self.close_protocol = "\000" def draw(self, path, start_x, start_y, width, height): - # Save cursor - curses.putp(curses.tigetstr("sc")) - - y = start_y - # Move to drawing zone - self._move_to(start_x, y) - - # Write intent - sys.stdout.write("%s}ic#%d;%d;%s%s" % ( - self.display_protocol, - width, height, - path, - self.close_protocol)) - - # Write Replacement commands ('#') - for _ in range(0, height): - sys.stdout.write("%s}ib%s%s%s}ie%s" % ( - self.display_protocol, - self.close_protocol, - "#" * width, + with temporarily_moved_cursor(start_y, start_x): + # Write intent + sys.stdout.write("%s}ic#%d;%d;%s%s" % ( self.display_protocol, + width, height, + path, self.close_protocol)) - y = y + 1 - self._move_to(start_x, y) - - # Restore cursor - curses.putp(curses.tigetstr("rc")) - - sys.stdout.flush() - @staticmethod - def _move_to(x, y): - # curses.move(y, x) - tparm = curses.tparm(curses.tigetstr("cup"), y, x) - if sys.version_info[0] < 3: - sys.stdout.write(tparm) - else: - sys.stdout.buffer.write(tparm) # pylint: disable=no-member + # Write Replacement commands ('#') + for y in range(0, height): + move_cur(start_y + y, start_x) + sys.stdout.write("%s}ib%s%s%s}ie%s\n" % ( # needs a newline to work + self.display_protocol, + self.close_protocol, + "#" * width, + self.display_protocol, + self.close_protocol)) def clear(self, start_x, start_y, width, height): self.fm.ui.win.redrawwin() @@ -434,20 +440,20 @@ class URXVTImageDisplayer(ImageDisplayer, FileManagerAware): pct_width, pct_height = self._get_sizes() sys.stdout.write( - self.display_protocol + - path + - ";{pct_width}x{pct_height}+{pct_x}+{pct_y}:op=keep-aspect".format( + self.display_protocol + + path + + ";{pct_width}x{pct_height}+{pct_x}+{pct_y}:op=keep-aspect".format( pct_width=pct_width, pct_height=pct_height, pct_x=pct_x, pct_y=pct_y - ) + - self.close_protocol + ) + + self.close_protocol ) sys.stdout.flush() def clear(self, start_x, start_y, width, height): sys.stdout.write( - self.display_protocol + - ";100x100+1000+1000" + - self.close_protocol + self.display_protocol + + ";100x100+1000+1000" + + self.close_protocol ) sys.stdout.flush() @@ -465,3 +471,196 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer): def _get_offsets(self): """Center the image.""" return self._get_centered_offsets() + + +class KittyImageDisplayer(ImageDisplayer): + """Implementation of ImageDisplayer for kitty (https://github.com/kovidgoyal/kitty/) + terminal. It uses the built APC to send commands and data to kitty, + which in turn renders the image. The APC takes the form + '\033_Gk=v,k=v...;bbbbbbbbbbbbbb\033\\' + | ---------- -------------- | + escape code | | escape code + | base64 encoded payload + key: value pairs as parameters + For more info please head over to : + https://github.com/kovidgoyal/kitty/blob/master/graphics-protocol.asciidoc""" + protocol_start = b'\x1b_G' + protocol_end = b'\x1b\\' + # we are going to use stdio in binary mode a lot, so due to py2 -> py3 + # differnces is worth to do this: + stdbout = getattr(sys.stdout, 'buffer', sys.stdout) + stdbin = getattr(sys.stdin, 'buffer', sys.stdin) + # counter for image ids on kitty's end + image_id = 0 + # we need to find out the encoding for a path string, ascii won't cut it + try: + fsenc = sys.getfilesystemencoding() # returns None if standard utf-8 is used + # throws LookupError if can't find the codec, TypeError if fsenc is None + codecs.lookup(fsenc) + except (LookupError, TypeError): + fsenc = 'utf-8' + + def __init__(self): + # the rest of the initializations that require reading stdio or raising exceptions + # are delayed to the first draw call, since curses + # and ranger exception handler are not online at __init__() time + self.needs_late_init = True + # to init in _late_init() + self.backend = None + self.stream = None + self.pix_row, self.pix_col = (0, 0) + + def _late_init(self): + # tmux + if 'kitty' not in os.environ['TERM']: + # this doesn't seem to work, ranger freezes... + # commenting out the response check does nothing + # self.protocol_start = b'\033Ptmux;\033' + self.protocol_start + # self.protocol_end += b'\033\\' + raise ImgDisplayUnsupportedException( + 'kitty previews only work in' + + ' kitty and outside tmux. ' + + 'Make sure your TERM contains the string "kitty"') + + # automatic check if we share the filesystem using a dummy file + with NamedTemporaryFile() as tmpf: + tmpf.write(bytearray([0xFF] * 3)) + tmpf.flush() + for cmd in self._format_cmd_str( + {'a': 'q', 'i': 1, 'f': 24, 't': 'f', 's': 1, 'v': 1, 'S': 3}, + payload=base64.standard_b64encode(tmpf.name.encode(self.fsenc))): + self.stdbout.write(cmd) + sys.stdout.flush() + resp = b'' + while resp[-2:] != self.protocol_end: + resp += self.stdbin.read(1) + # set the transfer method based on the response + # if resp.find(b'OK') != -1: + if b'OK' in resp: + self.stream = False + elif b'EBADF' in resp: + self.stream = True + else: + raise ImgDisplayUnsupportedException( + 'kitty replied an unexpected response: {}'.format(resp)) + + # get the image manipulation backend + try: + # pillow is the default since we are not going + # to spawn other processes, so it _should_ be faster + import PIL.Image + self.backend = PIL.Image + except ImportError: + raise ImageDisplayError("Image previews in kitty require PIL (pillow)") + # TODO: implement a wrapper class for Imagemagick process to + # replicate the functionality we use from im + + # get dimensions of a cell in pixels + ret = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0)) + n_cols, n_rows, x_px_tot, y_px_tot = struct.unpack('HHHH', ret) + self.pix_row, self.pix_col = x_px_tot // n_rows, y_px_tot // n_cols + self.needs_late_init = False + + def draw(self, path, start_x, start_y, width, height): + self.image_id += 1 + # dictionary to store the command arguments for kitty + # a is the display command, with T going for immediate output + # i is the id entifier for the image + cmds = {'a': 'T', 'i': self.image_id} + # sys.stderr.write('{}-{}@{}x{}\t'.format(start_x, start_y, width, height)) + + # finish initialization if it is the first call + if self.needs_late_init: + self._late_init() + + with warnings.catch_warnings(record=True): # as warn: + warnings.simplefilter('ignore', self.backend.DecompressionBombWarning) + image = self.backend.open(path) + # TODO: find a way to send a message to the user that + # doesn't stop the image from displaying + # if warn: + # raise ImageDisplayError(str(warn[-1].message)) + box = (width * self.pix_row, height * self.pix_col) + + if image.width > box[0] or image.height > box[1]: + scale = min(box[0] / image.width, box[1] / image.height) + image = image.resize((int(scale * image.width), int(scale * image.height)), + self.backend.LANCZOS) + + # start_x += ((box[0] - image.width) // 2) // self.pix_row + # start_y += ((box[1] - image.height) // 2) // self.pix_col + if self.stream: + # encode the whole image as base64 + # TODO: implement z compression + # to possibly increase resolution in sent image + if image.mode != 'RGB' and image.mode != 'RGBA': + image = image.convert('RGB') + # t: transmissium medium, 'd' for embedded + # f: size of a pixel fragment (8bytes per color) + # s, v: size of the image to recompose the flattened data + # c, r: size in cells of the viewbox + cmds.update({'t': 'd', 'f': len(image.getbands()) * 8, + 's': image.width, 'v': image.height, }) + payload = base64.standard_b64encode( + bytearray().join(map(bytes, image.getdata()))) + else: + # put the image in a temporary png file + # t: transmissium medium, 't' for temporary file (kitty will delete it for us) + # f: size of a pixel fragment (100 just mean that the file is png encoded, + # the only format except raw RGB(A) bitmap that kitty understand) + # c, r: size in cells of the viewbox + cmds.update({'t': 't', 'f': 100, }) + with NamedTemporaryFile(prefix='ranger_thumb_', suffix='.png', delete=False) as tmpf: + image.save(tmpf, format='png', compress_level=0) + payload = base64.standard_b64encode(tmpf.name.encode(self.fsenc)) + + with temporarily_moved_cursor(int(start_y), int(start_x)): + for cmd_str in self._format_cmd_str(cmds, payload=payload): + self.stdbout.write(cmd_str) + # catch kitty answer before the escape codes corrupt the console + resp = b'' + while resp[-2:] != self.protocol_end: + resp += self.stdbin.read(1) + if b'OK' in resp: + return + else: + raise ImageDisplayError('kitty replied "{}"'.format(resp)) + + def clear(self, start_x, start_y, width, height): + # let's assume that every time ranger call this + # it actually wants just to remove the previous image + # TODO: implement this using the actual x, y, since the protocol supports it + cmds = {'a': 'd', 'i': self.image_id} + for cmd_str in self._format_cmd_str(cmds): + self.stdbout.write(cmd_str) + self.stdbout.flush() + # kitty doesn't seem to reply on deletes, checking like we do in draw() + # will slows down scrolling with timeouts from select + self.image_id -= 1 + + def _format_cmd_str(self, cmd, payload=None, max_slice_len=2048): + central_blk = ','.join(["{}={}".format(k, v) for k, v in cmd.items()]).encode('ascii') + if payload is not None: + # we add the m key to signal a multiframe communication + # appending the end (m=0) key to a single message has no effect + while len(payload) > max_slice_len: + payload_blk, payload = payload[:max_slice_len], payload[max_slice_len:] + yield self.protocol_start + \ + central_blk + b',m=1;' + payload_blk + \ + self.protocol_end + yield self.protocol_start + \ + central_blk + b',m=0;' + payload + \ + self.protocol_end + else: + yield self.protocol_start + central_blk + b';' + self.protocol_end + + def quit(self): + # clear all remaining images, then check if all files went through or are orphaned + while self.image_id >= 1: + self.clear(0, 0, 0, 0) + # for k in self.temp_paths: + # try: + # os.remove(self.temp_paths[k]) + # except (OSError, IOError): + # continue diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index 70215039..07a76488 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -261,6 +261,14 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes process = Popen(["file", "--mime-type", "-Lb", fname], stdout=PIPE, stderr=PIPE) mimetype, _ = process.communicate() self._mimetype = mimetype.decode(ENCODING).strip() + if self._mimetype == 'application/octet-stream': + try: + process = Popen(["mimetype", "--output-format", "%m", fname], + stdout=PIPE, stderr=PIPE) + mimetype, _ = process.communicate() + self._mimetype = mimetype.decode(ENCODING).strip() + except OSError: + pass return self._mimetype def _build_command(self, files, action, flags): @@ -349,7 +357,7 @@ class Rifle(object): # pylint: disable=too-many-instance-attributes self.hook_before_executing(command, self._mimetype, self._app_flags) try: if 'r' in flags: - prefix = ['sudo', '-E', 'su', '-mc'] + prefix = ['sudo', '-E', 'su', 'root', '-mc'] else: prefix = ['/bin/sh', '-c'] diff --git a/ranger/gui/context.py b/ranger/gui/context.py index d8d1957c..96849686 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -23,7 +23,7 @@ CONTEXT_KEYS = [ 'keybuffer', 'infostring', 'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit', 'vcsdate', - 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', + 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', 'vcsuntracked', 'vcsstaged', 'vcssync', 'vcsnone', 'vcsbehind', 'vcsahead', 'vcsdiverged' ] diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 990db0ad..4f76dfab 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -113,7 +113,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method self._draw_title = curses.tigetflag('hs') # has_status_line # Save tmux setting `automatic-rename` - if self.settings.update_tmux_title: + if self.settings.update_tmux_title and 'TMUX' in os.environ: try: self._tmux_automatic_rename = check_output( ['tmux', 'show-window-options', '-v', 'automatic-rename']).strip() @@ -123,7 +123,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method self.update_size() self.is_on = True - if self.settings.update_tmux_title: + if self.settings.update_tmux_title and 'TMUX' in os.environ: sys.stdout.write("\033kranger\033\\") sys.stdout.flush() @@ -172,7 +172,7 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method DisplayableContainer.destroy(self) # Restore tmux setting `automatic-rename` - if self.settings.update_tmux_title: + if self.settings.update_tmux_title and 'TMUX' in os.environ: if self._tmux_automatic_rename: try: check_output(['tmux', 'set-window-option', diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index 36292103..c8f1262b 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -12,11 +12,11 @@ class Widget(Displayable): 'conflict': ( 'X', ['vcsconflict']), 'untracked': ( - '+', ['vcschanged']), + '?', ['vcsuntracked']), 'deleted': ( '-', ['vcschanged']), 'changed': ( - '*', ['vcschanged']), + '+', ['vcschanged']), 'staged': ( '*', ['vcsstaged']), 'ignored': ( @@ -26,7 +26,7 @@ class Widget(Displayable): 'none': ( ' ', []), 'unknown': ( - '?', ['vcsunknown']), + '!', ['vcsunknown']), } vcsremotestatus_symb = { @@ -41,7 +41,7 @@ class Widget(Displayable): 'none': ( '⌂', ['vcsnone']), 'unknown': ( - '?', ['vcsunknown']), + '!', ['vcsunknown']), } ellipsis = {False: '~', True: '…'} diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index b3272cbc..bc6f7b1b 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -10,6 +10,12 @@ import stat from time import time from os.path import splitext +try: + from bidi.algorithm import get_display # pylint: disable=import-error + HAVE_BIDI = True +except ImportError: + HAVE_BIDI = False + from ranger.ext.widestring import WideString from ranger.core import linemode @@ -183,12 +189,12 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes def _draw_file(self): """Draw a preview of the file, if the settings allow it""" self.win.move(0, 0) - if not self.target.accessible: - self.addnstr("not accessible", self.wid) + if self.target is None or not self.target.has_preview(): Pager.close(self) return - if self.target is None or not self.target.has_preview(): + if not self.target.accessible: + self.addnstr("not accessible", self.wid) Pager.close(self) return @@ -206,7 +212,7 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes line_number = i if self.settings.line_numbers == 'relative': line_number = abs(selected_i - i) - if line_number == 0: + if not self.settings.relative_current_zero and line_number == 0: if self.settings.one_indexed: line_number = selected_i + 1 else: @@ -318,8 +324,8 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes text = current_linemode.filetitle(drawn, metadata) - if drawn.marked and (self.main_column or - self.settings.display_tags_in_all_columns): + if drawn.marked and (self.main_column + or self.settings.display_tags_in_all_columns): text = " " + text # Computing predisplay data. predisplay contains a list of lists @@ -410,9 +416,15 @@ class BrowserColumn(Pager): # pylint: disable=too-many-instance-attributes def _total_len(predisplay): return sum([len(WideString(s)) for s, _ in predisplay]) + def _bidi_transpose(self, text): + if self.settings.bidi_support and HAVE_BIDI: + return get_display(text) + return text + def _draw_text_display(self, text, space): - wtext = WideString(text) - wext = WideString(splitext(text)[1]) + bidi_text = self._bidi_transpose(text) + wtext = WideString(bidi_text) + wext = WideString(splitext(bidi_text)[1]) wellip = WideString(self.ellipsis[self.settings.unicode_ellipsis]) if len(wtext) > space: wtext = wtext[:max(1, space - len(wext) - len(wellip))] + wellip + wext diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index 42adf1e9..d64d4ac1 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -109,8 +109,9 @@ class Pager(Widget): # pylint: disable=too-many-instance-attributes try: self.fm.image_displayer.draw(self.image, self.x, self.y, self.wid, self.hei) - except ImgDisplayUnsupportedException: + except ImgDisplayUnsupportedException as ex: self.fm.settings.preview_images = False + self.fm.notify(ex, bad=True) except Exception as ex: # pylint: disable=broad-except self.fm.notify(ex, bad=True) else: @@ -233,7 +234,7 @@ class Pager(Widget): # pylint: disable=too-many-instance-attributes def _generate_lines(self, starty, startx): i = starty if not self.source: - raise StopIteration + return while True: try: line = self._get_line(i).expandtabs(4) @@ -243,5 +244,5 @@ class Pager(Widget): # pylint: disable=too-many-instance-attributes line = line[startx:self.wid + startx] yield line.rstrip().replace('\r\n', '\n') except IndexError: - raise StopIteration + return i += 1 diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 266d48ca..3457955e 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -275,13 +275,14 @@ class StatusBar(Widget): # pylint: disable=too-many-instance-attributes right.add("/" + str(len(target.marked_items))) else: right.add(human_readable(target.disk_usage, separator='') + " sum") - try: - free = get_free_space(target.mount_path) - except OSError: - pass - else: - right.add(", ", "space") - right.add(human_readable(free, separator='') + " free") + if self.settings.display_free_space_in_status_bar: + try: + free = get_free_space(target.mount_path) + except OSError: + pass + else: + right.add(", ", "space") + right.add(human_readable(free, separator='') + " free") right.add(" ", "space") if target.marked_items: diff --git a/ranger/gui/widgets/view_base.py b/ranger/gui/widgets/view_base.py index 80061004..4493443e 100644 --- a/ranger/gui/widgets/view_base.py +++ b/ranger/gui/widgets/view_base.py @@ -72,8 +72,8 @@ class ViewBase(Widget, DisplayableContainer): # pylint: disable=too-many-instan sorted_bookmarks = sorted( ( item for item in self.fm.bookmarks - if self.fm.settings.show_hidden_bookmarks or - '/.' not in item[1].path + if self.fm.settings.show_hidden_bookmarks + or '/.' not in item[1].path ), key=lambda t: t[0].lower(), ) diff --git a/setup.py b/setup.py index d7c54b00..edf48c4a 100755 --- a/setup.py +++ b/setup.py @@ -111,7 +111,7 @@ def main(): 'doc/rifle.1', ]), ('share/doc/ranger', [ - 'doc/colorschemes.txt', + 'doc/colorschemes.md', 'CHANGELOG.md', 'HACKING.md', 'README.md', diff --git a/tests/ranger/core/__init__.py b/tests/ranger/core/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/ranger/core/__init__.py diff --git a/tests/ranger/core/test_main.py b/tests/ranger/core/test_main.py new file mode 100644 index 00000000..d992b8a7 --- /dev/null +++ b/tests/ranger/core/test_main.py @@ -0,0 +1,18 @@ +import collections +import os + +from ranger.core import main + + +def test_get_paths(): + args_tuple = collections.namedtuple('args', 'paths') + args = args_tuple(paths=None) + + paths = main.get_paths(args) + + for path in paths: + assert os.path.exists(path) + + +if __name__ == '__main__': + test_get_paths() |