diff options
36 files changed, 1455 insertions, 265 deletions
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/Makefile b/Makefile index 47e2fa01..79b4f9d4 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,16 @@ NAME_RIFLE = rifle VERSION_RIFLE = $(VERSION) SNAPSHOT_NAME ?= $(NAME)-$(VERSION)-$(shell git rev-parse HEAD | cut -b 1-8).tar.gz # Find suitable python version (need python >= 2.6 or 3.1): -PYTHON ?= $(shell python -c 'import sys; sys.exit(sys.version < "2.6")' && \ - which python || which python3.3 || which python3.2 || which python3.1 || \ - which python3 || which python2.7 || which python2.6) +PYTHON ?= $(shell \ + (python -c 'import sys; sys.exit(sys.version < "2.6")' && \ + which python) \ + || (which python3) \ + || (python2 -c 'import sys; sys.exit(sys.version < "2.6")' && \ + which python2) \ + ) +ifeq ($(PYTHON),) + $(error No suitable python found.) +endif SETUPOPTS ?= '--record=install_log.txt' DOCDIR ?= doc/pydoc DESTDIR ?= / @@ -98,7 +105,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 +133,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 df17f731..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`): @@ -79,6 +80,7 @@ Optional, for enhanced file previews (with `scope.sh`): * `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/howto-publish-a-release.md b/doc/howto-publish-a-release.md index 572a2eb7..e8d669ce 100644 --- a/doc/howto-publish-a-release.md +++ b/doc/howto-publish-a-release.md @@ -31,7 +31,7 @@ Test everything Make a release commit --------------------- * [ ] Update the number in the `README` -* [ ] Update `__version__` and `VERSION` in `ranger/__init__.py` +* [ ] Update `__version__` and `__release__` in `ranger/__init__.py` * [ ] Update `__version__` in `ranger/ext/rifle.py` * [ ] `make man` * [ ] Write changelog entry diff --git a/doc/ranger.1 b/doc/ranger.1 index 1290cb58..acf3f29a 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35) +.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -54,16 +54,20 @@ .\" Avoid warning from groff about undefined register 'F'. .de IX .. -.if !\nF .nr F 0 -.if \nF>0 \{\ -. de IX -. tm Index:\\$1\t\\n%\t"\\$2" +.nr rF 0 +.if \n(.g .if rF .nr rF 1 +.if (\n(rF:(\n(.g==0)) \{\ +. if \nF \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" .. -. if !\nF==2 \{\ -. nr % 0 -. nr F 2 +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 +. \} . \} .\} +.rr rF .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. @@ -129,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.9.1" "2018-05-14" "ranger manual" +.TH RANGER 1 "ranger-1.9.1" "2018-09-08" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -169,7 +173,7 @@ plugins, sample configuration files and some programs for integrating ranger with other software. They are usually installed to \&\fI/usr/share/doc/ranger/examples\fR. .PP -The man page of \fIrifle\fR\|(1) describes the functions of the file opener +The man page of \fBrifle\fR\|(1) describes the functions of the file opener .PP The section \fI\s-1LINKS\s0\fR of this man page contains further resources. .SH "POSITIONAL ARGUMENTS" @@ -307,6 +311,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 +335,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 @@ -624,6 +643,9 @@ to use to open the current file selection. .IP "cd" 14 .IX Item "cd" Open the console with the content \*(L"cd \*(R" +.IP "^P" 14 +.IX Item "^P" +Open the console with the most recent command. .IP "Alt\-\fIN\fR" 14 .IX Item "Alt-N" Open a tab. N has to be a number from 0 to 9. If the tab doesn't exist yet, it @@ -642,6 +664,41 @@ Close the current tab. The last tab cannot be closed this way. A key chain that allows you to quickly change the line mode of all the files of the current directory. For a more permanent solution, use the command \&\*(L"default_linemode\*(R" in your rc.conf. +.IP ".n" 14 +.IX Item ".n" +Apply a new filename filter. +.IP ".d" 14 +.IX Item ".d" +Apply the typefilter \*(L"directory\*(R". +.IP ".f" 14 +.IX Item ".f" +Apply the typefilter \*(L"file\*(R". +.IP ".l" 14 +.IX Item ".l" +Apply the typefilter \*(L"symlink\*(R". +.IP ".|" 14 +Combine the two topmost filters from the filter stack in the \*(L"\s-1OR\*(R"\s0 +relationship, instead of the \*(L"\s-1AND\*(R"\s0 used implicitly. +.IP ".&" 14 +Explicitly combine the two topmost filters in the \*(L"\s-1AND\*(R"\s0 +relationship. Usually not needed though might be useful in more +complicated scenarios. +.IP ".!" 14 +Negate the topmost filter. +.IP ".r" 14 +.IX Item ".r" +Rotate the filter stack by N elements. Just confirm with enter to +rotate by 1, i.e. move the topmost element to the bottom of the stack. +.IP ".c" 14 +.IX Item ".c" +Clear the filter stack. +.IP ".*" 14 +Decompose the topmost filter combinator (e.g. \f(CW\*(C`.!\*(C'\fR, \f(CW\*(C`.|\*(C'\fR). +.IP ".p" 14 +.IX Item ".p" +Pop the topmost filter from the filter stack. +.IP ".." 14 +Show the current filter stack state. .SS "READLINE-LIKE \s-1BINDINGS IN THE CONSOLE\s0" .IX Subsection "READLINE-LIKE BINDINGS IN THE CONSOLE" .IP "^B, ^F" 14 @@ -653,6 +710,12 @@ Move up and down (P for previous, N for Next) .IP "^A, ^E" 14 .IX Item "^A, ^E" Move to the start or to the end +.IP "Alt-B, Alt-LEFT" 14 +.IX Item "Alt-B, Alt-LEFT" +Move backwards by words. +.IP "Alt-F, Alt-RIGHT" 14 +.IX Item "Alt-F, Alt-RIGHT" +Move forwards by words. .IP "^D" 14 .IX Item "^D" Delete the current character. @@ -675,7 +738,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 @@ -777,9 +840,16 @@ 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? -.IP "draw_borders [bool]" 4 -.IX Item "draw_borders [bool]" -Draw borders around columns? +.IP "draw_borders [string]" 4 +.IX Item "draw_borders [string]" +Draw borders around or between the columns? Possible values are: +.Sp +.Vb 4 +\& none no borders of any sort +\& outline draw an outline around all the columns +\& separators draw only vertical lines between columns +\& both both of the above +.Ve .IP "draw_progress_bar_in_status_bar [bool]" 4 .IX Item "draw_progress_bar_in_status_bar [bool]" Draw a progress bar in the status bar which displays the average state of all @@ -893,6 +963,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 @@ -955,6 +1029,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? @@ -981,6 +1060,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 @@ -1569,7 +1652,7 @@ copy, run: .Ve .SH "SEE ALSO" .IX Header "SEE ALSO" -\&\fIrifle\fR\|(1) +\&\fBrifle\fR\|(1) .SH "BUGS" .IX Header "BUGS" Report bugs here: <https://github.com/ranger/ranger/issues> diff --git a/doc/ranger.pod b/doc/ranger.pod index 4cac8ef9..4fac5d50 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -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 @@ -584,6 +597,10 @@ to use to open the current file selection. Open the console with the content "cd " +=item ^P + +Open the console with the most recent command. + =item Alt-I<N> Open a tab. N has to be a number from 0 to 9. If the tab doesn't exist yet, it @@ -607,6 +624,58 @@ A key chain that allows you to quickly change the line mode of all the files of the current directory. For a more permanent solution, use the command "default_linemode" in your rc.conf. +=item .n + +Apply a new filename filter. + +=item .d + +Apply the typefilter "directory". + +=item .f + +Apply the typefilter "file". + +=item .l + +Apply the typefilter "symlink". + +=item .| + +Combine the two topmost filters from the filter stack in the "OR" +relationship, instead of the "AND" used implicitly. + +=item .& + +Explicitly combine the two topmost filters in the "AND" +relationship. Usually not needed though might be useful in more +complicated scenarios. + +=item .! + +Negate the topmost filter. + +=item .r + +Rotate the filter stack by N elements. Just confirm with enter to +rotate by 1, i.e. move the topmost element to the bottom of the stack. + +=item .c + +Clear the filter stack. + +=item .* + +Decompose the topmost filter combinator (e.g. C<.!>, C<.|>). + +=item .p + +Pop the topmost filter from the filter stack. + +=item .. + +Show the current filter stack state. + =back =head2 READLINE-LIKE BINDINGS IN THE CONSOLE @@ -625,6 +694,14 @@ Move up and down (P for previous, N for Next) Move to the start or to the end +=item Alt-B, Alt-LEFT + +Move backwards by words. + +=item Alt-F, Alt-RIGHT + +Move forwards by words. + =item ^D Delete the current character. @@ -662,7 +739,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 @@ -775,10 +852,15 @@ Display the free disk space in the status bar? Display tags in all columns? -=item draw_borders [bool] +=item draw_borders [string] -Draw borders around columns? +Draw borders around or between the columns? Possible values are: + none no borders of any sort + outline draw an outline around all the columns + separators draw only vertical lines between columns + both both of the above + =item draw_progress_bar_in_status_bar [bool] Draw a progress bar in the status bar which displays the average state of all @@ -908,6 +990,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 @@ -987,6 +1074,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? @@ -1017,6 +1110,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 diff --git a/doc/rifle.1 b/doc/rifle.1 index ad32e4f4..be605753 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.10 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -54,16 +54,20 @@ .\" Avoid warning from groff about undefined register 'F'. .de IX .. -.if !\nF .nr F 0 -.if \nF>0 \{\ -. de IX -. tm Index:\\$1\t\\n%\t"\\$2" +.nr rF 0 +.if \n(.g .if rF .nr rF 1 +.if (\n(rF:(\n(.g==0)) \{\ +. if \nF \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" .. -. if !\nF==2 \{\ -. nr % 0 -. nr F 2 +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 +. \} . \} .\} +.rr rF .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. @@ -129,7 +133,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-09-08" "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_pmount_dynamic.py b/examples/plugin_pmount_dynamic.py new file mode 100644 index 00000000..1448e15b --- /dev/null +++ b/examples/plugin_pmount_dynamic.py @@ -0,0 +1,70 @@ +# Tested with ranger 1.7.2 +# +# This plugin creates a bunch of keybindings used to mount and unmount +# the devices using pmount(1). +# +# (multiple partitions): alt+m <letter> <digit> : mount /dev/sd<letter><digit> +# (one partition): alt+m <letter> : mount /dev/sd<letter>1 +# (no partitions): alt+m <letter> : mount /dev/sd<letter> +# +# (multiple partitions): alt+M <letter> <digit> : unmount /dev/sd<letter><digit> +# (one partition): alt+M <letter> : unmount /dev/sd<letter>1 +# (no partitions): alt+M <letter> : unmount /dev/sd<letter> +# +# alt+n : list the devices + +from __future__ import (absolute_import, division, print_function) + +import subprocess +import ranger.api + +MOUNT_KEY = '<alt>m' +UMOUNT_KEY = '<alt>M' +LIST_MOUNTS_KEY = '<alt>n' +HOOK_INIT_OLD = ranger.api.hook_init + + +def hook_init(fm): + fm.execute_console("map {key} shell -p lsblk".format(key=LIST_MOUNTS_KEY)) + + diskcmd = "lsblk -lno NAME | awk '!/[1-9]/ {sub(/sd/, \"\"); print}'" + disks = subprocess.check_output( + diskcmd, shell=True).decode('utf-8').replace('\r', '').replace('\n', '') + + for disk in disks: + partcmd = "lsblk -lno NAME /dev/sd{0} | sed 's/sd{0}//' | tail -n 1".format(disk) + + try: + numparts = int(subprocess.check_output( + partcmd, shell=True).decode('utf-8').replace('\r', '').replace('\n', '')) + except ValueError: + numparts = 0 + + if numparts == 0: + # no partition, mount the whole device + fm.execute_console("map {key}{0} chain shell pmount sd{0}; cd /media/sd{0}".format( + disk, key=MOUNT_KEY)) + fm.execute_console("map {key}{0} chain cd; chain shell pumount sd{0}".format( + disk, key=UMOUNT_KEY)) + + elif numparts == 1: + # only one partition, mount the partition + fm.execute_console( + "map {key}{0} chain shell pmount sd{0}1; cd /media/sd{0}1".format( + disk, key=MOUNT_KEY)) + fm.execute_console("map {key}{0} chain cd; shell pumount sd{0}1".format( + disk, key=UMOUNT_KEY)) + + else: + # use range start 1, /dev/sd{device}0 doesn't exist + for part in range(1, numparts + 1): + fm.execute_console( + "map {key}{0}{1} chain shell pmount sd{0}{1}; cd /media/sd{0}{1}".format( + disk, part, key=MOUNT_KEY)) + fm.execute_console("map {key}{0}{1} chain cd; shell pumount sd{0}{1}".format( + disk, part, key=UMOUNT_KEY)) + + return HOOK_INIT_OLD(fm) + + +ranger.api.hook_init = hook_init diff --git a/ranger/__init__.py b/ranger/__init__.py index 0d7c8fdc..ae1ecc48 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -12,9 +12,28 @@ from __future__ import (absolute_import, division, print_function) import os + +# Version helper +def version_helper(): + if __release__: + version_string = 'ranger {0}'.format(__version__) + else: + import subprocess + version_string = 'ranger-master {0}' + try: + git_describe = subprocess.check_output(['git', 'describe'], + universal_newlines=True, + stderr=subprocess.PIPE) + version_string = version_string.format(git_describe.strip('\n')) + except (OSError, subprocess.CalledProcessError): + version_string = version_string.format(__version__) + return version_string + + # Information __license__ = 'GPL3' __version__ = '1.9.1' +__release__ = False __author__ = __maintainer__ = 'Roman Zimbelmann' __email__ = 'hut@hut.pm' @@ -27,7 +46,7 @@ MACRO_DELIMITER = '%' MACRO_DELIMITER_ESC = '%%' DEFAULT_PAGER = 'less' USAGE = '%prog [options] [path]' -VERSION = 'ranger-master {0}'.format(__version__) +VERSION = version_helper() # These variables are ignored if the corresponding # XDG environment variable is non-empty and absolute diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index 350c8359..a9e01e0c 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -6,7 +6,7 @@ from __future__ import (absolute_import, division, print_function) from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import ( black, blue, cyan, green, magenta, red, white, yellow, default, - normal, bold, reverse, + normal, bold, reverse, dim, BRIGHT, default_colors, ) @@ -39,18 +39,22 @@ class Default(ColorScheme): if context.directory: attr |= bold fg = blue + fg += BRIGHT elif context.executable and not \ any((context.media, context.container, context.fifo, context.socket)): attr |= bold fg = green + fg += BRIGHT if context.socket: - fg = magenta attr |= bold + fg = magenta + fg += BRIGHT if context.fifo or context.device: fg = yellow if context.device: attr |= bold + fg += BRIGHT if context.link: fg = cyan if context.good else magenta if context.tag_marker and not context.selected: @@ -59,10 +63,19 @@ class Default(ColorScheme): fg = white else: fg = red + fg += BRIGHT if not context.selected and (context.cut or context.copied): - fg = black attr |= bold + fg = black + fg += BRIGHT + # If the terminal doesn't support bright colors, use dim white + # instead of black. + if BRIGHT == 0: + attr |= dim + fg = white if context.main_column: + # Doubling up with BRIGHT here causes issues because it's + # additive not idempotent. if context.selected: attr |= bold if context.marked: @@ -78,7 +91,6 @@ class Default(ColorScheme): fg = cyan elif context.in_titlebar: - attr |= bold if context.hostname: fg = red if context.bad else green elif context.directory: @@ -88,6 +100,8 @@ class Default(ColorScheme): bg = green elif context.link: fg = cyan + attr |= bold + fg += BRIGHT elif context.in_statusbar: if context.permissions: @@ -98,13 +112,16 @@ class Default(ColorScheme): if context.marked: attr |= bold | reverse fg = yellow + fg += BRIGHT if context.frozen: attr |= bold | reverse fg = cyan + fg += BRIGHT if context.message: if context.bad: attr |= bold fg = red + fg += BRIGHT if context.loaded: bg = self.progress_bar_color if context.vcsinfo: diff --git a/ranger/colorschemes/snow.py b/ranger/colorschemes/snow.py index 8e9686a8..c5f23c1c 100644 --- a/ranger/colorschemes/snow.py +++ b/ranger/colorschemes/snow.py @@ -4,7 +4,7 @@ from __future__ import (absolute_import, division, print_function) from ranger.gui.colorscheme import ColorScheme -from ranger.gui.color import default_colors, reverse, bold +from ranger.gui.color import default_colors, reverse, bold, BRIGHT class Snow(ColorScheme): @@ -20,6 +20,7 @@ class Snow(ColorScheme): attr = reverse if context.directory: attr |= bold + fg += BRIGHT elif context.highlight: attr |= reverse @@ -36,6 +37,7 @@ class Snow(ColorScheme): elif context.in_taskview: if context.selected: attr |= bold + fg += BRIGHT if context.loaded: attr |= reverse diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 7f5fc764..d177203a 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1545,6 +1545,61 @@ class filter_inode_type(Command): self.fm.thisdir.refilter() +class filter_stack(Command): + """ + :filter_stack ... + + Manages the filter stack. + + filter_stack add FILTER_TYPE ARGS... + filter_stack pop + filter_stack decompose + filter_stack rotate [N=1] + filter_stack clear + filter_stack show + """ + def execute(self): + from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS + + subcommand = self.arg(1) + + if subcommand == "add": + try: + self.fm.thisdir.filter_stack.append( + SIMPLE_FILTERS[self.arg(2)](self.rest(3)) + ) + except KeyError: + FILTER_COMBINATORS[self.arg(2)](self.fm.thisdir.filter_stack) + elif subcommand == "pop": + self.fm.thisdir.filter_stack.pop() + elif subcommand == "decompose": + inner_filters = self.fm.thisdir.filter_stack.pop().decompose() + if inner_filters: + self.fm.thisdir.filter_stack.extend(inner_filters) + elif subcommand == "clear": + self.fm.thisdir.filter_stack = [] + elif subcommand == "rotate": + rotate_by = int(self.arg(2) or 1) + self.fm.thisdir.filter_stack = ( + self.fm.thisdir.filter_stack[-rotate_by:] + + self.fm.thisdir.filter_stack[:-rotate_by] + ) + elif subcommand == "show": + stack = list(map(str, self.fm.thisdir.filter_stack)) + pager = self.fm.ui.open_pager() + pager.set_source(["Filter stack: "] + stack) + pager.move(to=100, percentage=True) + return + else: + self.fm.notify( + "Unknown subcommand: {}".format(subcommand), + bad=True + ) + return + + self.fm.thisdir.refilter() + + class grep(Command): """:grep <string> diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 6b0802bb..4da1ffc0 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -86,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. @@ -93,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 @@ -102,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 @@ -125,8 +146,11 @@ set status_bar_on_top false # currently running tasks which support progress bars? set draw_progress_bar_in_status_bar true -# Draw borders around columns? -set draw_borders false +# Draw borders around columns? (separators, outline, both, or none) +# Separators are vertical lines between columns. +# Outline draws a box around all the columns. +# Both combines the two. +set draw_borders none # Display the directory name in tabs? set dirname_in_tabs false @@ -240,9 +264,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 @@ -323,6 +352,8 @@ map r chain draw_possible_programs; console open_with%%space map f console find%space map cd console cd%space +map <C-p> chain console; eval fm.ui.console.history_move(-1) + # Change the line mode map Mf linemode filename map Mi linemode fileinfo @@ -386,6 +417,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 ~ @@ -488,6 +520,8 @@ map <a-6> tab_open 6 map <a-7> tab_open 7 map <a-8> tab_open 8 map <a-9> tab_open 9 +map <a-r> tab_shift 1 +map <a-l> tab_shift -1 # Sorting map or set sort_reverse! @@ -530,6 +564,20 @@ map zv set use_preview_script! map zf console filter%space copymap zf zz +# Filter stack +map .n console filter_stack add name%space +map .d filter_stack add type d +map .f filter_stack add type f +map .l filter_stack add type l +map .| filter_stack add or +map .& filter_stack add and +map .! filter_stack add not +map .r console filter_stack rotate +map .c filter_stack clear +map .* filter_stack decompose +map .p filter_stack pop +map .. filter_stack show + # Bookmarks map `<any> enter_bookmark %any map '<any> enter_bookmark %any @@ -574,8 +622,11 @@ cmap <left> eval fm.ui.console.move(left=1) cmap <right> eval fm.ui.console.move(right=1) cmap <home> eval fm.ui.console.move(right=0, absolute=True) cmap <end> eval fm.ui.console.move(right=-1, absolute=True) -cmap <a-left> eval fm.ui.console.move_word(left=1) -cmap <a-right> eval fm.ui.console.move_word(right=1) +cmap <a-b> eval fm.ui.console.move_word(left=1) +cmap <a-f> eval fm.ui.console.move_word(right=1) + +copycmap <a-b> <a-left> +copycmap <a-f> <a-right> # Line Editing cmap <backspace> eval fm.ui.console.delete(-1) @@ -587,6 +638,7 @@ cmap <C-u> eval fm.ui.console.delete_rest(-1) cmap <C-y> eval fm.ui.console.paste() # And of course the emacs way +copycmap <ESC> <C-g> copycmap <up> <C-p> copycmap <down> <C-n> copycmap <left> <C-b> diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf index 83254a87..babdcda7 100644 --- a/ranger/config/rifle.conf +++ b/ranger/config/rifle.conf @@ -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 -- "$@" diff --git a/ranger/container/directory.py b/ranger/container/directory.py index e281e6c9..81dabb24 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -155,6 +155,8 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public self.marked_items = [] + self.filter_stack = [] + self._signal_functions = [] func = self.signal_function_factory(self.sort) self._signal_functions += [func] @@ -297,6 +299,7 @@ class Directory( # pylint: disable=too-many-instance-attributes,too-many-public if self.temporary_filter: temporary_filter_search = self.temporary_filter.search filters.append(lambda fobj: temporary_filter_search(fobj.basename)) + filters.extend(self.filter_stack) self.files = [f for f in self.files_all if accept_file(f, filters)] diff --git a/ranger/container/settings.py b/ranger/container/settings.py index 170ace5a..70a12b51 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, @@ -40,7 +41,7 @@ ALLOWED_SETTINGS = { 'display_size_in_status_bar': bool, "display_free_space_in_status_bar": bool, 'display_tags_in_all_columns': bool, - 'draw_borders': bool, + 'draw_borders': str, 'draw_progress_bar_in_status_bar': bool, 'flushinput': bool, 'freeze_files': bool, @@ -65,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, @@ -91,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, } @@ -98,9 +101,11 @@ ALLOWED_SETTINGS = { ALLOWED_VALUES = { 'cd_tab_case': ['sensitive', 'insensitive', 'smart'], 'confirm_on_delete': ['multiple', 'always', 'never'], + 'draw_borders': ['none', 'both', 'outline', 'separators'], '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'], @@ -113,6 +118,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 3e488159..40fd52a3 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: @@ -234,10 +240,25 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m cmd = cmd_class(string, quantifier=quantifier) if cmd.resolve_macros and _MacroTemplate.delimiter in cmd.line: - macros = dict(('any%d' % i, key_to_string(char)) - for i, char in enumerate(wildcards if wildcards is not None else [])) + def any_macro(i, char): + return ('any{:d}'.format(i), key_to_string(char)) + + def anypath_macro(i, char): + try: + val = self.fm.bookmarks[key_to_string(char)] + except KeyError: + self.notify('No bookmark defined for `{}`'.format( + key_to_string(char)), bad=True) + val = MACRO_FAIL + return ('any_path{:d}'.format(i), val) + + macros = dict(f(i, char) for f in (any_macro, anypath_macro) + for i, char in enumerate(wildcards if wildcards + is not None else [])) if 'any0' in macros: macros['any'] = macros['any0'] + if 'any_path0' in macros: + macros['any_path'] = macros['any_path0'] try: line = self.substitute_macros(cmd.line, additional=macros, escape=cmd.escape_macros_for_shell) @@ -401,7 +422,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m are multiple choices label: a string to select an opening method by its label flags: a string specifying additional options, see `man rifle` - mimetyle: pass the mimetype to rifle, overriding its own guess + mimetype: pass the mimetype to rifle, overriding its own guess """ mode = kw['mode'] if 'mode' in kw else 0 @@ -409,7 +430,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 = [] @@ -482,8 +504,13 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m if narg is not None: mode = narg tfile = self.thisfile - selection = self.thistab.get_selection() - if not self.thistab.enter_dir(tfile) and selection: + if kw.get('selection', True): + selection = self.thistab.get_selection() + else: + selection = [tfile] + if tfile.is_directory: + self.thistab.enter_dir(tfile) + elif selection: result = self.execute_file(selection, mode=mode) if result in (False, ASK_COMMAND): self.open_console('open_with ') @@ -605,6 +632,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 # -------------------------- @@ -978,6 +1022,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): @@ -1063,14 +1108,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 @@ -1111,6 +1153,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 # -------------------------- @@ -1191,6 +1262,53 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m i += 1 return self.tab_open(i, path) + def tab_shift(self, offset=0, to=None): # pylint: disable=invalid-name + """Shift the tab left/right + + Shift the current tab to the left or right by either: + offset - changes the tab number by offset + to - shifts the tab to the specified tab number + """ + + oldtab_index = self.current_tab + if to is None: + assert isinstance(offset, int) + # enumerated index (1 to 9) + newtab_index = oldtab_index + offset + else: + assert isinstance(to, int) + newtab_index = to + # shift tabs without enumerating, preserve tab numbers when can + if newtab_index != oldtab_index: + # the other tabs shift in the opposite direction + if (newtab_index - oldtab_index) > 0: + direction = -1 + else: + direction = 1 + + def tabshiftreorder(source_index): + # shift the tabs to make source_index empty + if source_index in self.tabs: + target_index = source_index + direction + # make the target_index empty recursively + tabshiftreorder(target_index) + # shift the source to target + source_tab = self.tabs[source_index] + self.tabs[target_index] = source_tab + del self.tabs[source_index] + + # first remove the current tab from the dict + oldtab = self.tabs[oldtab_index] + del self.tabs[oldtab_index] + # make newtab_index empty by shifting + tabshiftreorder(newtab_index) + self.tabs[newtab_index] = oldtab + self.current_tab = newtab_index + self.thistab = oldtab + self.ui.titlebar.request_redraw() + self.signal_emit('tab.layoutchange') + return None + def tab_switch(self, path, create_directory=False): """Switches to tab of given path, opening a new tab as necessary. diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py new file mode 100644 index 00000000..ff9f4080 --- /dev/null +++ b/ranger/core/filter_stack.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. +# Author: Wojciech Siewierski <wojciech.siewierski@onet.pl>, 2018 + +from __future__ import (absolute_import, division, print_function) + +import re + +from ranger.container.directory import accept_file, InodeFilterConstants + +# pylint: disable=too-few-public-methods + + +class BaseFilter(object): + def decompose(self): + return [self] + + +SIMPLE_FILTERS = {} +FILTER_COMBINATORS = {} + + +def stack_filter(filter_name): + def decorator(cls): + SIMPLE_FILTERS[filter_name] = cls + return cls + return decorator + + +def filter_combinator(combinator_name): + def decorator(cls): + FILTER_COMBINATORS[combinator_name] = cls + return cls + return decorator + + +@stack_filter("name") +class NameFilter(BaseFilter): + def __init__(self, pattern): + self.pattern = pattern + self.regex = re.compile(pattern) + + def __call__(self, fobj): + return self.regex.search(fobj.relative_path) + + def __str__(self): + return "<Filter: name =~ /{}/>".format(self.pattern) + + +@stack_filter("type") +class TypeFilter(BaseFilter): + type_to_function = { + InodeFilterConstants.DIRS: + (lambda fobj: fobj.is_directory), + InodeFilterConstants.FILES: + (lambda fobj: fobj.is_file and not fobj.is_link), + InodeFilterConstants.LINKS: + (lambda fobj: fobj.is_link), + } + + def __init__(self, filetype): + if filetype not in self.type_to_function: + raise KeyError(filetype) + self.filetype = filetype + + def __call__(self, fobj): + return self.type_to_function[self.filetype](fobj) + + def __str__(self): + return "<Filter: type == '{}'>".format(self.filetype) + + +@filter_combinator("or") +class OrFilter(BaseFilter): + def __init__(self, stack): + self.subfilters = [stack[-2], stack[-1]] + + stack.pop() + stack.pop() + + stack.append(self) + + def __call__(self, fobj): + # Turn logical AND (accept_file()) into a logical OR with the + # De Morgan's laws. + return not accept_file( + fobj, + ((lambda x, f=filt: not f(x)) + for filt + in self.subfilters), + ) + + def __str__(self): + return "<Filter: {}>".format(" or ".join(map(str, self.subfilters))) + + def decompose(self): + return self.subfilters + + +@filter_combinator("and") +class AndFilter(BaseFilter): + def __init__(self, stack): + self.subfilters = [stack[-2], stack[-1]] + + stack.pop() + stack.pop() + + stack.append(self) + + def __call__(self, fobj): + return accept_file(fobj, self.subfilters) + + def __str__(self): + return "<Filter: {}>".format(" and ".join(map(str, self.subfilters))) + + def decompose(self): + return self.subfilters + + +@filter_combinator("not") +class NotFilter(BaseFilter): + def __init__(self, stack): + self.subfilter = stack.pop() + stack.append(self) + + def __call__(self, fobj): + return not self.subfilter(fobj) + + def __str__(self): + return "<Filter: not {}>".format(str(self.subfilter)) + + def decompose(self): + return [self.subfilter] diff --git a/ranger/core/fm.py b/ranger/core/fm.py index d85dd48c..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()) + - '\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 b5d0af77..598ce243 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -363,7 +363,7 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many # Load custom commands def import_file(name, path): # From https://stackoverflow.com/a/67692 - # pragma pylint: disable=no-name-in-module,import-error,no-member + # 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) 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/data/scope.sh b/ranger/data/scope.sh index 25251533..13a25b45 100755 --- a/ranger/data/scope.sh +++ b/ranger/data/scope.sh @@ -123,6 +123,43 @@ handle_image() { # -jpeg -tiffcompression jpeg \ # -- "${FILE_PATH}" "${IMAGE_CACHE_PATH%.*}" \ # && exit 6 || exit 1;; + + # Preview archives using the first image inside. + # (Very useful for comic book collections for example.) + # application/zip|application/x-rar|application/x-7z-compressed|\ + # application/x-xz|application/x-bzip2|application/x-gzip|application/x-tar) + # local fn=""; local fe="" + # local zip=""; local rar=""; local tar=""; local bsd="" + # case "${mimetype}" in + # application/zip) zip=1 ;; + # application/x-rar) rar=1 ;; + # application/x-7z-compressed) ;; + # *) tar=1 ;; + # esac + # { [ "$tar" ] && fn=$(tar --list --file "${FILE_PATH}"); } || \ + # { fn=$(bsdtar --list --file "${FILE_PATH}") && bsd=1 && tar=""; } || \ + # { [ "$rar" ] && fn=$(unrar lb -p- -- "${FILE_PATH}"); } || \ + # { [ "$zip" ] && fn=$(zipinfo -1 -- "${FILE_PATH}"); } || return + # + # fn=$(echo "$fn" | python -c "import sys; import mimetypes as m; \ + # [ print(l, end='') for l in sys.stdin if \ + # (m.guess_type(l[:-1])[0] or '').startswith('image/') ]" |\ + # sort -V | head -n 1) + # [ "$fn" = "" ] && return + # [ "$bsd" ] && fn=$(printf '%b' "$fn") + # + # [ "$tar" ] && tar --extract --to-stdout \ + # --file "${FILE_PATH}" -- "$fn" > "${IMAGE_CACHE_PATH}" && exit 6 + # fe=$(echo -n "$fn" | sed 's/[][*?\]/\\\0/g') + # [ "$bsd" ] && bsdtar --extract --to-stdout \ + # --file "${FILE_PATH}" -- "$fe" > "${IMAGE_CACHE_PATH}" && exit 6 + # [ "$bsd" ] || [ "$tar" ] && rm -- "${IMAGE_CACHE_PATH}" + # [ "$rar" ] && unrar p -p- -inul -- "${FILE_PATH}" "$fn" > \ + # "${IMAGE_CACHE_PATH}" && exit 6 + # [ "$zip" ] && unzip -pP "" -- "${FILE_PATH}" "$fe" > \ + # "${IMAGE_CACHE_PATH}" && exit 6 + # [ "$rar" ] || [ "$zip" ] && rm -- "${IMAGE_CACHE_PATH}" + # ;; esac } diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 78d71cb2..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() @@ -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 6106656e..6832e2cf 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -357,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/color.py b/ranger/gui/color.py index 45f983e8..8f6439c7 100644 --- a/ranger/gui/color.py +++ b/ranger/gui/color.py @@ -66,6 +66,11 @@ blink = curses.A_BLINK reverse = curses.A_REVERSE underline = curses.A_UNDERLINE invisible = curses.A_INVIS +dim = curses.A_DIM default_colors = (default, default, normal) # pylint: enable=invalid-name,bad-whitespace + +curses.setupterm() +# Adding BRIGHT to a color achieves what `bold` was used for. +BRIGHT = 8 if curses.tigetnum('colors') >= 16 else 0 diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 4f76dfab..441e9032 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -365,7 +365,9 @@ class UI( # pylint: disable=too-many-instance-attributes,too-many-public-method DisplayableContainer.draw(self) if self._draw_title and self.settings.update_title: cwd = self.fm.thisdir.path - if cwd.startswith(self.fm.home_path): + if self.settings.tilde_in_titlebar \ + and (cwd == self.fm.home_path + or cwd.startswith(self.fm.home_path + "/")): cwd = '~' + cwd[len(self.fm.home_path):] if self.settings.shorten_title: split = cwd.rsplit(os.sep, self.settings.shorten_title) diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index cf322409..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: @@ -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/titlebar.py b/ranger/gui/widgets/titlebar.py index 042b4b04..765c1248 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -102,8 +102,9 @@ class TitleBar(Widget): bar.add(' ', 'hostname', clr, fixed=True) pathway = self.fm.thistab.pathway - if self.settings.tilde_in_titlebar and \ - self.fm.thisdir.path.startswith(self.fm.home_path): + if self.settings.tilde_in_titlebar \ + and (self.fm.thisdir.path.startswith(self.fm.home_path + "/") + or self.fm.thisdir.path == self.fm.home_path): pathway = pathway[self.fm.home_path.count('/') + 1:] bar.add('~/', 'directory', fixed=True) diff --git a/ranger/gui/widgets/view_miller.py b/ranger/gui/widgets/view_miller.py index e138ee4b..55d401a0 100644 --- a/ranger/gui/widgets/view_miller.py +++ b/ranger/gui/widgets/view_miller.py @@ -99,7 +99,12 @@ class ViewMiller(ViewBase): # pylint: disable=too-many-ancestors,too-many-insta directory.use() DisplayableContainer.draw(self) if self.settings.draw_borders: - self._draw_borders() + draw_borders = self.settings.draw_borders.lower() + if draw_borders in ['both', 'true']: # 'true' for backwards compat. + border_types = ['separators', 'outline'] + else: + border_types = [draw_borders] + self._draw_borders(border_types) if self.draw_bookmarks: self._draw_bookmarks() elif self.draw_hints: @@ -107,8 +112,9 @@ class ViewMiller(ViewBase): # pylint: disable=too-many-ancestors,too-many-insta elif self.draw_info: self._draw_info(self.draw_info) - def _draw_borders(self): + def _draw_borders(self, border_types): # pylint: disable=too-many-branches win = self.win + self.color('in_browser', 'border') left_start = 0 @@ -129,49 +135,57 @@ class ViewMiller(ViewBase): # pylint: disable=too-many-ancestors,too-many-insta right_end = self.wid - 1 # Draw horizontal lines and the leftmost vertical line - try: - # pylint: disable=no-member - win.hline(0, left_start, curses.ACS_HLINE, right_end - left_start) - win.hline(self.hei - 1, left_start, curses.ACS_HLINE, right_end - left_start) - win.vline(1, left_start, curses.ACS_VLINE, self.hei - 2) - # pylint: enable=no-member - except curses.error: - pass + if 'outline' in border_types: + try: + # pylint: disable=no-member + win.hline(0, left_start, curses.ACS_HLINE, right_end - left_start) + win.hline(self.hei - 1, left_start, curses.ACS_HLINE, right_end - left_start) + win.vline(1, left_start, curses.ACS_VLINE, self.hei - 2) + # pylint: enable=no-member + except curses.error: + pass # Draw the vertical lines in the middle - for child in self.columns[:-1]: - if not child.has_preview(): - continue - if child.main_column and self.pager.visible: - # If we "zoom in" with the pager, we have to - # skip the between main_column and pager. - break - x = child.x + child.wid - y = self.hei - 1 + if 'separators' in border_types: + for child in self.columns[:-1]: + if not child.has_preview(): + continue + if child.main_column and self.pager.visible: + # If we "zoom in" with the pager, we have to + # skip the between main_column and pager. + break + x = child.x + child.wid + y = self.hei - 1 + try: + # pylint: disable=no-member + win.vline(1, x, curses.ACS_VLINE, y - 1) + if 'outline' in border_types: + self.addch(0, x, curses.ACS_TTEE, 0) + self.addch(y, x, curses.ACS_BTEE, 0) + else: + self.addch(0, x, curses.ACS_VLINE, 0) + self.addch(y, x, curses.ACS_VLINE, 0) + # pylint: enable=no-member + except curses.error: + # in case it's off the boundaries + pass + + if 'outline' in border_types: + # Draw the last vertical line try: # pylint: disable=no-member - win.vline(1, x, curses.ACS_VLINE, y - 1) - self.addch(0, x, curses.ACS_TTEE, 0) - self.addch(y, x, curses.ACS_BTEE, 0) + win.vline(1, right_end, curses.ACS_VLINE, self.hei - 2) # pylint: enable=no-member except curses.error: - # in case it's off the boundaries pass - # Draw the last vertical line - try: + if 'outline' in border_types: # pylint: disable=no-member - win.vline(1, right_end, curses.ACS_VLINE, self.hei - 2) + self.addch(0, left_start, curses.ACS_ULCORNER) + self.addch(self.hei - 1, left_start, curses.ACS_LLCORNER) + self.addch(0, right_end, curses.ACS_URCORNER) + self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER) # pylint: enable=no-member - except curses.error: - pass - - # pylint: disable=no-member - self.addch(0, left_start, curses.ACS_ULCORNER) - self.addch(self.hei - 1, left_start, curses.ACS_LLCORNER) - self.addch(0, right_end, curses.ACS_URCORNER) - self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER) - # pylint: enable=no-member def _collapse(self): # Should the last column be cut off? (Because there is no preview) @@ -195,10 +209,12 @@ class ViewMiller(ViewBase): # pylint: disable=too-many-ancestors,too-many-insta """Resize all the columns according to the given ratio""" ViewBase.resize(self, y, x, hei, wid) - borders = self.settings.draw_borders - pad = 1 if borders else 0 + border_type = self.settings.draw_borders.lower() + if border_type in ['outline', 'both', 'true']: + pad = 1 + else: + pad = 0 left = pad - self.is_collapsed = self._collapse() if self.is_collapsed: generator = enumerate(self.stretch_ratios) 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', |