updated
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// sfm - Simple File Manager in C++17 with ncurses
|
||||
// sfm - Simple File Manager in C++17 with curses
|
||||
// Works with both ncurses and netbsd-curses.
|
||||
// Rewrite of the POSIX sh original for speed and efficiency
|
||||
#include <ncurses.h>
|
||||
#include <curses.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
@@ -18,6 +19,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
@@ -27,6 +29,140 @@ namespace fs = std::filesystem;
|
||||
// ─── terminal helpers ───────────────────────────────────────────────────────
|
||||
[[nodiscard]] bool can_color() { return has_colors(); }
|
||||
|
||||
// ─── key input normalization ────────────────────────────────────────────────
|
||||
// ncurses decodes escape sequences (arrows, Home/End, ...) into KEY_* codes
|
||||
// itself. netbsd-curses returns the raw bytes (e.g. ESC [ B for the down
|
||||
// arrow), which would leak stray characters into the command handling.
|
||||
// get_key() presents KEY_* codes (or 27 for a bare ESC) on both.
|
||||
|
||||
#ifdef NCURSES_VERSION
|
||||
|
||||
static int get_key() { return getch(); }
|
||||
|
||||
#else // manual escape-sequence decoding (netbsd-curses etc.)
|
||||
|
||||
static void nap_ms(int ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000000L;
|
||||
::nanosleep(&ts, nullptr);
|
||||
}
|
||||
|
||||
static int g_pushback[32];
|
||||
static int g_pushback_n = 0;
|
||||
|
||||
static void queue_key(int k) {
|
||||
if (g_pushback_n < static_cast<int>(sizeof(g_pushback) / sizeof(g_pushback[0])))
|
||||
g_pushback[g_pushback_n++] = k;
|
||||
}
|
||||
|
||||
// seq holds the bytes after ESC; seq[0] is '[' (CSI) or 'O' (SS3).
|
||||
static int decode_escape_seq(const std::string &seq) {
|
||||
if (seq.size() < 2) return ERR;
|
||||
char lead = seq[0];
|
||||
char fin = seq.back();
|
||||
if (lead == 'O') {
|
||||
if (seq.size() != 2) return ERR;
|
||||
switch (fin) {
|
||||
case 'A': return KEY_UP;
|
||||
case 'B': return KEY_DOWN;
|
||||
case 'C': return KEY_RIGHT;
|
||||
case 'D': return KEY_LEFT;
|
||||
case 'H': return KEY_HOME;
|
||||
case 'F': return KEY_END;
|
||||
case 'P': case 'Q': case 'R': case 'S': return KEY_F(fin - 'P' + 1);
|
||||
}
|
||||
return ERR;
|
||||
}
|
||||
if (lead != '[') return ERR;
|
||||
switch (fin) {
|
||||
case 'A': return KEY_UP;
|
||||
case 'B': return KEY_DOWN;
|
||||
case 'C': return KEY_RIGHT;
|
||||
case 'D': return KEY_LEFT;
|
||||
case 'H': return KEY_HOME;
|
||||
case 'F': return KEY_END;
|
||||
case 'Z': return KEY_BTAB;
|
||||
}
|
||||
// CSI n~ : Home/Insert/Delete/End/PgUp/PgDn (single digit only; multi-
|
||||
// digit and modified variants fall through and are swallowed).
|
||||
if (seq.size() == 3 && fin == '~' && seq[1] >= '0' && seq[1] <= '9') {
|
||||
switch (seq[1]) {
|
||||
case '1': case '7': return KEY_HOME;
|
||||
case '2': return KEY_IC;
|
||||
case '3': return KEY_DC;
|
||||
case '4': case '8': return KEY_END;
|
||||
case '5': return KEY_PPAGE;
|
||||
case '6': return KEY_NPAGE;
|
||||
}
|
||||
}
|
||||
return ERR;
|
||||
}
|
||||
|
||||
// Like getch(), but assembles raw ESC sequences into KEY_* codes. A bare ESC
|
||||
// is returned as 27 (possibly after a short wait for the rest of a sequence).
|
||||
static int get_key() {
|
||||
if (g_pushback_n > 0) return g_pushback[--g_pushback_n];
|
||||
|
||||
int k = getch();
|
||||
if (k != 27) return k;
|
||||
|
||||
// ESC — check whether an escape sequence follows. Real terminals send
|
||||
// the whole sequence in one burst, so the bytes are already queued.
|
||||
nodelay(stdscr, TRUE);
|
||||
std::string seq;
|
||||
int c;
|
||||
for (int i = 0; i < 4 && seq.empty(); ++i) {
|
||||
c = getch();
|
||||
if (c == ERR) nap_ms(10);
|
||||
else seq += static_cast<char>(c);
|
||||
}
|
||||
if (seq.empty()) { nodelay(stdscr, FALSE); return 27; } // bare ESC
|
||||
|
||||
if (seq[0] == '[') {
|
||||
// CSI: parameter/intermediate bytes, then a final byte (0x40-0x7E).
|
||||
// The lead '[' itself is in that range, so collect at least one more.
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (seq.size() >= 2 && seq.back() >= 0x40 && seq.back() <= 0x7E) break;
|
||||
c = getch();
|
||||
if (c == ERR) { nap_ms(10); c = getch(); }
|
||||
if (c == ERR) break;
|
||||
seq += static_cast<char>(c);
|
||||
}
|
||||
} else if (seq[0] == 'O') {
|
||||
// SS3: exactly one final byte follows.
|
||||
for (int i = 0; i < 4 && seq.size() < 2; ++i) {
|
||||
c = getch();
|
||||
if (c == ERR) { nap_ms(10); c = getch(); }
|
||||
if (c == ERR) break;
|
||||
seq += static_cast<char>(c);
|
||||
}
|
||||
}
|
||||
nodelay(stdscr, FALSE);
|
||||
|
||||
int key = decode_escape_seq(seq);
|
||||
if (key == ERR) {
|
||||
bool complete = seq.back() >= 0x40 && seq.back() <= 0x7E;
|
||||
if (!complete) {
|
||||
// Truncated sequence — replay it as raw keys (ESC first).
|
||||
for (size_t i = seq.size(); i-- > 0;)
|
||||
queue_key(static_cast<unsigned char>(seq[i]));
|
||||
queue_key(27);
|
||||
return 27;
|
||||
}
|
||||
// Unknown but well-formed sequence (e.g. an F-key): swallow it,
|
||||
// like curses implementations do.
|
||||
return ERR;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// netbsd-curses never reports KEY_RESIZE; catch SIGWINCH ourselves.
|
||||
static volatile sig_atomic_t g_winch = 0;
|
||||
static void on_sigwinch(int) { g_winch = 1; }
|
||||
|
||||
#endif // NCURSES_VERSION
|
||||
|
||||
// Color pair indices
|
||||
enum {
|
||||
CP_DIR = 1,
|
||||
@@ -200,6 +336,9 @@ private:
|
||||
const std::vector<std::string> &items,
|
||||
bool allow_cancel = true);
|
||||
|
||||
// ── input ──────────────────────────────────────────────────────────────
|
||||
bool handle_key(int key); // returns false to quit
|
||||
|
||||
// ── line input ─────────────────────────────────────────────────────────
|
||||
bool read_line(std::string &out, const std::string &prompt,
|
||||
const std::string &initial = "");
|
||||
@@ -945,7 +1084,7 @@ bool FileManager::confirm_overlay(const std::string &prompt) {
|
||||
|
||||
refresh();
|
||||
|
||||
int ch = getch();
|
||||
int ch = get_key();
|
||||
switch (ch) {
|
||||
case KEY_LEFT: case 'h': sel_yes = true; break;
|
||||
case KEY_RIGHT: case 'l': sel_yes = false; break;
|
||||
@@ -956,18 +1095,7 @@ bool FileManager::confirm_overlay(const std::string &prompt) {
|
||||
case '\n': case '\r': case KEY_ENTER:
|
||||
need_full_redraw_ = true;
|
||||
return sel_yes;
|
||||
case 27: {
|
||||
nodelay(stdscr, TRUE);
|
||||
int n = getch();
|
||||
nodelay(stdscr, FALSE);
|
||||
if (n == ERR) { need_full_redraw_ = true; return false; }
|
||||
if (n == '[') {
|
||||
int m = getch();
|
||||
if (m == 'D' || m == 'C') { sel_yes = !sel_yes; break; }
|
||||
}
|
||||
need_full_redraw_ = true;
|
||||
return false;
|
||||
}
|
||||
case 27: need_full_redraw_ = true; return false;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -1066,7 +1194,7 @@ std::string FileManager::overlay_picker(const std::string &title,
|
||||
|
||||
refresh();
|
||||
|
||||
int ch = getch();
|
||||
int ch = get_key();
|
||||
switch (ch) {
|
||||
case 'j': case KEY_DOWN: psel++; break;
|
||||
case 'k': case KEY_UP: psel--; break;
|
||||
@@ -1077,20 +1205,7 @@ std::string FileManager::overlay_picker(const std::string &title,
|
||||
case 'q': case 'Q': case 'h':
|
||||
if (allow_cancel) { need_full_redraw_ = true; return ""; }
|
||||
break;
|
||||
case 27: {
|
||||
nodelay(stdscr, TRUE);
|
||||
int n = getch();
|
||||
nodelay(stdscr, FALSE);
|
||||
if (n == ERR) { need_full_redraw_ = true; return ""; }
|
||||
if (n == '[') {
|
||||
int m = getch();
|
||||
if (m == 'A') psel--;
|
||||
else if (m == 'B') psel++;
|
||||
else if (m == 'D') { need_full_redraw_ = true; return ""; }
|
||||
else { need_full_redraw_ = true; return ""; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 27: need_full_redraw_ = true; return "";
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -1126,18 +1241,12 @@ bool FileManager::read_line(std::string &out, const std::string &prompt,
|
||||
move(bot, static_cast<int>(prompt.size() + pos));
|
||||
refresh();
|
||||
|
||||
int ch = getch();
|
||||
if (ch == 27) { // ESC
|
||||
nodelay(stdscr, TRUE);
|
||||
int n = getch();
|
||||
nodelay(stdscr, FALSE);
|
||||
if (n == ERR) {
|
||||
leaveok(stdscr, TRUE);
|
||||
curs_set(0);
|
||||
out.clear();
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
int ch = get_key();
|
||||
if (ch == 27) { // bare ESC — cancel
|
||||
leaveok(stdscr, TRUE);
|
||||
curs_set(0);
|
||||
out.clear();
|
||||
return false;
|
||||
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
|
||||
leaveok(stdscr, TRUE);
|
||||
curs_set(0);
|
||||
@@ -1307,20 +1416,14 @@ void FileManager::do_search() {
|
||||
need_full_redraw_ = true;
|
||||
draw();
|
||||
|
||||
int ch = getch();
|
||||
if (ch == 27) { // ESC
|
||||
nodelay(stdscr, TRUE);
|
||||
int n = getch();
|
||||
nodelay(stdscr, FALSE);
|
||||
if (n == ERR) {
|
||||
// Bare ESC — clear filter and exit search
|
||||
filter_.clear();
|
||||
searching_ = false;
|
||||
apply_filter();
|
||||
curs_set(0);
|
||||
need_full_redraw_ = true;
|
||||
}
|
||||
// else: arrow key or other sequence — ignore
|
||||
int ch = get_key();
|
||||
if (ch == 27) {
|
||||
// Bare ESC — clear filter and exit search
|
||||
filter_.clear();
|
||||
searching_ = false;
|
||||
apply_filter();
|
||||
curs_set(0);
|
||||
need_full_redraw_ = true;
|
||||
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
|
||||
searching_ = false;
|
||||
curs_set(0);
|
||||
@@ -1475,13 +1578,7 @@ void FileManager::do_info() {
|
||||
refresh();
|
||||
|
||||
// Wait for any key
|
||||
nodelay(stdscr, FALSE);
|
||||
int ch = getch();
|
||||
if (ch == 27) {
|
||||
nodelay(stdscr, TRUE);
|
||||
getch();
|
||||
nodelay(stdscr, FALSE);
|
||||
}
|
||||
get_key();
|
||||
need_full_redraw_ = true;
|
||||
}
|
||||
|
||||
@@ -1598,13 +1695,7 @@ void FileManager::do_help() {
|
||||
refresh();
|
||||
|
||||
// Wait for any key
|
||||
nodelay(stdscr, FALSE);
|
||||
int ch = getch();
|
||||
if (ch == 27) {
|
||||
nodelay(stdscr, TRUE);
|
||||
getch();
|
||||
nodelay(stdscr, FALSE);
|
||||
}
|
||||
get_key();
|
||||
need_full_redraw_ = true;
|
||||
}
|
||||
|
||||
@@ -2327,6 +2418,68 @@ void FileManager::open_file(const std::string &path) {
|
||||
}
|
||||
|
||||
// ─── main event loop ────────────────────────────────────────────────────────
|
||||
bool FileManager::handle_key(int key) {
|
||||
switch (key) {
|
||||
case 'j': case KEY_DOWN:
|
||||
if (!entries_.empty() && sel_ < static_cast<int>(entries_.size()) - 1)
|
||||
sel_++;
|
||||
break;
|
||||
case 'k': case KEY_UP:
|
||||
if (sel_ > 0) sel_--;
|
||||
break;
|
||||
case 'g': sel_ = 0; break;
|
||||
case 'G':
|
||||
if (!entries_.empty()) sel_ = static_cast<int>(entries_.size()) - 1;
|
||||
break;
|
||||
case KEY_NPAGE:
|
||||
sel_ = std::min(sel_ + rows_ / 2,
|
||||
std::max(0, static_cast<int>(entries_.size()) - 1));
|
||||
break;
|
||||
case KEY_PPAGE:
|
||||
sel_ = std::max(sel_ - rows_ / 2, 0);
|
||||
break;
|
||||
case KEY_DC: do_delete(); break;
|
||||
case '\n': case '\r': case KEY_ENTER: case 'l': case KEY_RIGHT:
|
||||
do_open(); break;
|
||||
case 'h': case KEY_LEFT:
|
||||
do_go_back(); break;
|
||||
case 'b': do_bookmark_add(); break;
|
||||
case 'B': do_bookmark_jump(); break;
|
||||
case '?': do_help(); break;
|
||||
case 'R': load_entries(); flash_msg("refreshed"); break;
|
||||
case '/': do_search(); break;
|
||||
case '.': do_toggle_hidden(); break;
|
||||
case 'T': do_toggle_details(); break;
|
||||
case 'P': do_toggle_preview(); break;
|
||||
case 'i': do_info(); break;
|
||||
case '+': do_chmod_x(true); break;
|
||||
case '-': do_chmod_x(false); break;
|
||||
case 'o': do_open_with(); break;
|
||||
case 's': do_sort(); break;
|
||||
case 'u': do_trash(); break;
|
||||
case 'U': do_open_trash(); break;
|
||||
case 'f': do_find(); break;
|
||||
case ':': do_jump_path(); break;
|
||||
case '~': do_go_home(); break;
|
||||
case '`': do_jump_back(); break;
|
||||
case 'c': do_copy_path(); break;
|
||||
case 27: do_clear_filter(); break; // ESC key
|
||||
case ' ': do_toggle_select(); break;
|
||||
case 'a': do_select_all(); break;
|
||||
case 'y': do_yank(); break;
|
||||
case 'x': do_cut(); break;
|
||||
case 'p': do_paste(); break;
|
||||
case 'd': do_delete(); break;
|
||||
case 'r': do_rename(); break;
|
||||
case 'm': do_mkdir(); break;
|
||||
case 'n': do_newfile(); break;
|
||||
case 'q': case 'Q': return false;
|
||||
case '!': do_shell(); break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileManager::run() {
|
||||
setup_term();
|
||||
if (can_color()) init_colors();
|
||||
@@ -2334,75 +2487,43 @@ void FileManager::run() {
|
||||
|
||||
is_root_ = (geteuid() == 0);
|
||||
|
||||
while (true) {
|
||||
draw();
|
||||
int key = getch();
|
||||
#ifndef NCURSES_VERSION
|
||||
struct sigaction sa;
|
||||
std::memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = on_sigwinch;
|
||||
sigaction(SIGWINCH, &sa, nullptr);
|
||||
#endif
|
||||
|
||||
// Handle terminal resize
|
||||
draw();
|
||||
while (true) {
|
||||
#ifndef NCURSES_VERSION
|
||||
// netbsd-curses never delivers KEY_RESIZE; poll for SIGWINCH and
|
||||
// re-query the terminal size ourselves.
|
||||
if (g_winch) {
|
||||
g_winch = 0;
|
||||
endwin(); // endwin()+refresh() makes curses re-read the size
|
||||
refresh();
|
||||
need_full_redraw_ = true;
|
||||
draw();
|
||||
}
|
||||
nodelay(stdscr, TRUE);
|
||||
#endif
|
||||
int key = get_key();
|
||||
#ifndef NCURSES_VERSION
|
||||
nodelay(stdscr, FALSE);
|
||||
if (key == ERR) { nap_ms(20); continue; }
|
||||
#endif
|
||||
|
||||
// Handle terminal resize (ncurses)
|
||||
if (key == KEY_RESIZE) {
|
||||
update_size();
|
||||
need_full_redraw_ = true;
|
||||
draw();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'j': case KEY_DOWN:
|
||||
if (!entries_.empty() && sel_ < static_cast<int>(entries_.size()) - 1)
|
||||
sel_++;
|
||||
break;
|
||||
case 'k': case KEY_UP:
|
||||
if (sel_ > 0) sel_--;
|
||||
break;
|
||||
case 'g': sel_ = 0; break;
|
||||
case 'G':
|
||||
if (!entries_.empty()) sel_ = static_cast<int>(entries_.size()) - 1;
|
||||
break;
|
||||
case KEY_NPAGE:
|
||||
sel_ = std::min(sel_ + rows_ / 2,
|
||||
std::max(0, static_cast<int>(entries_.size()) - 1));
|
||||
break;
|
||||
case KEY_PPAGE:
|
||||
sel_ = std::max(sel_ - rows_ / 2, 0);
|
||||
break;
|
||||
case KEY_DC: do_delete(); break;
|
||||
case '\n': case '\r': case KEY_ENTER: case 'l': case KEY_RIGHT:
|
||||
do_open(); break;
|
||||
case 'h': case KEY_LEFT:
|
||||
do_go_back(); break;
|
||||
case 'b': do_bookmark_add(); break;
|
||||
case 'B': do_bookmark_jump(); break;
|
||||
case '?': do_help(); break;
|
||||
case 'R': load_entries(); flash_msg("refreshed"); break;
|
||||
case '/': do_search(); break;
|
||||
case '.': do_toggle_hidden(); break;
|
||||
case 'T': do_toggle_details(); break;
|
||||
case 'P': do_toggle_preview(); break;
|
||||
case 'i': do_info(); break;
|
||||
case '+': do_chmod_x(true); break;
|
||||
case '-': do_chmod_x(false); break;
|
||||
case 'o': do_open_with(); break;
|
||||
case 's': do_sort(); break;
|
||||
case 'u': do_trash(); break;
|
||||
case 'U': do_open_trash(); break;
|
||||
case 'f': do_find(); break;
|
||||
case ':': do_jump_path(); break;
|
||||
case '~': do_go_home(); break;
|
||||
case '`': do_jump_back(); break;
|
||||
case 'c': do_copy_path(); break;
|
||||
case 27: do_clear_filter(); break; // ESC key
|
||||
case ' ': do_toggle_select(); break;
|
||||
case 'a': do_select_all(); break;
|
||||
case 'y': do_yank(); break;
|
||||
case 'x': do_cut(); break;
|
||||
case 'p': do_paste(); break;
|
||||
case 'd': do_delete(); break;
|
||||
case 'r': do_rename(); break;
|
||||
case 'm': do_mkdir(); break;
|
||||
case 'n': do_newfile(); break;
|
||||
case 'q': case 'Q': return;
|
||||
case '!': do_shell(); break;
|
||||
default: break;
|
||||
}
|
||||
if (!handle_key(key)) return;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user