Compare commits
	
		
			22 Commits
		
	
	
		
			338d00d605
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						977d1a7120
	
				 | 
					
					
						|||
| 
						
						
							
						
						9b274417c5
	
				 | 
					
					
						|||
| 
						
						
							
						
						cc14a9f735
	
				 | 
					
					
						|||
| 
						
						
							
						
						b070df6010
	
				 | 
					
					
						|||
| 
						
						
							
						
						3075d47aeb
	
				 | 
					
					
						|||
| 
						
						
							
						
						85b2d8a2ee
	
				 | 
					
					
						|||
| 
						
						
							
						
						272ee62ad8
	
				 | 
					
					
						|||
| 
						
						
							
						
						a85426541a
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9b003735d
	
				 | 
					
					
						|||
| 
						
						
							
						
						52a28f01c8
	
				 | 
					
					
						|||
| 
						
						
							
						
						3607757554
	
				 | 
					
					
						|||
| 
						
						
							
						
						6eb216a40a
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ce6f47716
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9662f1a7b
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ddeb03652
	
				 | 
					
					
						|||
| 
						
						
							
						
						acb187c6b1
	
				 | 
					
					
						|||
| 
						
						
							
						
						9427df62e7
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d6999c415
	
				 | 
					
					
						|||
| 
						
						
							
						
						30ed61fdd2
	
				 | 
					
					
						|||
| 
						
						
							
						
						2df916c9b3
	
				 | 
					
					
						|||
| 
						
						
							
						
						24401825b4
	
				 | 
					
					
						|||
| 
						
						
							
						
						2bfb490798
	
				 | 
					
					
						
@@ -1,33 +1,42 @@
 | 
				
			|||||||
# target_compile_features has been introduced in that version
 | 
					# target_compile_features has been introduced in that version
 | 
				
			||||||
cmake_minimum_required (VERSION 3.1)
 | 
					cmake_minimum_required (VERSION 3.1...3.27)
 | 
				
			||||||
project (sdn VERSION 0.1 LANGUAGES CXX)
 | 
					project (sdn VERSION 1.0 LANGUAGES CXX)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
 | 
					if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
 | 
				
			||||||
	set (CMAKE_CXX_FLAGS
 | 
						set (CMAKE_CXX_FLAGS
 | 
				
			||||||
		"${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic")
 | 
							"${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic")
 | 
				
			||||||
endif ()
 | 
					endif ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Since we use a language with slow compilers, let's at least use a fast linker
 | 
					find_package (PkgConfig REQUIRED)
 | 
				
			||||||
execute_process (COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=gold -Wl,--version
 | 
					pkg_check_modules (ACL libacl)
 | 
				
			||||||
	ERROR_QUIET OUTPUT_VARIABLE ld_version)
 | 
					pkg_check_modules (NCURSESW ncursesw)
 | 
				
			||||||
if ("${ld_version}" MATCHES "GNU gold")
 | 
					if (NOT NCURSESW_FOUND)
 | 
				
			||||||
	set (CMAKE_EXE_LINKER_FLAGS "-fuse-ld=gold ${CMAKE_EXE_LINKER_FLAGS}")
 | 
						find_library (NCURSESW_LIBRARIES NAMES ncursesw)
 | 
				
			||||||
 | 
						find_path (NCURSESW_INCLUDE_DIRS ncurses.h PATH_SUFFIXES ncurses)
 | 
				
			||||||
endif ()
 | 
					endif ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
find_package (PkgConfig REQUIRED)
 | 
					 | 
				
			||||||
pkg_check_modules (NCURSESW QUIET ncursesw)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
 | 
					add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
 | 
				
			||||||
target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS})
 | 
					target_include_directories (${PROJECT_NAME}
 | 
				
			||||||
target_link_libraries (${PROJECT_NAME} PUBLIC ${NCURSESW_LIBRARIES} acl)
 | 
						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_features (${PROJECT_NAME} PUBLIC cxx_std_14)
 | 
				
			||||||
target_compile_definitions (${PROJECT_NAME} PUBLIC
 | 
					target_compile_definitions (${PROJECT_NAME} PUBLIC
 | 
				
			||||||
	-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\")
 | 
						-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)
 | 
					include (GNUInstallDirs)
 | 
				
			||||||
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
					# sdn-mc-ext should be in libexec, but we prefer it in PATH.
 | 
				
			||||||
install (PROGRAMS ${PROJECT_NAME}-install DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
					install (TARGETS sdn sdn-mc-ext
 | 
				
			||||||
install (FILES sdn.1 sdn-install.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
 | 
						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})
 | 
					install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
 | 
					set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
Copyright (c) 2017 - 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Permission to use, copy, modify, and/or distribute this software for any
 | 
					Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
purpose with or without fee is hereby granted.
 | 
					purpose with or without fee is hereby granted.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								NEWS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								NEWS
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					Unreleased
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 * 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, and M-Enter is synonymous to t;
 | 
				
			||||||
 | 
					    - 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1.0.0 (2024-12-21)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 * Initial release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										32
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								README.adoc
									
									
									
									
									
								
							@@ -5,26 +5,29 @@ sdn
 | 
				
			|||||||
'sdn' is a simple directory navigator that you can invoke while editing shell
 | 
					'sdn' is a simple directory navigator that you can invoke while editing shell
 | 
				
			||||||
commands.  It enables you to:
 | 
					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:
 | 
					 * 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
 | 
					   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
 | 
					   can be simply forwarded if it is to be edited.  What's more, it will always
 | 
				
			||||||
   be obvious whether the navigator is running.
 | 
					   be obvious whether the navigator is running.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The only supported platform is Linux.  I wanted to try a different, simpler
 | 
					'sdn' runs on Linux and all BSD derivatives.  I wanted to try a different,
 | 
				
			||||||
approach here, and the end result is very friendly to tinkering.
 | 
					simpler approach here, and the end result is very friendly to tinkering.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
image::sdn.png[align="center"]
 | 
					image::sdn.png[align="center"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Packages
 | 
					Packages
 | 
				
			||||||
--------
 | 
					--------
 | 
				
			||||||
Regular releases are sporadic.  git master should be stable enough.  You can get
 | 
					Regular releases are sporadic.  git master should be stable enough.
 | 
				
			||||||
a package with the latest development version from Archlinux's AUR.
 | 
					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].
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Building
 | 
					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
 | 
					Runtime dependencies: ncursesw, libacl (on Linux)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Working around libasciidoc's missing support for escaping it like \++
 | 
					// Working around libasciidoc's missing support for escaping it like \++
 | 
				
			||||||
Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn'
 | 
					Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn'
 | 
				
			||||||
@@ -73,6 +76,7 @@ that of git, only named colours aren't supported:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
....
 | 
					....
 | 
				
			||||||
cursor 231 202
 | 
					cursor 231 202
 | 
				
			||||||
 | 
					select 202 bold
 | 
				
			||||||
bar 16 255 ul
 | 
					bar 16 255 ul
 | 
				
			||||||
cwd bold
 | 
					cwd bold
 | 
				
			||||||
input
 | 
					input
 | 
				
			||||||
@@ -89,15 +93,19 @@ To obtain more vifm-like controls, you may write the following to your
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
....
 | 
					....
 | 
				
			||||||
normal h parent
 | 
					normal h parent
 | 
				
			||||||
normal l choose
 | 
					normal l enter
 | 
				
			||||||
....
 | 
					....
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Helper programs
 | 
					Helper programs
 | 
				
			||||||
~~~~~~~~~~~~~~~
 | 
					~~~~~~~~~~~~~~~
 | 
				
			||||||
The F3 and F4 keys are normally bound to actions 'view' and 'edit', similarly to
 | 
					The F3, F13 and F4 keys are normally bound to actions 'view', 'view-raw',
 | 
				
			||||||
Norton Commander and other orthodox file managers.  The helper programs used
 | 
					and 'edit', similarly to Norton Commander and other orthodox file managers.
 | 
				
			||||||
here may be changed by setting the PAGER and VISUAL (or EDITOR) environment
 | 
					The helper programs used here may be changed by setting the PAGER and VISUAL
 | 
				
			||||||
variables.
 | 
					(or EDITOR) environment variables.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If 'view' finds Midnight Commander, it 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
 | 
					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
 | 
					`PAGER='mcview -u' sdn`, beware that this helper cannot read files from its
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -125,7 +125,7 @@ done
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Figure out the shell to integrate with
 | 
					# Figure out the shell to integrate with
 | 
				
			||||||
login=$(basename "$SHELL")
 | 
					login=$(basename "$SHELL")
 | 
				
			||||||
actual=$(ps -p $$ -o ppid= | xargs ps -o comm= -p)
 | 
					actual=$(ps -p $$ -o ppid= | xargs ps -o comm= -p | sed 's/^-//')
 | 
				
			||||||
if [ -z "$shell" ]
 | 
					if [ -z "$shell" ]
 | 
				
			||||||
then
 | 
					then
 | 
				
			||||||
  if [ "$login" != "$actual" ]
 | 
					  if [ "$login" != "$actual" ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
.Dd October 27, 2020
 | 
					.Dd October 27, 2020
 | 
				
			||||||
.Dt SDN-INSTALL 1
 | 
					.Dt SDN-INSTALL 1
 | 
				
			||||||
.Os Linux
 | 
					.Os
 | 
				
			||||||
.Sh NAME
 | 
					.Sh NAME
 | 
				
			||||||
.Nm sdn-install
 | 
					.Nm sdn-install
 | 
				
			||||||
.Nd integrate sdn with the shell
 | 
					.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
 | 
				
			||||||
							
								
								
									
										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
 | 
					\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
 | 
				
			||||||
.Dd October 27, 2020
 | 
					.Dd December 30, 2024
 | 
				
			||||||
.Dt SDN 1
 | 
					.Dt SDN 1
 | 
				
			||||||
.Os Linux
 | 
					.Os
 | 
				
			||||||
.Sh NAME
 | 
					.Sh NAME
 | 
				
			||||||
.Nm sdn
 | 
					.Nm sdn
 | 
				
			||||||
.Nd directory navigator
 | 
					.Nd directory navigator
 | 
				
			||||||
@@ -68,8 +68,8 @@ and you can use the
 | 
				
			|||||||
.Xr dircolors 1
 | 
					.Xr dircolors 1
 | 
				
			||||||
utility to initialize this variable.
 | 
					utility to initialize this variable.
 | 
				
			||||||
.It Ev PAGER
 | 
					.It Ev PAGER
 | 
				
			||||||
The viewer program to be launched by the F3 key binding as well as to show
 | 
					The viewer program to be launched by the F3 and F13 key bindings as well as
 | 
				
			||||||
the internal help message.
 | 
					to show the internal help message.
 | 
				
			||||||
If none is set, it defaults to
 | 
					If none is set, it defaults to
 | 
				
			||||||
.Xr less 1 .
 | 
					.Xr less 1 .
 | 
				
			||||||
.It Ev VISUAL , Ev EDITOR
 | 
					.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:
 | 
					To obtain more vifm-like controls and Windows-like quit abilities:
 | 
				
			||||||
.Bd -literal -offset indent
 | 
					.Bd -literal -offset indent
 | 
				
			||||||
normal h parent
 | 
					normal h parent
 | 
				
			||||||
normal l choose
 | 
					normal l enter
 | 
				
			||||||
normal M-f4 quit
 | 
					normal M-f4 quit
 | 
				
			||||||
.Ed
 | 
					.Ed
 | 
				
			||||||
.Pp
 | 
					.Pp
 | 
				
			||||||
@@ -107,7 +107,7 @@ For rxvt, that would be:
 | 
				
			|||||||
define C-ppage ^[[5^
 | 
					define C-ppage ^[[5^
 | 
				
			||||||
define C-npage ^[[6^
 | 
					define C-npage ^[[6^
 | 
				
			||||||
normal C-ppage parent
 | 
					normal C-ppage parent
 | 
				
			||||||
normal C-npage choose
 | 
					normal C-npage enter
 | 
				
			||||||
.Ed
 | 
					.Ed
 | 
				
			||||||
.Pp
 | 
					.Pp
 | 
				
			||||||
Escape characters must be inserted verbatim, e.g., by pressing C-v ESC in vi,
 | 
					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:
 | 
					following may work:
 | 
				
			||||||
.Bd -literal -offset indent
 | 
					.Bd -literal -offset indent
 | 
				
			||||||
cursor 231 202
 | 
					cursor 231 202
 | 
				
			||||||
 | 
					select 202 bold
 | 
				
			||||||
bar 16 255 ul
 | 
					bar 16 255 ul
 | 
				
			||||||
cwd bold
 | 
					cwd bold
 | 
				
			||||||
input
 | 
					input
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										434
									
								
								sdn.cpp
									
									
									
									
									
								
							
							
						
						
									
										434
									
								
								sdn.cpp
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
// sdn: simple directory navigator
 | 
					// sdn: simple directory navigator
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Copyright (c) 2017 - 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					// Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
@@ -28,37 +28,45 @@
 | 
				
			|||||||
#include <locale>
 | 
					#include <locale>
 | 
				
			||||||
#include <map>
 | 
					#include <map>
 | 
				
			||||||
#include <memory>
 | 
					#include <memory>
 | 
				
			||||||
 | 
					#include <set>
 | 
				
			||||||
 | 
					#include <sstream>
 | 
				
			||||||
#include <string>
 | 
					#include <string>
 | 
				
			||||||
#include <tuple>
 | 
					#include <tuple>
 | 
				
			||||||
#include <vector>
 | 
					#include <vector>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <dirent.h>
 | 
					#include <dirent.h>
 | 
				
			||||||
#include <fcntl.h>
 | 
					#include <fcntl.h>
 | 
				
			||||||
 | 
					#include <fnmatch.h>
 | 
				
			||||||
#include <grp.h>
 | 
					#include <grp.h>
 | 
				
			||||||
#include <libgen.h>
 | 
					#include <libgen.h>
 | 
				
			||||||
#include <pwd.h>
 | 
					#include <pwd.h>
 | 
				
			||||||
#include <signal.h>
 | 
					#include <signal.h>
 | 
				
			||||||
#include <sys/acl.h>
 | 
					 | 
				
			||||||
#include <sys/stat.h>
 | 
					#include <sys/stat.h>
 | 
				
			||||||
#include <sys/types.h>
 | 
					#include <sys/types.h>
 | 
				
			||||||
 | 
					#include <sys/wait.h>
 | 
				
			||||||
#include <time.h>
 | 
					#include <time.h>
 | 
				
			||||||
#include <unistd.h>
 | 
					#include <unistd.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <acl/libacl.h>
 | 
					#ifdef __linux__
 | 
				
			||||||
#include <ncurses.h>
 | 
					 | 
				
			||||||
#include <sys/inotify.h>
 | 
					#include <sys/inotify.h>
 | 
				
			||||||
#include <sys/types.h>
 | 
					// ACL information is not important enough to be ported
 | 
				
			||||||
#include <sys/wait.h>
 | 
					#include <acl/libacl.h>
 | 
				
			||||||
 | 
					#include <sys/acl.h>
 | 
				
			||||||
#include <sys/xattr.h>
 | 
					#include <sys/xattr.h>
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
					#include <sys/event.h>
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					#include <ncurses.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// To implement cbreak() with disabled ^S that gets reënabled on endwin()
 | 
					// To implement cbreak() with disabled ^S that gets reënabled on endwin()
 | 
				
			||||||
#define NCURSES_INTERNALS
 | 
					#define NCURSES_INTERNALS
 | 
				
			||||||
#include <term.h>
 | 
					#include <term.h>
 | 
				
			||||||
#undef CTRL  // term.h -> termios.h -> sys/ttydefaults.h, too simplistic
 | 
					#undef CTRL  // term.h -> termios.h -> sys/ttydefaults.h, too simplistic
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Unicode is complex enough already and we might make assumptions
 | 
					 | 
				
			||||||
#ifndef __STDC_ISO_10646__
 | 
					#ifndef __STDC_ISO_10646__
 | 
				
			||||||
#error Unicode required for wchar_t
 | 
					// Unicode is complex enough already and we might make assumptions,
 | 
				
			||||||
 | 
					// though macOS doesn't define this despite using UCS-4,
 | 
				
			||||||
 | 
					// and we won't build on Windows that seems to be the only one to use UTF-16.
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Trailing return types make C++ syntax suck considerably less
 | 
					// Trailing return types make C++ syntax suck considerably less
 | 
				
			||||||
@@ -302,7 +310,21 @@ fun xdg_config_write (const string &suffix) -> unique_ptr<fstream> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using ncstring = basic_string<cchar_t>;
 | 
					// This should be basic_string, however that crashes on macOS
 | 
				
			||||||
 | 
					using ncstring = vector<cchar_t>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun operator+ (const ncstring &lhs, const ncstring &rhs) -> ncstring {
 | 
				
			||||||
 | 
						ncstring result;
 | 
				
			||||||
 | 
						result.reserve (lhs.size () + rhs.size ());
 | 
				
			||||||
 | 
						result.insert (result.end (), lhs.begin (), lhs.end ());
 | 
				
			||||||
 | 
						result.insert (result.end (), rhs.begin (), rhs.end ());
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun operator+= (ncstring &lhs, const ncstring &rhs) -> ncstring & {
 | 
				
			||||||
 | 
						lhs.insert (lhs.end (), rhs.begin (), rhs.end ());
 | 
				
			||||||
 | 
						return lhs;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun cchar (chtype attrs, wchar_t c) -> cchar_t {
 | 
					fun cchar (chtype attrs, wchar_t c) -> cchar_t {
 | 
				
			||||||
	cchar_t ch {}; wchar_t ws[] = {c, 0};
 | 
						cchar_t ch {}; wchar_t ws[] = {c, 0};
 | 
				
			||||||
@@ -408,14 +430,17 @@ enum { ALT = 1 << 24, SYM = 1 << 25 };  // Outside the range of Unicode
 | 
				
			|||||||
#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
 | 
					#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
 | 
					#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(CHOOSE) XX(CHOOSE_FULL) XX(VIEW_RAW) XX(VIEW) XX(EDIT) \
 | 
				
			||||||
 | 
						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(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(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \
 | 
				
			||||||
	XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
 | 
						XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
 | 
				
			||||||
	XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
 | 
						XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
 | 
				
			||||||
	XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \
 | 
						XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \
 | 
				
			||||||
	XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) XX(INPUT_DELETE) \
 | 
						XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) XX(INPUT_DELETE) \
 | 
				
			||||||
	XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) XX(INPUT_QUOTED_INSERT) \
 | 
						XX(INPUT_B_KILL_WORD) XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) \
 | 
				
			||||||
 | 
						XX(INPUT_QUOTED_INSERT) \
 | 
				
			||||||
	XX(INPUT_BACKWARD) XX(INPUT_FORWARD) XX(INPUT_BEGINNING) XX(INPUT_END)
 | 
						XX(INPUT_BACKWARD) XX(INPUT_FORWARD) XX(INPUT_BEGINNING) XX(INPUT_END)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define XX(name) ACTION_ ## name,
 | 
					#define XX(name) ACTION_ ## name,
 | 
				
			||||||
@@ -427,14 +452,18 @@ static const char *g_action_names[] = {ACTIONS(XX)};
 | 
				
			|||||||
#undef XX
 | 
					#undef XX
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static map<wint_t, action> g_normal_actions {
 | 
					static map<wint_t, action> g_normal_actions {
 | 
				
			||||||
	{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
 | 
						{'\r', ACTION_ENTER}, {KEY (ENTER), ACTION_ENTER},
 | 
				
			||||||
	{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
 | 
						{ALT | '\r', ACTION_CHOOSE}, {ALT | KEY (ENTER), ACTION_CHOOSE},
 | 
				
			||||||
 | 
						{'t', ACTION_CHOOSE}, {'T', ACTION_CHOOSE_FULL},
 | 
				
			||||||
	{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
 | 
						{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},
 | 
						{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
 | 
				
			||||||
	// M-o ought to be the same shortcut the navigator is launched with
 | 
						// M-o ought to be the same shortcut the navigator is launched with
 | 
				
			||||||
	{ALT | 'o', ACTION_QUIT},
 | 
						{ALT | 'o', ACTION_QUIT}, {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
 | 
				
			||||||
	{'<', 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},
 | 
						{'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
 | 
				
			||||||
	{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
 | 
						{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
 | 
				
			||||||
	{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
 | 
						{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
 | 
				
			||||||
@@ -442,12 +471,13 @@ static map<wint_t, action> g_normal_actions {
 | 
				
			|||||||
	{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
 | 
						{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
 | 
				
			||||||
	{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
 | 
						{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
 | 
				
			||||||
	{CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
 | 
						{CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
 | 
				
			||||||
 | 
						{'z', ACTION_CENTER},
 | 
				
			||||||
	{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
 | 
						{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
 | 
				
			||||||
	{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
 | 
						{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
 | 
				
			||||||
	{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
 | 
						{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
 | 
				
			||||||
	{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
 | 
						{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
 | 
				
			||||||
	{KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR},
 | 
						{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},
 | 
						{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
 | 
				
			||||||
	{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
 | 
						{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -457,7 +487,8 @@ static map<wint_t, action> g_input_actions {
 | 
				
			|||||||
	// Sometimes terminfo is wrong, we need to accept both of these
 | 
						// Sometimes terminfo is wrong, we need to accept both of these
 | 
				
			||||||
	{L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE},
 | 
						{L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE},
 | 
				
			||||||
	{KEY (BACKSPACE), ACTION_INPUT_B_DELETE}, {KEY (DC), ACTION_INPUT_DELETE},
 | 
						{KEY (BACKSPACE), ACTION_INPUT_B_DELETE}, {KEY (DC), ACTION_INPUT_DELETE},
 | 
				
			||||||
	{CTRL ('D'), ACTION_INPUT_DELETE}, {CTRL ('U'), ACTION_INPUT_B_KILL_LINE},
 | 
						{CTRL ('W'), ACTION_INPUT_B_KILL_WORD}, {CTRL ('D'), ACTION_INPUT_DELETE},
 | 
				
			||||||
 | 
						{CTRL ('U'), ACTION_INPUT_B_KILL_LINE},
 | 
				
			||||||
	{CTRL ('K'), ACTION_INPUT_KILL_LINE},
 | 
						{CTRL ('K'), ACTION_INPUT_KILL_LINE},
 | 
				
			||||||
	{CTRL ('V'), ACTION_INPUT_QUOTED_INSERT},
 | 
						{CTRL ('V'), ACTION_INPUT_QUOTED_INSERT},
 | 
				
			||||||
	{CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD},
 | 
						{CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD},
 | 
				
			||||||
@@ -468,6 +499,7 @@ static map<wint_t, action> g_input_actions {
 | 
				
			|||||||
static map<wint_t, action> g_search_actions {
 | 
					static map<wint_t, action> g_search_actions {
 | 
				
			||||||
	{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
 | 
						{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
 | 
				
			||||||
	{CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
 | 
						{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<wint_t, action>*> g_binding_contexts {
 | 
				
			||||||
	{"normal", &g_normal_actions}, {"input", &g_input_actions},
 | 
						{"normal", &g_normal_actions}, {"input", &g_input_actions},
 | 
				
			||||||
@@ -491,7 +523,7 @@ static const char *g_ls_colors[] = {LS(XX)};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
struct stringcaseless {
 | 
					struct stringcaseless {
 | 
				
			||||||
	bool operator () (const string &a, const string &b) const {
 | 
						bool operator () (const string &a, const string &b) const {
 | 
				
			||||||
		const auto &c = locale::classic();
 | 
							const auto &c = locale::classic ();
 | 
				
			||||||
		return lexicographical_compare (begin (a), end (a), begin (b), end (b),
 | 
							return lexicographical_compare (begin (a), end (a), begin (b), end (b),
 | 
				
			||||||
			[&](char m, char n) { return tolower (m, c) < tolower (n, c); });
 | 
								[&](char m, char n) { return tolower (m, c) < tolower (n, c); });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -508,6 +540,7 @@ struct entry {
 | 
				
			|||||||
struct level {
 | 
					struct level {
 | 
				
			||||||
	int offset, cursor;                 ///< Scroll offset and cursor position
 | 
						int offset, cursor;                 ///< Scroll offset and cursor position
 | 
				
			||||||
	string path, filename;              ///< Level path and filename at cursor
 | 
						string path, filename;              ///< Level path and filename at cursor
 | 
				
			||||||
 | 
						set<string> selection;              ///< Filenames of selected entries
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static struct {
 | 
					static struct {
 | 
				
			||||||
@@ -515,6 +548,7 @@ static struct {
 | 
				
			|||||||
	string cwd;                         ///< Current working directory
 | 
						string cwd;                         ///< Current working directory
 | 
				
			||||||
	string start_dir;                   ///< Starting directory
 | 
						string start_dir;                   ///< Starting directory
 | 
				
			||||||
	vector<entry> entries;              ///< Current directory entries
 | 
						vector<entry> entries;              ///< Current directory entries
 | 
				
			||||||
 | 
						set<string> selection;              ///< Filenames of selected entries
 | 
				
			||||||
	vector<level> levels;               ///< Upper directory levels
 | 
						vector<level> levels;               ///< Upper directory levels
 | 
				
			||||||
	int offset, cursor;                 ///< Scroll offset and cursor position
 | 
						int offset, cursor;                 ///< Scroll offset and cursor position
 | 
				
			||||||
	bool full_view;                     ///< Show extended information
 | 
						bool full_view;                     ///< Show extended information
 | 
				
			||||||
@@ -529,12 +563,12 @@ static struct {
 | 
				
			|||||||
	wstring message;                    ///< Message for the user
 | 
						wstring message;                    ///< Message for the user
 | 
				
			||||||
	int message_ttl;                    ///< Time to live for the message
 | 
						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
 | 
						string ext_helper;                  ///< External helper to run
 | 
				
			||||||
	bool no_chdir;                      ///< Do not tell the shell to chdir
 | 
						bool no_chdir;                      ///< Do not tell the shell to chdir
 | 
				
			||||||
	bool quitting;                      ///< Whether we should quit already
 | 
						bool quitting;                      ///< Whether we should quit already
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	int inotify_fd, inotify_wd = -1;    ///< File watch
 | 
						int watch_fd, watch_wd = -1;        ///< File watch (inotify/kqueue)
 | 
				
			||||||
	bool out_of_date;                   ///< Entries may be out of date
 | 
						bool out_of_date;                   ///< Entries may be out of date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const wchar_t *editor;              ///< Prompt string for editing
 | 
						const wchar_t *editor;              ///< Prompt string for editing
 | 
				
			||||||
@@ -545,10 +579,11 @@ static struct {
 | 
				
			|||||||
	void (*editor_on_change) ();        ///< Callback on editor change
 | 
						void (*editor_on_change) ();        ///< Callback on editor change
 | 
				
			||||||
	map<action, void (*) ()> editor_on; ///< Handlers for custom actions
 | 
						map<action, void (*) ()> editor_on; ///< Handlers for custom actions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE, AT_COUNT };
 | 
						enum { AT_CURSOR, AT_SELECT, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE,
 | 
				
			||||||
	chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0};
 | 
							AT_COUNT };
 | 
				
			||||||
 | 
						chtype attrs[AT_COUNT] = {A_REVERSE, A_BOLD, 0, A_BOLD, 0, A_ITALIC, 0};
 | 
				
			||||||
	const char *attr_names[AT_COUNT] =
 | 
						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<int, chtype> ls_colors;         ///< LS_COLORS decoded
 | 
				
			||||||
	map<string, chtype> ls_exts;        ///< LS_COLORS file extensions
 | 
						map<string, chtype> ls_exts;        ///< LS_COLORS file extensions
 | 
				
			||||||
@@ -593,8 +628,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
 | 
				
			|||||||
			set (LS_MULTIHARDLINK);
 | 
								set (LS_MULTIHARDLINK);
 | 
				
			||||||
		if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
 | 
							if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
 | 
				
			||||||
			set (LS_EXECUTABLE);
 | 
								set (LS_EXECUTABLE);
 | 
				
			||||||
 | 
					#ifdef __linux__
 | 
				
			||||||
		if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
 | 
							if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
 | 
				
			||||||
			set (LS_CAPABILITY);
 | 
								set (LS_CAPABILITY);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
		if ((info.st_mode & S_ISGID))
 | 
							if ((info.st_mode & S_ISGID))
 | 
				
			||||||
			set (LS_SETGID);
 | 
								set (LS_SETGID);
 | 
				
			||||||
		if ((info.st_mode & S_ISUID))
 | 
							if ((info.st_mode & S_ISUID))
 | 
				
			||||||
@@ -636,6 +673,25 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
 | 
				
			|||||||
	return format;
 | 
						return format;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun suffixize (off_t size, unsigned shift, wchar_t suffix, std::wstring &out)
 | 
				
			||||||
 | 
						-> bool {
 | 
				
			||||||
 | 
						// Prevent implementation-defined and undefined behaviour
 | 
				
			||||||
 | 
						if (size < 0 || shift >= sizeof size * 8)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						off_t divided = size >> shift;
 | 
				
			||||||
 | 
						if (divided >= 10) {
 | 
				
			||||||
 | 
							out.assign (std::to_wstring (divided)).append (1, suffix);
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						} else if (divided > 0) {
 | 
				
			||||||
 | 
							unsigned times_ten = size / double (off_t (1) << shift) * 10.0;
 | 
				
			||||||
 | 
							out.assign ({L'0' + wchar_t (times_ten / 10), L'.',
 | 
				
			||||||
 | 
								L'0' + wchar_t (times_ten % 10), suffix});
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun make_entry (const struct dirent *f) -> entry {
 | 
					fun make_entry (const struct dirent *f) -> entry {
 | 
				
			||||||
	entry e;
 | 
						entry e;
 | 
				
			||||||
	e.filename = f->d_name;
 | 
						e.filename = f->d_name;
 | 
				
			||||||
@@ -669,11 +725,13 @@ fun make_entry (const struct dirent *f) -> entry {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	auto mode = decode_mode (info.st_mode);
 | 
						auto mode = decode_mode (info.st_mode);
 | 
				
			||||||
 | 
					#ifdef __linux__
 | 
				
			||||||
	// We're using a laughably small subset of libacl: this translates to
 | 
						// We're using a laughably small subset of libacl: this translates to
 | 
				
			||||||
	// two lgetxattr() calls, the results of which are compared with
 | 
						// two lgetxattr() calls, the results of which are compared with
 | 
				
			||||||
	// specific architecture-dependent constants.  Linux-only.
 | 
						// specific architecture-dependent constants.  Linux-only.
 | 
				
			||||||
	if (acl_extended_file_nofollow (f->d_name) > 0)
 | 
						if (acl_extended_file_nofollow (f->d_name) > 0)
 | 
				
			||||||
		mode += L"+";
 | 
							mode += L"+";
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
	e.cols[entry::MODES] = apply_attrs (mode, 0);
 | 
						e.cols[entry::MODES] = apply_attrs (mode, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	auto usr = g.unames.find (info.st_uid);
 | 
						auto usr = g.unames.find (info.st_uid);
 | 
				
			||||||
@@ -686,11 +744,12 @@ fun make_entry (const struct dirent *f) -> entry {
 | 
				
			|||||||
		? apply_attrs (grp->second, 0)
 | 
							? apply_attrs (grp->second, 0)
 | 
				
			||||||
		: apply_attrs (to_wstring (info.st_gid), 0);
 | 
							: apply_attrs (to_wstring (info.st_gid), 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	auto size = to_wstring (info.st_size);
 | 
						std::wstring size;
 | 
				
			||||||
	if      (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T";
 | 
						if (!suffixize (info.st_size, 40, L'T', size) &&
 | 
				
			||||||
	else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G";
 | 
							!suffixize (info.st_size, 30, L'G', size) &&
 | 
				
			||||||
	else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M";
 | 
							!suffixize (info.st_size, 20, L'M', size) &&
 | 
				
			||||||
	else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
 | 
							!suffixize (info.st_size, 10, L'K', size))
 | 
				
			||||||
 | 
							size = to_wstring (info.st_size);
 | 
				
			||||||
	e.cols[entry::SIZE] = apply_attrs (size, 0);
 | 
						e.cols[entry::SIZE] = apply_attrs (size, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	wchar_t buf[32] = L"";
 | 
						wchar_t buf[32] = L"";
 | 
				
			||||||
@@ -702,8 +761,8 @@ fun make_entry (const struct dirent *f) -> entry {
 | 
				
			|||||||
	auto &fn = e.cols[entry::FILENAME] =
 | 
						auto &fn = e.cols[entry::FILENAME] =
 | 
				
			||||||
		apply_attrs (to_wide (e.filename), ls_format (e, false));
 | 
							apply_attrs (to_wide (e.filename), ls_format (e, false));
 | 
				
			||||||
	if (!e.target_path.empty ()) {
 | 
						if (!e.target_path.empty ()) {
 | 
				
			||||||
		fn.append (apply_attrs (L" -> ", 0));
 | 
							fn += apply_attrs (L" -> ", 0);
 | 
				
			||||||
		fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
 | 
							fn += apply_attrs (to_wide (e.target_path), ls_format (e, true));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return e;
 | 
						return e;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -720,18 +779,25 @@ fun update () {
 | 
				
			|||||||
	int used = min (available, all - g.offset);
 | 
						int used = min (available, all - g.offset);
 | 
				
			||||||
	for (int i = 0; i < used; i++) {
 | 
						for (int i = 0; i < used; i++) {
 | 
				
			||||||
		auto index = g.offset + i;
 | 
							auto index = g.offset + i;
 | 
				
			||||||
		bool selected = index == g.cursor;
 | 
							bool cursored = index == g.cursor;
 | 
				
			||||||
		attrset (selected ? g.attrs[g.AT_CURSOR] : 0);
 | 
							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);
 | 
							move (g.gravity ? (available - used + i) : i, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		auto used = 0;
 | 
							auto used = 0;
 | 
				
			||||||
		for (int col = start_column; col < entry::COLUMNS; col++) {
 | 
							for (int col = start_column; col < entry::COLUMNS; col++) {
 | 
				
			||||||
			const auto &field = g.entries[index].cols[col];
 | 
								const auto &field = g.entries[index].cols[col];
 | 
				
			||||||
			auto aligned = align (field, alignment[col] * g.max_widths[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)
 | 
								if (g.sort_flash_ttl && col == g.sort_column)
 | 
				
			||||||
				for_each (begin (aligned), end (aligned), invert);
 | 
									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);
 | 
								used += print (aligned + apply_attrs (L" ", 0), COLS - used);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		hline (' ', COLS - used);
 | 
							hline (' ', COLS - used);
 | 
				
			||||||
@@ -773,12 +839,23 @@ fun update () {
 | 
				
			|||||||
			print (info, info_width);
 | 
								print (info, info_width);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		auto start = sanitize (prompt + line.substr (0, g.editor_cursor));
 | 
							line.resize (g.editor_cursor);
 | 
				
			||||||
		move (LINES - 1, compute_width (start));
 | 
							move (LINES - 1, compute_width (sanitize (prompt + line)));
 | 
				
			||||||
		curs_set (1);
 | 
							curs_set (1);
 | 
				
			||||||
	} else if (!g.message.empty ()) {
 | 
						} else if (!g.message.empty ()) {
 | 
				
			||||||
		move (LINES - 1, 0);
 | 
							move (LINES - 1, 0);
 | 
				
			||||||
		print (apply_attrs (g.message, 0), COLS);
 | 
							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 ()) {
 | 
						} else if (!g.cmdline.empty ()) {
 | 
				
			||||||
		move (LINES - 1, 0);
 | 
							move (LINES - 1, 0);
 | 
				
			||||||
		print (g.cmdline, COLS);
 | 
							print (g.cmdline, COLS);
 | 
				
			||||||
@@ -846,6 +923,15 @@ fun show_message (const string &message, int ttl = 30) {
 | 
				
			|||||||
	g.message_ttl = ttl;
 | 
						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) {
 | 
					fun reload (bool keep_anchor) {
 | 
				
			||||||
	g.unames.clear ();
 | 
						g.unames.clear ();
 | 
				
			||||||
	while (auto *ent = getpwent ())
 | 
						while (auto *ent = getpwent ())
 | 
				
			||||||
@@ -868,7 +954,7 @@ fun reload (bool keep_anchor) {
 | 
				
			|||||||
		show_message (strerror (errno));
 | 
							show_message (strerror (errno));
 | 
				
			||||||
		if (g.cwd != "/") {
 | 
							if (g.cwd != "/") {
 | 
				
			||||||
			struct dirent f = {};
 | 
								struct dirent f = {};
 | 
				
			||||||
			strncpy(f.d_name, "..", sizeof f.d_name);
 | 
								strncpy (f.d_name, "..", sizeof f.d_name);
 | 
				
			||||||
			f.d_type = DT_DIR;
 | 
								f.d_type = DT_DIR;
 | 
				
			||||||
			g.entries.push_back (make_entry (&f));
 | 
								g.entries.push_back (make_entry (&f));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -884,6 +970,8 @@ fun reload (bool keep_anchor) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	closedir (dir);
 | 
						closedir (dir);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.selection = filter_selection (g.selection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
readfail:
 | 
					readfail:
 | 
				
			||||||
	g.out_of_date = false;
 | 
						g.out_of_date = false;
 | 
				
			||||||
	for (int col = 0; col < entry::COLUMNS; col++) {
 | 
						for (int col = 0; col < entry::COLUMNS; col++) {
 | 
				
			||||||
@@ -897,23 +985,38 @@ readfail:
 | 
				
			|||||||
	g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1));
 | 
						g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1));
 | 
				
			||||||
	g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1));
 | 
						g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (g.inotify_wd != -1)
 | 
					#ifdef __linux__
 | 
				
			||||||
		inotify_rm_watch (g.inotify_fd, g.inotify_wd);
 | 
						if (g.watch_wd != -1)
 | 
				
			||||||
 | 
							inotify_rm_watch (g.watch_fd, g.watch_wd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// We don't show atime, so access and open are merely spam
 | 
						// We don't show atime, so access and open are merely spam
 | 
				
			||||||
	g.inotify_wd = inotify_add_watch (g.inotify_fd, ".",
 | 
						g.watch_wd = inotify_add_watch (g.watch_fd, ".",
 | 
				
			||||||
		(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
 | 
							(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
						if (g.watch_wd != -1)
 | 
				
			||||||
 | 
							close (g.watch_wd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ((g.watch_wd = open (".", O_RDONLY | O_DIRECTORY | O_CLOEXEC)) >= 0) {
 | 
				
			||||||
 | 
							// At least the macOS kqueue doesn't report anything too specific
 | 
				
			||||||
 | 
							struct kevent ev {};
 | 
				
			||||||
 | 
							EV_SET (&ev, g.watch_wd, EVFILT_VNODE, EV_ADD | EV_CLEAR,
 | 
				
			||||||
 | 
								NOTE_WRITE | NOTE_LINK, 0, nullptr);
 | 
				
			||||||
 | 
							(void) kevent (g.watch_fd, &ev, 1, nullptr, 0, nullptr);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun run_program (initializer_list<const char *> list, const string &filename) {
 | 
					fun run_program (initializer_list<const char *> list, const string &filename) {
 | 
				
			||||||
 | 
						auto args = (!filename.empty () && filename.front () == '-' ? " -- " : " ")
 | 
				
			||||||
 | 
							+ shell_escape (filename);
 | 
				
			||||||
	if (g.ext_helpers) {
 | 
						if (g.ext_helpers) {
 | 
				
			||||||
		// XXX: this doesn't try them all out, though it shouldn't make any
 | 
							// XXX: this doesn't try them all out,
 | 
				
			||||||
		// noticeable difference
 | 
							// though it shouldn't make any noticeable difference
 | 
				
			||||||
		const char *found = nullptr;
 | 
							const char *found = nullptr;
 | 
				
			||||||
		for (auto program : list)
 | 
							for (auto program : list)
 | 
				
			||||||
			if ((found = program))
 | 
								if ((found = program))
 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
		g.ext_helper = found + (" " + shell_escape (filename));
 | 
							g.ext_helper.assign (found).append (args);
 | 
				
			||||||
		g.quitting = true;
 | 
							g.quitting = true;
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -929,8 +1032,8 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
 | 
				
			|||||||
		tcsetpgrp (STDOUT_FILENO, getpgid (0));
 | 
							tcsetpgrp (STDOUT_FILENO, getpgid (0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (auto program : list)
 | 
							for (auto program : list)
 | 
				
			||||||
			if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program)
 | 
								if (program) execl ("/bin/sh", "/bin/sh", "-c",
 | 
				
			||||||
				+ " " + shell_escape (filename)).c_str (), NULL);
 | 
									(program + args).c_str (), NULL);
 | 
				
			||||||
		_exit (EXIT_FAILURE);
 | 
							_exit (EXIT_FAILURE);
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		// ...and make sure of it in the parent as well
 | 
							// ...and make sure of it in the parent as well
 | 
				
			||||||
@@ -953,12 +1056,17 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
 | 
				
			|||||||
	update ();
 | 
						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
 | 
						// 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
 | 
						// 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);
 | 
						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) {
 | 
					fun edit (const string &filename) {
 | 
				
			||||||
	run_program ({(const char *) getenv ("VISUAL"),
 | 
						run_program ({(const char *) getenv ("VISUAL"),
 | 
				
			||||||
		(const char *) getenv ("EDITOR"), "vi"}, filename);
 | 
							(const char *) getenv ("EDITOR"), "vi"}, filename);
 | 
				
			||||||
@@ -1030,24 +1138,7 @@ fun show_help () {
 | 
				
			|||||||
	fclose (contents);
 | 
						fclose (contents);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Stays on the current match when there are no better ones, unless it's pushed
 | 
					fun matches_to_editor_info (int matches) {
 | 
				
			||||||
fun search (const wstring &needle, int push) -> int {
 | 
					 | 
				
			||||||
	int best = g.cursor, best_n = 0, matches = 0, step = push != 0 ? push : 1;
 | 
					 | 
				
			||||||
	for (int i = 0, count = g.entries.size (); i < count; i++) {
 | 
					 | 
				
			||||||
		int o = (g.cursor + (count + i * step) + (count + push)) % count;
 | 
					 | 
				
			||||||
		size_t n = prefix_length (to_wide (g.entries[o].filename), needle);
 | 
					 | 
				
			||||||
		matches += n == needle.size ();
 | 
					 | 
				
			||||||
		if (n > (size_t) best_n) {
 | 
					 | 
				
			||||||
			best = o;
 | 
					 | 
				
			||||||
			best_n = n;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	g.cursor = best;
 | 
					 | 
				
			||||||
	return matches;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fun search_interactive (int push) {
 | 
					 | 
				
			||||||
	int matches = search (g.editor_line, push);
 | 
					 | 
				
			||||||
	if (g.editor_line.empty ())
 | 
						if (g.editor_line.empty ())
 | 
				
			||||||
		g.editor_info.clear ();
 | 
							g.editor_info.clear ();
 | 
				
			||||||
	else if (matches == 0)
 | 
						else if (matches == 0)
 | 
				
			||||||
@@ -1058,6 +1149,56 @@ fun search_interactive (int push) {
 | 
				
			|||||||
		g.editor_info = L"(" + to_wstring (matches) + L" matches)";
 | 
							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 (),
 | 
				
			||||||
 | 
							g.entries[g.cursor].filename.c_str (), 0) == FNM_NOMATCH;
 | 
				
			||||||
 | 
						int best = g.cursor, matches = 0, step = push + !push;
 | 
				
			||||||
 | 
						for (int i = 0, count = g.entries.size (); i < count; i++) {
 | 
				
			||||||
 | 
							int o = (g.cursor + (count + i * step) + (count + push)) % count;
 | 
				
			||||||
 | 
							if (!fnmatch (pattern.c_str (), g.entries[o].filename.c_str (), 0)
 | 
				
			||||||
 | 
							 && !matches++ && jump_to_first)
 | 
				
			||||||
 | 
								best = o;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g.cursor = best;
 | 
				
			||||||
 | 
						return matches;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun match_interactive (int push) {
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					fun lookup (const wstring &needle) {
 | 
				
			||||||
 | 
						int best = g.cursor;
 | 
				
			||||||
 | 
						size_t best_n = 0;
 | 
				
			||||||
 | 
						for (int i = 0, count = g.entries.size (); i < count; i++) {
 | 
				
			||||||
 | 
							int o = (g.cursor + i) % count;
 | 
				
			||||||
 | 
							size_t n = prefix_length (to_wide (g.entries[o].filename), needle);
 | 
				
			||||||
 | 
							if (n > best_n) {
 | 
				
			||||||
 | 
								best = o;
 | 
				
			||||||
 | 
								best_n = n;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g.cursor = best;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun fix_cursor_and_offset () {
 | 
					fun fix_cursor_and_offset () {
 | 
				
			||||||
	g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
 | 
						g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
 | 
				
			||||||
	g.cursor = max (g.cursor, 0);
 | 
						g.cursor = max (g.cursor, 0);
 | 
				
			||||||
@@ -1101,6 +1242,7 @@ fun pop_levels (const string &old_cwd) {
 | 
				
			|||||||
			g.offset = i->offset;
 | 
								g.offset = i->offset;
 | 
				
			||||||
			g.cursor = i->cursor;
 | 
								g.cursor = i->cursor;
 | 
				
			||||||
			anchor = i->filename;
 | 
								anchor = i->filename;
 | 
				
			||||||
 | 
								g.selection = filter_selection (i->selection);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		i++;
 | 
							i++;
 | 
				
			||||||
		g.levels.pop_back ();
 | 
							g.levels.pop_back ();
 | 
				
			||||||
@@ -1114,7 +1256,7 @@ fun pop_levels (const string &old_cwd) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	fix_cursor_and_offset ();
 | 
						fix_cursor_and_offset ();
 | 
				
			||||||
	if (!anchor.empty () && at_cursor ().filename != anchor)
 | 
						if (!anchor.empty () && at_cursor ().filename != anchor)
 | 
				
			||||||
		search (to_wide (anchor), 0);
 | 
							lookup (to_wide (anchor));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun explode_path (const string &path, vector<string> &out) {
 | 
					fun explode_path (const string &path, vector<string> &out) {
 | 
				
			||||||
@@ -1185,9 +1327,12 @@ fun change_dir (const string &path) {
 | 
				
			|||||||
		return;
 | 
							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;
 | 
						g.cwd = full_path;
 | 
				
			||||||
	bool same_path = last.path == g.cwd;
 | 
						bool same_path = last.path == g.cwd;
 | 
				
			||||||
 | 
						if (!same_path)
 | 
				
			||||||
 | 
							g.selection.clear ();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	reload (same_path);
 | 
						reload (same_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!same_path) {
 | 
						if (!same_path) {
 | 
				
			||||||
@@ -1225,12 +1370,23 @@ fun initial_cwd () -> string {
 | 
				
			|||||||
	return ok ? pwd : cwd;
 | 
						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
 | 
						// Dive into directories and accessible symlinks to them
 | 
				
			||||||
	if (!S_ISDIR (entry.info.st_mode)
 | 
						if (!S_ISDIR (entry.info.st_mode)
 | 
				
			||||||
	 && !S_ISDIR (entry.target_info.st_mode)) {
 | 
						 && !S_ISDIR (entry.target_info.st_mode)) {
 | 
				
			||||||
		g.chosen = entry.filename;
 | 
							// This could rather launch ${SDN_OPEN:-xdg-open} or something
 | 
				
			||||||
		g.quitting = true;
 | 
							choose (entry, false);
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		change_dir (entry.filename);
 | 
							change_dir (entry.filename);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -1306,6 +1462,17 @@ fun handle_editor (wint_t c) {
 | 
				
			|||||||
				break;
 | 
									break;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
 | 
						case ACTION_INPUT_B_KILL_WORD:
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							int i = g.editor_cursor;
 | 
				
			||||||
 | 
							while (i && g.editor_line[--i] == L' ');
 | 
				
			||||||
 | 
							while (i-- && g.editor_line[i] != L' ');
 | 
				
			||||||
 | 
							i++;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							g.editor_line.erase (i, g.editor_cursor - i);
 | 
				
			||||||
 | 
							g.editor_cursor = i;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	case ACTION_INPUT_B_KILL_LINE:
 | 
						case ACTION_INPUT_B_KILL_LINE:
 | 
				
			||||||
		g.editor_line.erase (0, g.editor_cursor);
 | 
							g.editor_line.erase (0, g.editor_cursor);
 | 
				
			||||||
		g.editor_cursor = 0;
 | 
							g.editor_cursor = 0;
 | 
				
			||||||
@@ -1321,7 +1488,8 @@ fun handle_editor (wint_t c) {
 | 
				
			|||||||
		if (auto handler = g.editor_on[action]) {
 | 
							if (auto handler = g.editor_on[action]) {
 | 
				
			||||||
			handler ();
 | 
								handler ();
 | 
				
			||||||
		} else if (c & (ALT | SYM)) {
 | 
							} else if (c & (ALT | SYM)) {
 | 
				
			||||||
			beep ();
 | 
								if (c != KEY (RESIZE))
 | 
				
			||||||
 | 
									beep ();
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			g.editor_line.insert (g.editor_cursor, 1, c);
 | 
								g.editor_line.insert (g.editor_cursor, 1, c);
 | 
				
			||||||
			g.editor_cursor++;
 | 
								g.editor_cursor++;
 | 
				
			||||||
@@ -1349,16 +1517,19 @@ fun handle (wint_t c) -> bool {
 | 
				
			|||||||
	auto i = g_normal_actions.find (c);
 | 
						auto i = g_normal_actions.find (c);
 | 
				
			||||||
	switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
 | 
						switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
 | 
				
			||||||
	case ACTION_CHOOSE_FULL:
 | 
						case ACTION_CHOOSE_FULL:
 | 
				
			||||||
		// FIXME: in the root directory, this inserts //item
 | 
							choose (current, true);
 | 
				
			||||||
		g.chosen = g.cwd + "/" + current.filename;
 | 
					 | 
				
			||||||
		g.no_chdir = true;
 | 
					 | 
				
			||||||
		g.quitting = true;
 | 
					 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
	case ACTION_CHOOSE:
 | 
						case ACTION_CHOOSE:
 | 
				
			||||||
		choose (current);
 | 
							choose (current, false);
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case ACTION_ENTER:
 | 
				
			||||||
 | 
							enter (current);
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case ACTION_VIEW_RAW:
 | 
				
			||||||
 | 
							// Mimic mc, it does not seem sensible to page directories
 | 
				
			||||||
 | 
							(is_directory ? change_dir : view_raw) (current.filename);
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
	case ACTION_VIEW:
 | 
						case ACTION_VIEW:
 | 
				
			||||||
		// Mimic mc, it does not seem sensible to page directories
 | 
					 | 
				
			||||||
		(is_directory ? change_dir : view) (current.filename);
 | 
							(is_directory ? change_dir : view) (current.filename);
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
	case ACTION_EDIT:
 | 
						case ACTION_EDIT:
 | 
				
			||||||
@@ -1385,6 +1556,33 @@ fun handle (wint_t c) -> bool {
 | 
				
			|||||||
		resort ();
 | 
							resort ();
 | 
				
			||||||
		break;
 | 
							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:
 | 
						case ACTION_UP:
 | 
				
			||||||
		g.cursor--;
 | 
							g.cursor--;
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
@@ -1421,6 +1619,9 @@ fun handle (wint_t c) -> bool {
 | 
				
			|||||||
	case ACTION_SCROLL_UP:
 | 
						case ACTION_SCROLL_UP:
 | 
				
			||||||
		g.offset--;
 | 
							g.offset--;
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
 | 
						case ACTION_CENTER:
 | 
				
			||||||
 | 
							g.offset = g.cursor - (visible_lines () - 1) / 2;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	case ACTION_CHDIR:
 | 
						case ACTION_CHDIR:
 | 
				
			||||||
		g.editor = L"chdir";
 | 
							g.editor = L"chdir";
 | 
				
			||||||
@@ -1440,10 +1641,15 @@ fun handle (wint_t c) -> bool {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	case ACTION_SEARCH:
 | 
						case ACTION_SEARCH:
 | 
				
			||||||
		g.editor = L"search";
 | 
							g.editor = L"search";
 | 
				
			||||||
		g.editor_on_change                = [] { search_interactive (0); };
 | 
							g.editor_on_change                = [] { match_interactive (0); };
 | 
				
			||||||
		g.editor_on[ACTION_UP]            = [] { search_interactive (-1); };
 | 
							g.editor_on[ACTION_UP]            = [] { match_interactive (-1); };
 | 
				
			||||||
		g.editor_on[ACTION_DOWN]          = [] { search_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;
 | 
							break;
 | 
				
			||||||
	case ACTION_RENAME_PREFILL:
 | 
						case ACTION_RENAME_PREFILL:
 | 
				
			||||||
		g.editor_line = to_wide (current.filename);
 | 
							g.editor_line = to_wide (current.filename);
 | 
				
			||||||
@@ -1495,19 +1701,27 @@ fun handle (wint_t c) -> bool {
 | 
				
			|||||||
	return !g.quitting;
 | 
						return !g.quitting;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun inotify_check () {
 | 
					fun watch_check () {
 | 
				
			||||||
	// Only provide simple indication that contents might have changed
 | 
					 | 
				
			||||||
	char buf[4096]; ssize_t len;
 | 
					 | 
				
			||||||
	bool changed = false;
 | 
						bool changed = false;
 | 
				
			||||||
	while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) {
 | 
						// Only provide simple indication that contents might have changed,
 | 
				
			||||||
 | 
						// if only because kqueue can't do any better
 | 
				
			||||||
 | 
					#ifdef __linux__
 | 
				
			||||||
 | 
						char buf[4096]; ssize_t len;
 | 
				
			||||||
 | 
						while ((len = read (g.watch_fd, buf, sizeof buf)) > 0) {
 | 
				
			||||||
		const inotify_event *e;
 | 
							const inotify_event *e;
 | 
				
			||||||
		for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
 | 
							for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
 | 
				
			||||||
			e = (const inotify_event *) buf;
 | 
								e = (const inotify_event *) buf;
 | 
				
			||||||
			if (e->wd == g.inotify_wd)
 | 
								if (e->wd == g.watch_wd)
 | 
				
			||||||
				changed = g.out_of_date = true;
 | 
									changed = true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (changed)
 | 
					#else
 | 
				
			||||||
 | 
						struct kevent ev {};
 | 
				
			||||||
 | 
						struct timespec timeout {};
 | 
				
			||||||
 | 
						if (kevent (g.watch_fd, nullptr, 0, &ev, 1, &timeout) > 0)
 | 
				
			||||||
 | 
							changed = ev.filter == EVFILT_VNODE && (ev.fflags & NOTE_WRITE);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
						if ((g.out_of_date = changed))
 | 
				
			||||||
		update ();
 | 
							update ();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1749,10 +1963,11 @@ fun load_bindings () {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun load_history_level (const vector<string> &v) {
 | 
					fun load_history_level (const vector<string> &v) {
 | 
				
			||||||
	if (v.size () != 7)
 | 
						if (v.size () < 7)
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	// Not checking the hostname and parent PID right now since we can't merge
 | 
						// 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 () {
 | 
					fun load_config () {
 | 
				
			||||||
@@ -1800,12 +2015,16 @@ fun save_config () {
 | 
				
			|||||||
		*hostname = 0;
 | 
							*hostname = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	auto ppid = std::to_string (getppid ());
 | 
						auto ppid = std::to_string (getppid ());
 | 
				
			||||||
	for (auto i = g.levels.begin (); i != g.levels.end (); i++)
 | 
						for (auto i = g.levels.begin (); i != g.levels.end (); i++) {
 | 
				
			||||||
		write_line (*config, {"history", hostname, ppid, i->path,
 | 
							vector<string> line {"history", hostname, ppid, i->path,
 | 
				
			||||||
			to_string (i->offset), to_string (i->cursor), i->filename});
 | 
								to_string (i->offset), to_string (i->cursor), i->filename};
 | 
				
			||||||
	write_line (*config, {"history", hostname, ppid, g.cwd,
 | 
							line.insert (end (line), begin (i->selection), end (i->selection));
 | 
				
			||||||
		to_string (g.offset), to_string (g.cursor),
 | 
							write_line (*config, line);
 | 
				
			||||||
		at_cursor ().filename});
 | 
						}
 | 
				
			||||||
 | 
						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[]) {
 | 
					int main (int argc, char *argv[]) {
 | 
				
			||||||
@@ -1829,10 +2048,17 @@ int main (int argc, char *argv[]) {
 | 
				
			|||||||
	// So that the neither us nor our children stop on tcsetpgrp()
 | 
						// So that the neither us nor our children stop on tcsetpgrp()
 | 
				
			||||||
	signal (SIGTTOU, SIG_IGN);
 | 
						signal (SIGTTOU, SIG_IGN);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
 | 
					#ifdef __linux__
 | 
				
			||||||
 | 
						if ((g.watch_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
 | 
				
			||||||
		cerr << "cannot initialize inotify" << endl;
 | 
							cerr << "cannot initialize inotify" << endl;
 | 
				
			||||||
		return 1;
 | 
							return 1;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
						if ((g.watch_fd = kqueue ()) < 0) {
 | 
				
			||||||
 | 
							cerr << "cannot initialize kqueue" << endl;
 | 
				
			||||||
 | 
							return 1;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	locale::global (locale (""));
 | 
						locale::global (locale (""));
 | 
				
			||||||
	load_bindings ();
 | 
						load_bindings ();
 | 
				
			||||||
@@ -1869,7 +2095,7 @@ int main (int argc, char *argv[]) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	wint_t c;
 | 
						wint_t c;
 | 
				
			||||||
	while (!read_key (c) || handle (c)) {
 | 
						while (!read_key (c) || handle (c)) {
 | 
				
			||||||
		inotify_check ();
 | 
							watch_check ();
 | 
				
			||||||
		if (g.sort_flash_ttl && !--g.sort_flash_ttl)
 | 
							if (g.sort_flash_ttl && !--g.sort_flash_ttl)
 | 
				
			||||||
			update ();
 | 
								update ();
 | 
				
			||||||
		if (g.message_ttl && !--g.message_ttl) {
 | 
							if (g.message_ttl && !--g.message_ttl) {
 | 
				
			||||||
@@ -1881,8 +2107,12 @@ int main (int argc, char *argv[]) {
 | 
				
			|||||||
	save_config ();
 | 
						save_config ();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Presumably it is going to end up as an argument, so quote it
 | 
						// Presumably it is going to end up as an argument, so quote it
 | 
				
			||||||
	if (!g.chosen.empty ())
 | 
						string chosen;
 | 
				
			||||||
		g.chosen = shell_escape (g.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
 | 
						// We can't portably create a standard stream from an FD, so modify the FD
 | 
				
			||||||
	dup2 (output_fd, STDOUT_FILENO);
 | 
						dup2 (output_fd, STDOUT_FILENO);
 | 
				
			||||||
@@ -1893,7 +2123,7 @@ int main (int argc, char *argv[]) {
 | 
				
			|||||||
	else
 | 
						else
 | 
				
			||||||
		cout << "local cd=" << endl;
 | 
							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;
 | 
						cout << "local helper=" << shell_escape (g.ext_helper) << endl;
 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user