diff --git a/CMakeLists.txt b/CMakeLists.txt index 87e5206..87a2c7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,8 +28,10 @@ target_compile_definitions (${PROJECT_NAME} PUBLIC 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) +install (PROGRAMS ${PROJECT_NAME}-install ${PROJECT_NAME}-view + DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (FILES sdn.1 sdn-install.1 sdn-view.1 + DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator") diff --git a/NEWS b/NEWS index 958058d..b5434ef 100644 --- a/NEWS +++ b/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 diff --git a/sdn-install.1 b/sdn-install.1 index c186f63..a6972c8 100644 --- a/sdn-install.1 +++ b/sdn-install.1 @@ -1,6 +1,6 @@ .Dd October 27, 2020 .Dt SDN-INSTALL 1 -.Os Linux +.Os .Sh NAME .Nm sdn-install .Nd integrate sdn with the shell diff --git a/sdn-view b/sdn-view new file mode 100755 index 0000000..b53db49 --- /dev/null +++ b/sdn-view @@ -0,0 +1,211 @@ +#!/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 + +export SDN_VIEW_CONFIG= +for dir in "$HOME"/.config/mc "$datadir" /etc/mc +do + if [ -n "$dir" -a -f "$dir/mc.ext.ini" ] + then + SDN_VIEW_CONFIG=$dir/mc.ext.ini + break + fi +done + +export PAGER=${PAGER:-less} +export MC_EXT_FILENAME=$(realpath "$1") +export MC_EXT_BASENAME=$(basename "$1") +export MC_EXT_CURRENTDIR=$(dirname "$1") +export SDN_VIEW_TYPE=$(file -bz "$1") +process() (awk -f - <<'EOF' +BEGIN { + if (!(Config = ENVIRON["SDN_VIEW_CONFIG"])) + exit + + Verb = "View" + Section = "" + while ((getline < Config) > 0) { + if (/^\s*(#.*)?$/) { + # Skip. + } else if (/^\[[^]]+\]$/) { + Sections[++SectionsLen] = Section = substr($0, 2, length($0) - 2) + } else if (/^[^=]+=[^=]*$/) { + split($0, kv, "=") + Keys[Section, kv[1]] = kv[2] + } + } + + Type = ENVIRON["SDN_VIEW_TYPE"] + Path = ENVIRON["MC_EXT_FILENAME"] + Basename = ENVIRON["MC_EXT_BASENAME"] + Dirname = ENVIRON["MC_EXT_CURRENTDIR"] + + for (i = 1; i <= SectionsLen; i++) { + if (Sections[i] == "mc.ext.ini" || + Sections[i] == "Default" || + Sections[i] ~ /^Include[\/]/) + continue + try(Sections[i]) + } + + # Not attempting any inclusions here. + print expand_command(Keys["Default", Verb]) +} + +function try(section, pair, a, key, full, include) { + for (pair in Keys) { + split(pair, a, SUBSEP) + if (a[1] == section) + full[a[2]] = Keys[pair] + } + if ("Include" in full) { + delete full["Open"] + delete full["View"] + delete full["Edit"] + include = "Include/" full["Include"] + for (pair in Keys) { + split(pair, a, SUBSEP) + if (a[1] == include) + full[a[2]] = Keys[pair] + } + } + if (ENVIRON["SDN_VIEW_DEBUG"]) { + print "[" section "]" > "/dev/stderr" + for (key in full) + print " " key ": " full[key] > "/dev/stderr" + } + if (Verb in full && section_matches(full, Type, Basename)) { + print expand_command(full[Verb]) + exit + } +} + +function shell_escape(string) { + gsub(/'/, "'\\''", string) + return "'" string "'" +} + +function expand_command(cmd, toview, out, seq, argument, value, a, pipe) { + out = "" + while (match(cmd, /%[a-zA-Z]*\{[^}]*\}|%[a-zA-Z]+|%%/)) { + out = out substr(cmd, 1, RSTART - 1) + seq = substr(cmd, RSTART + 1, RLENGTH - 1) + cmd = substr(cmd, RSTART + RLENGTH) + + argument = "" + if (match(seq, /\{.*\}$/)) { + argument = substr(seq, RSTART + 1, RLENGTH - 2) + seq = substr(seq, 1, RSTART - 1) + } + + if (seq == "%") { + out = out "%" + } else if (seq == "p") { + out = out shell_escape(Basename) + } else if (seq == "f") { + out = out shell_escape(Path) + } else if (seq == "d") { + out = out shell_escape(Dirname) + } else if (seq == "view") { + toview = 1 + + sub(/^ +/, "", cmd) + split(argument, a, /,/) + for (value in a) { + if (a[value] == "hex") + pipe = pipe " | od -t x1" + + # more(1) and less(1) either ignore or display this: + #if (a[value] == "nroff") + # pipe = pipe " | col -b" + } + } else if (seq == "var") { + value = "" + if (!match(argument, /:.*/)) { + if (argument in ENVIRON) + value = ENVIRON[argument] + } else { + value = substr(argument, RSTART + 1) + argument = substr(argument, 1, RSTART - 1) + if (argument in ENVIRON) + value = ENVIRON[argument] + } + out = out shell_escape(value) + } else if (seq == "") { + print Config ": prompting not supported" > "/dev/stderr" + return + } else { + print Config ": unsupported: %" seq > "/dev/stderr" + return + } + } + out = out cmd pipe + + # While the processing is mostly generic for all verbs, + # we'd have to distinguish non-view commands in this AWK script's output. + if (!toview) + return + + # In the case of out == "", we should just explicitly pass it to the pager, + # however it currently mixes with the case of "we can't use this View=". + return out +} + +function section_matches(section, type, basename, value) { + if ("Directory" in section) + return 0 + + if ("Type" in section) { + value = section["Type"] + if ("TypeIgnoreCase" in section && + section["TypeIgnoreCase"] == "true") { + type = tolower(type) + value = tolower(value) + } + gsub(/\\\\/, "\\", value) + gsub(/\\ /, " ", value) + if (type !~ value) + return 0 + } + if ("Regex" in section) { + value = section["Regex"] + if ("RegexIgnoreCase" in section && + section["RegexIgnoreCase"] == "true") { + basename = tolower(basename) + value = tolower(value) + } + gsub(/\\\\/, "\\", value) + return basename ~ value + } else if ("Shell" in section) { + value = section["Shell"] + if ("RegexIgnoreCase" in section && + section["ShellIgnoreCase"] == "true") { + basename = tolower(basename) + value = tolower(value) + } + if (value !~ /^[.]/) + return value == basename + return length(basename) >= length(value) && + substr(basename, length(basename) - length(value) + 1) == value + } + return type != "" +} +EOF +) +command=$(process) +if [ -z "$command" ] +then "$PAGER" -- "$MC_EXT_FILENAME" +else eval "$command" | "$PAGER" +fi diff --git a/sdn-view.1 b/sdn-view.1 new file mode 100644 index 0000000..d78d325 --- /dev/null +++ b/sdn-view.1 @@ -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. diff --git a/sdn.1 b/sdn.1 index 097a5de..0c9ee8d 100644 --- a/sdn.1 +++ b/sdn.1 @@ -1,7 +1,7 @@ \" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing .Dd October 27, 2020 .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 diff --git a/sdn.cpp b/sdn.cpp index 07f56a2..af2e09e 100644 --- a/sdn.cpp +++ b/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 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 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: