Compare commits

...

59 Commits

Author SHA1 Message Date
272ee62ad8 Make a release
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-12-21 09:10:54 +01:00
a85426541a sdn-install: improve macOS installation
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Login shells have a dash at the beginning of their first argument.
2024-12-21 08:16:23 +01:00
c9b003735d Fix OpenBSD build
All checks were successful
Arch Linux AUR Success
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-12-21 07:59:44 +01:00
52a28f01c8 Add support for BSD derivatives, fix macOS
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-21 07:37:06 +01:00
3607757554 Improve filename passing
All checks were successful
Alpine 3.19 Success
Arch Linux AUR Success
c9662f1 forgot about internal helpers.

Moreover, it is annoying to see the -- in shell history
for every single external helper call.
2024-04-17 02:00:10 +02:00
6eb216a40a Remove linker preference from CMakeLists.txt
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
ld.gold doesn't understand all options that ld.bfd does.

In particular, this broke AUR builds with the current makepkg.conf.
2024-04-08 01:49:55 +02:00
9ce6f47716 Use more precise filesizes
Some checks failed
Arch Linux AUR Scripts failed
The behaviour differs from GNU `ls -lh` in that we use binary units,
meaning we get 1023 before 1.0K rather than 999 before 1.0K,
which is nonetheless still four characters wide.
2024-04-07 16:00:21 +02:00
c9662f1a7b Fix passing filenames starting with -
We don't want to pass them as program options.
2024-01-20 08:33:17 +01:00
9ddeb03652 CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:11:11 +02:00
acb187c6b1 README.adoc: update package information 2023-07-01 21:58:29 +02:00
9427df62e7 Fix code formatting, bump copyright years 2023-06-12 14:00:58 +02:00
4d6999c415 Do not beep on window resizes 2023-06-11 21:11:20 +02:00
30ed61fdd2 Implement ^W in the editor 2023-06-11 21:05:55 +02:00
2df916c9b3 Support wildcards in interactive search
The previous search for the longest match is functionally
duplicated by typing individual characters on the input line,
and wildcards can be escaped, so there shouldn't be regressions
in terms of capability.
2022-09-03 12:17:02 +02:00
24401825b4 Add a missing case break 2022-01-08 11:23:36 +01:00
2bfb490798 Add and bind an action to center the cursor
"z" stands for VIM's "zz".
2022-01-08 11:17:24 +01:00
338d00d605 Do not crash on opendir() failures
Show an error message, and a way out.
2021-11-09 07:52:48 +01:00
015652e379 Fix build with recent ncurses
Easily gained, easily lost.  No more ^S binding.
2021-11-09 07:45:20 +01:00
c298b6fc97 bash integration: add helpers to command history 2021-11-05 22:11:30 +01:00
7c2ab8ab59 zsh integration: add helpers to command history 2021-11-05 21:54:56 +01:00
e423a3a1b1 Add clang-format configuration, clean up 2021-11-05 21:34:05 +01:00
916f354c9b Move the cursor to just created directories 2021-10-30 12:32:40 +02:00
050f875c47 Clean up 2021-10-30 12:13:45 +02:00
aeffe40efc Bind ^S to search, as in Emacs, with a trick
We heavily depend on ncurses anyway, so no worries about portability.
2021-10-18 11:23:17 +02:00
536aa57761 Slightly optimize very large directories
Cumulatively 10% of user time, give or take.

These are mainly pointless multibyte to wide string conversions.

The hit to source code readibility is minimal.
2021-10-05 21:01:32 +02:00
0d10ae06e6 Measured stat performance, io_uring not worth it
Large directories are slow for different reasons.
2021-10-05 19:18:15 +02:00
e1b0831854 Don't reload on sort changes
It is unexpected behaviour, and likes to take too long.
2021-09-26 09:38:16 +02:00
4e93dfbb8d Add a comment about libacl
So that I don't repeat my actions in the future.
2021-09-26 09:19:54 +02:00
8a6bb54eb5 Ignore empty XDG_CONFIG_DIRS
As the specification says we should.  GLib does this as well.

It is still possible to achieve an empty set by using ":",
which are two non-absolute paths that should be ignored.
GLib doesn't implement this.  Thus, we're now better than GLib.
2021-09-26 08:59:57 +02:00
4ef7c9edf7 Makefile: make the static build a non-phony target 2021-07-19 09:28:50 +02:00
3eea106c3c Explicitly disable GCC's -Wmisleading-indentation 2021-07-19 09:23:28 +02:00
7de8c84e8f Fix a signedness compiler warning 2021-07-19 09:12:17 +02:00
e17c5e2083 Bind F6 to a rename prompt
Mostly just so that the F7 binding isn't completely alone.

Since Shift-F6 isn't something we can just bind to, use a prefill.
2021-07-17 14:47:50 +02:00
9bd3739122 Bind F7 to an mkdir prompt
This might have needed more thought, but we'll see.
2021-07-17 14:41:57 +02:00
ec1f1031cc Implement search iteration
The interactive search now has its own keymap, overriding "input".

Closes #5
2021-07-17 14:19:37 +02:00
bc99b3dd48 Add a right-side prompt to the search 2021-07-17 13:04:42 +02:00
e948741864 Enable pushing the search in a certain direction
We want to make it possible to iterate all current matches.
2021-07-17 08:49:54 +02:00
0adbac2066 Make search() return the number of matches 2021-07-17 07:54:03 +02:00
2238db5a4e Make removing characters invoke g.editor_on_change 2021-07-17 07:24:16 +02:00
98612f5492 Improve C-char parsing
I was hoping that a superoptimiser would help me find a miraculous
branchless equation to cover it, but in the end the branching
doesn't hurt at all in our case.

It's more readable than `(((char >> 2) - 0x38) & 0x60) ^ char`
or `(char ^ 0x40) & (((char >> 1) ^ 0x20) | 0x5f)`.
2021-07-09 05:16:37 +02:00
1034321f81 sdn-install: explain why -dot is experimental
Add an explanatory link, so that I don't need to look it up again.
2021-07-07 22:13:46 +02:00
e7da32160c Makefile: fix version extraction
Regression introduced by cc59fcf.
2021-07-07 21:21:52 +02:00
fdb338fe12 Allow binding to custom key sequences
And document this feature in the manual page.
2021-07-07 21:21:32 +02:00
9056ef4194 README.adoc: make use of the "plus" attribute 2020-11-07 03:22:51 +01:00
b8a4742fb9 sdn.1: refer to a better manual page 2020-11-05 01:24:05 +01:00
c999e5a8e4 sdn.1: take care about sentence spacing
For more info, see e.g.
https://github.com/asciidoc/asciidoc-py3/issues/137

I'll probably commit to mandoc here but scdoc is appealing.

Somewhat sadly, it explicitly suppresses sentence spacing
but I'm wondering whether it couldn't be reimplemented better,
in a more AsciiDoc-like fashion.
2020-10-29 19:22:20 +01:00
07ef834a1e Include a header required for kill()
Yet, this program remains fairly unportable.
2020-10-29 03:24:37 +01:00
997f5c25a2 sdn.1: improve wording about envvars 2020-10-27 14:58:23 +01:00
39e68a977c sdn.1: fix default key binding references 2020-10-27 14:46:04 +01:00
c20d3780b2 Make the manpages mostly acceptable
Closes #1
2020-10-27 04:17:52 +01:00
22725ba3b7 Update README.adoc
I've put the project in AUR, it seems to deserve it.
2020-10-27 01:16:07 +01:00
df046bb071 Add some mediocre manpages
I'm not particularly happy to write in mdoc but here goes.
2020-10-26 20:46:09 +01:00
0c1a8d9902 Implement the --version option
Since we have a version number at all, even if it's meaningless.
2020-10-26 19:07:47 +01:00
cc59fcfb41 CMakeLists.txt: cleanup 2020-10-26 18:56:14 +01:00
c88566e7bb Bind F1 to show the help 2020-10-25 08:19:49 +01:00
39c840cd74 Mark a minor issue for later resolution 2020-10-24 19:28:11 +02:00
f231828e8d sdn-install: bash: fix UTF-8 collisions 2020-10-24 19:27:07 +02:00
1318c4983f Makefile: make portable, support static linking
We can happily stick to POSIX and the usual variable names,
with the following exceptions:
 - pkg-config(1) is used to find libraries and compile flags
 - the new "static" target uses non-standard compiler
   and linker flags, though it's not the default target

sdn is predestined for wild distribution,
even the dynamically linked libc is rather suboptimal.
2020-10-23 08:22:59 +02:00
c503954f44 Cleanup
The wchar_t variant of compute_width() is no longer needed.

So all in all the better help has saved code.
2020-10-23 07:53:37 +02:00
10 changed files with 612 additions and 208 deletions

14
.clang-format Normal file
View 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']

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

4
NEWS Normal file
View File

@@ -0,0 +1,4 @@
1.0.0 (2024-12-21)
* Initial release

View File

@@ -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

View File

@@ -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
View 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
View 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.

530
sdn.cpp
View File

@@ -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)
@@ -404,19 +423,20 @@ fun decode_attrs (const vector<string> &attrs) -> chtype {
// --- Application -------------------------------------------------------------
enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
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)) {
beep ();
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)
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) {