aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format14
-rw-r--r--CMakeLists.txt20
-rw-r--r--LICENSE2
-rw-r--r--Makefile23
-rw-r--r--README.adoc105
-rwxr-xr-xsdn-install182
-rw-r--r--sdn-install.135
-rw-r--r--sdn.1131
-rw-r--r--sdn.cpp682
9 files changed, 920 insertions, 274 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..fa8134e
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,14 @@
+BasedOnStyle: LLVM
+ColumnLimit: 80
+IndentWidth: 4
+TabWidth: 4
+UseTab: ForContinuationAndIndentation
+SpaceAfterCStyleCast: true
+SpaceBeforeParens: Always
+AlignAfterOpenBracket: DontAlign
+AlignEscapedNewlines: DontAlign
+AlignOperands: DontAlign
+AlignConsecutiveMacros: Consecutive
+BreakBeforeTernaryOperators: true
+SpacesBeforeTrailingComments: 2
+WhitespaceSensitiveMacros: ['XX', 'ACTIONS', 'LS']
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9c6d06b..175139a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,11 +1,10 @@
# target_compile_features has been introduced in that version
-cmake_minimum_required (VERSION 3.1.0)
-
-project (sdn CXX)
-set (version 0.1)
+cmake_minimum_required (VERSION 3.1...3.27)
+project (sdn VERSION 0.1 LANGUAGES CXX)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
- set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
+ set (CMAKE_CXX_FLAGS
+ "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic")
endif ()
# Since we use a language with slow compilers, let's at least use a fast linker
@@ -23,24 +22,25 @@ target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS})
target_link_libraries (${PROJECT_NAME} PUBLIC ${NCURSESW_LIBRARIES} acl)
target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14)
target_compile_definitions (${PROJECT_NAME} PUBLIC
- -DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${version}\")
+ -DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\")
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
+install (PROGRAMS ${PROJECT_NAME}-install DESTINATION ${CMAKE_INSTALL_BINDIR})
+install (FILES sdn.1 sdn-install.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
-set (CPACK_PACKAGE_VERSION ${version})
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
- "${PROJECT_NAME}-${version}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
-set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${version}")
+ "${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
+set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
-set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${version}")
+set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SET_DESTDIR TRUE)
include (CPack)
diff --git a/LICENSE b/LICENSE
index e3b945d..7511f3e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2017 - 2018, Přemysl Eric Janouch <p@janouch.name>
+Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
diff --git a/Makefile b/Makefile
index 84ead83..0468a72 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,19 @@
+.POSIX:
SHELL = /bin/sh
-CXXFLAGS = -g -std=c++14 -Wall -Wextra -pedantic -static-libstdc++
+CXXFLAGS = -g -std=c++14 -Wall -Wextra -Wno-misleading-indentation -pedantic
+CPPFLAGS = `sed -ne '/^project (\([^ )]*\) VERSION \([^ )]*\).*/ \
+ s//-DPROJECT_NAME="\1" -DPROJECT_VERSION="\2"/p' CMakeLists.txt`
-all: sdn
-%: %.cpp CMakeLists.txt
- $(CXX) $(CXXFLAGS) $< -o $@ `pkg-config --libs --cflags ncursesw` -lacl \
- `sed -ne 's/^project (\([^ )]*\).*/-DPROJECT_NAME="\1"/p' \
- -e 's/^set (version \([^ )]*\).*/-DPROJECT_VERSION="\1"/p' CMakeLists.txt`
+sdn: sdn.cpp CMakeLists.txt
+ $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
+ -lacl `pkg-config --libs --cflags ncursesw`
+sdn-static: sdn.cpp CMakeLists.txt
+ $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
+ -static-libstdc++ \
+ -Wl,--start-group,-Bstatic \
+ -lacl `pkg-config --static --libs --cflags ncursesw` \
+ -Wl,--end-group,-Bdynamic
clean:
- rm -f sdn
+ rm -f sdn sdn-static
-.PHONY: all clean
+.PHONY: clean
diff --git a/README.adoc b/README.adoc
index 3e713af..28ff6c1 100644
--- a/README.adoc
+++ b/README.adoc
@@ -16,16 +16,21 @@ approach here, and the end result is very friendly to tinkering.
image::sdn.png[align="center"]
+Packages
+--------
+Regular releases are sporadic. git master should be stable enough.
+You can get a package with the latest development version using Arch Linux's
+https://aur.archlinux.org/packages/sdn-git[AUR],
+or as a https://git.janouch.name/p/nixexprs[Nix derivation].
+
Building
--------
Build dependencies: CMake and/or make, a C++14 compiler, pkg-config +
Runtime dependencies: ncursesw, libacl
// Working around libasciidoc's missing support for escaping it like \++
-:doubleplus: ++
-
-Unfortunately most LLVM libc++ versions have a bug that crashes 'sdn' on start.
-Use GNU libstdc{doubleplus} if you're affected.
+Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn'
+on start. Use GNU libstdc{plus}{plus} if you're affected.
$ git clone https://git.janouch.name/p/sdn.git
$ mkdir sdn/build
@@ -45,81 +50,63 @@ Or you can try telling CMake to make a package for you. For Debian it is:
There is also a Makefile you can use to quickly build a binary to be copied
into the PATH of any machine you want to have 'sdn' on.
-zsh
----
-To start using this navigator, put the following in your .zshrc:
-
-....
-sdn-navigate () {
- # ... possibly zle-line-init
- eval "`sdn`"
- [ -z "$cd" ] || cd "$cd"
- [ -z "$insert" ] || LBUFFER="$LBUFFER$insert "
- zle reset-prompt
- # ... possibly zle-line-finish
-}
-zle -N sdn-navigate
-bindkey '\eo' sdn-navigate
-....
-
-bash
-----
-Here we can't reset the prompt from within a `bind -x` handler but there is
-an acceptable workaround:
-
-....
-sdn-navigate () {
- SDN_L=$READLINE_LINE SDN_P=$READLINE_POINT
- READLINE_LINE=
-
- eval "`sdn`"
- [[ -z "$cd" ]] || cd "$cd"
- [[ -z "$insert" ]] || {
- SDN_L="${SDN_L:0:$SDN_P}$insert ${SDN_L:$SDN_P}"
- ((SDN_P=SDN_P+${#insert}+1))
- }
-}
-sdn-restore () {
- READLINE_LINE=$SDN_L READLINE_POINT=$SDN_P
- unset SDN_L SDN_P
-}
-
-bind -x '"\200": sdn-navigate'
-bind -x '"\201": sdn-restore'
-bind '"\eo":"\200\C-m\201"'
-....
-
-Colors
-------
+Configuration
+-------------
+For a slightly more technical explanation please refer to manual pages.
+
+Integration
+~~~~~~~~~~~
+The package contains an installation script called 'sdn-install' which will bind
+'sdn' to Alt-o in your shell's initialisation file. The supported shells are:
+
+ - *zsh*: works well
+ - *bash*: minor issue: exiting the navigator confirms an empty prompt
+ - *fish*: works well
+ - *elvish*: version 0.14.1 and above, an unstable API is used, works well
++
+elvish is absolutely perverse. And so is integrating 'sdn' into it because it
+already includes a custom file manager, bound to Ctrl-N (though I find the
+ranger-like interface confusing and resource-demanding).
+
+Colours
+~~~~~~~
Here is an example of a '~/.config/sdn/look' file; the format is similar to
-that of git, only named colors aren't supported:
+that of git, only named colours aren't supported:
....
cursor 231 202
bar 16 255 ul
cwd bold
input
+cmdline 145
....
-Filename colors are taken from the `LS_COLORS` environment variable.
+Filename colours are taken from the `LS_COLORS` environment variable.
Run `dircolors` to get some defaults.
Bindings
---------
+~~~~~~~~
To obtain more vifm-like controls, you may write the following to your
'~/.config/sdn/bindings' file:
....
normal h parent
normal l choose
-normal ? help
....
-Similar software
-----------------
- * https://elvish.io/ is an entire shell with an integrated ranger-like file
- manager on Ctrl-N (I find this confusing and resource-demanding, preferring
- to keep closer to "orthodox file managers")
+Helper programs
+~~~~~~~~~~~~~~~
+The F3 and F4 keys are normally bound to actions 'view' and 'edit', similarly to
+Norton Commander and other orthodox file managers. The helper programs used
+here may be changed by setting the PAGER and VISUAL (or EDITOR) environment
+variables.
+
+While it is mostly possible to get 'mcview' working using an invocation like
+`PAGER='mcview -u' sdn`, beware that this helper cannot read files from its
+standard input, nor does it enable overstrike processing by default (F9, could
+be hacked around in 'mc.ext' by turning on the `nroff` switch for a custom file
+extension, just without actually invoking 'nroff'), and thus it can't show the
+program help. 'sdn' is currently optimised for 'less' as the pager.
Contributing and Support
------------------------
diff --git a/sdn-install b/sdn-install
new file mode 100755
index 0000000..0e28390
--- /dev/null
+++ b/sdn-install
@@ -0,0 +1,182 @@
+#!/bin/sh -e
+# sdn-install: integrate sdn with the shell, binding to M-o
+# vim: set sw=2 ts=2 sts=2 et tw=80:
+
+zsh() {
+cat <<'EOF'
+sdn-navigate () {
+ # optionally: zle zle-line-finish
+ while eval "`SDN=1 sdn "$BUFFER" "$CURSOR"`"
+ do
+ [ -z "$cd" ] || cd "$cd"
+ [ -z "$insert" ] || LBUFFER="$LBUFFER$insert "
+ [ -z "$helper" ] && break
+
+ # Workaround for "zsh: suspended (tty output)" when invoking
+ # helpers after the terminal has been resized while running sdn
+ command true
+
+ # Add to history, see https://www.zsh.org/mla/workers/2020/msg00633.html
+ fc -R =(print -- "$helper")
+
+ /bin/sh -c "$helper" </dev/tty || break
+ done
+ # optionally: zle zle-line-init
+ zle reset-prompt
+}
+
+zle -N sdn-navigate
+bindkey '\eo' sdn-navigate
+EOF
+}
+
+bash() {
+cat <<'EOF'
+# We can't make the shell update the prompt on directory changes
+# since there's no way to invoke `prompt_again()` from a `bind -x`
+# handler but we can work around it by submitting a blank line.
+sdn-cursor () {
+ if [[ $BASH_VERSINFO -lt 5 ]]
+ then echo -n "$SDN_L" | wc -m
+ else echo "$SDN_P"
+ fi
+}
+
+sdn-navigate () {
+ SDN_L=$READLINE_LINE SDN_P=$READLINE_POINT
+ READLINE_LINE=
+
+ while eval "`SDN=1 sdn "$SDN_L" "$(sdn-cursor)"`"
+ do
+ [[ -z $cd ]] || cd "$cd"
+ [[ -z $insert ]] || {
+ SDN_L="${SDN_L:0:$SDN_P}$insert ${SDN_L:$SDN_P}"
+ ((SDN_P=SDN_P+${#insert}+1))
+ }
+ [[ -z $helper ]] && break
+ history -s -- "$helper"
+ /bin/sh -c "$helper" || break
+ done
+}
+
+sdn-restore () {
+ READLINE_LINE=$SDN_L READLINE_POINT=$SDN_P
+ unset SDN_L SDN_P
+}
+
+# These never occur in UTF-8: \300-\301 \365-\367 \370-\377
+bind -x '"\300": sdn-navigate'
+bind -x '"\301": sdn-restore'
+bind '"\eo": "\300\C-m\301"'
+EOF
+}
+
+fish() {
+cat <<'EOF'
+function sdn-navigate
+ set --local IFS
+ set --local buffer (commandline)
+ set --local cursor (commandline --cursor)
+ while eval (SDN=1 sdn $buffer $cursor | \
+ string replace -ar '^(.*?)=' 'set --$1 ')
+ test -z "$cd" || cd "$cd"
+ test -z "$insert" || commandline --insert "$insert "
+ test -z "$helper" && break
+ /bin/sh -c "$helper" || break
+ end
+ commandline --function repaint
+end
+bind \eo sdn-navigate
+EOF
+}
+
+elvish() {
+cat <<'EOF'
+edit:insert:binding[Alt-o] = {
+ use str
+ local:reesc = [posix]{ str:replace "'\\''" "''" $posix }
+ local:posix = [cmd]{ /bin/sh -c $cmd </dev/tty >/dev/tty 2>&1 }
+
+ # XXX: the -dot is not a stable API, and may hence break soon
+ # https://elv.sh/ref/builtin.html#do-not-use-functions-and-variables
+ local:buffer = $edit:current-command
+ local:cursor = (str:to-codepoints $buffer[0..$edit:-dot] | count)
+ local:ns = (ns [&])
+ while ?(eval ($reesc (E:SDN=1 sdn $buffer $cursor |
+ sed 's/^local //' | slurp)) &ns=$ns) {
+ if (not-eq $ns[cd] "") { cd $ns[cd] }
+ if (not-eq $ns[insert] "") { edit:insert-at-dot $ns[insert]" " }
+ if (or (eq $ns[helper] "") (not ?($posix $ns[helper]))) { break }
+ }
+ edit:redraw &full=$true
+}
+EOF
+}
+
+shell= path=
+while getopts s:f:h opt
+do
+ case $opt in
+ s) shell=$OPTARG;;
+ f) path=$OPTARG;;
+ *) echo "Usage: $0 [-s SHELL] [-f RCPATH | -]"; exit 2
+ esac
+done
+
+# Figure out the shell to integrate with
+login=$(basename "$SHELL")
+actual=$(ps -p $$ -o ppid= | xargs ps -o comm= -p)
+if [ -z "$shell" ]
+then
+ if [ "$login" != "$actual" ]
+ then
+ echo "Conflict between login ($login) and invoking ($actual) shell."
+ echo "Specify the shell with the -s option."
+ exit 1
+ fi
+ shell=$actual
+fi
+
+# Figure out the default initialisation file
+case "$shell" in
+zsh|bash)
+ rc=~/.${shell}rc;;
+fish)
+ rc=${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/sdn.fish;;
+elvish)
+ rc=~/.elvish/rc.elv;;
+*)
+ echo "$shell is not supported."
+ exit 1
+esac
+
+# Just print out the snippet if requested
+if [ "$path" = "-" ]
+then
+ $shell
+ exit 0
+fi
+
+# Finally append to or update the appropriate file
+b="# sdn-install begin"
+e="# sdn-install end"
+[ -z "$path" ] && path=$rc
+mkdir -p "$(dirname "$path")"
+touch "$path"
+
+if ! grep -q "^$b" "$path" 2>/dev/null || ! grep -q "^$e" "$path" 2>/dev/null
+then
+ printf "\n$b\n%s\n$e\n" "$($shell)" >> "$path"
+ echo "The snippet has been added to $path"
+ exit 0
+fi
+
+# POSIX-compliant in-place sed, trying to retain permissions here
+temp=$path.sdn.new
+cp -p -- "$path" "$temp"
+sed < "$path" > "$temp" "/^$b/,/^$e/c\\
+$b\\
+$($shell | sed 's/\\/&&/g; s/$/\\/')
+$e"
+mv -- "$temp" "$path"
+echo "The snippet in $path has been updated."
diff --git a/sdn-install.1 b/sdn-install.1
new file mode 100644
index 0000000..c186f63
--- /dev/null
+++ b/sdn-install.1
@@ -0,0 +1,35 @@
+.Dd October 27, 2020
+.Dt SDN-INSTALL 1
+.Os Linux
+.Sh NAME
+.Nm sdn-install
+.Nd integrate sdn with the shell
+.Sh SYNOPSIS
+.Nm sdn-install
+.Op Fl s Ar shell
+.Op Fl p Ar - | rcpath
+.Sh DESCRIPTION
+.Nm
+integrates
+.Xr sdn 1
+with your shell, binding it to M-o. If the navigator has already been
+integrated, it updates the snippet in-place.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl p Ar -
+Merely print the integration snippet for the appropriate shell to the standard
+output, not changing anything.
+.It Fl p Ar rcpath
+Install the integration snippet into a different shell initialization file than
+the default one for your user.
+.It Fl s Ar shell
+If you want to integrate
+.Xr sdn 1
+with a different shell than the one you're running, use this option to specify
+it.
+.El
+.Sh REPORTING BUGS
+Use
+.Lk https://git.janouch.name/p/sdn
+to report bugs, request features, or submit pull requests.
diff --git a/sdn.1 b/sdn.1
new file mode 100644
index 0000000..097a5de
--- /dev/null
+++ b/sdn.1
@@ -0,0 +1,131 @@
+\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
+.Dd October 27, 2020
+.Dt SDN 1
+.Os Linux
+.Sh NAME
+.Nm sdn
+.Nd directory navigator
+.Sh SYNOPSIS
+.Nm sdn
+.Op Ar line Ar point
+.Nm sdn
+.Cm --version
+.Sh DESCRIPTION
+.Nm
+is a simple directory navigator that you can launch while editing shell
+commands.
+Use the
+.Xr sdn-install 1
+script to integrate it with your shell, then invoke it at any time with M-o.
+.Pp
+Press F1 to get a list of active key bindings and their assigned actions,
+grouped by their contexts.
+.Pp
+Program arguments are only used by integration snippets to forward the parent
+shell's command line.
+The
+.Ar point
+is given in terms of characters.
+.Sh OPTIONS
+While some behaviour can be toggled from within the program, some can only be
+changed by modifying configuration files manually.
+.Pp
+The files follow a simple syntax derived from the Bourne shell: each option is
+on its own line, with words separated by linear whitespace.
+Comments start with a hash (#) and continue until the end of the line.
+All special characters may be quoted using either a backslash or single-quoted
+strings.
+.Pp
+The options and the default key bindings controlling them are as follows:
+.Bl -tag
+.It full-view Em bool No (t)
+If non-zero, the equivalent format to
+.Ql ls -l
+is used to display directory contents rather than simply listing the filenames.
+.It gravity Em bool
+If non-zero, all entries stick to the bottom of the screen, i.e., all empty
+space is at the top.
+.It reverse-sort Em bool No (R)
+If non-zero, the order of entries is reversed.
+.It show-hidden Em bool No (M-.)
+If non-zero, filenames beginning with a full stop are shown.
+.It ext-helpers Em bool
+If non-zero, viewers and editors are launched from the parent shell.
+This way you can suspend them and use job control features of the shell.
+However it also enforces any pending change to the shell's working directory.
+.It sort-column Em number No (< >)
+The zero-based index of the
+.Ql full-view
+column that entries are ordered by.
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width 15n
+.It Ev LS_COLORS
+Used to retrieve filename colours.
+The format is described in
+.Xr dir_colors 5
+and you can use the
+.Xr dircolors 1
+utility to initialize this variable.
+.It Ev PAGER
+The viewer program to be launched by the F3 key binding as well as to show
+the internal help message.
+If none is set, it defaults to
+.Xr less 1 .
+.It Ev VISUAL , Ev EDITOR
+The editor program to be launched by the F4 key binding.
+If neither variable is set, it defaults to
+.Xr vi 1 .
+.El
+.Sh FILES
+.Bl -tag -width 25n -compact
+.It Pa ~/.config/sdn/config
+Program configuration and navigation state, initialized or overwritten on exit.
+.It Pa ~/.config/sdn/bindings
+Custom key binding overrides.
+.It Pa ~/.config/sdn/look
+Redefine terminal attributes for UI elements.
+.El
+.Sh EXAMPLES
+.Ss Pa bindings
+Key names or combinations follow the Emacs syntax for Control and Meta prefixes
+and
+.Xr terminfo 5
+names are used for special keys.
+To obtain more vifm-like controls and Windows-like quit abilities:
+.Bd -literal -offset indent
+normal h parent
+normal l choose
+normal M-f4 quit
+.Ed
+.Pp
+Midnight Commander binds the same traversal actions to sequences normally
+unknown to ncurses, due to them being missing from terminfo.
+You'll need to define them manually to match your terminal.
+For rxvt, that would be:
+.Bd -literal -offset indent
+define C-ppage ^[[5^
+define C-npage ^[[6^
+normal C-ppage parent
+normal C-npage choose
+.Ed
+.Pp
+Escape characters must be inserted verbatim, e.g., by pressing C-v ESC in vi,
+or C-q ESC in Emacs.
+.Ss Pa look
+Terminal attributes are accepted in a format similar to that of
+.Xr git-config 1 ,
+only named colours aren't supported.
+For a black-on-white terminal supporting 256 colours, a theme such as the
+following may work:
+.Bd -literal -offset indent
+cursor 231 202
+bar 16 255 ul
+cwd bold
+input
+cmdline 145
+.Ed
+.Sh REPORTING BUGS
+Use
+.Lk https://git.janouch.name/p/sdn
+to report bugs, request features, or submit pull requests.
diff --git a/sdn.cpp b/sdn.cpp
index 2de9f4e..9496475 100644
--- a/sdn.cpp
+++ b/sdn.cpp
@@ -1,7 +1,7 @@
//
// sdn: simple directory navigator
//
-// Copyright (c) 2017 - 2018, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -18,37 +18,44 @@
// May be required for ncursesw and we generally want it all anyway
#define _XOPEN_SOURCE_EXTENDED
-#include <string>
-#include <vector>
-#include <locale>
-#include <iostream>
#include <algorithm>
-#include <cwchar>
#include <climits>
#include <cstdlib>
#include <cstring>
+#include <cwchar>
#include <fstream>
+#include <iostream>
+#include <locale>
#include <map>
-#include <tuple>
#include <memory>
+#include <string>
+#include <tuple>
+#include <vector>
-#include <unistd.h>
#include <dirent.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/acl.h>
#include <fcntl.h>
-#include <pwd.h>
+#include <fnmatch.h>
#include <grp.h>
#include <libgen.h>
+#include <pwd.h>
+#include <signal.h>
+#include <sys/acl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
#include <time.h>
+#include <unistd.h>
+#include <acl/libacl.h>
+#include <ncurses.h>
#include <sys/inotify.h>
-#include <sys/xattr.h>
#include <sys/types.h>
#include <sys/wait.h>
-#include <acl/libacl.h>
-#include <ncurses.h>
+#include <sys/xattr.h>
+
+// To implement cbreak() with disabled ^S that gets reënabled on endwin()
+#define NCURSES_INTERNALS
+#include <term.h>
+#undef CTRL // term.h -> termios.h -> sys/ttydefaults.h, too simplistic
// Unicode is complex enough already and we might make assumptions
#ifndef __STDC_ISO_10646__
@@ -92,8 +99,8 @@ fun to_mb (const wstring &wide) -> string {
return mb;
}
-fun prefix_length (const wstring &in, const wstring &of) -> int {
- int score = 0;
+fun prefix_length (const wstring &in, const wstring &of) -> size_t {
+ size_t score = 0;
for (size_t i = 0; i < of.size () && in.size () >= i && in[i] == of[i]; i++)
score++;
return score;
@@ -113,6 +120,28 @@ fun split (const string &s, const string &sep) -> vector<string> {
vector<string> result; split (s, sep, result); return result;
}
+fun untilde (const string &path) -> string {
+ if (path.empty ())
+ return path;
+
+ string tail = path.substr (1);
+ if (path[0] == '\\')
+ return tail;
+ if (path[0] != '~')
+ return path;
+
+ // If there is something between the ~ and the first / (or the EOS)
+ if (size_t until_slash = strcspn (tail.c_str (), "/")) {
+ if (const auto *pw = getpwnam (tail.substr (0, until_slash).c_str ()))
+ return pw->pw_dir + tail.substr (until_slash);
+ } else if (const auto *home = getenv ("HOME")) {
+ return home + tail;
+ } else if (const auto *pw = getpwuid (getuid ())) {
+ return pw->pw_dir + tail;
+ }
+ return path;
+}
+
fun needs_shell_quoting (const string &v) -> bool {
// IEEE Std 1003.1 sh + the exclamation mark because of csh/bash
// history expansion, implicitly also the NUL character
@@ -136,11 +165,11 @@ fun shell_escape (const string &v) -> string {
}
fun parse_line (istream &is, vector<string> &out) -> bool {
- enum {STA, DEF, COM, ESC, WOR, QUO, STATES};
- enum {TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6};
- enum {TWOR = TAKE | WOR};
+ enum { STA, DEF, COM, ESC, WOR, QUO, STATES };
+ enum { TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6 };
+ enum { TWOR = TAKE | WOR };
- // We never transition back to the start state, so it can stay as a noop
+ // We never transition back to the start state, so it can stay as a no-op
static char table[STATES][7] = {
// state EOF SP, TAB ' # \ LF default
/* STA */ {ERROR, DEF, QUO, COM, ESC, STOP, TWOR},
@@ -221,8 +250,9 @@ fun capitalize (const string &s) -> string {
return result;
}
-/// Underlining for teletypes, also imitated in more(1) and less(1)
-fun underline (const string& s) -> string {
+/// Underlining for teletypes (also called overstriking),
+/// also imitated in more(1) and less(1)
+fun underline (const string &s) -> string {
string result;
for (auto c : s)
result.append ({c, 8, '_'});
@@ -244,7 +274,7 @@ fun xdg_config_home () -> string {
fun xdg_config_find (const string &suffix) -> unique_ptr<ifstream> {
vector<string> dirs {xdg_config_home ()};
const char *system_dirs = getenv ("XDG_CONFIG_DIRS");
- split (system_dirs ? system_dirs : "/etc/xdg", ":", dirs);
+ split ((system_dirs && *system_dirs) ? system_dirs : "/etc/xdg", ":", dirs);
for (const auto &dir : dirs) {
if (dir[0] != '/')
continue;
@@ -294,16 +324,16 @@ fun invert (cchar_t &ch) {
}
fun apply_attrs (const wstring &w, attr_t attrs) -> ncstring {
- ncstring res;
- for (auto c : w)
- res += cchar (attrs, c);
+ ncstring res (w.size (), cchar_t {});
+ for (size_t i = 0; i < w.size (); i++)
+ res[i] = cchar (attrs, w[i]);
return res;
}
fun sanitize_char (chtype attrs, wchar_t c) -> ncstring {
- if (c < 32)
+ if (c < 32 || c == 0x7f)
return {cchar (attrs | A_REVERSE, L'^'),
- cchar (attrs | A_REVERSE, c + 64)};
+ cchar (attrs | A_REVERSE, (c + 64) & 0x7f)};
if (!iswprint (c))
return {cchar (attrs | A_REVERSE, L'?')};
return {cchar (attrs, c)};
@@ -330,13 +360,6 @@ fun print (const ncstring &nc, int limit) -> int {
return total_width;
}
-fun compute_width (const wstring &w) -> int {
- int total = 0;
- for (const auto &c : w)
- total += wcwidth (c);
- return total;
-}
-
fun compute_width (const ncstring &nc) -> int {
int total = 0;
for (const auto &c : nc)
@@ -383,16 +406,19 @@ fun decode_attrs (const vector<string> &attrs) -> chtype {
enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define KEY(name) (SYM | KEY_ ## name)
-#define CTRL 31 &
+#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \
XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \
- XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) \
+ XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
- XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) \
+ XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \
- XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE)
+ XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) XX(INPUT_DELETE) \
+ XX(INPUT_B_KILL_WORD) XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) \
+ XX(INPUT_QUOTED_INSERT) \
+ XX(INPUT_BACKWARD) XX(INPUT_FORWARD) XX(INPUT_BEGINNING) XX(INPUT_END)
#define XX(name) ACTION_ ## name,
enum action { ACTIONS(XX) ACTION_COUNT };
@@ -405,32 +431,51 @@ static const char *g_action_names[] = {ACTIONS(XX)};
static map<wint_t, action> g_normal_actions {
{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
- {KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT}, {'h', ACTION_HELP},
+ {KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
+ {KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT},
{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
// M-o ought to be the same shortcut the navigator is launched with
{ALT | 'o', ACTION_QUIT},
{'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
- {'k', ACTION_UP}, {CTRL 'p', ACTION_UP}, {KEY (UP), ACTION_UP},
- {'j', ACTION_DOWN}, {CTRL 'n', ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
+ {'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
+ {'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
{'G', ACTION_BOTTOM}, {ALT | '>', ACTION_BOTTOM}, {KEY(END), ACTION_BOTTOM},
{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
- {CTRL 'y', ACTION_SCROLL_UP}, {CTRL 'e', ACTION_SCROLL_DOWN},
- {'c', ACTION_CHDIR}, {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
- {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH},
+ {CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
+ {'z', ACTION_CENTER},
+ {'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
+ {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
+ {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
+ {KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR},
{'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL},
{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
- {CTRL 'L', ACTION_REDRAW}, {'r', ACTION_RELOAD},
+ {CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
};
static map<wint_t, action> g_input_actions {
- {27, ACTION_INPUT_ABORT}, {CTRL 'g', ACTION_INPUT_ABORT},
+ {27, ACTION_INPUT_ABORT}, {CTRL ('G'), ACTION_INPUT_ABORT},
{L'\r', ACTION_INPUT_CONFIRM}, {KEY (ENTER), ACTION_INPUT_CONFIRM},
- {KEY (BACKSPACE), ACTION_INPUT_B_DELETE},
+ // Sometimes terminfo is wrong, we need to accept both of these
+ {L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE},
+ {KEY (BACKSPACE), ACTION_INPUT_B_DELETE}, {KEY (DC), ACTION_INPUT_DELETE},
+ {CTRL ('W'), ACTION_INPUT_B_KILL_WORD}, {CTRL ('D'), ACTION_INPUT_DELETE},
+ {CTRL ('U'), ACTION_INPUT_B_KILL_LINE},
+ {CTRL ('K'), ACTION_INPUT_KILL_LINE},
+ {CTRL ('V'), ACTION_INPUT_QUOTED_INSERT},
+ {CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD},
+ {CTRL ('F'), ACTION_INPUT_FORWARD}, {KEY (RIGHT), ACTION_INPUT_FORWARD},
+ {CTRL ('A'), ACTION_INPUT_BEGINNING}, {KEY (HOME), ACTION_INPUT_BEGINNING},
+ {CTRL ('E'), ACTION_INPUT_END}, {KEY (END), ACTION_INPUT_END},
+};
+static map<wint_t, action> g_search_actions {
+ {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
+ {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
};
static const map<string, map<wint_t, action>*> g_binding_contexts {
{"normal", &g_normal_actions}, {"input", &g_input_actions},
+ {"search", &g_search_actions},
};
#define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \
@@ -450,7 +495,7 @@ static const char *g_ls_colors[] = {LS(XX)};
struct stringcaseless {
bool operator () (const string &a, const string &b) const {
- const auto &c = locale::classic();
+ const auto &c = locale::classic ();
return lexicographical_compare (begin (a), end (a), begin (b), end (b),
[&](char m, char n) { return tolower (m, c) < tolower (n, c); });
}
@@ -470,6 +515,7 @@ struct level {
};
static struct {
+ ncstring cmdline; ///< Outer command line
string cwd; ///< Current working directory
string start_dir; ///< Starting directory
vector<entry> entries; ///< Current directory entries
@@ -479,6 +525,7 @@ static struct {
bool gravity; ///< Entries are shoved to the bottom
bool reverse_sort; ///< Reverse sort
bool show_hidden; ///< Show hidden files
+ bool ext_helpers; ///< Launch helpers externally
int max_widths[entry::COLUMNS]; ///< Column widths
int sort_column = entry::FILENAME; ///< Sorting column
int sort_flash_ttl; ///< Sorting column flash TTL
@@ -487,6 +534,7 @@ static struct {
int message_ttl; ///< Time to live for the message
string chosen; ///< Chosen item for the command line
+ string ext_helper; ///< External helper to run
bool no_chdir; ///< Do not tell the shell to chdir
bool quitting; ///< Whether we should quit already
@@ -494,13 +542,17 @@ static struct {
bool out_of_date; ///< Entries may be out of date
const wchar_t *editor; ///< Prompt string for editing
+ wstring editor_info; ///< Right-side prompt while editing
wstring editor_line; ///< Current user input
+ int editor_cursor = 0; ///< Cursor position
+ bool editor_inserting; ///< Inserting a literal character
void (*editor_on_change) (); ///< Callback on editor change
- void (*editor_on_confirm) (); ///< Callback on editor confirmation
+ map<action, void (*) ()> editor_on; ///< Handlers for custom actions
- enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_COUNT };
- chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0};
- const char *attr_names[AT_COUNT] = {"cursor", "bar", "cwd", "input"};
+ enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE, AT_COUNT };
+ chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0};
+ const char *attr_names[AT_COUNT] =
+ {"cursor", "bar", "cwd", "input", "info", "cmdline"};
map<int, chtype> ls_colors; ///< LS_COLORS decoded
map<string, chtype> ls_exts; ///< LS_COLORS file extensions
@@ -508,12 +560,13 @@ static struct {
map<string, wint_t, stringcaseless> name_to_key;
map<wint_t, string> key_to_name;
+ map<string, wint_t> custom_keys;
string action_names[ACTION_COUNT]; ///< Stylized action names
// Refreshed by reload():
- map<uid_t, string> unames; ///< User names by UID
- map<gid_t, string> gnames; ///< Group names by GID
+ map<uid_t, wstring> unames; ///< User names by UID
+ map<gid_t, wstring> gnames; ///< Group names by GID
struct tm now; ///< Current local time for display
} g;
@@ -560,8 +613,8 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
set (LS_STICKY_OTHER_WRITABLE);
} else if (S_ISLNK (info.st_mode)) {
type = LS_SYMLINK;
- if (!e.target_info.st_mode
- && (ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
+ if (!e.target_info.st_mode &&
+ (ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
type = LS_ORPHAN;
} else if (S_ISFIFO (info.st_mode)) {
type = LS_FIFO;
@@ -593,9 +646,8 @@ fun make_entry (const struct dirent *f) -> entry {
e.info.st_mode = DTTOIF (f->d_type);
auto &info = e.info;
- // TODO: benchmark just readdir() vs. lstat(), also on dead mounts;
- // it might make sense to stat asynchronously in threads
- // http://lkml.iu.edu/hypermail//linux/kernel/0804.3/1616.html
+ // io_uring is only at most about 50% faster, though it might help with
+ // slowly statting devices, at a major complexity cost.
if (lstat (f->d_name, &info)) {
e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode),
L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0);
@@ -621,19 +673,21 @@ fun make_entry (const struct dirent *f) -> entry {
}
auto mode = decode_mode (info.st_mode);
- // This is a Linux-only extension
+ // We're using a laughably small subset of libacl: this translates to
+ // two lgetxattr() calls, the results of which are compared with
+ // specific architecture-dependent constants. Linux-only.
if (acl_extended_file_nofollow (f->d_name) > 0)
mode += L"+";
e.cols[entry::MODES] = apply_attrs (mode, 0);
auto usr = g.unames.find (info.st_uid);
e.cols[entry::USER] = (usr != g.unames.end ())
- ? apply_attrs (to_wide (usr->second), 0)
+ ? apply_attrs (usr->second, 0)
: apply_attrs (to_wstring (info.st_uid), 0);
auto grp = g.gnames.find (info.st_gid);
e.cols[entry::GROUP] = (grp != g.gnames.end ())
- ? apply_attrs (to_wide (grp->second), 0)
+ ? apply_attrs (grp->second, 0)
: apply_attrs (to_wstring (info.st_gid), 0);
auto size = to_wstring (info.st_size);
@@ -643,16 +697,16 @@ fun make_entry (const struct dirent *f) -> entry {
else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
e.cols[entry::SIZE] = apply_attrs (size, 0);
- char buf[32] = "";
+ wchar_t buf[32] = L"";
auto tm = localtime (&info.st_mtime);
- strftime (buf, sizeof buf,
- (tm->tm_year == g.now.tm_year) ? "%b %e %H:%M" : "%b %e %Y", tm);
- e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0);
+ wcsftime (buf, sizeof buf / sizeof *buf,
+ (tm->tm_year == g.now.tm_year) ? L"%b %e %H:%M" : L"%b %e %Y", tm);
+ e.cols[entry::MTIME] = apply_attrs (buf, 0);
auto &fn = e.cols[entry::FILENAME] =
apply_attrs (to_wide (e.filename), ls_format (e, false));
if (!e.target_path.empty ()) {
- fn.append (apply_attrs (to_wide (" -> "), 0));
+ fn.append (apply_attrs (L" -> ", 0));
fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
}
return e;
@@ -662,7 +716,7 @@ fun inline visible_lines () -> int { return max (0, LINES - 2); }
fun update () {
int start_column = g.full_view ? 0 : entry::FILENAME;
- static int alignment[entry::COLUMNS] = { -1, -1, -1, 1, 1, -1 };
+ static int alignment[entry::COLUMNS] = {-1, -1, -1, 1, 1, -1};
erase ();
int available = visible_lines ();
@@ -713,21 +767,35 @@ fun update () {
curs_set (0);
if (g.editor) {
move (LINES - 1, 0);
- auto p = apply_attrs (wstring (g.editor) + L": ", 0);
- move (LINES - 1, print (p + apply_attrs (g.editor_line, 0), COLS - 1));
+ auto prompt = apply_attrs (wstring (g.editor) + L": ", 0),
+ line = apply_attrs (g.editor_line, 0),
+ info = apply_attrs (g.editor_info, g.attrs[g.AT_INFO]);
+
+ auto info_width = compute_width (info);
+ if (print (prompt + line, COLS - 1) < COLS - info_width) {
+ move (LINES - 1, COLS - info_width);
+ print (info, info_width);
+ }
+
+ auto start = sanitize (prompt + line.substr (0, g.editor_cursor));
+ move (LINES - 1, compute_width (start));
curs_set (1);
} else if (!g.message.empty ()) {
move (LINES - 1, 0);
print (apply_attrs (g.message, 0), COLS);
+ } else if (!g.cmdline.empty ()) {
+ move (LINES - 1, 0);
+ print (g.cmdline, COLS);
}
refresh ();
}
fun operator< (const entry &e1, const entry &e2) -> bool {
- auto t1 = make_tuple (e1.filename != "..",
+ static string dotdot {".."};
+ auto t1 = make_tuple (e1.filename != dotdot,
!S_ISDIR (e1.info.st_mode) && !S_ISDIR (e1.target_info.st_mode));
- auto t2 = make_tuple (e2.filename != "..",
+ auto t2 = make_tuple (e2.filename != dotdot,
!S_ISDIR (e2.info.st_mode) && !S_ISDIR (e2.target_info.st_mode));
if (t1 != t2)
return t1 < t2;
@@ -759,24 +827,57 @@ fun operator< (const entry &e1, const entry &e2) -> bool {
return a.filename < b.filename;
}
-fun reload (const string &old_cwd) {
- g.unames.clear();
+fun at_cursor () -> const entry & {
+ static entry invalid;
+ return g.cursor >= int (g.entries.size ()) ? invalid : g.entries[g.cursor];
+}
+
+fun focus (const string &anchor) {
+ if (!anchor.empty ()) {
+ for (size_t i = 0; i < g.entries.size (); i++)
+ if (g.entries[i].filename == anchor)
+ g.cursor = i;
+ }
+}
+
+fun resort (const string anchor = at_cursor ().filename) {
+ sort (begin (g.entries), end (g.entries));
+ focus (anchor);
+}
+
+fun show_message (const string &message, int ttl = 30) {
+ g.message = to_wide (message);
+ g.message_ttl = ttl;
+}
+
+fun reload (bool keep_anchor) {
+ g.unames.clear ();
while (auto *ent = getpwent ())
- g.unames.emplace (ent->pw_uid, ent->pw_name);
- endpwent();
+ g.unames.emplace (ent->pw_uid, to_wide (ent->pw_name));
+ endpwent ();
- g.gnames.clear();
+ g.gnames.clear ();
while (auto *ent = getgrent ())
- g.gnames.emplace (ent->gr_gid, ent->gr_name);
- endgrent();
+ g.gnames.emplace (ent->gr_gid, to_wide (ent->gr_name));
+ endgrent ();
string anchor;
- if (!g.entries.empty ())
- anchor = g.entries[g.cursor].filename;
+ if (keep_anchor)
+ anchor = at_cursor ().filename;
auto now = time (NULL); g.now = *localtime (&now);
auto dir = opendir (".");
g.entries.clear ();
+ if (!dir) {
+ show_message (strerror (errno));
+ if (g.cwd != "/") {
+ struct dirent f = {};
+ strncpy (f.d_name, "..", sizeof f.d_name);
+ f.d_type = DT_DIR;
+ g.entries.push_back (make_entry (&f));
+ }
+ goto readfail;
+ }
while (auto f = readdir (dir)) {
string name = f->d_name;
// Two dots are for navigation but this ain't as useful
@@ -786,22 +887,19 @@ fun reload (const string &old_cwd) {
g.entries.push_back (make_entry (f));
}
closedir (dir);
- sort (begin (g.entries), end (g.entries));
- g.out_of_date = false;
- if (g.cwd == old_cwd && !anchor.empty ()) {
- for (size_t i = 0; i < g.entries.size (); i++)
- if (g.entries[i].filename == anchor)
- g.cursor = i;
- }
+readfail:
+ g.out_of_date = false;
for (int col = 0; col < entry::COLUMNS; col++) {
auto &longest = g.max_widths[col] = 0;
for (const auto &entry : g.entries)
longest = max (longest, compute_width (entry.cols[col]));
}
- g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
- g.offset = min (g.offset, int (g.entries.size ()) - 1);
+ resort (anchor);
+
+ g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1));
+ g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1));
if (g.inotify_wd != -1)
inotify_rm_watch (g.inotify_fd, g.inotify_wd);
@@ -811,14 +909,20 @@ fun reload (const string &old_cwd) {
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
}
-fun show_message (const string &message, int ttl = 30) {
- g.message = to_wide (message);
- g.message_ttl = ttl;
-}
+fun run_program (initializer_list<const char *> list, const string &filename) {
+ if (g.ext_helpers) {
+ // XXX: this doesn't try them all out, though it shouldn't make any
+ // noticeable difference
+ const char *found = nullptr;
+ for (auto program : list)
+ if ((found = program))
+ break;
+ g.ext_helper = found + (" -- " + shell_escape (filename));
+ g.quitting = true;
+ return;
+ }
-fun run_program (initializer_list<const char*> list, const string &filename) {
endwin ();
-
switch (pid_t child = fork ()) {
int status;
case -1:
@@ -828,8 +932,8 @@ fun run_program (initializer_list<const char*> list, const string &filename) {
setpgid (0, 0);
tcsetpgrp (STDOUT_FILENO, getpgid (0));
- for (auto pager : list)
- if (pager) execl ("/bin/sh", "/bin/sh", "-c", (string (pager)
+ for (auto program : list)
+ if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program)
+ " " + shell_escape (filename)).c_str (), NULL);
_exit (EXIT_FAILURE);
default:
@@ -839,8 +943,14 @@ fun run_program (initializer_list<const char*> list, const string &filename) {
// We don't provide job control--don't let us hang after ^Z
while (waitpid (child, &status, WUNTRACED) > -1 && WIFSTOPPED (status))
if (WSTOPSIG (status) == SIGTSTP)
- kill (child, SIGCONT);
+ kill (-child, SIGCONT);
tcsetpgrp (STDOUT_FILENO, getpgid (0));
+
+ if (WIFEXITED (status) && WEXITSTATUS (status)) {
+ printf ("Helper returned non-zero exit status %d. "
+ "Press Enter to continue.\n", WEXITSTATUS (status));
+ string dummy; getline (cin, dummy);
+ }
}
refresh ();
@@ -848,7 +958,9 @@ fun run_program (initializer_list<const char*> list, const string &filename) {
}
fun view (const string &filename) {
- run_program ({(const char *) getenv ("PAGER"), "pager", "cat"}, filename);
+ // XXX: we cannot realistically detect that the pager hasn't made a pause
+ // at the end of the file, so we can't ensure all contents have been seen
+ run_program ({(const char *) getenv ("PAGER"), "less", "cat"}, filename);
}
fun edit (const string &filename) {
@@ -872,7 +984,7 @@ fun run_pager (FILE *contents) {
dup2 (fileno (contents), STDIN_FILENO);
// Behaviour copies man-db's man(1), similar to POSIX man(1)
- for (auto pager : {(const char *) getenv ("PAGER"), "pager", "cat"})
+ for (auto pager : {(const char *) getenv ("PAGER"), "less", "cat"})
if (pager) execl ("/bin/sh", "/bin/sh", "-c", pager, NULL);
_exit (EXIT_FAILURE);
default:
@@ -893,8 +1005,8 @@ fun encode_key (wint_t key) -> string {
wchar_t bare = key & ~ALT;
if (g.key_to_name.count (bare))
encoded.append (capitalize (g.key_to_name.at (bare)));
- else if (bare < 32)
- encoded.append ("C-").append ({char (tolower (bare + 64))});
+ else if (bare < 32 || bare == 0x7f)
+ encoded.append ("C-").append ({char (tolower ((bare + 64) & 0x7f))});
else
encoded.append (to_mb ({bare}));
return encoded;
@@ -908,11 +1020,13 @@ fun show_help () {
for (const auto &kv : g_binding_contexts) {
fprintf (contents, "%s\n",
underline (capitalize (kv.first + " key bindings")).c_str ());
- for (const auto &kv : *kv.second) {
- auto key = encode_key (kv.first);
- key.append (max (0, 10 - compute_width (to_wide (key))), ' ');
- fprintf (contents, "%s %s\n",
- key.c_str (), g.action_names[kv.second].c_str ());
+ map<action, string> agg;
+ for (const auto &kv : *kv.second)
+ agg[kv.second] += encode_key (kv.first) + " ";
+ for (const auto &kv : agg) {
+ auto action = g.action_names[kv.first];
+ action.append (max (0, 20 - int (action.length ())), ' ');
+ fprintf (contents, "%s %s\n", action.c_str (), kv.second.c_str ());
}
fprintf (contents, "\n");
}
@@ -920,11 +1034,40 @@ fun show_help () {
fclose (contents);
}
-fun search (const wstring &needle) {
- int best = g.cursor, best_n = 0;
- for (int i = 0; i < int (g.entries.size ()); i++) {
- auto o = (i + g.cursor) % g.entries.size ();
- int n = prefix_length (to_wide (g.entries[o].filename), needle);
+fun match (const wstring &needle, int push) -> int {
+ string pattern = to_mb (needle) + "*";
+ bool jump_to_first = push || fnmatch (pattern.c_str (),
+ g.entries[g.cursor].filename.c_str (), 0) == FNM_NOMATCH;
+ int best = g.cursor, matches = 0, step = push + !push;
+ for (int i = 0, count = g.entries.size (); i < count; i++) {
+ int o = (g.cursor + (count + i * step) + (count + push)) % count;
+ if (!fnmatch (pattern.c_str (), g.entries[o].filename.c_str (), 0)
+ && !matches++ && jump_to_first)
+ best = o;
+ }
+ g.cursor = best;
+ return matches;
+}
+
+fun match_interactive (int push) {
+ int matches = match (g.editor_line, push);
+ if (g.editor_line.empty ())
+ g.editor_info.clear ();
+ else if (matches == 0)
+ g.editor_info = L"(no match)";
+ else if (matches == 1)
+ g.editor_info = L"(1 match)";
+ else
+ g.editor_info = L"(" + to_wstring (matches) + L" matches)";
+}
+
+/// Stays on the current item unless there are better matches
+fun lookup (const wstring &needle) {
+ int best = g.cursor;
+ size_t best_n = 0;
+ for (int i = 0, count = g.entries.size (); i < count; i++) {
+ int o = (g.cursor + i) % count;
+ size_t n = prefix_length (to_wide (g.entries[o].filename), needle);
if (n > best_n) {
best = o;
best_n = n;
@@ -934,16 +1077,16 @@ fun search (const wstring &needle) {
}
fun fix_cursor_and_offset () {
- g.cursor = max (g.cursor, 0);
g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
+ g.cursor = max (g.cursor, 0);
// Decrease the offset when more items can suddenly fit
int pushable = visible_lines () - (int (g.entries.size ()) - g.offset);
g.offset -= max (pushable, 0);
// Make sure the cursor is visible
- g.offset = max (g.offset, 0);
g.offset = min (g.offset, int (g.entries.size ()) - 1);
+ g.offset = max (g.offset, 0);
if (g.offset > g.cursor)
g.offset = g.cursor;
@@ -957,7 +1100,19 @@ fun is_ancestor_dir (const string &ancestor, const string &of) -> bool {
return of[ancestor.length ()] == '/' || (ancestor == "/" && ancestor != of);
}
-fun pop_levels () {
+/// If `path` is equal to the `current` directory, or lies underneath it,
+/// return it as a relative path
+fun relativize (string current, const string &path) -> string {
+ if (current == path)
+ return ".";
+ if (current.back () != '/')
+ current += '/';
+ if (!strncmp (current.c_str (), path.c_str (), current.length ()))
+ return path.substr (current.length ());
+ return path;
+}
+
+fun pop_levels (const string &old_cwd) {
string anchor; auto i = g.levels.rbegin ();
while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) {
if (i->path == g.cwd) {
@@ -968,9 +1123,16 @@ fun pop_levels () {
i++;
g.levels.pop_back ();
}
+
+ // Don't pick up bullshit from foreign history entries, especially for /
+ if (is_ancestor_dir (g.cwd, old_cwd)) {
+ auto subpath = relativize (g.cwd, old_cwd);
+ anchor = subpath.substr (0, subpath.find ('/'));
+ }
+
fix_cursor_and_offset ();
- if (!anchor.empty () && g.entries[g.cursor].filename != anchor)
- search (to_wide (anchor));
+ if (!anchor.empty () && at_cursor ().filename != anchor)
+ lookup (to_wide (anchor));
}
fun explode_path (const string &path, vector<string> &out) {
@@ -1000,18 +1162,6 @@ fun absolutize (const string &abs_base, const string &path) -> string {
return abs_base + "/" + path;
}
-/// If `path` is equal to the `current` directory, or lies underneath it,
-/// return it as a relative path
-fun relativize (string current, const string &path) -> string {
- if (current == path)
- return ".";
- if (current.back () != '/')
- current += '/';
- if (!strncmp (current.c_str (), path.c_str (), current.length ()))
- return path.substr (current.length ());
- return path;
-}
-
// Roughly follows the POSIX description of `cd -L` because of symlinks.
// HOME and CDPATH handling is ommitted.
fun change_dir (const string &path) {
@@ -1040,7 +1190,7 @@ fun change_dir (const string &path) {
beep ();
return;
}
- if (!out.back().empty ())
+ if (!out.back ().empty ())
out.pop_back ();
} else if (in[i] != "." && (!in[i].empty () || i < startempty)) {
out.push_back (in[i]);
@@ -1053,23 +1203,27 @@ fun change_dir (const string &path) {
return;
}
- auto old_cwd = g.cwd;
- level last {g.offset, g.cursor, old_cwd, g.entries[g.cursor].filename};
+ level last {g.offset, g.cursor, g.cwd, at_cursor ().filename};
g.cwd = full_path;
- reload (old_cwd);
+ bool same_path = last.path == g.cwd;
+ reload (same_path);
- if (is_ancestor_dir (last.path, g.cwd)) {
- g.levels.push_back (last);
+ if (!same_path) {
g.offset = g.cursor = 0;
- } else {
- pop_levels ();
+ if (is_ancestor_dir (last.path, g.cwd))
+ g.levels.push_back (last);
+ else
+ pop_levels (last.path);
}
}
// Roughly follows the POSIX description of the PWD environment variable
fun initial_cwd () -> string {
- char cwd[4096] = ""; getcwd (cwd, sizeof cwd);
- const char *pwd = getenv ("PWD");
+ char cwd[4096] = ""; const char *pwd = getenv ("PWD");
+ if (!getcwd (cwd, sizeof cwd)) {
+ show_message (strerror (errno));
+ return pwd;
+ }
if (!pwd || pwd[0] != '/' || strlen (pwd) >= PATH_MAX)
return cwd;
@@ -1100,32 +1254,111 @@ fun choose (const entry &entry) {
}
}
+// Move the cursor in `diff` direction and look for non-combining characters
+fun move_towards_spacing (int diff) -> bool {
+ g.editor_cursor += diff;
+ return g.editor_cursor <= 0 ||
+ g.editor_cursor >= int (g.editor_line.length ()) ||
+ wcwidth (g.editor_line.at (g.editor_cursor));
+}
+
fun handle_editor (wint_t c) {
- auto i = g_input_actions.find (c);
- switch (i == g_input_actions.end () ? ACTION_NONE : i->second) {
+ auto action = ACTION_NONE;
+ if (g.editor_inserting) {
+ (void) halfdelay (1);
+ g.editor_inserting = false;
+ } else {
+ auto i = g_input_actions.find (c);
+ if (i != g_input_actions.end ())
+ action = i->second;
+
+ auto m = g_binding_contexts.find (to_mb (g.editor));
+ if (m != g_binding_contexts.end () &&
+ (i = m->second->find (c)) != m->second->end ())
+ action = i->second;
+ }
+
+ auto original = g.editor_line;
+ switch (action) {
case ACTION_INPUT_CONFIRM:
- if (g.editor_on_confirm)
- g.editor_on_confirm ();
+ if (auto handler = g.editor_on[action])
+ handler ();
// Fall-through
case ACTION_INPUT_ABORT:
- g.editor_line.clear ();
g.editor = 0;
+ g.editor_info.clear ();
+ g.editor_line.clear ();
+ g.editor_cursor = 0;
+ g.editor_inserting = false;
g.editor_on_change = nullptr;
- g.editor_on_confirm = nullptr;
+ g.editor_on.clear ();
+ return;
+ case ACTION_INPUT_BEGINNING:
+ g.editor_cursor = 0;
+ break;
+ case ACTION_INPUT_END:
+ g.editor_cursor = g.editor_line.length ();
+ break;
+ case ACTION_INPUT_BACKWARD:
+ while (g.editor_cursor > 0 &&
+ !move_towards_spacing (-1))
+ ;
+ break;
+ case ACTION_INPUT_FORWARD:
+ while (g.editor_cursor < int (g.editor_line.length ()) &&
+ !move_towards_spacing (+1))
+ ;
break;
case ACTION_INPUT_B_DELETE:
- if (!g.editor_line.empty ())
- g.editor_line.erase (g.editor_line.length () - 1);
+ while (g.editor_cursor > 0) {
+ auto finished = move_towards_spacing (-1);
+ g.editor_line.erase (g.editor_cursor, 1);
+ if (finished)
+ break;
+ }
+ break;
+ case ACTION_INPUT_DELETE:
+ while (g.editor_cursor < int (g.editor_line.length ())) {
+ g.editor_line.erase (g.editor_cursor, 1);
+ if (move_towards_spacing (0))
+ break;
+ }
+ break;
+ case ACTION_INPUT_B_KILL_WORD:
+ {
+ int i = g.editor_cursor;
+ while (i && g.editor_line[--i] == L' ');
+ while (i-- && g.editor_line[i] != L' ');
+ i++;
+
+ g.editor_line.erase (i, g.editor_cursor - i);
+ g.editor_cursor = i;
+ break;
+ }
+ case ACTION_INPUT_B_KILL_LINE:
+ g.editor_line.erase (0, g.editor_cursor);
+ g.editor_cursor = 0;
+ break;
+ case ACTION_INPUT_KILL_LINE:
+ g.editor_line.erase (g.editor_cursor);
+ break;
+ case ACTION_INPUT_QUOTED_INSERT:
+ (void) raw ();
+ g.editor_inserting = true;
break;
default:
- if (c & (ALT | SYM)) {
- beep ();
+ if (auto handler = g.editor_on[action]) {
+ handler ();
+ } else if (c & (ALT | SYM)) {
+ if (c != KEY (RESIZE))
+ beep ();
} else {
- g.editor_line += c;
- if (g.editor_on_change)
- g.editor_on_change ();
+ g.editor_line.insert (g.editor_cursor, 1, c);
+ g.editor_cursor++;
}
}
+ if (g.editor_on_change && g.editor_line != original)
+ g.editor_on_change ();
}
fun handle (wint_t c) -> bool {
@@ -1138,10 +1371,15 @@ fun handle (wint_t c) -> bool {
c = WEOF;
}
- const auto &current = g.entries[g.cursor];
+ const auto &current = at_cursor ();
+ bool is_directory =
+ S_ISDIR (current.info.st_mode) ||
+ S_ISDIR (current.target_info.st_mode);
+
auto i = g_normal_actions.find (c);
switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
case ACTION_CHOOSE_FULL:
+ // FIXME: in the root directory, this inserts //item
g.chosen = g.cwd + "/" + current.filename;
g.no_chdir = true;
g.quitting = true;
@@ -1150,7 +1388,8 @@ fun handle (wint_t c) -> bool {
choose (current);
break;
case ACTION_VIEW:
- view (current.filename);
+ // Mimic mc, it does not seem sensible to page directories
+ (is_directory ? change_dir : view) (current.filename);
break;
case ACTION_EDIT:
edit (current.filename);
@@ -1168,12 +1407,12 @@ fun handle (wint_t c) -> bool {
case ACTION_SORT_LEFT:
g.sort_column = (g.sort_column + entry::COLUMNS - 1) % entry::COLUMNS;
g.sort_flash_ttl = 2;
- reload (g.cwd);
+ resort ();
break;
case ACTION_SORT_RIGHT:
g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS;
g.sort_flash_ttl = 2;
- reload (g.cwd);
+ resort ();
break;
case ACTION_UP:
@@ -1212,11 +1451,14 @@ fun handle (wint_t c) -> bool {
case ACTION_SCROLL_UP:
g.offset--;
break;
+ case ACTION_CENTER:
+ g.offset = g.cursor - (visible_lines () - 1) / 2;
+ break;
case ACTION_CHDIR:
g.editor = L"chdir";
- g.editor_on_confirm = [] {
- change_dir (to_mb (g.editor_line));
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
+ change_dir (untilde (to_mb (g.editor_line)));
};
break;
case ACTION_PARENT:
@@ -1226,30 +1468,37 @@ fun handle (wint_t c) -> bool {
change_dir (g.start_dir);
break;
case ACTION_GO_HOME:
- if (const auto *home = getenv ("HOME"))
- change_dir (home);
- else if (const auto *pw = getpwuid (getuid ()))
- change_dir (pw->pw_dir);
+ change_dir (untilde ("~"));
break;
case ACTION_SEARCH:
g.editor = L"search";
- g.editor_on_change = [] {
- search (g.editor_line);
- };
- g.editor_on_confirm = [] {
- choose (g.entries[g.cursor]);
- };
+ g.editor_on_change = [] { match_interactive (0); };
+ g.editor_on[ACTION_UP] = [] { match_interactive (-1); };
+ g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); };
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); };
break;
case ACTION_RENAME_PREFILL:
g.editor_line = to_wide (current.filename);
+ g.editor_cursor = g.editor_line.length ();
// Fall-through
case ACTION_RENAME:
g.editor = L"rename";
- g.editor_on_confirm = [] {
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
auto mb = to_mb (g.editor_line);
- rename (g.entries[g.cursor].filename.c_str (), mb.c_str ());
- reload (g.cwd);
+ if (rename (at_cursor ().filename.c_str (), mb.c_str ()))
+ show_message (strerror (errno));
+ reload (true);
+ };
+ break;
+ case ACTION_MKDIR:
+ g.editor = L"mkdir";
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
+ auto mb = to_mb (g.editor_line);
+ if (mkdir (mb.c_str (), 0777))
+ show_message (strerror (errno));
+ reload (true);
+ focus (mb);
};
break;
@@ -1258,17 +1507,17 @@ fun handle (wint_t c) -> bool {
break;
case ACTION_REVERSE_SORT:
g.reverse_sort = !g.reverse_sort;
- reload (g.cwd);
+ resort ();
break;
case ACTION_SHOW_HIDDEN:
g.show_hidden = !g.show_hidden;
- reload (g.cwd);
+ reload (true);
break;
case ACTION_REDRAW:
clear ();
break;
case ACTION_RELOAD:
- reload (g.cwd);
+ reload (true);
break;
default:
if (c != KEY (RESIZE) && c != WEOF)
@@ -1295,6 +1544,20 @@ fun inotify_check () {
update ();
}
+fun load_cmdline (int argc, char *argv[]) {
+ if (argc < 3)
+ return;
+
+ wstring line = to_wide (argv[1]); int cursor = atoi (argv[2]);
+ if (line.empty () || cursor < 0 || cursor > (int) line.length ())
+ return;
+
+ std::replace_if (begin (line), end (line), iswspace, L' ');
+ g.cmdline = apply_attrs (line += L' ', g.attrs[g.AT_CMDLINE]);
+ // It is tempting to touch the cchar_t directly, though let's rather not
+ g.cmdline[cursor] = cchar (g.attrs[g.AT_CMDLINE] ^ A_REVERSE, line[cursor]);
+}
+
fun decode_ansi_sgr (const vector<string> &v) -> chtype {
vector<int> args;
for (const auto &arg : v) {
@@ -1341,8 +1604,8 @@ fun load_ls_colors (vector<string> colors) {
if (equal == string::npos)
continue;
auto key = pair.substr (0, equal), value = pair.substr (equal + 1);
- if (key != g_ls_colors[LS_SYMLINK]
- || !(g.ls_symlink_as_target = value == "target"))
+ if (key != g_ls_colors[LS_SYMLINK] ||
+ !(g.ls_symlink_as_target = value == "target"))
attrs[key] = decode_ansi_sgr (split (value, ";"));
}
for (int i = 0; i < LS_COUNT; i++) {
@@ -1415,16 +1678,16 @@ fun parse_key (const string &key_name) -> wint_t {
c |= ALT;
p += 2;
}
- if (!strncmp (p, "C-", 2)) {
+ if (g.name_to_key.count (p)) {
+ return c | g.name_to_key.at (p);
+ } else if (!strncmp (p, "C-", 2)) {
p += 2;
- if (*p < 32) {
+ if (*p < '?' || *p > '~') {
cerr << "bindings: invalid combination: " << key_name << endl;
return WEOF;
}
- c |= CTRL *p;
+ c |= CTRL (*p);
p += 1;
- } else if (g.name_to_key.count (p)) {
- return c | g.name_to_key.at (p);
} else {
wchar_t w; mbstate_t mb {};
auto len = strlen (p) + 1, res = mbrtowc (&w, p, len, &mb);
@@ -1453,7 +1716,9 @@ fun learn_named_key (const string &name, wint_t key) {
fun load_bindings () {
learn_named_key ("space", ' ');
learn_named_key ("escape", 0x1b);
- for (int kc = KEY_MIN; kc < KEY_MAX; kc++) {
+
+ int kc = 0;
+ for (kc = KEY_MIN; kc <= KEY_MAX; kc++) {
const char *name = keyname (kc);
if (!name)
continue;
@@ -1467,10 +1732,6 @@ fun load_bindings () {
learn_named_key (filtered, SYM | kc);
}
- auto config = xdg_config_find ("bindings");
- if (!config)
- return;
-
// Stringization in the preprocessor is a bit limited, we want lisp-case
map<string, action> actions;
int a = 0;
@@ -1482,16 +1743,27 @@ fun load_bindings () {
actions[name] = action (a++);
}
+ auto config = xdg_config_find ("bindings");
+ if (!config)
+ return;
+
vector<string> tokens;
while (parse_line (*config, tokens)) {
if (tokens.empty ())
continue;
if (tokens.size () < 3) {
- cerr << "bindings: expected: context binding action";
+ cerr << "bindings: expected: define name key-sequence"
+ " | context binding action";
continue;
}
auto context = tokens[0], key_name = tokens[1], action = tokens[2];
+ if (context == "define") {
+ // We haven't run initscr() yet, so define_key() would fail here
+ learn_named_key (key_name, SYM | (g.custom_keys[action] = ++kc));
+ continue;
+ }
+
auto m = g_binding_contexts.find (context);
if (m == g_binding_contexts.end ()) {
cerr << "bindings: invalid context: " << context << endl;
@@ -1534,6 +1806,8 @@ fun load_config () {
g.reverse_sort = tokens.at (1) == "1";
else if (tokens.front () == "show-hidden" && tokens.size () > 1)
g.show_hidden = tokens.at (1) == "1";
+ else if (tokens.front () == "ext-helpers" && tokens.size () > 1)
+ g.ext_helpers = tokens.at (1) == "1";
else if (tokens.front () == "sort-column" && tokens.size () > 1)
g.sort_column = stoi (tokens.at (1));
else if (tokens.front () == "history")
@@ -1550,6 +1824,7 @@ fun save_config () {
write_line (*config, {"gravity", g.gravity ? "1" : "0"});
write_line (*config, {"reverse-sort", g.reverse_sort ? "1" : "0"});
write_line (*config, {"show-hidden", g.show_hidden ? "1" : "0"});
+ write_line (*config, {"ext-helpers", g.ext_helpers ? "1" : "0"});
write_line (*config, {"sort-column", to_string (g.sort_column)});
@@ -1563,14 +1838,17 @@ fun save_config () {
to_string (i->offset), to_string (i->cursor), i->filename});
write_line (*config, {"history", hostname, ppid, g.cwd,
to_string (g.offset), to_string (g.cursor),
- g.entries[g.cursor].filename});
+ at_cursor ().filename});
}
int main (int argc, char *argv[]) {
- (void) argc;
- (void) argv;
+ if (argc == 2 && string (argv[1]) == "--version") {
+ cout << PROJECT_NAME << " " << PROJECT_VERSION << endl;
+ return 0;
+ }
- // That bitch zle closes stdin before exec without redirection
+ // zsh before 5.4 may close stdin before exec without redirection,
+ // since then it redirects stdin to /dev/null
(void) close (STDIN_FILENO);
if (open ("/dev/tty", O_RDWR)) {
cerr << "cannot open tty" << endl;
@@ -1597,13 +1875,23 @@ int main (int argc, char *argv[]) {
cerr << "cannot initialize screen" << endl;
return 1;
}
+ for (const auto &definition_kc : g.custom_keys)
+ define_key (definition_kc.first.c_str (), definition_kc.second);
load_colors ();
+ load_cmdline (argc, argv);
g.start_dir = g.cwd = initial_cwd ();
- reload (g.cwd);
- pop_levels ();
+ reload (false);
+ pop_levels (g.cwd);
update ();
+ // Cunt, now I need to reïmplement all signal handling
+#if NCURSES_VERSION_PATCH < 20210821
+ // This gets applied along with the following halfdelay()
+ cur_term->Nttyb.c_cc[VSTOP] =
+ cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE;
+#endif
+
// Invoking keypad() earlier would make ncurses flush its output buffer,
// which would worsen start-up flickering
if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
@@ -1632,11 +1920,13 @@ int main (int argc, char *argv[]) {
// We can't portably create a standard stream from an FD, so modify the FD
dup2 (output_fd, STDOUT_FILENO);
+ // TODO: avoid printing any of this unless the SDN envvar is set
if (g.cwd != g.start_dir && !g.no_chdir)
cout << "local cd=" << shell_escape (g.cwd) << endl;
else
cout << "local cd=" << endl;
cout << "local insert=" << shell_escape (g.chosen) << endl;
+ cout << "local helper=" << shell_escape (g.ext_helper) << endl;
return 0;
}