Compare commits
2 Commits
85b2d8a2ee
...
b070df6010
Author | SHA1 | Date | |
---|---|---|---|
b070df6010 | |||
3075d47aeb |
@ -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
8
NEWS
@ -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
|
||||
|
@ -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 \++
|
||||
|
@ -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
222
sdn-mc-ext.cpp
Normal 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> §ion) -> 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 §ion) -> 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 §ion : 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
54
sdn-view
Executable 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
23
sdn-view.1
Normal 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
6
sdn.1
@ -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
18
sdn.cpp
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user