aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format14
-rw-r--r--CMakeLists.txt39
-rw-r--r--LICENSE2
-rw-r--r--Makefile10
-rw-r--r--NEWS20
-rw-r--r--README.adoc32
-rwxr-xr-xsdn-install6
-rw-r--r--sdn-install.12
-rw-r--r--sdn-mc-ext.cpp222
-rwxr-xr-xsdn-view54
-rw-r--r--sdn-view.124
-rw-r--r--sdn.113
-rw-r--r--sdn.cpp622
13 files changed, 840 insertions, 220 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 0bef602..d0a6042 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,33 +1,42 @@
# target_compile_features has been introduced in that version
-cmake_minimum_required (VERSION 3.1)
-project (sdn VERSION 0.1 LANGUAGES CXX)
+cmake_minimum_required (VERSION 3.1...3.27)
+project (sdn VERSION 1.0 LANGUAGES CXX)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
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
-execute_process (COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=gold -Wl,--version
- ERROR_QUIET OUTPUT_VARIABLE ld_version)
-if ("${ld_version}" MATCHES "GNU gold")
- set (CMAKE_EXE_LINKER_FLAGS "-fuse-ld=gold ${CMAKE_EXE_LINKER_FLAGS}")
-endif ()
-
find_package (PkgConfig REQUIRED)
-pkg_check_modules (NCURSESW QUIET ncursesw)
+pkg_check_modules (ACL libacl)
+pkg_check_modules (NCURSESW ncursesw)
+if (NOT NCURSESW_FOUND)
+ find_library (NCURSESW_LIBRARIES NAMES ncursesw)
+ find_path (NCURSESW_INCLUDE_DIRS ncurses.h PATH_SUFFIXES ncurses)
+endif ()
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
-target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS})
-target_link_libraries (${PROJECT_NAME} PUBLIC ${NCURSESW_LIBRARIES} acl)
+target_include_directories (${PROJECT_NAME}
+ PUBLIC ${NCURSESW_INCLUDE_DIRS} ${ACL_INCLUDE_DIRS})
+target_link_directories (${PROJECT_NAME}
+ PUBLIC ${NCURSESW_LIBRARY_DIRS} ${ACL_LIBRARY_DIRS})
+target_link_libraries (${PROJECT_NAME}
+ PUBLIC ${NCURSESW_LIBRARIES} ${ACL_LIBRARIES})
target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14)
target_compile_definitions (${PROJECT_NAME} PUBLIC
-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\")
+add_executable (${PROJECT_NAME}-mc-ext ${PROJECT_NAME}-mc-ext.cpp)
+target_compile_features (${PROJECT_NAME}-mc-ext PUBLIC cxx_std_17)
+
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)
+# sdn-mc-ext should be in libexec, but we prefer it in PATH.
+install (TARGETS sdn sdn-mc-ext
+ DESTINATION ${CMAKE_INSTALL_BINDIR})
+install (PROGRAMS sdn-install sdn-view
+ DESTINATION ${CMAKE_INSTALL_BINDIR})
+install (FILES sdn.1 sdn-install.1 sdn-view.1
+ DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
diff --git a/LICENSE b/LICENSE
index 5e342a0..7511f3e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2017 - 2021, 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 57ff1bb..0468a72 100644
--- a/Makefile
+++ b/Makefile
@@ -5,15 +5,15 @@ CPPFLAGS = `sed -ne '/^project (\([^ )]*\) VERSION \([^ )]*\).*/ \
s//-DPROJECT_NAME="\1" -DPROJECT_VERSION="\2"/p' CMakeLists.txt`
sdn: sdn.cpp CMakeLists.txt
- $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o sdn \
+ $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
-lacl `pkg-config --libs --cflags ncursesw`
-static: sdn.cpp CMakeLists.txt
- $(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o sdn \
+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: static clean
+.PHONY: clean
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..54e2935
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,20 @@
+Unreleased
+
+ * Added selection functionality, and adjusted key bindings:
+ - C-t or Insert toggle whether the current item is selected;
+ - + and - adjust the selection using shell globs;
+ - t and T insert the selection into the external command line
+ in relative or absolute form, respectively;
+ - Enter is like t but enters directories, and M-Enter is synonymous to t;
+ - C-g or Escape clear the selection, similarly to the editor.
+
+ * Added an sdn-view script that can process Midnight Commander mc.ext.ini files
+ and apply matching filters; this script has been made the default F3 binding,
+ while the original direct pager invocation has been moved to F13 (which also
+ reflects Midnight Commander)
+
+
+1.0.0 (2024-12-21)
+
+ * Initial release
+
diff --git a/README.adoc b/README.adoc
index 6e8e363..b7aeecf 100644
--- a/README.adoc
+++ b/README.adoc
@@ -5,26 +5,29 @@ sdn
'sdn' is a simple directory navigator that you can invoke while editing shell
commands. It enables you to:
- * take a quick peek at directory contents without running `ls`
+ * take a quick peek at directory contents without running `ls`;
+ * select files to insert into the command line;
* browse the filesystem without all the mess that Midnight Commander does:
there's no need to create a subshell in a new pty. The current command line
can be simply forwarded if it is to be edited. What's more, it will always
be obvious whether the navigator is running.
-The only supported platform is Linux. I wanted to try a different, simpler
-approach here, and the end result is very friendly to tinkering.
+'sdn' runs on Linux and all BSD derivatives. I wanted to try a different,
+simpler 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 from Archlinux's AUR.
+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
+Build dependencies: CMake and/or make, a C++17 compiler, pkg-config +
+Runtime dependencies: ncursesw, libacl (on Linux)
// Working around libasciidoc's missing support for escaping it like \++
Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn'
@@ -73,6 +76,7 @@ that of git, only named colours aren't supported:
....
cursor 231 202
+select 202 bold
bar 16 255 ul
cwd bold
input
@@ -89,15 +93,19 @@ To obtain more vifm-like controls, you may write the following to your
....
normal h parent
-normal l choose
+normal l enter
....
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.
+The F3, F13 and F4 keys are normally bound to actions 'view', 'view-raw',
+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.
+
+If 'view' finds Midnight Commander, it will make use of its configuration
+to apply any matching filter, such as to produce archive listings,
+or it will run the respective command.
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
diff --git a/sdn-install b/sdn-install
index 93821bf..788c8bf 100755
--- a/sdn-install
+++ b/sdn-install
@@ -16,6 +16,9 @@ sdn-navigate () {
# 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
@@ -51,6 +54,7 @@ sdn-navigate () {
((SDN_P=SDN_P+${#insert}+1))
}
[[ -z $helper ]] && break
+ history -s -- "$helper"
/bin/sh -c "$helper" || break
done
}
@@ -121,7 +125,7 @@ done
# Figure out the shell to integrate with
login=$(basename "$SHELL")
-actual=$(ps -p $$ -o ppid= | xargs ps -o comm= -p)
+actual=$(ps -p $$ -o ppid= | xargs ps -o comm= -p | sed 's/^-//')
if [ -z "$shell" ]
then
if [ "$login" != "$actual" ]
diff --git a/sdn-install.1 b/sdn-install.1
index c186f63..a6972c8 100644
--- a/sdn-install.1
+++ b/sdn-install.1
@@ -1,6 +1,6 @@
.Dd October 27, 2020
.Dt SDN-INSTALL 1
-.Os Linux
+.Os
.Sh NAME
.Nm sdn-install
.Nd integrate sdn with the shell
diff --git a/sdn-mc-ext.cpp b/sdn-mc-ext.cpp
new file mode 100644
index 0000000..9e06760
--- /dev/null
+++ b/sdn-mc-ext.cpp
@@ -0,0 +1,222 @@
+//
+// sdn-mc-ext: Midnight Commander extension file processor
+//
+// Copyright (c) 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.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+#include <cstdlib>
+#include <cctype>
+#include <iostream>
+#include <regex>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+// Trailing return types make C++ syntax suck considerably less
+#define fun static auto
+
+using namespace std;
+
+// It is completely fine if this only modifies ASCII letters.
+fun tolower (const string &s) -> string {
+ string result;
+ for (auto c : s) result += tolower (c);
+ return result;
+}
+
+fun shell_escape (const string &v) -> string {
+ return "'" + regex_replace (v, regex {"'"}, "'\\''") + "'";
+}
+
+string arg_type, arg_path, arg_basename, arg_dirname, arg_verb;
+unordered_map<string, unordered_map<string, string>> sections;
+
+fun expand_command (string command) -> pair<string, string> {
+ regex re_sequence {R"(%(%|[[:alpha:]]*\{([^}]*)\}|[[:alpha:]]+))"};
+ regex re_name {R"([^{}]*)"};
+ regex re_parameter {R"([^,]+")"};
+ string kind, out, pipe; smatch m;
+ while (regex_search (command, m, re_sequence)) {
+ out.append (m.prefix ());
+ auto seq = m.str (1);
+ command = m.suffix ();
+
+ string argument = m.str (2);
+ if (regex_search (seq, m, re_name))
+ seq = m.str ();
+
+ if (seq == "%") {
+ out += "%";
+ } else if (seq == "p") {
+ out += shell_escape (arg_basename);
+ } else if (seq == "f") {
+ out += shell_escape (arg_path);
+ } else if (seq == "d") {
+ out += shell_escape (arg_dirname);
+ } else if (seq == "var") {
+ string value;
+ if (auto colon = argument.find (':'); colon == argument.npos) {
+ if (auto v = getenv (argument.c_str ()))
+ value = v;
+ } else {
+ value = argument.substr (colon + 1);
+ if (auto v = getenv (argument.substr (0, colon).c_str ()))
+ value = v;
+ }
+ out += shell_escape (value);
+ } else if (seq == "cd") {
+ kind = seq;
+ command = regex_replace (command, regex {"^ +"}, "");
+ } else if (seq == "view") {
+ kind = seq;
+ command = regex_replace (command, regex {"^ +"}, "");
+
+ sregex_token_iterator it (argument.begin (), argument.end (),
+ re_parameter, 0), end;
+ for (; it != end; it++) {
+ if (*it == "hex")
+ pipe.append (" | od -t x1");
+
+ // more(1) and less(1) either ignore or display this:
+ //if (*it == "nroff")
+ // pipe.append (" | col -b");
+ }
+ } else if (seq == "") {
+ cerr << "sdn-mc-ext: prompting not supported" << endl;
+ return {};
+ } else {
+ cerr << "sdn-mc-ext: unsupported: %" << seq << endl;
+ return {};
+ }
+ }
+ return {kind,
+ pipe.empty () ? out.append (command) : "(" + out + ")" + pipe};
+}
+
+fun print_command (string cmd) {
+ auto command = expand_command (cmd);
+ cout << get<0> (command) << endl << get<1> (command) << endl;
+}
+
+fun section_matches (const unordered_map<string, string> &section) -> bool {
+ if (section.count ("Directory"))
+ return false;
+
+ // The configuration went through some funky changes;
+ // unescape \\ but leave other escapes alone.
+ auto filter_re = [](const string &s) {
+ string result;
+ for (size_t i = 0; i < s.length (); ) {
+ auto c = s[i++];
+ if (c == '\\' && i < s.length ())
+ if (c = s[i++]; c != '\\')
+ result += '\\';
+ result += c;
+ }
+ return result;
+ };
+ auto is_true = [&](const string &name) {
+ auto value = section.find (name);
+ return value != section.end () && value->second == "true";
+ };
+ if (auto kv = section.find ("Type"); kv != section.end ()) {
+ auto flags = std::regex::ECMAScript;
+ if (is_true ("TypeIgnoreCase"))
+ flags |= regex_constants::icase;
+ if (!regex_search (arg_type, regex {filter_re (kv->second), flags}))
+ return false;
+ }
+ auto basename = arg_basename;
+ if (auto kv = section.find ("Regex"); kv != section.end ()) {
+ auto flags = std::regex::ECMAScript;
+ if (is_true ("RegexIgnoreCase"))
+ flags |= regex_constants::icase;
+ return regex_search (basename, regex {filter_re (kv->second), flags});
+ }
+ if (auto kv = section.find ("Shell"); kv != section.end ()) {
+ auto value = kv->second;
+ if (is_true ("ShellIgnoreCase")) {
+ value = tolower (value);
+ basename = tolower (arg_basename);
+ }
+ if (value.empty () || value[0] != '.')
+ return value == basename;
+ return basename.length () >= value.length () &&
+ basename.substr (basename.length () - value.length ()) == value;
+ }
+ return !arg_type.empty ();
+}
+
+fun process (const string &section) -> bool {
+ auto full = sections.at (section);
+ if (auto include = full.find ("Include"); include != full.end ()) {
+ full.erase ("Open");
+ full.erase ("View");
+ full.erase ("Edit");
+
+ if (auto included = sections.find ("Include/" + include->second);
+ included != sections.end ()) {
+ for (const auto &kv : included->second)
+ full[kv.first] = kv.second;
+ }
+ }
+ if (getenv ("SDN_MC_EXT_DEBUG")) {
+ cerr << "[" << section << "]" << endl;
+ for (const auto &kv : full)
+ cerr << " " << kv.first << ": " << kv.second << endl;
+ }
+ if (full.count (arg_verb) && section_matches (full)) {
+ print_command (full[arg_verb]);
+ return true;
+ }
+ return false;
+}
+
+int main (int argc, char *argv[]) {
+ if (argc != 6) {
+ cerr << "Usage: " << argv[0]
+ << " TYPE PATH BASENAME DIRNAME VERB < mc.ext.ini" << endl;
+ return 2;
+ }
+
+ arg_type = argv[1];
+ arg_path = argv[2], arg_basename = argv[3], arg_dirname = argv[4];
+ arg_verb = argv[5];
+
+ string line, section;
+ vector<string> order;
+ regex re_entry {R"(^([-\w]+) *= *(.*)$)"};
+ smatch m;
+ while (getline (cin, line)) {
+ if (line.empty () || line[0] == '#') {
+ continue;
+ } else if (auto length = line.length();
+ line.find_last_of ('[') == 0 &&
+ line.find_first_of (']') == length - 1) {
+ order.push_back ((section = line.substr (1, length - 2)));
+ } else if (regex_match (line, m, re_entry)) {
+ sections[section][m[1]] = m[2];
+ }
+ }
+ for (const auto &section : order) {
+ if (section == "mc.ext.ini" ||
+ section == "Default" ||
+ section.substr (0, 8) == "Include/")
+ continue;
+ if (process (section))
+ return 0;
+ }
+ print_command (sections["Default"][arg_verb]);
+ return 0;
+}
diff --git a/sdn-view b/sdn-view
new file mode 100755
index 0000000..d698e53
--- /dev/null
+++ b/sdn-view
@@ -0,0 +1,54 @@
+#!/bin/sh -e
+# sdn-view: a viewer for sdn that makes use of Midnight Commander configuration
+# to make more kinds of files directly viewable
+
+if [ "$#" -ne 1 ]
+then
+ echo "Usage: $0 FILE" >&2
+ exit 2
+fi
+
+# This handles both MC_DATADIR and odd installation locations.
+datadir=
+if command -v mc >/dev/null
+then datadir=$(mc --datadir | sed 's/ (.*)$//')
+fi
+
+config=
+for dir in "$HOME"/.config/mc "$datadir" /etc/mc
+do
+ if [ -n "$dir" -a -f "$dir/mc.ext.ini" ]
+ then
+ config=$dir/mc.ext.ini
+ break
+ fi
+done
+
+# This is often used in %env{} expansion, so let's be on the same page.
+export PAGER=${PAGER:-less}
+
+export MC_EXT_FILENAME=$(realpath "$1")
+export MC_EXT_BASENAME=$(basename "$1")
+export MC_EXT_CURRENTDIR=$(dirname "$MC_EXT_FILENAME")
+output=$(sdn-mc-ext <"$config" "$(file -Lbz "$1")" \
+ "$MC_EXT_FILENAME" "$MC_EXT_BASENAME" "$MC_EXT_CURRENTDIR" View || :)
+kind=$(echo "$output" | sed -n 1p)
+command=$(echo "$output" | sed -n 2p)
+
+case "$kind" in
+view)
+ if [ -n "$command" ]
+ then eval "$command" | "$PAGER"
+ else "$PAGER" -- "$MC_EXT_FILENAME"
+ fi
+ ;;
+'')
+ if [ -n "$command" ]
+ then eval "$command"
+ else "$PAGER" -- "$MC_EXT_FILENAME"
+ fi
+ ;;
+*)
+ echo "Unsupported: $kind" >&2
+ exit 1
+esac
diff --git a/sdn-view.1 b/sdn-view.1
new file mode 100644
index 0000000..b8c26c9
--- /dev/null
+++ b/sdn-view.1
@@ -0,0 +1,24 @@
+.Dd December 30, 2024
+.Dt SDN-VIEW 1
+.Os
+.Sh NAME
+.Nm sdn-view
+.Nd run Midnight Commander view configuration externally
+.Sh SYNOPSIS
+.Nm sdn-view
+.Ar path
+.Sh DESCRIPTION
+.Nm
+invokes
+.Ev PAGER
+or a fallback pager on the passed filename.
+.Pp
+If it succeeds in finding a
+.Xr mc 1
+.Pa mc.ext.ini
+file, it will first process it, and apply any matching filter,
+or run the respective command.
+.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
index 097a5de..8e76708 100644
--- a/sdn.1
+++ b/sdn.1
@@ -1,7 +1,7 @@
\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
-.Dd October 27, 2020
+.Dd December 30, 2024
.Dt SDN 1
-.Os Linux
+.Os
.Sh NAME
.Nm sdn
.Nd directory navigator
@@ -68,8 +68,8 @@ 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.
+The viewer program to be launched by the F3 and F13 key bindings as well as
+to show the internal help message.
If none is set, it defaults to
.Xr less 1 .
.It Ev VISUAL , Ev EDITOR
@@ -95,7 +95,7 @@ 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 l enter
normal M-f4 quit
.Ed
.Pp
@@ -107,7 +107,7 @@ For rxvt, that would be:
define C-ppage ^[[5^
define C-npage ^[[6^
normal C-ppage parent
-normal C-npage choose
+normal C-npage enter
.Ed
.Pp
Escape characters must be inserted verbatim, e.g., by pressing C-v ESC in vi,
@@ -120,6 +120,7 @@ For a black-on-white terminal supporting 256 colours, a theme such as the
following may work:
.Bd -literal -offset indent
cursor 231 202
+select 202 bold
bar 16 255 ul
cwd bold
input
diff --git a/sdn.cpp b/sdn.cpp
index ddcb342..624047a 100644
--- a/sdn.cpp
+++ b/sdn.cpp
@@ -1,7 +1,7 @@
//
// sdn: simple directory navigator
//
-// Copyright (c) 2017 - 2021, 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,42 +18,55 @@
// 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 <set>
+#include <sstream>
+#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 <time.h>
+#include <pwd.h>
#include <signal.h>
-
-#include <sys/inotify.h>
-#include <sys/xattr.h>
+#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifdef __linux__
+#include <sys/inotify.h>
+// ACL information is not important enough to be ported
#include <acl/libacl.h>
+#include <sys/acl.h>
+#include <sys/xattr.h>
+#else
+#include <sys/event.h>
+#endif
#include <ncurses.h>
-// Unicode is complex enough already and we might make assumptions
+// 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
+
#ifndef __STDC_ISO_10646__
-#error Unicode required for wchar_t
+// Unicode is complex enough already and we might make assumptions,
+// though macOS doesn't define this despite using UCS-4,
+// and we won't build on Windows that seems to be the only one to use UTF-16.
#endif
// Trailing return types make C++ syntax suck considerably less
@@ -159,9 +172,9 @@ 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 no-op
static char table[STATES][7] = {
@@ -246,7 +259,7 @@ fun capitalize (const string &s) -> string {
/// Underlining for teletypes (also called overstriking),
/// also imitated in more(1) and less(1)
-fun underline (const string& s) -> string {
+fun underline (const string &s) -> string {
string result;
for (auto c : s)
result.append ({c, 8, '_'});
@@ -268,7 +281,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;
@@ -297,7 +310,21 @@ fun xdg_config_write (const string &suffix) -> unique_ptr<fstream> {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-using ncstring = basic_string<cchar_t>;
+// This should be basic_string, however that crashes on macOS
+using ncstring = vector<cchar_t>;
+
+fun operator+ (const ncstring &lhs, const ncstring &rhs) -> ncstring {
+ ncstring result;
+ result.reserve (lhs.size () + rhs.size ());
+ result.insert (result.end (), lhs.begin (), lhs.end ());
+ result.insert (result.end (), rhs.begin (), rhs.end ());
+ return result;
+}
+
+fun operator+= (ncstring &lhs, const ncstring &rhs) -> ncstring & {
+ lhs.insert (lhs.end (), rhs.begin (), rhs.end ());
+ return lhs;
+}
fun cchar (chtype attrs, wchar_t c) -> cchar_t {
cchar_t ch {}; wchar_t ws[] = {c, 0};
@@ -318,9 +345,9 @@ 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;
}
@@ -403,14 +430,17 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#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(ENTER) XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW_RAW) XX(VIEW) XX(EDIT) \
+ XX(SORT_LEFT) XX(SORT_RIGHT) \
+ XX(SELECT) XX(DESELECT) XX(SELECT_TOGGLE) XX(SELECT_ABORT) \
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(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_DELETE) \
- XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) XX(INPUT_QUOTED_INSERT) \
+ 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,
@@ -422,14 +452,18 @@ static const char *g_action_names[] = {ACTIONS(XX)};
#undef 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},
+ {'\r', ACTION_ENTER}, {KEY (ENTER), ACTION_ENTER},
+ {ALT | '\r', ACTION_CHOOSE}, {ALT | KEY (ENTER), ACTION_CHOOSE},
+ {'t', ACTION_CHOOSE}, {'T', ACTION_CHOOSE_FULL},
{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
- {KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT},
+ {KEY (F (3)), ACTION_VIEW}, {KEY (F (13)), ACTION_VIEW_RAW},
+ {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},
+ {ALT | 'o', ACTION_QUIT}, {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
+ {'+', ACTION_SELECT}, {'-', ACTION_DESELECT},
+ {CTRL ('T'), ACTION_SELECT_TOGGLE}, {KEY (IC), ACTION_SELECT_TOGGLE},
+ {27, ACTION_SELECT_ABORT}, {CTRL ('G'), ACTION_SELECT_ABORT},
{'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},
@@ -437,12 +471,13 @@ static map<wint_t, action> g_normal_actions {
{'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},
+ {'z', ACTION_CENTER},
{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
- {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH},
+ {'/', 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},
+ {ALT | 't', ACTION_TOGGLE_FULL},
{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
};
@@ -452,7 +487,8 @@ static map<wint_t, action> g_input_actions {
// 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 ('D'), ACTION_INPUT_DELETE}, {CTRL ('U'), ACTION_INPUT_B_KILL_LINE},
+ {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},
@@ -463,6 +499,7 @@ static map<wint_t, action> g_input_actions {
static map<wint_t, action> g_search_actions {
{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
{CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
+ {'/', ACTION_ENTER},
};
static const map<string, map<wint_t, action>*> g_binding_contexts {
{"normal", &g_normal_actions}, {"input", &g_input_actions},
@@ -486,7 +523,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); });
}
@@ -503,6 +540,7 @@ struct entry {
struct level {
int offset, cursor; ///< Scroll offset and cursor position
string path, filename; ///< Level path and filename at cursor
+ set<string> selection; ///< Filenames of selected entries
};
static struct {
@@ -510,6 +548,7 @@ static struct {
string cwd; ///< Current working directory
string start_dir; ///< Starting directory
vector<entry> entries; ///< Current directory entries
+ set<string> selection; ///< Filenames of selected entries
vector<level> levels; ///< Upper directory levels
int offset, cursor; ///< Scroll offset and cursor position
bool full_view; ///< Show extended information
@@ -524,12 +563,12 @@ static struct {
wstring message; ///< Message for the user
int message_ttl; ///< Time to live for the message
- string chosen; ///< Chosen item for the command line
+ vector<string> chosen; ///< Chosen items 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
- int inotify_fd, inotify_wd = -1; ///< File watch
+ int watch_fd, watch_wd = -1; ///< File watch (inotify/kqueue)
bool out_of_date; ///< Entries may be out of date
const wchar_t *editor; ///< Prompt string for editing
@@ -538,13 +577,13 @@ static struct {
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_INFO, AT_CMDLINE, AT_COUNT };
- chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0};
+ enum { AT_CURSOR, AT_SELECT, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE,
+ AT_COUNT };
+ chtype attrs[AT_COUNT] = {A_REVERSE, A_BOLD, 0, A_BOLD, 0, A_ITALIC, 0};
const char *attr_names[AT_COUNT] =
- {"cursor", "bar", "cwd", "input", "info", "cmdline"};
+ {"cursor", "select", "bar", "cwd", "input", "info", "cmdline"};
map<int, chtype> ls_colors; ///< LS_COLORS decoded
map<string, chtype> ls_exts; ///< LS_COLORS file extensions
@@ -557,8 +596,8 @@ static struct {
// 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;
@@ -589,8 +628,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
set (LS_MULTIHARDLINK);
if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
set (LS_EXECUTABLE);
+#ifdef __linux__
if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
set (LS_CAPABILITY);
+#endif
if ((info.st_mode & S_ISGID))
set (LS_SETGID);
if ((info.st_mode & S_ISUID))
@@ -605,8 +646,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;
@@ -632,15 +673,33 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
return format;
}
+fun suffixize (off_t size, unsigned shift, wchar_t suffix, std::wstring &out)
+ -> bool {
+ // Prevent implementation-defined and undefined behaviour
+ if (size < 0 || shift >= sizeof size * 8)
+ return false;
+
+ off_t divided = size >> shift;
+ if (divided >= 10) {
+ out.assign (std::to_wstring (divided)).append (1, suffix);
+ return true;
+ } else if (divided > 0) {
+ unsigned times_ten = size / double (off_t (1) << shift) * 10.0;
+ out.assign ({L'0' + wchar_t (times_ten / 10), L'.',
+ L'0' + wchar_t (times_ten % 10), suffix});
+ return true;
+ }
+ return false;
+}
+
fun make_entry (const struct dirent *f) -> entry {
entry e;
e.filename = f->d_name;
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);
@@ -666,39 +725,44 @@ fun make_entry (const struct dirent *f) -> entry {
}
auto mode = decode_mode (info.st_mode);
- // This is a Linux-only extension
+#ifdef __linux__
+ // 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"+";
+#endif
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);
- if (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T";
- else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G";
- else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M";
- else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
+ std::wstring size;
+ if (!suffixize (info.st_size, 40, L'T', size) &&
+ !suffixize (info.st_size, 30, L'G', size) &&
+ !suffixize (info.st_size, 20, L'M', size) &&
+ !suffixize (info.st_size, 10, L'K', size))
+ size = to_wstring (info.st_size);
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 (to_wide (e.target_path), ls_format (e, true)));
+ fn += apply_attrs (L" -> ", 0);
+ fn += apply_attrs (to_wide (e.target_path), ls_format (e, true));
}
return e;
}
@@ -707,7 +771,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 ();
@@ -715,18 +779,25 @@ fun update () {
int used = min (available, all - g.offset);
for (int i = 0; i < used; i++) {
auto index = g.offset + i;
- bool selected = index == g.cursor;
- attrset (selected ? g.attrs[g.AT_CURSOR] : 0);
+ bool cursored = index == g.cursor;
+ bool selected = g.selection.count (g.entries[index].filename);
+ chtype attrs {};
+ if (selected)
+ attrs = g.attrs[g.AT_SELECT];
+ if (cursored)
+ attrs = g.attrs[g.AT_CURSOR] | (attrs & ~A_COLOR);
+ attrset (attrs);
+
move (g.gravity ? (available - used + i) : i, 0);
auto used = 0;
for (int col = start_column; col < entry::COLUMNS; col++) {
const auto &field = g.entries[index].cols[col];
auto aligned = align (field, alignment[col] * g.max_widths[col]);
+ if (cursored || selected)
+ for_each (begin (aligned), end (aligned), decolor);
if (g.sort_flash_ttl && col == g.sort_column)
for_each (begin (aligned), end (aligned), invert);
- if (selected)
- for_each (begin (aligned), end (aligned), decolor);
used += print (aligned + apply_attrs (L" ", 0), COLS - used);
}
hline (' ', COLS - used);
@@ -768,12 +839,23 @@ fun update () {
print (info, info_width);
}
- auto start = sanitize (prompt + line.substr (0, g.editor_cursor));
- move (LINES - 1, compute_width (start));
+ line.resize (g.editor_cursor);
+ move (LINES - 1, compute_width (sanitize (prompt + line)));
curs_set (1);
} else if (!g.message.empty ()) {
move (LINES - 1, 0);
print (apply_attrs (g.message, 0), COLS);
+ } else if (!g.selection.empty ()) {
+ uint64_t size = 0;
+ for (const auto &e : g.entries)
+ if (g.selection.count (e.filename)
+ && S_ISREG (e.info.st_mode) && e.info.st_size > 0)
+ size += e.info.st_size;
+
+ wostringstream status;
+ status << size << L" bytes in " << g.selection.size () << L" items";
+ move (LINES - 1, 0);
+ print (apply_attrs (status.str (), g.attrs[g.AT_SELECT]), COLS);
} else if (!g.cmdline.empty ()) {
move (LINES - 1, 0);
print (g.cmdline, COLS);
@@ -783,9 +865,10 @@ fun update () {
}
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;
@@ -822,16 +905,43 @@ fun at_cursor () -> const entry & {
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 filter_selection (const set<string> &selection) {
+ set<string> reselection;
+ if (!selection.empty ())
+ for (const auto &e : g.entries)
+ if (selection.count (e.filename))
+ reselection.insert (e.filename);
+ return reselection;
+}
+
fun reload (bool keep_anchor) {
- g.unames.clear();
+ 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 (keep_anchor)
@@ -840,6 +950,16 @@ fun reload (bool keep_anchor) {
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
@@ -849,45 +969,54 @@ fun reload (bool keep_anchor) {
g.entries.push_back (make_entry (f));
}
closedir (dir);
- sort (begin (g.entries), end (g.entries));
- g.out_of_date = false;
- if (!anchor.empty ()) {
- for (size_t i = 0; i < g.entries.size (); i++)
- if (g.entries[i].filename == anchor)
- g.cursor = i;
- }
+ g.selection = filter_selection (g.selection);
+
+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]));
}
+ 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);
+#ifdef __linux__
+ if (g.watch_wd != -1)
+ inotify_rm_watch (g.watch_fd, g.watch_wd);
// We don't show atime, so access and open are merely spam
- g.inotify_wd = inotify_add_watch (g.inotify_fd, ".",
+ g.watch_wd = inotify_add_watch (g.watch_fd, ".",
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
-}
+#else
+ if (g.watch_wd != -1)
+ close (g.watch_wd);
-fun show_message (const string &message, int ttl = 30) {
- g.message = to_wide (message);
- g.message_ttl = ttl;
+ if ((g.watch_wd = open (".", O_RDONLY | O_DIRECTORY | O_CLOEXEC)) >= 0) {
+ // At least the macOS kqueue doesn't report anything too specific
+ struct kevent ev {};
+ EV_SET (&ev, g.watch_wd, EVFILT_VNODE, EV_ADD | EV_CLEAR,
+ NOTE_WRITE | NOTE_LINK, 0, nullptr);
+ (void) kevent (g.watch_fd, &ev, 1, nullptr, 0, nullptr);
+ }
+#endif
}
-fun run_program (initializer_list<const char*> list, const string &filename) {
+fun run_program (initializer_list<const char *> list, const string &filename) {
+ auto args = (!filename.empty () && filename.front () == '-' ? " -- " : " ")
+ + shell_escape (filename);
if (g.ext_helpers) {
- // XXX: this doesn't try them all out, though it shouldn't make any
- // noticeable difference
+ // 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.ext_helper.assign (found).append (args);
g.quitting = true;
return;
}
@@ -903,8 +1032,8 @@ fun run_program (initializer_list<const char*> list, const string &filename) {
tcsetpgrp (STDOUT_FILENO, getpgid (0));
for (auto program : list)
- if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program)
- + " " + shell_escape (filename)).c_str (), NULL);
+ if (program) execl ("/bin/sh", "/bin/sh", "-c",
+ (program + args).c_str (), NULL);
_exit (EXIT_FAILURE);
default:
// ...and make sure of it in the parent as well
@@ -927,12 +1056,17 @@ fun run_program (initializer_list<const char*> list, const string &filename) {
update ();
}
-fun view (const string &filename) {
+fun view_raw (const string &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 view (const string &filename) {
+ run_program ({(const char *) getenv ("SDN_VIEWER"), "sdn-view",
+ (const char *) getenv ("PAGER"), "less", "cat"}, filename);
+}
+
fun edit (const string &filename) {
run_program ({(const char *) getenv ("VISUAL"),
(const char *) getenv ("EDITOR"), "vi"}, filename);
@@ -1004,24 +1138,7 @@ fun show_help () {
fclose (contents);
}
-/// Stays on the current match when there are no better ones, unless it's pushed
-fun search (const wstring &needle, int push) -> int {
- int best = g.cursor, best_n = 0, matches = 0, step = push != 0 ? push : 1;
- for (int i = 0, count = g.entries.size (); i < count; i++) {
- int o = (g.cursor + (count + i * step) + (count + push)) % count;
- size_t n = prefix_length (to_wide (g.entries[o].filename), needle);
- matches += n == needle.size ();
- if (n > (size_t) best_n) {
- best = o;
- best_n = n;
- }
- }
- g.cursor = best;
- return matches;
-}
-
-fun search_interactive (int push) {
- int matches = search (g.editor_line, push);
+fun matches_to_editor_info (int matches) {
if (g.editor_line.empty ())
g.editor_info.clear ();
else if (matches == 0)
@@ -1032,6 +1149,56 @@ fun search_interactive (int push) {
g.editor_info = L"(" + to_wstring (matches) + L" matches)";
}
+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) {
+ matches_to_editor_info (match (g.editor_line, push));
+}
+
+fun select_matches (bool dotdot) -> set<string> {
+ set<string> matches;
+ for (const auto &e : g.entries) {
+ if (!dotdot && e.filename == "..")
+ continue;
+ if (!fnmatch (to_mb (g.editor_line).c_str (),
+ e.filename.c_str (), FNM_PATHNAME))
+ matches.insert (e.filename);
+ }
+ return matches;
+}
+
+fun select_interactive (bool dotdot) {
+ matches_to_editor_info (select_matches (dotdot).size ());
+}
+
+/// 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;
+ }
+ }
+ g.cursor = best;
+}
+
fun fix_cursor_and_offset () {
g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
g.cursor = max (g.cursor, 0);
@@ -1068,13 +1235,14 @@ fun relativize (string current, const string &path) -> string {
return path;
}
-fun pop_levels (const string& old_cwd) {
+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) {
g.offset = i->offset;
g.cursor = i->cursor;
anchor = i->filename;
+ g.selection = filter_selection (i->selection);
}
i++;
g.levels.pop_back ();
@@ -1088,7 +1256,7 @@ fun pop_levels (const string& old_cwd) {
fix_cursor_and_offset ();
if (!anchor.empty () && at_cursor ().filename != anchor)
- search (to_wide (anchor), 0);
+ lookup (to_wide (anchor));
}
fun explode_path (const string &path, vector<string> &out) {
@@ -1146,7 +1314,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]);
@@ -1159,9 +1327,12 @@ fun change_dir (const string &path) {
return;
}
- level last {g.offset, g.cursor, g.cwd, at_cursor ().filename};
+ level last {g.offset, g.cursor, g.cwd, at_cursor ().filename, g.selection};
g.cwd = full_path;
bool same_path = last.path == g.cwd;
+ if (!same_path)
+ g.selection.clear ();
+
reload (same_path);
if (!same_path) {
@@ -1199,12 +1370,23 @@ fun initial_cwd () -> string {
return ok ? pwd : cwd;
}
-fun choose (const entry &entry) {
+fun choose (const entry &entry, bool full) {
+ if (g.selection.empty ())
+ g.selection.insert (entry.filename);
+ for (const string &item : g.selection)
+ g.chosen.push_back (full ? absolutize (g.cwd, item) : item);
+
+ g.selection.clear ();
+ g.no_chdir = full;
+ g.quitting = true;
+}
+
+fun enter (const entry &entry) {
// Dive into directories and accessible symlinks to them
if (!S_ISDIR (entry.info.st_mode)
&& !S_ISDIR (entry.target_info.st_mode)) {
- g.chosen = entry.filename;
- g.quitting = true;
+ // This could rather launch ${SDN_OPEN:-xdg-open} or something
+ choose (entry, false);
} else {
change_dir (entry.filename);
}
@@ -1213,9 +1395,9 @@ 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));
+ 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) {
@@ -1229,16 +1411,16 @@ fun handle_editor (wint_t c) {
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 ())
+ 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 = 0;
@@ -1247,7 +1429,6 @@ fun handle_editor (wint_t c) {
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:
@@ -1257,13 +1438,13 @@ fun handle_editor (wint_t c) {
g.editor_cursor = g.editor_line.length ();
break;
case ACTION_INPUT_BACKWARD:
- while (g.editor_cursor > 0
- && !move_towards_spacing (-1))
+ 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))
+ while (g.editor_cursor < int (g.editor_line.length ()) &&
+ !move_towards_spacing (+1))
;
break;
case ACTION_INPUT_B_DELETE:
@@ -1281,6 +1462,17 @@ fun handle_editor (wint_t c) {
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;
@@ -1296,7 +1488,8 @@ fun handle_editor (wint_t c) {
if (auto handler = g.editor_on[action]) {
handler ();
} else if (c & (ALT | SYM)) {
- beep ();
+ if (c != KEY (RESIZE))
+ beep ();
} else {
g.editor_line.insert (g.editor_cursor, 1, c);
g.editor_cursor++;
@@ -1324,16 +1517,19 @@ fun handle (wint_t c) -> bool {
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;
+ choose (current, true);
break;
case ACTION_CHOOSE:
- choose (current);
+ choose (current, false);
break;
- case ACTION_VIEW:
+ case ACTION_ENTER:
+ enter (current);
+ break;
+ case ACTION_VIEW_RAW:
// Mimic mc, it does not seem sensible to page directories
+ (is_directory ? change_dir : view_raw) (current.filename);
+ break;
+ case ACTION_VIEW:
(is_directory ? change_dir : view) (current.filename);
break;
case ACTION_EDIT:
@@ -1352,12 +1548,39 @@ 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 (true);
+ resort ();
break;
case ACTION_SORT_RIGHT:
g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS;
g.sort_flash_ttl = 2;
- reload (true);
+ resort ();
+ break;
+
+ case ACTION_SELECT:
+ g.editor = L"select";
+ g.editor_on_change = [] { select_interactive (false); };
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
+ auto matches = select_matches (false);
+ g.selection.insert (begin (matches), end (matches));
+ };
+ break;
+ case ACTION_DESELECT:
+ g.editor = L"deselect";
+ g.editor_on_change = [] { select_interactive (true); };
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
+ for (const auto &match : select_matches (true))
+ g.selection.erase (match);
+ };
+ break;
+ case ACTION_SELECT_TOGGLE:
+ if (g.selection.count (current.filename))
+ g.selection.erase (current.filename);
+ else
+ g.selection.insert (current.filename);
+ g.cursor++;
+ break;
+ case ACTION_SELECT_ABORT:
+ g.selection.clear ();
break;
case ACTION_UP:
@@ -1396,10 +1619,13 @@ 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 = [] {
+ g.editor_on[ACTION_INPUT_CONFIRM] = [] {
change_dir (untilde (to_mb (g.editor_line)));
};
break;
@@ -1415,10 +1641,15 @@ fun handle (wint_t c) -> bool {
case ACTION_SEARCH:
g.editor = L"search";
- g.editor_on_change = [] { search_interactive (0); };
- g.editor_on[ACTION_UP] = [] { search_interactive (-1); };
- g.editor_on[ACTION_DOWN] = [] { search_interactive (+1); };
- g.editor_on_confirm = [] { choose (at_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] = [] { enter (at_cursor ()); };
+ g.editor_on[ACTION_ENTER] = [] {
+ enter (at_cursor ());
+ g.editor_line.clear ();
+ g.editor_cursor = 0;
+ };
break;
case ACTION_RENAME_PREFILL:
g.editor_line = to_wide (current.filename);
@@ -1426,7 +1657,7 @@ fun handle (wint_t c) -> bool {
// 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);
if (rename (at_cursor ().filename.c_str (), mb.c_str ()))
show_message (strerror (errno));
@@ -1435,10 +1666,12 @@ fun handle (wint_t c) -> bool {
break;
case ACTION_MKDIR:
g.editor = L"mkdir";
- g.editor_on_confirm = [] {
- if (mkdir (to_mb (g.editor_line).c_str (), 0777))
+ 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;
@@ -1447,7 +1680,7 @@ fun handle (wint_t c) -> bool {
break;
case ACTION_REVERSE_SORT:
g.reverse_sort = !g.reverse_sort;
- reload (true);
+ resort ();
break;
case ACTION_SHOW_HIDDEN:
g.show_hidden = !g.show_hidden;
@@ -1468,19 +1701,27 @@ fun handle (wint_t c) -> bool {
return !g.quitting;
}
-fun inotify_check () {
- // Only provide simple indication that contents might have changed
- char buf[4096]; ssize_t len;
+fun watch_check () {
bool changed = false;
- while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) {
+ // Only provide simple indication that contents might have changed,
+ // if only because kqueue can't do any better
+#ifdef __linux__
+ char buf[4096]; ssize_t len;
+ while ((len = read (g.watch_fd, buf, sizeof buf)) > 0) {
const inotify_event *e;
for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
e = (const inotify_event *) buf;
- if (e->wd == g.inotify_wd)
- changed = g.out_of_date = true;
+ if (e->wd == g.watch_wd)
+ changed = true;
}
}
- if (changed)
+#else
+ struct kevent ev {};
+ struct timespec timeout {};
+ if (kevent (g.watch_fd, nullptr, 0, &ev, 1, &timeout) > 0)
+ changed = ev.filter == EVFILT_VNODE && (ev.fflags & NOTE_WRITE);
+#endif
+ if ((g.out_of_date = changed))
update ();
}
@@ -1544,8 +1785,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++) {
@@ -1722,10 +1963,11 @@ fun load_bindings () {
}
fun load_history_level (const vector<string> &v) {
- if (v.size () != 7)
+ if (v.size () < 7)
return;
// Not checking the hostname and parent PID right now since we can't merge
- g.levels.push_back ({stoi (v.at (4)), stoi (v.at (5)), v.at (3), v.at (6)});
+ g.levels.push_back ({stoi (v.at (4)), stoi (v.at (5)), v.at (3), v.at (6),
+ set<string> (begin (v) + 7, end (v))});
}
fun load_config () {
@@ -1773,12 +2015,16 @@ fun save_config () {
*hostname = 0;
auto ppid = std::to_string (getppid ());
- for (auto i = g.levels.begin (); i != g.levels.end (); i++)
- write_line (*config, {"history", hostname, ppid, i->path,
- 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),
- at_cursor ().filename});
+ for (auto i = g.levels.begin (); i != g.levels.end (); i++) {
+ vector<string> line {"history", hostname, ppid, i->path,
+ to_string (i->offset), to_string (i->cursor), i->filename};
+ line.insert (end (line), begin (i->selection), end (i->selection));
+ write_line (*config, line);
+ }
+ vector<string> line {"history", hostname, ppid, g.cwd,
+ to_string (g.offset), to_string (g.cursor), at_cursor ().filename};
+ line.insert (end (line), begin (g.selection), end (g.selection));
+ write_line (*config, line);
}
int main (int argc, char *argv[]) {
@@ -1802,10 +2048,17 @@ int main (int argc, char *argv[]) {
// So that the neither us nor our children stop on tcsetpgrp()
signal (SIGTTOU, SIG_IGN);
- if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
+#ifdef __linux__
+ if ((g.watch_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
cerr << "cannot initialize inotify" << endl;
return 1;
}
+#else
+ if ((g.watch_fd = kqueue ()) < 0) {
+ cerr << "cannot initialize kqueue" << endl;
+ return 1;
+ }
+#endif
locale::global (locale (""));
load_bindings ();
@@ -1825,6 +2078,13 @@ int main (int argc, char *argv[]) {
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) {
@@ -1835,7 +2095,7 @@ int main (int argc, char *argv[]) {
wint_t c;
while (!read_key (c) || handle (c)) {
- inotify_check ();
+ watch_check ();
if (g.sort_flash_ttl && !--g.sort_flash_ttl)
update ();
if (g.message_ttl && !--g.message_ttl) {
@@ -1847,8 +2107,12 @@ int main (int argc, char *argv[]) {
save_config ();
// Presumably it is going to end up as an argument, so quote it
- if (!g.chosen.empty ())
- g.chosen = shell_escape (g.chosen);
+ string chosen;
+ for (const auto &item : g.chosen) {
+ if (!chosen.empty ())
+ chosen += ' ';
+ chosen += shell_escape (item);
+ }
// We can't portably create a standard stream from an FD, so modify the FD
dup2 (output_fd, STDOUT_FILENO);
@@ -1859,7 +2123,7 @@ int main (int argc, char *argv[]) {
else
cout << "local cd=" << endl;
- cout << "local insert=" << shell_escape (g.chosen) << endl;
+ cout << "local insert=" << shell_escape (chosen) << endl;
cout << "local helper=" << shell_escape (g.ext_helper) << endl;
return 0;
}