Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
769be55153
|
|||
|
1dc39f900c
|
|||
|
dbea5cb193
|
|||
|
600d5b724b
|
|||
|
4d787e728c
|
|||
|
431d266411
|
|||
|
5a0732a622
|
|||
|
0407c2abce
|
|||
|
0a3224dd96
|
|||
|
10b6c04c97
|
|||
|
16ef3f9e47
|
|||
|
3e39cc5660
|
|||
|
977d1a7120
|
|||
|
9b274417c5
|
|||
|
cc14a9f735
|
|||
|
b070df6010
|
|||
|
3075d47aeb
|
|||
|
85b2d8a2ee
|
@@ -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)
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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.
|
||||
|
||||
11
Makefile
11
Makefile
@@ -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
27
NEWS
@@ -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
|
||||
|
||||
35
README.adoc
35
README.adoc
@@ -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
48
sdn-edit
Executable 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
24
sdn-edit.1
Normal 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.
|
||||
@@ -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;
|
||||
}
|
||||
72
sdn-open
Executable file
72
sdn-open
Executable 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
23
sdn-open.1
Normal 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
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
|
||||
24
sdn-view.1
Normal file
24
sdn-view.1
Normal 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
13
sdn.1
@@ -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
360
sdn.cpp
@@ -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 ¤t = 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user