Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8556ea1588
|
|||
|
338572806b
|
|||
|
60fcff0285
|
|||
|
f33795329e
|
|||
|
85416db143
|
|||
|
c3662b0f13
|
|||
|
f7f85cb08a
|
|||
|
aa4be26520
|
|||
|
73093cac74
|
|||
|
9873a06761
|
|||
|
2ac93c0e0e
|
|||
|
28dcbd8a2c
|
|||
|
b2701d93bd
|
|||
|
b7d516c66f
|
|||
|
d6e0b9bc01
|
|||
|
f3f0bc3d33
|
|||
|
5a3948de21
|
|||
|
7d4cb5ec7a
|
|||
|
ea694cc633
|
|||
|
4c4a64bff4
|
|||
|
d1e54fee0d
|
|||
|
d9b1ea079e
|
|||
|
769be55153
|
|||
|
1dc39f900c
|
|||
|
dbea5cb193
|
+8
-5
@@ -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,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`
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 ();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ();
|
||||
|
||||
Reference in New Issue
Block a user