Compare commits
5 Commits
b070df6010
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
16ef3f9e47
|
|||
|
3e39cc5660
|
|||
|
977d1a7120
|
|||
|
9b274417c5
|
|||
|
cc14a9f735
|
@@ -33,9 +33,9 @@ include (GNUInstallDirs)
|
|||||||
# sdn-mc-ext should be in libexec, but we prefer it in PATH.
|
# sdn-mc-ext should be in libexec, but we prefer it in PATH.
|
||||||
install (TARGETS sdn sdn-mc-ext
|
install (TARGETS sdn sdn-mc-ext
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR})
|
DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
install (PROGRAMS sdn-install sdn-view
|
install (PROGRAMS sdn-install sdn-open sdn-view
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR})
|
DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
install (FILES sdn.1 sdn-install.1 sdn-view.1
|
install (FILES sdn.1 sdn-install.1 sdn-open.1 sdn-view.1
|
||||||
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
Copyright (c) 2017 - 2025, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
|||||||
11
NEWS
11
NEWS
@@ -1,10 +1,21 @@
|
|||||||
Unreleased
|
Unreleased
|
||||||
|
|
||||||
|
* Added selection functionality, and adjusted key bindings:
|
||||||
|
- C-t or Insert toggle whether the current item is selected;
|
||||||
|
- + and - adjust the selection using shell globs;
|
||||||
|
- t and T insert the selection into the external command line
|
||||||
|
in relative or absolute form, respectively;
|
||||||
|
- Enter is like t but enters directories;
|
||||||
|
- C-g or Escape clear the selection, similarly to the editor.
|
||||||
|
|
||||||
* Added an sdn-view script that can process Midnight Commander mc.ext.ini files
|
* Added an sdn-view script that can process Midnight Commander mc.ext.ini files
|
||||||
and apply matching filters; this script has been made the default F3 binding,
|
and apply matching filters; this script has been made the default F3 binding,
|
||||||
while the original direct pager invocation has been moved to F13 (which also
|
while the original direct pager invocation has been moved to F13 (which also
|
||||||
reflects Midnight Commander)
|
reflects Midnight Commander)
|
||||||
|
|
||||||
|
* Added an sdn-open script which does the same kind of processing as above
|
||||||
|
on top of xdg-open. This is what is now executed by M-Enter.
|
||||||
|
|
||||||
|
|
||||||
1.0.0 (2024-12-21)
|
1.0.0 (2024-12-21)
|
||||||
|
|
||||||
|
|||||||
18
README.adoc
18
README.adoc
@@ -5,7 +5,8 @@ sdn
|
|||||||
'sdn' is a simple directory navigator that you can invoke while editing shell
|
'sdn' is a simple directory navigator that you can invoke while editing shell
|
||||||
commands. It enables you to:
|
commands. It enables you to:
|
||||||
|
|
||||||
* take a quick peek at directory contents without running `ls`
|
* take a quick peek at directory contents without running `ls`;
|
||||||
|
* select files to insert into the command line;
|
||||||
* browse the filesystem without all the mess that Midnight Commander does:
|
* browse the filesystem without all the mess that Midnight Commander does:
|
||||||
there's no need to create a subshell in a new pty. The current command line
|
there's no need to create a subshell in a new pty. The current command line
|
||||||
can be simply forwarded if it is to be edited. What's more, it will always
|
can be simply forwarded if it is to be edited. What's more, it will always
|
||||||
@@ -75,6 +76,7 @@ that of git, only named colours aren't supported:
|
|||||||
|
|
||||||
....
|
....
|
||||||
cursor 231 202
|
cursor 231 202
|
||||||
|
select 202 bold
|
||||||
bar 16 255 ul
|
bar 16 255 ul
|
||||||
cwd bold
|
cwd bold
|
||||||
input
|
input
|
||||||
@@ -91,15 +93,19 @@ To obtain more vifm-like controls, you may write the following to your
|
|||||||
|
|
||||||
....
|
....
|
||||||
normal h parent
|
normal h parent
|
||||||
normal l choose
|
normal l enter
|
||||||
....
|
....
|
||||||
|
|
||||||
Helper programs
|
Helper programs
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
The F3 and F4 keys are normally bound to actions 'view' and 'edit', similarly to
|
The F3, F13 and F4 keys are normally bound to actions 'view', 'view-raw',
|
||||||
Norton Commander and other orthodox file managers. The helper programs used
|
and 'edit', similarly to Norton Commander and other orthodox file managers.
|
||||||
here may be changed by setting the PAGER and VISUAL (or EDITOR) environment
|
The helper programs used here may be changed by setting the PAGER and VISUAL
|
||||||
variables.
|
(or EDITOR) environment variables.
|
||||||
|
|
||||||
|
If 'view' finds Midnight Commander, it will make use of its configuration
|
||||||
|
to apply any matching filter, such as to produce archive listings,
|
||||||
|
or it will run the respective command.
|
||||||
|
|
||||||
While it is mostly possible to get 'mcview' working using an invocation like
|
While it is mostly possible to get 'mcview' working using an invocation like
|
||||||
`PAGER='mcview -u' sdn`, beware that this helper cannot read files from its
|
`PAGER='mcview -u' sdn`, beware that this helper cannot read files from its
|
||||||
|
|||||||
72
sdn-open
Executable file
72
sdn-open
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
# sdn-open: an opener for sdn that makes use of Midnight Commander configuration
|
||||||
|
# to make more kinds of files directly openable
|
||||||
|
|
||||||
|
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" Open || :)
|
||||||
|
kind=$(echo "$output" | sed -n 1p)
|
||||||
|
command=$(echo "$output" | sed -n 2p)
|
||||||
|
|
||||||
|
# We're trying to retain any explicit user preferences while navigating through:
|
||||||
|
# - Debian-based systems have /etc/alternatives/open as /usr/bin/open,
|
||||||
|
# pointing to either /usr/bin/xdg-open or /usr/bin/run-mailcap;
|
||||||
|
# - macOS has /usr/bin/open, and typically no xdg-open;
|
||||||
|
# - Windows Subsystem for Linux has explorer.exe in PATH,
|
||||||
|
# where launched applications may have problems with UNC paths,
|
||||||
|
# and possibly also xdg-open that may not be capable of opening much.
|
||||||
|
#
|
||||||
|
# Both macOS open and Windows explorer.exe are capable of opening files,
|
||||||
|
# directories, as well as URLs through native associations.
|
||||||
|
if [ -n "$MC_XDG_OPEN" ]
|
||||||
|
then :
|
||||||
|
elif command -v explorer.exe >/dev/null
|
||||||
|
then export MC_XDG_OPEN=explorer.exe
|
||||||
|
elif command -v open >/dev/null
|
||||||
|
then export MC_XDG_OPEN=open
|
||||||
|
elif command -v xdg-open >/dev/null
|
||||||
|
then export MC_XDG_OPEN=xdg-open
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$kind" in
|
||||||
|
cd)
|
||||||
|
# These mostly enter virtual filesystems, which we do not understand.
|
||||||
|
"$MC_XDG_OPEN" "$MC_EXT_FILENAME"
|
||||||
|
;;
|
||||||
|
'')
|
||||||
|
if [ -n "$command" ]
|
||||||
|
then eval "$command"
|
||||||
|
else "$MC_XDG_OPEN" "$MC_EXT_FILENAME"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported: $kind" >&2
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
23
sdn-open.1
Normal file
23
sdn-open.1
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.Dd November 20, 2025
|
||||||
|
.Dt SDN-OPEN 1
|
||||||
|
.Os
|
||||||
|
.Sh NAME
|
||||||
|
.Nm sdn-open
|
||||||
|
.Nd run Midnight Commander open configuration externally
|
||||||
|
.Sh SYNOPSIS
|
||||||
|
.Nm sdn-open
|
||||||
|
.Ar path
|
||||||
|
.Sh DESCRIPTION
|
||||||
|
.Nm
|
||||||
|
invokes
|
||||||
|
.Xr xdg-open 1
|
||||||
|
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 any matching command instead.
|
||||||
|
.Sh REPORTING BUGS
|
||||||
|
Use
|
||||||
|
.Lk https://git.janouch.name/p/sdn
|
||||||
|
to report bugs, request features, or submit pull requests.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.Dd December 28, 2024
|
.Dd December 30, 2024
|
||||||
.Dt SDN-VIEW 1
|
.Dt SDN-VIEW 1
|
||||||
.Os
|
.Os
|
||||||
.Sh NAME
|
.Sh NAME
|
||||||
@@ -16,7 +16,8 @@ or a fallback pager on the passed filename.
|
|||||||
If it succeeds in finding a
|
If it succeeds in finding a
|
||||||
.Xr mc 1
|
.Xr mc 1
|
||||||
.Pa mc.ext.ini
|
.Pa mc.ext.ini
|
||||||
file, it will first process it, and apply any matching filter.
|
file, it will first process it, and apply any matching filter,
|
||||||
|
or run the respective command.
|
||||||
.Sh REPORTING BUGS
|
.Sh REPORTING BUGS
|
||||||
Use
|
Use
|
||||||
.Lk https://git.janouch.name/p/sdn
|
.Lk https://git.janouch.name/p/sdn
|
||||||
|
|||||||
7
sdn.1
7
sdn.1
@@ -1,5 +1,5 @@
|
|||||||
\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
|
\" https://mandoc.bsd.lv/man/roff.7.html#Sentence_Spacing
|
||||||
.Dd October 27, 2020
|
.Dd December 30, 2024
|
||||||
.Dt SDN 1
|
.Dt SDN 1
|
||||||
.Os
|
.Os
|
||||||
.Sh NAME
|
.Sh NAME
|
||||||
@@ -95,7 +95,7 @@ names are used for special keys.
|
|||||||
To obtain more vifm-like controls and Windows-like quit abilities:
|
To obtain more vifm-like controls and Windows-like quit abilities:
|
||||||
.Bd -literal -offset indent
|
.Bd -literal -offset indent
|
||||||
normal h parent
|
normal h parent
|
||||||
normal l choose
|
normal l enter
|
||||||
normal M-f4 quit
|
normal M-f4 quit
|
||||||
.Ed
|
.Ed
|
||||||
.Pp
|
.Pp
|
||||||
@@ -107,7 +107,7 @@ For rxvt, that would be:
|
|||||||
define C-ppage ^[[5^
|
define C-ppage ^[[5^
|
||||||
define C-npage ^[[6^
|
define C-npage ^[[6^
|
||||||
normal C-ppage parent
|
normal C-ppage parent
|
||||||
normal C-npage choose
|
normal C-npage enter
|
||||||
.Ed
|
.Ed
|
||||||
.Pp
|
.Pp
|
||||||
Escape characters must be inserted verbatim, e.g., by pressing C-v ESC in vi,
|
Escape characters must be inserted verbatim, e.g., by pressing C-v ESC in vi,
|
||||||
@@ -120,6 +120,7 @@ For a black-on-white terminal supporting 256 colours, a theme such as the
|
|||||||
following may work:
|
following may work:
|
||||||
.Bd -literal -offset indent
|
.Bd -literal -offset indent
|
||||||
cursor 231 202
|
cursor 231 202
|
||||||
|
select 202 bold
|
||||||
bar 16 255 ul
|
bar 16 255 ul
|
||||||
cwd bold
|
cwd bold
|
||||||
input
|
input
|
||||||
|
|||||||
220
sdn.cpp
220
sdn.cpp
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// sdn: simple directory navigator
|
// sdn: simple directory navigator
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2017 - 2025, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
#include <locale>
|
#include <locale>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -428,8 +430,9 @@ enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
|
|||||||
#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
|
#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
|
||||||
|
|
||||||
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
|
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
|
||||||
XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW_RAW) XX(VIEW) XX(EDIT) \
|
XX(ENTER) XX(OPEN) XX(CHOOSE) XX(CHOOSE_FULL) \
|
||||||
XX(SORT_LEFT) XX(SORT_RIGHT) \
|
XX(VIEW_RAW) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \
|
||||||
|
XX(SELECT) XX(DESELECT) XX(SELECT_TOGGLE) XX(SELECT_ABORT) \
|
||||||
XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \
|
XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \
|
||||||
XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \
|
XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \
|
||||||
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
|
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
|
||||||
@@ -449,15 +452,18 @@ static const char *g_action_names[] = {ACTIONS(XX)};
|
|||||||
#undef XX
|
#undef XX
|
||||||
|
|
||||||
static map<wint_t, action> g_normal_actions {
|
static map<wint_t, action> g_normal_actions {
|
||||||
{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
|
{'\r', ACTION_ENTER}, {KEY (ENTER), ACTION_ENTER},
|
||||||
{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
|
{ALT | '\r', ACTION_OPEN}, {ALT | KEY (ENTER), ACTION_OPEN},
|
||||||
|
{'t', ACTION_CHOOSE}, {'T', ACTION_CHOOSE_FULL},
|
||||||
{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
|
{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
|
||||||
{KEY (F (3)), ACTION_VIEW}, {KEY (F (13)), ACTION_VIEW_RAW},
|
{KEY (F (3)), ACTION_VIEW}, {KEY (F (13)), ACTION_VIEW_RAW},
|
||||||
{KEY (F (4)), ACTION_EDIT},
|
{KEY (F (4)), ACTION_EDIT},
|
||||||
{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
|
{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
|
||||||
// M-o ought to be the same shortcut the navigator is launched with
|
// M-o ought to be the same shortcut the navigator is launched with
|
||||||
{ALT | 'o', ACTION_QUIT},
|
{ALT | 'o', ACTION_QUIT}, {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
|
||||||
{'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
|
{'+', ACTION_SELECT}, {'-', ACTION_DESELECT},
|
||||||
|
{CTRL ('T'), ACTION_SELECT_TOGGLE}, {KEY (IC), ACTION_SELECT_TOGGLE},
|
||||||
|
{27, ACTION_SELECT_ABORT}, {CTRL ('G'), ACTION_SELECT_ABORT},
|
||||||
{'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
|
{'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
|
||||||
{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
|
{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
|
||||||
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
|
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
|
||||||
@@ -471,7 +477,7 @@ static map<wint_t, action> g_normal_actions {
|
|||||||
{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
|
{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
|
||||||
{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
|
{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
|
||||||
{KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR},
|
{KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR},
|
||||||
{'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL},
|
{ALT | 't', ACTION_TOGGLE_FULL},
|
||||||
{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
|
{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
|
||||||
{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
|
{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
|
||||||
};
|
};
|
||||||
@@ -493,6 +499,7 @@ static map<wint_t, action> g_input_actions {
|
|||||||
static map<wint_t, action> g_search_actions {
|
static map<wint_t, action> g_search_actions {
|
||||||
{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
|
{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
|
||||||
{CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
|
{CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
|
||||||
|
{'/', ACTION_ENTER},
|
||||||
};
|
};
|
||||||
static const map<string, map<wint_t, action>*> g_binding_contexts {
|
static const map<string, map<wint_t, action>*> g_binding_contexts {
|
||||||
{"normal", &g_normal_actions}, {"input", &g_input_actions},
|
{"normal", &g_normal_actions}, {"input", &g_input_actions},
|
||||||
@@ -533,6 +540,7 @@ struct entry {
|
|||||||
struct level {
|
struct level {
|
||||||
int offset, cursor; ///< Scroll offset and cursor position
|
int offset, cursor; ///< Scroll offset and cursor position
|
||||||
string path, filename; ///< Level path and filename at cursor
|
string path, filename; ///< Level path and filename at cursor
|
||||||
|
set<string> selection; ///< Filenames of selected entries
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct {
|
static struct {
|
||||||
@@ -540,6 +548,7 @@ static struct {
|
|||||||
string cwd; ///< Current working directory
|
string cwd; ///< Current working directory
|
||||||
string start_dir; ///< Starting directory
|
string start_dir; ///< Starting directory
|
||||||
vector<entry> entries; ///< Current directory entries
|
vector<entry> entries; ///< Current directory entries
|
||||||
|
set<string> selection; ///< Filenames of selected entries
|
||||||
vector<level> levels; ///< Upper directory levels
|
vector<level> levels; ///< Upper directory levels
|
||||||
int offset, cursor; ///< Scroll offset and cursor position
|
int offset, cursor; ///< Scroll offset and cursor position
|
||||||
bool full_view; ///< Show extended information
|
bool full_view; ///< Show extended information
|
||||||
@@ -554,7 +563,7 @@ static struct {
|
|||||||
wstring message; ///< Message for the user
|
wstring message; ///< Message for the user
|
||||||
int message_ttl; ///< Time to live for the message
|
int message_ttl; ///< Time to live for the message
|
||||||
|
|
||||||
string chosen; ///< Chosen item for the command line
|
vector<string> chosen; ///< Chosen items for the command line
|
||||||
string ext_helper; ///< External helper to run
|
string ext_helper; ///< External helper to run
|
||||||
bool no_chdir; ///< Do not tell the shell to chdir
|
bool no_chdir; ///< Do not tell the shell to chdir
|
||||||
bool quitting; ///< Whether we should quit already
|
bool quitting; ///< Whether we should quit already
|
||||||
@@ -570,10 +579,11 @@ static struct {
|
|||||||
void (*editor_on_change) (); ///< Callback on editor change
|
void (*editor_on_change) (); ///< Callback on editor change
|
||||||
map<action, void (*) ()> editor_on; ///< Handlers for custom actions
|
map<action, void (*) ()> editor_on; ///< Handlers for custom actions
|
||||||
|
|
||||||
enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE, AT_COUNT };
|
enum { AT_CURSOR, AT_SELECT, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE,
|
||||||
chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0};
|
AT_COUNT };
|
||||||
|
chtype attrs[AT_COUNT] = {A_REVERSE, A_BOLD, 0, A_BOLD, 0, A_ITALIC, 0};
|
||||||
const char *attr_names[AT_COUNT] =
|
const char *attr_names[AT_COUNT] =
|
||||||
{"cursor", "bar", "cwd", "input", "info", "cmdline"};
|
{"cursor", "select", "bar", "cwd", "input", "info", "cmdline"};
|
||||||
|
|
||||||
map<int, chtype> ls_colors; ///< LS_COLORS decoded
|
map<int, chtype> ls_colors; ///< LS_COLORS decoded
|
||||||
map<string, chtype> ls_exts; ///< LS_COLORS file extensions
|
map<string, chtype> ls_exts; ///< LS_COLORS file extensions
|
||||||
@@ -769,18 +779,25 @@ fun update () {
|
|||||||
int used = min (available, all - g.offset);
|
int used = min (available, all - g.offset);
|
||||||
for (int i = 0; i < used; i++) {
|
for (int i = 0; i < used; i++) {
|
||||||
auto index = g.offset + i;
|
auto index = g.offset + i;
|
||||||
bool selected = index == g.cursor;
|
bool cursored = index == g.cursor;
|
||||||
attrset (selected ? g.attrs[g.AT_CURSOR] : 0);
|
bool selected = g.selection.count (g.entries[index].filename);
|
||||||
|
chtype attrs {};
|
||||||
|
if (selected)
|
||||||
|
attrs = g.attrs[g.AT_SELECT];
|
||||||
|
if (cursored)
|
||||||
|
attrs = g.attrs[g.AT_CURSOR] | (attrs & ~A_COLOR);
|
||||||
|
attrset (attrs);
|
||||||
|
|
||||||
move (g.gravity ? (available - used + i) : i, 0);
|
move (g.gravity ? (available - used + i) : i, 0);
|
||||||
|
|
||||||
auto used = 0;
|
auto used = 0;
|
||||||
for (int col = start_column; col < entry::COLUMNS; col++) {
|
for (int col = start_column; col < entry::COLUMNS; col++) {
|
||||||
const auto &field = g.entries[index].cols[col];
|
const auto &field = g.entries[index].cols[col];
|
||||||
auto aligned = align (field, alignment[col] * g.max_widths[col]);
|
auto aligned = align (field, alignment[col] * g.max_widths[col]);
|
||||||
|
if (cursored || selected)
|
||||||
|
for_each (begin (aligned), end (aligned), decolor);
|
||||||
if (g.sort_flash_ttl && col == g.sort_column)
|
if (g.sort_flash_ttl && col == g.sort_column)
|
||||||
for_each (begin (aligned), end (aligned), invert);
|
for_each (begin (aligned), end (aligned), invert);
|
||||||
if (selected)
|
|
||||||
for_each (begin (aligned), end (aligned), decolor);
|
|
||||||
used += print (aligned + apply_attrs (L" ", 0), COLS - used);
|
used += print (aligned + apply_attrs (L" ", 0), COLS - used);
|
||||||
}
|
}
|
||||||
hline (' ', COLS - used);
|
hline (' ', COLS - used);
|
||||||
@@ -828,6 +845,17 @@ fun update () {
|
|||||||
} else if (!g.message.empty ()) {
|
} else if (!g.message.empty ()) {
|
||||||
move (LINES - 1, 0);
|
move (LINES - 1, 0);
|
||||||
print (apply_attrs (g.message, 0), COLS);
|
print (apply_attrs (g.message, 0), COLS);
|
||||||
|
} else if (!g.selection.empty ()) {
|
||||||
|
uint64_t size = 0;
|
||||||
|
for (const auto &e : g.entries)
|
||||||
|
if (g.selection.count (e.filename)
|
||||||
|
&& S_ISREG (e.info.st_mode) && e.info.st_size > 0)
|
||||||
|
size += e.info.st_size;
|
||||||
|
|
||||||
|
wostringstream status;
|
||||||
|
status << size << L" bytes in " << g.selection.size () << L" items";
|
||||||
|
move (LINES - 1, 0);
|
||||||
|
print (apply_attrs (status.str (), g.attrs[g.AT_SELECT]), COLS);
|
||||||
} else if (!g.cmdline.empty ()) {
|
} else if (!g.cmdline.empty ()) {
|
||||||
move (LINES - 1, 0);
|
move (LINES - 1, 0);
|
||||||
print (g.cmdline, COLS);
|
print (g.cmdline, COLS);
|
||||||
@@ -895,6 +923,15 @@ fun show_message (const string &message, int ttl = 30) {
|
|||||||
g.message_ttl = ttl;
|
g.message_ttl = ttl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun filter_selection (const set<string> &selection) {
|
||||||
|
set<string> reselection;
|
||||||
|
if (!selection.empty ())
|
||||||
|
for (const auto &e : g.entries)
|
||||||
|
if (selection.count (e.filename))
|
||||||
|
reselection.insert (e.filename);
|
||||||
|
return reselection;
|
||||||
|
}
|
||||||
|
|
||||||
fun reload (bool keep_anchor) {
|
fun reload (bool keep_anchor) {
|
||||||
g.unames.clear ();
|
g.unames.clear ();
|
||||||
while (auto *ent = getpwent ())
|
while (auto *ent = getpwent ())
|
||||||
@@ -933,6 +970,8 @@ fun reload (bool keep_anchor) {
|
|||||||
}
|
}
|
||||||
closedir (dir);
|
closedir (dir);
|
||||||
|
|
||||||
|
g.selection = filter_selection (g.selection);
|
||||||
|
|
||||||
readfail:
|
readfail:
|
||||||
g.out_of_date = false;
|
g.out_of_date = false;
|
||||||
for (int col = 0; col < entry::COLUMNS; col++) {
|
for (int col = 0; col < entry::COLUMNS; col++) {
|
||||||
@@ -968,7 +1007,7 @@ readfail:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun run_program (initializer_list<const char *> list, const string &filename) {
|
fun run_program (initializer_list<const char *> list, const string &filename) {
|
||||||
auto args = (!filename.empty() && filename.front() == '-' ? " -- " : " ")
|
auto args = (!filename.empty () && filename.front () == '-' ? " -- " : " ")
|
||||||
+ shell_escape (filename);
|
+ shell_escape (filename);
|
||||||
if (g.ext_helpers) {
|
if (g.ext_helpers) {
|
||||||
// XXX: this doesn't try them all out,
|
// XXX: this doesn't try them all out,
|
||||||
@@ -1017,13 +1056,18 @@ fun run_program (initializer_list<const char *> list, const string &filename) {
|
|||||||
update ();
|
update ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sdn_open (const string &filename) {
|
||||||
|
run_program ({(const char *) getenv ("SDN_OPENER"), "sdn-open", "xdg-open"},
|
||||||
|
filename);
|
||||||
|
}
|
||||||
|
|
||||||
fun view_raw (const string &filename) {
|
fun view_raw (const string &filename) {
|
||||||
// XXX: we cannot realistically detect that the pager hasn't made a pause
|
// XXX: we cannot realistically detect that the pager hasn't made a pause
|
||||||
// at the end of the file, so we can't ensure all contents have been seen
|
// at the end of the file, so we can't ensure all contents have been seen
|
||||||
run_program ({(const char *) getenv ("PAGER"), "less", "cat"}, filename);
|
run_program ({(const char *) getenv ("PAGER"), "less", "cat"}, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun view (const string &filename) {
|
fun sdn_view (const string &filename) {
|
||||||
run_program ({(const char *) getenv ("SDN_VIEWER"), "sdn-view",
|
run_program ({(const char *) getenv ("SDN_VIEWER"), "sdn-view",
|
||||||
(const char *) getenv ("PAGER"), "less", "cat"}, filename);
|
(const char *) getenv ("PAGER"), "less", "cat"}, filename);
|
||||||
}
|
}
|
||||||
@@ -1099,6 +1143,17 @@ fun show_help () {
|
|||||||
fclose (contents);
|
fclose (contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun matches_to_editor_info (int matches) {
|
||||||
|
if (g.editor_line.empty ())
|
||||||
|
g.editor_info.clear ();
|
||||||
|
else if (matches == 0)
|
||||||
|
g.editor_info = L"(no match)";
|
||||||
|
else if (matches == 1)
|
||||||
|
g.editor_info = L"(1 match)";
|
||||||
|
else
|
||||||
|
g.editor_info = L"(" + to_wstring (matches) + L" matches)";
|
||||||
|
}
|
||||||
|
|
||||||
fun match (const wstring &needle, int push) -> int {
|
fun match (const wstring &needle, int push) -> int {
|
||||||
string pattern = to_mb (needle) + "*";
|
string pattern = to_mb (needle) + "*";
|
||||||
bool jump_to_first = push || fnmatch (pattern.c_str (),
|
bool jump_to_first = push || fnmatch (pattern.c_str (),
|
||||||
@@ -1115,15 +1170,23 @@ fun match (const wstring &needle, int push) -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun match_interactive (int push) {
|
fun match_interactive (int push) {
|
||||||
int matches = match (g.editor_line, push);
|
matches_to_editor_info (match (g.editor_line, push));
|
||||||
if (g.editor_line.empty ())
|
}
|
||||||
g.editor_info.clear ();
|
|
||||||
else if (matches == 0)
|
fun select_matches (bool dotdot) -> set<string> {
|
||||||
g.editor_info = L"(no match)";
|
set<string> matches;
|
||||||
else if (matches == 1)
|
for (const auto &e : g.entries) {
|
||||||
g.editor_info = L"(1 match)";
|
if (!dotdot && e.filename == "..")
|
||||||
else
|
continue;
|
||||||
g.editor_info = L"(" + to_wstring (matches) + L" matches)";
|
if (!fnmatch (to_mb (g.editor_line).c_str (),
|
||||||
|
e.filename.c_str (), FNM_PATHNAME))
|
||||||
|
matches.insert (e.filename);
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun select_interactive (bool dotdot) {
|
||||||
|
matches_to_editor_info (select_matches (dotdot).size ());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stays on the current item unless there are better matches
|
/// Stays on the current item unless there are better matches
|
||||||
@@ -1184,6 +1247,7 @@ fun pop_levels (const string &old_cwd) {
|
|||||||
g.offset = i->offset;
|
g.offset = i->offset;
|
||||||
g.cursor = i->cursor;
|
g.cursor = i->cursor;
|
||||||
anchor = i->filename;
|
anchor = i->filename;
|
||||||
|
g.selection = filter_selection (i->selection);
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
g.levels.pop_back ();
|
g.levels.pop_back ();
|
||||||
@@ -1268,9 +1332,12 @@ fun change_dir (const string &path) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
level last {g.offset, g.cursor, g.cwd, at_cursor ().filename};
|
level last {g.offset, g.cursor, g.cwd, at_cursor ().filename, g.selection};
|
||||||
g.cwd = full_path;
|
g.cwd = full_path;
|
||||||
bool same_path = last.path == g.cwd;
|
bool same_path = last.path == g.cwd;
|
||||||
|
if (!same_path)
|
||||||
|
g.selection.clear ();
|
||||||
|
|
||||||
reload (same_path);
|
reload (same_path);
|
||||||
|
|
||||||
if (!same_path) {
|
if (!same_path) {
|
||||||
@@ -1308,12 +1375,23 @@ fun initial_cwd () -> string {
|
|||||||
return ok ? pwd : cwd;
|
return ok ? pwd : cwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun choose (const entry &entry) {
|
fun choose (const entry &entry, bool full) {
|
||||||
|
if (g.selection.empty ())
|
||||||
|
g.selection.insert (entry.filename);
|
||||||
|
for (const string &item : g.selection)
|
||||||
|
g.chosen.push_back (full ? absolutize (g.cwd, item) : item);
|
||||||
|
|
||||||
|
g.selection.clear ();
|
||||||
|
g.no_chdir = full;
|
||||||
|
g.quitting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enter (const entry &entry) {
|
||||||
// Dive into directories and accessible symlinks to them
|
// Dive into directories and accessible symlinks to them
|
||||||
if (!S_ISDIR (entry.info.st_mode)
|
if (!S_ISDIR (entry.info.st_mode)
|
||||||
&& !S_ISDIR (entry.target_info.st_mode)) {
|
&& !S_ISDIR (entry.target_info.st_mode)) {
|
||||||
g.chosen = entry.filename;
|
// This could rather launch ${SDN_OPEN:-xdg-open} or something
|
||||||
g.quitting = true;
|
choose (entry, false);
|
||||||
} else {
|
} else {
|
||||||
change_dir (entry.filename);
|
change_dir (entry.filename);
|
||||||
}
|
}
|
||||||
@@ -1444,20 +1522,23 @@ fun handle (wint_t c) -> bool {
|
|||||||
auto i = g_normal_actions.find (c);
|
auto i = g_normal_actions.find (c);
|
||||||
switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
|
switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
|
||||||
case ACTION_CHOOSE_FULL:
|
case ACTION_CHOOSE_FULL:
|
||||||
// FIXME: in the root directory, this inserts //item
|
choose (current, true);
|
||||||
g.chosen = g.cwd + "/" + current.filename;
|
|
||||||
g.no_chdir = true;
|
|
||||||
g.quitting = true;
|
|
||||||
break;
|
break;
|
||||||
case ACTION_CHOOSE:
|
case ACTION_CHOOSE:
|
||||||
choose (current);
|
choose (current, false);
|
||||||
|
break;
|
||||||
|
case ACTION_ENTER:
|
||||||
|
enter (current);
|
||||||
|
break;
|
||||||
|
case ACTION_OPEN:
|
||||||
|
sdn_open (current.filename);
|
||||||
break;
|
break;
|
||||||
case ACTION_VIEW_RAW:
|
case ACTION_VIEW_RAW:
|
||||||
// Mimic mc, it does not seem sensible to page directories
|
// Mimic mc, it does not seem sensible to page directories
|
||||||
(is_directory ? change_dir : view_raw) (current.filename);
|
(is_directory ? change_dir : view_raw) (current.filename);
|
||||||
break;
|
break;
|
||||||
case ACTION_VIEW:
|
case ACTION_VIEW:
|
||||||
(is_directory ? change_dir : view) (current.filename);
|
(is_directory ? change_dir : sdn_view) (current.filename);
|
||||||
break;
|
break;
|
||||||
case ACTION_EDIT:
|
case ACTION_EDIT:
|
||||||
edit (current.filename);
|
edit (current.filename);
|
||||||
@@ -1483,6 +1564,33 @@ fun handle (wint_t c) -> bool {
|
|||||||
resort ();
|
resort ();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ACTION_SELECT:
|
||||||
|
g.editor = L"select";
|
||||||
|
g.editor_on_change = [] { select_interactive (false); };
|
||||||
|
g.editor_on[ACTION_INPUT_CONFIRM] = [] {
|
||||||
|
auto matches = select_matches (false);
|
||||||
|
g.selection.insert (begin (matches), end (matches));
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case ACTION_DESELECT:
|
||||||
|
g.editor = L"deselect";
|
||||||
|
g.editor_on_change = [] { select_interactive (true); };
|
||||||
|
g.editor_on[ACTION_INPUT_CONFIRM] = [] {
|
||||||
|
for (const auto &match : select_matches (true))
|
||||||
|
g.selection.erase (match);
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case ACTION_SELECT_TOGGLE:
|
||||||
|
if (g.selection.count (current.filename))
|
||||||
|
g.selection.erase (current.filename);
|
||||||
|
else
|
||||||
|
g.selection.insert (current.filename);
|
||||||
|
g.cursor++;
|
||||||
|
break;
|
||||||
|
case ACTION_SELECT_ABORT:
|
||||||
|
g.selection.clear ();
|
||||||
|
break;
|
||||||
|
|
||||||
case ACTION_UP:
|
case ACTION_UP:
|
||||||
g.cursor--;
|
g.cursor--;
|
||||||
break;
|
break;
|
||||||
@@ -1544,7 +1652,12 @@ fun handle (wint_t c) -> bool {
|
|||||||
g.editor_on_change = [] { match_interactive (0); };
|
g.editor_on_change = [] { match_interactive (0); };
|
||||||
g.editor_on[ACTION_UP] = [] { match_interactive (-1); };
|
g.editor_on[ACTION_UP] = [] { match_interactive (-1); };
|
||||||
g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); };
|
g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); };
|
||||||
g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); };
|
g.editor_on[ACTION_INPUT_CONFIRM] = [] { enter (at_cursor ()); };
|
||||||
|
g.editor_on[ACTION_ENTER] = [] {
|
||||||
|
enter (at_cursor ());
|
||||||
|
g.editor_line.clear ();
|
||||||
|
g.editor_cursor = 0;
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
case ACTION_RENAME_PREFILL:
|
case ACTION_RENAME_PREFILL:
|
||||||
g.editor_line = to_wide (current.filename);
|
g.editor_line = to_wide (current.filename);
|
||||||
@@ -1858,10 +1971,11 @@ fun load_bindings () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun load_history_level (const vector<string> &v) {
|
fun load_history_level (const vector<string> &v) {
|
||||||
if (v.size () != 7)
|
if (v.size () < 7)
|
||||||
return;
|
return;
|
||||||
// Not checking the hostname and parent PID right now since we can't merge
|
// Not checking the hostname and parent PID right now since we can't merge
|
||||||
g.levels.push_back ({stoi (v.at (4)), stoi (v.at (5)), v.at (3), v.at (6)});
|
g.levels.push_back ({stoi (v.at (4)), stoi (v.at (5)), v.at (3), v.at (6),
|
||||||
|
set<string> (begin (v) + 7, end (v))});
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load_config () {
|
fun load_config () {
|
||||||
@@ -1909,12 +2023,16 @@ fun save_config () {
|
|||||||
*hostname = 0;
|
*hostname = 0;
|
||||||
|
|
||||||
auto ppid = std::to_string (getppid ());
|
auto ppid = std::to_string (getppid ());
|
||||||
for (auto i = g.levels.begin (); i != g.levels.end (); i++)
|
for (auto i = g.levels.begin (); i != g.levels.end (); i++) {
|
||||||
write_line (*config, {"history", hostname, ppid, i->path,
|
vector<string> line {"history", hostname, ppid, i->path,
|
||||||
to_string (i->offset), to_string (i->cursor), i->filename});
|
to_string (i->offset), to_string (i->cursor), i->filename};
|
||||||
write_line (*config, {"history", hostname, ppid, g.cwd,
|
line.insert (end (line), begin (i->selection), end (i->selection));
|
||||||
to_string (g.offset), to_string (g.cursor),
|
write_line (*config, line);
|
||||||
at_cursor ().filename});
|
}
|
||||||
|
vector<string> line {"history", hostname, ppid, g.cwd,
|
||||||
|
to_string (g.offset), to_string (g.cursor), at_cursor ().filename};
|
||||||
|
line.insert (end (line), begin (g.selection), end (g.selection));
|
||||||
|
write_line (*config, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main (int argc, char *argv[]) {
|
int main (int argc, char *argv[]) {
|
||||||
@@ -1997,8 +2115,12 @@ int main (int argc, char *argv[]) {
|
|||||||
save_config ();
|
save_config ();
|
||||||
|
|
||||||
// Presumably it is going to end up as an argument, so quote it
|
// Presumably it is going to end up as an argument, so quote it
|
||||||
if (!g.chosen.empty ())
|
string chosen;
|
||||||
g.chosen = shell_escape (g.chosen);
|
for (const auto &item : g.chosen) {
|
||||||
|
if (!chosen.empty ())
|
||||||
|
chosen += ' ';
|
||||||
|
chosen += shell_escape (item);
|
||||||
|
}
|
||||||
|
|
||||||
// We can't portably create a standard stream from an FD, so modify the FD
|
// We can't portably create a standard stream from an FD, so modify the FD
|
||||||
dup2 (output_fd, STDOUT_FILENO);
|
dup2 (output_fd, STDOUT_FILENO);
|
||||||
@@ -2009,7 +2131,7 @@ int main (int argc, char *argv[]) {
|
|||||||
else
|
else
|
||||||
cout << "local cd=" << endl;
|
cout << "local cd=" << endl;
|
||||||
|
|
||||||
cout << "local insert=" << shell_escape (g.chosen) << endl;
|
cout << "local insert=" << shell_escape (chosen) << endl;
|
||||||
cout << "local helper=" << shell_escape (g.ext_helper) << endl;
|
cout << "local helper=" << shell_escape (g.ext_helper) << endl;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user