Compare commits
	
		
			59 Commits
		
	
	
		
			77973fc026
			...
			v1.0.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						272ee62ad8
	
				 | 
					
					
						|||
| 
						
						
							
						
						a85426541a
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9b003735d
	
				 | 
					
					
						|||
| 
						
						
							
						
						52a28f01c8
	
				 | 
					
					
						|||
| 
						
						
							
						
						3607757554
	
				 | 
					
					
						|||
| 
						
						
							
						
						6eb216a40a
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ce6f47716
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9662f1a7b
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ddeb03652
	
				 | 
					
					
						|||
| 
						
						
							
						
						acb187c6b1
	
				 | 
					
					
						|||
| 
						
						
							
						
						9427df62e7
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d6999c415
	
				 | 
					
					
						|||
| 
						
						
							
						
						30ed61fdd2
	
				 | 
					
					
						|||
| 
						
						
							
						
						2df916c9b3
	
				 | 
					
					
						|||
| 
						
						
							
						
						24401825b4
	
				 | 
					
					
						|||
| 
						
						
							
						
						2bfb490798
	
				 | 
					
					
						|||
| 
						
						
							
						
						338d00d605
	
				 | 
					
					
						|||
| 
						
						
							
						
						015652e379
	
				 | 
					
					
						|||
| 
						
						
							
						
						c298b6fc97
	
				 | 
					
					
						|||
| 
						
						
							
						
						7c2ab8ab59
	
				 | 
					
					
						|||
| 
						
						
							
						
						e423a3a1b1
	
				 | 
					
					
						|||
| 
						
						
							
						
						916f354c9b
	
				 | 
					
					
						|||
| 
						
						
							
						
						050f875c47
	
				 | 
					
					
						|||
| 
						
						
							
						
						aeffe40efc
	
				 | 
					
					
						|||
| 
						
						
							
						
						536aa57761
	
				 | 
					
					
						|||
| 
						
						
							
						
						0d10ae06e6
	
				 | 
					
					
						|||
| 
						
						
							
						
						e1b0831854
	
				 | 
					
					
						|||
| 
						
						
							
						
						4e93dfbb8d
	
				 | 
					
					
						|||
| 
						
						
							
						
						8a6bb54eb5
	
				 | 
					
					
						|||
| 
						
						
							
						
						4ef7c9edf7
	
				 | 
					
					
						|||
| 
						
						
							
						
						3eea106c3c
	
				 | 
					
					
						|||
| 
						
						
							
						
						7de8c84e8f
	
				 | 
					
					
						|||
| 
						
						
							
						
						e17c5e2083
	
				 | 
					
					
						|||
| 
						
						
							
						
						9bd3739122
	
				 | 
					
					
						|||
| 
						
						
							
						
						ec1f1031cc
	
				 | 
					
					
						|||
| 
						
						
							
						
						bc99b3dd48
	
				 | 
					
					
						|||
| 
						
						
							
						
						e948741864
	
				 | 
					
					
						|||
| 
						
						
							
						
						0adbac2066
	
				 | 
					
					
						|||
| 
						
						
							
						
						2238db5a4e
	
				 | 
					
					
						|||
| 
						
						
							
						
						98612f5492
	
				 | 
					
					
						|||
| 
						
						
							
						
						1034321f81
	
				 | 
					
					
						|||
| 
						
						
							
						
						e7da32160c
	
				 | 
					
					
						|||
| 
						
						
							
						
						fdb338fe12
	
				 | 
					
					
						|||
| 
						
						
							
						
						9056ef4194
	
				 | 
					
					
						|||
| 
						
						
							
						
						b8a4742fb9
	
				 | 
					
					
						|||
| 
						
						
							
						
						c999e5a8e4
	
				 | 
					
					
						|||
| 
						
						
							
						
						07ef834a1e
	
				 | 
					
					
						|||
| 
						
						
							
						
						997f5c25a2
	
				 | 
					
					
						|||
| 
						
						
							
						
						39e68a977c
	
				 | 
					
					
						|||
| 
						
						
							
						
						c20d3780b2
	
				 | 
					
					
						|||
| 
						
						
							
						
						22725ba3b7
	
				 | 
					
					
						|||
| 
						
						
							
						
						df046bb071
	
				 | 
					
					
						|||
| 
						
						
							
						
						0c1a8d9902
	
				 | 
					
					
						|||
| 
						
						
							
						
						cc59fcfb41
	
				 | 
					
					
						|||
| 
						
						
							
						
						c88566e7bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						39c840cd74
	
				 | 
					
					
						|||
| 
						
						
							
						
						f231828e8d
	
				 | 
					
					
						|||
| 
						
						
							
						
						1318c4983f
	
				 | 
					
					
						|||
| 
						
						
							
						
						c503954f44
	
				 | 
					
					
						
							
								
								
									
										14
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
BasedOnStyle: LLVM
 | 
			
		||||
ColumnLimit: 80
 | 
			
		||||
IndentWidth: 4
 | 
			
		||||
TabWidth: 4
 | 
			
		||||
UseTab: ForContinuationAndIndentation
 | 
			
		||||
SpaceAfterCStyleCast: true
 | 
			
		||||
SpaceBeforeParens: Always
 | 
			
		||||
AlignAfterOpenBracket: DontAlign
 | 
			
		||||
AlignEscapedNewlines: DontAlign
 | 
			
		||||
AlignOperands: DontAlign
 | 
			
		||||
AlignConsecutiveMacros: Consecutive
 | 
			
		||||
BreakBeforeTernaryOperators: true
 | 
			
		||||
SpacesBeforeTrailingComments: 2
 | 
			
		||||
WhitespaceSensitiveMacros: ['XX', 'ACTIONS', 'LS']
 | 
			
		||||
@@ -1,47 +1,45 @@
 | 
			
		||||
# target_compile_features has been introduced in that version
 | 
			
		||||
cmake_minimum_required (VERSION 3.1.0)
 | 
			
		||||
 | 
			
		||||
project (sdn CXX)
 | 
			
		||||
set (version 0.1)
 | 
			
		||||
cmake_minimum_required (VERSION 3.1...3.27)
 | 
			
		||||
project (sdn VERSION 1.0 LANGUAGES CXX)
 | 
			
		||||
 | 
			
		||||
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
 | 
			
		||||
	set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
# Since we use a language with slow compilers, let's at least use a fast linker
 | 
			
		||||
execute_process (COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=gold -Wl,--version
 | 
			
		||||
	ERROR_QUIET OUTPUT_VARIABLE ld_version)
 | 
			
		||||
if ("${ld_version}" MATCHES "GNU gold")
 | 
			
		||||
	set (CMAKE_EXE_LINKER_FLAGS "-fuse-ld=gold ${CMAKE_EXE_LINKER_FLAGS}")
 | 
			
		||||
	set (CMAKE_CXX_FLAGS
 | 
			
		||||
		"${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-misleading-indentation -pedantic")
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
find_package (PkgConfig REQUIRED)
 | 
			
		||||
pkg_check_modules (NCURSESW QUIET ncursesw)
 | 
			
		||||
pkg_check_modules (ACL libacl)
 | 
			
		||||
pkg_check_modules (NCURSESW ncursesw)
 | 
			
		||||
if (NOT NCURSESW_FOUND)
 | 
			
		||||
	find_library (NCURSESW_LIBRARIES NAMES ncursesw)
 | 
			
		||||
	find_path (NCURSESW_INCLUDE_DIRS ncurses.h PATH_SUFFIXES ncurses)
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.cpp)
 | 
			
		||||
target_include_directories (${PROJECT_NAME} PUBLIC ${NCURSESW_INCLUDE_DIRS})
 | 
			
		||||
target_link_libraries (${PROJECT_NAME} PUBLIC ${NCURSESW_LIBRARIES} acl)
 | 
			
		||||
target_link_libraries (${PROJECT_NAME}
 | 
			
		||||
	PUBLIC ${NCURSESW_LIBRARIES} ${ACL_LIBRARIES})
 | 
			
		||||
target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14)
 | 
			
		||||
target_compile_definitions (${PROJECT_NAME} PUBLIC
 | 
			
		||||
	-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${version}\")
 | 
			
		||||
	-DPROJECT_NAME=\"${PROJECT_NAME}\" -DPROJECT_VERSION=\"${PROJECT_VERSION}\")
 | 
			
		||||
 | 
			
		||||
include (GNUInstallDirs)
 | 
			
		||||
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
			
		||||
install (PROGRAMS ${PROJECT_NAME}-install DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
			
		||||
install (FILES sdn.1 sdn-install.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
 | 
			
		||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
 | 
			
		||||
 | 
			
		||||
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
 | 
			
		||||
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
 | 
			
		||||
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
 | 
			
		||||
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
 | 
			
		||||
set (CPACK_PACKAGE_VERSION ${version})
 | 
			
		||||
set (CPACK_GENERATOR "TGZ;ZIP")
 | 
			
		||||
set (CPACK_PACKAGE_FILE_NAME
 | 
			
		||||
	"${PROJECT_NAME}-${version}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
 | 
			
		||||
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${version}")
 | 
			
		||||
	"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
 | 
			
		||||
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
 | 
			
		||||
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
 | 
			
		||||
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
 | 
			
		||||
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${version}")
 | 
			
		||||
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
 | 
			
		||||
 | 
			
		||||
set (CPACK_SET_DESTDIR TRUE)
 | 
			
		||||
include (CPack)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
Copyright (c) 2017 - 2020, 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
 | 
			
		||||
purpose with or without fee is hereby granted.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								Makefile
									
									
									
									
									
								
							@@ -1,12 +1,19 @@
 | 
			
		||||
.POSIX:
 | 
			
		||||
SHELL = /bin/sh
 | 
			
		||||
CXXFLAGS = -g -std=c++14 -Wall -Wextra -pedantic -static-libstdc++
 | 
			
		||||
CXXFLAGS = -g -std=c++14 -Wall -Wextra -Wno-misleading-indentation -pedantic
 | 
			
		||||
CPPFLAGS = `sed -ne '/^project (\([^ )]*\) VERSION \([^ )]*\).*/ \
 | 
			
		||||
	s//-DPROJECT_NAME="\1" -DPROJECT_VERSION="\2"/p' CMakeLists.txt`
 | 
			
		||||
 | 
			
		||||
all: sdn
 | 
			
		||||
%: %.cpp CMakeLists.txt
 | 
			
		||||
	$(CXX) $(CXXFLAGS) $< -o $@ `pkg-config --libs --cflags ncursesw` -lacl \
 | 
			
		||||
	`sed -ne 's/^project (\([^ )]*\).*/-DPROJECT_NAME="\1"/p' \
 | 
			
		||||
	-e 's/^set (version \([^ )]*\).*/-DPROJECT_VERSION="\1"/p' CMakeLists.txt`
 | 
			
		||||
sdn: sdn.cpp CMakeLists.txt
 | 
			
		||||
	$(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
 | 
			
		||||
	-lacl `pkg-config --libs --cflags ncursesw`
 | 
			
		||||
sdn-static: sdn.cpp CMakeLists.txt
 | 
			
		||||
	$(CXX) $(CXXFLAGS) $(CPPFLAGS) $< -o $@ \
 | 
			
		||||
	-static-libstdc++ \
 | 
			
		||||
	-Wl,--start-group,-Bstatic \
 | 
			
		||||
	-lacl `pkg-config --static --libs --cflags ncursesw` \
 | 
			
		||||
	-Wl,--end-group,-Bdynamic
 | 
			
		||||
clean:
 | 
			
		||||
	rm -f sdn
 | 
			
		||||
	rm -f sdn sdn-static
 | 
			
		||||
 | 
			
		||||
.PHONY: all clean
 | 
			
		||||
.PHONY: clean
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								README.adoc
									
									
									
									
									
								
							@@ -11,21 +11,26 @@ commands.  It enables you to:
 | 
			
		||||
   can be simply forwarded if it is to be edited.  What's more, it will always
 | 
			
		||||
   be obvious whether the navigator is running.
 | 
			
		||||
 | 
			
		||||
The only supported platform is Linux.  I wanted to try a different, simpler
 | 
			
		||||
approach here, and the end result is very friendly to tinkering.
 | 
			
		||||
'sdn' runs on Linux and all BSD derivatives.  I wanted to try a different,
 | 
			
		||||
simpler approach here, and the end result is very friendly to tinkering.
 | 
			
		||||
 | 
			
		||||
image::sdn.png[align="center"]
 | 
			
		||||
 | 
			
		||||
Packages
 | 
			
		||||
--------
 | 
			
		||||
Regular releases are sporadic.  git master should be stable enough.
 | 
			
		||||
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
 | 
			
		||||
--------
 | 
			
		||||
Build dependencies: CMake and/or make, a C++14 compiler, pkg-config +
 | 
			
		||||
Runtime dependencies: ncursesw, libacl
 | 
			
		||||
Runtime dependencies: ncursesw, libacl (on Linux)
 | 
			
		||||
 | 
			
		||||
// Working around libasciidoc's missing support for escaping it like \++
 | 
			
		||||
:doubleplus: ++
 | 
			
		||||
 | 
			
		||||
Unfortunately most LLVM libc++ versions have a bug that crashes 'sdn' on start.
 | 
			
		||||
Use GNU libstdc{doubleplus} if you're affected.
 | 
			
		||||
Unfortunately most LLVM libc{plus}{plus} versions have a bug that crashes 'sdn'
 | 
			
		||||
on start.  Use GNU libstdc{plus}{plus} if you're affected.
 | 
			
		||||
 | 
			
		||||
 $ git clone https://git.janouch.name/p/sdn.git
 | 
			
		||||
 $ mkdir sdn/build
 | 
			
		||||
@@ -45,8 +50,12 @@ Or you can try telling CMake to make a package for you.  For Debian it is:
 | 
			
		||||
There is also a Makefile you can use to quickly build a binary to be copied
 | 
			
		||||
into the PATH of any machine you want to have 'sdn' on.
 | 
			
		||||
 | 
			
		||||
Configuration
 | 
			
		||||
-------------
 | 
			
		||||
For a slightly more technical explanation please refer to manual pages.
 | 
			
		||||
 | 
			
		||||
Integration
 | 
			
		||||
-----------
 | 
			
		||||
~~~~~~~~~~~
 | 
			
		||||
The package contains an installation script called 'sdn-install' which will bind
 | 
			
		||||
'sdn' to Alt-o in your shell's initialisation file.  The supported shells are:
 | 
			
		||||
 | 
			
		||||
@@ -54,14 +63,11 @@ The package contains an installation script called 'sdn-install' which will bind
 | 
			
		||||
 - *bash*: minor issue: exiting the navigator confirms an empty prompt
 | 
			
		||||
 - *fish*: works well
 | 
			
		||||
 - *elvish*: version 0.14.1 and above, an unstable API is used, works well
 | 
			
		||||
 | 
			
		||||
+
 | 
			
		||||
elvish is absolutely perverse.  And so is integrating 'sdn' into it because it
 | 
			
		||||
already includes a custom file manager, bound to Ctrl-N (though I find the
 | 
			
		||||
ranger-like interface confusing and resource-demanding).
 | 
			
		||||
 | 
			
		||||
Configuration
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Colours
 | 
			
		||||
~~~~~~~
 | 
			
		||||
Here is an example of a '~/.config/sdn/look' file; the format is similar to
 | 
			
		||||
@@ -86,7 +92,6 @@ To obtain more vifm-like controls, you may write the following to your
 | 
			
		||||
....
 | 
			
		||||
normal h parent
 | 
			
		||||
normal l choose
 | 
			
		||||
normal ? help
 | 
			
		||||
....
 | 
			
		||||
 | 
			
		||||
Helper programs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								sdn-install
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								sdn-install
									
									
									
									
									
								
							@@ -16,6 +16,9 @@ sdn-navigate () {
 | 
			
		||||
    # helpers after the terminal has been resized while running sdn
 | 
			
		||||
    command true
 | 
			
		||||
 | 
			
		||||
    # Add to history, see https://www.zsh.org/mla/workers/2020/msg00633.html
 | 
			
		||||
    fc -R =(print -- "$helper")
 | 
			
		||||
 | 
			
		||||
    /bin/sh -c "$helper" </dev/tty || break
 | 
			
		||||
  done
 | 
			
		||||
  # optionally: zle zle-line-init
 | 
			
		||||
@@ -51,6 +54,7 @@ sdn-navigate () {
 | 
			
		||||
      ((SDN_P=SDN_P+${#insert}+1))
 | 
			
		||||
    }
 | 
			
		||||
    [[ -z $helper ]] && break
 | 
			
		||||
    history -s -- "$helper"
 | 
			
		||||
    /bin/sh -c "$helper" || break
 | 
			
		||||
  done
 | 
			
		||||
}
 | 
			
		||||
@@ -60,9 +64,10 @@ sdn-restore () {
 | 
			
		||||
  unset SDN_L SDN_P
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bind -x '"\200": sdn-navigate'
 | 
			
		||||
bind -x '"\201": sdn-restore'
 | 
			
		||||
bind '"\eo":"\200\C-m\201"'
 | 
			
		||||
# These never occur in UTF-8: \300-\301 \365-\367 \370-\377
 | 
			
		||||
bind -x '"\300": sdn-navigate'
 | 
			
		||||
bind -x '"\301": sdn-restore'
 | 
			
		||||
bind '"\eo": "\300\C-m\301"'
 | 
			
		||||
EOF
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -93,6 +98,7 @@ edit:insert:binding[Alt-o] = {
 | 
			
		||||
  local:posix = [cmd]{ /bin/sh -c $cmd </dev/tty >/dev/tty 2>&1 }
 | 
			
		||||
 | 
			
		||||
  # XXX: the -dot is not a stable API, and may hence break soon
 | 
			
		||||
  # https://elv.sh/ref/builtin.html#do-not-use-functions-and-variables
 | 
			
		||||
  local:buffer = $edit:current-command
 | 
			
		||||
  local:cursor = (str:to-codepoints $buffer[0..$edit:-dot] | count)
 | 
			
		||||
  local:ns = (ns [&])
 | 
			
		||||
@@ -119,7 +125,7 @@ done
 | 
			
		||||
 | 
			
		||||
# Figure out the shell to integrate with
 | 
			
		||||
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" ]
 | 
			
		||||
then
 | 
			
		||||
  if [ "$login" != "$actual" ]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								sdn-install.1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								sdn-install.1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
.Dd October 27, 2020
 | 
			
		||||
.Dt SDN-INSTALL 1
 | 
			
		||||
.Os Linux
 | 
			
		||||
.Sh NAME
 | 
			
		||||
.Nm sdn-install
 | 
			
		||||
.Nd integrate sdn with the shell
 | 
			
		||||
.Sh SYNOPSIS
 | 
			
		||||
.Nm sdn-install
 | 
			
		||||
.Op Fl s Ar shell
 | 
			
		||||
.Op Fl p Ar - | rcpath
 | 
			
		||||
.Sh DESCRIPTION
 | 
			
		||||
.Nm
 | 
			
		||||
integrates
 | 
			
		||||
.Xr sdn 1
 | 
			
		||||
with your shell, binding it to M-o.  If the navigator has already been
 | 
			
		||||
integrated, it updates the snippet in-place.
 | 
			
		||||
.Pp
 | 
			
		||||
The options are as follows:
 | 
			
		||||
.Bl -tag -width Ds
 | 
			
		||||
.It Fl p Ar -
 | 
			
		||||
Merely print the integration snippet for the appropriate shell to the standard
 | 
			
		||||
output, not changing anything.
 | 
			
		||||
.It Fl p Ar rcpath
 | 
			
		||||
Install the integration snippet into a different shell initialization file than
 | 
			
		||||
the default one for your user.
 | 
			
		||||
.It Fl s Ar shell
 | 
			
		||||
If you want to integrate
 | 
			
		||||
.Xr sdn 1
 | 
			
		||||
with a different shell than the one you're running, use this option to specify
 | 
			
		||||
it.
 | 
			
		||||
.El
 | 
			
		||||
.Sh REPORTING BUGS
 | 
			
		||||
Use
 | 
			
		||||
.Lk https://git.janouch.name/p/sdn
 | 
			
		||||
to report bugs, request features, or submit pull requests.
 | 
			
		||||
							
								
								
									
										131
									
								
								sdn.1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								sdn.1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
 | 
			
		||||
.Dd October 27, 2020
 | 
			
		||||
.Dt SDN 1
 | 
			
		||||
.Os Linux
 | 
			
		||||
.Sh NAME
 | 
			
		||||
.Nm sdn
 | 
			
		||||
.Nd directory navigator
 | 
			
		||||
.Sh SYNOPSIS
 | 
			
		||||
.Nm sdn
 | 
			
		||||
.Op Ar line Ar point
 | 
			
		||||
.Nm sdn
 | 
			
		||||
.Cm --version
 | 
			
		||||
.Sh DESCRIPTION
 | 
			
		||||
.Nm
 | 
			
		||||
is a simple directory navigator that you can launch while editing shell
 | 
			
		||||
commands.
 | 
			
		||||
Use the
 | 
			
		||||
.Xr sdn-install 1
 | 
			
		||||
script to integrate it with your shell, then invoke it at any time with M-o.
 | 
			
		||||
.Pp
 | 
			
		||||
Press F1 to get a list of active key bindings and their assigned actions,
 | 
			
		||||
grouped by their contexts.
 | 
			
		||||
.Pp
 | 
			
		||||
Program arguments are only used by integration snippets to forward the parent
 | 
			
		||||
shell's command line.
 | 
			
		||||
The
 | 
			
		||||
.Ar point
 | 
			
		||||
is given in terms of characters.
 | 
			
		||||
.Sh OPTIONS
 | 
			
		||||
While some behaviour can be toggled from within the program, some can only be
 | 
			
		||||
changed by modifying configuration files manually.
 | 
			
		||||
.Pp
 | 
			
		||||
The files follow a simple syntax derived from the Bourne shell: each option is
 | 
			
		||||
on its own line, with words separated by linear whitespace.
 | 
			
		||||
Comments start with a hash (#) and continue until the end of the line.
 | 
			
		||||
All special characters may be quoted using either a backslash or single-quoted
 | 
			
		||||
strings.
 | 
			
		||||
.Pp
 | 
			
		||||
The options and the default key bindings controlling them are as follows:
 | 
			
		||||
.Bl -tag
 | 
			
		||||
.It full-view Em bool No (t)
 | 
			
		||||
If non-zero, the equivalent format to
 | 
			
		||||
.Ql ls -l
 | 
			
		||||
is used to display directory contents rather than simply listing the filenames.
 | 
			
		||||
.It gravity Em bool
 | 
			
		||||
If non-zero, all entries stick to the bottom of the screen, i.e., all empty
 | 
			
		||||
space is at the top.
 | 
			
		||||
.It reverse-sort Em bool No (R)
 | 
			
		||||
If non-zero, the order of entries is reversed.
 | 
			
		||||
.It show-hidden Em bool No (M-.)
 | 
			
		||||
If non-zero, filenames beginning with a full stop are shown.
 | 
			
		||||
.It ext-helpers Em bool
 | 
			
		||||
If non-zero, viewers and editors are launched from the parent shell.
 | 
			
		||||
This way you can suspend them and use job control features of the shell.
 | 
			
		||||
However it also enforces any pending change to the shell's working directory.
 | 
			
		||||
.It sort-column Em number No (< >)
 | 
			
		||||
The zero-based index of the
 | 
			
		||||
.Ql full-view
 | 
			
		||||
column that entries are ordered by.
 | 
			
		||||
.El
 | 
			
		||||
.Sh ENVIRONMENT
 | 
			
		||||
.Bl -tag -width 15n
 | 
			
		||||
.It Ev LS_COLORS
 | 
			
		||||
Used to retrieve filename colours.
 | 
			
		||||
The format is described in
 | 
			
		||||
.Xr dir_colors 5
 | 
			
		||||
and you can use the
 | 
			
		||||
.Xr dircolors 1
 | 
			
		||||
utility to initialize this variable.
 | 
			
		||||
.It Ev PAGER
 | 
			
		||||
The viewer program to be launched by the F3 key binding as well as to show
 | 
			
		||||
the internal help message.
 | 
			
		||||
If none is set, it defaults to
 | 
			
		||||
.Xr less 1 .
 | 
			
		||||
.It Ev VISUAL , Ev EDITOR
 | 
			
		||||
The editor program to be launched by the F4 key binding.
 | 
			
		||||
If neither variable is set, it defaults to
 | 
			
		||||
.Xr vi 1 .
 | 
			
		||||
.El
 | 
			
		||||
.Sh FILES
 | 
			
		||||
.Bl -tag -width 25n -compact
 | 
			
		||||
.It Pa ~/.config/sdn/config
 | 
			
		||||
Program configuration and navigation state, initialized or overwritten on exit.
 | 
			
		||||
.It Pa ~/.config/sdn/bindings
 | 
			
		||||
Custom key binding overrides.
 | 
			
		||||
.It Pa ~/.config/sdn/look
 | 
			
		||||
Redefine terminal attributes for UI elements.
 | 
			
		||||
.El
 | 
			
		||||
.Sh EXAMPLES
 | 
			
		||||
.Ss Pa bindings
 | 
			
		||||
Key names or combinations follow the Emacs syntax for Control and Meta prefixes
 | 
			
		||||
and
 | 
			
		||||
.Xr terminfo 5
 | 
			
		||||
names are used for special keys.
 | 
			
		||||
To obtain more vifm-like controls and Windows-like quit abilities:
 | 
			
		||||
.Bd -literal -offset indent
 | 
			
		||||
normal h parent
 | 
			
		||||
normal l choose
 | 
			
		||||
normal M-f4 quit
 | 
			
		||||
.Ed
 | 
			
		||||
.Pp
 | 
			
		||||
Midnight Commander binds the same traversal actions to sequences normally
 | 
			
		||||
unknown to ncurses, due to them being missing from terminfo.
 | 
			
		||||
You'll need to define them manually to match your terminal.
 | 
			
		||||
For rxvt, that would be:
 | 
			
		||||
.Bd -literal -offset indent
 | 
			
		||||
define C-ppage ^[[5^
 | 
			
		||||
define C-npage ^[[6^
 | 
			
		||||
normal C-ppage parent
 | 
			
		||||
normal C-npage choose
 | 
			
		||||
.Ed
 | 
			
		||||
.Pp
 | 
			
		||||
Escape characters must be inserted verbatim, e.g., by pressing C-v ESC in vi,
 | 
			
		||||
or C-q ESC in Emacs.
 | 
			
		||||
.Ss Pa look
 | 
			
		||||
Terminal attributes are accepted in a format similar to that of
 | 
			
		||||
.Xr git-config 1 ,
 | 
			
		||||
only named colours aren't supported.
 | 
			
		||||
For a black-on-white terminal supporting 256 colours, a theme such as the
 | 
			
		||||
following may work:
 | 
			
		||||
.Bd -literal -offset indent
 | 
			
		||||
cursor 231 202
 | 
			
		||||
bar 16 255 ul
 | 
			
		||||
cwd bold
 | 
			
		||||
input
 | 
			
		||||
cmdline 145
 | 
			
		||||
.Ed
 | 
			
		||||
.Sh REPORTING BUGS
 | 
			
		||||
Use
 | 
			
		||||
.Lk https://git.janouch.name/p/sdn
 | 
			
		||||
to report bugs, request features, or submit pull requests.
 | 
			
		||||
							
								
								
									
										528
									
								
								sdn.cpp
									
									
									
									
									
								
							
							
						
						
									
										528
									
								
								sdn.cpp
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
//
 | 
			
		||||
// sdn: simple directory navigator
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2017 - 2020, 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
 | 
			
		||||
// purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -18,41 +18,53 @@
 | 
			
		||||
// May be required for ncursesw and we generally want it all anyway
 | 
			
		||||
#define _XOPEN_SOURCE_EXTENDED
 | 
			
		||||
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <locale>
 | 
			
		||||
#include <iostream>
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <cwchar>
 | 
			
		||||
#include <climits>
 | 
			
		||||
#include <cstdlib>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <cwchar>
 | 
			
		||||
#include <fstream>
 | 
			
		||||
#include <iostream>
 | 
			
		||||
#include <locale>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <tuple>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <tuple>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
#include <dirent.h>
 | 
			
		||||
#include <sys/stat.h>
 | 
			
		||||
#include <sys/types.h>
 | 
			
		||||
#include <sys/acl.h>
 | 
			
		||||
#include <fcntl.h>
 | 
			
		||||
#include <pwd.h>
 | 
			
		||||
#include <fnmatch.h>
 | 
			
		||||
#include <grp.h>
 | 
			
		||||
#include <libgen.h>
 | 
			
		||||
#include <time.h>
 | 
			
		||||
 | 
			
		||||
#include <sys/inotify.h>
 | 
			
		||||
#include <sys/xattr.h>
 | 
			
		||||
#include <pwd.h>
 | 
			
		||||
#include <signal.h>
 | 
			
		||||
#include <sys/stat.h>
 | 
			
		||||
#include <sys/types.h>
 | 
			
		||||
#include <sys/wait.h>
 | 
			
		||||
#include <time.h>
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
 | 
			
		||||
#ifdef __linux__
 | 
			
		||||
#include <sys/inotify.h>
 | 
			
		||||
// ACL information is not important enough to be ported
 | 
			
		||||
#include <acl/libacl.h>
 | 
			
		||||
#include <sys/acl.h>
 | 
			
		||||
#include <sys/xattr.h>
 | 
			
		||||
#else
 | 
			
		||||
#include <sys/event.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include <ncurses.h>
 | 
			
		||||
 | 
			
		||||
// Unicode is complex enough already and we might make assumptions
 | 
			
		||||
// To implement cbreak() with disabled ^S that gets reënabled on endwin()
 | 
			
		||||
#define NCURSES_INTERNALS
 | 
			
		||||
#include <term.h>
 | 
			
		||||
#undef CTRL  // term.h -> termios.h -> sys/ttydefaults.h, too simplistic
 | 
			
		||||
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
// Trailing return types make C++ syntax suck considerably less
 | 
			
		||||
@@ -92,8 +104,8 @@ fun to_mb (const wstring &wide) -> string {
 | 
			
		||||
	return mb;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun prefix_length (const wstring &in, const wstring &of) -> int {
 | 
			
		||||
	int score = 0;
 | 
			
		||||
fun prefix_length (const wstring &in, const wstring &of) -> size_t {
 | 
			
		||||
	size_t score = 0;
 | 
			
		||||
	for (size_t i = 0; i < of.size () && in.size () >= i && in[i] == of[i]; i++)
 | 
			
		||||
		score++;
 | 
			
		||||
	return score;
 | 
			
		||||
@@ -158,9 +170,9 @@ fun shell_escape (const string &v) -> string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun parse_line (istream &is, vector<string> &out) -> bool {
 | 
			
		||||
	enum {STA, DEF, COM, ESC, WOR, QUO, STATES};
 | 
			
		||||
	enum {TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6};
 | 
			
		||||
	enum {TWOR = TAKE | WOR};
 | 
			
		||||
	enum { STA, DEF, COM, ESC, WOR, QUO, STATES };
 | 
			
		||||
	enum { TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6 };
 | 
			
		||||
	enum { TWOR = TAKE | WOR };
 | 
			
		||||
 | 
			
		||||
	// We never transition back to the start state, so it can stay as a no-op
 | 
			
		||||
	static char table[STATES][7] = {
 | 
			
		||||
@@ -245,7 +257,7 @@ fun capitalize (const string &s) -> string {
 | 
			
		||||
 | 
			
		||||
/// Underlining for teletypes (also called overstriking),
 | 
			
		||||
/// also imitated in more(1) and less(1)
 | 
			
		||||
fun underline (const string& s) -> string {
 | 
			
		||||
fun underline (const string &s) -> string {
 | 
			
		||||
	string result;
 | 
			
		||||
	for (auto c : s)
 | 
			
		||||
		result.append ({c, 8, '_'});
 | 
			
		||||
@@ -267,7 +279,7 @@ fun xdg_config_home () -> string {
 | 
			
		||||
fun xdg_config_find (const string &suffix) -> unique_ptr<ifstream> {
 | 
			
		||||
	vector<string> dirs {xdg_config_home ()};
 | 
			
		||||
	const char *system_dirs = getenv ("XDG_CONFIG_DIRS");
 | 
			
		||||
	split (system_dirs ? system_dirs : "/etc/xdg", ":", dirs);
 | 
			
		||||
	split ((system_dirs && *system_dirs) ? system_dirs : "/etc/xdg", ":", dirs);
 | 
			
		||||
	for (const auto &dir : dirs) {
 | 
			
		||||
		if (dir[0] != '/')
 | 
			
		||||
			continue;
 | 
			
		||||
@@ -296,7 +308,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 {
 | 
			
		||||
	cchar_t ch {}; wchar_t ws[] = {c, 0};
 | 
			
		||||
@@ -317,9 +343,9 @@ fun invert (cchar_t &ch) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun apply_attrs (const wstring &w, attr_t attrs) -> ncstring {
 | 
			
		||||
	ncstring res;
 | 
			
		||||
	for (auto c : w)
 | 
			
		||||
		res += cchar (attrs, c);
 | 
			
		||||
	ncstring res (w.size (), cchar_t {});
 | 
			
		||||
	for (size_t i = 0; i < w.size (); i++)
 | 
			
		||||
		res[i] = cchar (attrs, w[i]);
 | 
			
		||||
	return res;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -353,13 +379,6 @@ fun print (const ncstring &nc, int limit) -> int {
 | 
			
		||||
	return total_width;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun compute_width (const wstring &w) -> int {
 | 
			
		||||
	int total = 0;
 | 
			
		||||
	for (const auto &c : w)
 | 
			
		||||
		total += wcwidth (c);
 | 
			
		||||
	return total;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun compute_width (const ncstring &nc) -> int {
 | 
			
		||||
	int total = 0;
 | 
			
		||||
	for (const auto &c : nc)
 | 
			
		||||
@@ -406,17 +425,18 @@ fun decode_attrs (const vector<string> &attrs) -> chtype {
 | 
			
		||||
 | 
			
		||||
enum { ALT = 1 << 24, SYM = 1 << 25 };  // Outside the range of Unicode
 | 
			
		||||
#define KEY(name) (SYM | KEY_ ## name)
 | 
			
		||||
#define CTRL(char) ((char - 64) & 0x7f)  // 60..7f aren't translated correctly
 | 
			
		||||
#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
 | 
			
		||||
 | 
			
		||||
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
 | 
			
		||||
	XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \
 | 
			
		||||
	XX(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(SEARCH) XX(RENAME) XX(RENAME_PREFILL) \
 | 
			
		||||
	XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
 | 
			
		||||
	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_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)
 | 
			
		||||
 | 
			
		||||
#define XX(name) ACTION_ ## name,
 | 
			
		||||
@@ -430,7 +450,8 @@ static const char *g_action_names[] = {ACTIONS(XX)};
 | 
			
		||||
static map<wint_t, action> g_normal_actions {
 | 
			
		||||
	{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
 | 
			
		||||
	{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
 | 
			
		||||
	{KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT}, {'h', ACTION_HELP},
 | 
			
		||||
	{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
 | 
			
		||||
	{KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT},
 | 
			
		||||
	{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
 | 
			
		||||
	// M-o ought to be the same shortcut the navigator is launched with
 | 
			
		||||
	{ALT | 'o', ACTION_QUIT},
 | 
			
		||||
@@ -442,10 +463,12 @@ static map<wint_t, action> g_normal_actions {
 | 
			
		||||
	{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
 | 
			
		||||
	{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
 | 
			
		||||
	{CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
 | 
			
		||||
	{'z', ACTION_CENTER},
 | 
			
		||||
	{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
 | 
			
		||||
	{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
 | 
			
		||||
	{'/', ACTION_SEARCH},  {'s', ACTION_SEARCH},
 | 
			
		||||
	{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
 | 
			
		||||
	{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
 | 
			
		||||
	{KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR},
 | 
			
		||||
	{'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL},
 | 
			
		||||
	{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
 | 
			
		||||
	{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
 | 
			
		||||
@@ -456,7 +479,8 @@ static map<wint_t, action> g_input_actions {
 | 
			
		||||
	// Sometimes terminfo is wrong, we need to accept both of these
 | 
			
		||||
	{L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_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 ('V'), ACTION_INPUT_QUOTED_INSERT},
 | 
			
		||||
	{CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD},
 | 
			
		||||
@@ -464,8 +488,13 @@ static map<wint_t, action> g_input_actions {
 | 
			
		||||
	{CTRL ('A'), ACTION_INPUT_BEGINNING}, {KEY (HOME), ACTION_INPUT_BEGINNING},
 | 
			
		||||
	{CTRL ('E'), ACTION_INPUT_END}, {KEY (END), ACTION_INPUT_END},
 | 
			
		||||
};
 | 
			
		||||
static map<wint_t, action> g_search_actions {
 | 
			
		||||
	{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
 | 
			
		||||
	{CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
 | 
			
		||||
};
 | 
			
		||||
static const map<string, map<wint_t, action>*> g_binding_contexts {
 | 
			
		||||
	{"normal", &g_normal_actions}, {"input", &g_input_actions},
 | 
			
		||||
	{"search", &g_search_actions},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \
 | 
			
		||||
@@ -485,7 +514,7 @@ static const char *g_ls_colors[] = {LS(XX)};
 | 
			
		||||
 | 
			
		||||
struct stringcaseless {
 | 
			
		||||
	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),
 | 
			
		||||
			[&](char m, char n) { return tolower (m, c) < tolower (n, c); });
 | 
			
		||||
	}
 | 
			
		||||
@@ -528,20 +557,21 @@ static struct {
 | 
			
		||||
	bool no_chdir;                      ///< Do not tell the shell to chdir
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
	const wchar_t *editor;              ///< Prompt string for editing
 | 
			
		||||
	wstring editor_info;                ///< Right-side prompt while editing
 | 
			
		||||
	wstring editor_line;                ///< Current user input
 | 
			
		||||
	int editor_cursor = 0;              ///< Cursor position
 | 
			
		||||
	bool editor_inserting;              ///< Inserting a literal character
 | 
			
		||||
	void (*editor_on_change) ();        ///< Callback on editor change
 | 
			
		||||
	void (*editor_on_confirm) ();       ///< Callback on editor confirmation
 | 
			
		||||
	map<action, void (*) ()> editor_on; ///< Handlers for custom actions
 | 
			
		||||
 | 
			
		||||
	enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_CMDLINE, AT_COUNT };
 | 
			
		||||
	chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, 0};
 | 
			
		||||
	enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE, AT_COUNT };
 | 
			
		||||
	chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0};
 | 
			
		||||
	const char *attr_names[AT_COUNT] =
 | 
			
		||||
		{"cursor", "bar", "cwd", "input", "cmdline"};
 | 
			
		||||
		{"cursor", "bar", "cwd", "input", "info", "cmdline"};
 | 
			
		||||
 | 
			
		||||
	map<int, chtype> ls_colors;         ///< LS_COLORS decoded
 | 
			
		||||
	map<string, chtype> ls_exts;        ///< LS_COLORS file extensions
 | 
			
		||||
@@ -549,12 +579,13 @@ static struct {
 | 
			
		||||
 | 
			
		||||
	map<string, wint_t, stringcaseless> name_to_key;
 | 
			
		||||
	map<wint_t, string> key_to_name;
 | 
			
		||||
	map<string, wint_t> custom_keys;
 | 
			
		||||
	string action_names[ACTION_COUNT];  ///< Stylized action names
 | 
			
		||||
 | 
			
		||||
	// Refreshed by reload():
 | 
			
		||||
 | 
			
		||||
	map<uid_t, string> unames;          ///< User names by UID
 | 
			
		||||
	map<gid_t, string> gnames;          ///< Group names by GID
 | 
			
		||||
	map<uid_t, wstring> unames;         ///< User names by UID
 | 
			
		||||
	map<gid_t, wstring> gnames;         ///< Group names by GID
 | 
			
		||||
	struct tm now;                      ///< Current local time for display
 | 
			
		||||
} g;
 | 
			
		||||
 | 
			
		||||
@@ -585,8 +616,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
 | 
			
		||||
			set (LS_MULTIHARDLINK);
 | 
			
		||||
		if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
 | 
			
		||||
			set (LS_EXECUTABLE);
 | 
			
		||||
#ifdef __linux__
 | 
			
		||||
		if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
 | 
			
		||||
			set (LS_CAPABILITY);
 | 
			
		||||
#endif
 | 
			
		||||
		if ((info.st_mode & S_ISGID))
 | 
			
		||||
			set (LS_SETGID);
 | 
			
		||||
		if ((info.st_mode & S_ISUID))
 | 
			
		||||
@@ -601,8 +634,8 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
 | 
			
		||||
			set (LS_STICKY_OTHER_WRITABLE);
 | 
			
		||||
	} else if (S_ISLNK (info.st_mode)) {
 | 
			
		||||
		type = LS_SYMLINK;
 | 
			
		||||
		if (!e.target_info.st_mode
 | 
			
		||||
		 && (ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
 | 
			
		||||
		if (!e.target_info.st_mode &&
 | 
			
		||||
			(ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
 | 
			
		||||
			type = LS_ORPHAN;
 | 
			
		||||
	} else if (S_ISFIFO (info.st_mode)) {
 | 
			
		||||
		type = LS_FIFO;
 | 
			
		||||
@@ -628,15 +661,33 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
 | 
			
		||||
	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 {
 | 
			
		||||
	entry e;
 | 
			
		||||
	e.filename = f->d_name;
 | 
			
		||||
	e.info.st_mode = DTTOIF (f->d_type);
 | 
			
		||||
	auto &info = e.info;
 | 
			
		||||
 | 
			
		||||
	// TODO: benchmark just readdir() vs. lstat(), also on dead mounts;
 | 
			
		||||
	//   it might make sense to stat asynchronously in threads
 | 
			
		||||
	//   http://lkml.iu.edu/hypermail//linux/kernel/0804.3/1616.html
 | 
			
		||||
	// io_uring is only at most about 50% faster, though it might help with
 | 
			
		||||
	// slowly statting devices, at a major complexity cost.
 | 
			
		||||
	if (lstat (f->d_name, &info)) {
 | 
			
		||||
		e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode),
 | 
			
		||||
			L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0);
 | 
			
		||||
@@ -662,39 +713,44 @@ fun make_entry (const struct dirent *f) -> entry {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto mode = decode_mode (info.st_mode);
 | 
			
		||||
	// This is a Linux-only extension
 | 
			
		||||
#ifdef __linux__
 | 
			
		||||
	// We're using a laughably small subset of libacl: this translates to
 | 
			
		||||
	// two lgetxattr() calls, the results of which are compared with
 | 
			
		||||
	// specific architecture-dependent constants.  Linux-only.
 | 
			
		||||
	if (acl_extended_file_nofollow (f->d_name) > 0)
 | 
			
		||||
		mode += L"+";
 | 
			
		||||
#endif
 | 
			
		||||
	e.cols[entry::MODES] = apply_attrs (mode, 0);
 | 
			
		||||
 | 
			
		||||
	auto usr = g.unames.find (info.st_uid);
 | 
			
		||||
	e.cols[entry::USER] = (usr != g.unames.end ())
 | 
			
		||||
		? apply_attrs (to_wide (usr->second), 0)
 | 
			
		||||
		? apply_attrs (usr->second, 0)
 | 
			
		||||
		: apply_attrs (to_wstring (info.st_uid), 0);
 | 
			
		||||
 | 
			
		||||
	auto grp = g.gnames.find (info.st_gid);
 | 
			
		||||
	e.cols[entry::GROUP] = (grp != g.gnames.end ())
 | 
			
		||||
		? apply_attrs (to_wide (grp->second), 0)
 | 
			
		||||
		? apply_attrs (grp->second, 0)
 | 
			
		||||
		: apply_attrs (to_wstring (info.st_gid), 0);
 | 
			
		||||
 | 
			
		||||
	auto size = to_wstring (info.st_size);
 | 
			
		||||
	if      (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T";
 | 
			
		||||
	else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G";
 | 
			
		||||
	else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M";
 | 
			
		||||
	else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
 | 
			
		||||
	std::wstring size;
 | 
			
		||||
	if (!suffixize (info.st_size, 40, L'T', size) &&
 | 
			
		||||
		!suffixize (info.st_size, 30, L'G', size) &&
 | 
			
		||||
		!suffixize (info.st_size, 20, L'M', size) &&
 | 
			
		||||
		!suffixize (info.st_size, 10, L'K', size))
 | 
			
		||||
		size = to_wstring (info.st_size);
 | 
			
		||||
	e.cols[entry::SIZE] = apply_attrs (size, 0);
 | 
			
		||||
 | 
			
		||||
	char buf[32] = "";
 | 
			
		||||
	wchar_t buf[32] = L"";
 | 
			
		||||
	auto tm = localtime (&info.st_mtime);
 | 
			
		||||
	strftime (buf, sizeof buf,
 | 
			
		||||
		(tm->tm_year == g.now.tm_year) ? "%b %e %H:%M" : "%b %e  %Y", tm);
 | 
			
		||||
	e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0);
 | 
			
		||||
	wcsftime (buf, sizeof buf / sizeof *buf,
 | 
			
		||||
		(tm->tm_year == g.now.tm_year) ? L"%b %e %H:%M" : L"%b %e  %Y", tm);
 | 
			
		||||
	e.cols[entry::MTIME] = apply_attrs (buf, 0);
 | 
			
		||||
 | 
			
		||||
	auto &fn = e.cols[entry::FILENAME] =
 | 
			
		||||
		apply_attrs (to_wide (e.filename), ls_format (e, false));
 | 
			
		||||
	if (!e.target_path.empty ()) {
 | 
			
		||||
		fn.append (apply_attrs (to_wide (" -> "), 0));
 | 
			
		||||
		fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
 | 
			
		||||
		fn += apply_attrs (L" -> ", 0);
 | 
			
		||||
		fn += apply_attrs (to_wide (e.target_path), ls_format (e, true));
 | 
			
		||||
	}
 | 
			
		||||
	return e;
 | 
			
		||||
}
 | 
			
		||||
@@ -703,7 +759,7 @@ fun inline visible_lines () -> int { return max (0, LINES - 2); }
 | 
			
		||||
 | 
			
		||||
fun update () {
 | 
			
		||||
	int start_column = g.full_view ? 0 : entry::FILENAME;
 | 
			
		||||
	static int alignment[entry::COLUMNS] = { -1, -1, -1, 1, 1, -1 };
 | 
			
		||||
	static int alignment[entry::COLUMNS] = {-1, -1, -1, 1, 1, -1};
 | 
			
		||||
	erase ();
 | 
			
		||||
 | 
			
		||||
	int available = visible_lines ();
 | 
			
		||||
@@ -754,11 +810,18 @@ fun update () {
 | 
			
		||||
	curs_set (0);
 | 
			
		||||
	if (g.editor) {
 | 
			
		||||
		move (LINES - 1, 0);
 | 
			
		||||
		auto prompt = apply_attrs (wstring (g.editor) + L": ", 0);
 | 
			
		||||
		auto line = apply_attrs (g.editor_line, 0);
 | 
			
		||||
		print (prompt + line, COLS - 1);
 | 
			
		||||
		auto start = sanitize (prompt + line.substr (0, g.editor_cursor));
 | 
			
		||||
		move (LINES - 1, compute_width (start));
 | 
			
		||||
		auto prompt = apply_attrs (wstring (g.editor) + L": ", 0),
 | 
			
		||||
			line = apply_attrs (g.editor_line, 0),
 | 
			
		||||
			info = apply_attrs (g.editor_info, g.attrs[g.AT_INFO]);
 | 
			
		||||
 | 
			
		||||
		auto info_width = compute_width (info);
 | 
			
		||||
		if (print (prompt + line, COLS - 1) < COLS - info_width) {
 | 
			
		||||
			move (LINES - 1, COLS - info_width);
 | 
			
		||||
			print (info, info_width);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		line.resize (g.editor_cursor);
 | 
			
		||||
		move (LINES - 1, compute_width (sanitize (prompt + line)));
 | 
			
		||||
		curs_set (1);
 | 
			
		||||
	} else if (!g.message.empty ()) {
 | 
			
		||||
		move (LINES - 1, 0);
 | 
			
		||||
@@ -772,9 +835,10 @@ fun update () {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun operator< (const entry &e1, const entry &e2) -> bool {
 | 
			
		||||
	auto t1 = make_tuple (e1.filename != "..",
 | 
			
		||||
	static string dotdot {".."};
 | 
			
		||||
	auto t1 = make_tuple (e1.filename != dotdot,
 | 
			
		||||
		!S_ISDIR (e1.info.st_mode) && !S_ISDIR (e1.target_info.st_mode));
 | 
			
		||||
	auto t2 = make_tuple (e2.filename != "..",
 | 
			
		||||
	auto t2 = make_tuple (e2.filename != dotdot,
 | 
			
		||||
		!S_ISDIR (e2.info.st_mode) && !S_ISDIR (e2.target_info.st_mode));
 | 
			
		||||
	if (t1 != t2)
 | 
			
		||||
		return t1 < t2;
 | 
			
		||||
@@ -811,16 +875,34 @@ fun at_cursor () -> const entry & {
 | 
			
		||||
	return g.cursor >= int (g.entries.size ()) ? invalid : g.entries[g.cursor];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun reload (bool keep_anchor) {
 | 
			
		||||
	g.unames.clear();
 | 
			
		||||
	while (auto *ent = getpwent ())
 | 
			
		||||
		g.unames.emplace (ent->pw_uid, ent->pw_name);
 | 
			
		||||
	endpwent();
 | 
			
		||||
fun focus (const string &anchor) {
 | 
			
		||||
	if (!anchor.empty ()) {
 | 
			
		||||
		for (size_t i = 0; i < g.entries.size (); i++)
 | 
			
		||||
			if (g.entries[i].filename == anchor)
 | 
			
		||||
				g.cursor = i;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	g.gnames.clear();
 | 
			
		||||
fun resort (const string anchor = at_cursor ().filename) {
 | 
			
		||||
	sort (begin (g.entries), end (g.entries));
 | 
			
		||||
	focus (anchor);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun show_message (const string &message, int ttl = 30) {
 | 
			
		||||
	g.message = to_wide (message);
 | 
			
		||||
	g.message_ttl = ttl;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun reload (bool keep_anchor) {
 | 
			
		||||
	g.unames.clear ();
 | 
			
		||||
	while (auto *ent = getpwent ())
 | 
			
		||||
		g.unames.emplace (ent->pw_uid, to_wide (ent->pw_name));
 | 
			
		||||
	endpwent ();
 | 
			
		||||
 | 
			
		||||
	g.gnames.clear ();
 | 
			
		||||
	while (auto *ent = getgrent ())
 | 
			
		||||
		g.gnames.emplace (ent->gr_gid, ent->gr_name);
 | 
			
		||||
	endgrent();
 | 
			
		||||
		g.gnames.emplace (ent->gr_gid, to_wide (ent->gr_name));
 | 
			
		||||
	endgrent ();
 | 
			
		||||
 | 
			
		||||
	string anchor;
 | 
			
		||||
	if (keep_anchor)
 | 
			
		||||
@@ -829,6 +911,16 @@ fun reload (bool keep_anchor) {
 | 
			
		||||
	auto now = time (NULL); g.now = *localtime (&now);
 | 
			
		||||
	auto dir = opendir (".");
 | 
			
		||||
	g.entries.clear ();
 | 
			
		||||
	if (!dir) {
 | 
			
		||||
		show_message (strerror (errno));
 | 
			
		||||
		if (g.cwd != "/") {
 | 
			
		||||
			struct dirent f = {};
 | 
			
		||||
			strncpy (f.d_name, "..", sizeof f.d_name);
 | 
			
		||||
			f.d_type = DT_DIR;
 | 
			
		||||
			g.entries.push_back (make_entry (&f));
 | 
			
		||||
		}
 | 
			
		||||
		goto readfail;
 | 
			
		||||
	}
 | 
			
		||||
	while (auto f = readdir (dir)) {
 | 
			
		||||
		string name = f->d_name;
 | 
			
		||||
		// Two dots are for navigation but this ain't as useful
 | 
			
		||||
@@ -838,45 +930,52 @@ fun reload (bool keep_anchor) {
 | 
			
		||||
			g.entries.push_back (make_entry (f));
 | 
			
		||||
	}
 | 
			
		||||
	closedir (dir);
 | 
			
		||||
	sort (begin (g.entries), end (g.entries));
 | 
			
		||||
	g.out_of_date = false;
 | 
			
		||||
 | 
			
		||||
	if (!anchor.empty ()) {
 | 
			
		||||
		for (size_t i = 0; i < g.entries.size (); i++)
 | 
			
		||||
			if (g.entries[i].filename == anchor)
 | 
			
		||||
				g.cursor = i;
 | 
			
		||||
	}
 | 
			
		||||
readfail:
 | 
			
		||||
	g.out_of_date = false;
 | 
			
		||||
	for (int col = 0; col < entry::COLUMNS; col++) {
 | 
			
		||||
		auto &longest = g.max_widths[col] = 0;
 | 
			
		||||
		for (const auto &entry : g.entries)
 | 
			
		||||
			longest = max (longest, compute_width (entry.cols[col]));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resort (anchor);
 | 
			
		||||
 | 
			
		||||
	g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1));
 | 
			
		||||
	g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1));
 | 
			
		||||
 | 
			
		||||
	if (g.inotify_wd != -1)
 | 
			
		||||
		inotify_rm_watch (g.inotify_fd, g.inotify_wd);
 | 
			
		||||
#ifdef __linux__
 | 
			
		||||
	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
 | 
			
		||||
	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));
 | 
			
		||||
#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 show_message (const string &message, int ttl = 30) {
 | 
			
		||||
	g.message = to_wide (message);
 | 
			
		||||
	g.message_ttl = ttl;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
		// XXX: this doesn't try them all out, though it shouldn't make any
 | 
			
		||||
		// noticeable difference
 | 
			
		||||
		// XXX: this doesn't try them all out,
 | 
			
		||||
		// though it shouldn't make any noticeable difference
 | 
			
		||||
		const char *found = nullptr;
 | 
			
		||||
		for (auto program : list)
 | 
			
		||||
			if ((found = program))
 | 
			
		||||
				break;
 | 
			
		||||
		g.ext_helper = found + (" " + shell_escape (filename));
 | 
			
		||||
		g.ext_helper.assign (found).append (args);
 | 
			
		||||
		g.quitting = true;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
@@ -892,8 +991,8 @@ fun run_program (initializer_list<const char*> list, const string &filename) {
 | 
			
		||||
		tcsetpgrp (STDOUT_FILENO, getpgid (0));
 | 
			
		||||
 | 
			
		||||
		for (auto program : list)
 | 
			
		||||
			if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program)
 | 
			
		||||
				+ " " + shell_escape (filename)).c_str (), NULL);
 | 
			
		||||
			if (program) execl ("/bin/sh", "/bin/sh", "-c",
 | 
			
		||||
				(program + args).c_str (), NULL);
 | 
			
		||||
		_exit (EXIT_FAILURE);
 | 
			
		||||
	default:
 | 
			
		||||
		// ...and make sure of it in the parent as well
 | 
			
		||||
@@ -993,11 +1092,40 @@ fun show_help () {
 | 
			
		||||
	fclose (contents);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun search (const wstring &needle) {
 | 
			
		||||
	int best = g.cursor, best_n = 0;
 | 
			
		||||
	for (int i = 0; i < int (g.entries.size ()); i++) {
 | 
			
		||||
		auto o = (i + g.cursor) % g.entries.size ();
 | 
			
		||||
		int n = prefix_length (to_wide (g.entries[o].filename), needle);
 | 
			
		||||
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) {
 | 
			
		||||
	int matches = match (g.editor_line, push);
 | 
			
		||||
	if (g.editor_line.empty ())
 | 
			
		||||
		g.editor_info.clear ();
 | 
			
		||||
	else if (matches == 0)
 | 
			
		||||
		g.editor_info = L"(no match)";
 | 
			
		||||
	else if (matches == 1)
 | 
			
		||||
		g.editor_info = L"(1 match)";
 | 
			
		||||
	else
 | 
			
		||||
		g.editor_info = L"(" + to_wstring (matches) + L" matches)";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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;
 | 
			
		||||
@@ -1042,7 +1170,7 @@ fun relativize (string current, const string &path) -> string {
 | 
			
		||||
	return path;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun pop_levels (const string& old_cwd) {
 | 
			
		||||
fun pop_levels (const string &old_cwd) {
 | 
			
		||||
	string anchor; auto i = g.levels.rbegin ();
 | 
			
		||||
	while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) {
 | 
			
		||||
		if (i->path == g.cwd) {
 | 
			
		||||
@@ -1062,7 +1190,7 @@ fun pop_levels (const string& old_cwd) {
 | 
			
		||||
 | 
			
		||||
	fix_cursor_and_offset ();
 | 
			
		||||
	if (!anchor.empty () && at_cursor ().filename != anchor)
 | 
			
		||||
		search (to_wide (anchor));
 | 
			
		||||
		lookup (to_wide (anchor));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun explode_path (const string &path, vector<string> &out) {
 | 
			
		||||
@@ -1120,7 +1248,7 @@ fun change_dir (const string &path) {
 | 
			
		||||
				beep ();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			if (!out.back().empty ())
 | 
			
		||||
			if (!out.back ().empty ())
 | 
			
		||||
				out.pop_back ();
 | 
			
		||||
		} else if (in[i] != "." && (!in[i].empty () || i < startempty)) {
 | 
			
		||||
			out.push_back (in[i]);
 | 
			
		||||
@@ -1187,31 +1315,42 @@ fun choose (const entry &entry) {
 | 
			
		||||
// Move the cursor in `diff` direction and look for non-combining characters
 | 
			
		||||
fun move_towards_spacing (int diff) -> bool {
 | 
			
		||||
	g.editor_cursor += diff;
 | 
			
		||||
	return g.editor_cursor <= 0
 | 
			
		||||
		|| g.editor_cursor >= int (g.editor_line.length ())
 | 
			
		||||
		|| wcwidth (g.editor_line.at (g.editor_cursor));
 | 
			
		||||
	return g.editor_cursor <= 0 ||
 | 
			
		||||
		g.editor_cursor >= int (g.editor_line.length ()) ||
 | 
			
		||||
		wcwidth (g.editor_line.at (g.editor_cursor));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun handle_editor (wint_t c) {
 | 
			
		||||
	auto i = g_input_actions.find (g.editor_inserting ? WEOF : c);
 | 
			
		||||
	auto action = ACTION_NONE;
 | 
			
		||||
	if (g.editor_inserting) {
 | 
			
		||||
		(void) halfdelay (1);
 | 
			
		||||
		g.editor_inserting = false;
 | 
			
		||||
	} else {
 | 
			
		||||
		auto i = g_input_actions.find (c);
 | 
			
		||||
		if (i != g_input_actions.end ())
 | 
			
		||||
			action = i->second;
 | 
			
		||||
 | 
			
		||||
		auto m = g_binding_contexts.find (to_mb (g.editor));
 | 
			
		||||
		if (m != g_binding_contexts.end () &&
 | 
			
		||||
			(i = m->second->find (c)) != m->second->end ())
 | 
			
		||||
			action = i->second;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch (i == g_input_actions.end () ? ACTION_NONE : i->second) {
 | 
			
		||||
	auto original = g.editor_line;
 | 
			
		||||
	switch (action) {
 | 
			
		||||
	case ACTION_INPUT_CONFIRM:
 | 
			
		||||
		if (g.editor_on_confirm)
 | 
			
		||||
			g.editor_on_confirm ();
 | 
			
		||||
		if (auto handler = g.editor_on[action])
 | 
			
		||||
			handler ();
 | 
			
		||||
		// Fall-through
 | 
			
		||||
	case ACTION_INPUT_ABORT:
 | 
			
		||||
		g.editor = 0;
 | 
			
		||||
		g.editor_info.clear ();
 | 
			
		||||
		g.editor_line.clear ();
 | 
			
		||||
		g.editor_cursor = 0;
 | 
			
		||||
		g.editor_inserting = false;
 | 
			
		||||
		g.editor_on_change = nullptr;
 | 
			
		||||
		g.editor_on_confirm = nullptr;
 | 
			
		||||
		break;
 | 
			
		||||
		g.editor_on.clear ();
 | 
			
		||||
		return;
 | 
			
		||||
	case ACTION_INPUT_BEGINNING:
 | 
			
		||||
		g.editor_cursor = 0;
 | 
			
		||||
		break;
 | 
			
		||||
@@ -1219,13 +1358,13 @@ fun handle_editor (wint_t c) {
 | 
			
		||||
		g.editor_cursor = g.editor_line.length ();
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_INPUT_BACKWARD:
 | 
			
		||||
		while (g.editor_cursor > 0
 | 
			
		||||
			&& !move_towards_spacing (-1))
 | 
			
		||||
		while (g.editor_cursor > 0 &&
 | 
			
		||||
			!move_towards_spacing (-1))
 | 
			
		||||
			;
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_INPUT_FORWARD:
 | 
			
		||||
		while (g.editor_cursor < int (g.editor_line.length ())
 | 
			
		||||
			&& !move_towards_spacing (+1))
 | 
			
		||||
		while (g.editor_cursor < int (g.editor_line.length ()) &&
 | 
			
		||||
			!move_towards_spacing (+1))
 | 
			
		||||
			;
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_INPUT_B_DELETE:
 | 
			
		||||
@@ -1243,6 +1382,17 @@ fun handle_editor (wint_t c) {
 | 
			
		||||
				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:
 | 
			
		||||
		g.editor_line.erase (0, g.editor_cursor);
 | 
			
		||||
		g.editor_cursor = 0;
 | 
			
		||||
@@ -1255,15 +1405,18 @@ fun handle_editor (wint_t c) {
 | 
			
		||||
		g.editor_inserting = true;
 | 
			
		||||
		break;
 | 
			
		||||
	default:
 | 
			
		||||
		if (c & (ALT | SYM)) {
 | 
			
		||||
		if (auto handler = g.editor_on[action]) {
 | 
			
		||||
			handler ();
 | 
			
		||||
		} else if (c & (ALT | SYM)) {
 | 
			
		||||
			if (c != KEY (RESIZE))
 | 
			
		||||
				beep ();
 | 
			
		||||
		} else {
 | 
			
		||||
			g.editor_line.insert (g.editor_cursor, 1, c);
 | 
			
		||||
			g.editor_cursor++;
 | 
			
		||||
			if (g.editor_on_change)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (g.editor_on_change && g.editor_line != original)
 | 
			
		||||
		g.editor_on_change ();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun handle (wint_t c) -> bool {
 | 
			
		||||
@@ -1284,6 +1437,7 @@ fun handle (wint_t c) -> bool {
 | 
			
		||||
	auto i = g_normal_actions.find (c);
 | 
			
		||||
	switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
 | 
			
		||||
	case ACTION_CHOOSE_FULL:
 | 
			
		||||
		// FIXME: in the root directory, this inserts //item
 | 
			
		||||
		g.chosen = g.cwd + "/" + current.filename;
 | 
			
		||||
		g.no_chdir = true;
 | 
			
		||||
		g.quitting = true;
 | 
			
		||||
@@ -1311,12 +1465,12 @@ fun handle (wint_t c) -> bool {
 | 
			
		||||
	case ACTION_SORT_LEFT:
 | 
			
		||||
		g.sort_column = (g.sort_column + entry::COLUMNS - 1) % entry::COLUMNS;
 | 
			
		||||
		g.sort_flash_ttl = 2;
 | 
			
		||||
		reload (true);
 | 
			
		||||
		resort ();
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_SORT_RIGHT:
 | 
			
		||||
		g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS;
 | 
			
		||||
		g.sort_flash_ttl = 2;
 | 
			
		||||
		reload (true);
 | 
			
		||||
		resort ();
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case ACTION_UP:
 | 
			
		||||
@@ -1355,10 +1509,13 @@ fun handle (wint_t c) -> bool {
 | 
			
		||||
	case ACTION_SCROLL_UP:
 | 
			
		||||
		g.offset--;
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_CENTER:
 | 
			
		||||
		g.offset = g.cursor - (visible_lines () - 1) / 2;
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case ACTION_CHDIR:
 | 
			
		||||
		g.editor = L"chdir";
 | 
			
		||||
		g.editor_on_confirm = [] {
 | 
			
		||||
		g.editor_on[ACTION_INPUT_CONFIRM] = [] {
 | 
			
		||||
			change_dir (untilde (to_mb (g.editor_line)));
 | 
			
		||||
		};
 | 
			
		||||
		break;
 | 
			
		||||
@@ -1374,12 +1531,10 @@ fun handle (wint_t c) -> bool {
 | 
			
		||||
 | 
			
		||||
	case ACTION_SEARCH:
 | 
			
		||||
		g.editor = L"search";
 | 
			
		||||
		g.editor_on_change = [] {
 | 
			
		||||
			search (g.editor_line);
 | 
			
		||||
		};
 | 
			
		||||
		g.editor_on_confirm = [] {
 | 
			
		||||
			choose (at_cursor ());
 | 
			
		||||
		};
 | 
			
		||||
		g.editor_on_change                = [] { match_interactive (0); };
 | 
			
		||||
		g.editor_on[ACTION_UP]            = [] { match_interactive (-1); };
 | 
			
		||||
		g.editor_on[ACTION_DOWN]          = [] { match_interactive (+1); };
 | 
			
		||||
		g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); };
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_RENAME_PREFILL:
 | 
			
		||||
		g.editor_line = to_wide (current.filename);
 | 
			
		||||
@@ -1387,19 +1542,30 @@ fun handle (wint_t c) -> bool {
 | 
			
		||||
		// Fall-through
 | 
			
		||||
	case ACTION_RENAME:
 | 
			
		||||
		g.editor = L"rename";
 | 
			
		||||
		g.editor_on_confirm = [] {
 | 
			
		||||
		g.editor_on[ACTION_INPUT_CONFIRM] = [] {
 | 
			
		||||
			auto mb = to_mb (g.editor_line);
 | 
			
		||||
			rename (at_cursor ().filename.c_str (), mb.c_str ());
 | 
			
		||||
			if (rename (at_cursor ().filename.c_str (), mb.c_str ()))
 | 
			
		||||
				show_message (strerror (errno));
 | 
			
		||||
			reload (true);
 | 
			
		||||
		};
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_MKDIR:
 | 
			
		||||
		g.editor = L"mkdir";
 | 
			
		||||
		g.editor_on[ACTION_INPUT_CONFIRM] = [] {
 | 
			
		||||
			auto mb = to_mb (g.editor_line);
 | 
			
		||||
			if (mkdir (mb.c_str (), 0777))
 | 
			
		||||
				show_message (strerror (errno));
 | 
			
		||||
			reload (true);
 | 
			
		||||
			focus (mb);
 | 
			
		||||
		};
 | 
			
		||||
		break;
 | 
			
		||||
 | 
			
		||||
	case ACTION_TOGGLE_FULL:
 | 
			
		||||
		g.full_view = !g.full_view;
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_REVERSE_SORT:
 | 
			
		||||
		g.reverse_sort = !g.reverse_sort;
 | 
			
		||||
		reload (true);
 | 
			
		||||
		resort ();
 | 
			
		||||
		break;
 | 
			
		||||
	case ACTION_SHOW_HIDDEN:
 | 
			
		||||
		g.show_hidden = !g.show_hidden;
 | 
			
		||||
@@ -1420,19 +1586,27 @@ fun handle (wint_t c) -> bool {
 | 
			
		||||
	return !g.quitting;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun inotify_check () {
 | 
			
		||||
	// Only provide simple indication that contents might have changed
 | 
			
		||||
	char buf[4096]; ssize_t len;
 | 
			
		||||
fun watch_check () {
 | 
			
		||||
	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;
 | 
			
		||||
		for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
 | 
			
		||||
			e = (const inotify_event *) buf;
 | 
			
		||||
			if (e->wd == g.inotify_wd)
 | 
			
		||||
				changed = g.out_of_date = true;
 | 
			
		||||
			if (e->wd == g.watch_wd)
 | 
			
		||||
				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 ();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1496,8 +1670,8 @@ fun load_ls_colors (vector<string> colors) {
 | 
			
		||||
		if (equal == string::npos)
 | 
			
		||||
			continue;
 | 
			
		||||
		auto key = pair.substr (0, equal), value = pair.substr (equal + 1);
 | 
			
		||||
		if (key != g_ls_colors[LS_SYMLINK]
 | 
			
		||||
		 || !(g.ls_symlink_as_target = value == "target"))
 | 
			
		||||
		if (key != g_ls_colors[LS_SYMLINK] ||
 | 
			
		||||
			!(g.ls_symlink_as_target = value == "target"))
 | 
			
		||||
			attrs[key] = decode_ansi_sgr (split (value, ";"));
 | 
			
		||||
	}
 | 
			
		||||
	for (int i = 0; i < LS_COUNT; i++) {
 | 
			
		||||
@@ -1570,16 +1744,16 @@ fun parse_key (const string &key_name) -> wint_t {
 | 
			
		||||
		c |= ALT;
 | 
			
		||||
		p += 2;
 | 
			
		||||
	}
 | 
			
		||||
	if (!strncmp (p, "C-", 2)) {
 | 
			
		||||
	if (g.name_to_key.count (p)) {
 | 
			
		||||
		return c | g.name_to_key.at (p);
 | 
			
		||||
	} else if (!strncmp (p, "C-", 2)) {
 | 
			
		||||
		p += 2;
 | 
			
		||||
		if (*p < '?' || *p > 'z') {
 | 
			
		||||
		if (*p < '?' || *p > '~') {
 | 
			
		||||
			cerr << "bindings: invalid combination: " << key_name << endl;
 | 
			
		||||
			return WEOF;
 | 
			
		||||
		}
 | 
			
		||||
		c |= CTRL (toupper (*p));
 | 
			
		||||
		c |= CTRL (*p);
 | 
			
		||||
		p += 1;
 | 
			
		||||
	} else if (g.name_to_key.count (p)) {
 | 
			
		||||
		return c | g.name_to_key.at (p);
 | 
			
		||||
	} else {
 | 
			
		||||
		wchar_t w; mbstate_t mb {};
 | 
			
		||||
		auto len = strlen (p) + 1, res = mbrtowc (&w, p, len, &mb);
 | 
			
		||||
@@ -1608,7 +1782,9 @@ fun learn_named_key (const string &name, wint_t key) {
 | 
			
		||||
fun load_bindings () {
 | 
			
		||||
	learn_named_key ("space", ' ');
 | 
			
		||||
	learn_named_key ("escape", 0x1b);
 | 
			
		||||
	for (int kc = KEY_MIN; kc < KEY_MAX; kc++) {
 | 
			
		||||
 | 
			
		||||
	int kc = 0;
 | 
			
		||||
	for (kc = KEY_MIN; kc <= KEY_MAX; kc++) {
 | 
			
		||||
		const char *name = keyname (kc);
 | 
			
		||||
		if (!name)
 | 
			
		||||
			continue;
 | 
			
		||||
@@ -1642,11 +1818,18 @@ fun load_bindings () {
 | 
			
		||||
		if (tokens.empty ())
 | 
			
		||||
			continue;
 | 
			
		||||
		if (tokens.size () < 3) {
 | 
			
		||||
			cerr << "bindings: expected: context binding action";
 | 
			
		||||
			cerr << "bindings: expected: define name key-sequence"
 | 
			
		||||
				" | context binding action";
 | 
			
		||||
			continue;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto context = tokens[0], key_name = tokens[1], action = tokens[2];
 | 
			
		||||
		if (context == "define") {
 | 
			
		||||
			// We haven't run initscr() yet, so define_key() would fail here
 | 
			
		||||
			learn_named_key (key_name, SYM | (g.custom_keys[action] = ++kc));
 | 
			
		||||
			continue;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto m = g_binding_contexts.find (context);
 | 
			
		||||
		if (m == g_binding_contexts.end ()) {
 | 
			
		||||
			cerr << "bindings: invalid context: " << context << endl;
 | 
			
		||||
@@ -1725,6 +1908,11 @@ fun save_config () {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int main (int argc, char *argv[]) {
 | 
			
		||||
	if (argc == 2 && string (argv[1]) == "--version") {
 | 
			
		||||
		cout << PROJECT_NAME << " " << PROJECT_VERSION << endl;
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// zsh before 5.4 may close stdin before exec without redirection,
 | 
			
		||||
	// since then it redirects stdin to /dev/null
 | 
			
		||||
	(void) close (STDIN_FILENO);
 | 
			
		||||
@@ -1740,10 +1928,17 @@ int main (int argc, char *argv[]) {
 | 
			
		||||
	// So that the neither us nor our children stop on tcsetpgrp()
 | 
			
		||||
	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;
 | 
			
		||||
		return 1;
 | 
			
		||||
	}
 | 
			
		||||
#else
 | 
			
		||||
	if ((g.watch_fd = kqueue ()) < 0) {
 | 
			
		||||
		cerr << "cannot initialize kqueue" << endl;
 | 
			
		||||
		return 1;
 | 
			
		||||
	}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
	locale::global (locale (""));
 | 
			
		||||
	load_bindings ();
 | 
			
		||||
@@ -1753,6 +1948,8 @@ int main (int argc, char *argv[]) {
 | 
			
		||||
		cerr << "cannot initialize screen" << endl;
 | 
			
		||||
		return 1;
 | 
			
		||||
	}
 | 
			
		||||
	for (const auto &definition_kc : g.custom_keys)
 | 
			
		||||
		define_key (definition_kc.first.c_str (), definition_kc.second);
 | 
			
		||||
 | 
			
		||||
	load_colors ();
 | 
			
		||||
	load_cmdline (argc, argv);
 | 
			
		||||
@@ -1761,6 +1958,13 @@ int main (int argc, char *argv[]) {
 | 
			
		||||
	pop_levels (g.cwd);
 | 
			
		||||
	update ();
 | 
			
		||||
 | 
			
		||||
	// Cunt, now I need to reïmplement all signal handling
 | 
			
		||||
#if NCURSES_VERSION_PATCH < 20210821
 | 
			
		||||
	// This gets applied along with the following halfdelay()
 | 
			
		||||
	cur_term->Nttyb.c_cc[VSTOP] =
 | 
			
		||||
		cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
	// Invoking keypad() earlier would make ncurses flush its output buffer,
 | 
			
		||||
	// which would worsen start-up flickering
 | 
			
		||||
	if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
 | 
			
		||||
@@ -1771,7 +1975,7 @@ int main (int argc, char *argv[]) {
 | 
			
		||||
 | 
			
		||||
	wint_t c;
 | 
			
		||||
	while (!read_key (c) || handle (c)) {
 | 
			
		||||
		inotify_check ();
 | 
			
		||||
		watch_check ();
 | 
			
		||||
		if (g.sort_flash_ttl && !--g.sort_flash_ttl)
 | 
			
		||||
			update ();
 | 
			
		||||
		if (g.message_ttl && !--g.message_ttl) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user