2017-06-29 02:50:39 +02:00
|
|
|
|
//
|
|
|
|
|
// sdn: simple directory navigator
|
|
|
|
|
//
|
2022-09-03 12:11:18 +02:00
|
|
|
|
// Copyright (c) 2017 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
2017-06-29 02:50:39 +02:00
|
|
|
|
//
|
|
|
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
2018-06-22 15:35:58 +02:00
|
|
|
|
// purpose with or without fee is hereby granted.
|
2017-06-29 02:50:39 +02:00
|
|
|
|
//
|
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
|
|
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
|
|
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
|
|
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
|
//
|
|
|
|
|
|
2017-07-17 23:11:13 +02:00
|
|
|
|
// May be required for ncursesw and we generally want it all anyway
|
|
|
|
|
#define _XOPEN_SOURCE_EXTENDED
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <climits>
|
2017-06-30 21:29:10 +02:00
|
|
|
|
#include <cstdlib>
|
2018-10-25 23:00:55 +02:00
|
|
|
|
#include <cstring>
|
2021-11-05 21:30:51 +01:00
|
|
|
|
#include <cwchar>
|
2017-06-30 21:29:10 +02:00
|
|
|
|
#include <fstream>
|
2021-11-05 21:30:51 +01:00
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <locale>
|
2017-07-15 02:57:58 +02:00
|
|
|
|
#include <map>
|
2018-11-03 15:23:31 +01:00
|
|
|
|
#include <memory>
|
2021-11-05 21:30:51 +01:00
|
|
|
|
#include <string>
|
|
|
|
|
#include <tuple>
|
|
|
|
|
#include <vector>
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
|
|
|
|
#include <dirent.h>
|
|
|
|
|
#include <fcntl.h>
|
2022-09-03 12:11:18 +02:00
|
|
|
|
#include <fnmatch.h>
|
2017-06-30 01:39:49 +02:00
|
|
|
|
#include <grp.h>
|
2018-11-02 15:37:56 +01:00
|
|
|
|
#include <libgen.h>
|
2021-11-05 21:30:51 +01:00
|
|
|
|
#include <pwd.h>
|
2020-10-29 03:23:32 +01:00
|
|
|
|
#include <signal.h>
|
2021-11-05 21:30:51 +01:00
|
|
|
|
#include <sys/acl.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
#include <unistd.h>
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
2021-11-05 21:30:51 +01:00
|
|
|
|
#include <acl/libacl.h>
|
|
|
|
|
#include <ncurses.h>
|
2017-07-15 02:57:58 +02:00
|
|
|
|
#include <sys/inotify.h>
|
2018-11-01 15:54:32 +01:00
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <sys/wait.h>
|
2021-11-05 21:30:51 +01:00
|
|
|
|
#include <sys/xattr.h>
|
2017-07-14 21:49:32 +02:00
|
|
|
|
|
2021-10-18 11:23:17 +02:00
|
|
|
|
// To implement cbreak() with disabled ^S that gets reënabled on endwin()
|
|
|
|
|
#define NCURSES_INTERNALS
|
|
|
|
|
#include <term.h>
|
|
|
|
|
#undef CTRL // term.h -> termios.h -> sys/ttydefaults.h, too simplistic
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
// Unicode is complex enough already and we might make assumptions
|
|
|
|
|
#ifndef __STDC_ISO_10646__
|
|
|
|
|
#error Unicode required for wchar_t
|
|
|
|
|
#endif
|
|
|
|
|
|
2017-06-30 01:39:49 +02:00
|
|
|
|
// Trailing return types make C++ syntax suck considerably less
|
|
|
|
|
#define fun static auto
|
|
|
|
|
|
2017-06-30 21:29:10 +02:00
|
|
|
|
#ifndef A_ITALIC
|
|
|
|
|
#define A_ITALIC 0
|
|
|
|
|
#endif
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
using namespace std;
|
|
|
|
|
|
|
|
|
|
// For some reason handling of encoding in C and C++ is extremely annoying
|
|
|
|
|
// and C++17 ironically obsoletes C++11 additions that made it less painful
|
2017-06-30 01:39:49 +02:00
|
|
|
|
fun to_wide (const string &multi) -> wstring {
|
2017-06-29 02:50:39 +02:00
|
|
|
|
wstring wide; wchar_t w; mbstate_t mb {};
|
|
|
|
|
size_t n = 0, len = multi.length () + 1;
|
|
|
|
|
while (auto res = mbrtowc (&w, multi.c_str () + n, len - n, &mb)) {
|
|
|
|
|
if (res == size_t (-1) || res == size_t (-2))
|
|
|
|
|
return L"/invalid encoding/";
|
|
|
|
|
|
|
|
|
|
n += res;
|
|
|
|
|
wide += w;
|
|
|
|
|
}
|
|
|
|
|
return wide;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 01:39:49 +02:00
|
|
|
|
fun to_mb (const wstring &wide) -> string {
|
2017-06-29 02:50:39 +02:00
|
|
|
|
string mb; char buf[MB_LEN_MAX + 1]; mbstate_t mbs {};
|
|
|
|
|
for (size_t n = 0; n <= wide.length (); n++) {
|
|
|
|
|
auto res = wcrtomb (buf, wide.c_str ()[n], &mbs);
|
|
|
|
|
if (res == size_t (-1))
|
|
|
|
|
throw invalid_argument ("invalid encoding");
|
|
|
|
|
mb.append (buf, res);
|
|
|
|
|
}
|
|
|
|
|
// There's one extra NUL character added by wcrtomb()
|
|
|
|
|
mb.erase (mb.length () - 1);
|
|
|
|
|
return mb;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-19 09:12:17 +02:00
|
|
|
|
fun prefix_length (const wstring &in, const wstring &of) -> size_t {
|
|
|
|
|
size_t score = 0;
|
2017-06-30 02:01:47 +02:00
|
|
|
|
for (size_t i = 0; i < of.size () && in.size () >= i && in[i] == of[i]; i++)
|
|
|
|
|
score++;
|
|
|
|
|
return score;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-08 01:41:41 +01:00
|
|
|
|
// TODO: this omits empty elements, check usages
|
2017-06-30 21:29:10 +02:00
|
|
|
|
fun split (const string &s, const string &sep, vector<string> &out) {
|
|
|
|
|
size_t mark = 0, p = s.find (sep);
|
|
|
|
|
for (; p != string::npos; p = s.find (sep, (mark = p + sep.length ())))
|
|
|
|
|
if (mark < p)
|
|
|
|
|
out.push_back (s.substr (mark, p - mark));
|
|
|
|
|
if (mark < s.length ())
|
|
|
|
|
out.push_back (s.substr (mark));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun split (const string &s, const string &sep) -> vector<string> {
|
|
|
|
|
vector<string> result; split (s, sep, result); return result;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-28 01:13:06 +02:00
|
|
|
|
fun untilde (const string &path) -> string {
|
|
|
|
|
if (path.empty ())
|
|
|
|
|
return path;
|
|
|
|
|
|
|
|
|
|
string tail = path.substr (1);
|
|
|
|
|
if (path[0] == '\\')
|
|
|
|
|
return tail;
|
2020-09-28 01:58:55 +02:00
|
|
|
|
if (path[0] != '~')
|
2020-09-28 01:13:06 +02:00
|
|
|
|
return path;
|
|
|
|
|
|
|
|
|
|
// If there is something between the ~ and the first / (or the EOS)
|
2020-09-28 01:58:55 +02:00
|
|
|
|
if (size_t until_slash = strcspn (tail.c_str (), "/")) {
|
2020-09-28 01:13:06 +02:00
|
|
|
|
if (const auto *pw = getpwnam (tail.substr (0, until_slash).c_str ()))
|
|
|
|
|
return pw->pw_dir + tail.substr (until_slash);
|
|
|
|
|
} else if (const auto *home = getenv ("HOME")) {
|
|
|
|
|
return home + tail;
|
|
|
|
|
} else if (const auto *pw = getpwuid (getuid ())) {
|
|
|
|
|
return pw->pw_dir + tail;
|
|
|
|
|
}
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-27 21:25:11 +02:00
|
|
|
|
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))
|
|
|
|
|
return true;
|
2018-11-02 12:08:43 +01:00
|
|
|
|
return v.empty ();
|
2018-10-27 21:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 02:01:47 +02:00
|
|
|
|
fun shell_escape (const string &v) -> string {
|
2018-10-27 21:25:11 +02:00
|
|
|
|
if (!needs_shell_quoting (v))
|
|
|
|
|
return v;
|
|
|
|
|
|
2017-06-30 02:01:47 +02:00
|
|
|
|
string result;
|
|
|
|
|
for (auto c : v)
|
|
|
|
|
if (c == '\'')
|
|
|
|
|
result += "'\\''";
|
|
|
|
|
else
|
|
|
|
|
result += c;
|
|
|
|
|
return "'" + result + "'";
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 22:20:02 +01:00
|
|
|
|
fun parse_line (istream &is, vector<string> &out) -> bool {
|
2021-11-05 21:30:51 +01:00
|
|
|
|
enum { STA, DEF, COM, ESC, WOR, QUO, STATES };
|
|
|
|
|
enum { TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6 };
|
|
|
|
|
enum { TWOR = TAKE | WOR };
|
2018-11-01 22:20:02 +01:00
|
|
|
|
|
2020-09-28 01:45:05 +02:00
|
|
|
|
// We never transition back to the start state, so it can stay as a no-op
|
2018-11-01 22:20:02 +01:00
|
|
|
|
static char table[STATES][7] = {
|
|
|
|
|
// state EOF SP, TAB ' # \ LF default
|
|
|
|
|
/* STA */ {ERROR, DEF, QUO, COM, ESC, STOP, TWOR},
|
|
|
|
|
/* DEF */ {STOP, 0, QUO, COM, ESC, STOP, TWOR},
|
|
|
|
|
/* COM */ {STOP, 0, 0, 0, 0, STOP, 0},
|
|
|
|
|
/* ESC */ {ERROR, TWOR, TWOR, TWOR, TWOR, TWOR, TWOR},
|
|
|
|
|
/* WOR */ {STOP | PUSH, DEF | PUSH, QUO, TAKE, ESC, STOP | PUSH, TAKE},
|
|
|
|
|
/* QUO */ {ERROR, TAKE, WOR, TAKE, TAKE, TAKE, TAKE},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
out.clear (); string token; int state = STA;
|
|
|
|
|
constexpr auto eof = istream::traits_type::eof ();
|
|
|
|
|
while (1) {
|
|
|
|
|
int ch = is.get (), edge = 0;
|
|
|
|
|
switch (ch) {
|
|
|
|
|
case eof: edge = table[state][0]; break;
|
|
|
|
|
case '\t':
|
|
|
|
|
case ' ': edge = table[state][1]; break;
|
|
|
|
|
case '\'': edge = table[state][2]; break;
|
|
|
|
|
case '#': edge = table[state][3]; break;
|
|
|
|
|
case '\\': edge = table[state][4]; break;
|
|
|
|
|
case '\n': edge = table[state][5]; break;
|
|
|
|
|
default: edge = table[state][6]; break;
|
|
|
|
|
}
|
|
|
|
|
if (edge & TAKE)
|
|
|
|
|
token += ch;
|
|
|
|
|
if (edge & PUSH) {
|
|
|
|
|
out.push_back (token);
|
|
|
|
|
token.clear ();
|
|
|
|
|
}
|
|
|
|
|
if (edge & STOP)
|
|
|
|
|
return true;
|
|
|
|
|
if (edge & ERROR)
|
|
|
|
|
return false;
|
|
|
|
|
if (edge &= 7)
|
|
|
|
|
state = edge;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 14:32:24 +01:00
|
|
|
|
fun write_line (ostream &os, const vector<string> &in) {
|
|
|
|
|
if (!in.empty ())
|
|
|
|
|
os << shell_escape (in.at (0));
|
|
|
|
|
for (size_t i = 1; i < in.size (); i++)
|
|
|
|
|
os << " " << shell_escape (in.at (i));
|
|
|
|
|
os << endl;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 02:01:47 +02:00
|
|
|
|
fun decode_type (mode_t m) -> wchar_t {
|
|
|
|
|
if (S_ISDIR (m)) return L'd'; if (S_ISBLK (m)) return L'b';
|
|
|
|
|
if (S_ISCHR (m)) return L'c'; if (S_ISLNK (m)) return L'l';
|
|
|
|
|
if (S_ISFIFO (m)) return L'p'; if (S_ISSOCK (m)) return L's';
|
2018-10-25 14:51:19 +02:00
|
|
|
|
if (S_ISREG (m)) return L'-';
|
|
|
|
|
return L'?';
|
2017-06-30 02:01:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return the modes of a file in the usual stat/ls format
|
|
|
|
|
fun decode_mode (mode_t m) -> wstring {
|
2018-11-01 22:19:31 +01:00
|
|
|
|
return {decode_type (m),
|
2017-06-30 02:01:47 +02:00
|
|
|
|
L"r-"[!(m & S_IRUSR)],
|
|
|
|
|
L"w-"[!(m & S_IWUSR)],
|
|
|
|
|
((m & S_ISUID) ? L"sS" : L"x-")[!(m & S_IXUSR)],
|
|
|
|
|
L"r-"[!(m & S_IRGRP)],
|
|
|
|
|
L"w-"[!(m & S_IWGRP)],
|
|
|
|
|
((m & S_ISGID) ? L"sS" : L"x-")[!(m & S_IXGRP)],
|
|
|
|
|
L"r-"[!(m & S_IROTH)],
|
|
|
|
|
L"w-"[!(m & S_IWOTH)],
|
2018-11-01 22:19:31 +01:00
|
|
|
|
((m & S_ISVTX) ? L"tT" : L"x-")[!(m & S_IXOTH)]};
|
2017-06-30 02:01:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 21:29:10 +02:00
|
|
|
|
template<class T> fun shift (vector<T> &v) -> T {
|
|
|
|
|
auto front = v.front (); v.erase (begin (v)); return front;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 15:54:32 +01:00
|
|
|
|
fun capitalize (const string &s) -> string {
|
|
|
|
|
string result;
|
|
|
|
|
for (auto c : s)
|
|
|
|
|
result += result.empty () ? toupper (c) : tolower (c);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-28 20:11:38 +02:00
|
|
|
|
/// Underlining for teletypes (also called overstriking),
|
|
|
|
|
/// also imitated in more(1) and less(1)
|
2021-11-05 21:30:51 +01:00
|
|
|
|
fun underline (const string &s) -> string {
|
2018-11-01 15:54:32 +01:00
|
|
|
|
string result;
|
|
|
|
|
for (auto c : s)
|
|
|
|
|
result.append ({c, 8, '_'});
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 21:29:10 +02:00
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
fun xdg_config_home () -> string {
|
|
|
|
|
const char *user_dir = getenv ("XDG_CONFIG_HOME");
|
|
|
|
|
if (user_dir && user_dir[0] == '/')
|
|
|
|
|
return user_dir;
|
|
|
|
|
|
|
|
|
|
const char *home_dir = getenv ("HOME");
|
|
|
|
|
return string (home_dir ? home_dir : "") + "/.config";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In C++17 we will get <optional> but until then there's unique_ptr
|
|
|
|
|
fun xdg_config_find (const string &suffix) -> unique_ptr<ifstream> {
|
|
|
|
|
vector<string> dirs {xdg_config_home ()};
|
|
|
|
|
const char *system_dirs = getenv ("XDG_CONFIG_DIRS");
|
2021-09-26 08:59:57 +02:00
|
|
|
|
split ((system_dirs && *system_dirs) ? system_dirs : "/etc/xdg", ":", dirs);
|
2017-06-30 21:29:10 +02:00
|
|
|
|
for (const auto &dir : dirs) {
|
|
|
|
|
if (dir[0] != '/')
|
|
|
|
|
continue;
|
2018-11-04 19:44:44 +01:00
|
|
|
|
auto ifs = make_unique<ifstream>
|
|
|
|
|
(dir + "/" PROJECT_NAME "/" + suffix);
|
|
|
|
|
if (*ifs)
|
|
|
|
|
return ifs;
|
2017-06-30 21:29:10 +02:00
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 14:32:24 +01:00
|
|
|
|
fun xdg_config_write (const string &suffix) -> unique_ptr<fstream> {
|
|
|
|
|
auto dir = xdg_config_home ();
|
|
|
|
|
if (dir[0] == '/') {
|
2018-11-02 15:37:56 +01:00
|
|
|
|
auto path = dir + "/" PROJECT_NAME "/" + suffix;
|
|
|
|
|
if (!fork ())
|
|
|
|
|
_exit (-execlp ("mkdir", "mkdir", "-p",
|
|
|
|
|
dirname (strdup (path.c_str ())), NULL));
|
2018-11-04 19:44:44 +01:00
|
|
|
|
auto fs = make_unique<fstream>
|
|
|
|
|
(path, fstream::in | fstream::out | fstream::trunc);
|
|
|
|
|
if (*fs)
|
|
|
|
|
return fs;
|
2018-11-02 14:32:24 +01:00
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 01:39:49 +02:00
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
using ncstring = basic_string<cchar_t>;
|
|
|
|
|
|
2017-06-30 07:01:45 +02:00
|
|
|
|
fun cchar (chtype attrs, wchar_t c) -> cchar_t {
|
2018-11-01 20:05:06 +01:00
|
|
|
|
cchar_t ch {}; wchar_t ws[] = {c, 0};
|
|
|
|
|
setcchar (&ch, ws, attrs, PAIR_NUMBER (attrs), nullptr);
|
2017-06-30 07:01:45 +02:00
|
|
|
|
return ch;
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-15 02:57:58 +02:00
|
|
|
|
fun decolor (cchar_t &ch) {
|
|
|
|
|
wchar_t c[CCHARW_MAX]; attr_t attrs; short pair;
|
|
|
|
|
getcchar (&ch, c, &attrs, &pair, nullptr);
|
|
|
|
|
setcchar (&ch, c, attrs &~ A_REVERSE, 0, nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 18:14:53 +01:00
|
|
|
|
fun invert (cchar_t &ch) {
|
|
|
|
|
wchar_t c[CCHARW_MAX]; attr_t attrs; short pair;
|
|
|
|
|
getcchar (&ch, c, &attrs, &pair, nullptr);
|
|
|
|
|
setcchar (&ch, c, attrs ^ A_REVERSE, 0, nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 01:39:49 +02:00
|
|
|
|
fun apply_attrs (const wstring &w, attr_t attrs) -> ncstring {
|
2021-10-05 20:15:52 +02:00
|
|
|
|
ncstring res (w.size (), cchar_t {});
|
|
|
|
|
for (size_t i = 0; i < w.size (); i++)
|
|
|
|
|
res[i] = cchar (attrs, w[i]);
|
2017-06-30 01:39:49 +02:00
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 07:01:45 +02:00
|
|
|
|
fun sanitize_char (chtype attrs, wchar_t c) -> ncstring {
|
2020-10-08 13:25:00 +02:00
|
|
|
|
if (c < 32 || c == 0x7f)
|
2017-06-30 07:01:45 +02:00
|
|
|
|
return {cchar (attrs | A_REVERSE, L'^'),
|
2020-10-08 13:25:00 +02:00
|
|
|
|
cchar (attrs | A_REVERSE, (c + 64) & 0x7f)};
|
2017-06-30 07:01:45 +02:00
|
|
|
|
if (!iswprint (c))
|
|
|
|
|
return {cchar (attrs | A_REVERSE, L'?')};
|
|
|
|
|
return {cchar (attrs, c)};
|
|
|
|
|
}
|
2017-06-30 01:39:49 +02:00
|
|
|
|
|
2017-06-30 07:01:45 +02:00
|
|
|
|
fun sanitize (const ncstring &nc) -> ncstring {
|
|
|
|
|
ncstring out;
|
|
|
|
|
for (cchar_t c : nc)
|
|
|
|
|
for (size_t i = 0; i < CCHARW_MAX && c.chars[i]; i++)
|
|
|
|
|
out += sanitize_char (c.attr, c.chars[i]);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun print (const ncstring &nc, int limit) -> int {
|
|
|
|
|
int total_width = 0;
|
|
|
|
|
for (cchar_t c : sanitize (nc)) {
|
|
|
|
|
int width = wcwidth (c.chars[0]);
|
2017-06-30 01:39:49 +02:00
|
|
|
|
if (total_width + width > limit)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
add_wch (&c);
|
|
|
|
|
total_width += width;
|
|
|
|
|
}
|
|
|
|
|
return total_width;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 08:22:38 +02:00
|
|
|
|
fun compute_width (const ncstring &nc) -> int {
|
|
|
|
|
int total = 0;
|
|
|
|
|
for (const auto &c : nc)
|
|
|
|
|
total += wcwidth (c.chars[0]);
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: maybe we need formatting for the padding passed in?
|
|
|
|
|
fun align (const ncstring &nc, int target) -> ncstring {
|
|
|
|
|
auto current = compute_width (nc);
|
|
|
|
|
auto missing = abs (target) - current;
|
|
|
|
|
if (missing <= 0)
|
|
|
|
|
return nc;
|
|
|
|
|
return target < 0
|
|
|
|
|
? nc + apply_attrs (wstring (missing, L' '), 0)
|
|
|
|
|
: apply_attrs (wstring (missing, L' '), 0) + nc;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 21:29:10 +02:00
|
|
|
|
fun allocate_pair (short fg, short bg) -> short {
|
|
|
|
|
static short counter = 1; init_pair (counter, fg, bg); return counter++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun decode_attrs (const vector<string> &attrs) -> chtype {
|
|
|
|
|
chtype result = 0; int fg = -1, bg = -1, colors = 0;
|
|
|
|
|
for (const auto &s : attrs) {
|
|
|
|
|
char *end; auto color = strtol (s.c_str (), &end, 10);
|
|
|
|
|
if (!*end && color >= -1 && color < COLORS) {
|
|
|
|
|
if (++colors == 1) fg = color;
|
|
|
|
|
else if (colors == 2) bg = color;
|
|
|
|
|
}
|
|
|
|
|
else if (s == "bold") result |= A_BOLD;
|
|
|
|
|
else if (s == "dim") result |= A_DIM;
|
|
|
|
|
else if (s == "ul") result |= A_UNDERLINE;
|
|
|
|
|
else if (s == "blink") result |= A_BLINK;
|
|
|
|
|
else if (s == "reverse") result |= A_REVERSE;
|
|
|
|
|
else if (s == "italic") result |= A_ITALIC;
|
|
|
|
|
}
|
|
|
|
|
if (fg != -1 || bg != -1)
|
|
|
|
|
result |= COLOR_PAIR (allocate_pair (fg, bg));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
// --- Application -------------------------------------------------------------
|
|
|
|
|
|
2021-07-09 04:36:43 +02:00
|
|
|
|
enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
|
2018-10-25 21:04:13 +02:00
|
|
|
|
#define KEY(name) (SYM | KEY_ ## name)
|
2021-07-09 04:36:43 +02:00
|
|
|
|
#define CTRL(char) ((char) == '?' ? 0x7f : (char) & 0x1f)
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
2018-11-02 15:50:58 +01:00
|
|
|
|
#define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
|
2018-12-03 22:05:00 +01:00
|
|
|
|
XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \
|
2019-09-27 21:08:12 +02:00
|
|
|
|
XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \
|
2022-01-08 11:13:08 +01:00
|
|
|
|
XX(PAGE_PREVIOUS) XX(PAGE_NEXT) XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CENTER) \
|
2020-09-15 19:15:30 +02:00
|
|
|
|
XX(CHDIR) XX(PARENT) XX(GO_START) XX(GO_HOME) \
|
2021-07-17 14:41:57 +02:00
|
|
|
|
XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) XX(MKDIR) \
|
2018-11-02 17:45:18 +01:00
|
|
|
|
XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \
|
2020-10-22 00:05:24 +02:00
|
|
|
|
XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE) XX(INPUT_DELETE) \
|
2023-06-11 21:05:55 +02:00
|
|
|
|
XX(INPUT_B_KILL_WORD) XX(INPUT_B_KILL_LINE) XX(INPUT_KILL_LINE) \
|
|
|
|
|
XX(INPUT_QUOTED_INSERT) \
|
2020-10-21 23:53:30 +02:00
|
|
|
|
XX(INPUT_BACKWARD) XX(INPUT_FORWARD) XX(INPUT_BEGINNING) XX(INPUT_END)
|
2018-10-25 21:04:13 +02:00
|
|
|
|
|
|
|
|
|
#define XX(name) ACTION_ ## name,
|
|
|
|
|
enum action { ACTIONS(XX) ACTION_COUNT };
|
|
|
|
|
#undef XX
|
|
|
|
|
|
2018-10-25 23:00:55 +02:00
|
|
|
|
#define XX(name) #name,
|
|
|
|
|
static const char *g_action_names[] = {ACTIONS(XX)};
|
|
|
|
|
#undef XX
|
|
|
|
|
|
2018-11-01 22:19:31 +01:00
|
|
|
|
static map<wint_t, action> g_normal_actions {
|
2018-10-25 21:04:13 +02:00
|
|
|
|
{ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
|
2018-12-03 22:05:00 +01:00
|
|
|
|
{'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
|
2020-10-25 08:19:49 +01:00
|
|
|
|
{KEY (F (1)), ACTION_HELP}, {'h', ACTION_HELP},
|
|
|
|
|
{KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT},
|
2018-11-02 15:50:58 +01:00
|
|
|
|
{'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
// M-o ought to be the same shortcut the navigator is launched with
|
2018-11-02 15:50:58 +01:00
|
|
|
|
{ALT | 'o', ACTION_QUIT},
|
2018-11-02 18:07:06 +01:00
|
|
|
|
{'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
|
2020-10-08 13:25:00 +02:00
|
|
|
|
{'k', ACTION_UP}, {CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
|
|
|
|
|
{'j', ACTION_DOWN}, {CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
{'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
|
|
|
|
|
{'G', ACTION_BOTTOM}, {ALT | '>', ACTION_BOTTOM}, {KEY(END), ACTION_BOTTOM},
|
2019-09-27 21:08:12 +02:00
|
|
|
|
{'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
{KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
|
2020-10-08 13:25:00 +02:00
|
|
|
|
{CTRL ('Y'), ACTION_SCROLL_UP}, {CTRL ('E'), ACTION_SCROLL_DOWN},
|
2022-01-08 11:13:08 +01:00
|
|
|
|
{'z', ACTION_CENTER},
|
2020-10-23 03:38:47 +02:00
|
|
|
|
{'c', ACTION_CHDIR}, {ALT | KEY (UP), ACTION_PARENT},
|
|
|
|
|
{'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
|
2021-10-18 11:23:17 +02:00
|
|
|
|
{'/', ACTION_SEARCH}, {'s', ACTION_SEARCH}, {CTRL ('S'), ACTION_SEARCH},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
{ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
|
2021-07-17 14:44:30 +02:00
|
|
|
|
{KEY (F (6)), ACTION_RENAME_PREFILL}, {KEY (F (7)), ACTION_MKDIR},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
{'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL},
|
2018-11-02 17:45:18 +01:00
|
|
|
|
{'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
|
2020-10-08 13:25:00 +02:00
|
|
|
|
{CTRL ('L'), ACTION_REDRAW}, {'r', ACTION_RELOAD},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
};
|
2018-11-01 22:19:31 +01:00
|
|
|
|
static map<wint_t, action> g_input_actions {
|
2020-10-08 13:25:00 +02:00
|
|
|
|
{27, ACTION_INPUT_ABORT}, {CTRL ('G'), ACTION_INPUT_ABORT},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
{L'\r', ACTION_INPUT_CONFIRM}, {KEY (ENTER), ACTION_INPUT_CONFIRM},
|
2020-09-21 21:15:15 +02:00
|
|
|
|
// Sometimes terminfo is wrong, we need to accept both of these
|
2020-10-08 13:25:00 +02:00
|
|
|
|
{L'\b', ACTION_INPUT_B_DELETE}, {CTRL ('?'), ACTION_INPUT_B_DELETE},
|
2020-10-22 00:05:24 +02:00
|
|
|
|
{KEY (BACKSPACE), ACTION_INPUT_B_DELETE}, {KEY (DC), ACTION_INPUT_DELETE},
|
2023-06-11 21:05:55 +02:00
|
|
|
|
{CTRL ('W'), ACTION_INPUT_B_KILL_WORD}, {CTRL ('D'), ACTION_INPUT_DELETE},
|
|
|
|
|
{CTRL ('U'), ACTION_INPUT_B_KILL_LINE},
|
2020-10-23 01:21:25 +02:00
|
|
|
|
{CTRL ('K'), ACTION_INPUT_KILL_LINE},
|
2020-10-23 01:32:43 +02:00
|
|
|
|
{CTRL ('V'), ACTION_INPUT_QUOTED_INSERT},
|
2020-10-21 23:53:30 +02:00
|
|
|
|
{CTRL ('B'), ACTION_INPUT_BACKWARD}, {KEY (LEFT), ACTION_INPUT_BACKWARD},
|
|
|
|
|
{CTRL ('F'), ACTION_INPUT_FORWARD}, {KEY (RIGHT), ACTION_INPUT_FORWARD},
|
|
|
|
|
{CTRL ('A'), ACTION_INPUT_BEGINNING}, {KEY (HOME), ACTION_INPUT_BEGINNING},
|
|
|
|
|
{CTRL ('E'), ACTION_INPUT_END}, {KEY (END), ACTION_INPUT_END},
|
2018-10-25 21:04:13 +02:00
|
|
|
|
};
|
2021-07-17 14:01:29 +02:00
|
|
|
|
static map<wint_t, action> g_search_actions {
|
|
|
|
|
{CTRL ('P'), ACTION_UP}, {KEY (UP), ACTION_UP},
|
|
|
|
|
{CTRL ('N'), ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
|
|
|
|
|
};
|
2018-11-01 22:19:31 +01:00
|
|
|
|
static const map<string, map<wint_t, action>*> g_binding_contexts {
|
2018-10-25 23:00:55 +02:00
|
|
|
|
{"normal", &g_normal_actions}, {"input", &g_input_actions},
|
2021-07-17 14:01:29 +02:00
|
|
|
|
{"search", &g_search_actions},
|
2018-10-25 23:00:55 +02:00
|
|
|
|
};
|
2018-10-25 21:04:13 +02:00
|
|
|
|
|
2017-07-15 02:57:58 +02:00
|
|
|
|
#define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \
|
|
|
|
|
XX(DIRECTORY, "di") XX(SYMLINK, "ln") XX(MULTIHARDLINK, "mh") \
|
|
|
|
|
XX(FIFO, "pi") XX(SOCKET, "so") XX(DOOR, "do") XX(BLOCK, "bd") \
|
|
|
|
|
XX(CHARACTER, "cd") XX(ORPHAN, "or") XX(MISSING, "mi") XX(SETUID, "su") \
|
|
|
|
|
XX(SETGID, "sg") XX(CAPABILITY, "ca") XX(STICKY_OTHER_WRITABLE, "tw") \
|
|
|
|
|
XX(OTHER_WRITABLE, "ow") XX(STICKY, "st") XX(EXECUTABLE, "ex")
|
|
|
|
|
|
|
|
|
|
#define XX(id, name) LS_ ## id,
|
|
|
|
|
enum { LS(XX) LS_COUNT };
|
|
|
|
|
#undef XX
|
|
|
|
|
|
|
|
|
|
#define XX(id, name) name,
|
|
|
|
|
static const char *g_ls_colors[] = {LS(XX)};
|
|
|
|
|
#undef XX
|
|
|
|
|
|
2018-10-25 23:00:55 +02:00
|
|
|
|
struct stringcaseless {
|
|
|
|
|
bool operator () (const string &a, const string &b) const {
|
|
|
|
|
const auto &c = locale::classic();
|
|
|
|
|
return lexicographical_compare (begin (a), end (a), begin (b), end (b),
|
|
|
|
|
[&](char m, char n) { return tolower (m, c) < tolower (n, c); });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2018-11-01 18:25:38 +01:00
|
|
|
|
struct entry {
|
|
|
|
|
string filename, target_path;
|
|
|
|
|
struct stat info = {}, target_info = {};
|
|
|
|
|
|
|
|
|
|
enum { MODES, USER, GROUP, SIZE, MTIME, FILENAME, COLUMNS };
|
|
|
|
|
ncstring cols[COLUMNS];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct level {
|
|
|
|
|
int offset, cursor; ///< Scroll offset and cursor position
|
|
|
|
|
string path, filename; ///< Level path and filename at cursor
|
|
|
|
|
};
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
static struct {
|
2020-10-01 13:31:39 +02:00
|
|
|
|
ncstring cmdline; ///< Outer command line
|
2018-10-24 05:05:26 +02:00
|
|
|
|
string cwd; ///< Current working directory
|
2018-10-24 05:14:19 +02:00
|
|
|
|
string start_dir; ///< Starting directory
|
2018-10-24 05:05:26 +02:00
|
|
|
|
vector<entry> entries; ///< Current directory entries
|
2018-11-01 18:25:38 +01:00
|
|
|
|
vector<level> levels; ///< Upper directory levels
|
2018-10-24 05:05:26 +02:00
|
|
|
|
int offset, cursor; ///< Scroll offset and cursor position
|
|
|
|
|
bool full_view; ///< Show extended information
|
2018-11-02 14:32:24 +01:00
|
|
|
|
bool gravity; ///< Entries are shoved to the bottom
|
2018-11-02 17:25:10 +01:00
|
|
|
|
bool reverse_sort; ///< Reverse sort
|
2018-11-02 17:45:18 +01:00
|
|
|
|
bool show_hidden; ///< Show hidden files
|
2020-09-29 00:55:01 +02:00
|
|
|
|
bool ext_helpers; ///< Launch helpers externally
|
2018-10-25 14:51:19 +02:00
|
|
|
|
int max_widths[entry::COLUMNS]; ///< Column widths
|
2018-11-02 18:07:06 +01:00
|
|
|
|
int sort_column = entry::FILENAME; ///< Sorting column
|
2018-11-02 18:14:53 +01:00
|
|
|
|
int sort_flash_ttl; ///< Sorting column flash TTL
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
2018-11-02 15:05:04 +01:00
|
|
|
|
wstring message; ///< Message for the user
|
|
|
|
|
int message_ttl; ///< Time to live for the message
|
|
|
|
|
|
2018-10-24 05:05:26 +02:00
|
|
|
|
string chosen; ///< Chosen item for the command line
|
2020-09-29 00:55:01 +02:00
|
|
|
|
string ext_helper; ///< External helper to run
|
2018-11-02 15:50:58 +01:00
|
|
|
|
bool no_chdir; ///< Do not tell the shell to chdir
|
2018-11-02 22:12:34 +01:00
|
|
|
|
bool quitting; ///< Whether we should quit already
|
2017-06-30 01:39:49 +02:00
|
|
|
|
|
2018-10-24 05:05:26 +02:00
|
|
|
|
int inotify_fd, inotify_wd = -1; ///< File watch
|
|
|
|
|
bool out_of_date; ///< Entries may be out of date
|
2017-06-30 02:01:47 +02:00
|
|
|
|
|
2018-11-01 12:31:02 +01:00
|
|
|
|
const wchar_t *editor; ///< Prompt string for editing
|
2021-07-17 12:44:01 +02:00
|
|
|
|
wstring editor_info; ///< Right-side prompt while editing
|
2018-10-24 05:05:26 +02:00
|
|
|
|
wstring editor_line; ///< Current user input
|
2020-10-21 23:13:09 +02:00
|
|
|
|
int editor_cursor = 0; ///< Cursor position
|
2020-10-23 01:32:43 +02:00
|
|
|
|
bool editor_inserting; ///< Inserting a literal character
|
2018-11-01 12:31:02 +01:00
|
|
|
|
void (*editor_on_change) (); ///< Callback on editor change
|
2021-07-17 14:01:29 +02:00
|
|
|
|
map<action, void (*) ()> editor_on; ///< Handlers for custom actions
|
2017-06-30 21:29:10 +02:00
|
|
|
|
|
2021-07-17 12:44:01 +02:00
|
|
|
|
enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_INFO, AT_CMDLINE, AT_COUNT };
|
|
|
|
|
chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0, A_ITALIC, 0};
|
2020-10-01 11:54:01 +02:00
|
|
|
|
const char *attr_names[AT_COUNT] =
|
2021-07-17 12:44:01 +02:00
|
|
|
|
{"cursor", "bar", "cwd", "input", "info", "cmdline"};
|
2017-07-15 02:57:58 +02:00
|
|
|
|
|
2018-10-24 05:05:26 +02:00
|
|
|
|
map<int, chtype> ls_colors; ///< LS_COLORS decoded
|
|
|
|
|
map<string, chtype> ls_exts; ///< LS_COLORS file extensions
|
2018-10-25 17:26:36 +02:00
|
|
|
|
bool ls_symlink_as_target; ///< ln=target in dircolors
|
2018-10-24 08:47:02 +02:00
|
|
|
|
|
2018-11-01 15:54:32 +01:00
|
|
|
|
map<string, wint_t, stringcaseless> name_to_key;
|
|
|
|
|
map<wint_t, string> key_to_name;
|
2021-07-07 20:30:46 +02:00
|
|
|
|
map<string, wint_t> custom_keys;
|
2018-11-01 15:54:32 +01:00
|
|
|
|
string action_names[ACTION_COUNT]; ///< Stylized action names
|
2018-10-25 23:00:55 +02:00
|
|
|
|
|
2018-10-24 08:47:02 +02:00
|
|
|
|
// Refreshed by reload():
|
|
|
|
|
|
2021-10-05 20:15:52 +02:00
|
|
|
|
map<uid_t, wstring> unames; ///< User names by UID
|
|
|
|
|
map<gid_t, wstring> gnames; ///< Group names by GID
|
2018-10-24 08:47:02 +02:00
|
|
|
|
struct tm now; ///< Current local time for display
|
2017-06-30 02:01:47 +02:00
|
|
|
|
} g;
|
2017-06-30 01:39:49 +02:00
|
|
|
|
|
2018-10-25 17:26:36 +02:00
|
|
|
|
// The coloring logic has been more or less exactly copied from GNU ls,
|
|
|
|
|
// simplified and rewritten to reflect local implementation specifics
|
|
|
|
|
fun ls_is_colored (int type) -> bool {
|
|
|
|
|
auto i = g.ls_colors.find (type);
|
|
|
|
|
return i != g.ls_colors.end () && i->second != 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun ls_format (const entry &e, bool for_target) -> chtype {
|
2017-07-15 02:57:58 +02:00
|
|
|
|
int type = LS_ORPHAN;
|
2018-10-25 17:26:36 +02:00
|
|
|
|
auto set = [&](int t) { if (ls_is_colored (t)) type = t; };
|
|
|
|
|
|
|
|
|
|
const auto &name = for_target
|
|
|
|
|
? e.target_path : e.filename;
|
|
|
|
|
const auto &info =
|
|
|
|
|
(for_target || (g.ls_symlink_as_target && e.target_info.st_mode))
|
|
|
|
|
? e.target_info : e.info;
|
|
|
|
|
|
|
|
|
|
if (for_target && info.st_mode == 0) {
|
|
|
|
|
// This differs from GNU ls: we use ORPHAN when MISSING is not set,
|
|
|
|
|
// but GNU ls colors by dirent::d_type
|
|
|
|
|
set (LS_MISSING);
|
|
|
|
|
} else if (S_ISREG (info.st_mode)) {
|
2017-07-15 02:57:58 +02:00
|
|
|
|
type = LS_FILE;
|
|
|
|
|
if (info.st_nlink > 1)
|
|
|
|
|
set (LS_MULTIHARDLINK);
|
|
|
|
|
if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
|
|
|
|
|
set (LS_EXECUTABLE);
|
2018-10-25 17:26:36 +02:00
|
|
|
|
if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
|
2017-07-15 02:57:58 +02:00
|
|
|
|
set (LS_CAPABILITY);
|
|
|
|
|
if ((info.st_mode & S_ISGID))
|
|
|
|
|
set (LS_SETGID);
|
|
|
|
|
if ((info.st_mode & S_ISUID))
|
|
|
|
|
set (LS_SETUID);
|
|
|
|
|
} else if (S_ISDIR (info.st_mode)) {
|
|
|
|
|
type = LS_DIRECTORY;
|
|
|
|
|
if ((info.st_mode & S_ISVTX))
|
|
|
|
|
set (LS_STICKY);
|
|
|
|
|
if ((info.st_mode & S_IWOTH))
|
|
|
|
|
set (LS_OTHER_WRITABLE);
|
|
|
|
|
if ((info.st_mode & S_ISVTX) && (info.st_mode & S_IWOTH))
|
|
|
|
|
set (LS_STICKY_OTHER_WRITABLE);
|
|
|
|
|
} else if (S_ISLNK (info.st_mode)) {
|
|
|
|
|
type = LS_SYMLINK;
|
2021-11-05 21:30:51 +01:00
|
|
|
|
if (!e.target_info.st_mode &&
|
|
|
|
|
(ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
|
2018-10-25 17:26:36 +02:00
|
|
|
|
type = LS_ORPHAN;
|
2017-07-15 02:57:58 +02:00
|
|
|
|
} else if (S_ISFIFO (info.st_mode)) {
|
|
|
|
|
type = LS_FIFO;
|
|
|
|
|
} else if (S_ISSOCK (info.st_mode)) {
|
|
|
|
|
type = LS_SOCKET;
|
|
|
|
|
} else if (S_ISBLK (info.st_mode)) {
|
|
|
|
|
type = LS_BLOCK;
|
|
|
|
|
} else if (S_ISCHR (info.st_mode)) {
|
|
|
|
|
type = LS_CHARACTER;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chtype format = 0;
|
|
|
|
|
const auto x = g.ls_colors.find (type);
|
|
|
|
|
if (x != g.ls_colors.end ())
|
|
|
|
|
format = x->second;
|
|
|
|
|
|
2018-10-25 17:26:36 +02:00
|
|
|
|
auto dot = name.find_last_of ('.');
|
2017-07-15 02:57:58 +02:00
|
|
|
|
if (dot != string::npos && type == LS_FILE) {
|
2018-10-25 17:26:36 +02:00
|
|
|
|
const auto x = g.ls_exts.find (name.substr (++dot));
|
2017-07-15 02:57:58 +02:00
|
|
|
|
if (x != g.ls_exts.end ())
|
|
|
|
|
format = x->second;
|
|
|
|
|
}
|
2017-07-15 22:18:45 +02:00
|
|
|
|
return format;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 14:51:19 +02:00
|
|
|
|
fun make_entry (const struct dirent *f) -> entry {
|
|
|
|
|
entry e;
|
|
|
|
|
e.filename = f->d_name;
|
|
|
|
|
e.info.st_mode = DTTOIF (f->d_type);
|
2018-11-01 15:54:32 +01:00
|
|
|
|
auto &info = e.info;
|
2018-10-25 14:51:19 +02:00
|
|
|
|
|
2021-10-05 19:18:15 +02:00
|
|
|
|
// io_uring is only at most about 50% faster, though it might help with
|
|
|
|
|
// slowly statting devices, at a major complexity cost.
|
2018-10-25 14:51:19 +02:00
|
|
|
|
if (lstat (f->d_name, &info)) {
|
|
|
|
|
e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode),
|
|
|
|
|
L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0);
|
|
|
|
|
|
|
|
|
|
e.cols[entry::USER] = e.cols[entry::GROUP] =
|
|
|
|
|
e.cols[entry::SIZE] = e.cols[entry::MTIME] = apply_attrs (L"?", 0);
|
|
|
|
|
|
2018-10-25 17:26:36 +02:00
|
|
|
|
e.cols[entry::FILENAME] =
|
|
|
|
|
apply_attrs (to_wide (e.filename), ls_format (e, false));
|
2018-10-25 14:51:19 +02:00
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (S_ISLNK (info.st_mode)) {
|
|
|
|
|
char buf[PATH_MAX] = {};
|
|
|
|
|
auto len = readlink (f->d_name, buf, sizeof buf);
|
|
|
|
|
if (len < 0 || size_t (len) >= sizeof buf) {
|
|
|
|
|
e.target_path = "?";
|
|
|
|
|
} else {
|
|
|
|
|
e.target_path = buf;
|
2018-10-25 17:26:36 +02:00
|
|
|
|
// If a symlink links to another symlink, we follow all the way
|
|
|
|
|
(void) stat (buf, &e.target_info);
|
2018-10-25 14:51:19 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-15 22:18:45 +02:00
|
|
|
|
auto mode = decode_mode (info.st_mode);
|
2021-09-26 09:19:54 +02:00
|
|
|
|
// 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.
|
2018-10-25 14:51:19 +02:00
|
|
|
|
if (acl_extended_file_nofollow (f->d_name) > 0)
|
2017-07-15 22:18:45 +02:00
|
|
|
|
mode += L"+";
|
2018-10-25 14:51:19 +02:00
|
|
|
|
e.cols[entry::MODES] = apply_attrs (mode, 0);
|
2017-07-15 22:18:45 +02:00
|
|
|
|
|
2018-10-24 08:47:02 +02:00
|
|
|
|
auto usr = g.unames.find (info.st_uid);
|
2018-10-25 14:51:19 +02:00
|
|
|
|
e.cols[entry::USER] = (usr != g.unames.end ())
|
2021-10-05 20:15:52 +02:00
|
|
|
|
? apply_attrs (usr->second, 0)
|
2018-10-24 08:47:02 +02:00
|
|
|
|
: apply_attrs (to_wstring (info.st_uid), 0);
|
2017-07-15 22:18:45 +02:00
|
|
|
|
|
2018-10-24 08:47:02 +02:00
|
|
|
|
auto grp = g.gnames.find (info.st_gid);
|
2018-12-01 15:57:35 +01:00
|
|
|
|
e.cols[entry::GROUP] = (grp != g.gnames.end ())
|
2021-10-05 20:15:52 +02:00
|
|
|
|
? apply_attrs (grp->second, 0)
|
2018-10-24 08:47:02 +02:00
|
|
|
|
: apply_attrs (to_wstring (info.st_gid), 0);
|
2017-07-15 22:18:45 +02:00
|
|
|
|
|
|
|
|
|
auto size = to_wstring (info.st_size);
|
|
|
|
|
if (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T";
|
|
|
|
|
else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G";
|
|
|
|
|
else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M";
|
|
|
|
|
else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
|
2018-10-25 14:51:19 +02:00
|
|
|
|
e.cols[entry::SIZE] = apply_attrs (size, 0);
|
2017-07-15 22:18:45 +02:00
|
|
|
|
|
2021-10-05 20:15:52 +02:00
|
|
|
|
wchar_t buf[32] = L"";
|
2017-07-15 22:18:45 +02:00
|
|
|
|
auto tm = localtime (&info.st_mtime);
|
2021-10-05 20:15:52 +02:00
|
|
|
|
wcsftime (buf, sizeof buf / sizeof *buf,
|
|
|
|
|
(tm->tm_year == g.now.tm_year) ? L"%b %e %H:%M" : L"%b %e %Y", tm);
|
|
|
|
|
e.cols[entry::MTIME] = apply_attrs (buf, 0);
|
2018-10-25 14:51:19 +02:00
|
|
|
|
|
|
|
|
|
auto &fn = e.cols[entry::FILENAME] =
|
2018-10-25 17:26:36 +02:00
|
|
|
|
apply_attrs (to_wide (e.filename), ls_format (e, false));
|
2018-10-25 14:51:19 +02:00
|
|
|
|
if (!e.target_path.empty ()) {
|
2021-10-05 20:15:52 +02:00
|
|
|
|
fn.append (apply_attrs (L" -> ", 0));
|
2018-10-25 17:26:36 +02:00
|
|
|
|
fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
|
2018-10-25 14:51:19 +02:00
|
|
|
|
}
|
|
|
|
|
return e;
|
2017-06-30 01:39:49 +02:00
|
|
|
|
}
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
2017-06-30 02:01:47 +02:00
|
|
|
|
fun inline visible_lines () -> int { return max (0, LINES - 2); }
|
|
|
|
|
|
2017-06-30 01:39:49 +02:00
|
|
|
|
fun update () {
|
2018-10-25 14:51:19 +02:00
|
|
|
|
int start_column = g.full_view ? 0 : entry::FILENAME;
|
2021-11-05 21:30:51 +01:00
|
|
|
|
static int alignment[entry::COLUMNS] = {-1, -1, -1, 1, 1, -1};
|
2017-06-29 02:50:39 +02:00
|
|
|
|
erase ();
|
|
|
|
|
|
2017-06-29 21:13:51 +02:00
|
|
|
|
int available = visible_lines ();
|
2019-09-27 22:38:48 +02:00
|
|
|
|
int all = g.entries.size ();
|
|
|
|
|
int used = min (available, all - g.offset);
|
2017-06-29 21:13:51 +02:00
|
|
|
|
for (int i = 0; i < used; i++) {
|
2017-06-30 21:29:10 +02:00
|
|
|
|
auto index = g.offset + i;
|
2017-07-15 02:57:58 +02:00
|
|
|
|
bool selected = index == g.cursor;
|
|
|
|
|
attrset (selected ? g.attrs[g.AT_CURSOR] : 0);
|
2018-11-02 14:32:24 +01:00
|
|
|
|
move (g.gravity ? (available - used + i) : i, 0);
|
2017-06-30 08:22:38 +02:00
|
|
|
|
|
2017-06-30 21:29:10 +02:00
|
|
|
|
auto used = 0;
|
2018-10-25 14:51:19 +02:00
|
|
|
|
for (int col = start_column; col < entry::COLUMNS; col++) {
|
|
|
|
|
const auto &field = g.entries[index].cols[col];
|
2017-06-30 08:22:38 +02:00
|
|
|
|
auto aligned = align (field, alignment[col] * g.max_widths[col]);
|
2018-11-02 18:14:53 +01:00
|
|
|
|
if (g.sort_flash_ttl && col == g.sort_column)
|
|
|
|
|
for_each (begin (aligned), end (aligned), invert);
|
2017-07-15 02:57:58 +02:00
|
|
|
|
if (selected)
|
|
|
|
|
for_each (begin (aligned), end (aligned), decolor);
|
2017-06-30 21:29:10 +02:00
|
|
|
|
used += print (aligned + apply_attrs (L" ", 0), COLS - used);
|
2017-06-30 01:39:49 +02:00
|
|
|
|
}
|
2017-06-30 21:29:10 +02:00
|
|
|
|
hline (' ', COLS - used);
|
2017-06-29 02:50:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 21:29:10 +02:00
|
|
|
|
auto bar = apply_attrs (to_wide (g.cwd), g.attrs[g.AT_CWD]);
|
2018-11-02 17:45:18 +01:00
|
|
|
|
if (!g.show_hidden)
|
|
|
|
|
bar += apply_attrs (L" (hidden)", 0);
|
2017-06-29 21:13:51 +02:00
|
|
|
|
if (g.out_of_date)
|
2017-06-30 21:29:10 +02:00
|
|
|
|
bar += apply_attrs (L" [+]", 0);
|
|
|
|
|
|
|
|
|
|
move (LINES - 2, 0);
|
|
|
|
|
attrset (g.attrs[g.AT_BAR]);
|
2019-09-27 22:38:48 +02:00
|
|
|
|
int unused = COLS - print (bar, COLS);
|
|
|
|
|
hline (' ', unused);
|
|
|
|
|
|
|
|
|
|
auto pos = to_wstring (int (double (g.offset) / all * 100)) + L"%";
|
|
|
|
|
if (used == all)
|
|
|
|
|
pos = L"All";
|
|
|
|
|
else if (g.offset == 0)
|
|
|
|
|
pos = L"Top";
|
|
|
|
|
else if (g.offset + used == all)
|
|
|
|
|
pos = L"Bot";
|
|
|
|
|
|
|
|
|
|
if (int (pos.size ()) < unused)
|
|
|
|
|
mvaddwstr (LINES - 2, COLS - pos.size (), pos.c_str ());
|
2017-06-29 21:13:51 +02:00
|
|
|
|
|
2017-06-30 21:29:10 +02:00
|
|
|
|
attrset (g.attrs[g.AT_INPUT]);
|
2018-11-02 15:05:04 +01:00
|
|
|
|
curs_set (0);
|
2017-06-29 02:50:39 +02:00
|
|
|
|
if (g.editor) {
|
2017-06-29 21:13:51 +02:00
|
|
|
|
move (LINES - 1, 0);
|
2021-07-17 12:44:01 +02:00
|
|
|
|
auto prompt = apply_attrs (wstring (g.editor) + L": ", 0),
|
|
|
|
|
line = apply_attrs (g.editor_line, 0),
|
|
|
|
|
info = apply_attrs (g.editor_info, g.attrs[g.AT_INFO]);
|
|
|
|
|
|
|
|
|
|
auto info_width = compute_width (info);
|
|
|
|
|
if (print (prompt + line, COLS - 1) < COLS - info_width) {
|
|
|
|
|
move (LINES - 1, COLS - info_width);
|
|
|
|
|
print (info, info_width);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-21 23:13:09 +02:00
|
|
|
|
auto start = sanitize (prompt + line.substr (0, g.editor_cursor));
|
|
|
|
|
move (LINES - 1, compute_width (start));
|
2017-06-29 02:50:39 +02:00
|
|
|
|
curs_set (1);
|
2018-11-02 15:05:04 +01:00
|
|
|
|
} else if (!g.message.empty ()) {
|
|
|
|
|
move (LINES - 1, 0);
|
|
|
|
|
print (apply_attrs (g.message, 0), COLS);
|
2020-10-01 11:54:01 +02:00
|
|
|
|
} else if (!g.cmdline.empty ()) {
|
|
|
|
|
move (LINES - 1, 0);
|
2020-10-01 13:31:39 +02:00
|
|
|
|
print (g.cmdline, COLS);
|
2018-11-02 15:05:04 +01:00
|
|
|
|
}
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
|
|
|
|
refresh ();
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 17:25:10 +01:00
|
|
|
|
fun operator< (const entry &e1, const entry &e2) -> bool {
|
2021-10-05 20:15:52 +02:00
|
|
|
|
static string dotdot {".."};
|
|
|
|
|
auto t1 = make_tuple (e1.filename != dotdot,
|
2018-11-10 01:32:59 +01:00
|
|
|
|
!S_ISDIR (e1.info.st_mode) && !S_ISDIR (e1.target_info.st_mode));
|
2021-10-05 20:15:52 +02:00
|
|
|
|
auto t2 = make_tuple (e2.filename != dotdot,
|
2018-11-10 01:32:59 +01:00
|
|
|
|
!S_ISDIR (e2.info.st_mode) && !S_ISDIR (e2.target_info.st_mode));
|
2018-11-02 17:25:10 +01:00
|
|
|
|
if (t1 != t2)
|
|
|
|
|
return t1 < t2;
|
2018-11-02 18:07:06 +01:00
|
|
|
|
|
|
|
|
|
const auto &a = g.reverse_sort ? e2 : e1;
|
|
|
|
|
const auto &b = g.reverse_sort ? e1 : e2;
|
|
|
|
|
switch (g.sort_column) {
|
|
|
|
|
case entry::MODES:
|
|
|
|
|
if (a.info.st_mode != b.info.st_mode)
|
|
|
|
|
return a.info.st_mode < b.info.st_mode;
|
|
|
|
|
break;
|
|
|
|
|
case entry::USER:
|
|
|
|
|
if (a.info.st_uid != b.info.st_uid)
|
|
|
|
|
return a.info.st_uid < b.info.st_uid;
|
|
|
|
|
break;
|
|
|
|
|
case entry::GROUP:
|
|
|
|
|
if (a.info.st_gid != b.info.st_gid)
|
|
|
|
|
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;
|
|
|
|
|
break;
|
|
|
|
|
case entry::MTIME:
|
|
|
|
|
if (a.info.st_mtime != b.info.st_mtime)
|
|
|
|
|
return a.info.st_mtime < b.info.st_mtime;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return a.filename < b.filename;
|
2018-11-02 17:25:10 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-02 16:31:36 +02:00
|
|
|
|
fun at_cursor () -> const entry & {
|
|
|
|
|
static entry invalid;
|
|
|
|
|
return g.cursor >= int (g.entries.size ()) ? invalid : g.entries[g.cursor];
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-30 12:32:40 +02:00
|
|
|
|
fun focus (const string &anchor) {
|
2021-09-26 09:38:16 +02:00
|
|
|
|
if (!anchor.empty ()) {
|
|
|
|
|
for (size_t i = 0; i < g.entries.size (); i++)
|
|
|
|
|
if (g.entries[i].filename == anchor)
|
|
|
|
|
g.cursor = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-30 12:32:40 +02:00
|
|
|
|
fun resort (const string anchor = at_cursor ().filename) {
|
|
|
|
|
sort (begin (g.entries), end (g.entries));
|
|
|
|
|
focus (anchor);
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-09 07:52:09 +01:00
|
|
|
|
fun show_message (const string &message, int ttl = 30) {
|
|
|
|
|
g.message = to_wide (message);
|
|
|
|
|
g.message_ttl = ttl;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-28 22:51:15 +02:00
|
|
|
|
fun reload (bool keep_anchor) {
|
2021-11-05 21:30:51 +01:00
|
|
|
|
g.unames.clear ();
|
2018-12-03 00:11:00 +01:00
|
|
|
|
while (auto *ent = getpwent ())
|
2021-10-05 20:15:52 +02:00
|
|
|
|
g.unames.emplace (ent->pw_uid, to_wide (ent->pw_name));
|
2021-11-05 21:30:51 +01:00
|
|
|
|
endpwent ();
|
2018-10-24 08:47:02 +02:00
|
|
|
|
|
2021-11-05 21:30:51 +01:00
|
|
|
|
g.gnames.clear ();
|
2018-12-03 00:11:00 +01:00
|
|
|
|
while (auto *ent = getgrent ())
|
2021-10-05 20:15:52 +02:00
|
|
|
|
g.gnames.emplace (ent->gr_gid, to_wide (ent->gr_name));
|
2021-11-05 21:30:51 +01:00
|
|
|
|
endgrent ();
|
2018-10-24 08:47:02 +02:00
|
|
|
|
|
2018-11-02 17:25:10 +01:00
|
|
|
|
string anchor;
|
2020-10-02 16:31:36 +02:00
|
|
|
|
if (keep_anchor)
|
|
|
|
|
anchor = at_cursor ().filename;
|
2018-11-02 17:25:10 +01:00
|
|
|
|
|
2018-10-24 08:47:02 +02:00
|
|
|
|
auto now = time (NULL); g.now = *localtime (&now);
|
2017-06-29 02:50:39 +02:00
|
|
|
|
auto dir = opendir (".");
|
|
|
|
|
g.entries.clear ();
|
2021-11-09 07:52:09 +01:00
|
|
|
|
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;
|
|
|
|
|
}
|
2017-06-29 02:50:39 +02:00
|
|
|
|
while (auto f = readdir (dir)) {
|
2018-11-02 17:45:18 +01:00
|
|
|
|
string name = f->d_name;
|
2017-06-29 02:50:39 +02:00
|
|
|
|
// Two dots are for navigation but this ain't as useful
|
2018-12-09 05:28:47 +01:00
|
|
|
|
if (name == ".")
|
|
|
|
|
continue;
|
|
|
|
|
if (name == ".." ? g.cwd != "/" : (name[0] != '.' || g.show_hidden))
|
2018-10-25 14:51:19 +02:00
|
|
|
|
g.entries.push_back (make_entry (f));
|
2017-06-29 02:50:39 +02:00
|
|
|
|
}
|
|
|
|
|
closedir (dir);
|
|
|
|
|
|
2021-11-09 07:52:09 +01:00
|
|
|
|
readfail:
|
2021-09-26 09:38:16 +02:00
|
|
|
|
g.out_of_date = false;
|
2018-10-25 14:51:19 +02:00
|
|
|
|
for (int col = 0; col < entry::COLUMNS; col++) {
|
2017-06-30 08:22:38 +02:00
|
|
|
|
auto &longest = g.max_widths[col] = 0;
|
|
|
|
|
for (const auto &entry : g.entries)
|
2018-10-25 14:51:19 +02:00
|
|
|
|
longest = max (longest, compute_width (entry.cols[col]));
|
2017-06-30 08:22:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-26 09:38:16 +02:00
|
|
|
|
resort (anchor);
|
|
|
|
|
|
2020-10-02 16:31:36 +02:00
|
|
|
|
g.cursor = max (0, min (g.cursor, int (g.entries.size ()) - 1));
|
|
|
|
|
g.offset = max (0, min (g.offset, int (g.entries.size ()) - 1));
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
|
|
|
|
if (g.inotify_wd != -1)
|
|
|
|
|
inotify_rm_watch (g.inotify_fd, g.inotify_wd);
|
|
|
|
|
|
2018-10-23 05:24:56 +02:00
|
|
|
|
// We don't show atime, so access and open are merely spam
|
2019-09-27 23:06:12 +02:00
|
|
|
|
g.inotify_wd = inotify_add_watch (g.inotify_fd, ".",
|
2018-10-23 05:24:56 +02:00
|
|
|
|
(IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
|
2017-06-29 02:50:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-11-05 21:30:51 +01:00
|
|
|
|
fun run_program (initializer_list<const char *> list, const string &filename) {
|
2020-09-29 00:55:01 +02:00
|
|
|
|
if (g.ext_helpers) {
|
|
|
|
|
// XXX: this doesn't try them all out, though it shouldn't make any
|
|
|
|
|
// noticeable difference
|
|
|
|
|
const char *found = nullptr;
|
|
|
|
|
for (auto program : list)
|
|
|
|
|
if ((found = program))
|
|
|
|
|
break;
|
2020-10-08 19:43:19 +02:00
|
|
|
|
g.ext_helper = found + (" " + shell_escape (filename));
|
2020-09-29 00:55:01 +02:00
|
|
|
|
g.quitting = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-12-03 22:05:00 +01:00
|
|
|
|
|
2020-09-29 00:55:01 +02:00
|
|
|
|
endwin ();
|
2018-12-03 22:05:00 +01:00
|
|
|
|
switch (pid_t child = fork ()) {
|
|
|
|
|
int status;
|
|
|
|
|
case -1:
|
|
|
|
|
break;
|
|
|
|
|
case 0:
|
|
|
|
|
// Put the child in a new foreground process group...
|
|
|
|
|
setpgid (0, 0);
|
|
|
|
|
tcsetpgrp (STDOUT_FILENO, getpgid (0));
|
|
|
|
|
|
2020-09-29 00:55:01 +02:00
|
|
|
|
for (auto program : list)
|
|
|
|
|
if (program) execl ("/bin/sh", "/bin/sh", "-c", (string (program)
|
2018-12-03 22:05:00 +01:00
|
|
|
|
+ " " + shell_escape (filename)).c_str (), NULL);
|
|
|
|
|
_exit (EXIT_FAILURE);
|
|
|
|
|
default:
|
|
|
|
|
// ...and make sure of it in the parent as well
|
|
|
|
|
(void) setpgid (child, child);
|
2019-12-09 19:01:16 +01:00
|
|
|
|
|
|
|
|
|
// We don't provide job control--don't let us hang after ^Z
|
|
|
|
|
while (waitpid (child, &status, WUNTRACED) > -1 && WIFSTOPPED (status))
|
|
|
|
|
if (WSTOPSIG (status) == SIGTSTP)
|
2020-10-08 19:27:19 +02:00
|
|
|
|
kill (-child, SIGCONT);
|
2018-12-03 22:05:00 +01:00
|
|
|
|
tcsetpgrp (STDOUT_FILENO, getpgid (0));
|
2020-09-28 20:04:52 +02:00
|
|
|
|
|
|
|
|
|
if (WIFEXITED (status) && WEXITSTATUS (status)) {
|
|
|
|
|
printf ("Helper returned non-zero exit status %d. "
|
|
|
|
|
"Press Enter to continue.\n", WEXITSTATUS (status));
|
|
|
|
|
string dummy; getline (cin, dummy);
|
|
|
|
|
}
|
2018-12-03 22:05:00 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refresh ();
|
|
|
|
|
update ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun view (const string &filename) {
|
2020-09-28 21:34:09 +02:00
|
|
|
|
// 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
|
|
|
|
|
run_program ({(const char *) getenv ("PAGER"), "less", "cat"}, filename);
|
2018-12-03 22:05:00 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun edit (const string &filename) {
|
|
|
|
|
run_program ({(const char *) getenv ("VISUAL"),
|
|
|
|
|
(const char *) getenv ("EDITOR"), "vi"}, filename);
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 15:54:32 +01:00
|
|
|
|
fun run_pager (FILE *contents) {
|
|
|
|
|
// We don't really need to set O_CLOEXEC, so we're not going to
|
|
|
|
|
rewind (contents);
|
|
|
|
|
endwin ();
|
|
|
|
|
|
|
|
|
|
switch (pid_t child = fork ()) {
|
|
|
|
|
int status;
|
|
|
|
|
case -1:
|
|
|
|
|
break;
|
|
|
|
|
case 0:
|
|
|
|
|
// Put the child in a new foreground process group...
|
|
|
|
|
setpgid (0, 0);
|
|
|
|
|
tcsetpgrp (STDOUT_FILENO, getpgid (0));
|
|
|
|
|
dup2 (fileno (contents), STDIN_FILENO);
|
|
|
|
|
|
|
|
|
|
// Behaviour copies man-db's man(1), similar to POSIX man(1)
|
2020-09-28 21:34:09 +02:00
|
|
|
|
for (auto pager : {(const char *) getenv ("PAGER"), "less", "cat"})
|
2018-11-01 15:54:32 +01:00
|
|
|
|
if (pager) execl ("/bin/sh", "/bin/sh", "-c", pager, NULL);
|
|
|
|
|
_exit (EXIT_FAILURE);
|
|
|
|
|
default:
|
|
|
|
|
// ...and make sure of it in the parent as well
|
|
|
|
|
(void) setpgid (child, child);
|
|
|
|
|
waitpid (child, &status, 0);
|
|
|
|
|
tcsetpgrp (STDOUT_FILENO, getpgid (0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refresh ();
|
|
|
|
|
update ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun encode_key (wint_t key) -> string {
|
|
|
|
|
string encoded;
|
|
|
|
|
if (key & ALT)
|
|
|
|
|
encoded.append ("M-");
|
|
|
|
|
wchar_t bare = key & ~ALT;
|
|
|
|
|
if (g.key_to_name.count (bare))
|
|
|
|
|
encoded.append (capitalize (g.key_to_name.at (bare)));
|
2020-10-08 13:25:00 +02:00
|
|
|
|
else if (bare < 32 || bare == 0x7f)
|
|
|
|
|
encoded.append ("C-").append ({char (tolower ((bare + 64) & 0x7f))});
|
2018-11-01 15:54:32 +01:00
|
|
|
|
else
|
|
|
|
|
encoded.append (to_mb ({bare}));
|
|
|
|
|
return encoded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun show_help () {
|
|
|
|
|
FILE *contents = tmpfile ();
|
|
|
|
|
if (!contents)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
for (const auto &kv : g_binding_contexts) {
|
|
|
|
|
fprintf (contents, "%s\n",
|
|
|
|
|
underline (capitalize (kv.first + " key bindings")).c_str ());
|
2020-10-23 03:49:39 +02:00
|
|
|
|
map<action, string> agg;
|
|
|
|
|
for (const auto &kv : *kv.second)
|
|
|
|
|
agg[kv.second] += encode_key (kv.first) + " ";
|
|
|
|
|
for (const auto &kv : agg) {
|
|
|
|
|
auto action = g.action_names[kv.first];
|
|
|
|
|
action.append (max (0, 20 - int (action.length ())), ' ');
|
|
|
|
|
fprintf (contents, "%s %s\n", action.c_str (), kv.second.c_str ());
|
2018-11-01 15:54:32 +01:00
|
|
|
|
}
|
|
|
|
|
fprintf (contents, "\n");
|
|
|
|
|
}
|
|
|
|
|
run_pager (contents);
|
|
|
|
|
fclose (contents);
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-03 12:11:18 +02:00
|
|
|
|
fun match (const wstring &needle, int push) -> int {
|
|
|
|
|
string pattern = to_mb (needle) + "*";
|
|
|
|
|
bool jump_to_first = push || fnmatch (pattern.c_str(),
|
|
|
|
|
g.entries[g.cursor].filename.c_str (), 0) == FNM_NOMATCH;
|
|
|
|
|
int best = g.cursor, matches = 0, step = push + !push;
|
2021-07-17 08:15:07 +02:00
|
|
|
|
for (int i = 0, count = g.entries.size (); i < count; i++) {
|
2021-07-19 09:12:17 +02:00
|
|
|
|
int o = (g.cursor + (count + i * step) + (count + push)) % count;
|
2022-09-03 12:11:18 +02:00
|
|
|
|
if (!fnmatch (pattern.c_str (), g.entries[o].filename.c_str (), 0)
|
|
|
|
|
&& !matches++ && jump_to_first)
|
2017-06-29 02:50:39 +02:00
|
|
|
|
best = o;
|
|
|
|
|
}
|
|
|
|
|
g.cursor = best;
|
2021-07-17 07:54:03 +02:00
|
|
|
|
return matches;
|
2017-06-29 02:50:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-03 12:11:18 +02:00
|
|
|
|
fun match_interactive (int push) {
|
|
|
|
|
int matches = match (g.editor_line, push);
|
2021-07-17 14:01:29 +02:00
|
|
|
|
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)";
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-03 12:11:18 +02:00
|
|
|
|
/// Stays on the current item unless there are better matches
|
|
|
|
|
fun lookup (const wstring &needle) {
|
|
|
|
|
int best = g.cursor;
|
|
|
|
|
size_t best_n = 0;
|
|
|
|
|
for (int i = 0, count = g.entries.size (); i < count; i++) {
|
|
|
|
|
int o = (g.cursor + i) % count;
|
|
|
|
|
size_t n = prefix_length (to_wide (g.entries[o].filename), needle);
|
|
|
|
|
if (n > best_n) {
|
|
|
|
|
best = o;
|
|
|
|
|
best_n = n;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.cursor = best;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 20:22:50 +01:00
|
|
|
|
fun fix_cursor_and_offset () {
|
|
|
|
|
g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
|
2020-10-02 16:31:36 +02:00
|
|
|
|
g.cursor = max (g.cursor, 0);
|
2018-11-02 20:22:50 +01:00
|
|
|
|
|
|
|
|
|
// Decrease the offset when more items can suddenly fit
|
|
|
|
|
int pushable = visible_lines () - (int (g.entries.size ()) - g.offset);
|
|
|
|
|
g.offset -= max (pushable, 0);
|
|
|
|
|
|
2019-09-27 21:08:12 +02:00
|
|
|
|
// Make sure the cursor is visible
|
2018-11-02 20:22:50 +01:00
|
|
|
|
g.offset = min (g.offset, int (g.entries.size ()) - 1);
|
2020-10-02 16:31:36 +02:00
|
|
|
|
g.offset = max (g.offset, 0);
|
2018-11-02 20:22:50 +01:00
|
|
|
|
|
|
|
|
|
if (g.offset > g.cursor)
|
|
|
|
|
g.offset = g.cursor;
|
|
|
|
|
if (g.cursor - g.offset >= visible_lines ())
|
|
|
|
|
g.offset = g.cursor - visible_lines () + 1;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 18:25:38 +01:00
|
|
|
|
fun is_ancestor_dir (const string &ancestor, const string &of) -> bool {
|
|
|
|
|
if (strncmp (ancestor.c_str (), of.c_str (), ancestor.length ()))
|
|
|
|
|
return false;
|
2018-11-02 12:35:03 +01:00
|
|
|
|
return of[ancestor.length ()] == '/' || (ancestor == "/" && ancestor != of);
|
2018-11-01 18:25:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-29 02:36:03 +02:00
|
|
|
|
/// If `path` is equal to the `current` directory, or lies underneath it,
|
|
|
|
|
/// return it as a relative path
|
|
|
|
|
fun relativize (string current, const string &path) -> string {
|
|
|
|
|
if (current == path)
|
|
|
|
|
return ".";
|
|
|
|
|
if (current.back () != '/')
|
|
|
|
|
current += '/';
|
|
|
|
|
if (!strncmp (current.c_str (), path.c_str (), current.length ()))
|
|
|
|
|
return path.substr (current.length ());
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-05 21:30:51 +01:00
|
|
|
|
fun pop_levels (const string &old_cwd) {
|
2018-11-02 15:09:27 +01:00
|
|
|
|
string anchor; auto i = g.levels.rbegin ();
|
2018-11-02 14:32:24 +01:00
|
|
|
|
while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) {
|
|
|
|
|
if (i->path == g.cwd) {
|
|
|
|
|
g.offset = i->offset;
|
|
|
|
|
g.cursor = i->cursor;
|
|
|
|
|
anchor = i->filename;
|
|
|
|
|
}
|
|
|
|
|
i++;
|
|
|
|
|
g.levels.pop_back ();
|
|
|
|
|
}
|
2020-09-29 02:36:03 +02:00
|
|
|
|
|
|
|
|
|
// Don't pick up bullshit from foreign history entries, especially for /
|
|
|
|
|
if (is_ancestor_dir (g.cwd, old_cwd)) {
|
|
|
|
|
auto subpath = relativize (g.cwd, old_cwd);
|
|
|
|
|
anchor = subpath.substr (0, subpath.find ('/'));
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 20:22:50 +01:00
|
|
|
|
fix_cursor_and_offset ();
|
2020-10-02 16:31:36 +02:00
|
|
|
|
if (!anchor.empty () && at_cursor ().filename != anchor)
|
2022-09-03 12:11:18 +02:00
|
|
|
|
lookup (to_wide (anchor));
|
2018-11-02 14:32:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-08 01:41:41 +01:00
|
|
|
|
fun explode_path (const string &path, vector<string> &out) {
|
|
|
|
|
size_t mark = 0, p = path.find ("/");
|
|
|
|
|
for (; p != string::npos; p = path.find ("/", (mark = p + 1)))
|
|
|
|
|
out.push_back (path.substr (mark, p - mark));
|
|
|
|
|
if (mark < path.length ())
|
|
|
|
|
out.push_back (path.substr (mark));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun serialize_path (const vector<string> &components) -> string {
|
|
|
|
|
string result;
|
|
|
|
|
for (const auto &i : components)
|
|
|
|
|
result.append (i).append ("/");
|
|
|
|
|
|
|
|
|
|
auto n = result.find_last_not_of ('/');
|
|
|
|
|
if (n != result.npos)
|
|
|
|
|
return result.erase (n + 1);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun absolutize (const string &abs_base, const string &path) -> string {
|
|
|
|
|
if (path[0] == '/')
|
|
|
|
|
return path;
|
|
|
|
|
if (!abs_base.empty () && abs_base.back () == '/')
|
|
|
|
|
return abs_base + path;
|
|
|
|
|
return abs_base + "/" + path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Roughly follows the POSIX description of `cd -L` because of symlinks.
|
|
|
|
|
// HOME and CDPATH handling is ommitted.
|
2018-11-01 12:31:02 +01:00
|
|
|
|
fun change_dir (const string &path) {
|
2018-12-08 01:41:41 +01:00
|
|
|
|
if (g.cwd[0] != '/') {
|
|
|
|
|
show_message ("cannot figure out absolute path");
|
|
|
|
|
beep ();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vector<string> in, out;
|
|
|
|
|
explode_path (absolutize (g.cwd, path), in);
|
|
|
|
|
|
|
|
|
|
// Paths with exactly two leading slashes may get special treatment
|
2018-12-09 05:28:47 +01:00
|
|
|
|
size_t startempty = 1;
|
2018-12-08 01:41:41 +01:00
|
|
|
|
if (in.size () >= 2 && in[1] == "" && (in.size () < 3 || in[2] != ""))
|
|
|
|
|
startempty = 2;
|
|
|
|
|
|
|
|
|
|
struct stat s{};
|
|
|
|
|
for (size_t i = 0; i < in.size (); i++)
|
|
|
|
|
if (in[i] == "..") {
|
|
|
|
|
auto parent = relativize (g.cwd, serialize_path (out));
|
|
|
|
|
if (errno = 0, !stat (parent.c_str (), &s) && !S_ISDIR (s.st_mode))
|
|
|
|
|
errno = ENOTDIR;
|
|
|
|
|
if (errno) {
|
|
|
|
|
show_message (parent + ": " + strerror (errno));
|
|
|
|
|
beep ();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-11-05 21:30:51 +01:00
|
|
|
|
if (!out.back ().empty ())
|
2018-12-08 01:41:41 +01:00
|
|
|
|
out.pop_back ();
|
|
|
|
|
} else if (in[i] != "." && (!in[i].empty () || i < startempty)) {
|
|
|
|
|
out.push_back (in[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto full_path = serialize_path (out);
|
|
|
|
|
if (chdir (relativize (g.cwd, full_path).c_str ())) {
|
2018-11-02 15:05:04 +01:00
|
|
|
|
show_message (strerror (errno));
|
2018-10-24 05:14:19 +02:00
|
|
|
|
beep ();
|
2018-11-01 18:25:38 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-02 16:31:36 +02:00
|
|
|
|
level last {g.offset, g.cursor, g.cwd, at_cursor ().filename};
|
2018-12-08 01:41:41 +01:00
|
|
|
|
g.cwd = full_path;
|
2020-09-29 02:31:49 +02:00
|
|
|
|
bool same_path = last.path == g.cwd;
|
2020-09-28 22:51:15 +02:00
|
|
|
|
reload (same_path);
|
2018-11-01 18:25:38 +01:00
|
|
|
|
|
2020-09-29 02:36:03 +02:00
|
|
|
|
if (!same_path) {
|
2018-11-01 18:25:38 +01:00
|
|
|
|
g.offset = g.cursor = 0;
|
2020-09-29 02:36:03 +02:00
|
|
|
|
if (is_ancestor_dir (last.path, g.cwd))
|
|
|
|
|
g.levels.push_back (last);
|
|
|
|
|
else
|
|
|
|
|
pop_levels (last.path);
|
2018-10-24 05:14:19 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-08 01:41:41 +01:00
|
|
|
|
// Roughly follows the POSIX description of the PWD environment variable
|
|
|
|
|
fun initial_cwd () -> string {
|
2020-10-02 16:31:36 +02:00
|
|
|
|
char cwd[4096] = ""; const char *pwd = getenv ("PWD");
|
|
|
|
|
if (!getcwd (cwd, sizeof cwd)) {
|
|
|
|
|
show_message (strerror (errno));
|
|
|
|
|
return pwd;
|
|
|
|
|
}
|
2018-12-08 01:41:41 +01:00
|
|
|
|
if (!pwd || pwd[0] != '/' || strlen (pwd) >= PATH_MAX)
|
|
|
|
|
return cwd;
|
|
|
|
|
|
|
|
|
|
// Extra slashes shouldn't break anything for us
|
|
|
|
|
vector<string> components;
|
|
|
|
|
explode_path (pwd, components);
|
|
|
|
|
for (const auto &i : components) {
|
|
|
|
|
if (i == "." || i == "..")
|
|
|
|
|
return cwd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if it "is an absolute pathname of the current working directory."
|
|
|
|
|
// This particular method won't match on bind mounts, which is desired.
|
|
|
|
|
char *real = realpath (pwd, nullptr);
|
|
|
|
|
bool ok = real && !strcmp (cwd, real);
|
|
|
|
|
free (real);
|
|
|
|
|
return ok ? pwd : cwd;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 22:12:34 +01:00
|
|
|
|
fun choose (const entry &entry) {
|
2017-06-30 23:33:52 +02:00
|
|
|
|
// Dive into directories and accessible symlinks to them
|
2018-10-25 14:51:19 +02:00
|
|
|
|
if (!S_ISDIR (entry.info.st_mode)
|
|
|
|
|
&& !S_ISDIR (entry.target_info.st_mode)) {
|
2017-06-30 23:33:52 +02:00
|
|
|
|
g.chosen = entry.filename;
|
2018-11-02 22:12:34 +01:00
|
|
|
|
g.quitting = true;
|
|
|
|
|
} else {
|
|
|
|
|
change_dir (entry.filename);
|
2017-06-30 23:33:52 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-22 00:33:07 +02:00
|
|
|
|
// Move the cursor in `diff` direction and look for non-combining characters
|
|
|
|
|
fun move_towards_spacing (int diff) -> bool {
|
|
|
|
|
g.editor_cursor += diff;
|
2021-11-05 21:30:51 +01:00
|
|
|
|
return g.editor_cursor <= 0 ||
|
|
|
|
|
g.editor_cursor >= int (g.editor_line.length ()) ||
|
|
|
|
|
wcwidth (g.editor_line.at (g.editor_cursor));
|
2020-10-22 00:33:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 21:04:13 +02:00
|
|
|
|
fun handle_editor (wint_t c) {
|
2021-07-17 14:01:29 +02:00
|
|
|
|
auto action = ACTION_NONE;
|
2020-10-23 01:32:43 +02:00
|
|
|
|
if (g.editor_inserting) {
|
|
|
|
|
(void) halfdelay (1);
|
|
|
|
|
g.editor_inserting = false;
|
2021-07-17 14:01:29 +02:00
|
|
|
|
} else {
|
|
|
|
|
auto i = g_input_actions.find (c);
|
|
|
|
|
if (i != g_input_actions.end ())
|
|
|
|
|
action = i->second;
|
|
|
|
|
|
|
|
|
|
auto m = g_binding_contexts.find (to_mb (g.editor));
|
2021-11-05 21:30:51 +01:00
|
|
|
|
if (m != g_binding_contexts.end () &&
|
|
|
|
|
(i = m->second->find (c)) != m->second->end ())
|
2021-07-17 14:01:29 +02:00
|
|
|
|
action = i->second;
|
2020-10-23 01:32:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-17 07:24:16 +02:00
|
|
|
|
auto original = g.editor_line;
|
2021-07-17 14:01:29 +02:00
|
|
|
|
switch (action) {
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_INPUT_CONFIRM:
|
2021-10-30 12:13:45 +02:00
|
|
|
|
if (auto handler = g.editor_on[action])
|
|
|
|
|
handler ();
|
2018-11-01 12:31:02 +01:00
|
|
|
|
// Fall-through
|
|
|
|
|
case ACTION_INPUT_ABORT:
|
2018-10-25 21:04:13 +02:00
|
|
|
|
g.editor = 0;
|
2021-07-17 12:44:01 +02:00
|
|
|
|
g.editor_info.clear ();
|
2020-10-21 23:13:09 +02:00
|
|
|
|
g.editor_line.clear ();
|
|
|
|
|
g.editor_cursor = 0;
|
2020-10-23 01:32:43 +02:00
|
|
|
|
g.editor_inserting = false;
|
2018-11-01 12:31:02 +01:00
|
|
|
|
g.editor_on_change = nullptr;
|
2021-07-17 14:01:29 +02:00
|
|
|
|
g.editor_on.clear ();
|
2021-07-17 07:24:16 +02:00
|
|
|
|
return;
|
2020-10-21 23:53:30 +02:00
|
|
|
|
case ACTION_INPUT_BEGINNING:
|
|
|
|
|
g.editor_cursor = 0;
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_INPUT_END:
|
|
|
|
|
g.editor_cursor = g.editor_line.length ();
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_INPUT_BACKWARD:
|
2021-11-05 21:30:51 +01:00
|
|
|
|
while (g.editor_cursor > 0 &&
|
|
|
|
|
!move_towards_spacing (-1))
|
2020-10-22 00:33:07 +02:00
|
|
|
|
;
|
2020-10-21 23:53:30 +02:00
|
|
|
|
break;
|
|
|
|
|
case ACTION_INPUT_FORWARD:
|
2021-11-05 21:30:51 +01:00
|
|
|
|
while (g.editor_cursor < int (g.editor_line.length ()) &&
|
|
|
|
|
!move_towards_spacing (+1))
|
2020-10-22 00:33:07 +02:00
|
|
|
|
;
|
2020-10-21 23:53:30 +02:00
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_INPUT_B_DELETE:
|
2020-10-21 23:33:32 +02:00
|
|
|
|
while (g.editor_cursor > 0) {
|
2020-10-22 00:33:07 +02:00
|
|
|
|
auto finished = move_towards_spacing (-1);
|
2020-10-21 23:33:32 +02:00
|
|
|
|
g.editor_line.erase (g.editor_cursor, 1);
|
2020-10-22 00:33:07 +02:00
|
|
|
|
if (finished)
|
2020-10-21 23:33:32 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
2018-10-25 21:04:13 +02:00
|
|
|
|
break;
|
2020-10-22 00:05:24 +02:00
|
|
|
|
case ACTION_INPUT_DELETE:
|
|
|
|
|
while (g.editor_cursor < int (g.editor_line.length ())) {
|
|
|
|
|
g.editor_line.erase (g.editor_cursor, 1);
|
2020-10-22 00:33:07 +02:00
|
|
|
|
if (move_towards_spacing (0))
|
2020-10-22 00:05:24 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2023-06-11 21:05:55 +02:00
|
|
|
|
case ACTION_INPUT_B_KILL_WORD:
|
|
|
|
|
{
|
|
|
|
|
int i = g.editor_cursor;
|
|
|
|
|
while (i && g.editor_line[--i] == L' ');
|
|
|
|
|
while (i-- && g.editor_line[i] != L' ');
|
|
|
|
|
i++;
|
|
|
|
|
|
|
|
|
|
g.editor_line.erase (i, g.editor_cursor - i);
|
|
|
|
|
g.editor_cursor = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2020-10-23 01:21:25 +02:00
|
|
|
|
case ACTION_INPUT_B_KILL_LINE:
|
|
|
|
|
g.editor_line.erase (0, g.editor_cursor);
|
|
|
|
|
g.editor_cursor = 0;
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_INPUT_KILL_LINE:
|
|
|
|
|
g.editor_line.erase (g.editor_cursor);
|
|
|
|
|
break;
|
2020-10-23 01:32:43 +02:00
|
|
|
|
case ACTION_INPUT_QUOTED_INSERT:
|
|
|
|
|
(void) raw ();
|
|
|
|
|
g.editor_inserting = true;
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
default:
|
2021-07-17 14:01:29 +02:00
|
|
|
|
if (auto handler = g.editor_on[action]) {
|
|
|
|
|
handler ();
|
|
|
|
|
} else if (c & (ALT | SYM)) {
|
2018-10-25 21:04:13 +02:00
|
|
|
|
beep ();
|
|
|
|
|
} else {
|
2020-10-21 23:13:09 +02:00
|
|
|
|
g.editor_line.insert (g.editor_cursor, 1, c);
|
|
|
|
|
g.editor_cursor++;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-07-17 07:24:16 +02:00
|
|
|
|
if (g.editor_on_change && g.editor_line != original)
|
|
|
|
|
g.editor_on_change ();
|
2018-10-25 21:04:13 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun handle (wint_t c) -> bool {
|
2020-04-19 22:40:57 +02:00
|
|
|
|
if (c == WEOF)
|
|
|
|
|
return false;
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
// If an editor is active, let it handle the key instead and eat it
|
|
|
|
|
if (g.editor) {
|
2018-10-25 21:04:13 +02:00
|
|
|
|
handle_editor (c);
|
2017-06-29 02:50:39 +02:00
|
|
|
|
c = WEOF;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-02 16:31:36 +02:00
|
|
|
|
const auto ¤t = at_cursor ();
|
2020-09-27 22:58:40 +02:00
|
|
|
|
bool is_directory =
|
|
|
|
|
S_ISDIR (current.info.st_mode) ||
|
|
|
|
|
S_ISDIR (current.target_info.st_mode);
|
|
|
|
|
|
2018-10-25 21:04:13 +02:00
|
|
|
|
auto i = g_normal_actions.find (c);
|
|
|
|
|
switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
|
|
|
|
|
case ACTION_CHOOSE_FULL:
|
2020-10-24 19:28:11 +02:00
|
|
|
|
// FIXME: in the root directory, this inserts //item
|
2018-11-02 15:50:58 +01:00
|
|
|
|
g.chosen = g.cwd + "/" + current.filename;
|
|
|
|
|
g.no_chdir = true;
|
2018-11-02 22:12:34 +01:00
|
|
|
|
g.quitting = true;
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_CHOOSE:
|
2018-11-02 22:12:34 +01:00
|
|
|
|
choose (current);
|
|
|
|
|
break;
|
2018-12-03 22:05:00 +01:00
|
|
|
|
case ACTION_VIEW:
|
2020-09-27 22:58:40 +02:00
|
|
|
|
// Mimic mc, it does not seem sensible to page directories
|
|
|
|
|
(is_directory ? change_dir : view) (current.filename);
|
2018-12-03 22:05:00 +01:00
|
|
|
|
break;
|
|
|
|
|
case ACTION_EDIT:
|
|
|
|
|
edit (current.filename);
|
|
|
|
|
break;
|
2018-11-01 15:54:32 +01:00
|
|
|
|
case ACTION_HELP:
|
|
|
|
|
show_help ();
|
2018-11-02 22:12:34 +01:00
|
|
|
|
break;
|
2018-11-02 15:50:58 +01:00
|
|
|
|
case ACTION_QUIT_NO_CHDIR:
|
|
|
|
|
g.no_chdir = true;
|
2018-11-02 22:12:34 +01:00
|
|
|
|
// Fall-through
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_QUIT:
|
2018-11-02 22:12:34 +01:00
|
|
|
|
g.quitting = true;
|
|
|
|
|
break;
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
2018-11-02 18:07:06 +01:00
|
|
|
|
case ACTION_SORT_LEFT:
|
|
|
|
|
g.sort_column = (g.sort_column + entry::COLUMNS - 1) % entry::COLUMNS;
|
2018-11-02 18:14:53 +01:00
|
|
|
|
g.sort_flash_ttl = 2;
|
2021-09-26 09:38:16 +02:00
|
|
|
|
resort ();
|
2018-11-02 18:07:06 +01:00
|
|
|
|
break;
|
|
|
|
|
case ACTION_SORT_RIGHT:
|
|
|
|
|
g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS;
|
2018-11-02 18:14:53 +01:00
|
|
|
|
g.sort_flash_ttl = 2;
|
2021-09-26 09:38:16 +02:00
|
|
|
|
resort ();
|
2018-11-02 18:07:06 +01:00
|
|
|
|
break;
|
|
|
|
|
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_UP:
|
2017-06-29 02:50:39 +02:00
|
|
|
|
g.cursor--;
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_DOWN:
|
2017-06-29 02:50:39 +02:00
|
|
|
|
g.cursor++;
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_TOP:
|
2017-06-29 02:50:39 +02:00
|
|
|
|
g.cursor = 0;
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_BOTTOM:
|
2017-06-29 02:50:39 +02:00
|
|
|
|
g.cursor = int (g.entries.size ()) - 1;
|
|
|
|
|
break;
|
|
|
|
|
|
2019-09-27 21:08:12 +02:00
|
|
|
|
case ACTION_HIGH:
|
|
|
|
|
g.cursor = g.offset;
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_MIDDLE:
|
|
|
|
|
g.cursor = g.offset + (min (int (g.entries.size ()) - g.offset,
|
|
|
|
|
visible_lines ()) - 1) / 2;
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_LOW:
|
|
|
|
|
g.cursor = g.offset + visible_lines () - 1;
|
|
|
|
|
break;
|
|
|
|
|
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_PAGE_PREVIOUS:
|
|
|
|
|
g.cursor -= LINES;
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_PAGE_NEXT:
|
|
|
|
|
g.cursor += LINES;
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_SCROLL_DOWN:
|
|
|
|
|
g.offset++;
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_SCROLL_UP:
|
|
|
|
|
g.offset--;
|
2022-01-08 11:23:36 +01:00
|
|
|
|
break;
|
2022-01-08 11:13:08 +01:00
|
|
|
|
case ACTION_CENTER:
|
|
|
|
|
g.offset = g.cursor - (visible_lines () - 1) / 2;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
break;
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
2018-11-02 15:09:27 +01:00
|
|
|
|
case ACTION_CHDIR:
|
|
|
|
|
g.editor = L"chdir";
|
2021-10-30 12:13:45 +02:00
|
|
|
|
g.editor_on[ACTION_INPUT_CONFIRM] = [] {
|
2020-09-28 01:13:06 +02:00
|
|
|
|
change_dir (untilde (to_mb (g.editor_line)));
|
2018-11-02 15:09:27 +01:00
|
|
|
|
};
|
|
|
|
|
break;
|
2020-09-15 19:15:30 +02:00
|
|
|
|
case ACTION_PARENT:
|
|
|
|
|
change_dir ("..");
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_GO_START:
|
2018-10-24 05:14:19 +02:00
|
|
|
|
change_dir (g.start_dir);
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_GO_HOME:
|
2020-09-28 01:13:06 +02:00
|
|
|
|
change_dir (untilde ("~"));
|
2018-10-24 05:14:19 +02:00
|
|
|
|
break;
|
|
|
|
|
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_SEARCH:
|
2018-11-01 12:31:02 +01:00
|
|
|
|
g.editor = L"search";
|
2022-09-03 12:11:18 +02:00
|
|
|
|
g.editor_on_change = [] { match_interactive (0); };
|
|
|
|
|
g.editor_on[ACTION_UP] = [] { match_interactive (-1); };
|
|
|
|
|
g.editor_on[ACTION_DOWN] = [] { match_interactive (+1); };
|
2021-10-30 12:13:45 +02:00
|
|
|
|
g.editor_on[ACTION_INPUT_CONFIRM] = [] { choose (at_cursor ()); };
|
2017-06-30 01:39:49 +02:00
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_RENAME_PREFILL:
|
2017-06-29 02:50:39 +02:00
|
|
|
|
g.editor_line = to_wide (current.filename);
|
2020-10-21 23:13:09 +02:00
|
|
|
|
g.editor_cursor = g.editor_line.length ();
|
2017-06-29 02:50:39 +02:00
|
|
|
|
// Fall-through
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_RENAME:
|
2018-11-01 12:31:02 +01:00
|
|
|
|
g.editor = L"rename";
|
2021-10-30 12:13:45 +02:00
|
|
|
|
g.editor_on[ACTION_INPUT_CONFIRM] = [] {
|
2018-11-01 12:31:02 +01:00
|
|
|
|
auto mb = to_mb (g.editor_line);
|
2021-07-17 14:41:57 +02:00
|
|
|
|
if (rename (at_cursor ().filename.c_str (), mb.c_str ()))
|
|
|
|
|
show_message (strerror (errno));
|
|
|
|
|
reload (true);
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_MKDIR:
|
|
|
|
|
g.editor = L"mkdir";
|
2021-10-30 12:13:45 +02:00
|
|
|
|
g.editor_on[ACTION_INPUT_CONFIRM] = [] {
|
2021-10-30 12:32:40 +02:00
|
|
|
|
auto mb = to_mb (g.editor_line);
|
|
|
|
|
if (mkdir (mb.c_str (), 0777))
|
2021-07-17 14:41:57 +02:00
|
|
|
|
show_message (strerror (errno));
|
2020-09-28 22:51:15 +02:00
|
|
|
|
reload (true);
|
2021-10-30 12:32:40 +02:00
|
|
|
|
focus (mb);
|
2018-11-01 12:31:02 +01:00
|
|
|
|
};
|
2017-06-29 02:50:39 +02:00
|
|
|
|
break;
|
|
|
|
|
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_TOGGLE_FULL:
|
|
|
|
|
g.full_view = !g.full_view;
|
|
|
|
|
break;
|
2018-11-02 17:25:10 +01:00
|
|
|
|
case ACTION_REVERSE_SORT:
|
|
|
|
|
g.reverse_sort = !g.reverse_sort;
|
2021-09-26 09:38:16 +02:00
|
|
|
|
resort ();
|
2018-11-02 17:25:10 +01:00
|
|
|
|
break;
|
2018-11-02 17:45:18 +01:00
|
|
|
|
case ACTION_SHOW_HIDDEN:
|
|
|
|
|
g.show_hidden = !g.show_hidden;
|
2020-09-28 22:51:15 +02:00
|
|
|
|
reload (true);
|
2018-11-02 17:45:18 +01:00
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_REDRAW:
|
2017-06-29 02:50:39 +02:00
|
|
|
|
clear ();
|
|
|
|
|
break;
|
2018-10-25 21:04:13 +02:00
|
|
|
|
case ACTION_RELOAD:
|
2020-09-28 22:51:15 +02:00
|
|
|
|
reload (true);
|
2017-06-29 02:50:39 +02:00
|
|
|
|
break;
|
|
|
|
|
default:
|
2018-10-25 21:04:13 +02:00
|
|
|
|
if (c != KEY (RESIZE) && c != WEOF)
|
|
|
|
|
beep ();
|
2017-06-29 02:50:39 +02:00
|
|
|
|
}
|
2018-11-02 20:22:50 +01:00
|
|
|
|
fix_cursor_and_offset ();
|
2017-06-29 02:50:39 +02:00
|
|
|
|
update ();
|
2018-11-02 22:12:34 +01:00
|
|
|
|
return !g.quitting;
|
2017-06-29 02:50:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 01:39:49 +02:00
|
|
|
|
fun inotify_check () {
|
2017-06-29 02:50:39 +02:00
|
|
|
|
// Only provide simple indication that contents might have changed
|
|
|
|
|
char buf[4096]; ssize_t len;
|
|
|
|
|
bool changed = false;
|
|
|
|
|
while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) {
|
|
|
|
|
const inotify_event *e;
|
|
|
|
|
for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
|
|
|
|
|
e = (const inotify_event *) buf;
|
|
|
|
|
if (e->wd == g.inotify_wd)
|
2017-06-30 21:34:11 +02:00
|
|
|
|
changed = g.out_of_date = true;
|
2017-06-29 02:50:39 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (changed)
|
|
|
|
|
update ();
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-01 11:54:01 +02:00
|
|
|
|
fun load_cmdline (int argc, char *argv[]) {
|
|
|
|
|
if (argc < 3)
|
|
|
|
|
return;
|
|
|
|
|
|
2020-10-01 13:31:39 +02:00
|
|
|
|
wstring line = to_wide (argv[1]); int cursor = atoi (argv[2]);
|
|
|
|
|
if (line.empty () || cursor < 0 || cursor > (int) line.length ())
|
2020-10-01 11:54:01 +02:00
|
|
|
|
return;
|
|
|
|
|
|
2020-10-01 13:31:39 +02:00
|
|
|
|
std::replace_if (begin (line), end (line), iswspace, L' ');
|
|
|
|
|
g.cmdline = apply_attrs (line += L' ', g.attrs[g.AT_CMDLINE]);
|
|
|
|
|
// It is tempting to touch the cchar_t directly, though let's rather not
|
|
|
|
|
g.cmdline[cursor] = cchar (g.attrs[g.AT_CMDLINE] ^ A_REVERSE, line[cursor]);
|
2020-10-01 11:54:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-15 02:57:58 +02:00
|
|
|
|
fun decode_ansi_sgr (const vector<string> &v) -> chtype {
|
|
|
|
|
vector<int> args;
|
|
|
|
|
for (const auto &arg : v) {
|
|
|
|
|
char *end; unsigned long ul = strtoul (arg.c_str (), &end, 10);
|
|
|
|
|
if (*end != '\0' || ul > 255)
|
|
|
|
|
return 0;
|
|
|
|
|
args.push_back (ul);
|
|
|
|
|
}
|
|
|
|
|
chtype result = 0; int fg = -1, bg = -1;
|
|
|
|
|
for (size_t i = 0; i < args.size (); i++) {
|
|
|
|
|
auto arg = args[i];
|
|
|
|
|
if (arg == 0) {
|
|
|
|
|
result = 0; fg = -1; bg = -1;
|
|
|
|
|
} else if (arg == 1) {
|
|
|
|
|
result |= A_BOLD;
|
|
|
|
|
} else if (arg == 4) {
|
|
|
|
|
result |= A_UNDERLINE;
|
|
|
|
|
} else if (arg == 5) {
|
|
|
|
|
result |= A_BLINK;
|
|
|
|
|
} else if (arg == 7) {
|
|
|
|
|
result |= A_REVERSE;
|
|
|
|
|
} else if (arg >= 30 && arg <= 37) {
|
|
|
|
|
fg = arg - 30;
|
|
|
|
|
} else if (arg >= 40 && arg <= 47) {
|
|
|
|
|
bg = arg - 40;
|
|
|
|
|
// Anything other than indexed colours will be rejected completely
|
|
|
|
|
} else if (arg == 38 && (i += 2) < args.size ()) {
|
|
|
|
|
if (args[i - 1] != 5 || (fg = args[i]) >= COLORS)
|
|
|
|
|
return 0;
|
|
|
|
|
} else if (arg == 48 && (i += 2) < args.size ()) {
|
|
|
|
|
if (args[i - 1] != 5 || (bg = args[i]) >= COLORS)
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (fg != -1 || bg != -1)
|
|
|
|
|
result |= COLOR_PAIR (allocate_pair (fg, bg));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun load_ls_colors (vector<string> colors) {
|
|
|
|
|
map<string, chtype> attrs;
|
|
|
|
|
for (const auto &pair : colors) {
|
|
|
|
|
auto equal = pair.find ('=');
|
|
|
|
|
if (equal == string::npos)
|
|
|
|
|
continue;
|
2018-10-25 17:26:36 +02:00
|
|
|
|
auto key = pair.substr (0, equal), value = pair.substr (equal + 1);
|
2021-11-05 21:30:51 +01:00
|
|
|
|
if (key != g_ls_colors[LS_SYMLINK] ||
|
|
|
|
|
!(g.ls_symlink_as_target = value == "target"))
|
2018-10-25 17:26:36 +02:00
|
|
|
|
attrs[key] = decode_ansi_sgr (split (value, ";"));
|
2017-07-15 02:57:58 +02:00
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < LS_COUNT; i++) {
|
|
|
|
|
auto m = attrs.find (g_ls_colors[i]);
|
|
|
|
|
if (m != attrs.end ())
|
|
|
|
|
g.ls_colors[i] = m->second;
|
|
|
|
|
}
|
|
|
|
|
for (const auto &pair : attrs) {
|
|
|
|
|
if (pair.first.substr (0, 2) == "*.")
|
|
|
|
|
g.ls_exts[pair.first.substr (2)] = pair.second;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 23:00:55 +02:00
|
|
|
|
fun load_colors () {
|
2017-06-30 21:29:10 +02:00
|
|
|
|
// Bail out on dumb terminals, there's not much one can do about them
|
|
|
|
|
if (!has_colors () || start_color () == ERR || use_default_colors () == ERR)
|
|
|
|
|
return;
|
2018-10-25 23:00:55 +02:00
|
|
|
|
if (const char *colors = getenv ("LS_COLORS"))
|
|
|
|
|
load_ls_colors (split (colors, ":"));
|
|
|
|
|
|
2018-11-02 14:32:24 +01:00
|
|
|
|
auto config = xdg_config_find ("look");
|
2018-10-25 23:00:55 +02:00
|
|
|
|
if (!config)
|
|
|
|
|
return;
|
2017-06-30 21:29:10 +02:00
|
|
|
|
|
2018-11-01 22:20:02 +01:00
|
|
|
|
vector<string> tokens;
|
|
|
|
|
while (parse_line (*config, tokens)) {
|
|
|
|
|
if (tokens.empty ())
|
2017-06-30 21:29:10 +02:00
|
|
|
|
continue;
|
|
|
|
|
auto name = shift (tokens);
|
|
|
|
|
for (int i = 0; i < g.AT_COUNT; i++)
|
|
|
|
|
if (name == g.attr_names[i])
|
|
|
|
|
g.attrs[i] = decode_attrs (tokens);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-19 22:40:57 +02:00
|
|
|
|
fun monotonic_ts_ms () -> int64_t {
|
|
|
|
|
timespec ts{1, 0}; // A very specific fail-safe value
|
|
|
|
|
(void) clock_gettime (CLOCK_MONOTONIC, &ts);
|
|
|
|
|
return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 21:04:13 +02:00
|
|
|
|
fun read_key (wint_t &c) -> bool {
|
2020-04-19 22:40:57 +02:00
|
|
|
|
// XXX: on at least some systems, when run over ssh in a bind handler,
|
|
|
|
|
// after closing the terminal emulator we receive no fatal signal but our
|
|
|
|
|
// parent shell gets reparented under init and our stdin gets closed,
|
|
|
|
|
// so we'd keep getting ERR in an infinite loop, as that is what ncurses
|
|
|
|
|
// automatically converts EOF into. The most reasonable way to detect this
|
|
|
|
|
// situation appears to be via timing. Checking errno doesn't work and
|
|
|
|
|
// resetting signal dispositions or the signal mask has no effect.
|
|
|
|
|
auto start = monotonic_ts_ms ();
|
2018-10-25 21:04:13 +02:00
|
|
|
|
int res = get_wch (&c);
|
2020-04-19 22:40:57 +02:00
|
|
|
|
if (res == ERR) {
|
|
|
|
|
c = WEOF;
|
|
|
|
|
if (monotonic_ts_ms () - start >= 50)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2018-10-25 21:04:13 +02:00
|
|
|
|
|
|
|
|
|
wint_t metafied{};
|
|
|
|
|
if (c == 27 && (res = get_wch (&metafied)) != ERR)
|
|
|
|
|
c = ALT | metafied;
|
|
|
|
|
if (res == KEY_CODE_YES)
|
|
|
|
|
c |= SYM;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 23:00:55 +02:00
|
|
|
|
fun parse_key (const string &key_name) -> wint_t {
|
|
|
|
|
wint_t c{};
|
|
|
|
|
auto p = key_name.c_str ();
|
|
|
|
|
if (!strncmp (p, "M-", 2)) {
|
|
|
|
|
c |= ALT;
|
|
|
|
|
p += 2;
|
|
|
|
|
}
|
2021-07-07 20:30:46 +02:00
|
|
|
|
if (g.name_to_key.count (p)) {
|
|
|
|
|
return c | g.name_to_key.at (p);
|
|
|
|
|
} else if (!strncmp (p, "C-", 2)) {
|
2018-10-25 23:00:55 +02:00
|
|
|
|
p += 2;
|
2021-07-09 04:36:43 +02:00
|
|
|
|
if (*p < '?' || *p > '~') {
|
2018-10-25 23:00:55 +02:00
|
|
|
|
cerr << "bindings: invalid combination: " << key_name << endl;
|
|
|
|
|
return WEOF;
|
|
|
|
|
}
|
2021-07-09 04:36:43 +02:00
|
|
|
|
c |= CTRL (*p);
|
2018-10-25 23:00:55 +02:00
|
|
|
|
p += 1;
|
|
|
|
|
} else {
|
|
|
|
|
wchar_t w; mbstate_t mb {};
|
|
|
|
|
auto len = strlen (p) + 1, res = mbrtowc (&w, p, len, &mb);
|
|
|
|
|
if (res == 0) {
|
|
|
|
|
cerr << "bindings: missing key name: " << key_name << endl;
|
|
|
|
|
return WEOF;
|
|
|
|
|
}
|
|
|
|
|
if (res == size_t (-1) || res == size_t (-2)) {
|
|
|
|
|
cerr << "bindings: invalid encoding: " << key_name << endl;
|
|
|
|
|
return WEOF;
|
|
|
|
|
}
|
|
|
|
|
c |= w;
|
|
|
|
|
p += res;
|
|
|
|
|
}
|
|
|
|
|
if (*p) {
|
|
|
|
|
cerr << "key name has unparsable trailing part: " << key_name << endl;
|
|
|
|
|
return WEOF;
|
|
|
|
|
}
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 15:54:32 +01:00
|
|
|
|
fun learn_named_key (const string &name, wint_t key) {
|
|
|
|
|
g.name_to_key[g.key_to_name[key] = name] = key;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 23:00:55 +02:00
|
|
|
|
fun load_bindings () {
|
2018-11-01 15:54:32 +01:00
|
|
|
|
learn_named_key ("space", ' ');
|
|
|
|
|
learn_named_key ("escape", 0x1b);
|
2021-07-07 20:30:46 +02:00
|
|
|
|
|
|
|
|
|
int kc = 0;
|
|
|
|
|
for (kc = KEY_MIN; kc <= KEY_MAX; kc++) {
|
2018-10-25 23:00:55 +02:00
|
|
|
|
const char *name = keyname (kc);
|
|
|
|
|
if (!name)
|
|
|
|
|
continue;
|
|
|
|
|
if (!strncmp (name, "KEY_", 4))
|
|
|
|
|
name += 4;
|
|
|
|
|
string filtered;
|
|
|
|
|
for (; *name; name++) {
|
|
|
|
|
if (*name != '(' && *name != ')')
|
|
|
|
|
filtered += *name;
|
|
|
|
|
}
|
2018-11-01 15:54:32 +01:00
|
|
|
|
learn_named_key (filtered, SYM | kc);
|
2018-10-25 23:00:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stringization in the preprocessor is a bit limited, we want lisp-case
|
|
|
|
|
map<string, action> actions;
|
2018-11-01 19:28:51 +01:00
|
|
|
|
int a = 0;
|
2018-10-25 23:00:55 +02:00
|
|
|
|
for (auto p : g_action_names) {
|
|
|
|
|
string name;
|
|
|
|
|
for (; *p; p++)
|
|
|
|
|
name += *p == '_' ? '-' : *p + 'a' - 'A';
|
2018-11-01 15:54:32 +01:00
|
|
|
|
g.action_names[a] = name;
|
2018-10-25 23:00:55 +02:00
|
|
|
|
actions[name] = action (a++);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-08 13:31:54 +02:00
|
|
|
|
auto config = xdg_config_find ("bindings");
|
|
|
|
|
if (!config)
|
|
|
|
|
return;
|
|
|
|
|
|
2018-11-01 22:20:02 +01:00
|
|
|
|
vector<string> tokens;
|
|
|
|
|
while (parse_line (*config, tokens)) {
|
|
|
|
|
if (tokens.empty ())
|
2018-10-25 23:00:55 +02:00
|
|
|
|
continue;
|
|
|
|
|
if (tokens.size () < 3) {
|
2021-07-07 20:30:46 +02:00
|
|
|
|
cerr << "bindings: expected: define name key-sequence"
|
|
|
|
|
" | context binding action";
|
2018-10-25 23:00:55 +02:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto context = tokens[0], key_name = tokens[1], action = tokens[2];
|
2021-07-07 20:30:46 +02:00
|
|
|
|
if (context == "define") {
|
|
|
|
|
// We haven't run initscr() yet, so define_key() would fail here
|
|
|
|
|
learn_named_key (key_name, SYM | (g.custom_keys[action] = ++kc));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 23:00:55 +02:00
|
|
|
|
auto m = g_binding_contexts.find (context);
|
|
|
|
|
if (m == g_binding_contexts.end ()) {
|
|
|
|
|
cerr << "bindings: invalid context: " << context << endl;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
wint_t c = parse_key (key_name);
|
|
|
|
|
if (c == WEOF)
|
|
|
|
|
continue;
|
|
|
|
|
auto i = actions.find (action);
|
|
|
|
|
if (i == actions.end ()) {
|
|
|
|
|
cerr << "bindings: invalid action: " << action << endl;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
(*m->second)[c] = i->second;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-02 14:32:24 +01:00
|
|
|
|
fun load_history_level (const vector<string> &v) {
|
2018-11-02 18:28:04 +01:00
|
|
|
|
if (v.size () != 7)
|
2018-11-02 14:32:24 +01:00
|
|
|
|
return;
|
|
|
|
|
// 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)});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun load_config () {
|
|
|
|
|
auto config = xdg_config_find ("config");
|
|
|
|
|
if (!config)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
vector<string> tokens;
|
|
|
|
|
while (parse_line (*config, tokens)) {
|
|
|
|
|
if (tokens.empty ())
|
|
|
|
|
continue;
|
|
|
|
|
|
2018-11-02 18:07:06 +01:00
|
|
|
|
if (tokens.front () == "full-view" && tokens.size () > 1)
|
|
|
|
|
g.full_view = tokens.at (1) == "1";
|
|
|
|
|
else if (tokens.front () == "gravity" && tokens.size () > 1)
|
|
|
|
|
g.gravity = tokens.at (1) == "1";
|
|
|
|
|
else if (tokens.front () == "reverse-sort" && tokens.size () > 1)
|
|
|
|
|
g.reverse_sort = tokens.at (1) == "1";
|
|
|
|
|
else if (tokens.front () == "show-hidden" && tokens.size () > 1)
|
|
|
|
|
g.show_hidden = tokens.at (1) == "1";
|
2020-09-29 00:55:01 +02:00
|
|
|
|
else if (tokens.front () == "ext-helpers" && tokens.size () > 1)
|
|
|
|
|
g.ext_helpers = tokens.at (1) == "1";
|
2018-11-02 18:07:06 +01:00
|
|
|
|
else if (tokens.front () == "sort-column" && tokens.size () > 1)
|
|
|
|
|
g.sort_column = stoi (tokens.at (1));
|
2018-11-02 14:32:24 +01:00
|
|
|
|
else if (tokens.front () == "history")
|
|
|
|
|
load_history_level (tokens);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun save_config () {
|
|
|
|
|
auto config = xdg_config_write ("config");
|
|
|
|
|
if (!config)
|
|
|
|
|
return;
|
|
|
|
|
|
2018-11-02 17:25:10 +01:00
|
|
|
|
write_line (*config, {"full-view", g.full_view ? "1" : "0"});
|
|
|
|
|
write_line (*config, {"gravity", g.gravity ? "1" : "0"});
|
|
|
|
|
write_line (*config, {"reverse-sort", g.reverse_sort ? "1" : "0"});
|
2018-11-02 17:45:18 +01:00
|
|
|
|
write_line (*config, {"show-hidden", g.show_hidden ? "1" : "0"});
|
2020-09-29 00:55:01 +02:00
|
|
|
|
write_line (*config, {"ext-helpers", g.ext_helpers ? "1" : "0"});
|
2018-11-02 14:32:24 +01:00
|
|
|
|
|
2018-11-02 18:07:06 +01:00
|
|
|
|
write_line (*config, {"sort-column", to_string (g.sort_column)});
|
|
|
|
|
|
2018-11-02 14:32:24 +01:00
|
|
|
|
char hostname[256];
|
|
|
|
|
if (gethostname (hostname, sizeof hostname))
|
|
|
|
|
*hostname = 0;
|
|
|
|
|
|
|
|
|
|
auto ppid = std::to_string (getppid ());
|
|
|
|
|
for (auto i = g.levels.begin (); i != g.levels.end (); i++)
|
|
|
|
|
write_line (*config, {"history", hostname, ppid, i->path,
|
|
|
|
|
to_string (i->offset), to_string (i->cursor), i->filename});
|
|
|
|
|
write_line (*config, {"history", hostname, ppid, g.cwd,
|
|
|
|
|
to_string (g.offset), to_string (g.cursor),
|
2020-10-02 16:31:36 +02:00
|
|
|
|
at_cursor ().filename});
|
2018-11-02 14:32:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-30 01:39:49 +02:00
|
|
|
|
int main (int argc, char *argv[]) {
|
2020-10-26 19:07:47 +01:00
|
|
|
|
if (argc == 2 && string (argv[1]) == "--version") {
|
|
|
|
|
cout << PROJECT_NAME << " " << PROJECT_VERSION << endl;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-04 14:20:10 +02:00
|
|
|
|
// zsh before 5.4 may close stdin before exec without redirection,
|
|
|
|
|
// since then it redirects stdin to /dev/null
|
2017-06-29 02:50:39 +02:00
|
|
|
|
(void) close (STDIN_FILENO);
|
|
|
|
|
if (open ("/dev/tty", O_RDWR)) {
|
|
|
|
|
cerr << "cannot open tty" << endl;
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save the original stdout and force ncurses to use the terminal directly
|
|
|
|
|
auto output_fd = dup (STDOUT_FILENO);
|
|
|
|
|
dup2 (STDIN_FILENO, STDOUT_FILENO);
|
|
|
|
|
|
2018-11-01 15:54:32 +01:00
|
|
|
|
// So that the neither us nor our children stop on tcsetpgrp()
|
|
|
|
|
signal (SIGTTOU, SIG_IGN);
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
|
|
|
|
|
cerr << "cannot initialize inotify" << endl;
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
locale::global (locale (""));
|
2018-10-25 23:00:55 +02:00
|
|
|
|
load_bindings ();
|
2018-11-02 14:32:24 +01:00
|
|
|
|
load_config ();
|
2018-10-25 23:00:55 +02:00
|
|
|
|
|
2018-10-24 08:53:01 +02:00
|
|
|
|
if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR) {
|
2017-06-29 02:50:39 +02:00
|
|
|
|
cerr << "cannot initialize screen" << endl;
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
2021-07-07 20:30:46 +02:00
|
|
|
|
for (const auto &definition_kc : g.custom_keys)
|
|
|
|
|
define_key (definition_kc.first.c_str (), definition_kc.second);
|
2017-06-30 21:29:10 +02:00
|
|
|
|
|
2018-10-25 23:00:55 +02:00
|
|
|
|
load_colors ();
|
2020-10-01 13:31:39 +02:00
|
|
|
|
load_cmdline (argc, argv);
|
2018-12-08 01:41:41 +01:00
|
|
|
|
g.start_dir = g.cwd = initial_cwd ();
|
2020-09-28 22:51:15 +02:00
|
|
|
|
reload (false);
|
2020-09-29 02:36:03 +02:00
|
|
|
|
pop_levels (g.cwd);
|
2018-06-22 17:10:36 +02:00
|
|
|
|
update ();
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
2021-11-09 07:45:20 +01:00
|
|
|
|
// Cunt, now I need to reïmplement all signal handling
|
|
|
|
|
#if NCURSES_VERSION_PATCH < 20210821
|
2021-10-18 11:23:17 +02:00
|
|
|
|
// This gets applied along with the following halfdelay()
|
|
|
|
|
cur_term->Nttyb.c_cc[VSTOP] =
|
|
|
|
|
cur_term->Nttyb.c_cc[VSTART] = _POSIX_VDISABLE;
|
2021-11-09 07:45:20 +01:00
|
|
|
|
#endif
|
2021-10-18 11:23:17 +02:00
|
|
|
|
|
2018-10-24 08:53:01 +02:00
|
|
|
|
// Invoking keypad() earlier would make ncurses flush its output buffer,
|
|
|
|
|
// which would worsen start-up flickering
|
|
|
|
|
if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
|
|
|
|
|
endwin ();
|
2018-10-25 23:00:55 +02:00
|
|
|
|
cerr << "cannot configure input" << endl;
|
2018-10-24 08:53:01 +02:00
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-29 02:50:39 +02:00
|
|
|
|
wint_t c;
|
2018-11-02 15:05:04 +01:00
|
|
|
|
while (!read_key (c) || handle (c)) {
|
2017-06-29 02:50:39 +02:00
|
|
|
|
inotify_check ();
|
2018-11-02 18:14:53 +01:00
|
|
|
|
if (g.sort_flash_ttl && !--g.sort_flash_ttl)
|
|
|
|
|
update ();
|
2018-11-02 15:05:04 +01:00
|
|
|
|
if (g.message_ttl && !--g.message_ttl) {
|
|
|
|
|
g.message.clear ();
|
|
|
|
|
update ();
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-06-29 02:50:39 +02:00
|
|
|
|
endwin ();
|
2018-11-02 14:32:24 +01:00
|
|
|
|
save_config ();
|
2017-06-29 02:50:39 +02:00
|
|
|
|
|
|
|
|
|
// Presumably it is going to end up as an argument, so quote it
|
|
|
|
|
if (!g.chosen.empty ())
|
|
|
|
|
g.chosen = shell_escape (g.chosen);
|
|
|
|
|
|
|
|
|
|
// We can't portably create a standard stream from an FD, so modify the FD
|
|
|
|
|
dup2 (output_fd, STDOUT_FILENO);
|
|
|
|
|
|
2020-10-21 08:18:39 +02:00
|
|
|
|
// TODO: avoid printing any of this unless the SDN envvar is set
|
2018-11-02 15:50:58 +01:00
|
|
|
|
if (g.cwd != g.start_dir && !g.no_chdir)
|
2017-06-29 02:50:39 +02:00
|
|
|
|
cout << "local cd=" << shell_escape (g.cwd) << endl;
|
2018-11-03 15:01:19 +01:00
|
|
|
|
else
|
|
|
|
|
cout << "local cd=" << endl;
|
|
|
|
|
|
|
|
|
|
cout << "local insert=" << shell_escape (g.chosen) << endl;
|
2020-09-29 00:55:01 +02:00
|
|
|
|
cout << "local helper=" << shell_escape (g.ext_helper) << endl;
|
2017-06-29 02:50:39 +02:00
|
|
|
|
return 0;
|
|
|
|
|
}
|