shfm ________________________________________________________________________________ file manager written in posix shell screenshot: https://user-images.githubusercontent.com/6799467/89270554-2b40ab00-d644-11ea-9f2b-bdabcba61a09.png features ________________________________________________________________________________ * no dependencies other than a POSIX shell + POSIX printf, dd and stty *** * tiny * single file * no compilation needed * correctly handles files with funky names (newlines, etc) * works with very small terminal sizes. *** SIGWINCH and the size parameter to stty are not /yet/ POSIX but will be. - https://austingroupbugs.net/view.php?id=1053 - https://austingroupbugs.net/view.php?id=1151 *** Raw escape sequences are used in place of tput. These are strictly VT100 sequences minus \033[?1049[lh], \033[?25[lh] and \033[?7[lh] (?) keybinds ________________________________________________________________________________ j - down k - up l - open file or directory h - go up level g - go to top G - go to bottom q - quit : - cd to <input> / - search current directory *<input>* - - go to last directory ~ - go home ! - spawn shell . - toggle hidden files 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 todo ________________________________________________________________________________ - [x] sanitize filenames for display. - [ ] print directories first (hard). - [x] fix buggy focus after exit from inline editor. - [ ] maybe file operations. - [ ] add / to directories. - [ ] use clearer variable names. - [x] going up directories should center entry. - [ ] abstract over sequences. - [ ] look into whether tput is feasible. opener ________________________________________________________________________________ Opening files in different applications (based on mime-type or file extension) can be achieved via an environment variable (SHFM_OPENER) set to the location of a small external script. If unset, the default for all files is '$EDITOR' (and if that is unset, 'vi'). The script receives a single argument, the full path to the selected file. The opener script is also useful on the command-line. The environment variable is set as follows. export SHFM_OPENER=/path/to/script 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 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". This is one of the approaches taken to reduce the need for anything external. * 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 } # Afterwards, the variable 'safe' contains the escaped filename. Using # globals here is a must. Printing to the screen and capturing that # output is too slow. * SIGWINCH handler isn't executed until key press is made. SIGWINCH doesn't seem to execute asynchronously when the script is also waiting for input. This causes resize to require a key press. I'm not too bothered by this. It does save me implementing resize logic which is utter torture. :)
file manager written in posix shell
file manager written in posix shellCategory: Linux / Shell Script Development |
Watchers: 1 |
Star: 21 |
Fork: 1 |
Last update: Aug 6, 2020 |
I am using the environment variable to run external /sbin/xdg-open which is a custom script in the initramfs, to perform basic mime handling.
I had it returning non-zero if failed to find an appropriate app to run the file. However, this causes shfm to exit.
shfm should do nothing if receive a non-zero return from xdg-open.
That is standard behaviour of xdg-open, as defined in its man page, and I think nnn also reads the return value.
The only thing keeping me from switching from fff
to shfm
is image previews. Is this possible?
Admittedly not the most elegant solution but it works as far as I can tell using the files shown below as a test.
shfm » tree
.
├── empty/
├── .github/
│ └── workflows/
│ └── main.yml
├── .onlyhidden/
│ ├── ..hidden
│ └── .hidden
├── onlyhidden/
│ ├── ..hidden
│ └── .hidden
├── star/
│ ├── *
│ └── ..?*
├── star2/
│ ├── **
│ └── .[!.]*
├── ..foo
├── LICENSE
├── README
└── shfm*
It's very difficult to reason about how any of this works!
Prints directories first, as mentioned in the todo. Albeit not the prettiest solution, it does seem to get the job done. Wouldn't mind seeing a better implementation of it though, but that'd require skills greater than my own.
Running on desktop, Sakura terminal emulator, TERM=xterm, when hit "!" to spawn a shell, keys are not echoed to the screen. Works, but not echoed.
I found the culprit, it is this commit "use alternative screen to avoid redraws":
https://github.com/dylanaraps/shfm/commit/60119b02629abf127fb25ebf1498d5a9c2f7def5
The commit before that, in a shell can type and it is echoed.
Regards, Barry
I got toying with a pull request to allow the use of ~
with the cd feature. Let's say you are deep within the file system and you want to move to your Downloads directory. It would be much nicer to just type :
~/Downloads
than :
/home/user/Downloads
or go up like a dozen directories to get $HOME
.
This is what I have got so far:
@@ -321,7 +321,11 @@
:?)
prompt "cd: " r
- cd "${ans:="$0"}" >/dev/null 2>&1|| continue
+ path=${ans:="$0"}
+ case $path in "~"*)
+ path=${path/\~/$HOME}
+ esac
+ cd "$path" >/dev/null 2>&1|| continue
set -- *
y=1 y2=1 cur=$1
redraw "$@"
This is not pure Posix and shellcheck errors out with the following:
$ shellcheck shfm
In shfm line 326:
path=${path/\~/$HOME}
^--------------^ SC2039: In POSIX sh, string replacement is undefined.
For more information:
https://www.shellcheck.net/wiki/SC2039 -- In POSIX sh, string replacement i...
While this works for me, it does not fit within the goals of this project. I am also concerned it may not work on other systems.
At this point, I am a bit stumped and not sure if this functionality is possible within pure Posix. Posix is a bit out of my wheelhouse so any suggestions would be welcomed.
to reproduce bug, press down arrow followed by capital A
To reproduce the bug, start shfm, resize your terminal to trigger SIGWINCH's trap, then exit shfm. You'll notice that icanon and echo are disabled even though term_reset should've reset those. At program start, stty is saved so that it can be restored at exit. SIGWINCH's trap calls term_setup which overwrites stty unnecessarily. I moved stty set out of term_setup to avoid this.
I might be doing something wrong here, but I can't get shfm to cd into the working directory on exit.
I've tried a few different things, but with the suggested method of adding this to the shellrc file (in mycase .shrc)(ksh)
shfm() {
cd "$(command shfm "$@")"
}
I get a pwd output on exit but I end up back in my home dir. I imagine I need to replace "command" with an actual command?
I'd also like to shorten it to sf so I tried:
sf() {
shfm "$@"
cd "$(command shfm "$@")"
}
I even tried running it with cd $(shfm)
which also prints out the working dir but does not cd into it.
Truth be told, I'm not very familiar with shell functions and am a bit of a noob so go easy on me.
Cheers :)
EDIT: Curiously, I noticed in the keybinds that !
"spawns a terminal" into the current directory but is it really spawning a terminal or is it just returning to the terminal whilst staying in the current directory? Anyhow, I suppose this can be my workaround for now but for the sake of clarity would still like to know how to resolve this.
Otherwise the value of "$hidden" is kept. This means if you press '.' in a directory then cd to a new directory, you currently need to press '.' twice to view hidden files.
0.4.2(Aug 12, 2020)
* Directories are now bold and colored.
* Fixed bug with terminal not clearing during subcommand.
* Fixed issues with subcommands in the tty.
* Added messages for empty directories, no search results, etc.
* Fixed cd when dir starts with --.
* globs now work in search queries.
0.4.1(Aug 6, 2020)
* fix bug with cd on exit
0.4(Aug 6, 2020)
- added '?' keybind to show all keybinds.
- search is less greedy.
- fixed issue with search and hidden files.
- added -h and --help to show usage.
- fixed issue with international file names.
- tilde ~ now works with :cd.
- statusline is now red for root and blue for everyone else.
- added cd-on-exit support (see README).
0.2(Aug 4, 2020)
bug fixes
0.1(Aug 4, 2020)
initial release