18 Commits

Author SHA1 Message Date
769be55153 Update README.adoc
All checks were successful
Alpine 3.23 Success
Alpine 3.23 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-27 17:23:21 +01:00
1dc39f900c Add and integrate sdn-edit
All checks were successful
Alpine 3.23 Success
Alpine 3.23 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-27 15:35:55 +01:00
dbea5cb193 CMakeLists.txt: adjust packaging
All checks were successful
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Alpine 3.23 aarch64 Success
2026-01-25 12:02:53 +01:00
600d5b724b Bump version
All checks were successful
Alpine 3.23 Success
Alpine 3.23 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-10 13:53:33 +01:00
4d787e728c README.adoc: patch up formatting 2026-01-04 14:30:13 +01:00
431d266411 README.adoc: update
All checks were successful
Alpine 3.23 Success
Alpine 3.23 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-04 14:05:39 +01:00
5a0732a622 README.adoc: link to static builds
All checks were successful
Alpine 3.22 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-03 09:31:58 +01:00
0407c2abce Tune up the static build rule 2026-01-03 08:07:16 +01:00
0a3224dd96 Old libstdc++ is probably circumvented already 2026-01-03 07:09:25 +01:00
10b6c04c97 Add support for MSYS2
All checks were successful
Alpine 3.22 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-03 04:30:53 +01:00
16ef3f9e47 sdn-open: improve macOS and WSL experiences
All checks were successful
Alpine 3.22 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2025-11-21 12:52:56 +01:00
3e39cc5660 Add and integrate sdn-open
All checks were successful
Alpine 3.22 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Originally I thought that not supporting %cd would be an issue,
making this kind of utility unclean.

It turns out the desire to launch xdg-open quickly is stronger.
2025-11-20 21:16:30 +01:00
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
15 changed files with 816 additions and 131 deletions

View File

@@ -1,6 +1,6 @@
# target_compile_features has been introduced in that version
cmake_minimum_required (VERSION 3.1...3.27)
project (sdn VERSION 1.0 LANGUAGES CXX)
project (sdn VERSION 1.1 LANGUAGES CXX)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
set (CMAKE_CXX_FLAGS
@@ -16,24 +16,37 @@ if (NOT NCURSESW_FOUND)
endif ()
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS})
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}\")
PROJECT_NAME=\"${PROJECT_NAME}\" PROJECT_VERSION=\"${PROJECT_VERSION}\")
if (MSYS)
target_compile_definitions (${PROJECT_NAME} PUBLIC _GNU_SOURCE)
endif ()
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-open sdn-view sdn-edit
DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES sdn.1 sdn-install.1 sdn-open.1 sdn-view.1 sdn-edit.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_GENERATOR "TGZ")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
@@ -41,5 +54,8 @@ set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_DEBIAN_PACKAGE_RECOMMENDS "mc")
set (CPACK_DEBIAN_PACKAGE_SECTION "utils")
set (CPACK_SET_DESTDIR TRUE)
include (CPack)

View File

@@ -1,4 +1,4 @@
Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2017 - 2026, 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.

View File

@@ -7,13 +7,20 @@ CPPFLAGS = `sed -ne '/^project (\([^ )]*\) VERSION \([^ )]*\).*/ \
sdn: sdn.cpp CMakeLists.txt
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
-lacl `pkg-config --libs --cflags ncursesw`
sdn-static: sdn.cpp CMakeLists.txt
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
-static-libstdc++ \
-static -lacl `pkg-config --static --libs --cflags ncursesw`
# Works for Debian derivatives and Alpine, resulting in only a libc dependency.
sdn-portable: sdn.cpp CMakeLists.txt
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
-static-libstdc++ -static-libgcc \
-Wl,--start-group,-Bstatic \
-lacl `pkg-config --static --libs --cflags ncursesw` \
-Wl,--end-group,-Bdynamic
clean:
rm -f sdn sdn-static
rm -f sdn sdn-static sdn-portable
.PHONY: clean

27
NEWS
View File

@@ -1,3 +1,30 @@
Unreleased
* Added an sdn-edit script analogous to sdn-view, bound it to F4,
and moved the original key binding to F14.
1.1.0 (2026-01-10)
* 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;
- 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)
* Added an sdn-open script which does the same kind of processing as above
on top of xdg-open. This is what is now executed by M-Enter.
* Added support for Cygwin, in particular the MSYS2 flavour.
1.0.0 (2024-12-21)
* Initial release

View File

@@ -5,14 +5,16 @@ 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.
'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.
'sdn' runs on Linux, all BSD derivatives, and within MSYS2. I wanted to try
a different, simpler approach with this project, and the end result is
very friendly to tinkering.
image::sdn.png[align="center"]
@@ -23,14 +25,14 @@ 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].
https://janouch.name/cd[Static Linux builds can be found here],
simply place the binaries somewhere in your PATH.
Building
--------
Build dependencies: CMake and/or make, a C++14 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'
on start. Use GNU libstdc{plus}{plus} if you're affected.
Build-only dependencies: CMake and/or make, a C++17 compiler, pkg-config +
Runtime dependencies: ncursesw, libacl (on Linux) +
Optional runtime dependencies: Midnight Commander
$ git clone https://git.janouch.name/p/sdn.git
$ mkdir sdn/build
@@ -75,6 +77,7 @@ that of git, only named colours aren't supported:
....
cursor 231 202
select 202 bold
bar 16 255 ul
cwd bold
input
@@ -91,15 +94,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, F4, F14 keys are normally bound to actions 'view', 'view-raw',
'edit', and 'edit-raw', 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' and 'edit' find Midnight Commander, they 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

48
sdn-edit Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/sh -e
# sdn-edit: an editor for sdn that makes use of Midnight Commander configuration
# to make more kinds of files directly editable
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" Edit || :)
kind=$(echo "$output" | sed -n 1p)
command=$(echo "$output" | sed -n 2p)
case "$kind" in
'')
if [ -n "$command" ]
then eval "$command"
else "${VISUAL:-${EDITOR:-vi}}" -- "$MC_EXT_FILENAME"
fi
;;
*)
echo "Unsupported: $kind" >&2
exit 1
esac

24
sdn-edit.1 Normal file
View File

@@ -0,0 +1,24 @@
.Dd January 27, 2026
.Dt SDN-EDIT 1
.Os
.Sh NAME
.Nm sdn-edit
.Nd run Midnight Commander edit configuration externally
.Sh SYNOPSIS
.Nm sdn-edit
.Ar path
.Sh DESCRIPTION
.Nm
invokes
.Ev VISUAL ,
.Ev EDITOR
or a fallback editor 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 run the respective command.
.Sh REPORTING BUGS
Use
.Lk https://git.janouch.name/p/sdn
to report bugs, request features, or submit pull requests.

View File

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

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

72
sdn-open Executable file
View File

@@ -0,0 +1,72 @@
#!/bin/sh -e
# sdn-open: an opener for sdn that makes use of Midnight Commander configuration
# to make more kinds of files directly openable
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" Open || :)
kind=$(echo "$output" | sed -n 1p)
command=$(echo "$output" | sed -n 2p)
# We're trying to retain any explicit user preferences while navigating through:
# - Debian-based systems have /etc/alternatives/open as /usr/bin/open,
# pointing to either /usr/bin/xdg-open or /usr/bin/run-mailcap;
# - macOS has /usr/bin/open, and typically no xdg-open;
# - Windows Subsystem for Linux has explorer.exe in PATH,
# where launched applications may have problems with UNC paths,
# and possibly also xdg-open that may not be capable of opening much.
#
# Both macOS open and Windows explorer.exe are capable of opening files,
# directories, as well as URLs through native associations.
if [ -n "$MC_XDG_OPEN" ]
then :
elif command -v explorer.exe >/dev/null
then export MC_XDG_OPEN=explorer.exe
elif command -v open >/dev/null
then export MC_XDG_OPEN=open
elif command -v xdg-open >/dev/null
then export MC_XDG_OPEN=xdg-open
fi
case "$kind" in
cd)
# These mostly enter virtual filesystems, which we do not understand.
"$MC_XDG_OPEN" "$MC_EXT_FILENAME"
;;
'')
if [ -n "$command" ]
then eval "$command"
else "$MC_XDG_OPEN" "$MC_EXT_FILENAME"
fi
;;
*)
echo "Unsupported: $kind" >&2
exit 1
esac

23
sdn-open.1 Normal file
View File

@@ -0,0 +1,23 @@
.Dd November 20, 2025
.Dt SDN-OPEN 1
.Os
.Sh NAME
.Nm sdn-open
.Nd run Midnight Commander open configuration externally
.Sh SYNOPSIS
.Nm sdn-open
.Ar path
.Sh DESCRIPTION
.Nm
invokes
.Xr xdg-open 1
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 run any matching command instead.
.Sh REPORTING BUGS
Use
.Lk https://git.janouch.name/p/sdn
to report bugs, request features, or submit pull requests.

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

360
sdn.cpp
View File

@@ -1,7 +1,7 @@
//
// sdn: simple directory navigator
//
// Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2017 - 2026, 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.
@@ -28,6 +28,8 @@
#include <locale>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
@@ -51,7 +53,7 @@
#include <acl/libacl.h>
#include <sys/acl.h>
#include <sys/xattr.h>
#else
#elif !defined __CYGWIN__
#include <sys/event.h>
#endif
#include <ncurses.h>
@@ -423,12 +425,23 @@ fun decode_attrs (const vector<string> &attrs) -> chtype {
// --- Application -------------------------------------------------------------
using Key =
#if WINT_MAX >= INT32_MAX
wint_t;
#elif WINT_MIN < 0
int32_t;
#else
uint32_t;
#endif
enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define KEY(name) (SYM | KEY_ ## name)
#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(OPEN) XX(CHOOSE) XX(CHOOSE_FULL) \
XX(VIEW_RAW) XX(VIEW) XX(EDIT) XX(EDIT_RAW) 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(CENTER) \
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
@@ -447,15 +460,19 @@ enum action { ACTIONS(XX) ACTION_COUNT };
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},
static map<Key, action> g_normal_actions {
{'\r', ACTION_ENTER}, {KEY (ENTER), ACTION_ENTER},
{ALT | '\r', ACTION_OPEN}, {ALT | KEY (ENTER), ACTION_OPEN},
{'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}, {KEY (F (14)), ACTION_EDIT_RAW},
{'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},
@@ -469,11 +486,11 @@ static map<wint_t, action> g_normal_actions {
{'/', 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},
};
static map<wint_t, action> g_input_actions {
static map<Key, action> g_input_actions {
{27, ACTION_INPUT_ABORT}, {CTRL ('G'), ACTION_INPUT_ABORT},
{L'\r', ACTION_INPUT_CONFIRM}, {KEY (ENTER), ACTION_INPUT_CONFIRM},
// Sometimes terminfo is wrong, we need to accept both of these
@@ -488,11 +505,12 @@ static map<wint_t, action> g_input_actions {
{CTRL ('A'), ACTION_INPUT_BEGINNING}, {KEY (HOME), ACTION_INPUT_BEGINNING},
{CTRL ('E'), ACTION_INPUT_END}, {KEY (END), ACTION_INPUT_END},
};
static map<wint_t, action> g_search_actions {
static map<Key, 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 {
static const map<string, map<Key, action>*> g_binding_contexts {
{"normal", &g_normal_actions}, {"input", &g_input_actions},
{"search", &g_search_actions},
};
@@ -531,6 +549,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 {
@@ -538,6 +557,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
@@ -552,7 +572,7 @@ 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
@@ -568,18 +588,19 @@ static struct {
void (*editor_on_change) (); ///< Callback on editor change
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
bool ls_symlink_as_target; ///< ln=target in dircolors
map<string, wint_t, stringcaseless> name_to_key;
map<wint_t, string> key_to_name;
map<string, wint_t> custom_keys;
map<string, Key, stringcaseless> name_to_key;
map<Key, string> key_to_name;
map<string, Key> custom_keys;
string action_names[ACTION_COUNT]; ///< Stylized action names
// Refreshed by reload():
@@ -767,18 +788,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);
@@ -826,6 +854,17 @@ fun update () {
} 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);
@@ -893,6 +932,15 @@ fun show_message (const string &message, int ttl = 30) {
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 ();
while (auto *ent = getpwent ())
@@ -931,6 +979,8 @@ fun reload (bool keep_anchor) {
}
closedir (dir);
g.selection = filter_selection (g.selection);
readfail:
g.out_of_date = false;
for (int col = 0; col < entry::COLUMNS; col++) {
@@ -951,7 +1001,7 @@ readfail:
// We don't show atime, so access and open are merely spam
g.watch_wd = inotify_add_watch (g.watch_fd, ".",
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
#else
#elif !defined __CYGWIN__
if (g.watch_wd != -1)
close (g.watch_wd);
@@ -966,7 +1016,7 @@ readfail:
}
fun run_program (initializer_list<const char *> list, const string &filename) {
auto args = (!filename.empty() && filename.front() == '-' ? " -- " : " ")
auto args = (!filename.empty () && filename.front () == '-' ? " -- " : " ")
+ shell_escape (filename);
if (g.ext_helpers) {
// XXX: this doesn't try them all out,
@@ -1015,15 +1065,32 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
update ();
}
fun view (const string &filename) {
fun sdn_open (const string &filename) {
run_program ({(const char *) getenv ("SDN_OPENER"), "sdn-open", "xdg-open"},
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 edit (const string &filename) {
run_program ({(const char *) getenv ("VISUAL"),
(const char *) getenv ("EDITOR"), "vi"}, filename);
fun sdn_view (const string &filename) {
run_program ({(const char *) getenv ("SDN_VIEWER"), "sdn-view",
(const char *) getenv ("PAGER"), "less", "cat"}, filename);
}
fun edit_raw (const string &filename) {
run_program ({
(const char *) getenv ("VISUAL"), (const char *) getenv ("EDITOR"),
"vi"}, filename);
}
fun sdn_edit (const string &filename) {
run_program ({(const char *) getenv ("SDN_EDITOR"), "sdn-edit",
(const char *) getenv ("VISUAL"), (const char *) getenv ("EDITOR"),
"vi"}, filename);
}
fun run_pager (FILE *contents) {
@@ -1056,17 +1123,17 @@ fun run_pager (FILE *contents) {
update ();
}
fun encode_key (wint_t key) -> string {
fun encode_key (Key k) -> string {
string encoded;
if (key & ALT)
if (k & ALT)
encoded.append ("M-");
wchar_t bare = key & ~ALT;
Key bare = k & ~ALT;
if (g.key_to_name.count (bare))
encoded.append (capitalize (g.key_to_name.at (bare)));
else if (bare < 32 || bare == 0x7f)
encoded.append ("C-").append ({char (tolower ((bare + 64) & 0x7f))});
else
encoded.append (to_mb ({bare}));
encoded.append (to_mb ({wchar_t (bare)}));
return encoded;
}
@@ -1092,6 +1159,17 @@ fun show_help () {
fclose (contents);
}
fun matches_to_editor_info (int matches) {
if (g.editor_line.empty ())
g.editor_info.clear ();
else if (matches == 0)
g.editor_info = L"(no match)";
else if (matches == 1)
g.editor_info = L"(1 match)";
else
g.editor_info = L"(" + to_wstring (matches) + L" matches)";
}
fun match (const wstring &needle, int push) -> int {
string pattern = to_mb (needle) + "*";
bool jump_to_first = push || fnmatch (pattern.c_str (),
@@ -1108,15 +1186,23 @@ fun match (const wstring &needle, int push) -> int {
}
fun match_interactive (int push) {
int matches = match (g.editor_line, push);
if (g.editor_line.empty ())
g.editor_info.clear ();
else if (matches == 0)
g.editor_info = L"(no match)";
else if (matches == 1)
g.editor_info = L"(1 match)";
else
g.editor_info = L"(" + to_wstring (matches) + L" matches)";
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
@@ -1177,6 +1263,7 @@ fun pop_levels (const string &old_cwd) {
g.offset = i->offset;
g.cursor = i->cursor;
anchor = i->filename;
g.selection = filter_selection (i->selection);
}
i++;
g.levels.pop_back ();
@@ -1261,9 +1348,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) {
@@ -1301,12 +1391,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);
}
@@ -1320,19 +1421,19 @@ fun move_towards_spacing (int diff) -> bool {
wcwidth (g.editor_line.at (g.editor_cursor));
}
fun handle_editor (wint_t c) {
fun handle_editor (Key k) {
auto action = ACTION_NONE;
if (g.editor_inserting) {
(void) halfdelay (1);
g.editor_inserting = false;
} else {
auto i = g_input_actions.find (c);
auto i = g_input_actions.find (k);
if (i != g_input_actions.end ())
action = i->second;
auto m = g_binding_contexts.find (to_mb (g.editor));
if (m != g_binding_contexts.end () &&
(i = m->second->find (c)) != m->second->end ())
(i = m->second->find (k)) != m->second->end ())
action = i->second;
}
@@ -1407,11 +1508,11 @@ fun handle_editor (wint_t c) {
default:
if (auto handler = g.editor_on[action]) {
handler ();
} else if (c & (ALT | SYM)) {
if (c != KEY (RESIZE))
} else if (k & (ALT | SYM)) {
if (k != KEY (RESIZE))
beep ();
} else {
g.editor_line.insert (g.editor_cursor, 1, c);
g.editor_line.insert (g.editor_cursor, 1, k);
g.editor_cursor++;
}
}
@@ -1419,14 +1520,14 @@ fun handle_editor (wint_t c) {
g.editor_on_change ();
}
fun handle (wint_t c) -> bool {
if (c == WEOF)
fun handle (Key k) -> bool {
if (k == WEOF)
return false;
// If an editor is active, let it handle the key instead and eat it
if (g.editor) {
handle_editor (c);
c = WEOF;
handle_editor (k);
k = WEOF;
}
const auto &current = at_cursor ();
@@ -1434,23 +1535,32 @@ fun handle (wint_t c) -> bool {
S_ISDIR (current.info.st_mode) ||
S_ISDIR (current.target_info.st_mode);
auto i = g_normal_actions.find (c);
auto i = g_normal_actions.find (k);
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_ENTER:
enter (current);
break;
case ACTION_OPEN:
sdn_open (current.filename);
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:
// Mimic mc, it does not seem sensible to page directories
(is_directory ? change_dir : view) (current.filename);
(is_directory ? change_dir : sdn_view) (current.filename);
break;
case ACTION_EDIT:
edit (current.filename);
sdn_edit (current.filename);
break;
case ACTION_EDIT_RAW:
edit_raw (current.filename);
break;
case ACTION_HELP:
show_help ();
@@ -1473,6 +1583,33 @@ fun handle (wint_t c) -> bool {
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:
g.cursor--;
break;
@@ -1534,7 +1671,12 @@ fun handle (wint_t c) -> bool {
g.editor_on_change = [] { match_interactive (0); };
g.editor_on[ACTION_UP] = [] { match_interactive (-1); };
g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); };
g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); };
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);
@@ -1578,7 +1720,7 @@ fun handle (wint_t c) -> bool {
reload (true);
break;
default:
if (c != KEY (RESIZE) && c != WEOF)
if (k != KEY (RESIZE) && k != WEOF)
beep ();
}
fix_cursor_and_offset ();
@@ -1600,7 +1742,7 @@ fun watch_check () {
changed = true;
}
}
#else
#elif !defined __CYGWIN__
struct kevent ev {};
struct timespec timeout {};
if (kevent (g.watch_fd, nullptr, 0, &ev, 1, &timeout) > 0)
@@ -1713,7 +1855,7 @@ fun monotonic_ts_ms () -> int64_t {
return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6;
}
fun read_key (wint_t &c) -> bool {
fun read_key (Key &k) -> bool {
// XXX: on at least some systems, when run over ssh in a bind handler,
// after closing the terminal emulator we receive no fatal signal but our
// parent shell gets reparented under init and our stdin gets closed,
@@ -1722,37 +1864,40 @@ fun read_key (wint_t &c) -> bool {
// situation appears to be via timing. Checking errno doesn't work and
// resetting signal dispositions or the signal mask has no effect.
auto start = monotonic_ts_ms ();
wint_t c{};
int res = get_wch (&c);
if (res == ERR) {
c = WEOF;
if (monotonic_ts_ms () - start >= 50)
return false;
}
k = c;
wint_t metafied{};
if (c == 27 && (res = get_wch (&metafied)) != ERR)
c = ALT | metafied;
if (k == 27 && (res = get_wch (&metafied)) != ERR)
k = ALT | metafied;
if (res == KEY_CODE_YES)
c |= SYM;
k |= SYM;
return true;
}
fun parse_key (const string &key_name) -> wint_t {
wint_t c{};
fun parse_key (const string &key_name) -> Key {
Key k{};
auto p = key_name.c_str ();
if (!strncmp (p, "M-", 2)) {
c |= ALT;
k |= ALT;
p += 2;
}
if (g.name_to_key.count (p)) {
return c | g.name_to_key.at (p);
return k | g.name_to_key.at (p);
} else if (!strncmp (p, "C-", 2)) {
p += 2;
if (*p < '?' || *p > '~') {
cerr << "bindings: invalid combination: " << key_name << endl;
return WEOF;
}
c |= CTRL (*p);
k |= CTRL (*p);
p += 1;
} else {
wchar_t w; mbstate_t mb {};
@@ -1765,18 +1910,18 @@ fun parse_key (const string &key_name) -> wint_t {
cerr << "bindings: invalid encoding: " << key_name << endl;
return WEOF;
}
c |= w;
k |= w;
p += res;
}
if (*p) {
cerr << "key name has unparsable trailing part: " << key_name << endl;
return WEOF;
}
return c;
return k;
}
fun learn_named_key (const string &name, wint_t key) {
g.name_to_key[g.key_to_name[key] = name] = key;
fun learn_named_key (const string &name, Key k) {
g.name_to_key[g.key_to_name[k] = name] = k;
}
fun load_bindings () {
@@ -1835,23 +1980,24 @@ fun load_bindings () {
cerr << "bindings: invalid context: " << context << endl;
continue;
}
wint_t c = parse_key (key_name);
if (c == WEOF)
Key k = parse_key (key_name);
if (k == WEOF)
continue;
auto i = actions.find (action);
if (i == actions.end ()) {
cerr << "bindings: invalid action: " << action << endl;
continue;
}
(*m->second)[c] = i->second;
(*m->second)[k] = i->second;
}
}
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 () {
@@ -1899,12 +2045,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[]) {
@@ -1933,14 +2083,20 @@ int main (int argc, char *argv[]) {
cerr << "cannot initialize inotify" << endl;
return 1;
}
#else
#elif !defined __CYGWIN__
if ((g.watch_fd = kqueue ()) < 0) {
cerr << "cannot initialize kqueue" << endl;
return 1;
}
#endif
locale::global (locale (""));
try {
// Under MSYS2, the C++ locale mechanism cannot load UTF-8 this way.
locale::global (locale (""));
} catch (const runtime_error &) {
setlocale (LC_CTYPE, "");
}
load_bindings ();
load_config ();
@@ -1973,8 +2129,8 @@ int main (int argc, char *argv[]) {
return 1;
}
wint_t c;
while (!read_key (c) || handle (c)) {
Key k;
while (!read_key (k) || handle (k)) {
watch_check ();
if (g.sort_flash_ttl && !--g.sort_flash_ttl)
update ();
@@ -1987,8 +2143,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);
@@ -1999,7 +2159,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;
}