Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53567c1f20 | ||
|
|
c6f4023768 | ||
|
|
cc0dbb0cad | ||
|
|
cd2a4dffb8 |
@@ -7,7 +7,10 @@ DOCDIR ?= $(PREFIX)/share/doc/sfm
|
|||||||
|
|
||||||
CXX ?= g++
|
CXX ?= g++
|
||||||
CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra
|
CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra
|
||||||
LDFLAGS ?= -lncursesw
|
# Pick a curses library: prefer ncursesw, fall back to ncurses, then plain
|
||||||
|
# curses (netbsd-curses and BSD base systems provide only libcurses).
|
||||||
|
CURSES_LIBS ?= $(shell pkg-config --libs ncursesw 2>/dev/null || pkg-config --libs ncurses 2>/dev/null || echo -lcurses)
|
||||||
|
LDLIBS ?= $(CURSES_LIBS)
|
||||||
|
|
||||||
INSTALL ?= install
|
INSTALL ?= install
|
||||||
RM ?= rm -f
|
RM ?= rm -f
|
||||||
@@ -17,7 +20,7 @@ RM ?= rm -f
|
|||||||
all: sfm
|
all: sfm
|
||||||
|
|
||||||
sfm: sfm.cpp
|
sfm: sfm.cpp
|
||||||
$(CXX) $(CXXFLAGS) -o $@ $< $(LDFLAGS)
|
$(CXX) $(CXXFLAGS) -o $@ $< $(LDLIBS)
|
||||||
|
|
||||||
install: all
|
install: all
|
||||||
@echo "Installing sfm to $(DESTDIR)$(BINDIR)/sfm ..."
|
@echo "Installing sfm to $(DESTDIR)$(BINDIR)/sfm ..."
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ sfm - Simple File Manager
|
|||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
-----------
|
-----------
|
||||||
sfm is a lightweight, terminal-based file manager with a vim-inspired
|
sfm is a lightweight, terminal-based file manager with a vim-inspired
|
||||||
key layout. The primary version is a fast C++/ncurses binary. A portable
|
key layout. The primary version is a fast C++ binary that works with
|
||||||
POSIX sh version (sfm.sh) is also included, requiring only standard Unix
|
both ncurses and netbsd-curses. A portable POSIX sh version (sfm.sh) is
|
||||||
tools (ls, awk, tput, stty, mv, cp, rm).
|
also included, requiring only standard Unix tools (ls, awk, tput, stty,
|
||||||
|
mv, cp, rm).
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS
|
REQUIREMENTS
|
||||||
------------
|
------------
|
||||||
C++ version (sfm):
|
C++ version (sfm):
|
||||||
- ncursesw (libncursesw)
|
- A curses library: ncursesw, ncurses, or netbsd-curses (libcurses)
|
||||||
- A C++17 compiler (g++ or compatible)
|
- A C++17 compiler (g++ or compatible)
|
||||||
|
|
||||||
Shell version (sfm.sh):
|
Shell version (sfm.sh):
|
||||||
@@ -151,8 +152,8 @@ MULTI-SELECT
|
|||||||
|
|
||||||
BOOKMARKS
|
BOOKMARKS
|
||||||
---------
|
---------
|
||||||
b Bookmark current directory (press again to remove)
|
b Open bookmark picker (j/k navigate, enter jump, del remove)
|
||||||
B Open bookmark picker (j/k navigate, enter jump)
|
B Bookmark current directory (press again to remove)
|
||||||
|
|
||||||
Bookmarks are saved to: ~/.config/sfm/bookmarks
|
Bookmarks are saved to: ~/.config/sfm/bookmarks
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,11 @@ Multi-select works with
|
|||||||
.SS Bookmarks
|
.SS Bookmarks
|
||||||
.TP
|
.TP
|
||||||
.B b
|
.B b
|
||||||
Bookmark the current directory. Press again to remove it.
|
Open the bookmark picker.
|
||||||
|
Use j/k to navigate, enter to jump, del to remove, esc to close.
|
||||||
.TP
|
.TP
|
||||||
.B B
|
.B B
|
||||||
Open the bookmark picker.
|
Bookmark the current directory. Press again to remove it.
|
||||||
Use j/k to navigate, enter to jump, esc to close.
|
|
||||||
|
|
||||||
.SS Other
|
.SS Other
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
@@ -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
|
// Rewrite of the POSIX sh original for speed and efficiency
|
||||||
#include <ncurses.h>
|
#include <curses.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <grp.h>
|
#include <grp.h>
|
||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -27,6 +30,140 @@ namespace fs = std::filesystem;
|
|||||||
// ─── terminal helpers ───────────────────────────────────────────────────────
|
// ─── terminal helpers ───────────────────────────────────────────────────────
|
||||||
[[nodiscard]] bool can_color() { return has_colors(); }
|
[[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
|
// Color pair indices
|
||||||
enum {
|
enum {
|
||||||
CP_DIR = 1,
|
CP_DIR = 1,
|
||||||
@@ -42,6 +179,7 @@ enum {
|
|||||||
CP_OVERLAY_TITLE,
|
CP_OVERLAY_TITLE,
|
||||||
CP_BUTTON_YES,
|
CP_BUTTON_YES,
|
||||||
CP_BUTTON_NO,
|
CP_BUTTON_NO,
|
||||||
|
CP_ROOT_WARN,
|
||||||
};
|
};
|
||||||
|
|
||||||
#define ATTR_DIR (COLOR_PAIR(CP_DIR) | A_BOLD)
|
#define ATTR_DIR (COLOR_PAIR(CP_DIR) | A_BOLD)
|
||||||
@@ -54,12 +192,13 @@ enum {
|
|||||||
#define ATTR_MARKER COLOR_PAIR(CP_MARKER)
|
#define ATTR_MARKER COLOR_PAIR(CP_MARKER)
|
||||||
#define ATTR_INFO COLOR_PAIR(CP_INFO)
|
#define ATTR_INFO COLOR_PAIR(CP_INFO)
|
||||||
#define ATTR_OVERLAY_BORDER COLOR_PAIR(CP_OVERLAY_BORDER)
|
#define ATTR_OVERLAY_BORDER COLOR_PAIR(CP_OVERLAY_BORDER)
|
||||||
|
#define ATTR_ROOT_WARN (COLOR_PAIR(CP_ROOT_WARN) | A_BOLD)
|
||||||
|
|
||||||
// ─── Entry data ─────────────────────────────────────────────────────────────
|
// ─── Entry data ─────────────────────────────────────────────────────────────
|
||||||
enum class EntryType { Directory, File, Symlink };
|
enum class EntryType { Directory, File, Symlink };
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
std::string name; // display name (dirs end with '/', symlinks with '@')
|
std::string name; // display name (dirs end with '/')
|
||||||
std::string full_path; // absolute canonical path
|
std::string full_path; // absolute canonical path
|
||||||
EntryType type = EntryType::File;
|
EntryType type = EntryType::File;
|
||||||
bool is_hidden = false;
|
bool is_hidden = false;
|
||||||
@@ -154,6 +293,7 @@ private:
|
|||||||
bool show_details_ = false;
|
bool show_details_ = false;
|
||||||
bool show_preview_ = false;
|
bool show_preview_ = false;
|
||||||
bool searching_ = false;
|
bool searching_ = false;
|
||||||
|
bool is_root_ = false;
|
||||||
std::string filter_;
|
std::string filter_;
|
||||||
std::string info_msg_;
|
std::string info_msg_;
|
||||||
|
|
||||||
@@ -194,8 +334,12 @@ private:
|
|||||||
// ── overlays ───────────────────────────────────────────────────────────
|
// ── overlays ───────────────────────────────────────────────────────────
|
||||||
bool confirm_overlay(const std::string &prompt);
|
bool confirm_overlay(const std::string &prompt);
|
||||||
std::string overlay_picker(const std::string &title,
|
std::string overlay_picker(const std::string &title,
|
||||||
const std::vector<std::string> &items,
|
std::vector<std::string> items,
|
||||||
bool allow_cancel = true);
|
bool allow_cancel = true,
|
||||||
|
const std::function<void(const std::string &)> &on_delete = nullptr);
|
||||||
|
|
||||||
|
// ── input ──────────────────────────────────────────────────────────────
|
||||||
|
bool handle_key(int key); // returns false to quit
|
||||||
|
|
||||||
// ── line input ─────────────────────────────────────────────────────────
|
// ── line input ─────────────────────────────────────────────────────────
|
||||||
bool read_line(std::string &out, const std::string &prompt,
|
bool read_line(std::string &out, const std::string &prompt,
|
||||||
@@ -295,6 +439,8 @@ void FileManager::restore_term() {
|
|||||||
curs_set(1);
|
curs_set(1);
|
||||||
endwin();
|
endwin();
|
||||||
}
|
}
|
||||||
|
// Clear screen and move cursor home (visible in scrollback otherwise)
|
||||||
|
printf("\033[2J\033[H");
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileManager::update_size() {
|
void FileManager::update_size() {
|
||||||
@@ -321,6 +467,7 @@ void FileManager::init_colors() {
|
|||||||
init_pair(CP_OVERLAY_TITLE, COLOR_CYAN, -1);
|
init_pair(CP_OVERLAY_TITLE, COLOR_CYAN, -1);
|
||||||
init_pair(CP_BUTTON_YES, COLOR_GREEN, -1);
|
init_pair(CP_BUTTON_YES, COLOR_GREEN, -1);
|
||||||
init_pair(CP_BUTTON_NO, COLOR_RED, -1);
|
init_pair(CP_BUTTON_NO, COLOR_RED, -1);
|
||||||
|
init_pair(CP_ROOT_WARN, COLOR_RED, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── directory loading ──────────────────────────────────────────────────────
|
// ─── directory loading ──────────────────────────────────────────────────────
|
||||||
@@ -359,7 +506,6 @@ void FileManager::load_entries() {
|
|||||||
e.name += '/';
|
e.name += '/';
|
||||||
} else if (S_ISLNK(st.st_mode)) {
|
} else if (S_ISLNK(st.st_mode)) {
|
||||||
e.type = EntryType::Symlink;
|
e.type = EntryType::Symlink;
|
||||||
e.name += '@';
|
|
||||||
char buf[PATH_MAX];
|
char buf[PATH_MAX];
|
||||||
ssize_t len = readlink(e.full_path.c_str(), buf, sizeof(buf) - 1);
|
ssize_t len = readlink(e.full_path.c_str(), buf, sizeof(buf) - 1);
|
||||||
if (len > 0) { buf[len] = '\0'; e.symlink_target = buf; }
|
if (len > 0) { buf[len] = '\0'; e.symlink_target = buf; }
|
||||||
@@ -554,13 +700,22 @@ void FileManager::draw_botbar() {
|
|||||||
right = " press ? for help ";
|
right = " press ? for help ";
|
||||||
}
|
}
|
||||||
|
|
||||||
int total = static_cast<int>(left.size() + right.size());
|
std::string root_tag = is_root_ ? "[root] " : "";
|
||||||
|
int total = static_cast<int>(root_tag.size() + left.size() + right.size());
|
||||||
int pad = cols_ - total;
|
int pad = cols_ - total;
|
||||||
if (pad < 0) pad = 0;
|
if (pad < 0) pad = 0;
|
||||||
|
|
||||||
|
int x = 0;
|
||||||
|
if (is_root_) {
|
||||||
|
attron(ATTR_ROOT_WARN);
|
||||||
|
mvprintw(rows_ - 1, 0, "%s", root_tag.c_str());
|
||||||
|
attroff(ATTR_ROOT_WARN);
|
||||||
|
x = static_cast<int>(root_tag.size());
|
||||||
|
}
|
||||||
|
|
||||||
attron(ATTR_TOPBAR);
|
attron(ATTR_TOPBAR);
|
||||||
mvprintw(rows_ - 1, 0, "%s", left.c_str());
|
mvprintw(rows_ - 1, x, "%s", left.c_str());
|
||||||
for (int i = 0; i < pad; ++i) mvaddch(rows_ - 1, static_cast<int>(left.size()) + i, ' ');
|
for (int i = 0; i < pad; ++i) mvaddch(rows_ - 1, x + static_cast<int>(left.size()) + i, ' ');
|
||||||
attroff(ATTR_TOPBAR);
|
attroff(ATTR_TOPBAR);
|
||||||
|
|
||||||
attron(ATTR_INFO);
|
attron(ATTR_INFO);
|
||||||
@@ -711,11 +866,10 @@ void FileManager::draw_preview() {
|
|||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|
||||||
// Draw vertical divider
|
// Draw vertical divider
|
||||||
for (int r = 1; r < rows_ - 1; ++r) {
|
attron(ATTR_DIVIDER);
|
||||||
attron(ATTR_DIVIDER);
|
for (int r = 1; r < rows_ - 1; ++r)
|
||||||
mvaddch(r, list_cols_, '|');
|
mvaddch(r, list_cols_, ACS_VLINE);
|
||||||
attroff(ATTR_DIVIDER);
|
attroff(ATTR_DIVIDER);
|
||||||
}
|
|
||||||
|
|
||||||
// Clear entire preview area of previous content (stale lines from last preview)
|
// Clear entire preview area of previous content (stale lines from last preview)
|
||||||
for (int r = 1; r < rows_ - 1; ++r) {
|
for (int r = 1; r < rows_ - 1; ++r) {
|
||||||
@@ -932,7 +1086,7 @@ bool FileManager::confirm_overlay(const std::string &prompt) {
|
|||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
int ch = getch();
|
int ch = get_key();
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
case KEY_LEFT: case 'h': sel_yes = true; break;
|
case KEY_LEFT: case 'h': sel_yes = true; break;
|
||||||
case KEY_RIGHT: case 'l': sel_yes = false; break;
|
case KEY_RIGHT: case 'l': sel_yes = false; break;
|
||||||
@@ -943,26 +1097,16 @@ bool FileManager::confirm_overlay(const std::string &prompt) {
|
|||||||
case '\n': case '\r': case KEY_ENTER:
|
case '\n': case '\r': case KEY_ENTER:
|
||||||
need_full_redraw_ = true;
|
need_full_redraw_ = true;
|
||||||
return sel_yes;
|
return sel_yes;
|
||||||
case 27: {
|
case 27: need_full_redraw_ = true; return false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string FileManager::overlay_picker(const std::string &title,
|
std::string FileManager::overlay_picker(const std::string &title,
|
||||||
const std::vector<std::string> &items,
|
std::vector<std::string> items,
|
||||||
bool allow_cancel) {
|
bool allow_cancel,
|
||||||
|
const std::function<void(const std::string &)> &on_delete) {
|
||||||
if (items.empty()) return "";
|
if (items.empty()) return "";
|
||||||
|
|
||||||
int nitems = static_cast<int>(items.size());
|
int nitems = static_cast<int>(items.size());
|
||||||
@@ -1053,7 +1197,7 @@ std::string FileManager::overlay_picker(const std::string &title,
|
|||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
int ch = getch();
|
int ch = get_key();
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
case 'j': case KEY_DOWN: psel++; break;
|
case 'j': case KEY_DOWN: psel++; break;
|
||||||
case 'k': case KEY_UP: psel--; break;
|
case 'k': case KEY_UP: psel--; break;
|
||||||
@@ -1064,20 +1208,15 @@ std::string FileManager::overlay_picker(const std::string &title,
|
|||||||
case 'q': case 'Q': case 'h':
|
case 'q': case 'Q': case 'h':
|
||||||
if (allow_cancel) { need_full_redraw_ = true; return ""; }
|
if (allow_cancel) { need_full_redraw_ = true; return ""; }
|
||||||
break;
|
break;
|
||||||
case 27: {
|
case KEY_DC:
|
||||||
nodelay(stdscr, TRUE);
|
if (on_delete && !items.empty()) {
|
||||||
int n = getch();
|
on_delete(items[psel]);
|
||||||
nodelay(stdscr, FALSE);
|
items.erase(items.begin() + psel);
|
||||||
if (n == ERR) { need_full_redraw_ = true; return ""; }
|
nitems = static_cast<int>(items.size());
|
||||||
if (n == '[') {
|
if (nitems == 0) { need_full_redraw_ = true; return ""; }
|
||||||
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;
|
break;
|
||||||
}
|
case 27: need_full_redraw_ = true; return "";
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1113,18 +1252,12 @@ bool FileManager::read_line(std::string &out, const std::string &prompt,
|
|||||||
move(bot, static_cast<int>(prompt.size() + pos));
|
move(bot, static_cast<int>(prompt.size() + pos));
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
int ch = getch();
|
int ch = get_key();
|
||||||
if (ch == 27) { // ESC
|
if (ch == 27) { // bare ESC — cancel
|
||||||
nodelay(stdscr, TRUE);
|
leaveok(stdscr, TRUE);
|
||||||
int n = getch();
|
curs_set(0);
|
||||||
nodelay(stdscr, FALSE);
|
out.clear();
|
||||||
if (n == ERR) {
|
return false;
|
||||||
leaveok(stdscr, TRUE);
|
|
||||||
curs_set(0);
|
|
||||||
out.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
|
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
|
||||||
leaveok(stdscr, TRUE);
|
leaveok(stdscr, TRUE);
|
||||||
curs_set(0);
|
curs_set(0);
|
||||||
@@ -1175,8 +1308,8 @@ void FileManager::do_open() {
|
|||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|
||||||
std::string target = join_path(cwd_, e->name);
|
std::string target = join_path(cwd_, e->name);
|
||||||
// Strip trailing / for directory and @ for symlink
|
// Strip trailing / for directory
|
||||||
if (!target.empty() && (target.back() == '/' || target.back() == '@'))
|
if (!target.empty() && target.back() == '/')
|
||||||
target.pop_back();
|
target.pop_back();
|
||||||
|
|
||||||
switch (e->type) {
|
switch (e->type) {
|
||||||
@@ -1294,20 +1427,14 @@ void FileManager::do_search() {
|
|||||||
need_full_redraw_ = true;
|
need_full_redraw_ = true;
|
||||||
draw();
|
draw();
|
||||||
|
|
||||||
int ch = getch();
|
int ch = get_key();
|
||||||
if (ch == 27) { // ESC
|
if (ch == 27) {
|
||||||
nodelay(stdscr, TRUE);
|
// Bare ESC — clear filter and exit search
|
||||||
int n = getch();
|
filter_.clear();
|
||||||
nodelay(stdscr, FALSE);
|
searching_ = false;
|
||||||
if (n == ERR) {
|
apply_filter();
|
||||||
// Bare ESC — clear filter and exit search
|
curs_set(0);
|
||||||
filter_.clear();
|
need_full_redraw_ = true;
|
||||||
searching_ = false;
|
|
||||||
apply_filter();
|
|
||||||
curs_set(0);
|
|
||||||
need_full_redraw_ = true;
|
|
||||||
}
|
|
||||||
// else: arrow key or other sequence — ignore
|
|
||||||
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
|
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
|
||||||
searching_ = false;
|
searching_ = false;
|
||||||
curs_set(0);
|
curs_set(0);
|
||||||
@@ -1462,13 +1589,7 @@ void FileManager::do_info() {
|
|||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
// Wait for any key
|
// Wait for any key
|
||||||
nodelay(stdscr, FALSE);
|
get_key();
|
||||||
int ch = getch();
|
|
||||||
if (ch == 27) {
|
|
||||||
nodelay(stdscr, TRUE);
|
|
||||||
getch();
|
|
||||||
nodelay(stdscr, FALSE);
|
|
||||||
}
|
|
||||||
need_full_redraw_ = true;
|
need_full_redraw_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1511,8 +1632,8 @@ void FileManager::do_help() {
|
|||||||
{" + chmod +x (make executable)", false, false},
|
{" + chmod +x (make executable)", false, false},
|
||||||
{" - chmod -x (remove executable)", false, false},
|
{" - chmod -x (remove executable)", false, false},
|
||||||
{"", true, false},
|
{"", true, false},
|
||||||
{" b bookmark current dir", false, false},
|
{" b open bookmark picker", false, false},
|
||||||
{" B open bookmark picker", false, false},
|
{" B bookmark current dir", false, false},
|
||||||
{" c copy path to clipboard", false, false},
|
{" c copy path to clipboard", false, false},
|
||||||
{" ~ go to home directory", false, false},
|
{" ~ go to home directory", false, false},
|
||||||
{" ` jump to previous directory", false, false},
|
{" ` jump to previous directory", false, false},
|
||||||
@@ -1585,22 +1706,22 @@ void FileManager::do_help() {
|
|||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
// Wait for any key
|
// Wait for any key
|
||||||
nodelay(stdscr, FALSE);
|
get_key();
|
||||||
int ch = getch();
|
|
||||||
if (ch == 27) {
|
|
||||||
nodelay(stdscr, TRUE);
|
|
||||||
getch();
|
|
||||||
nodelay(stdscr, FALSE);
|
|
||||||
}
|
|
||||||
need_full_redraw_ = true;
|
need_full_redraw_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileManager::do_rename() {
|
void FileManager::do_rename() {
|
||||||
const Entry *e = get_sel();
|
const Entry *e = get_sel();
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
// Strip trailing / or @
|
|
||||||
|
if (access(cwd_.c_str(), W_OK) != 0) {
|
||||||
|
flash_msg("permission denied: " + cwd_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip trailing /
|
||||||
std::string name = e->name;
|
std::string name = e->name;
|
||||||
if (!name.empty() && (name.back() == '/' || name.back() == '@'))
|
if (!name.empty() && name.back() == '/')
|
||||||
name.pop_back();
|
name.pop_back();
|
||||||
|
|
||||||
std::string new_name;
|
std::string new_name;
|
||||||
@@ -1612,11 +1733,16 @@ void FileManager::do_rename() {
|
|||||||
if (std::rename(src.c_str(), dst.c_str()) == 0) {
|
if (std::rename(src.c_str(), dst.c_str()) == 0) {
|
||||||
load_entries();
|
load_entries();
|
||||||
} else {
|
} else {
|
||||||
flash_msg("rename failed");
|
flash_msg("rename failed: " + std::string(std::strerror(errno)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileManager::do_mkdir() {
|
void FileManager::do_mkdir() {
|
||||||
|
if (access(cwd_.c_str(), W_OK) != 0) {
|
||||||
|
flash_msg("permission denied: " + cwd_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
if (!read_line(name, " Directory name: ")) { need_full_redraw_ = true; return; }
|
if (!read_line(name, " Directory name: ")) { need_full_redraw_ = true; return; }
|
||||||
if (name.empty()) { flash_msg("cancelled"); return; }
|
if (name.empty()) { flash_msg("cancelled"); return; }
|
||||||
@@ -1625,11 +1751,16 @@ void FileManager::do_mkdir() {
|
|||||||
if (fs::create_directory(path)) {
|
if (fs::create_directory(path)) {
|
||||||
load_entries();
|
load_entries();
|
||||||
} else {
|
} else {
|
||||||
flash_msg("mkdir failed");
|
flash_msg("mkdir failed: " + std::string(std::strerror(errno)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileManager::do_newfile() {
|
void FileManager::do_newfile() {
|
||||||
|
if (access(cwd_.c_str(), W_OK) != 0) {
|
||||||
|
flash_msg("permission denied: " + cwd_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
if (!read_line(name, " File name: ")) { need_full_redraw_ = true; return; }
|
if (!read_line(name, " File name: ")) { need_full_redraw_ = true; return; }
|
||||||
if (name.empty()) { flash_msg("cancelled"); return; }
|
if (name.empty()) { flash_msg("cancelled"); return; }
|
||||||
@@ -1640,22 +1771,27 @@ void FileManager::do_newfile() {
|
|||||||
f.close();
|
f.close();
|
||||||
load_entries();
|
load_entries();
|
||||||
} else {
|
} else {
|
||||||
flash_msg("cannot create file");
|
flash_msg("cannot create file: " + std::string(std::strerror(errno)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileManager::do_delete() {
|
void FileManager::do_delete() {
|
||||||
|
if (access(cwd_.c_str(), W_OK) != 0) {
|
||||||
|
flash_msg("permission denied: " + cwd_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Gather items to delete
|
// Gather items to delete
|
||||||
std::vector<std::string> targets;
|
std::vector<std::string> targets;
|
||||||
if (!selected_.empty()) {
|
if (!selected_.empty()) {
|
||||||
for (const auto &sel_name : selected_)
|
for (const auto &sel_name : selected_)
|
||||||
targets.push_back(join_path(cwd_, strip_suffix(strip_suffix(sel_name, "@"), "/")));
|
targets.push_back(join_path(cwd_, strip_suffix(sel_name, "/")));
|
||||||
} else {
|
} else {
|
||||||
const Entry *e = get_sel();
|
const Entry *e = get_sel();
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
std::string n = e->name;
|
std::string n = e->name;
|
||||||
if (!n.empty() && n.back() == '/') n.pop_back();
|
if (!n.empty() && n.back() == '/') n.pop_back();
|
||||||
if (!n.empty() && n.back() == '@') n.pop_back();
|
|
||||||
targets.push_back(join_path(cwd_, n));
|
targets.push_back(join_path(cwd_, n));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1709,9 +1845,14 @@ void FileManager::do_delete() {
|
|||||||
void FileManager::do_trash() {
|
void FileManager::do_trash() {
|
||||||
const Entry *e = get_sel();
|
const Entry *e = get_sel();
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|
||||||
|
if (access(cwd_.c_str(), W_OK) != 0) {
|
||||||
|
flash_msg("permission denied: " + cwd_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string name = e->name;
|
std::string name = e->name;
|
||||||
if (!name.empty() && name.back() == '/') name.pop_back();
|
if (!name.empty() && name.back() == '/') name.pop_back();
|
||||||
if (!name.empty() && name.back() == '@') name.pop_back();
|
|
||||||
|
|
||||||
std::string src = join_path(cwd_, name);
|
std::string src = join_path(cwd_, name);
|
||||||
|
|
||||||
@@ -1743,7 +1884,7 @@ void FileManager::do_trash() {
|
|||||||
sel_ = std::max(0, static_cast<int>(entries_.size()) - 2);
|
sel_ = std::max(0, static_cast<int>(entries_.size()) - 2);
|
||||||
load_entries();
|
load_entries();
|
||||||
} else {
|
} else {
|
||||||
flash_msg("trash failed");
|
flash_msg("trash failed: " + std::string(std::strerror(errno)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1758,7 +1899,6 @@ void FileManager::do_open_with() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string name = e->name;
|
std::string name = e->name;
|
||||||
if (!name.empty() && name.back() == '@') name.pop_back();
|
|
||||||
std::string target = join_path(cwd_, name);
|
std::string target = join_path(cwd_, name);
|
||||||
|
|
||||||
std::string prog;
|
std::string prog;
|
||||||
@@ -1788,7 +1928,6 @@ void FileManager::do_chmod_x(bool set) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string name = e->name;
|
std::string name = e->name;
|
||||||
if (!name.empty() && name.back() == '@') name.pop_back();
|
|
||||||
std::string target = join_path(cwd_, name);
|
std::string target = join_path(cwd_, name);
|
||||||
|
|
||||||
struct stat st;
|
struct stat st;
|
||||||
@@ -1805,7 +1944,7 @@ void FileManager::do_chmod_x(bool set) {
|
|||||||
flash_msg(set ? "chmod +x: " + e->name : "chmod -x: " + e->name);
|
flash_msg(set ? "chmod +x: " + e->name : "chmod -x: " + e->name);
|
||||||
load_entries();
|
load_entries();
|
||||||
} else {
|
} else {
|
||||||
flash_msg("chmod failed");
|
flash_msg("chmod failed: " + std::string(std::strerror(errno)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1895,7 +2034,18 @@ void FileManager::do_bookmark_jump() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string chosen = overlay_picker(" BOOKMARKS", bookmarks);
|
auto remove_bookmark = [&](const std::string &path) {
|
||||||
|
std::vector<std::string> kept;
|
||||||
|
std::ifstream fin(bookmark_file_);
|
||||||
|
std::string l;
|
||||||
|
while (std::getline(fin, l))
|
||||||
|
if (l != path) kept.push_back(l);
|
||||||
|
fin.close();
|
||||||
|
std::ofstream fout(bookmark_file_, std::ios::trunc);
|
||||||
|
for (const auto &k : kept) fout << k << '\n';
|
||||||
|
flash_msg("bookmark removed: " + path);
|
||||||
|
};
|
||||||
|
std::string chosen = overlay_picker(" BOOKMARKS", bookmarks, true, remove_bookmark);
|
||||||
if (chosen.empty()) { need_full_redraw_ = true; return; }
|
if (chosen.empty()) { need_full_redraw_ = true; return; }
|
||||||
|
|
||||||
if (fs::is_directory(chosen)) {
|
if (fs::is_directory(chosen)) {
|
||||||
@@ -1911,7 +2061,6 @@ void FileManager::do_copy_path() {
|
|||||||
if (!e) return;
|
if (!e) return;
|
||||||
std::string name = e->name;
|
std::string name = e->name;
|
||||||
if (!name.empty() && name.back() == '/') name.pop_back();
|
if (!name.empty() && name.back() == '/') name.pop_back();
|
||||||
if (!name.empty() && name.back() == '@') name.pop_back();
|
|
||||||
std::string path = join_path(cwd_, name);
|
std::string path = join_path(cwd_, name);
|
||||||
|
|
||||||
// Try clipboard tools
|
// Try clipboard tools
|
||||||
@@ -1971,7 +2120,7 @@ void FileManager::do_yank() {
|
|||||||
for (const auto &sel_name : selected_) {
|
for (const auto &sel_name : selected_) {
|
||||||
std::string n = sel_name;
|
std::string n = sel_name;
|
||||||
if (!n.empty() && n.back() == '/') n.pop_back();
|
if (!n.empty() && n.back() == '/') n.pop_back();
|
||||||
if (!n.empty() && n.back() == '@') n.pop_back();
|
|
||||||
clipboard_paths_.push_back(join_path(cwd_, n));
|
clipboard_paths_.push_back(join_path(cwd_, n));
|
||||||
}
|
}
|
||||||
clip_mode_ = ClipMode::Copy;
|
clip_mode_ = ClipMode::Copy;
|
||||||
@@ -1982,7 +2131,7 @@ void FileManager::do_yank() {
|
|||||||
if (!e) return;
|
if (!e) return;
|
||||||
std::string n = e->name;
|
std::string n = e->name;
|
||||||
if (!n.empty() && n.back() == '/') n.pop_back();
|
if (!n.empty() && n.back() == '/') n.pop_back();
|
||||||
if (!n.empty() && n.back() == '@') n.pop_back();
|
|
||||||
clipboard_paths_.push_back(join_path(cwd_, n));
|
clipboard_paths_.push_back(join_path(cwd_, n));
|
||||||
clip_mode_ = ClipMode::Copy;
|
clip_mode_ = ClipMode::Copy;
|
||||||
flash_msg("yanked: " + e->name);
|
flash_msg("yanked: " + e->name);
|
||||||
@@ -1996,7 +2145,7 @@ void FileManager::do_cut() {
|
|||||||
for (const auto &sel_name : selected_) {
|
for (const auto &sel_name : selected_) {
|
||||||
std::string n = sel_name;
|
std::string n = sel_name;
|
||||||
if (!n.empty() && n.back() == '/') n.pop_back();
|
if (!n.empty() && n.back() == '/') n.pop_back();
|
||||||
if (!n.empty() && n.back() == '@') n.pop_back();
|
|
||||||
clipboard_paths_.push_back(join_path(cwd_, n));
|
clipboard_paths_.push_back(join_path(cwd_, n));
|
||||||
}
|
}
|
||||||
clip_mode_ = ClipMode::Cut;
|
clip_mode_ = ClipMode::Cut;
|
||||||
@@ -2007,7 +2156,7 @@ void FileManager::do_cut() {
|
|||||||
if (!e) return;
|
if (!e) return;
|
||||||
std::string n = e->name;
|
std::string n = e->name;
|
||||||
if (!n.empty() && n.back() == '/') n.pop_back();
|
if (!n.empty() && n.back() == '/') n.pop_back();
|
||||||
if (!n.empty() && n.back() == '@') n.pop_back();
|
|
||||||
clipboard_paths_.push_back(join_path(cwd_, n));
|
clipboard_paths_.push_back(join_path(cwd_, n));
|
||||||
clip_mode_ = ClipMode::Cut;
|
clip_mode_ = ClipMode::Cut;
|
||||||
flash_msg("cut: " + e->name);
|
flash_msg("cut: " + e->name);
|
||||||
@@ -2021,6 +2170,11 @@ void FileManager::do_paste() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (access(cwd_.c_str(), W_OK) != 0) {
|
||||||
|
flash_msg("permission denied: " + cwd_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto &src : clipboard_paths_) {
|
for (const auto &src : clipboard_paths_) {
|
||||||
std::string name = fs::path(src).filename().string();
|
std::string name = fs::path(src).filename().string();
|
||||||
std::string dst = join_path(cwd_, name);
|
std::string dst = join_path(cwd_, name);
|
||||||
@@ -2286,80 +2440,112 @@ void FileManager::open_file(const std::string &path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── main event loop ────────────────────────────────────────────────────────
|
// ─── 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_jump(); break;
|
||||||
|
case 'B': do_bookmark_add(); 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() {
|
void FileManager::run() {
|
||||||
setup_term();
|
setup_term();
|
||||||
if (can_color()) init_colors();
|
if (can_color()) init_colors();
|
||||||
load_entries();
|
load_entries();
|
||||||
|
|
||||||
while (true) {
|
is_root_ = (geteuid() == 0);
|
||||||
draw();
|
|
||||||
int key = getch();
|
|
||||||
|
|
||||||
// Handle terminal resize
|
#ifndef NCURSES_VERSION
|
||||||
|
struct sigaction sa;
|
||||||
|
std::memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_handler = on_sigwinch;
|
||||||
|
sigaction(SIGWINCH, &sa, nullptr);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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) {
|
if (key == KEY_RESIZE) {
|
||||||
update_size();
|
update_size();
|
||||||
need_full_redraw_ = true;
|
need_full_redraw_ = true;
|
||||||
|
draw();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (key) {
|
if (!handle_key(key)) return;
|
||||||
case 'j': case KEY_DOWN:
|
draw();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -912,8 +912,8 @@ do_help() {
|
|||||||
help_row 30 " + chmod +x (make executable)"
|
help_row 30 " + chmod +x (make executable)"
|
||||||
help_row 31 " - chmod -x (remove executable)"
|
help_row 31 " - chmod -x (remove executable)"
|
||||||
help_sep 32
|
help_sep 32
|
||||||
help_row 33 " b bookmark current dir"
|
help_row 33 " b open bookmark picker"
|
||||||
help_row 34 " B open bookmark picker"
|
help_row 34 " B bookmark current dir"
|
||||||
help_row 35 " c copy path to clipboard"
|
help_row 35 " c copy path to clipboard"
|
||||||
help_row 36 " ~ go to home directory"
|
help_row 36 " ~ go to home directory"
|
||||||
help_row 37 " \` jump to previous directory"
|
help_row 37 " \` jump to previous directory"
|
||||||
@@ -1674,6 +1674,19 @@ do_bookmark_jump() {
|
|||||||
fi
|
fi
|
||||||
return ;;
|
return ;;
|
||||||
'[D') NEED_FULL_REDRAW=1; return ;; # left — close
|
'[D') NEED_FULL_REDRAW=1; return ;; # left — close
|
||||||
|
'[3') # delete — remove selected bookmark
|
||||||
|
IFS= read -r -n1 -t 0.05 _rest 2>/dev/null # swallow trailing ~
|
||||||
|
_chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE")
|
||||||
|
if [ -n "$_chosen" ]; then
|
||||||
|
_tmp=$(grep -vxF "$_chosen" "$BOOKMARK_FILE")
|
||||||
|
printf '%s\n' "$_tmp" > "$BOOKMARK_FILE"
|
||||||
|
INFO_MSG="bookmark removed: ${_chosen}"
|
||||||
|
_bc=$((_bc-1))
|
||||||
|
if [ "$_bc" -eq 0 ]; then NEED_FULL_REDRAW=1; return; fi
|
||||||
|
[ "$_bsel" -gt "$_bc" ] && _bsel=$_bc
|
||||||
|
[ "$_bsel" -lt "$_boff" ] && _boff=$_bsel
|
||||||
|
[ "$_bsel" -ge $((_boff + _bvis)) ] && _boff=$((_bsel - _bvis + 1))
|
||||||
|
fi ;;
|
||||||
*) NEED_FULL_REDRAW=1; return ;; # bare esc — close
|
*) NEED_FULL_REDRAW=1; return ;; # bare esc — close
|
||||||
esac ;;
|
esac ;;
|
||||||
"$(printf '\n')"|\
|
"$(printf '\n')"|\
|
||||||
@@ -1829,8 +1842,8 @@ while true; do
|
|||||||
l) do_open ;;
|
l) do_open ;;
|
||||||
"$(printf '\033[D')"|\
|
"$(printf '\033[D')"|\
|
||||||
h) do_go_back ;;
|
h) do_go_back ;;
|
||||||
b) do_bookmark_add ;;
|
b) do_bookmark_jump ;;
|
||||||
B) do_bookmark_jump ;;
|
B) do_bookmark_add ;;
|
||||||
'?') do_help ;;
|
'?') do_help ;;
|
||||||
R) load_entries; INFO_MSG="refreshed" ;;
|
R) load_entries; INFO_MSG="refreshed" ;;
|
||||||
/) do_search ;;
|
/) do_search ;;
|
||||||
|
|||||||
Reference in New Issue
Block a user