25 Commits

Author SHA1 Message Date
p 8556ea1588 Bump version
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-04 06:23:02 +02:00
p 338572806b Assign unnesting level to the last history level
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Surprise unnesting ~ is no fun.
2026-08-04 06:16:34 +02:00
p 60fcff0285 Update documentation
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-04 05:46:31 +02:00
p f33795329e README.adoc: add a nicer demo
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
I'm not quite sure what to put in it, but it beats a static image.
2026-08-01 03:25:51 +02:00
p 85416db143 Let's call it unnesting rather than unpacking 2026-08-01 03:25:51 +02:00
p c3662b0f13 Fix shifted function keys
Make both the normal and rxvt interpretations of pcfkeys work,
while trying to make rxvt work better in general.

Blargh.
2026-07-31 23:54:21 +02:00
p f7f85cb08a README.adoc: rewrite the pitch
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
I suppose it's now "shell directory navigator" rather than "simple ...".
2026-07-31 13:01:05 +02:00
p aa4be26520 Add an action to compute directory sizes
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
The binding, modelled after Midnight Commander, aliases to C-Space.

Not saving this to the state file.
2026-07-28 22:59:33 +02:00
p 73093cac74 Adjust keyboard bindings 2026-07-28 22:51:35 +02:00
p 9873a06761 Improve matching in the lookup function
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-24 19:42:29 +02:00
p 2ac93c0e0e Allow unpacking multiple levels of directories
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
To get an overview despite hierarchies.
2026-07-24 15:30:37 +02:00
p 28dcbd8a2c Make status bar contents configurable
Started calling it a status bar rather than just a bar now.

Inventing our own format, because:
 - tmux doesn't have quite enough useful variables,
   besides #h/#{host_short}, #{USER}, and perhaps #{PWD}.
 - zsh uses weird names for everything.
 - bash uses mildly inconvenient \escapes, and lacks formatting.

Also bumping C++ to 17, because the project already requires it
to build anyway (it's a relict from the time of sdn's conception).
2026-07-24 15:30:37 +02:00
p b2701d93bd Avoid variable shadowing 2026-07-24 15:30:37 +02:00
p b7d516c66f Fix shell quoting
The tilde character was an odd UTF-8 multibyte one.
2026-07-24 15:30:32 +02:00
p d6e0b9bc01 Rectify a non-commented odd condition 2026-07-24 15:30:32 +02:00
p f3f0bc3d33 Rename ic and dc keys
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Since I got confused by this, others would too.

Update NEWS.
2026-07-24 07:03:10 +02:00
p 5a3948de21 sdn-install: work around zsh 5.9.2 regression
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-22 07:06:20 +02:00
p 7d4cb5ec7a Fix the mc.ext.ini parser
Alpine 3.24 Success
Alpine 3.24 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-22 05:45:07 +02:00
p ea694cc633 Don't lose configuration on first run 2026-07-22 05:38:47 +02:00
p 4c4a64bff4 Move pages by visible lines
Alpine 3.23 Success
Alpine 3.23 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-03-23 03:01:14 +01:00
p d1e54fee0d Fix inotify readouts 2026-03-23 00:39:32 +01:00
p d9b1ea079e Bind C-b C-f in the normal mode 2026-03-23 00:28:23 +01:00
p 769be55153 Update README.adoc
Alpine 3.23 Success
Alpine 3.23 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-27 17:23:21 +01:00
p 1dc39f900c Add and integrate sdn-edit
Alpine 3.23 Success
Alpine 3.23 aarch64 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-27 15:35:55 +01:00
p dbea5cb193 CMakeLists.txt: adjust packaging
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Alpine 3.23 aarch64 Success
2026-01-25 12:02:53 +01:00
13 changed files with 694 additions and 115 deletions
+8 -5
View File
@@ -1,6 +1,6 @@
# target_compile_features has been introduced in that version
cmake_minimum_required (VERSION 3.1...3.27)
project (sdn VERSION 1.1 LANGUAGES CXX)
project (sdn VERSION 1.2 LANGUAGES CXX)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
set (CMAKE_CXX_FLAGS
@@ -22,7 +22,7 @@ target_link_directories (${PROJECT_NAME}
PUBLIC ${NCURSESW_LIBRARY_DIRS} ${ACL_LIBRARY_DIRS})
target_link_libraries (${PROJECT_NAME}
PUBLIC ${NCURSESW_LIBRARIES} ${ACL_LIBRARIES})
target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_14)
target_compile_features (${PROJECT_NAME} PUBLIC cxx_std_17)
target_compile_definitions (${PROJECT_NAME} PUBLIC
PROJECT_NAME=\"${PROJECT_NAME}\" PROJECT_VERSION=\"${PROJECT_VERSION}\")
if (MSYS)
@@ -36,9 +36,9 @@ include (GNUInstallDirs)
# sdn-mc-ext should be in libexec, but we prefer it in PATH.
install (TARGETS sdn sdn-mc-ext
DESTINATION ${CMAKE_INSTALL_BINDIR})
install (PROGRAMS sdn-install sdn-open sdn-view
install (PROGRAMS sdn-install sdn-open sdn-view sdn-edit
DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES sdn.1 sdn-install.1 sdn-open.1 sdn-view.1
install (FILES sdn.1 sdn-install.1 sdn-open.1 sdn-view.1 sdn-edit.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
@@ -46,7 +46,7 @@ set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Directory navigator")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_GENERATOR "TGZ")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
@@ -54,5 +54,8 @@ set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_DEBIAN_PACKAGE_RECOMMENDS "mc")
set (CPACK_DEBIAN_PACKAGE_SECTION "utils")
set (CPACK_SET_DESTDIR TRUE)
include (CPack)
+1 -1
View File
@@ -1,6 +1,6 @@
.POSIX:
SHELL = /bin/sh
CXXFLAGS = -g -std=c++14 -Wall -Wextra -Wno-misleading-indentation -pedantic
CXXFLAGS = -g -std=c++17 -Wall -Wextra -Wno-misleading-indentation -pedantic
CPPFLAGS = `sed -ne '/^project (\([^ )]*\) VERSION \([^ )]*\).*/ \
s//-DPROJECT_NAME="\1" -DPROJECT_VERSION="\2"/p' CMakeLists.txt`
+31
View File
@@ -1,3 +1,34 @@
1.2.0 (2026-08-04)
* Added a configurable status bar template, also replacing the "cwd" attribute.
* Added a C-@ binding to compute apparent sizes of selected directories.
* Added ]/[/}/{ bindings to unnest multiple levels of subdirectories
into the listing as relative paths (subdir/file).
* Added C-b and C-f bindings for page up and page down navigation
in normal mode. The respective actions now move by visible lines
rather than by the full height of the terminal.
* Added an sdn-edit script analogous to sdn-view, bound it to F4,
and moved the original key binding to S-F4 (F14/F16).
* Moved the & binding to @, and added . as an alias to M-.
* Renamed "ic" and "dc" keys to "insert" and "delete" respectively.
* Fixed the intended S-F3 binding to work in other terminal types than rxvt.
* Fixed zsh 5.9.2 compatibility in sdn-install.
* Fixed configuration initialisation possibly not working the first time.
* Fixed shell quoting of the tilde character.
* Fixed %view{hex} not working in the mc.ext.ini parser.
1.1.0 (2026-01-10)
* Added selection functionality, and adjusted key bindings:
+29 -19
View File
@@ -2,21 +2,19 @@ sdn
===
:compact-option:
'sdn' is a simple directory navigator that you can invoke while editing shell
commands. It enables you to:
'sdn' is a keyboard-driven file picker and directory navigator for your shell.
* take a quick peek at directory contents without running `ls`;
* select files to insert into the command line;
* browse the filesystem without all the mess that Midnight Commander does:
there's no need to create a subshell in a new pty. The current command line
can be simply forwarded if it is to be edited. What's more, it will always
be obvious whether the navigator is running.
Press Alt-o at any point while composing a command to explore the filesystem
visually. When you return, 'sdn' can change the shell's working directory,
or insert one or more shell-quoted paths directly into the command line,
all without discarding what you were already typing.
'sdn' runs on Linux, all BSD derivatives, and within MSYS2. I wanted to try
a different, simpler approach with this project, and the end result is
very friendly to tinkering.
The high-level goal has been to turn Midnight Commander inside out: unlike with
MC's Ctrl-o escape, it will always be obvious whether the navigator is running.
image::sdn.png[align="center"]
'sdn' runs on Linux, all BSD derivatives, and within MSYS2.
image::sdn.webp[align="center"]
Packages
--------
@@ -79,11 +77,18 @@ that of git, only named colours aren't supported:
cursor 231 202
select 202 bold
bar 16 255 ul
cwd bold
input
cmdline 145
....
The left side of the status bar defaults to the current working directory.
Add a line such as the following to '~/.config/sdn/config' to show
'user@host' with a tilde-shortened path instead:
....
status '%u@%h %B%~%b'
....
Filename colours are taken from the `LS_COLORS` environment variable.
Run `dircolors` to get some defaults.
@@ -99,13 +104,13 @@ normal l enter
Helper programs
~~~~~~~~~~~~~~~
The F3, F13 and F4 keys are normally bound to actions 'view', 'view-raw',
and 'edit', similarly to Norton Commander and other orthodox file managers.
The helper programs used here may be changed by setting the PAGER and VISUAL
(or EDITOR) environment variables.
The F3, S-F3 (F13/F15), F4, S-F4 (F14/F16) keys are normally bound to actions
'view', 'view-raw', 'edit', and 'edit-raw', similarly to Norton Commander
and other orthodox file managers. The helper programs used here may be changed
by setting the PAGER and VISUAL (or EDITOR) environment variables.
If 'view' finds Midnight Commander, it will make use of its configuration
to apply any matching filter, such as to produce archive listings,
If 'view' and 'edit' find Midnight Commander, they will make use of its
configuration to apply any matching filter, such as to produce archive listings,
or it will run the respective command.
While it is mostly possible to get 'mcview' working using an invocation like
@@ -115,6 +120,11 @@ be hacked around in 'mc.ext' by turning on the `nroff` switch for a custom file
extension, just without actually invoking 'nroff'), and thus it can't show the
program help. 'sdn' is currently optimised for 'less' as the pager.
Notes
-----
I wanted to try a different, simpler approach with this project, and the end
result is very friendly to tinkering.
Contributing and Support
------------------------
Use https://git.janouch.name/p/sdn to report any bugs, request features,
Executable
+117
View File
@@ -0,0 +1,117 @@
#!/bin/sh -e
# gen-demo.sh: an automated way of creating an example session recording
#
# Prerequisites:
# - the parent shell has sdn installed,
# - https://git.janouch.name/p/liberty tools/wdye is in PATH.
checkout=/tmp/sdn
if [ -d "$checkout/.git" ]
then
git -C "$checkout" reset --hard
git -C "$checkout" clean -fdx
else
git clone https://git.janouch.name/p/sdn.git "$checkout"
fi
WDYE_LOGGING=1 wdye /dev/stdin <<'LUA'
for k, v in pairs(wdye) do _G[k] = v end
local checkout = "/tmp/sdn"
local shell = spawn {
os.getenv "SHELL", "-c", 'cd -- "$1" && exec "$SHELL"',
"gen-demo.sh", checkout,
environ = {TERM="xterm-256color", SHLVL=false},
}
assert(shell, "failed to spawn the user's shell")
local function pause(seconds)
expect(timeout {seconds},
shell:eof {function () error "shell exited prematurely" end})
end
local function key(sequence)
assert(sequence, "terminal does not define a required key")
shell:send(sequence)
pause(.75)
end
local function tap(sequence)
assert(sequence, "terminal does not define a required key")
shell:send(sequence)
pause(.375)
end
local function type(text)
for i = 1, #text do
shell:send(text:sub(i, i))
if i < #text then pause(.125) end
end
pause(.75)
end
local enter = "\r"
local meta_o = "\27o"
local meta_t = "\27t"
-- Let the shell finish drawing its initial prompt, then open sdn.
pause(.5)
key(meta_o)
-- Toggle hidden entries, unnest one level, calculate directory sizes, nest.
key "."
key "."
key "]"
key "\0"
pause(.25)
key "["
-- Find NEWS through sdn, create ChangeLog -> NEWS, then show the new symlink.
key(meta_o)
type "ln -s "
key(meta_o)
key "s"
type "NE"
key(enter)
type "ChangeLog"
key(enter)
key(meta_o)
-- Exercise both viewers and the editor on a file near the bottom.
tap "G"
tap(shell.term.key_up)
key(shell.term.key_up)
key(shell.term.key_f3)
key "q"
key(shell.term.key_f15)
key "q"
key(shell.term.key_f4)
type ":q"
key(enter)
-- Select all C++ sources in sdn and insert them into a shell command.
key(meta_o)
type "wc -l "
key(meta_o)
key "+"
type "*.cpp"
key(enter)
key "t"
key(enter)
pause(.5)
key(meta_o)
-- Return to sdn, cycle through every sort column, and toggle the full view.
for _ = 1, 6 do tap ">" end
pause(.25)
key(meta_t)
key(meta_t)
key(meta_o)
shell:send "\4"
expect(shell:eof {true},
timeout {5, function () error "shell did not exit after C-d" end})
local status = shell:wait()
assert(status == 0, "shell exited abnormally: " .. status)
LUA
Executable
+48
View File
@@ -0,0 +1,48 @@
#!/bin/sh -e
# sdn-edit: an editor for sdn that makes use of Midnight Commander configuration
# to make more kinds of files directly editable
if [ "$#" -ne 1 ]
then
echo "Usage: $0 FILE" >&2
exit 2
fi
# This handles both MC_DATADIR and odd installation locations.
datadir=
if command -v mc >/dev/null
then datadir=$(mc --datadir | sed 's/ (.*)$//')
fi
config=
for dir in "$HOME"/.config/mc "$datadir" /etc/mc
do
if [ -n "$dir" -a -f "$dir/mc.ext.ini" ]
then
config=$dir/mc.ext.ini
break
fi
done
# This is often used in %env{} expansion, so let's be on the same page.
export PAGER=${PAGER:-less}
export MC_EXT_FILENAME=$(realpath "$1")
export MC_EXT_BASENAME=$(basename "$1")
export MC_EXT_CURRENTDIR=$(dirname "$MC_EXT_FILENAME")
output=$(sdn-mc-ext <"$config" "$(file -Lbz "$1")" \
"$MC_EXT_FILENAME" "$MC_EXT_BASENAME" "$MC_EXT_CURRENTDIR" Edit || :)
kind=$(echo "$output" | sed -n 1p)
command=$(echo "$output" | sed -n 2p)
case "$kind" in
'')
if [ -n "$command" ]
then eval "$command"
else "${VISUAL:-${EDITOR:-vi}}" -- "$MC_EXT_FILENAME"
fi
;;
*)
echo "Unsupported: $kind" >&2
exit 1
esac
+24
View File
@@ -0,0 +1,24 @@
.Dd January 27, 2026
.Dt SDN-EDIT 1
.Os
.Sh NAME
.Nm sdn-edit
.Nd run Midnight Commander edit configuration externally
.Sh SYNOPSIS
.Nm sdn-edit
.Ar path
.Sh DESCRIPTION
.Nm
invokes
.Ev VISUAL ,
.Ev EDITOR
or a fallback editor on the passed filename.
.Pp
If it succeeds in finding a
.Xr mc 1
.Pa mc.ext.ini
file, it will first process it, and run the respective command.
.Sh REPORTING BUGS
Use
.Lk https://git.janouch.name/p/sdn
to report bugs, request features, or submit pull requests.
+4 -2
View File
@@ -5,7 +5,7 @@
zsh() {
cat <<'EOF'
sdn-navigate () {
# optionally: zle zle-line-finish
# Optionally: zle zle-line-finish
while eval "`SDN=1 sdn "$BUFFER" "$CURSOR"`"
do
[ -z "$cd" ] || cd "$cd"
@@ -20,8 +20,10 @@ sdn-navigate () {
fc -R =(print -- "$helper")
/bin/sh -c "$helper" </dev/tty || break
(:) # Workaround for zsh 5.9.2 regression with gio open (7f494139fe95)
done
# optionally: zle zle-line-init
# Optionally: zle zle-line-init
zle reset-prompt
}
+2 -3
View File
@@ -45,14 +45,13 @@ unordered_map<string, unordered_map<string, string>> sections;
fun expand_command (string command) -> pair<string, string> {
regex re_sequence {R"(%(%|[[:alpha:]]*\{([^}]*)\}|[[:alpha:]]+))"};
regex re_name {R"([^{}]*)"};
regex re_parameter {R"([^,]+")"};
regex re_parameter {R"([^,]+)"};
string kind, out, pipe; smatch m;
while (regex_search (command, m, re_sequence)) {
out.append (m.prefix ());
auto seq = m.str (1);
auto seq = m.str (1), argument = m.str (2);
command = m.suffix ();
string argument = m.str (2);
if (regex_search (seq, m, re_name))
seq = m.str ();
+63 -5
View File
@@ -57,6 +57,28 @@ However it also enforces any pending change to the shell's working directory.
The zero-based index of the
.Ql full-view
column that entries are ordered by.
.It status Em string
Template for the left side of the status bar.
The following escapes are supported:
.Bl -tag -width 7n -compact
.It %H
Full hostname.
.It %h
Hostname up to the first dot.
.It %u
Username.
.It %d
Current working directory.
.It %~
Current working directory with
.Ev HOME
replaced by a tilde.
.It \&%B , \&%b
Turn bold on and off for subsequent text.
.It %%
A literal percent sign.
.El
Unknown sequences are reproduced literally.
.El
.Sh ENVIRONMENT
.Bl -tag -width 15n
@@ -68,14 +90,32 @@ 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 and F13 key bindings as well as
to show the internal help message.
If none is set, it defaults to
The viewer program to be launched by the
.Ic view
and
.Ic view-raw
actions, normally bound to F3 and S-F3 (F13/F15) respectively,
as well as to show the internal help message.
If not set, it defaults to
.Xr less 1 .
.It Ev VISUAL , Ev EDITOR
The editor program to be launched by the F4 key binding.
The editor program to be launched by the
.Ic edit
and
.Ic edit-raw
actions, normally bound to F4 and S-F4 (F14/F16) respectively.
If neither variable is set, it defaults to
.Xr vi 1 .
.It Ev SDN_OPENER , Ev SDN_VIEWER , Ev SDN_EDITOR
Overrides for the programs used by the
.Ic open , Ic view ,
and
.Ic edit
actions before they try
.Xr sdn-open 1 , Xr sdn-view 1 ,
or
.Xr sdn-edit 1
respectively.
.El
.Sh FILES
.Bl -tag -width 25n -compact
@@ -87,6 +127,14 @@ Custom key binding overrides.
Redefine terminal attributes for UI elements.
.El
.Sh EXAMPLES
.Ss Pa config
The left side of the status bar defaults to the current working directory.
To show, e.g.,
.Ar user@host
followed by a bold tilde-shortened path:
.Bd -literal -offset indent
status '%u@%h %B%~%b'
.Ed
.Ss Pa bindings
Key names or combinations follow the Emacs syntax for Control and Meta prefixes
and
@@ -122,10 +170,20 @@ following may work:
cursor 231 202
select 202 bold
bar 16 255 ul
cwd bold
input
cmdline 145
.Ed
.Sh SEE ALSO
.Xr dircolors 1 ,
.Xr git-config 1 ,
.Xr less 1 ,
.Xr sdn-edit 1 ,
.Xr sdn-install 1 ,
.Xr sdn-open 1 ,
.Xr sdn-view 1 ,
.Xr vi 1 ,
.Xr dir_colors 5 ,
.Xr terminfo 5
.Sh REPORTING BUGS
Use
.Lk https://git.janouch.name/p/sdn
+367 -80
View File
@@ -25,6 +25,7 @@
#include <cwchar>
#include <fstream>
#include <iostream>
#include <limits>
#include <locale>
#include <map>
#include <memory>
@@ -106,13 +107,6 @@ fun to_mb (const wstring &wide) -> string {
return mb;
}
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;
}
// TODO: this omits empty elements, check usages
fun split (const string &s, const string &sep, vector<string> &out) {
size_t mark = 0, p = s.find (sep);
@@ -149,11 +143,35 @@ fun untilde (const string &path) -> string {
return path;
}
fun tildeify (const string &path) -> string {
const char *home = getenv ("HOME");
if (!home) {
if (const auto *pw = getpwuid (getuid ()))
home = pw->pw_dir;
}
if (home && *home) {
size_t len = strlen (home);
if (!path.compare (0, len, home) && strchr ("/", path[len]))
return "~" + path.substr (len);
}
return path;
}
fun get_user () -> const char * {
if (auto user = getenv ("USER"))
return user;
if (auto user = getenv ("LOGNAME"))
return user;
if (const auto *pw = getpwuid (getuid ()))
return pw->pw_name;
return "";
}
fun needs_shell_quoting (const string &v) -> bool {
// IEEE Std 1003.1 sh + the exclamation mark because of csh/bash
// history expansion, implicitly also the NUL character
for (auto c : v)
if (strchr ("|&;<>()$`\\\"' \t\n" "*?[#˜=%" "!", c))
if (strchr ("|&;<>()$`\\\"' \t\n" "*?[#~=%" "!", c))
return true;
return v.empty ();
}
@@ -297,7 +315,9 @@ fun xdg_config_write (const string &suffix) -> unique_ptr<fstream> {
auto dir = xdg_config_home ();
if (dir[0] == '/') {
auto path = dir + "/" PROJECT_NAME "/" + suffix;
if (!fork ())
if (auto child = fork ())
(void) waitpid (child, nullptr, 0);
else
_exit (-execlp ("mkdir", "mkdir", "-p",
dirname (strdup (path.c_str ())), NULL));
auto fs = make_unique<fstream>
@@ -440,13 +460,14 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
XX(ENTER) XX(OPEN) XX(CHOOSE) XX(CHOOSE_FULL) \
XX(VIEW_RAW) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \
XX(SELECT) XX(DESELECT) XX(SELECT_TOGGLE) XX(SELECT_ABORT) \
XX(VIEW) XX(VIEW_RAW) XX(EDIT) XX(EDIT_RAW) XX(SORT_LEFT) XX(SORT_RIGHT) \
XX(SELECT) XX(DESELECT) XX(SELECT_TOGGLE) XX(SELECT_ABORT) XX(DIR_SIZES) \
XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \
XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \
XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) \
XX(NEST) XX(UNNEST) XX(NEST_ALL) XX(UNNEST_ALL) XX(REDRAW) XX(RELOAD) \
XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) XX(INPUT_DELETE) \
XX(INPUT_B_KILL_WORD) XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) \
XX(INPUT_QUOTED_INSERT) \
@@ -465,29 +486,35 @@ static map<Key, action> g_normal_actions {
{ALT | '\r', ACTION_OPEN}, {ALT | KEY (ENTER), ACTION_OPEN},
{'t', ACTION_CHOOSE}, {'T', ACTION_CHOOSE_FULL},
{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
{KEY (F (3)), ACTION_VIEW}, {KEY (F (13)), ACTION_VIEW_RAW},
{KEY (F (4)), ACTION_EDIT},
{KEY (F (3)), ACTION_VIEW}, {KEY (F (15)), ACTION_VIEW_RAW},
{KEY (F (4)), ACTION_EDIT}, {KEY (F (16)), ACTION_EDIT_RAW},
{KEY (F (13)), ACTION_VIEW_RAW}, // Normally S-F1, but rxvt S-F3
{KEY (F (14)), ACTION_EDIT_RAW}, // Normally S-F2, but rxvt S-F4
{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
// M-o ought to be the same shortcut the navigator is launched with
{ALT | 'o', ACTION_QUIT}, {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
{'+', ACTION_SELECT}, {'-', ACTION_DESELECT},
{CTRL ('T'), ACTION_SELECT_TOGGLE}, {KEY (IC), ACTION_SELECT_TOGGLE},
{27, ACTION_SELECT_ABORT}, {CTRL ('G'), ACTION_SELECT_ABORT},
{CTRL ('@'), ACTION_DIR_SIZES},
{'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
{'G', ACTION_BOTTOM}, {ALT | '>', ACTION_BOTTOM}, {KEY(END), ACTION_BOTTOM},
{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
{CTRL ('B'), ACTION_PAGE_PREVIOUS}, {CTRL ('F'), 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_GO_START}, {'~', ACTION_GO_HOME},
{'/', 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},
{ALT | 't', ACTION_TOGGLE_FULL},
{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
{ALT | 't', ACTION_TOGGLE_FULL}, {'R', ACTION_REVERSE_SORT},
{ALT | '.', ACTION_SHOW_HIDDEN}, {'.', ACTION_SHOW_HIDDEN},
{']', ACTION_UNNEST}, {'[', ACTION_NEST},
{'}', ACTION_UNNEST_ALL}, {'{', ACTION_NEST_ALL},
{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
};
static map<Key, action> g_input_actions {
@@ -538,9 +565,16 @@ struct stringcaseless {
}
};
struct longerstringsfirst {
bool operator () (const string &a, const string &b) const {
return make_pair (b.size (), a) < make_pair (a.size (), b);
}
};
struct entry {
string filename, target_path;
struct stat info = {}, target_info = {};
off_t size = -1; ///< Recursively computed directory size
enum { MODES, USER, GROUP, SIZE, MTIME, FILENAME, COLUMNS };
ncstring cols[COLUMNS];
@@ -555,6 +589,7 @@ struct level {
static struct {
ncstring cmdline; ///< Outer command line
string cwd; ///< Current working directory
wstring status = L"%d"; ///< Status bar template
string start_dir; ///< Starting directory
vector<entry> entries; ///< Current directory entries
set<string> selection; ///< Filenames of selected entries
@@ -565,6 +600,7 @@ static struct {
bool reverse_sort; ///< Reverse sort
bool show_hidden; ///< Show hidden files
bool ext_helpers; ///< Launch helpers externally
int unnest_depth; ///< Nested listing depth (-1 = all)
int max_widths[entry::COLUMNS]; ///< Column widths
int sort_column = entry::FILENAME; ///< Sorting column
int sort_flash_ttl; ///< Sorting column flash TTL
@@ -588,11 +624,11 @@ static struct {
void (*editor_on_change) (); ///< Callback on editor change
map<action, void (*) ()> editor_on; ///< Handlers for custom actions
enum { AT_CURSOR, AT_SELECT, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE,
enum { AT_CURSOR, AT_SELECT, AT_BAR, AT_INPUT, AT_INFO, AT_CMDLINE,
AT_COUNT };
chtype attrs[AT_COUNT] = {A_REVERSE, A_BOLD, 0, A_BOLD, 0, A_ITALIC, 0};
chtype attrs[AT_COUNT] = {A_REVERSE, A_BOLD, 0, 0, A_ITALIC, 0};
const char *attr_names[AT_COUNT] =
{"cursor", "select", "bar", "cwd", "input", "info", "cmdline"};
{"cursor", "select", "bar", "input", "info", "cmdline"};
map<int, chtype> ls_colors; ///< LS_COLORS decoded
map<string, chtype> ls_exts; ///< LS_COLORS file extensions
@@ -673,8 +709,10 @@ fun ls_format (const entry &e, bool for_target) -> chtype {
if (x != g.ls_colors.end ())
format = x->second;
auto dot = name.find_last_of ('.');
if (dot != string::npos && type == LS_FILE) {
auto slash = name.rfind ('/');
auto base = slash == string::npos ? 0 : slash + 1;
auto dot = name.rfind ('.');
if (dot != string::npos && dot >= base && type == LS_FILE) {
const auto x = g.ls_exts.find (name.substr (++dot));
if (x != g.ls_exts.end ())
format = x->second;
@@ -701,15 +739,25 @@ fun suffixize (off_t size, unsigned shift, wchar_t suffix, std::wstring &out)
return false;
}
fun make_entry (const struct dirent *f) -> entry {
fun format_size (off_t value, bool dirsize) -> ncstring {
std::wstring size;
if (!suffixize (value, 40, L'T', size) &&
!suffixize (value, 30, L'G', size) &&
!suffixize (value, 20, L'M', size) &&
!suffixize (value, 10, L'K', size))
size = to_wstring (value);
return apply_attrs (size, dirsize ? A_BOLD : 0);
}
fun make_entry (const string &path, unsigned char d_type) -> entry {
entry e;
e.filename = f->d_name;
e.info.st_mode = DTTOIF (f->d_type);
e.filename = path;
e.info.st_mode = DTTOIF (d_type);
auto &info = e.info;
// 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)) {
if (lstat (path.c_str (), &info)) {
e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode),
L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0);
@@ -723,13 +771,20 @@ fun make_entry (const struct dirent *f) -> entry {
if (S_ISLNK (info.st_mode)) {
char buf[PATH_MAX] = {};
auto len = readlink (f->d_name, buf, sizeof buf);
auto len = readlink (path.c_str (), buf, sizeof buf);
if (len < 0 || size_t (len) >= sizeof buf) {
e.target_path = "?";
} else {
e.target_path = buf;
// Resolve relative targets against the symlink's directory
string target = buf;
if (target[0] != '/') {
if (auto slash = path.rfind ('/'); slash != string::npos)
target = path.substr (0, slash + 1) + target;
}
// If a symlink links to another symlink, we follow all the way
(void) stat (buf, &e.target_info);
(void) stat (target.c_str (), &e.target_info);
}
}
@@ -738,7 +793,7 @@ fun make_entry (const struct dirent *f) -> entry {
// 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)
if (acl_extended_file_nofollow (path.c_str ()) > 0)
mode += L"+";
#endif
e.cols[entry::MODES] = apply_attrs (mode, 0);
@@ -753,13 +808,7 @@ fun make_entry (const struct dirent *f) -> entry {
? apply_attrs (grp->second, 0)
: apply_attrs (to_wstring (info.st_gid), 0);
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);
e.cols[entry::SIZE] = format_size (info.st_size, false);
wchar_t buf[32] = L"";
auto tm = localtime (&info.st_mtime);
@@ -776,6 +825,34 @@ fun make_entry (const struct dirent *f) -> entry {
return e;
}
fun format_status (const wstring &tpl) -> ncstring {
char hostname[256] = "";
(void) gethostname (hostname, sizeof hostname);
string host_short = hostname;
if (auto dot = host_short.find ('.'); dot != string::npos)
host_short.resize (dot);
ncstring s; chtype attrs = 0;
for (size_t i = 0; i < tpl.size (); i++) {
if (tpl[i] != L'%' || i + 1 >= tpl.size ()) {
s += apply_attrs (tpl.substr (i, 1), attrs);
continue;
}
switch (tpl[++i]) {
case L'%': s += apply_attrs (L"%", attrs); break;
case L'H': s += apply_attrs (to_wide (hostname), attrs); break;
case L'h': s += apply_attrs (to_wide (host_short), attrs); break;
case L'u': s += apply_attrs (to_wide (get_user ()), attrs); break;
case L'd': s += apply_attrs (to_wide (g.cwd), attrs); break;
case L'~': s += apply_attrs (to_wide (tildeify (g.cwd)), attrs); break;
case L'B': attrs |= A_BOLD; break;
case L'b': attrs &= ~A_BOLD; break;
default: s += apply_attrs (tpl.substr (i - 1, 2), attrs);
}
}
return s;
}
fun inline visible_lines () -> int { return max (0, LINES - 2); }
fun update () {
@@ -799,7 +876,7 @@ fun update () {
move (g.gravity ? (available - used + i) : i, 0);
auto used = 0;
auto width = 0;
for (int col = start_column; col < entry::COLUMNS; col++) {
const auto &field = g.entries[index].cols[col];
auto aligned = align (field, alignment[col] * g.max_widths[col]);
@@ -807,12 +884,16 @@ fun update () {
for_each (begin (aligned), end (aligned), decolor);
if (g.sort_flash_ttl && col == g.sort_column)
for_each (begin (aligned), end (aligned), invert);
used += print (aligned + apply_attrs (L" ", 0), COLS - used);
width += print (aligned + apply_attrs (L" ", 0), COLS - width);
}
hline (' ', COLS - used);
hline (' ', COLS - width);
}
auto bar = apply_attrs (to_wide (g.cwd), g.attrs[g.AT_CWD]);
auto bar = format_status (g.status);
if (g.unnest_depth < 0)
bar += apply_attrs (L" [/*]", 0);
else if (g.unnest_depth > 0)
bar += apply_attrs (L" [/" + to_wstring (g.unnest_depth) + L"]", 0);
if (!g.show_hidden)
bar += apply_attrs (L" (hidden)", 0);
if (g.out_of_date)
@@ -857,8 +938,11 @@ fun update () {
} else if (!g.selection.empty ()) {
uint64_t size = 0;
for (const auto &e : g.entries)
if (g.selection.count (e.filename)
&& S_ISREG (e.info.st_mode) && e.info.st_size > 0)
if (!g.selection.count (e.filename))
continue;
else if (e.size >= 0)
size += e.size;
else if (S_ISREG (e.info.st_mode) && e.info.st_size > 0)
size += e.info.st_size;
wostringstream status;
@@ -898,9 +982,13 @@ fun operator< (const entry &e1, const entry &e2) -> bool {
return a.info.st_gid < b.info.st_gid;
break;
case entry::SIZE:
if (a.info.st_size != b.info.st_size)
return a.info.st_size < b.info.st_size;
{
auto size_a = a.size >= 0 ? a.size : a.info.st_size;
auto size_b = b.size >= 0 ? b.size : b.info.st_size;
if (size_a != size_b)
return size_a < size_b;
break;
}
case entry::MTIME:
if (a.info.st_mtime != b.info.st_mtime)
return a.info.st_mtime < b.info.st_mtime;
@@ -922,8 +1010,35 @@ fun focus (const string &anchor) {
}
}
fun resort_emit (map<string, vector<entry>> &children, const string &parent)
-> void {
auto &kids = children[parent];
sort (begin (kids), end (kids));
for (auto &e : kids) {
auto name = e.filename;
g.entries.push_back (std::move (e));
if (children.count (name))
resort_emit (children, name);
}
}
fun resort_hierarchical () {
map<string, vector<entry>> children;
for (auto &e : g.entries) {
if (auto slash = e.filename.rfind ('/'); slash == string::npos)
children[""].push_back (std::move (e));
else
children[e.filename.substr (0, slash)].push_back (std::move (e));
}
g.entries.clear ();
resort_emit (children, "");
}
fun resort (const string anchor = at_cursor ().filename) {
sort (begin (g.entries), end (g.entries));
if (g.unnest_depth)
resort_hierarchical ();
else
sort (begin (g.entries), end (g.entries));
focus (anchor);
}
@@ -941,6 +1056,47 @@ fun filter_selection (const set<string> &selection) {
return reselection;
}
fun collect_subentries (const string &prefix, int depth) {
auto dir = opendir (prefix.c_str ());
if (!dir)
return;
while (auto f = readdir (dir)) {
string name = f->d_name;
if (name == "." || name == ".." || (name[0] == '.' && !g.show_hidden))
continue;
string path = prefix + "/" + name;
g.entries.push_back (make_entry (path, f->d_type));
if (depth && S_ISDIR (g.entries.back ().info.st_mode))
collect_subentries (path, depth - (depth > 0));
}
closedir (dir);
}
fun collect_entries (int depth) {
auto dir = opendir (".");
if (!dir) {
show_message (strerror (errno));
if (g.cwd != "/")
g.entries.push_back (make_entry ("..", DT_DIR));
return;
}
while (auto f = readdir (dir)) {
string name = f->d_name;
// Two dots are for navigation but this ain't as useful
if (name == ".")
continue;
if (name == ".." ? g.cwd == "/" : (name[0] == '.' && !g.show_hidden))
continue;
g.entries.push_back (make_entry (name, f->d_type));
if (depth && name != ".." && S_ISDIR (g.entries.back ().info.st_mode))
collect_subentries (name, depth - (depth > 0));
}
closedir (dir);
}
fun reload (bool keep_anchor) {
g.unames.clear ();
while (auto *ent = getpwent ())
@@ -957,31 +1113,11 @@ fun reload (bool keep_anchor) {
anchor = at_cursor ().filename;
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
if (name == ".")
continue;
if (name == ".." ? g.cwd != "/" : (name[0] != '.' || g.show_hidden))
g.entries.push_back (make_entry (f));
}
closedir (dir);
collect_entries (g.unnest_depth);
g.selection = filter_selection (g.selection);
readfail:
g.out_of_date = false;
for (int col = 0; col < entry::COLUMNS; col++) {
auto &longest = g.max_widths[col] = 0;
@@ -998,9 +1134,10 @@ readfail:
if (g.watch_wd != -1)
inotify_rm_watch (g.watch_fd, g.watch_wd);
// We don't show atime, so access and open are merely spam
// We don't show atime, so access and read-only open/close are merely spam
g.watch_wd = inotify_add_watch (g.watch_fd, ".",
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) &
~(IN_ACCESS | IN_OPEN | IN_CLOSE_NOWRITE));
#elif !defined __CYGWIN__
if (g.watch_wd != -1)
close (g.watch_wd);
@@ -1015,6 +1152,81 @@ readfail:
#endif
}
// This feature interacts a bit oddly with unnesting.
// At least let's use a dynamic algorithm of starting depth-first
// and reusing these values higher up.
using size_map = map<string, off_t, longerstringsfirst>;
fun apparent_size (const string &path, const size_map &cache) -> off_t {
if (auto i = cache.find (path); i != cache.end () && i->second >= 0)
return i->second;
struct stat info {};
if (lstat (path.c_str (), &info))
return -1;
if (!S_ISDIR (info.st_mode))
return info.st_size;
auto dir = opendir (path.c_str ());
if (!dir)
return -1;
off_t total = 0;
while (auto f = readdir (dir)) {
string name = f->d_name;
if (name == "." || name == "..")
continue;
// The most likely error is a lack of privileges.
if (off_t child = apparent_size (path + "/" + name, cache); child >= 0)
total += min (child, numeric_limits<off_t>::max () - total);
}
closedir (dir);
return total;
}
fun compute_dir_sizes () {
show_message ("Computing directory sizes", 0);
update ();
size_map sizes;
if (!g.selection.empty ()) {
for (const auto &e : g.entries)
if (S_ISDIR (e.info.st_mode) && g.selection.count (e.filename))
sizes.emplace (e.filename, -1);
} else {
// Midnight Commander cannot place .. in the selection,
// but under the cursor it counts all direct subdirectories.
const auto &cursor = at_cursor ();
if (cursor.filename == "..") {
for (const auto &e : g.entries)
if (S_ISDIR (e.info.st_mode))
sizes.emplace (e.filename, -1);
} else if (!cursor.filename.empty ()) {
sizes.emplace (cursor.filename, -1);
}
}
sizes.erase ("..");
errno = 0;
for (auto &[path, size] : sizes)
size = apparent_size (path, sizes);
if (errno)
show_message (strerror (errno));
else
g.message.clear ();
auto &longest = g.max_widths[entry::SIZE] = 0;
for (auto &e : g.entries) {
auto size = sizes.find (e.filename);
if (size != sizes.end () && size->second >= 0)
e.cols[entry::SIZE] = format_size ((e.size = size->second), true);
longest = max (longest, compute_width (e.cols[entry::SIZE]));
}
if (g.sort_column == entry::SIZE)
resort ();
}
fun run_program (initializer_list<const char *> list, const string &filename) {
auto args = (!filename.empty () && filename.front () == '-' ? " -- " : " ")
+ shell_escape (filename);
@@ -1081,9 +1293,16 @@ fun sdn_view (const string &filename) {
(const char *) getenv ("PAGER"), "less", "cat"}, filename);
}
fun edit (const string &filename) {
run_program ({(const char *) getenv ("VISUAL"),
(const char *) getenv ("EDITOR"), "vi"}, filename);
fun edit_raw (const string &filename) {
run_program ({
(const char *) getenv ("VISUAL"), (const char *) getenv ("EDITOR"),
"vi"}, filename);
}
fun sdn_edit (const string &filename) {
run_program ({(const char *) getenv ("SDN_EDITOR"), "sdn-edit",
(const char *) getenv ("VISUAL"), (const char *) getenv ("EDITOR"),
"vi"}, filename);
}
fun run_pager (FILE *contents) {
@@ -1187,8 +1406,10 @@ fun select_matches (bool dotdot) -> set<string> {
for (const auto &e : g.entries) {
if (!dotdot && e.filename == "..")
continue;
if (!fnmatch (to_mb (g.editor_line).c_str (),
e.filename.c_str (), FNM_PATHNAME))
auto base = e.filename.c_str ();
if (auto slash = e.filename.rfind ('/'); slash != string::npos)
base += slash + 1;
if (!fnmatch (to_mb (g.editor_line).c_str (), base, FNM_PATHNAME))
matches.insert (e.filename);
}
return matches;
@@ -1200,11 +1421,19 @@ fun select_interactive (bool dotdot) {
/// Stays on the current item unless there are better matches
fun lookup (const wstring &needle) {
// An exact match must score better than a mere prefix match, thus <= size
auto eval = [&] (const wstring &s) -> size_t {
size_t i = 0;
while (i <= needle.size () && i <= s.size () && needle[i] == s[i])
i++;
return i;
};
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);
size_t n = eval (to_wide (g.entries[o].filename));
if (n > best_n) {
best = o;
best_n = n;
@@ -1344,8 +1573,10 @@ fun change_dir (const string &path) {
level last {g.offset, g.cursor, g.cwd, at_cursor ().filename, g.selection};
g.cwd = full_path;
bool same_path = last.path == g.cwd;
if (!same_path)
if (!same_path) {
g.unnest_depth = 0;
g.selection.clear ();
}
reload (same_path);
@@ -1399,7 +1630,7 @@ fun enter (const entry &entry) {
// Dive into directories and accessible symlinks to them
if (!S_ISDIR (entry.info.st_mode)
&& !S_ISDIR (entry.target_info.st_mode)) {
// This could rather launch ${SDN_OPEN:-xdg-open} or something
// This could rather launch ${SDN_OPENER:-xdg-open} or something
choose (entry, false);
} else {
change_dir (entry.filename);
@@ -1550,7 +1781,10 @@ fun handle (Key k) -> bool {
(is_directory ? change_dir : sdn_view) (current.filename);
break;
case ACTION_EDIT:
edit (current.filename);
sdn_edit (current.filename);
break;
case ACTION_EDIT_RAW:
edit_raw (current.filename);
break;
case ACTION_HELP:
show_help ();
@@ -1599,6 +1833,9 @@ fun handle (Key k) -> bool {
case ACTION_SELECT_ABORT:
g.selection.clear ();
break;
case ACTION_DIR_SIZES:
compute_dir_sizes ();
break;
case ACTION_UP:
g.cursor--;
@@ -1625,10 +1862,10 @@ fun handle (Key k) -> bool {
break;
case ACTION_PAGE_PREVIOUS:
g.cursor -= LINES;
g.cursor -= visible_lines ();
break;
case ACTION_PAGE_NEXT:
g.cursor += LINES;
g.cursor += visible_lines ();
break;
case ACTION_SCROLL_DOWN:
g.offset++;
@@ -1703,6 +1940,30 @@ fun handle (Key k) -> bool {
g.show_hidden = !g.show_hidden;
reload (true);
break;
case ACTION_NEST:
if (g.unnest_depth == 0)
break;
g.unnest_depth = max (0, g.unnest_depth - 1);
reload (true);
break;
case ACTION_UNNEST:
if (g.unnest_depth < 0)
break;
g.unnest_depth++;
reload (true);
break;
case ACTION_NEST_ALL:
if (!g.unnest_depth)
break;
g.unnest_depth = 0;
reload (true);
break;
case ACTION_UNNEST_ALL:
if (g.unnest_depth < 0)
break;
g.unnest_depth = -1;
reload (true);
break;
case ACTION_REDRAW:
clear ();
break;
@@ -1727,7 +1988,7 @@ fun watch_check () {
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;
e = (const inotify_event *) ptr;
if (e->wd == g.watch_wd)
changed = true;
}
@@ -1864,6 +2125,19 @@ fun read_key (Key &k) -> bool {
}
k = c;
// rxvt keeps F11, F12 ambiguous with S-F1, S-F2, and there is a matching
// overlap in terminfo function keys, shifting many of them to the left.
// Let's normalize this while preferring the former two interpretations.
//
// We still bind to S-F1, S-F2 as well as the proper S-F3, S-F4
// in g_normal_actions because of screen/tmux used in rxvt.
//
// If this has made you confused, welcome to the club.
char *term = termname ();
if (term && !strncmp (term, "rxvt", 4)
&& k > KEY_F (12) && k <= KEY_F (44))
k += 2 * (1 + (k > KEY_F (34)));
wint_t metafied{};
if (k == 27 && (res = get_wch (&metafied)) != ERR)
k = ALT | metafied;
@@ -1930,6 +2204,10 @@ fun load_bindings () {
if (*name != '(' && *name != ')')
filtered += *name;
}
if (filtered == "IC")
filtered = "insert";
if (filtered == "DC")
filtered = "delete";
learn_named_key (filtered, SYM | kc);
}
@@ -2012,6 +2290,10 @@ fun load_config () {
g.ext_helpers = tokens.at (1) == "1";
else if (tokens.front () == "sort-column" && tokens.size () > 1)
g.sort_column = stoi (tokens.at (1));
else if (tokens.front () == "unnest-depth" && tokens.size () > 1)
g.unnest_depth = stoi (tokens.at (1));
else if (tokens.front () == "status" && tokens.size () > 1)
g.status = to_wide (tokens.at (1));
else if (tokens.front () == "history")
load_history_level (tokens);
}
@@ -2029,6 +2311,8 @@ fun save_config () {
write_line (*config, {"ext-helpers", g.ext_helpers ? "1" : "0"});
write_line (*config, {"sort-column", to_string (g.sort_column)});
write_line (*config, {"unnest-depth", to_string (g.unnest_depth)});
write_line (*config, {"status", to_mb (g.status)});
char hostname[256];
if (gethostname (hostname, sizeof hostname))
@@ -2100,6 +2384,9 @@ int main (int argc, char *argv[]) {
load_colors ();
load_cmdline (argc, argv);
g.start_dir = g.cwd = initial_cwd ();
// XXX: We might want to store this within history levels.
if (g.levels.empty () || g.levels.back ().path != g.cwd)
g.unnest_depth = 0;
reload (false);
pop_levels (g.cwd);
update ();
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB