Compare commits

..

24 Commits

Author SHA1 Message Date
977d1a7120 Update documentation
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-12-30 19:36:51 +01:00
9b274417c5 Make pressing / in search activate ACTION_ENTER
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
You can disable this in the bindings file using: search / none

Closes #6
2024-12-30 00:57:55 +01:00
cc14a9f735 Add selection functionality
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
At least for now, other actions mostly ignore the selection,
which is consistent with MC.

The selection is saved in the config/state file.

Closes #2
2024-12-30 00:30:34 +01:00
b070df6010 Rewrite sdn-view AWK core in C++
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
sdn-view is still slower than I'd like it to be,
just no longer ridiculously so.
2024-12-29 02:58:35 +01:00
3075d47aeb Add and integrate a script to execute mc.ext.ini
Tsk, tsk, parasiting on what we wanted to replace.

macOS is annoying to port to.

Unfortunately, this script is also very slow, for some reason.
2024-12-28 04:58:13 +01:00
85b2d8a2ee Use more of the relevant pkg-config variables
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-12-21 16:57:43 +01:00
272ee62ad8 Make a release
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-12-21 09:10:54 +01:00
a85426541a sdn-install: improve macOS installation
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Login shells have a dash at the beginning of their first argument.
2024-12-21 08:16:23 +01:00
c9b003735d Fix OpenBSD build
All checks were successful
Arch Linux AUR Success
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-12-21 07:59:44 +01:00
52a28f01c8 Add support for BSD derivatives, fix macOS
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-21 07:37:06 +01:00
3607757554 Improve filename passing
All checks were successful
Alpine 3.19 Success
Arch Linux AUR Success
c9662f1 forgot about internal helpers.

Moreover, it is annoying to see the -- in shell history
for every single external helper call.
2024-04-17 02:00:10 +02:00
6eb216a40a Remove linker preference from CMakeLists.txt
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
ld.gold doesn't understand all options that ld.bfd does.

In particular, this broke AUR builds with the current makepkg.conf.
2024-04-08 01:49:55 +02:00
9ce6f47716 Use more precise filesizes
Some checks failed
Arch Linux AUR Scripts failed
The behaviour differs from GNU `ls -lh` in that we use binary units,
meaning we get 1023 before 1.0K rather than 999 before 1.0K,
which is nonetheless still four characters wide.
2024-04-07 16:00:21 +02:00
c9662f1a7b Fix passing filenames starting with -
We don't want to pass them as program options.
2024-01-20 08:33:17 +01:00
9ddeb03652 CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:11:11 +02:00
acb187c6b1 README.adoc: update package information 2023-07-01 21:58:29 +02:00
9427df62e7 Fix code formatting, bump copyright years 2023-06-12 14:00:58 +02:00
4d6999c415 Do not beep on window resizes 2023-06-11 21:11:20 +02:00
30ed61fdd2 Implement ^W in the editor 2023-06-11 21:05:55 +02:00
2df916c9b3 Support wildcards in interactive search
The previous search for the longest match is functionally
duplicated by typing individual characters on the input line,
and wildcards can be escaped, so there shouldn't be regressions
in terms of capability.
2022-09-03 12:17:02 +02:00
24401825b4 Add a missing case break 2022-01-08 11:23:36 +01:00
2bfb490798 Add and bind an action to center the cursor
"z" stands for VIM's "zz".
2022-01-08 11:17:24 +01:00
338d00d605 Do not crash on opendir() failures
Show an error message, and a way out.
2021-11-09 07:52:48 +01:00
015652e379 Fix build with recent ncurses
Easily gained, easily lost.  No more ^S binding.
2021-11-09 07:45:20 +01:00
11 changed files with 723 additions and 141 deletions

View File

@@ -1,33 +1,42 @@
# target_compile_features has been introduced in that version # target_compile_features has been introduced in that version
cmake_minimum_required (VERSION 3.1) cmake_minimum_required (VERSION 3.1...3.27)
project (sdn VERSION 0.1 LANGUAGES CXX) project (sdn VERSION 1.0 LANGUAGES CXX)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU") if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
set (CMAKE_CXX_FLAGS set (CMAKE_CXX_FLAGS
"${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic") "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic")
endif () endif ()
# Since we use a language with slow compilers, let's at least use a fast linker find_package (PkgConfig REQUIRED)
execute_process (COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=gold -Wl,--version pkg_check_modules (ACL libacl)
ERROR_QUIET OUTPUT_VARIABLE ld_version) pkg_check_modules (NCURSESW ncursesw)
if ("${ld_version}" MATCHES "GNU gold") if (NOT NCURSESW_FOUND)
set (CMAKE_EXE_LINKER_FLAGS "-fuse-ld=gold ${CMAKE_EXE_LINKER_FLAGS}") find_library (NCURSESW_LIBRARIES NAMES ncursesw)
find_path (NCURSESW_INCLUDE_DIRS ncurses.h PATH_SUFFIXES ncurses)
endif () endif ()
find_package (PkgConfig REQUIRED)
pkg_check_modules (NCURSESW QUIET ncursesw)
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp) add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS}) target_include_directories (${PROJECT_NAME}
target_link_libraries (${PROJECT_NAME} PUBLIC ${NCURSESW_LIBRARIES} acl) 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_features (${PROJECT_NAME} PUBLIC cxx_std_14)
target_compile_definitions (${PROJECT_NAME} PUBLIC target_compile_definitions (${PROJECT_NAME} PUBLIC
-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\") -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) include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) # sdn-mc-ext should be in libexec, but we prefer it in PATH.
install (PROGRAMS ${PROJECT_NAME}-install DESTINATION ${CMAKE_INSTALL_BINDIR}) install (TARGETS sdn sdn-mc-ext
install (FILES sdn.1 sdn-install.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) 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}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator") set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")

View File

@@ -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 Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. purpose with or without fee is hereby granted.

20
NEWS Normal file
View File

@@ -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

View File

@@ -5,26 +5,29 @@ sdn
'sdn' is a simple directory navigator that you can invoke while editing shell 'sdn' is a simple directory navigator that you can invoke while editing shell
commands. It enables you to: 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: * 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 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 can be simply forwarded if it is to be edited. What's more, it will always
be obvious whether the navigator is running. be obvious whether the navigator is running.
The only supported platform is Linux. I wanted to try a different, simpler 'sdn' runs on Linux and all BSD derivatives. I wanted to try a different,
approach here, and the end result is very friendly to tinkering. simpler approach here, and the end result is very friendly to tinkering.
image::sdn.png[align="center"] image::sdn.png[align="center"]
Packages Packages
-------- --------
Regular releases are sporadic. git master should be stable enough. You can get Regular releases are sporadic. git master should be stable enough.
a package with the latest development version from Archlinux's AUR. 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 Building
-------- --------
Build dependencies: CMake and/or make, a C++14 compiler, pkg-config + Build dependencies: CMake and/or make, a C++17 compiler, pkg-config +
Runtime dependencies: ncursesw, libacl Runtime dependencies: ncursesw, libacl (on Linux)
// Working around libasciidoc's missing support for escaping it like \++ // Working around libasciidoc's missing support for escaping it like \++
Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn' 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 cursor 231 202
select 202 bold
bar 16 255 ul bar 16 255 ul
cwd bold cwd bold
input input
@@ -89,15 +93,19 @@ To obtain more vifm-like controls, you may write the following to your
.... ....
normal h parent normal h parent
normal l choose normal l enter
.... ....
Helper programs Helper programs
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
The F3 and F4 keys are normally bound to actions 'view' and 'edit', similarly to The F3, F13 and F4 keys are normally bound to actions 'view', 'view-raw',
Norton Commander and other orthodox file managers. The helper programs used and 'edit', similarly to Norton Commander and other orthodox file managers.
here may be changed by setting the PAGER and VISUAL (or EDITOR) environment The helper programs used here may be changed by setting the PAGER and VISUAL
variables. (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 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 `PAGER='mcview -u' sdn`, beware that this helper cannot read files from its

View File

@@ -125,7 +125,7 @@ done
# Figure out the shell to integrate with # Figure out the shell to integrate with
login=$(basename "$SHELL") 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" ] if [ -z "$shell" ]
then then
if [ "$login" != "$actual" ] if [ "$login" != "$actual" ]

View File

@@ -1,6 +1,6 @@
.Dd October 27, 2020 .Dd October 27, 2020
.Dt SDN-INSTALL 1 .Dt SDN-INSTALL 1
.Os Linux .Os
.Sh NAME .Sh NAME
.Nm sdn-install .Nm sdn-install
.Nd integrate sdn with the shell .Nd integrate sdn with the shell

222
sdn-mc-ext.cpp Normal file
View File

@@ -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;
}

54
sdn-view Executable file
View File

@@ -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

24
sdn-view.1 Normal file
View File

@@ -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.

13
sdn.1
View File

@@ -1,7 +1,7 @@
\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing \" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
.Dd October 27, 2020 .Dd December 30, 2024
.Dt SDN 1 .Dt SDN 1
.Os Linux .Os
.Sh NAME .Sh NAME
.Nm sdn .Nm sdn
.Nd directory navigator .Nd directory navigator
@@ -68,8 +68,8 @@ and you can use the
.Xr dircolors 1 .Xr dircolors 1
utility to initialize this variable. utility to initialize this variable.
.It Ev PAGER .It Ev PAGER
The viewer program to be launched by the F3 key binding as well as to show The viewer program to be launched by the F3 and F13 key bindings as well as
the internal help message. to show the internal help message.
If none is set, it defaults to If none is set, it defaults to
.Xr less 1 . .Xr less 1 .
.It Ev VISUAL , Ev EDITOR .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: To obtain more vifm-like controls and Windows-like quit abilities:
.Bd -literal -offset indent .Bd -literal -offset indent
normal h parent normal h parent
normal l choose normal l enter
normal M-f4 quit normal M-f4 quit
.Ed .Ed
.Pp .Pp
@@ -107,7 +107,7 @@ For rxvt, that would be:
define C-ppage ^[[5^ define C-ppage ^[[5^
define C-npage ^[[6^ define C-npage ^[[6^
normal C-ppage parent normal C-ppage parent
normal C-npage choose normal C-npage enter
.Ed .Ed
.Pp .Pp
Escape characters must be inserted verbatim, e.g., by pressing C-v ESC in vi, 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: following may work:
.Bd -literal -offset indent .Bd -literal -offset indent
cursor 231 202 cursor 231 202
select 202 bold
bar 16 255 ul bar 16 255 ul
cwd bold cwd bold
input input

450
sdn.cpp
View File

@@ -1,7 +1,7 @@
// //
// sdn: simple directory navigator // 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 // Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted. // purpose with or without fee is hereby granted.
@@ -28,37 +28,45 @@
#include <locale> #include <locale>
#include <map> #include <map>
#include <memory> #include <memory>
#include <set>
#include <sstream>
#include <string> #include <string>
#include <tuple> #include <tuple>
#include <vector> #include <vector>
#include <dirent.h> #include <dirent.h>
#include <fcntl.h> #include <fcntl.h>
#include <fnmatch.h>
#include <grp.h> #include <grp.h>
#include <libgen.h> #include <libgen.h>
#include <pwd.h> #include <pwd.h>
#include <signal.h> #include <signal.h>
#include <sys/acl.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/wait.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#include <acl/libacl.h> #ifdef __linux__
#include <ncurses.h>
#include <sys/inotify.h> #include <sys/inotify.h>
#include <sys/types.h> // ACL information is not important enough to be ported
#include <sys/wait.h> #include <acl/libacl.h>
#include <sys/acl.h>
#include <sys/xattr.h> #include <sys/xattr.h>
#else
#include <sys/event.h>
#endif
#include <ncurses.h>
// To implement cbreak() with disabled ^S that gets reënabled on endwin() // To implement cbreak() with disabled ^S that gets reënabled on endwin()
#define NCURSES_INTERNALS #define NCURSES_INTERNALS
#include <term.h> #include <term.h>
#undef CTRL // term.h -> termios.h -> sys/ttydefaults.h, too simplistic #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__ #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 #endif
// Trailing return types make C++ syntax suck considerably less // Trailing return types make C++ syntax suck considerably less
@@ -302,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 { fun cchar (chtype attrs, wchar_t c) -> cchar_t {
cchar_t ch {}; wchar_t ws[] = {c, 0}; cchar_t ch {}; wchar_t ws[] = {c, 0};
@@ -408,14 +430,17 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f) #define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \ #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(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(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \ XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \ 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_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) XX(INPUT_BACKWARD) XX(INPUT_FORWARD) XX(INPUT_BEGINNING) XX(INPUT_END)
#define XX(name) ACTION_ ## name, #define XX(name) ACTION_ ## name,
@@ -427,14 +452,18 @@ static const char *g_action_names[] = {ACTIONS(XX)};
#undef XX #undef XX
static map<wint_t, action> g_normal_actions { static map<wint_t, action> g_normal_actions {
{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL}, {'\r', ACTION_ENTER}, {KEY (ENTER), ACTION_ENTER},
{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE}, {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 (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}, {'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
// M-o ought to be the same shortcut the navigator is launched with // M-o ought to be the same shortcut the navigator is launched with
{ALT | 'o', ACTION_QUIT}, {ALT | 'o', ACTION_QUIT}, {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
{'<', 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}, {'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN}, {'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP}, {'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
@@ -442,12 +471,13 @@ static map<wint_t, action> g_normal_actions {
{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW}, {'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT}, {KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
{CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN}, {CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
{'z', ACTION_CENTER},
{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT}, {'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME}, {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH}, {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME}, {ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
{KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR}, {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}, {'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD}, {CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
}; };
@@ -457,7 +487,8 @@ static map<wint_t, action> g_input_actions {
// Sometimes terminfo is wrong, we need to accept both of these // Sometimes terminfo is wrong, we need to accept both of these
{L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE}, {L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE},
{KEY (BACKSPACE), ACTION_INPUT_B_DELETE}, {KEY (DC), ACTION_INPUT_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 ('K'), ACTION_INPUT_KILL_LINE},
{CTRL ('V'), ACTION_INPUT_QUOTED_INSERT}, {CTRL ('V'), ACTION_INPUT_QUOTED_INSERT},
{CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD}, {CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD},
@@ -468,6 +499,7 @@ static map<wint_t, action> g_input_actions {
static map<wint_t, action> g_search_actions { static map<wint_t, action> g_search_actions {
{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
{CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
{'/', ACTION_ENTER},
}; };
static const map<string, map<wint_t, action>*> g_binding_contexts { static const map<string, map<wint_t, action>*> g_binding_contexts {
{"normal", &g_normal_actions}, {"input", &g_input_actions}, {"normal", &g_normal_actions}, {"input", &g_input_actions},
@@ -508,6 +540,7 @@ struct entry {
struct level { struct level {
int offset, cursor; ///< Scroll offset and cursor position int offset, cursor; ///< Scroll offset and cursor position
string path, filename; ///< Level path and filename at cursor string path, filename; ///< Level path and filename at cursor
set<string> selection; ///< Filenames of selected entries
}; };
static struct { static struct {
@@ -515,6 +548,7 @@ static struct {
string cwd; ///< Current working directory string cwd; ///< Current working directory
string start_dir; ///< Starting directory string start_dir; ///< Starting directory
vector<entry> entries; ///< Current directory entries vector<entry> entries; ///< Current directory entries
set<string> selection; ///< Filenames of selected entries
vector<level> levels; ///< Upper directory levels vector<level> levels; ///< Upper directory levels
int offset, cursor; ///< Scroll offset and cursor position int offset, cursor; ///< Scroll offset and cursor position
bool full_view; ///< Show extended information bool full_view; ///< Show extended information
@@ -529,12 +563,12 @@ static struct {
wstring message; ///< Message for the user wstring message; ///< Message for the user
int message_ttl; ///< Time to live for the message 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 string ext_helper; ///< External helper to run
bool no_chdir; ///< Do not tell the shell to chdir bool no_chdir; ///< Do not tell the shell to chdir
bool quitting; ///< Whether we should quit already 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 bool out_of_date; ///< Entries may be out of date
const wchar_t *editor; ///< Prompt string for editing const wchar_t *editor; ///< Prompt string for editing
@@ -545,10 +579,11 @@ static struct {
void (*editor_on_change) (); ///< Callback on editor change void (*editor_on_change) (); ///< Callback on editor change
map<action, void (*) ()> editor_on; ///< Handlers for custom actions map<action, void (*) ()> editor_on; ///< Handlers for custom actions
enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE, AT_COUNT }; enum { AT_CURSOR, AT_SELECT, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE,
chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0}; AT_COUNT };
chtype attrs[AT_COUNT] = {A_REVERSE, A_BOLD, 0, A_BOLD, 0, A_ITALIC, 0};
const char *attr_names[AT_COUNT] = 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<int, chtype> ls_colors; ///< LS_COLORS decoded
map<string, chtype> ls_exts; ///< LS_COLORS file extensions map<string, chtype> ls_exts; ///< LS_COLORS file extensions
@@ -593,8 +628,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
set (LS_MULTIHARDLINK); set (LS_MULTIHARDLINK);
if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
set (LS_EXECUTABLE); set (LS_EXECUTABLE);
#ifdef __linux__
if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0) if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
set (LS_CAPABILITY); set (LS_CAPABILITY);
#endif
if ((info.st_mode & S_ISGID)) if ((info.st_mode & S_ISGID))
set (LS_SETGID); set (LS_SETGID);
if ((info.st_mode & S_ISUID)) if ((info.st_mode & S_ISUID))
@@ -636,6 +673,25 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
return format; 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 { fun make_entry (const struct dirent *f) -> entry {
entry e; entry e;
e.filename = f->d_name; e.filename = f->d_name;
@@ -669,11 +725,13 @@ fun make_entry (const struct dirent *f) -> entry {
} }
auto mode = decode_mode (info.st_mode); auto mode = decode_mode (info.st_mode);
#ifdef __linux__
// We're using a laughably small subset of libacl: this translates to // We're using a laughably small subset of libacl: this translates to
// two lgetxattr() calls, the results of which are compared with // two lgetxattr() calls, the results of which are compared with
// specific architecture-dependent constants. Linux-only. // specific architecture-dependent constants. Linux-only.
if (acl_extended_file_nofollow (f->d_name) > 0) if (acl_extended_file_nofollow (f->d_name) > 0)
mode += L"+"; mode += L"+";
#endif
e.cols[entry::MODES] = apply_attrs (mode, 0); e.cols[entry::MODES] = apply_attrs (mode, 0);
auto usr = g.unames.find (info.st_uid); auto usr = g.unames.find (info.st_uid);
@@ -686,11 +744,12 @@ fun make_entry (const struct dirent *f) -> entry {
? apply_attrs (grp->second, 0) ? apply_attrs (grp->second, 0)
: apply_attrs (to_wstring (info.st_gid), 0); : apply_attrs (to_wstring (info.st_gid), 0);
auto size = to_wstring (info.st_size); std::wstring size;
if (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T"; if (!suffixize (info.st_size, 40, L'T', size) &&
else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G"; !suffixize (info.st_size, 30, L'G', size) &&
else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M"; !suffixize (info.st_size, 20, L'M', size) &&
else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K"; !suffixize (info.st_size, 10, L'K', size))
size = to_wstring (info.st_size);
e.cols[entry::SIZE] = apply_attrs (size, 0); e.cols[entry::SIZE] = apply_attrs (size, 0);
wchar_t buf[32] = L""; wchar_t buf[32] = L"";
@@ -702,8 +761,8 @@ fun make_entry (const struct dirent *f) -> entry {
auto &fn = e.cols[entry::FILENAME] = auto &fn = e.cols[entry::FILENAME] =
apply_attrs (to_wide (e.filename), ls_format (e, false)); apply_attrs (to_wide (e.filename), ls_format (e, false));
if (!e.target_path.empty ()) { if (!e.target_path.empty ()) {
fn.append (apply_attrs (L" -> ", 0)); fn += apply_attrs (L" -> ", 0);
fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true))); fn += apply_attrs (to_wide (e.target_path), ls_format (e, true));
} }
return e; return e;
} }
@@ -720,18 +779,25 @@ fun update () {
int used = min (available, all - g.offset); int used = min (available, all - g.offset);
for (int i = 0; i < used; i++) { for (int i = 0; i < used; i++) {
auto index = g.offset + i; auto index = g.offset + i;
bool selected = index == g.cursor; bool cursored = index == g.cursor;
attrset (selected ? g.attrs[g.AT_CURSOR] : 0); 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); move (g.gravity ? (available - used + i) : i, 0);
auto used = 0; auto used = 0;
for (int col = start_column; col < entry::COLUMNS; col++) { for (int col = start_column; col < entry::COLUMNS; col++) {
const auto &field = g.entries[index].cols[col]; const auto &field = g.entries[index].cols[col];
auto aligned = align (field, alignment[col] * g.max_widths[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) if (g.sort_flash_ttl && col == g.sort_column)
for_each (begin (aligned), end (aligned), invert); 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); used += print (aligned + apply_attrs (L" ", 0), COLS - used);
} }
hline (' ', COLS - used); hline (' ', COLS - used);
@@ -773,12 +839,23 @@ fun update () {
print (info, info_width); print (info, info_width);
} }
auto start = sanitize (prompt + line.substr (0, g.editor_cursor)); line.resize (g.editor_cursor);
move (LINES - 1, compute_width (start)); move (LINES - 1, compute_width (sanitize (prompt + line)));
curs_set (1); curs_set (1);
} else if (!g.message.empty ()) { } else if (!g.message.empty ()) {
move (LINES - 1, 0); move (LINES - 1, 0);
print (apply_attrs (g.message, 0), COLS); 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 ()) { } else if (!g.cmdline.empty ()) {
move (LINES - 1, 0); move (LINES - 1, 0);
print (g.cmdline, COLS); print (g.cmdline, COLS);
@@ -841,6 +918,20 @@ fun resort (const string anchor = at_cursor ().filename) {
focus (anchor); 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) { fun reload (bool keep_anchor) {
g.unames.clear (); g.unames.clear ();
while (auto *ent = getpwent ()) while (auto *ent = getpwent ())
@@ -859,6 +950,16 @@ fun reload (bool keep_anchor) {
auto now = time (NULL); g.now = *localtime (&now); auto now = time (NULL); g.now = *localtime (&now);
auto dir = opendir ("."); auto dir = opendir (".");
g.entries.clear (); 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)) { while (auto f = readdir (dir)) {
string name = f->d_name; string name = f->d_name;
// Two dots are for navigation but this ain't as useful // Two dots are for navigation but this ain't as useful
@@ -869,6 +970,9 @@ fun reload (bool keep_anchor) {
} }
closedir (dir); closedir (dir);
g.selection = filter_selection (g.selection);
readfail:
g.out_of_date = false; g.out_of_date = false;
for (int col = 0; col < entry::COLUMNS; col++) { for (int col = 0; col < entry::COLUMNS; col++) {
auto &longest = g.max_widths[col] = 0; auto &longest = g.max_widths[col] = 0;
@@ -881,28 +985,38 @@ fun reload (bool keep_anchor) {
g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1)); g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1));
g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1)); g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1));
if (g.inotify_wd != -1) #ifdef __linux__
inotify_rm_watch (g.inotify_fd, g.inotify_wd); 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 // 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)); (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) { if ((g.watch_wd = open (".", O_RDONLY | O_DIRECTORY | O_CLOEXEC)) >= 0) {
g.message = to_wide (message); // At least the macOS kqueue doesn't report anything too specific
g.message_ttl = ttl; 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) { if (g.ext_helpers) {
// XXX: this doesn't try them all out, though it shouldn't make any // XXX: this doesn't try them all out,
// noticeable difference // though it shouldn't make any noticeable difference
const char *found = nullptr; const char *found = nullptr;
for (auto program : list) for (auto program : list)
if ((found = program)) if ((found = program))
break; break;
g.ext_helper = found + (" " + shell_escape (filename)); g.ext_helper.assign (found).append (args);
g.quitting = true; g.quitting = true;
return; return;
} }
@@ -918,8 +1032,8 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
tcsetpgrp (STDOUT_FILENO, getpgid (0)); tcsetpgrp (STDOUT_FILENO, getpgid (0));
for (auto program : list) for (auto program : list)
if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program) if (program) execl ("/bin/sh", "/bin/sh", "-c",
+ " " + shell_escape (filename)).c_str (), NULL); (program + args).c_str (), NULL);
_exit (EXIT_FAILURE); _exit (EXIT_FAILURE);
default: default:
// ...and make sure of it in the parent as well // ...and make sure of it in the parent as well
@@ -942,12 +1056,17 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
update (); 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 // 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 // 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); 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) { fun edit (const string &filename) {
run_program ({(const char *) getenv ("VISUAL"), run_program ({(const char *) getenv ("VISUAL"),
(const char *) getenv ("EDITOR"), "vi"}, filename); (const char *) getenv ("EDITOR"), "vi"}, filename);
@@ -1019,24 +1138,7 @@ fun show_help () {
fclose (contents); fclose (contents);
} }
/// Stays on the current match when there are no better ones, unless it's pushed fun matches_to_editor_info (int matches) {
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);
if (g.editor_line.empty ()) if (g.editor_line.empty ())
g.editor_info.clear (); g.editor_info.clear ();
else if (matches == 0) else if (matches == 0)
@@ -1047,6 +1149,56 @@ fun search_interactive (int push) {
g.editor_info = L"(" + to_wstring (matches) + L" matches)"; 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 () { fun fix_cursor_and_offset () {
g.cursor = min (g.cursor, int (g.entries.size ()) - 1); g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
g.cursor = max (g.cursor, 0); g.cursor = max (g.cursor, 0);
@@ -1090,6 +1242,7 @@ fun pop_levels (const string &old_cwd) {
g.offset = i->offset; g.offset = i->offset;
g.cursor = i->cursor; g.cursor = i->cursor;
anchor = i->filename; anchor = i->filename;
g.selection = filter_selection (i->selection);
} }
i++; i++;
g.levels.pop_back (); g.levels.pop_back ();
@@ -1103,7 +1256,7 @@ fun pop_levels (const string &old_cwd) {
fix_cursor_and_offset (); fix_cursor_and_offset ();
if (!anchor.empty () && at_cursor ().filename != anchor) 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) { fun explode_path (const string &path, vector<string> &out) {
@@ -1174,9 +1327,12 @@ fun change_dir (const string &path) {
return; 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; g.cwd = full_path;
bool same_path = last.path == g.cwd; bool same_path = last.path == g.cwd;
if (!same_path)
g.selection.clear ();
reload (same_path); reload (same_path);
if (!same_path) { if (!same_path) {
@@ -1214,12 +1370,23 @@ fun initial_cwd () -> string {
return ok ? pwd : cwd; 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 // Dive into directories and accessible symlinks to them
if (!S_ISDIR (entry.info.st_mode) if (!S_ISDIR (entry.info.st_mode)
&& !S_ISDIR (entry.target_info.st_mode)) { && !S_ISDIR (entry.target_info.st_mode)) {
g.chosen = entry.filename; // This could rather launch ${SDN_OPEN:-xdg-open} or something
g.quitting = true; choose (entry, false);
} else { } else {
change_dir (entry.filename); change_dir (entry.filename);
} }
@@ -1295,6 +1462,17 @@ fun handle_editor (wint_t c) {
break; break;
} }
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: case ACTION_INPUT_B_KILL_LINE:
g.editor_line.erase (0, g.editor_cursor); g.editor_line.erase (0, g.editor_cursor);
g.editor_cursor = 0; g.editor_cursor = 0;
@@ -1310,6 +1488,7 @@ fun handle_editor (wint_t c) {
if (auto handler = g.editor_on[action]) { if (auto handler = g.editor_on[action]) {
handler (); handler ();
} else if (c & (ALT | SYM)) { } else if (c & (ALT | SYM)) {
if (c != KEY (RESIZE))
beep (); beep ();
} else { } else {
g.editor_line.insert (g.editor_cursor, 1, c); g.editor_line.insert (g.editor_cursor, 1, c);
@@ -1338,16 +1517,19 @@ fun handle (wint_t c) -> bool {
auto i = g_normal_actions.find (c); auto i = g_normal_actions.find (c);
switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) { switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
case ACTION_CHOOSE_FULL: case ACTION_CHOOSE_FULL:
// FIXME: in the root directory, this inserts //item choose (current, true);
g.chosen = g.cwd + "/" + current.filename;
g.no_chdir = true;
g.quitting = true;
break; break;
case ACTION_CHOOSE: case ACTION_CHOOSE:
choose (current); choose (current, false);
break;
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; break;
case ACTION_VIEW: case ACTION_VIEW:
// Mimic mc, it does not seem sensible to page directories
(is_directory ? change_dir : view) (current.filename); (is_directory ? change_dir : view) (current.filename);
break; break;
case ACTION_EDIT: case ACTION_EDIT:
@@ -1374,6 +1556,33 @@ fun handle (wint_t c) -> bool {
resort (); resort ();
break; 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: case ACTION_UP:
g.cursor--; g.cursor--;
break; break;
@@ -1410,6 +1619,9 @@ fun handle (wint_t c) -> bool {
case ACTION_SCROLL_UP: case ACTION_SCROLL_UP:
g.offset--; g.offset--;
break; break;
case ACTION_CENTER:
g.offset = g.cursor - (visible_lines () - 1) / 2;
break;
case ACTION_CHDIR: case ACTION_CHDIR:
g.editor = L"chdir"; g.editor = L"chdir";
@@ -1429,10 +1641,15 @@ fun handle (wint_t c) -> bool {
case ACTION_SEARCH: case ACTION_SEARCH:
g.editor = L"search"; g.editor = L"search";
g.editor_on_change = [] { search_interactive (0); }; g.editor_on_change = [] { match_interactive (0); };
g.editor_on[ACTION_UP] = [] { search_interactive (-1); }; g.editor_on[ACTION_UP] = [] { match_interactive (-1); };
g.editor_on[ACTION_DOWN] = [] { search_interactive (+1); }; g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); };
g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); }; 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; break;
case ACTION_RENAME_PREFILL: case ACTION_RENAME_PREFILL:
g.editor_line = to_wide (current.filename); g.editor_line = to_wide (current.filename);
@@ -1484,19 +1701,27 @@ fun handle (wint_t c) -> bool {
return !g.quitting; return !g.quitting;
} }
fun inotify_check () { fun watch_check () {
// Only provide simple indication that contents might have changed
char buf[4096]; ssize_t len;
bool changed = false; 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; const inotify_event *e;
for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) { for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
e = (const inotify_event *) buf; e = (const inotify_event *) buf;
if (e->wd == g.inotify_wd) if (e->wd == g.watch_wd)
changed = g.out_of_date = true; 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 (); update ();
} }
@@ -1738,10 +1963,11 @@ fun load_bindings () {
} }
fun load_history_level (const vector<string> &v) { fun load_history_level (const vector<string> &v) {
if (v.size () != 7) if (v.size () < 7)
return; return;
// Not checking the hostname and parent PID right now since we can't merge // 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 () { fun load_config () {
@@ -1789,12 +2015,16 @@ fun save_config () {
*hostname = 0; *hostname = 0;
auto ppid = std::to_string (getppid ()); auto ppid = std::to_string (getppid ());
for (auto i = g.levels.begin (); i != g.levels.end (); i++) for (auto i = g.levels.begin (); i != g.levels.end (); i++) {
write_line (*config, {"history", hostname, ppid, i->path, vector<string> line {"history", hostname, ppid, i->path,
to_string (i->offset), to_string (i->cursor), i->filename}); to_string (i->offset), to_string (i->cursor), i->filename};
write_line (*config, {"history", hostname, ppid, g.cwd, line.insert (end (line), begin (i->selection), end (i->selection));
to_string (g.offset), to_string (g.cursor), write_line (*config, line);
at_cursor ().filename}); }
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[]) { int main (int argc, char *argv[]) {
@@ -1818,10 +2048,17 @@ int main (int argc, char *argv[]) {
// So that the neither us nor our children stop on tcsetpgrp() // So that the neither us nor our children stop on tcsetpgrp()
signal (SIGTTOU, SIG_IGN); 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; cerr << "cannot initialize inotify" << endl;
return 1; return 1;
} }
#else
if ((g.watch_fd = kqueue ()) < 0) {
cerr << "cannot initialize kqueue" << endl;
return 1;
}
#endif
locale::global (locale ("")); locale::global (locale (""));
load_bindings (); load_bindings ();
@@ -1841,9 +2078,12 @@ int main (int argc, char *argv[]) {
pop_levels (g.cwd); pop_levels (g.cwd);
update (); update ();
// Cunt, now I need to reïmplement all signal handling
#if NCURSES_VERSION_PATCH < 20210821
// This gets applied along with the following halfdelay() // This gets applied along with the following halfdelay()
cur_term->Nttyb.c_cc[VSTOP] = cur_term->Nttyb.c_cc[VSTOP] =
cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE; cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE;
#endif
// Invoking keypad() earlier would make ncurses flush its output buffer, // Invoking keypad() earlier would make ncurses flush its output buffer,
// which would worsen start-up flickering // which would worsen start-up flickering
@@ -1855,7 +2095,7 @@ int main (int argc, char *argv[]) {
wint_t c; wint_t c;
while (!read_key (c) || handle (c)) { while (!read_key (c) || handle (c)) {
inotify_check (); watch_check ();
if (g.sort_flash_ttl && !--g.sort_flash_ttl) if (g.sort_flash_ttl && !--g.sort_flash_ttl)
update (); update ();
if (g.message_ttl && !--g.message_ttl) { if (g.message_ttl && !--g.message_ttl) {
@@ -1867,8 +2107,12 @@ int main (int argc, char *argv[]) {
save_config (); save_config ();
// Presumably it is going to end up as an argument, so quote it // Presumably it is going to end up as an argument, so quote it
if (!g.chosen.empty ()) string chosen;
g.chosen = shell_escape (g.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 // We can't portably create a standard stream from an FD, so modify the FD
dup2 (output_fd, STDOUT_FILENO); dup2 (output_fd, STDOUT_FILENO);
@@ -1879,7 +2123,7 @@ int main (int argc, char *argv[]) {
else else
cout << "local cd=" << endl; 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; cout << "local helper=" << shell_escape (g.ext_helper) << endl;
return 0; return 0;
} }