2020-08-04 00:24:58 +02:00
|
|
|
shfm
|
2020-08-03 15:30:38 +02:00
|
|
|
________________________________________________________________________________
|
|
|
|
|
2020-08-04 00:24:58 +02:00
|
|
|
file manager written in posix shell
|
2020-08-03 15:30:38 +02:00
|
|
|
|
2020-08-04 10:17:36 +02:00
|
|
|
screenshot: https://user-images.githubusercontent.com/6799467/89270554-2b40ab00-d644-11ea-9f2b-bdabcba61a09.png
|
|
|
|
|
2020-08-03 15:35:13 +02:00
|
|
|
|
|
|
|
features
|
|
|
|
________________________________________________________________________________
|
|
|
|
|
2020-08-04 00:23:27 +02:00
|
|
|
* no dependencies other than a POSIX shell + POSIX printf, dd and stty
|
|
|
|
* tiny
|
|
|
|
* single file
|
|
|
|
* no compilation needed
|
2020-08-04 11:06:42 +02:00
|
|
|
* correctly handles files with funky names (newlines, etc)
|
2020-08-03 15:36:58 +02:00
|
|
|
|
2020-08-03 15:35:13 +02:00
|
|
|
|
2020-08-03 15:38:13 +02:00
|
|
|
keybinds
|
|
|
|
________________________________________________________________________________
|
|
|
|
|
|
|
|
j - down
|
|
|
|
k - up
|
|
|
|
l - open file or directory
|
|
|
|
h - go up level
|
2020-08-03 22:00:36 +02:00
|
|
|
g - go to top
|
|
|
|
G - go to bottom
|
2020-08-03 15:38:13 +02:00
|
|
|
q - quit
|
2020-08-03 23:17:56 +02:00
|
|
|
: - cd to <input>
|
2020-08-03 23:27:28 +02:00
|
|
|
/ - search current directory *<input>*
|
2020-08-04 00:02:11 +02:00
|
|
|
- - go to last directory
|
|
|
|
~ - go home
|
|
|
|
! - spawn shell
|
2020-08-04 00:09:15 +02:00
|
|
|
. - toggle hidden files
|
2020-08-03 22:33:13 +02:00
|
|
|
|
2020-08-04 11:19:43 +02:00
|
|
|
Additional keybinds:
|
|
|
|
|
|
|
|
down arrow - down
|
|
|
|
up arrow - up
|
|
|
|
right arrow - open file or directory
|
|
|
|
left arrow - go up level
|
|
|
|
|
|
|
|
enter/return - open file or directory
|
|
|
|
backspace - go up level
|
|
|
|
|
2020-08-03 22:33:13 +02:00
|
|
|
|
2020-08-04 01:00:49 +02:00
|
|
|
todo
|
|
|
|
________________________________________________________________________________
|
|
|
|
|
2020-08-04 11:04:15 +02:00
|
|
|
- [x] sanitize filenames for display.
|
2020-08-04 11:20:31 +02:00
|
|
|
- [ ] print directories first (hard).
|
2020-08-04 01:00:49 +02:00
|
|
|
- [ ] fix buggy focus after exit from inline editor.
|
2020-08-04 10:17:36 +02:00
|
|
|
- [ ] maybe file operations.
|
|
|
|
- [ ] add / to directories.
|
|
|
|
- [ ] use clearer variable names.
|
2020-08-04 11:07:18 +02:00
|
|
|
- [ ] unify entry printing.
|
2020-08-04 01:00:49 +02:00
|
|
|
|
|
|
|
|
2020-08-03 22:33:13 +02:00
|
|
|
opener
|
|
|
|
________________________________________________________________________________
|
|
|
|
|
2020-08-03 22:37:58 +02:00
|
|
|
Opening files in different applications (based on mime-type or file extension)
|
2020-08-03 22:33:13 +02:00
|
|
|
can be achieved via an environment variable (SHFM_OPENER) set to the location of
|
2020-08-03 22:37:58 +02:00
|
|
|
a small external script. If unset, the default for all files is '$EDITOR' (and
|
|
|
|
if that is unset, 'vi').
|
2020-08-03 22:33:13 +02:00
|
|
|
|
|
|
|
The script receives a single argument, the full path to the selected file.
|
2020-08-03 22:36:48 +02:00
|
|
|
The opener script is also useful on the command-line. The environment variable
|
|
|
|
is set as follows.
|
|
|
|
|
|
|
|
export SHFM_OPENER=/path/to/script
|
2020-08-03 22:33:13 +02:00
|
|
|
|
|
|
|
Example scripts:
|
|
|
|
|
|
|
|
#!/bin/sh -e
|
|
|
|
#
|
|
|
|
# open file in application based on file extension
|
|
|
|
|
|
|
|
case $1 in
|
|
|
|
*.mp3|*.flac|*.wav)
|
|
|
|
mpv --no-video "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
*.mp4|*.mkv|*.webm)
|
|
|
|
mpv "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
*.png|*.gif||*.jpg|*.jpe|*.jpeg)
|
|
|
|
gimp "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
*.html|*.pdf)
|
|
|
|
firefox "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
# all other files
|
|
|
|
*)
|
|
|
|
"${EDITOR:=vi}" "$1"
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
#!/bin/sh -e
|
|
|
|
#
|
|
|
|
# open file in application based on mime-type
|
|
|
|
|
|
|
|
mime_type=$(file -bi)
|
|
|
|
|
|
|
|
case $mime_type in
|
|
|
|
audio/*)
|
|
|
|
mpv --no-video "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
video/*)
|
|
|
|
mpv "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
image/*)
|
|
|
|
gimp "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
text/html*|application/pdf*)
|
|
|
|
firefox "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
text/*|)
|
|
|
|
"${EDITOR:=vi}" "$1"
|
|
|
|
;;
|
|
|
|
|
|
|
|
*)
|
|
|
|
printf 'unknown mime-type %s\n' "$mime_type"
|
|
|
|
;;
|
|
|
|
esac
|
2020-08-04 01:00:49 +02:00
|
|
|
|
|
|
|
|
2020-08-04 09:34:33 +02:00
|
|
|
implementation details
|
|
|
|
________________________________________________________________________________
|
|
|
|
|
|
|
|
* Draws are partial!
|
|
|
|
|
|
|
|
The file manager will only redraw what is necessary. Every line scrolled
|
|
|
|
corresponds to three lines being redrawn. The current line (clear highlight),
|
|
|
|
the destination line (set highlight) and the status line (update location).
|
|
|
|
|
|
|
|
|
|
|
|
* POSIX shell has no arrays.
|
|
|
|
|
|
|
|
It does however have an argument list (used for passing command-line arguments
|
|
|
|
to the script and when calling functions).
|
|
|
|
|
|
|
|
Restrictions:
|
|
|
|
|
|
|
|
- Can only have one list at a time (in the same scope).
|
|
|
|
- Can restrict a list's scope but cannot extend it.
|
|
|
|
- Cannot grab element by index.
|
|
|
|
|
|
|
|
Things I'm thankful for:
|
|
|
|
|
|
|
|
- Elements can be "popped" off the front of the list (using shift).
|
|
|
|
- List size is given to us (via $#).
|
|
|
|
- No need to use a string delimited by some character.
|
|
|
|
- Can loop over elements.
|
|
|
|
|
|
|
|
|
|
|
|
* Cursor position is tracked manually.
|
|
|
|
|
|
|
|
Grabbing the current cursor position cannot be done reliably from POSIX shell.
|
|
|
|
Instead, the cursor starts at 0,0 and each movement modifies the value of a
|
|
|
|
variable (relative Y position in screen). This variable is how the file
|
|
|
|
manager knows which line of the screen the cursor is on.
|
|
|
|
|
|
|
|
|
|
|
|
* Multi-byte input is handled by using a 2D case statement.
|
|
|
|
|
|
|
|
(I don't really know what to call this, suggestions appreciated)
|
|
|
|
|
|
|
|
Rather than using read timeouts (we can't sleep < 1s in POSIX shell anyway)
|
|
|
|
to handle multi-byte input, shfm tracks location within sequences and handles
|
|
|
|
this in a really nice way.
|
|
|
|
|
|
|
|
The case statement matches "$char$esc" with "$esc" being an integer holding
|
|
|
|
position in sequences. To give an example, down arrow emits '\033[B'.
|
|
|
|
|
|
|
|
- When '\033?' is found, the value of 'esc' is set to '1'.
|
|
|
|
- When '[1' is found, the value of 'esc' is set to '2'.
|
|
|
|
- When 'B2' is found, we know it's '\033[B' and handle down arrow.
|
|
|
|
- If input doesn't follow this sequence, 'esc' is reset to '0'.
|
|
|
|
|
|
|
|
|
|
|
|
* There is no usage of '[' or 'test'.
|
|
|
|
|
|
|
|
Despite these being commonly provided as "shell builtins" (part of the shell),
|
|
|
|
a lot of shells still use the external utilities from the coreutils. All usage
|
|
|
|
of these has been replaced with 'case' as it is always a "shell keyword".
|
|
|
|
|
2020-08-04 11:33:11 +02:00
|
|
|
This is one of the approaches taken to reduce the need for anything external.
|
|
|
|
|
2020-08-04 09:34:33 +02:00
|
|
|
|
2020-08-04 11:30:59 +02:00
|
|
|
* Filename escaping works via looping over a string char by char.
|
|
|
|
|
|
|
|
I didn't think this was possible in POSIX shell until I needed to do this in
|
|
|
|
KISS Linux's package manager and found a way to do so.
|
|
|
|
|
|
|
|
I'll let the code speak for itself (comments added for clarity):
|
|
|
|
|
|
|
|
file_escape() {
|
|
|
|
# store the argument (file name) in a temporary variable.
|
|
|
|
# ensure that 'safe' is empty (we have no access to the local keyword
|
|
|
|
# and can't use local variables without also using a sub-shell). This
|
|
|
|
# variable will contain its prior value (if it has one) otherwise.
|
|
|
|
tmp=$1 safe=
|
|
|
|
|
|
|
|
# loop over string char by char.
|
|
|
|
# this takes the approach of infinite loop + inner break condition as
|
|
|
|
# we have no access to [ (personal restriction).
|
|
|
|
while :; do
|
|
|
|
# Remove everything after the first character.
|
|
|
|
c=${tmp%"${tmp#?}"*}
|
|
|
|
|
|
|
|
# Construct a new string, replacing anything unprintable with '?'.
|
|
|
|
case $c in
|
|
|
|
[[:print:]]) safe=$safe$c ;;
|
|
|
|
'') return ;; # we have nothing more to do, return.
|
|
|
|
*) safe=$safe\? ;;
|
|
|
|
esac
|
|
|
|
|
|
|
|
# Remove the first character.
|
|
|
|
# This shifts our position forward.
|
|
|
|
tmp=${tmp#?}
|
|
|
|
done
|
|
|
|
}
|