Compare commits

...

2 Commits

Author SHA1 Message Date
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
9 changed files with 336 additions and 12 deletions

View File

@ -26,10 +26,17 @@ target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14)
target_compile_definitions (${PROJECT_NAME} PUBLIC
-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\")
add_executable (${PROJECT_NAME}-mc-ext ${PROJECT_NAME}-mc-ext.cpp)
target_compile_features (${PROJECT_NAME}-mc-ext PUBLIC cxx_std_17)
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (PROGRAMS ${PROJECT_NAME}-install DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES sdn.1 sdn-install.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
# sdn-mc-ext should be in libexec, but we prefer it in PATH.
install (TARGETS sdn sdn-mc-ext
DESTINATION ${CMAKE_INSTALL_BINDIR})
install (PROGRAMS sdn-install sdn-view
DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES sdn.1 sdn-install.1 sdn-view.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")

8
NEWS
View File

@ -1,3 +1,11 @@
Unreleased
* 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

@ -25,7 +25,7 @@ or as a https://git.janouch.name/p/nixexprs[Nix derivation].
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 (on Linux)
// Working around libasciidoc's missing support for escaping it like \++

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

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

23
sdn-view.1 Normal file
View File

@ -0,0 +1,23 @@
.Dd December 28, 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.
.Sh REPORTING BUGS
Use
.Lk https://git.janouch.name/p/sdn
to report bugs, request features, or submit pull requests.

6
sdn.1
View File

@ -1,7 +1,7 @@
\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
.Dd October 27, 2020
.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

18
sdn.cpp
View File

@ -428,7 +428,8 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \
XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW_RAW) XX(VIEW) XX(EDIT) \
XX(SORT_LEFT) XX(SORT_RIGHT) \
XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \
XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
@ -451,7 +452,8 @@ static map<wint_t, action> g_normal_actions {
{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
{KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT},
{KEY (F (3)), ACTION_VIEW}, {KEY (F (13)), ACTION_VIEW_RAW},
{KEY (F (4)), ACTION_EDIT},
{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
// M-o ought to be the same shortcut the navigator is launched with
{ALT | 'o', ACTION_QUIT},
@ -1015,12 +1017,17 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
update ();
}
fun view (const string &filename) {
fun view_raw (const string &filename) {
// XXX: we cannot realistically detect that the pager hasn't made a pause
// at the end of the file, so we can't ensure all contents have been seen
run_program ({(const char *) getenv ("PAGER"), "less", "cat"}, filename);
}
fun view (const string &filename) {
run_program ({(const char *) getenv ("SDN_VIEWER"), "sdn-view",
(const char *) getenv ("PAGER"), "less", "cat"}, filename);
}
fun edit (const string &filename) {
run_program ({(const char *) getenv ("VISUAL"),
(const char *) getenv ("EDITOR"), "vi"}, filename);
@ -1445,8 +1452,11 @@ fun handle (wint_t c) -> bool {
case ACTION_CHOOSE:
choose (current);
break;
case ACTION_VIEW:
case ACTION_VIEW_RAW:
// Mimic mc, it does not seem sensible to page directories
(is_directory ? change_dir : view_raw) (current.filename);
break;
case ACTION_VIEW:
(is_directory ? change_dir : view) (current.filename);
break;
case ACTION_EDIT: