aboutsummaryrefslogtreecommitdiff
path: root/dfm.c
diff options
context:
space:
mode:
authorDylan Araps <dylan.araps@gmail.com>2026-02-27 13:41:56 +0200
committerDylan Araps <dylan.araps@gmail.com>2026-02-27 13:41:56 +0200
commitda28548905dab7c60c3fcec4975ccfa23e315909 (patch)
tree9cef34a718563969a6bcda5cfeff033eeaf6b1a0 /dfm.c
0.99.0
Diffstat (limited to 'dfm.c')
-rw-r--r--dfm.c3502
1 files changed, 3502 insertions, 0 deletions
diff --git a/dfm.c b/dfm.c
new file mode 100644
index 0000000..566eeb3
--- /dev/null
+++ b/dfm.c
@@ -0,0 +1,3502 @@
+/*
+ * vim: foldmethod=marker
+ *
+ *
+ * oooooooooo. oooooooooooo ooo ooooo
+ * `888' `Y8b `888' `8 `88. .888'
+ * 888 888 888 888b d'888
+ * 888 888 888oooo8 8 Y88. .P 888
+ * 888 888 888 " 8 `888' 888
+ * 888 d88' 888 8 Y 888
+ * o888bood8P' o888o o8o o888o
+ *
+ * Dylan's File Manager
+ *
+ *
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ftw.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+
+#include "config.h"
+
+#include "lib/arg.h"
+#include "lib/bitset.h"
+#include "lib/date.h"
+#include "lib/readline.h"
+#include "lib/str.h"
+#include "lib/term.h"
+#include "lib/term_key.h"
+#include "lib/vt.h"
+
+#ifdef __linux__
+#include "platform/linux.h"
+#else
+#include "platform/posix.h"
+#endif
+
+static const char DFM_HELP[] =
+ "usage: " CFG_NAME " [options] [path]\n\n"
+ "options:\n"
+ "-H | +H toggle hidden files (-H off, +H on)\n"
+ "-p picker mode (print selected path to stdout and exit)\n"
+ "-o <opener> program to use when opening files (default: xdg-open)\n"
+ "-s <mode> change default sort\n"
+ " n name\n"
+ " N name reverse\n"
+ " e extension\n"
+ " s size\n"
+ " S size reverse\n"
+ " d date\n"
+ " D date reverse\n"
+ "-v <mode> change default view\n"
+ " n name only\n"
+ " s size\n"
+ " p permissions\n"
+ " t time\n"
+ " a all\n\n"
+ "--help show this help\n"
+ "--version show version\n\n"
+ "path:\n"
+ "directory to open (default: \".\")\n\n"
+ "environment:\n"
+ "DFM_OPENER program used to open files (overridden by -o)\n"
+ "DFM_BOOKMARK_[0-9] bookmark directories\n"
+ "DFM_COPYER program used to copy PWD and file contents.\n"
+;
+
+enum fm_opt {
+ FM_ERROR = 1 << 0,
+ FM_ROOT = 1 << 1,
+
+ FM_REDRAW_DIR = 1 << 2,
+ FM_REDRAW_NAV = 1 << 3,
+ FM_REDRAW_CMD = 1 << 4,
+ FM_REDRAW_FLUSH = 1 << 5,
+ FM_REDRAW = FM_REDRAW_DIR|FM_REDRAW_NAV|FM_REDRAW_CMD|FM_REDRAW_FLUSH,
+
+ FM_DIRTY = 1 << 6,
+ FM_DIRTY_WITHIN = 1 << 7,
+ FM_HIDDEN = 1 << 8,
+ FM_TRUNC = 1 << 9,
+ FM_MARK_PWD = 1 << 10,
+ FM_MSG = 1 << 11,
+ FM_MSG_ERR = 1 << 12,
+ FM_PICKER = 1 << 13,
+ FM_PRINT_PWD = 1 << 14,
+ FM_SEARCH = 1 << 15,
+};
+
+struct fm;
+typedef void (*fm_key_press)(struct fm *, int k, cut, cut);
+typedef int (*fm_key_enter)(struct fm *, str *);
+typedef int (*fm_filter)(struct fm *, usize, cut, cut);
+
+struct fm {
+ struct term t;
+ struct term_key k;
+ struct platform p;
+ struct readline r;
+
+ int dfd;
+ str pwd;
+ str ppwd;
+ str mpwd;
+
+ str io;
+
+ usize ml;
+ usize mp;
+
+ char de[DFM_ENT_MAX];
+ usize del;
+ usize dec;
+
+ union {
+ align_max _a;
+ unsigned char d[DFM_DIR_MAX * sizeof(u32)];
+ } d;
+
+ usize dl;
+ u8 dv;
+ u8 ds;
+ u32 du;
+
+ u64 v[BITSET_W(DFM_DIR_MAX)];
+ u16 vp[BITSET_W(DFM_DIR_MAX)];
+ usize vl;
+ char vq[DFM_NAME_MAX];
+ usize vql;
+
+ u64 vm[BITSET_W(DFM_DIR_MAX)];
+ usize vml;
+
+ u32 ht[DFM_DIR_HT_CAP];
+
+ usize y;
+ usize o;
+ usize c;
+ u32 st;
+
+ u16 row;
+ u16 col;
+
+ u32 f;
+ u32 cf;
+
+ cut opener;
+
+ fm_key_press kp;
+ fm_key_enter kd;
+ fm_filter sf;
+
+ s64 tz;
+};
+
+// Entry Virtual {{{
+
+#define ENT_V_OFF 0, 20
+#define ENT_V_CHAR 20, 8
+#define ENT_V_TOMB 28, 1
+#define ENT_V_MARK 29, 1
+#define ENT_V_VIS 30, 1
+#define ENT_V_DOT 31, 1
+
+#define ent_v_get(e, o) bitfield_get32((e), ENT_V_##o)
+#define ent_v_set(e, o, v) bitfield_set32((e), (v), ENT_V_##o)
+#define ent_v_geto(p, i, o) ent_v_get(ent_v_load((p), (i)), o)
+
+static inline unsigned char *
+ent_v_ptr(struct fm *p, usize i)
+{
+ return p->d.d + (i * sizeof(u32));
+}
+
+static inline const unsigned char *
+ent_v_ptr_const(const struct fm *p, usize i)
+{
+ return p->d.d + (i * sizeof(u32));
+}
+
+static inline u32
+ent_v_load(const struct fm *p, usize i)
+{
+ u32 v;
+ memcpy(&v, ent_v_ptr_const(p, i), sizeof(v));
+ return v;
+}
+
+static inline void
+ent_v_store(struct fm *p, usize i, u32 v)
+{
+ memcpy(ent_v_ptr(p, i), &v, sizeof(v));
+}
+
+// }}}
+
+// Entry Physical {{{
+
+enum {
+ ENT_DIR = 0,
+ ENT_LNK_DIR = 1,
+ ENT_LNK = 3,
+ ENT_LNK_BRK = 5,
+ ENT_UNKNOWN = 4,
+ ENT_FIFO = 6,
+ ENT_SOCK = 8,
+ ENT_SPEC = 10,
+ ENT_REG = 12,
+ ENT_REG_EXEC = 14,
+ ENT_TYPE_MAX = 16,
+};
+
+#define ENT_IS_LNK(t) ((t) & 1)
+#define ENT_IS_DIR(t) ((t) <= ENT_LNK_DIR)
+
+#define ENT_UTF8 0, 1
+#define ENT_WIDE 1, 1
+#define ENT_LOC 2, 16
+#define ENT_LEN 18, 8
+#define ENT_SIZE 26, 12
+#define ENT_TYPE 38, 4
+#define ENT_PERM 42, 12
+#define ENT_TIME 54, 5
+#define ENT_HASH 59, 5
+
+#define ent_get(e, o) bitfield_get64((u64)(e), ENT_##o)
+#define ent_set(e, o, v) bitfield_set64((e), (v), ENT_##o)
+#define lnk_set(l, o, v) bitfield_set8((l), (v), ENT_##o)
+
+static inline u64
+ent_load(const struct fm *p, usize i)
+{
+ u64 m;
+ memcpy(&m, p->de + ent_v_geto(p, i, OFF) - sizeof(m), sizeof(m));
+ return m;
+}
+
+static inline void
+ent_store(struct fm *p, usize i, u64 m)
+{
+ memcpy(p->de + ent_v_geto(p, i, OFF) - sizeof(m), &m, 8);
+}
+
+static inline u64
+ent_load_off(const struct fm *p, u32 o)
+{
+ u64 m;
+ memcpy(&m, p->de + o - sizeof(m), sizeof(m));
+ return m;
+}
+
+static inline void
+ent_perm_decode(str *s, mode_t m, u8 t)
+{
+ char b[11];
+ int d = t ? t == ENT_DIR : S_ISDIR(m);
+ b[0] = d ? 'd' : '-';
+ b[1] = (m & S_IRUSR) ? 'r' : '-';
+ b[2] = (m & S_IWUSR) ? 'w' : '-';
+ b[3] = (m & S_ISUID) ? (m & S_IXUSR) ? 's' : 'S' : (m & S_IXUSR) ? 'x' : '-';
+ b[4] = (m & S_IRGRP) ? 'r' : '-';
+ b[5] = (m & S_IWGRP) ? 'w' : '-';
+ b[6] = (m & S_ISGID) ? (m & S_IXGRP) ? 's' : 'S' : (m & S_IXGRP) ? 'x' : '-';
+ b[7] = (m & S_IROTH) ? 'r' : '-';
+ b[8] = (m & S_IWOTH) ? 'w' : '-';
+ b[9] = (m & S_ISVTX) ? (m & S_IXOTH) ? 't' : 'T' : (m & S_IXOTH) ? 'x' : '-';
+ b[10] = ' ';
+ str_push(s, b, sizeof(b));
+}
+
+static inline u32
+ent_size_encode(off_t s)
+{
+ if (s <= 0) return 0;
+ u64 v = (u64)s;
+ u32 e = 63 - u64_clz(v);
+ u64 b = 1ULL << e;
+ u32 f = 0;
+ if (e) {
+ u64 d = v - b;
+ f = (u32)((d << 6) >> e);
+ if (f > 63) f = 63;
+ }
+ return (e << 6) | f;
+}
+
+static inline u64
+ent_size_bytes(u32 v, u8 t)
+{
+ if (ENT_IS_LNK(t)) return v;
+ if (!v) return 0;
+ u32 e = v >> 6;
+ u32 f = v & 63;
+ u64 b = 1ULL << e;
+ return b + ((b * f) >> 6);
+}
+
+static inline u32
+ent_size_add(u32 e, u64 a)
+{
+ if (!e) return ent_size_encode(a);
+ if (!a) return e;
+ u64 c = ent_size_bytes(e, ENT_TYPE_MAX);
+ return ent_size_encode(c + a);
+}
+
+static inline u32
+ent_size_sub(u32 e, u64 s)
+{
+ if (!e) return 0;
+ u64 c = ent_size_bytes(e, ENT_TYPE_MAX);
+ if (s >= c) return 0;
+ return ent_size_encode(c - s);
+}
+
+static inline void
+ent_size_decode(str *s, u32 v, usize p, u8 t)
+{
+ if (ENT_IS_LNK(t) || !v) {
+ str_push_u32_p(s, v, ' ', p ? p - 1 : 0);
+ str_push_c(s, 'B');
+ if (p) str_push_c(s, ' ');
+ return;
+ }
+ u32 e = v >> 6;
+ u32 f = v & 63;
+ u32 u = e / 10;
+ if (u > 6) u = 6;
+ u64 b = 1ULL << (e - u * 10);
+ u64 ip = b + ((b * f) >> 6);
+ u32 d = ((f * 10) + 32) >> 6;
+ if (d == 10) { ip++; d = 0; }
+ int sd = (u && ip < 10);
+ usize su = 1 + (sd ? 2 : 0);
+ usize pa = p > su ? p - su : 0;
+ str_push_u32_p(s, (u32)ip, ' ', pa);
+ if (sd) {
+ str_push_c(s, '.');
+ str_push_u32(s, d);
+ }
+ str_push_c(s, "BKMGTPE"[u]);
+ if (p) str_push_c(s, ' ');
+}
+
+static inline u32
+ent_time_encode(time_t t)
+{
+ time_t d = time(NULL) - t;
+ return d <= 0 ? 0 : (u32)(63 - u64_clz(d));
+}
+
+static inline void
+ent_time_decode(str *s, u32 v)
+{
+ static const char *u[] = {
+ "s ", "s ", "s ", "s ", "s ", "s ",
+ "m ", "m ", "m ", "m ", "m ", "m ",
+ "h ", "h ", "h ", "h ", "h ",
+ "d ", "d ", "d ", "d ", "d ",
+ "w ", "w ", "w ", "w ",
+ "mo", "mo", "mo", "mo", "mo"
+ };
+ if (v > 31) v = 31;
+ str_push(s, v == 31 ? ">= " : " ", 3);
+ str_push_u32_p(s, v == 31 ? 1u << 5 : 1u << (v % 6), ' ', 2);
+ str_push(s, u[v], 2);
+ str_push_c(s, ' ');
+}
+
+static inline void
+ent_map_stat(u64 *e, const struct stat *s, u8 t)
+{
+ if (t != ENT_TYPE_MAX) goto e;
+ if (S_ISDIR(s->st_mode)) t = ENT_DIR;
+ else if (S_ISLNK(s->st_mode)) t = ENT_LNK;
+ else if (S_ISREG(s->st_mode) && (s->st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)))
+ t = ENT_REG_EXEC;
+ else if (S_ISREG(s->st_mode)) t = ENT_REG;
+ else if (S_ISFIFO(s->st_mode)) t = ENT_FIFO;
+ else if (S_ISSOCK(s->st_mode)) t = ENT_SOCK;
+ else if (S_ISCHR(s->st_mode) || S_ISBLK(s->st_mode))
+ t = ENT_SPEC;
+ else t = ENT_UNKNOWN;
+e:
+ ent_set(e, TYPE, t);
+ ent_set(e, PERM, s->st_mode & 07777);
+ ent_set(e, TIME, ent_time_encode(s->st_mtime));
+}
+
+static inline void
+ent_map_stat_size(u64 *e, const struct stat *s)
+{
+ ent_set(e, SIZE, S_ISLNK(s->st_mode)
+ ? s->st_size : ent_size_encode(s->st_size));
+}
+
+static inline usize
+ent_name_len(const char *s, u8 *utf8, u8 *wide)
+{
+ const unsigned char *m = (const unsigned char *)s;
+ const unsigned char *p = m;
+ *utf8 = 0;
+ *wide = 0;
+#ifdef __GNUC__
+ typedef size_t __attribute__((__may_alias__)) W;
+ #define ONES ((size_t)-1 / UCHAR_MAX)
+ #define HIGHS (ONES * (UCHAR_MAX / 2 + 1))
+ for (; (uintptr_t)p % sizeof(W); p++) {
+ if (!*p) return (size)(p - m);
+ if (*p & 0x80) { *utf8 = 1; goto check_wide; }
+ }
+ for (const W *w = (const void *)p;; w++) {
+ W v = *w;
+ if ((v & HIGHS) | ((v - ONES) & ~v & HIGHS)) {
+ p = (const unsigned char *)w;
+ for (;; p++) {
+ if (!*p) return (size)(p - m);
+ if (*p & 0x80) { *utf8 = 1; goto check_wide; }
+ }
+ }
+ }
+#endif
+ for (;;) {
+ unsigned char b = *p;
+ if (!b) return (size)(p - m);
+ if (!(b & 0x80)) { p++; continue; }
+ *utf8 = 1;
+check_wide:;
+ unsigned char b2 = *p;
+ if ((b2 & 0xF8) == 0xF0) { *wide = 1; break; }
+ if ((b2 & 0xF0) == 0xE0) {
+ u32 cp;
+ utf8_decode((void *)p, &cp);
+ if (utf8_width(cp) > 1) { *wide = 1; break; }
+ }
+ usize n = utf8_expected(b2);
+ p += n ? n : 1;
+ }
+ for (p++;;) {
+#ifdef __GNUC__
+ for (; (uintptr_t)p % sizeof(W); p++) if (!*p) return (size)(p - m);
+ for (const W *w = (const void *)p;; w++) {
+ if ((*w - ONES) & ~*w & HIGHS) {
+ p = (const unsigned char *)w;
+ for (; *p; p++);
+ return (size)(p - m);
+ }
+ }
+#endif
+ if (!*p) return (size)(p - m);
+ p++;
+ }
+}
+
+static inline usize
+ent_next(struct fm *p, usize i)
+{
+ return bitset_next_set(p->v, i, p->dl);
+}
+
+static inline usize
+ent_prev(struct fm *p, usize i)
+{
+ return bitset_prev_set(p->v, i, p->dl);
+}
+
+// }}}
+
+// Sorting {{{
+
+typedef int (*ent_sort_cb)(struct fm *, u32, u32);
+
+static inline int
+fm_ent_cmp_name(struct fm *p, u32 a, u32 b)
+{
+ static const unsigned char t[256] = {
+ ['0']=1,['1']=1,['2']=1,['3']=1,['4']=1,
+ ['5']=1,['6']=1,['7']=1,['8']=1,['9']=1
+ };
+
+ u32 oa = ent_v_get(a, OFF);
+ u32 ob = ent_v_get(b, OFF);
+ u64 ma = ent_load_off(p, oa);
+ u64 mb = ent_load_off(p, ob);
+
+ int r = ENT_IS_DIR(ent_get(mb, TYPE)) - ENT_IS_DIR(ent_get(ma, TYPE));
+ if (unlikely(r)) return r;
+
+ u8 fa = ent_v_get(a, CHAR);
+ u8 fb = ent_v_get(b, CHAR);
+
+ int da = (unsigned)(fa - '0') < 10;
+ int db = (unsigned)(fb - '0') < 10;
+ if (da ^ db)
+ return da ? -1 : 1;
+
+ if (fa != fb && !(t[fa] & t[fb]))
+ return fa < fb ? -1 : 1;
+
+ const char *pa = p->de + oa;
+ const char *pb = p->de + ob;
+ usize la = ent_get(ma, LEN);
+ usize lb = ent_get(mb, LEN);
+ usize i = 0;
+ usize j = 0;
+
+ while (i < la && j < lb) {
+ unsigned char ca = (unsigned char)pa[i];
+ unsigned char cb = (unsigned char)pb[j];
+
+ if (unlikely(t[ca] & t[cb])) {
+ usize ia = i;
+ usize ja = j;
+ for (; ia < la && pa[ia] == '0'; ia++);
+ for (; ja < lb && pb[ja] == '0'; ja++);
+ usize ea = ia;
+ usize eb = ja;
+ for (; ea < la && t[(unsigned char)pa[ea]]; ea++);
+ for (; eb < lb && t[(unsigned char)pb[eb]]; eb++);
+ usize na = ea - ia;
+ usize nb = eb - ja;
+ if (na != nb) return na < nb ? -1 : 1;
+ int cmp = memcmp(pa + ia, pb + ja, na);
+ if (cmp) return cmp;
+ usize za = ia - i;
+ usize zb = ja - j;
+ if (za != zb) return za < zb ? -1 : 1;
+ i = ea;
+ j = eb;
+ continue;
+ }
+
+ if (ca != cb) return ca < cb ? -1 : 1;
+ i++;
+ j++;
+ }
+
+ return (i < la) - (j < lb);
+}
+
+static inline int
+fm_ent_cmp_name_rev(struct fm *p, u32 a, u32 b)
+{
+ return fm_ent_cmp_name(p, a, b) * -1;
+}
+
+static inline int
+fm_ent_cmp_size(struct fm *p, u32 a, u32 b)
+{
+ u64 ma = ent_load_off(p, ent_v_get(a, OFF));
+ u64 mb = ent_load_off(p, ent_v_get(b, OFF));
+ u32 sa = ent_size_bytes(ent_get(ma, SIZE), ent_get(ma, TYPE));
+ u32 sb = ent_size_bytes(ent_get(mb, SIZE), ent_get(mb, TYPE));
+ return (size)sa - (size)sb;
+}
+
+static inline int
+fm_ent_cmp_date(struct fm *p, u32 a, u32 b)
+{
+ u64 ma = ent_load_off(p, ent_v_get(a, OFF));
+ u64 mb = ent_load_off(p, ent_v_get(b, OFF));
+ return (size)ent_get(ma, TIME) - (size)ent_get(mb, TIME);
+}
+
+static inline int
+fm_ent_cmp_size_rev(struct fm *p, u32 a, u32 b)
+{
+ return fm_ent_cmp_size(p, b, a);
+}
+
+static inline int
+fm_ent_cmp_date_rev(struct fm *p, u32 a, u32 b)
+{
+ return fm_ent_cmp_date(p, b, a);
+}
+
+static inline int
+fm_ent_cmp_fext(struct fm *p, u32 a, u32 b)
+{
+ u32 oa = ent_v_get(a, OFF);
+ u32 ob = ent_v_get(b, OFF);
+ u64 ma = ent_load_off(p, oa);
+ u64 mb = ent_load_off(p, ob);
+ cut ca = { p->de + oa, ent_get(ma, LEN) };
+ cut cb = { p->de + ob, ent_get(mb, LEN) };
+ const char *pa = ca.d + ca.l;
+ const char *pb = cb.d + cb.l;
+ for (; pa > ca.d && pa[-1] != '.'; pa--);
+ for (; pb > cb.d && pb[-1] != '.'; pb--);
+ if (pa == ca.d && pb != cb.d) return 1;
+ if (pb == cb.d && pa != ca.d) return -1;
+ if (pa == ca.d && pb == cb.d) return 0;
+ usize la = (usize)(ca.d + ca.l - pa);
+ usize lb = (usize)(cb.d + cb.l - pb);
+ int r = memcmp(pa, pb, la < lb ? la : lb);
+ return r ? r : (int)(la < lb) - (int)(la > lb);
+}
+
+static inline void
+fm_ent_isort(struct fm *p, ent_sort_cb f, usize lo, usize hi)
+{
+ for (usize i = lo + 1; i < hi; i++) {
+ u32 x = ent_v_load(p, i);
+ usize j = i;
+ for (; j > lo && f(p, ent_v_load(p, j - 1), x) > 0; j--)
+ ent_v_store(p, j, ent_v_load(p, j - 1));
+ ent_v_store(p, j, x);
+ }
+}
+
+static inline void
+fm_ent_qsort(struct fm *p, ent_sort_cb f, usize lo, usize hi, int d)
+{
+ while (hi - lo > 16) {
+ if (!d--) break;
+ usize mid = lo + ((hi - lo) >> 1);
+
+ u32 a = ent_v_load(p, lo);
+ u32 b = ent_v_load(p, mid);
+ u32 c = ent_v_load(p, hi - 1);
+ u32 pivot = (f(p, a, b) < 0
+ ? (f(p, b, c) < 0 ? b : (f(p, a, c) < 0 ? c : a))
+ : (f(p, a, c) < 0 ? a : (f(p, b, c) < 0 ? c : b)));
+
+ usize i = lo;
+ usize j = hi - 1;
+
+ for (;; i++, j--) {
+ for (; f(p, ent_v_load(p, i), pivot) < 0; i++);
+ for (; f(p, pivot, ent_v_load(p, j)) < 0; j--);
+ if (i >= j) break;
+ u32 t = ent_v_load(p, i);
+ ent_v_store(p, i, ent_v_load(p, j));
+ ent_v_store(p, j, t);
+ }
+
+ if (j - lo < hi - (j + 1)) {
+ fm_ent_qsort(p, f, lo, j + 1, d);
+ lo = j + 1;
+ } else {
+ fm_ent_qsort(p, f, j + 1, hi, d);
+ hi = j + 1;
+ }
+ }
+
+ fm_ent_isort(p, f, lo, hi);
+}
+
+static inline ent_sort_cb
+fm_sort_fn(u8 s)
+{
+ switch (s) {
+ case 'n': return fm_ent_cmp_name;
+ case 'N': return fm_ent_cmp_name_rev;
+ case 'e': return fm_ent_cmp_fext;
+ case 's': return fm_ent_cmp_size;
+ case 'S': return fm_ent_cmp_size_rev;
+ case 'd': return fm_ent_cmp_date;
+ case 'D': return fm_ent_cmp_date_rev;
+ default: return 0;
+ }
+}
+
+// }}}
+
+// Util {{{
+
+static inline cut
+fm_ent(const struct fm *p, usize i)
+{
+ u32 o = ent_v_geto(p, i, OFF);
+ u64 m = ent_load_off(p, o);
+ return (cut) {p->de + o, ent_get(m, LEN) };
+}
+
+static inline cut
+fm_file_type(mode_t m)
+{
+ if (S_ISREG(m) && m & (S_IXUSR|S_IXGRP|S_IXOTH))
+ return (cut){ S("executable file") };
+ if (S_ISREG(m)) return (cut){ S("regular file") };
+ if (S_ISDIR(m)) return (cut){ S("directory") };
+ if (S_ISLNK(m)) return (cut){ S("symlink") };
+ if (S_ISCHR(m)) return (cut){ S("char device") };
+ if (S_ISBLK(m)) return (cut){ S("block device") };
+ if (S_ISFIFO(m)) return (cut){ S("fifo") };
+ if (S_ISSOCK(m)) return (cut){ S("socket") };
+ return (cut){ S("unknown") };
+}
+
+static inline void
+str_push_time(str *s, s64 tz, const struct timespec *ts)
+{
+ s32 y; u32 m; u32 d;
+ u32 H; u32 M; u32 S;
+ ut_to_date_time(tz, ts->tv_sec, &y, &m, &d, &H, &M, &S);
+ str_push_u32_p(s, y, '0', 2);
+ str_push_c(s, '-');
+ str_push_u32_p(s, m, '0', 2);
+ str_push_c(s, '-');
+ str_push_u32_p(s, d, '0', 2);
+ str_push_c(s, ' ');
+ str_push_u32_p(s, H, '0', 2);
+ str_push_c(s, ':');
+ str_push_u32_p(s, M, '0', 2);
+ str_push_c(s, ':');
+ str_push_u32_p(s, S, '0', 2);
+}
+
+static inline int
+next_tok(const char *s, usize l, usize *c, cut *o)
+{
+ usize p = *c;
+ for (; p < l && (s[p] == ' ' || !s[p]); p++);
+ if (p >= l) {
+ *c = p;
+ *o = (cut){ s, 0 };
+ return 0;
+ }
+ usize t = p;
+ for (; p < l && s[p] != ' ' && s[p]; p++);
+ *c = p;
+ *o = (cut){ &s[t], p - t };
+ return 1;
+}
+
+// }}}
+
+// Visibility {{{
+
+static inline void
+fm_v_clr(struct fm *p, usize i)
+{
+ if (!ent_v_geto(p, i, VIS)) return;
+ u32 e = ent_v_load(p, i);
+ ent_v_set(&e, VIS, 0);
+ ent_v_store(p, i, e);
+}
+
+static inline void
+fm_v_assign(struct fm *p, usize i, u8 v)
+{
+ if (ent_v_geto(p, i, VIS) == v)
+ return;
+ u32 e = ent_v_load(p, i);
+ ent_v_set(&e, VIS, v);
+ ent_v_store(p, i, e);
+}
+
+static inline void
+fm_v_rebuild(struct fm *p)
+{
+ p->vl = 0;
+ u16 s = 0;
+ for (usize b = 0, c = BITSET_W(p->dl); b < c; b++) {
+ u64 w = 0;
+ for (usize j = 0; j < 64; j++) {
+ usize i = (b << 6) + j;
+ if (i >= p->dl) break;
+ if (ent_v_geto(p, i, VIS))
+ w |= 1ULL << j;
+ }
+ p->v[b] = w;
+ p->vp[b] = s;
+ s += u64_popcount(w);
+ }
+ p->vl = s;
+}
+
+// }}}
+
+// Filtering {{{
+
+static inline usize
+fm_filter_pct_rank(struct fm *p, usize idx)
+{
+ usize b = idx >> 6;
+ usize o = idx & 63;
+ u64 m = o ? ((1ULL << o) - 1) : 0ULL;
+ return (usize)p->vp[b] + u64_popcount(p->v[b] & m);
+}
+
+static inline void
+fm_filter_apply(struct fm *p, fm_filter f, cut cl, cut cr)
+{
+ for (usize i = 0; i < p->dl; i++)
+ if (ent_v_geto(p, i, TOMB))
+ fm_v_clr(p, i);
+ else
+ fm_v_assign(p, i, f(p, i, cl, cr));
+ fm_v_rebuild(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+fm_filter_apply_inc(struct fm *p, fm_filter f, cut cl, cut cr)
+{
+ for (usize i = ent_next(p, 0); i != SIZE_MAX; i = ent_next(p, i + 1))
+ if (ent_v_geto(p, i, TOMB) || !f(p, i, cl, cr))
+ fm_v_clr(p, i);
+ fm_v_rebuild(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static int
+fm_filter_hidden(struct fm *p, usize i, cut cl, cut cr)
+{
+ (void)cl;
+ (void)cr;
+ if (ent_v_geto(p, i, TOMB))
+ return 0;
+ if (p->f & FM_HIDDEN)
+ return 1;
+ return !ent_v_geto(p, i, DOT);
+}
+
+static inline int
+fm_filter_startswith(struct fm *p, usize i, cut cl, cut cr)
+{
+ usize al = cl.l;
+ usize bl = cr.l;
+ const char *am = cl.d;
+ const char *bm = cr.d;
+ u64 m = ent_load(p, i);
+ u32 o = ent_v_geto(p, i, OFF);
+ cut n = { p->de + o, ent_get(m, LEN) };
+ usize w = al + bl;
+ if (w > n.l) return 0;
+ if (al && (*n.d != *am || (al > 1 && memcmp(n.d + 1, am + 1, al - 1))))
+ return 0;
+ return !(bl && memcmp(n.d + al, bm, bl));
+}
+
+static int
+fm_filter_substr(struct fm *p, usize i, cut cl, cut cr)
+{
+ usize al = cl.l;
+ usize bl = cr.l;
+ const char *am = cl.d;
+ const char *bm = cr.d;
+ usize w = al + bl;
+ if (!w) return 1;
+ u64 m = ent_load(p, i);
+ u32 o = ent_v_geto(p, i, OFF);
+ cut n = { p->de + o, ent_get(m, LEN) };
+ if (w > n.l) return 0;
+ for (usize j = 0, x = n.l - w; j <= x; j++) {
+ if (al && memcmp(n.d + j, am, al)) continue;
+ if (bl && memcmp(n.d + j + al, bm, bl)) continue;
+ return 1;
+ }
+ return 0;
+}
+
+//
+// TODO: Make this incremental.
+//
+static inline void
+fm_filter_save(struct fm *p, cut cl, cut cr)
+{
+ usize c = sizeof(p->vq);
+ usize i = 0;
+ if (cl.l) {
+ usize n = MIN(cl.l, c - 1);
+ memcpy(p->vq, cl.d, n);
+ i += n;
+ }
+ if (cr.l && i < c - 1) {
+ usize n = MIN(cr.l, c - 1 - i);
+ memcpy(p->vq + i, cr.d, n);
+ i += n;
+ }
+ p->vq[i] = 0;
+ p->vql = i;
+}
+
+static inline void
+fm_filter_clear(struct fm *p)
+{
+ fm_filter_apply(p, fm_filter_hidden, CUT_NULL, CUT_NULL);
+ p->vql = 0;
+ p->f &= ~FM_SEARCH;
+}
+
+static inline usize
+fm_visible_select(struct fm *p, usize k)
+{
+ if (k >= p->vl) return SIZE_MAX;
+ usize lo = 0;
+ usize hi = BITSET_W(p->dl);
+ if (!hi) return SIZE_MAX;
+ while (lo + 1 < hi) {
+ usize mi = lo + ((hi - lo) >> 1);
+ if (p->vp[mi] <= k) lo = mi;
+ else hi = mi;
+ }
+ u64 w = p->v[lo];
+ usize rank = k - p->vp[lo];
+ for (; rank--; w &= w - 1);
+ return (lo << 6) + u64_ctz(w);
+}
+
+// }}}
+
+// UTF8 Truncation Cache {{{
+
+#define DFM_HT_OCC 0x800u
+#define DFM_HT_CACHE 0x40000000u
+#define CACHE_HASH(x) ((u32)((x) & 0x0003F7FFu))
+#define CACHE_LEN(x) ((u16)(((x) >> 18) & 0x0FFFu))
+#define CACHE_IS(x) (((x) & (DFM_HT_CACHE | DFM_HT_OCC)) == DFM_HT_CACHE)
+#define CACHE_PACK(h,l) (DFM_HT_CACHE | \
+ ((h) & 0x0003F7FFu) | (((u32)(l) & 0x0FFFu) << 18))
+
+static inline u16
+fm_cache_hash(const struct fm *p, const char *n, usize l)
+{
+ u32 h = hash_fnv1a32(n, l);
+ u32 m = h;
+ m ^= (u32)p->col * 0x9E3779B1u;
+ m ^= (u32)p->dv * 0x85EBCA6Bu;
+ m ^= m >> 16;
+ return (u16)m;
+}
+
+static inline usize
+fm_cache_slot(u16 h)
+{
+ return (h & 0xF7FFu) & (DFM_DIR_HT_CAP - 1);
+}
+
+static inline void
+fm_dir_ht_clear_cache(struct fm *p)
+{
+ for (usize i = 0; i < DFM_DIR_HT_CAP; i++)
+ if (CACHE_IS(p->ht[i])) p->ht[i] = 0;
+}
+
+// }}}
+
+// Directory Lookup {{{
+
+#define DFM_HT_TOMB 0x7FFu
+#define DFM_HT_IS_FREE(x) (!((x) & DFM_HT_OCC))
+
+static inline void
+fm_dir_ht_hash_split(u32 h, u16 *a, u8 *b)
+{
+ u32 m = h ^ (h >> 16);
+ u16 x = m & 0x07FF;
+ *a = x ? x : 1;
+ *b = (m >> 11) & 0x1F;
+}
+
+static inline usize
+fm_dir_ht_find(struct fm *p, cut c, u16 *o)
+{
+ u32 h = hash_fnv1a32(c.d, c.l);
+ u16 a;
+ u8 b;
+ fm_dir_ht_hash_split(h, &a, &b);
+ usize i = h & (DFM_DIR_HT_CAP - 1);
+ for (;;) {
+ u32 s = p->ht[i];
+ if (!s) {
+ *o = 0xFFFF;
+ return i;
+ }
+ if ((s & DFM_HT_OCC) && !CACHE_IS(s) && (s & 0x07FF) == a) {
+ u64 m = ent_load_off(p, s >> 12);
+ if (ent_get(m, HASH) == b) {
+ u16 j = (u16)ent_get(m, LOC);
+ if (!ent_v_geto(p, j, TOMB) && cut_cmp(fm_ent(p, j), c)) {
+ *o = j;
+ return i;
+ }
+ }
+ }
+ i = (i + 1) & (DFM_DIR_HT_CAP - 1);
+ }
+}
+
+static inline int
+fm_dir_exists(struct fm *p, cut c)
+{
+ u16 i;
+ fm_dir_ht_find(p, c, &i);
+ return i != 0xFFFF;
+}
+
+static inline usize
+fm_dir_ht_find_insert(struct fm *p, u32 h)
+{
+ usize i = h & (DFM_DIR_HT_CAP - 1);
+ usize ft = SIZE_MAX;
+ for (;;) {
+ u32 s = p->ht[i];
+ if (s == DFM_HT_TOMB) {
+ if (ft == SIZE_MAX) ft = i;
+ } else if (DFM_HT_IS_FREE(s) || CACHE_IS(s))
+ return ft != SIZE_MAX ? ft : i;
+ i = (i + 1) & (DFM_DIR_HT_CAP - 1);
+ }
+}
+
+static inline void
+fm_dir_ht_insert(struct fm *p, cut c, u16 o, u64 *m)
+{
+ u32 h = hash_fnv1a32(c.d, c.l);
+ u16 a;
+ u8 b;
+ fm_dir_ht_hash_split(h, &a, &b);
+ ent_set(m, HASH, b);
+ usize i = fm_dir_ht_find_insert(p, h);
+ p->ht[i] = (ent_v_geto(p, o, OFF) << 12) | DFM_HT_OCC | a;
+}
+
+static inline void
+fm_dir_ht_remove(struct fm *p, usize i)
+{
+ p->ht[i] = DFM_HT_TOMB;
+}
+
+static inline void
+fm_dir_ht_clear(struct fm *p)
+{
+ memset(p->ht, 0, sizeof(p->ht));
+}
+
+// }}}
+
+// Draw {{{
+
+static inline void
+fm_draw_flush(struct fm *p)
+{
+ STR_PUSH(&p->io, VT_ESU);
+ p->io.f(&p->io, p, 0);
+ STR_PUSH(&p->io, VT_BSU);
+}
+
+static inline usize
+fm_draw_trunc_name(struct fm *p, u64 m, const char *n, usize l, usize c)
+{
+ if (!c) return 0;
+ int w = ent_get(m, WIDE);
+ if (l < c) return l;
+ int u = ent_get(m, UTF8);
+ if (!u) return MIN(l, c);
+ if (!w) return utf8_trunc_narrow(n, l, c);
+ u16 h = fm_cache_hash(p, n, l);
+ usize i = fm_cache_slot(h);
+ for (usize j = 0; j < 4; j++) {
+ usize s = (i + j) & (DFM_DIR_HT_CAP - 1);
+ u32 v = p->ht[s];
+ if (CACHE_IS(v) && CACHE_HASH(v) == (h & 0xF7FFu))
+ return CACHE_LEN(v) < l ? CACHE_LEN(v) : l;
+ }
+ usize tl = utf8_trunc_wide(n, l, c);
+ for (usize j = 0; j < 4; j++) {
+ usize s = (i + j) & (DFM_DIR_HT_CAP - 1);
+ u32 v = p->ht[s];
+ if (CACHE_IS(v) || !(v & DFM_HT_OCC)) {
+ p->ht[s] = CACHE_PACK(h, (u16)tl);
+ break;
+ }
+ }
+ return tl;
+}
+
+static inline void
+fm_draw_ent(struct fm *p, usize n)
+{
+ u64 e = ent_load(p, n);
+ u32 o = ent_v_geto(p, n, OFF);
+ u32 t = ent_get(e, TYPE);
+ s32 vw = p->col;
+
+ switch (p->dv) {
+ case 's': vw -= 7; ent_size_decode(&p->io, ent_get(e, SIZE), 6, t); break;
+ case 'p': vw -= 11; ent_perm_decode(&p->io, ent_get(e, PERM), t); break;
+ case 't': vw -= 8; ent_time_decode(&p->io, ent_get(e, TIME)); break;
+ case 'a':
+ vw -= 26;
+ ent_perm_decode(&p->io, ent_get(e, PERM), t);
+ ent_time_decode(&p->io, ent_get(e, TIME));
+ ent_size_decode(&p->io, ent_get(e, SIZE), 6, t);
+ break;
+ }
+
+ switch (t) {
+ case ENT_DIR: STR_PUSH(&p->io, DFM_COL_DIR); vw--; break;
+ case ENT_FIFO: STR_PUSH(&p->io, DFM_COL_FIFO); break;
+ case ENT_LNK: STR_PUSH(&p->io, DFM_COL_LNK); break;
+ case ENT_LNK_BRK: STR_PUSH(&p->io, DFM_COL_LNK_BRK); break;
+ case ENT_LNK_DIR: STR_PUSH(&p->io, DFM_COL_LNK_DIR); break;
+ case ENT_REG_EXEC: STR_PUSH(&p->io, DFM_COL_REG_EXEC); vw--; break;
+ case ENT_SOCK: STR_PUSH(&p->io, DFM_COL_SOCK); break;
+ case ENT_SPEC: STR_PUSH(&p->io, DFM_COL_SPEC); break;
+ case ENT_UNKNOWN: STR_PUSH(&p->io, DFM_COL_UNKNOWN); break;
+ }
+
+ int m = p->f & FM_MARK_PWD && p->vml && ent_v_geto(p, n, MARK);
+ if (m) {
+ STR_PUSH(&p->io, DFM_COL_MARK " ");
+ vw -= 2;
+ }
+ if (p->c == n) STR_PUSH(&p->io, DFM_COL_CURSOR);
+ usize l = ent_get(e, LEN);
+ const char *dn = &p->de[o];
+ usize c = fm_draw_trunc_name(p, e, dn, l, vw < 0 ? 0 : vw);
+ str_push_sanitize(&p->io, dn, c);
+
+ switch (t) {
+ case ENT_LNK_DIR:
+ case ENT_DIR: str_push_c(&p->io, '/'); break;
+ case ENT_REG_EXEC: str_push_c(&p->io, '*'); break;
+ }
+
+ if (m) str_push_c(&p->io, '*');
+
+ if (ENT_IS_LNK(t)) {
+ u8 sl = ent_get(e, SIZE);
+ vw -= c + 4;
+ if (vw <= 0) goto e;
+ STR_PUSH(&p->io, VT_SGR0 " -> ");
+ if (sl) {
+ dn = &p->de[o + l + 2];
+ c = fm_draw_trunc_name(p, dn[-1], dn, sl, vw < 0 ? 0 : vw);
+ str_push_sanitize(&p->io, dn, c);
+ } else
+ str_push_c(&p->io, '?');
+ }
+
+e:
+ STR_PUSH(&p->io, VT_SGR0 VT_EL0 VT_CR);
+}
+
+static inline void
+fm_draw_dir(struct fm *p)
+{
+ usize s = p->y >= p->o ? p->y - p->o : 0;
+ usize m = p->vl - s;
+ usize d = MIN(m, p->row);
+ usize c = fm_visible_select(p, s);
+ STR_PUSH(&p->io, VT_CUP1);
+
+ for (usize i = 0; i < d && c != SIZE_MAX; i++) {
+ fm_draw_ent(p, c);
+ STR_PUSH(&p->io, VT_CUD1);
+ c = ent_next(p, c + 1);
+ }
+
+ for (usize i = d; i < p->row; i++)
+ STR_PUSH(&p->io, VT_EL2 VT_CUD1);
+}
+
+static inline void
+fm_draw_nav_begin(struct fm *p, cut c)
+{
+ vt_cup(&p->io, 0, p->row + (DFM_MARGIN - 1));
+ str_push(&p->io, c.d, c.l);
+ str_memset(&p->io, ' ', p->col);
+ STR_PUSH(&p->io, VT_CR);
+}
+
+static inline void
+fm_draw_nav_end(struct fm *p)
+{
+ STR_PUSH(&p->io, VT_SGR0);
+}
+
+static inline void
+fm_draw_inf(struct fm *p)
+{
+ cut c = p->f & (FM_TRUNC|FM_ERROR) ? CUT(DFM_COL_NAV_ERR) :
+ p->f & FM_ROOT ? CUT(DFM_COL_NAV_ROOT) : CUT(DFM_COL_NAV);
+ fm_draw_nav_begin(p, c);
+ str_push_c(&p->io, ' ');
+ str_push_u32(&p->io, p->y + !!p->vl);
+ str_push_c(&p->io, '/');
+ str_push_u32(&p->io, p->vl);
+ STR_PUSH(&p->io, " ");
+
+ str_push_c(&p->io, '[');
+ if (unlikely(p->f & FM_ROOT)) str_push_c(&p->io, 'R');
+ if (likely(!(p->f & FM_TRUNC))) str_push_c(&p->io, p->ds);
+ else str_push_c(&p->io, 'T');
+ if (unlikely(p->f & FM_ERROR)) str_push_c(&p->io, 'E');
+ if (unlikely(p->f & FM_HIDDEN)) str_push_c(&p->io, 'H');
+ STR_PUSH(&p->io, "] ");
+
+ if (p->vml) {
+ STR_PUSH(&p->io, DFM_COL_NAV_MARK " ");
+ str_push_u32(&p->io, p->vml);
+ STR_PUSH(&p->io, " marked " VT_SGR0);
+ str_push(&p->io, c.d, c.l);
+ str_push_c(&p->io, ' ');
+ }
+
+ if (likely(!(p->f & FM_TRUNC))) {
+ STR_PUSH(&p->io, "~");
+ ent_size_decode(&p->io, p->du, 0, ENT_TYPE_MAX);
+ STR_PUSH(&p->io, " ");
+ }
+
+ str_push_sanitize(&p->io, p->pwd.m, MIN(p->pwd.l, p->col));
+
+ if (p->f & FM_SEARCH) {
+ STR_PUSH(&p->io, "/" VT_SGR(1));
+ if (p->sf == fm_filter_substr) str_push_c(&p->io, '*');
+ str_push(&p->io, p->vq, p->vql);
+ STR_PUSH(&p->io, "*" VT_SGR0);
+ }
+
+ fm_draw_nav_end(p);
+}
+
+static inline void
+fm_draw_msg(struct fm *p, const char *s, usize l)
+{
+ p->f |= FM_MSG|FM_REDRAW_NAV;
+ rl_clear(&p->r);
+ str_push(&p->r.cl, s, l);
+}
+
+static inline void
+fm_draw_err(struct fm *p, const char *s, usize l, int e)
+{
+ p->f |= FM_MSG_ERR|FM_REDRAW_NAV;
+ rl_clear(&p->r);
+ STR_PUSH(&p->r.cl, " error: ");
+ str_push(&p->r.cl, s, l);
+ if (!e) return;
+ STR_PUSH(&p->r.cl, ": ");
+ str_push_s(&p->r.cl, strerror(e));
+}
+
+static inline void
+fm_draw_cmd(struct fm *p)
+{
+ vt_cup(&p->io, 0, p->row + DFM_MARGIN);
+ rl_write_visible(&p->r, &p->io);
+ STR_PUSH(&p->io, VT_EL0);
+}
+
+static inline void
+fm_draw_buf(struct fm *p, cut c)
+{
+ fm_draw_nav_begin(p, c);
+ str_push(&p->io, p->r.cl.m, p->r.cl.l);
+ fm_draw_nav_end(p);
+}
+
+static inline void
+fm_draw_nav(struct fm *p)
+{
+ if (p->f & (FM_MSG|FM_MSG_ERR)) {
+ fm_draw_buf(p, p->f & FM_MSG ? CUT(DFM_COL_NAV_MSG) : CUT(DFM_COL_NAV_ERR));
+ rl_clear(&p->r);
+ p->f &= ~(FM_MSG|FM_MSG_ERR);
+ } else
+ fm_draw_inf(p);
+}
+
+// }}}
+
+// Cursor {{{
+
+static inline void
+fm_cursor_set(struct fm *p, usize y, usize o)
+{
+ if (!p->vl || !p->row) {
+ p->y = 0;
+ p->o = 0;
+ p->c = ent_next(p, 0);
+ return;
+ }
+ if (y >= p->vl) y = p->vl - 1;
+ if (o >= p->row) o = p->row - 1;
+ if (o > y) o = y;
+ p->y = y;
+ p->o = o;
+ p->c = fm_visible_select(p, y);
+}
+
+static inline void
+fm_scroll_to(struct fm *p, cut d)
+{
+ if (!p->vl) goto e;
+ u16 i;
+ fm_dir_ht_find(p, d, &i);
+ if (i == 0xFFFF || !ent_v_geto(p, i, VIS))
+ goto e;
+ usize r = fm_filter_pct_rank(p, i);
+ usize ms = p->vl > p->row ? p->vl - p->row : 0;
+ usize h = p->row >> 1;
+ usize s = r <= p->row - 2 ? 0 : r >= ms ? ms : r > h ? r - h : 0;
+ if (s > ms) s = ms;
+ fm_cursor_set(p, r, r - s);
+ return;
+e:
+ fm_cursor_set(p, 0, 0);
+}
+
+static inline size
+fm_scroll_to_rank(struct fm *p, usize r)
+{
+ size dy = (size)r - (size)p->y;
+ if (!dy || !p->vl) return 0;
+ if (dy > (size)p->row || dy < -(size)p->row) {
+ size h = (size)p->row >> 1;
+ size j = (size)r - (dy > 0 ? h : -h);
+ if (j < 0) j = 0;
+ if (j >= (size)p->vl) j = (size)p->vl - 1;
+ fm_cursor_set(p, (usize)j, 0);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+ dy = (size)r - (size)p->y;
+ }
+ return dy;
+}
+
+static inline void
+fm_cursor_sync(struct fm *p)
+{
+ if (!p->vl || !p->row) {
+ p->y = 0;
+ p->o = 0;
+ p->c = SIZE_MAX;
+ return;
+ }
+ if (p->y >= p->vl) p->y = p->vl - 1;
+ if (p->o >= p->row) p->o = p->row - 1;
+ if (p->o > p->y) p->o = p->y;
+ p->c = fm_visible_select(p, p->y);
+}
+
+// }}}
+
+// Terminal {{{
+
+static inline int
+fm_term_resize(struct fm *p)
+{
+ if (term_size_update(&p->t, &p->row, &p->col) < 0)
+ return -1;
+ p->row = p->row > DFM_MARGIN ? p->row - DFM_MARGIN : 1;
+ rl_vw_set(&p->r, p->col);
+ vt_decstbm(&p->io, 1, p->row);
+ fm_cursor_set(p, p->y, p->o);
+ p->f |= FM_REDRAW;
+ return 0;
+}
+
+static inline int
+fm_term_raw(struct fm *p)
+{
+ STR_PUSH(&p->io,
+ VT_ALT_SCREEN_Y VT_DECTCEM_N VT_DECAWM_N VT_BPASTE_ON VT_ED2 VT_CUP1);
+ return term_raw(&p->t) < 0 ? -1 : fm_term_resize(p);
+}
+
+static inline int
+fm_term_cooked(struct fm *p)
+{
+ vt_decstbm(&p->io, 1, p->row + DFM_MARGIN);
+ STR_PUSH(&p->io,
+ VT_SGR0 VT_BPASTE_OFF VT_DECAWM_Y VT_DECTCEM_Y VT_ALT_SCREEN_N);
+ fm_draw_flush(p);
+ return term_cooked(&p->t);
+}
+
+static inline int
+fm_term_init(struct fm *p)
+{
+ int r = term_init(&p->t);
+ return fm_term_raw(p) < 0 ? -1 : r < 0 ? -1 : fm_term_resize(p);
+}
+
+static inline int
+fm_term_free(struct fm *p)
+{
+ int r = fm_term_cooked(p);
+ term_destroy(&p->t);
+ return r;
+}
+
+// }}}
+
+// Entry Marking {{{
+
+static inline void *
+fm_mark_slot(struct fm *p, usize i)
+{
+ return p->d.d + i * sizeof(char *);
+}
+
+static inline char *
+fm_mark_load(struct fm *p, usize i)
+{
+ char *v;
+ memcpy(&v, fm_mark_slot(p, p->mp + i), sizeof(v));
+ return v;
+}
+
+static inline void
+fm_mark_store(struct fm *p, usize i, char *v)
+{
+ memcpy(fm_mark_slot(p, p->mp + i), &v, sizeof(v));
+}
+
+static inline int
+fm_mark_has_room(const struct fm *p)
+{
+ return p->mp * sizeof(char *) >
+ (p->dl + DFM_MARK_CMD_PRE) * sizeof(u32) + sizeof(char *);
+}
+
+static inline u8
+fm_mark_len(const char *p)
+{
+ return (u8)p[-1];
+}
+
+static inline cut
+fm_mark_at(struct fm *p, usize i)
+{
+ char *m = fm_mark_load(p, i);
+ return (cut) { m, fm_mark_len(m) };
+}
+
+static inline void
+fm_mark_terminate(struct fm *p)
+{
+ fm_mark_store(p, p->ml, NULL);
+}
+
+static inline void
+fm_mark_write_at(struct fm *p, usize i, char *v)
+{
+ fm_mark_store(p, i, v);
+}
+
+static inline void
+fm_mark_write_newest(struct fm *p, char *v)
+{
+ p->mp--;
+ p->ml++;
+ fm_mark_store(p, 0, v);
+ fm_mark_terminate(p);
+}
+
+static inline void
+fm_mark_clear_ptr(struct fm *p)
+{
+#define DIR_PTR_CAP (DFM_DIR_MAX / (sizeof(char *) / sizeof(u32)))
+ p->mp = DIR_PTR_CAP - DFM_MARK_CMD_PRE - DFM_MARK_CMD_POST;
+}
+
+static inline void
+fm_mark_clear_range(struct fm *p, usize lo, usize hi)
+{
+ if (hi <= lo) return;
+ usize b0 = lo >> 6;
+ usize b1 = (hi - 1) >> 6;
+ for (usize b = b0; b <= b1; b++) {
+ u64 m = ~0ULL;
+ if (b == b0) {
+ u64 m0 = (lo & 63) ? ((1ULL << (lo & 63)) - 1ULL) : 0ULL;
+ m &= ~m0;
+ }
+ if (b == b1) {
+ u64 end = ((hi - 1) & 63);
+ u64 m1 = (end == 63) ? ~0ULL : ((1ULL << (end + 1)) - 1ULL);
+ m &= m1;
+ }
+ u64 tc = p->vm[b] & p->v[b] & m;
+ if (!tc) continue;
+ u64 w = tc;
+ while (w) {
+ usize i = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (i >= p->dl) break;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ }
+ p->vm[b] &= ~m;
+ p->vml -= u64_popcount(tc);
+ }
+}
+
+static inline void
+fm_mark_clear_all(struct fm *p)
+{
+ p->ml = 0;
+ p->vml = 0;
+ memset(p->vm, 0, sizeof(p->vm));
+ fm_mark_clear_ptr(p);
+ fm_mark_terminate(p);
+ for (usize i = 0; i < p->dl; i++) {
+ u32 e = ent_v_load(p, i);
+ ent_v_set(&e, MARK, 0);
+ ent_v_store(p, i, e);
+ }
+ p->dec = sizeof(p->de);
+}
+
+static inline int
+fm_mark_push(struct fm *p, cut c)
+{
+ usize n = c.l + 4;
+ if (unlikely(!fm_mark_has_room(p) || p->dec < p->del + n))
+ return 0;
+ p->dec -= n;
+ char *b = p->de + p->dec;
+ u16 h = (u16) hash_fnv1a32(c.d, c.l);
+ b[0] = (unsigned char)(h & 0xff);
+ b[1] = (unsigned char)(h >> 8);
+ b[2] = (unsigned char)c.l;
+ memcpy(b + 3, c.d, c.l);
+ b[c.l + 3] = 0;
+ fm_mark_write_newest(p, b + 3);
+ p->f |= FM_MARK_PWD;
+ return 1;
+}
+
+static inline void
+fm_mark_drop_idx(struct fm *p, usize i)
+{
+ if (!p->ml) return;
+ if (i != p->ml - 1)
+ fm_mark_write_at(p, i, fm_mark_load(p, p->ml - 1));
+ p->ml--;
+ fm_mark_terminate(p);
+}
+
+static inline usize
+fm_mark_find(struct fm *p, usize c, int d)
+{
+ usize n = SIZE_MAX;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ for (u64 w = p->vm[b] & p->v[b]; w; ) {
+ usize j = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (d > 0) {
+ if (j > c && (n == SIZE_MAX || j < n))
+ n = j;
+ } else {
+ if (j < c && (n == SIZE_MAX || j > n))
+ n = j;
+ }
+ }
+ }
+ return n;
+}
+
+static inline void
+fm_mark_apply_bitset(struct fm *p, const u64 *s)
+{
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ u64 w = s[b];
+ while (w) {
+ usize i = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (i >= p->dl) break;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 1);
+ ent_v_store(p, i, x);
+ }
+ }
+}
+
+static inline void
+fm_mark_invalidate(struct fm *p)
+{
+ p->ml = 0;
+ fm_mark_clear_ptr(p);
+ fm_mark_terminate(p);
+ p->dec = sizeof(p->de);
+}
+
+static inline usize
+fm_mark_materialize_range(struct fm *p, usize *x)
+{
+ if (!p->vml) return 0;
+ if (!p->mpwd.l) return 0;
+ usize n = 0;
+ usize i = *x;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = i >> 6; b < nw; b++) {
+ u64 w = p->vm[b] & p->v[b];
+ if (b == (i >> 6))
+ w &= ~((1ULL << (i & 63)) - 1ULL);
+ while (w) {
+ usize bit = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (bit >= p->dl) continue;
+ cut c = fm_ent(p, bit);
+ usize cl = c.l + 4;
+ if (!fm_mark_has_room(p) || p->dec < p->del + cl) {
+ *x = n ? bit : i;
+ return n;
+ }
+ if (!fm_mark_push(p, c)) {
+ *x = n ? bit : i;
+ return n;
+ }
+ n++;
+ *x = bit + 1;
+ }
+ }
+ *x = p->dl;
+ return n;
+}
+
+static inline int
+fm_mark_materialize(struct fm *p)
+{
+ if (!p->vml) return 0;
+ if (p->ml) return 0;
+ if (!p->mpwd.l) return 0;
+ if (!str_cmp(&p->mpwd, &p->pwd)) return 0;
+ fm_mark_invalidate(p);
+ usize oml = p->ml;
+ usize omp = p->mp;
+ usize odec = p->dec;
+ usize i = 0;
+ usize n = fm_mark_materialize_range(p, &i);
+ if (n != p->vml) {
+ p->ml = oml;
+ p->mp = omp;
+ p->dec = odec;
+ return -1;
+ }
+ return 0;
+}
+
+static inline void
+fm_mark_clear_idx(struct fm *p, usize i)
+{
+ if (!ent_v_geto(p, i, MARK))
+ return;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ usize b = i >> 6;
+ u64 bit = 1ULL << (i & 63);
+ p->vm[b] &= ~bit;
+ p->vml--;
+}
+
+static inline void
+fm_mark_pop_first(struct fm *p)
+{
+ if (!p->ml) return;
+ cut m = fm_mark_at(p, 0);
+ u16 j;
+ fm_dir_ht_find(p, m, &j);
+ if (j != 0xFFFF)
+ fm_mark_clear_idx(p, j);
+ fm_mark_drop_idx(p, 0);
+}
+
+static inline void
+fm_mark_clear(struct fm *p)
+{
+ fm_mark_clear_all(p);
+ p->mpwd.l = 0;
+ p->f &= ~FM_MARK_PWD;
+}
+
+static inline void
+fm_mark_init(struct fm *p)
+{
+ p->mpwd.l = 0;
+ str_push(&p->mpwd, p->pwd.m, p->pwd.l);
+ str_terminate(&p->mpwd);
+ p->f |= FM_MARK_PWD;
+}
+
+static inline u32
+fm_mark_toggle_idx(struct fm *p, usize i)
+{
+ u8 s = ent_v_geto(p, i, MARK);
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, !s);
+ ent_v_store(p, i, x);
+ usize b = i >> 6;
+ u64 bit = 1ULL << (i & 63);
+ if (s) {
+ p->vm[b] &= ~bit;
+ p->vml--;
+ } else {
+ p->vm[b] |= bit;
+ p->vml++;
+ }
+ if (p->ml) fm_mark_invalidate(p);
+ return 1;
+}
+
+// }}}
+
+// Filesystem {{{
+
+static inline int
+fm_dir_has_room(const struct fm *p, usize e)
+{
+ return (p->dl + e) * sizeof(u32) <=
+ p->mp * sizeof(char *) - DFM_MARK_CMD_PRE * sizeof(char *);
+}
+
+static inline void
+fm_dir_rebuild_loc(struct fm *p)
+{
+ for (usize i = 0; i < p->dl; i++) {
+ u64 m = ent_load(p, i);
+ ent_set(&m, LOC, (u16)i);
+ ent_store(p, i, m);
+ }
+}
+
+static inline void
+fm_dir_sort(struct fm *p)
+{
+ if (likely(!(p->f & FM_TRUNC))) {
+ fm_ent_qsort(p, fm_sort_fn(p->ds), 0, p->dl, 32);
+ fm_dir_rebuild_loc(p);
+ }
+ fm_filter f = rl_empty(&p->r) ? fm_filter_hidden : p->sf;
+ fm_filter_apply(p, f, rl_cl_get(&p->r), rl_cr_get(&p->r));
+ fm_cursor_set(p, p->y, p->o);
+}
+
+static inline void
+fm_dir_mark_rebuild(struct fm *p)
+{
+ if (!p->ml || !(p->f & FM_MARK_PWD))
+ return;
+ memset(p->vm, 0, sizeof(p->vm));
+ p->vml = 0;
+ for (usize i = 0; i < p->dl; i++) {
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ }
+ for (usize i = 0; i < p->ml; i++) {
+ cut m = fm_mark_at(p, i);
+ u16 j;
+ fm_dir_ht_find(p, m, &j);
+ if (j != 0xFFFF) {
+ u32 x = ent_v_load(p, j);
+ ent_v_set(&x, MARK, 1);
+ ent_v_store(p, j, x);
+ p->vm[j >> 6] |= 1ULL << (j & 63);
+ p->vml++;
+ u64 md = ent_load(p, j);
+ ent_store(p, j, md);
+ }
+ }
+}
+
+static inline void
+fm_dir_clear(struct fm *p)
+{
+ p->y = 0;
+ p->o = 0;
+ p->c = 0;
+ p->f &= ~FM_TRUNC;
+ rl_clear(&p->r);
+ p->del = 0;
+ p->dl = 0;
+ p->du = 0;
+ p->st = 0;
+ fm_dir_ht_clear(p);
+}
+
+static inline int
+fm_dir_load_ent(struct fm *p, const char *s)
+{
+ if (s[0] == '.' && (s[1] == '\0' || (s[1] == '.' && s[2] == '\0')))
+ return 0;
+ if (unlikely(!fm_dir_has_room(p, 1)))
+ return -1;
+
+ u8 utf8;
+ u8 wide;
+ u8 l = (u8)ent_name_len(s, &utf8, &wide);
+
+ if (unlikely(p->del + sizeof(u64) + l + 1 >= p->dec))
+ return -1;
+
+ u64 m = 0;
+ u32 o = p->del;
+ u32 x = 0;
+ ent_v_set(&x, OFF, o + sizeof(m));
+ ent_v_set(&x, CHAR, s[0]);
+ ent_v_set(&x, DOT, s[0] == '.');
+ ent_v_store(p, p->dl, x);
+ ent_set(&m, LEN, l);
+ ent_set(&m, LOC, (u16)p->dl);
+ ent_set(&m, UTF8, utf8);
+ ent_set(&m, WIDE, wide);
+
+ memcpy(p->de + p->del + sizeof(m), s, l + 1);
+ p->del += sizeof(m) + l + 1;
+ p->dl++;
+
+ struct stat st;
+ if (unlikely(fstatat(p->dfd, s, &st, AT_SYMLINK_NOFOLLOW) == -1)) {
+ ent_set(&m, TYPE, ENT_UNKNOWN);
+ ent_set(&m, SIZE, 0);
+ ent_set(&m, TIME, 0);
+ ent_set(&m, PERM, 0);
+ goto t;
+ }
+
+ if (S_ISLNK(st.st_mode)) {
+ struct stat ts;
+ if (fstatat(p->dfd, s, &ts, 0) == -1)
+ ent_map_stat(&m, &st, ENT_LNK_BRK);
+ else
+ ent_map_stat(&m, &ts, S_ISDIR(ts.st_mode) ? ENT_LNK_DIR : ENT_LNK);
+
+ usize ll = (usize)st.st_size;
+ if (p->del + ll + 2 < p->dec) {
+ char *lm = p->de + p->del + 1;
+ ssize_t r = readlinkat(p->dfd, s, lm, st.st_size);
+ if (likely(r != -1)) {
+ u8 lu;
+ u8 lw;
+ ent_name_len(lm, &lu, &lw);
+ u8 f = 0;
+ lnk_set(&f, UTF8, lu);
+ lnk_set(&f, WIDE, lw);
+ lm[-1] = f;
+ lm[ll] = 0;
+ p->del += ll + 2;
+ } else {
+ ent_set(&m, SIZE, 0);
+ goto t;
+ }
+ }
+ goto w;
+ }
+
+ ent_map_stat(&m, &st, ENT_TYPE_MAX);
+w:
+ ent_map_stat_size(&m, &st);
+ u64 sz = ent_size_bytes(ent_get(m, SIZE), ent_get(m, TYPE));
+ p->du = ent_size_add(p->du, sz);
+t:
+ fm_dir_ht_insert(p, (cut){ s, l }, (u16)(p->dl - 1), &m);
+ memcpy(p->de + o, &m, sizeof(m));
+ return 0;
+}
+
+static inline int
+fm_dir_load(struct fm *p)
+{
+ int d = openat(p->dfd, ".", O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ if (d < 0) return 0;
+ DIR *n = fdopendir(d);
+ if (unlikely(!n)) { close(d); return 0; }
+ fm_dir_clear(p);
+
+ for (struct dirent *e; (e = readdir(n)); )
+ if (fm_dir_load_ent(p, e->d_name) == -1) {
+ p->f |= FM_TRUNC;
+ break;
+ }
+
+ closedir(n);
+ fm_dir_sort(p);
+ fm_dir_mark_rebuild(p);
+ fs_watch(&p->p, ".");
+ return 1;
+}
+
+static inline int
+fm_dir_add(struct fm *p, cut c)
+{
+ if (fm_dir_exists(p, c))
+ return 0;
+ if (fm_dir_load_ent(p, c.d) == -1)
+ return -1;
+ int h = !(p->f & FM_HIDDEN) && *c.d == '.';
+ fm_v_assign(p, p->dl - 1, !h);
+ p->f |= FM_DIRTY;
+ p->st = ent_v_geto(p, p->dl - 1, OFF);
+ return 0;
+}
+
+static inline int
+fm_dir_del(struct fm *p, cut c)
+{
+ u16 f;
+ usize s = fm_dir_ht_find(p, c, &f);
+ if (f == 0xFFFF) return -1;
+
+ u64 m = ent_load(p, f);
+ u64 sz = ent_size_bytes(ent_get(m, SIZE), ent_get(m, TYPE));
+ p->du = ent_size_sub(p->du, sz);
+
+ u32 x = ent_v_load(p, f);
+ ent_v_set(&x, TOMB, 1);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, f, x);
+
+ fm_dir_ht_remove(p, s);
+ p->f |= FM_DIRTY;
+ return 0;
+}
+
+static inline void
+fm_dir_refresh(struct fm *p)
+{
+ cut o = p->c == SIZE_MAX ? CUT_NULL : fm_ent(p, p->c);
+ fm_dir_load(p);
+ fm_scroll_to(p, o);
+ fm_cursor_sync(p);
+ p->f |= FM_DIRTY;
+}
+
+// }}}
+
+// Core {{{
+
+static inline int
+fm_path_change(struct fm *p)
+{
+ fm_filter_clear(p);
+ if (fm_mark_materialize(p) < 0) {
+ fm_draw_err(p,
+ S("Not enough memory to materialize marks, unmark to cd"), 0);
+ return 0;
+ }
+ return 1;
+}
+
+static inline int
+fm_path_open(struct fm *p)
+{
+ int fd = open(p->pwd.m, O_DIRECTORY|O_CLOEXEC);
+ if (fd == -1) return 0;
+ if (p->dfd != AT_FDCWD) close(p->dfd);
+ p->dfd = fd;
+ p->f ^= (-(p->mpwd.l && str_cmp(&p->mpwd, &p->pwd)) ^ p->f) & FM_MARK_PWD;
+ return fchdir(fd) != -1;
+}
+
+static inline void
+fm_path_save(struct fm *p)
+{
+ p->ppwd.l = 0;
+ str_push(&p->ppwd, p->pwd.m, p->pwd.l);
+}
+
+static inline void
+fm_path_load(struct fm *p)
+{
+ p->pwd.l = 0;
+ str_push(&p->pwd, p->ppwd.m, p->ppwd.l);
+}
+
+static inline int
+fm_path_cd(struct fm *p, const char *d, usize l)
+{
+ if (!fm_path_change(p)) return 0;
+ fm_path_save(p);
+ p->pwd.l = 0;
+ str_push(&p->pwd, d, l);
+ str_terminate(&p->pwd);
+ usize nl = fm_path_resolve(p->pwd.m, p->pwd.l);
+ p->pwd.l = nl;
+ int r = fm_path_open(p);
+ if (!r) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ }
+ return r && fm_dir_load(p);
+}
+
+static inline int
+fm_path_chdir(struct fm *p, const char *d)
+{
+ if (!fm_path_change(p)) return 0;
+ fm_path_save(p);
+ p->pwd.l = 0;
+ str_push(&p->pwd, d, strlen(d));
+ str_terminate(&p->pwd);
+ int r = fm_path_open(p);
+ if (!r || !fm_dir_load(p)) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ return 0;
+ }
+ fm_path_save(p);
+ if (!getcwd(p->pwd.m, p->pwd.c)) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ return 0;
+ }
+ p->pwd.l = strlen(p->pwd.m);
+ str_terminate(&p->pwd);
+ return 1;
+}
+
+static inline int
+fm_path_cd_relative(struct fm *p, const char *d, u8 l)
+{
+ if (!fm_path_change(p)) return 0;
+ fm_path_save(p);
+ if (p->pwd.l > 1) str_push_c(&p->pwd, '/');
+ str_push(&p->pwd, d, l);
+ str_terminate(&p->pwd);
+ usize nl = fm_path_resolve(p->pwd.m, p->pwd.l);
+ p->pwd.l = nl;
+ int r = fm_path_open(p);
+ if (!r) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ }
+ return r && fm_dir_load(p);
+}
+
+static inline cut
+fm_path_cd_up(struct fm *p)
+{
+ if (!fm_path_change(p)) return CUT_NULL;
+ fm_path_save(p);
+ usize l = p->pwd.l;
+ usize i = l;
+ for (; i > 1 && p->pwd.m[i - 1] != '/'; i--);
+ usize n = (i > 1) ? i - 1 : 1;
+ char s = p->pwd.m[n];
+ p->pwd.m[n] = 0;
+ p->pwd.l = n;
+ int r = fm_path_open(p);
+ if (!r) {
+ p->pwd.m[n] = s;
+ p->pwd.l = l;
+ fm_draw_err(p, S("cd"), errno);
+ }
+ return r && fm_dir_load(p) ? (cut){ p->ppwd.m + i, l - i } : CUT_NULL;
+}
+
+static inline int
+fm_exec(struct fm *p, int in, const char *d, const char *const a[], bool bg, bool tf)
+{
+ if (tf) fm_term_cooked(p);
+ int r = run_cmd(bg ? p->t.null : p->t.fd, in, d, a, bg);
+ if (tf) fm_term_raw(p);
+ if (r == -1) {
+ fm_draw_err(p, S("exec"), errno);
+ return -1;
+ }
+ if (WIFEXITED(r)) {
+ int ec = WEXITSTATUS(r);
+ if (ec == 127) {
+ fm_draw_err(p, S("exec: command not found"), 0);
+ return -1;
+ } else if (ec) {
+ fm_draw_err(p, S("exec: exited non-zero"), 0);
+ return -1;
+ }
+ }
+ if (WIFSIGNALED(r)) {
+ fm_draw_err(p, S("exec: killed by signal"), 0);
+ return -1;
+ }
+ return 0;
+}
+
+static inline void
+fm_open(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ cut c = fm_ent(p, p->c);
+ if (!c.l) return;
+ u64 m = ent_load(p, p->c);
+ if (ENT_IS_DIR(ent_get(m, TYPE)))
+ fm_path_cd_relative(p, c.d, c.l);
+ else if (unlikely(p->f & FM_PICKER)) {
+ str_push_c(&p->pwd, '/');
+ str_push(&p->pwd, c.d, c.l);
+ term_set_dead(&p->t, 1);
+ } else {
+ const char *const a[] = { p->opener.d, c.d, NULL };
+ fm_exec(p, -1, NULL, a, 0, 1);
+ }
+}
+
+// }}}
+
+// Command {{{
+
+struct fm_cmd {
+ cut prompt;
+ cut left;
+ cut right;
+ fm_key_press press;
+ fm_key_enter enter;
+ u32 config;
+};
+
+#define FM_CMD(C, ...) \
+static inline void \
+C(struct fm *p) \
+{ \
+ fm_cmd(p, &(struct fm_cmd){__VA_ARGS__}); \
+}
+
+enum {
+ CMD_BG = 1 << 0,
+ CMD_CONFLICT = 1 << 1,
+ CMD_MUT = 1 << 2,
+ CMD_EXEC = 1 << 3,
+ CMD_MARK_DIR = 1 << 4,
+ CMD_NOT_MARK_DIR = 1 << 5,
+ CMD_STDIN = 1 << 6,
+ CMD_FILE_CURSOR = 1 << 7,
+ CMD_EXEC_MARK = 1 << 8,
+ CMD_EXEC_ROOT = 1 << 9,
+
+ CMD_MODE_EACH = 0,
+ CMD_MODE_VIRTUAL,
+ CMD_MODE_CHUNK,
+ CMD_MODE_BULK,
+ CMD_MODE_SINGLE,
+};
+
+static inline void
+fm_cmd_exec(struct fm *p)
+{
+ if (p->kd && p->kd(p, &p->r.cl) >= 0)
+ rl_clear(&p->r);
+ p->r.vx = 0;
+ p->r.pr.l = 0;
+ p->kp = 0;
+ p->kd = 0;
+}
+
+static inline void
+fm_cmd(struct fm *p, struct fm_cmd *c)
+{
+ if (!c->press && !c->enter) {
+ fm_draw_err(p, S("no callbacks defined"), 0);
+ return;
+ }
+ rl_clear(&p->r);
+ rl_pr_set(&p->r, c->prompt);
+ if (c->left.l) {
+ str_push(&p->r.cl, c->left.d, c->left.l);
+ str_terminate(&p->r.cl);
+ }
+ if (c->right.l) rl_cr_set(&p->r, c->right);
+ if (c->config & CMD_FILE_CURSOR) {
+ if (p->c == SIZE_MAX) return;
+ cut e = fm_ent(p, p->c);
+ str_push(&p->r.cl, e.d, e.l);
+ }
+ rl_cl_sync(&p->r);
+ p->cf = c->config;
+ p->kp = c->press;
+ p->kd = c->enter;
+ p->f |= FM_REDRAW_CMD;
+ if (p->f & FM_ROOT && !(p->cf & CMD_EXEC_ROOT))
+ return;
+ if ((p->cf & CMD_EXEC_MARK && p->vml) || p->cf & CMD_EXEC)
+ fm_cmd_exec(p);
+}
+
+static inline void
+fm_cmd_search_press(struct fm *p, int k, cut cl, cut cr)
+{
+ if (cl.l > 1 && k != KEY_BACKSPACE && p->vl != p->dl && !cr.l) {
+ fm_filter_apply_inc(p, p->sf, cl, cr);
+ } else
+ fm_filter_apply(p, p->sf, cl, cr);
+ fm_filter_save(p, cl, cr);
+ fm_cursor_set(p, 0, 0);
+}
+
+static inline int
+fm_cmd_search(struct fm *p, str *s)
+{
+ if (p->vl == 1) fm_open(p);
+ else {
+ if (s->l) {
+ cut q = (cut){ s->m, s->l };
+ fm_filter_apply(p, p->sf, q, CUT_NULL);
+ fm_filter_save(p, q, CUT_NULL);
+ }
+ else
+ fm_filter_apply(p, fm_filter_hidden, (cut){ s->m, s->l }, CUT_NULL);
+ fm_cursor_set(p, 0, 0);
+ }
+ return -1;
+}
+
+static inline int
+fm_cmd_cd(struct fm *p, str *s)
+{
+ int r = 0;
+ if (s->m[0] == '/')
+ r = fm_path_cd(p, s->m, s->l);
+ else
+ r = fm_path_cd_relative(p, s->m, s->l);
+ return r ? 0 : -1;
+}
+
+static u8
+fm_prompt_conflict(struct fm *p, cut d)
+{
+ fm_draw_nav_begin(p, CUT(DFM_COL_NAV_ERR));
+ STR_PUSH(&p->io, "conflict: '");
+ str_push(&p->io, d.d, d.l);
+ STR_PUSH(&p->io, "': try overwrite?");
+ STR_PUSH(&p->io, " [a]bort [y]es [Y]es all [n]o [N]o all");
+ fm_draw_nav_end(p);
+ fm_draw_flush(p);
+ for (;;) {
+ if (!term_key_read(p->t.fd, &p->k))
+ return 'a';
+ switch (p->k.b[0]) {
+ case 'a': case 'y': case 'Y': case 'n': case 'N':
+ return p->k.b[0];
+ }
+ }
+}
+
+static inline int
+fm_prepare_marks_conflict(struct fm *p)
+{
+ usize i = 0;
+ cut m = CUT_NULL;
+ int om = 0;
+ if (!p->ml) {
+ m = fm_ent(p, p->c);
+ goto c;
+ }
+ for (; i < p->ml; ) {
+ m = fm_mark_at(p, i);
+c:
+ if (!fm_dir_exists(p, m)) goto s;
+ if (om != 'Y' && om != 'N')
+ om = fm_prompt_conflict(p, m);
+ switch (om) {
+ case 'a': return -1;
+ case 'y': case 'Y': goto s;
+ case 'n': fm_mark_drop_idx(p, i); om = -2; continue;
+ case 'N': p->ml = 0; return -1;
+ }
+s:
+ i++;
+ }
+ return om;
+}
+
+static inline int
+fm_cmd_build_bulk_exec(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ usize omi = ti;
+ usize prn = ti;
+ usize pon = (tc - omi - 1);
+ usize tt = prn + p->ml + pon;
+ usize pri = p->mp - prn;
+ usize poi = p->mp + p->ml;
+ char **m = (char **)(void *)p->d.d;
+ cut c;
+ for (usize i = 0, n = 0; next_tok(s.d, s.l, &n, &c); i++) {
+ if (i == omi) {
+ if (p->ml) continue;
+ cut e = fm_ent(p, p->c);
+ c = e;
+ } else if (c.l == 2 && c.d[0] == '%' && c.d[1] == 'd')
+ c = (cut) { p->pwd.m, p->pwd.l };
+ else if (c.l > 1 && c.d[0] == '$') {
+ c.d = getenv(c.d + 1);
+ if (!c.d) return -2;
+ }
+ m[i < ti ? pri++ : poi++] = (char *)c.d;
+ }
+ char **a = &m[p->mp - prn];
+ a[tt] = NULL;
+ const char *wd = p->mpwd.m;
+ return fm_exec(p, -1, wd, (const char *const *)a, f & CMD_BG, !(f & CMD_BG));
+}
+
+static inline int
+fm_cmd_build_bulk_chunk(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ int r = 0;
+ assert(!p->ml);
+ usize b = 0;
+ while (b < p->dl && p->vml) {
+ fm_mark_invalidate(p);
+ usize pb = b;
+ usize n = fm_mark_materialize_range(p, &b);
+ if (!n) break;
+ r = fm_cmd_build_bulk_exec(p, s, ti, tc, f);
+ if (r < 0) return r;
+ fm_mark_clear_range(p, pb, b);
+ }
+ if (!p->vml) fm_mark_clear_all(p);
+ return r;
+}
+
+static inline int
+fm_cmd_build_bulk(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ if (fm_mark_materialize(p) < 0) {
+ fm_draw_err(p, S("Not enough memory to materialize marks"), 0);
+ return -1;
+ }
+ if (fm_cmd_build_bulk_exec(p, s, ti, tc, f) < 0)
+ return -1;
+ fm_mark_clear_all(p);
+ return 0;
+}
+
+static inline int
+fm_cmd_build(struct fm *p, cut s, usize ti, usize tc, u32 f, usize mp, cut mk, bool t)
+{
+ char **m = (char **)(void *)p->d.d;
+ usize pri = mp;
+ cut c;
+ for (usize j = 0, n = 0; next_tok(s.d, s.l, &n, &c); j++)
+ if (j == ti)
+ m[pri++] = (char *)mk.d;
+ else if (c.l == 2 && c.d[0] == '%' && c.d[1] == 'd')
+ m[pri++] = p->pwd.m;
+ else if (c.l > 1 && c.d[0] == '$') {
+ char *e = getenv(c.d + 1);
+ if (!e) return -2;
+ m[pri++] = e;
+ } else
+ m[pri++] = (char *)c.d;
+ u32 lf = f;
+ m[mp + tc] = NULL;
+ int fd = -1;
+ if (lf & CMD_STDIN) {
+ fd = open(mk.d, O_RDONLY|O_CLOEXEC);
+ if (fd < 0) return -1;
+ }
+ int r = fm_exec(p, fd, p->pwd.m, (const char *const *)&m[mp], lf & CMD_BG, t);
+ if (fd >= 0) close(fd);
+ return r;
+}
+
+static inline int
+fm_cmd_build_each_virtual(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ cut mk = CUT_NULL;
+ usize mp = p->mp - (tc + 1);
+ if (!p->vml) {
+ if (p->c == SIZE_MAX) return 0;
+ mk = fm_ent(p, p->c);
+ return fm_cmd_build(p, s, ti, tc, f, mp, mk, 1);
+ }
+ if (!(f & CMD_BG)) fm_term_cooked(p);
+ int r = 0;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ for (u64 w = p->vm[b] & p->v[b]; w; ) {
+ usize i = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (i >= p->dl) break;
+ mk = fm_ent(p, i);
+ if (fm_cmd_build(p, s, ti, tc, f, mp, mk, 0) < 0) {
+ r = -1;
+ goto e;
+ }
+ fm_mark_clear_idx(p, i);
+ if (!p->vml) goto e;
+ }
+ }
+e:
+ if (!(f & CMD_BG)) fm_term_raw(p);
+ return r;
+}
+
+static inline int
+fm_cmd_build_each(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ cut mk = CUT_NULL;
+ usize mp = p->mp - (tc + 1);
+ if (!p->vml) {
+ if (p->c == SIZE_MAX) return 0;
+ mk = fm_ent(p, p->c);
+ return fm_cmd_build(p, s, ti, tc, f, mp, mk, 1);
+ }
+ if (!(f & CMD_BG)) fm_term_cooked(p);
+ for (; p->ml; ) {
+ mk = fm_mark_at(p, 0);
+ str fp = p->mpwd;
+ str_push_c(&fp, '/');
+ str_push(&fp, mk.d, mk.l);
+ str_terminate(&fp);
+ mk = (cut) { fp.m, fp.l };
+ if (fm_cmd_build(p, s, ti, tc, f, mp, mk, 0) < 0) {
+ if (!(f & CMD_BG)) fm_term_raw(p);
+ return -1;
+ }
+ fm_mark_pop_first(p);
+ if (p->vml) p->vml--;
+ }
+ if (!(f & CMD_BG)) fm_term_raw(p);
+ return 0;
+}
+
+static inline int
+fm_cmd_sh(struct fm *p, cut c)
+{
+ if (!c.l) return 0;
+ cut f = !p->ml && p->c != SIZE_MAX ? fm_ent(p, p->c) : CUT_NULL;
+ cut sh = get_env("SHELL", "/bin/sh");
+ const char *cmd[] = { sh.d, DFM_SHELL_OPTS, c.d, CFG_NAME, f.d, NULL };
+ STATIC_ASSERT(ARR_SIZE(cmd) < DFM_MARK_CMD_PRE, "");
+ usize l = ARR_SIZE(cmd) - (!f.l * 2);
+ char **m = (char **)(void *)p->d.d;
+ char **a = &m[p->mp - l];
+ memcpy(a, cmd, sizeof(*cmd) * l);
+ if (!f.l) a[l + p->ml] = NULL;
+ int r =
+ fm_exec(p, -1, f.l ? p->pwd.m : p->mpwd.m, (const char *const *)a, 0, 1);
+ if (!f.l) fm_mark_clear_all(p);
+ if (r < 0) p->f |= FM_ERROR;
+ return r;
+}
+
+static inline int
+fm_cmd_run_sh(struct fm *p, str *s)
+{
+ if (fm_mark_materialize(p) < 0) {
+ fm_draw_err(p, S("Not enough memory to materialize marks"), 0);
+ return -1;
+ }
+ usize e = s->m[0] == '!';
+ return fm_cmd_sh(p, (cut){ s->m + e, s->l - e });
+}
+
+static inline cut
+fm_cmd_parse(struct fm *p, str *s, usize *oti, usize *ott, usize *otc)
+{
+ cut c;
+ usize tc = 0;
+ usize ti = SIZE_MAX;
+ usize tt = 0;
+ usize li = 0;
+ cut lt = CUT_NULL;
+ for (usize i = 0, n = 0; next_tok(s->m, s->l, &n, &c); i++, tc++) {
+ ((char *)c.d)[c.l] = 0;
+ lt = c;
+ li = i;
+ if (ti == SIZE_MAX && c.l == 2 && c.d[0] == '%') {
+ if (c.d[1] == 'm' || c.d[1] == 'f' ) {
+ ti = i;
+ tt = c.d[1];
+ }
+ }
+ }
+ if (tc && lt.l == 1 && lt.d[0] == '&') {
+ p->cf |= CMD_BG;
+ tc--;
+ if (ti != SIZE_MAX && li < ti) ti--;
+ }
+ *oti = ti;
+ *ott = tt;
+ *otc = tc;
+ c = (cut){ s->m, s->l };
+ switch (s->m[0]) {
+ case '<': c.d++; c.l--; p->cf |= CMD_STDIN; break;
+ }
+ return c;
+}
+
+static inline int
+fm_cmd_run(struct fm *p, str *s)
+{
+ int m = CMD_MODE_SINGLE;
+ int r = 0;
+ if (!s->l) return 0;
+ if (p->cf & CMD_MARK_DIR && !(p->f & FM_MARK_PWD) && p->vml) {
+ fm_draw_err(p, S("Not in mark directory"), 0);
+ return -1;
+ }
+ if (p->cf & CMD_NOT_MARK_DIR && p->f & FM_MARK_PWD) {
+ fm_draw_err(p, S("In mark directory"), 0);
+ return -1;
+ }
+ if (s->m[0] == '!')
+ return fm_cmd_run_sh(p, s);
+ usize ti;
+ usize tt;
+ usize tc;
+ cut a = fm_cmd_parse(p, s, &ti, &tt, &tc);
+ if ((tt && (!p->vml && !p->vl))) {
+ fm_draw_err(p, S("nothing to operate on"), 0);
+ return -1;
+ }
+ if (p->cf & (CMD_STDIN|CMD_FILE_CURSOR))
+ m = CMD_MODE_SINGLE;
+ else if (tt == 'm') {
+ m = p->f & FM_MARK_PWD ? CMD_MODE_CHUNK : CMD_MODE_BULK;
+ m = p->vml ? m : CMD_MODE_EACH;
+ } else if (tt == 'f')
+ m = p->f & FM_MARK_PWD ? CMD_MODE_VIRTUAL : CMD_MODE_EACH;
+ switch (m) {
+ case CMD_MODE_SINGLE:
+ case CMD_MODE_EACH:
+ case CMD_MODE_VIRTUAL:
+ if (tc > DFM_MARK_CMD_PRE) {
+ fm_draw_err(p, S("argv too large"), 0);
+ return -1;
+ }
+ break;
+ case CMD_MODE_BULK:
+ case CMD_MODE_CHUNK:
+ if (ti > DFM_MARK_CMD_PRE || tc - ti - 1 > DFM_MARK_CMD_POST) {
+ fm_draw_err(p, S("argv too large"), 0);
+ return -1;
+ }
+ }
+ switch (m) {
+ case CMD_MODE_EACH:
+ case CMD_MODE_BULK:
+ if (p->cf & CMD_CONFLICT) {
+ r = fm_prepare_marks_conflict(p);
+ if (r < 0) p->f |= FM_REDRAW_NAV;
+ if (r == -1) return 0;
+ if (r == -2 && !p->ml) return 0;
+ }
+ break;
+ }
+ cut mk = CUT_NULL;
+ switch (m) {
+ case CMD_MODE_SINGLE:
+ if (p->c != SIZE_MAX)
+ mk = fm_ent(p, p->c);
+ r = fm_cmd_build(p, a, ti, tc, p->cf, p->mp - (tc + 1), mk, 1);
+ break;
+ case CMD_MODE_EACH:
+ r = fm_cmd_build_each(p, a, ti, tc, p->cf);
+ break;
+ case CMD_MODE_VIRTUAL:
+ r = fm_cmd_build_each_virtual(p, a, ti, tc, p->cf);
+ break;
+ case CMD_MODE_BULK:
+ r = fm_cmd_build_bulk(p, a, ti, tc, p->cf);
+ break;
+ case CMD_MODE_CHUNK:
+ r = fm_cmd_build_bulk_chunk(p, a, ti, tc, p->cf);
+ break;
+ }
+#ifdef FS_WATCH
+ if (r != -1 && p->cf & CMD_MUT)
+ p->f |= FM_DIRTY_WITHIN;
+#else
+ if (r != -1 && (p->cf & (CMD_MUT)))
+ fm_dir_refresh(p);
+#endif
+ if (r == -2)
+ fm_draw_err(p, S("environment variable unset"), 0);
+ if (r < 0) p->f |= FM_ERROR;
+ return r;
+}
+
+// }}}
+
+// Action {{{
+
+static inline void
+act_quit(struct fm *p)
+{
+ term_set_dead(&p->t, 1);
+}
+
+// static inline void
+// act_crash(struct fm *p)
+// {
+// (void) p;
+// *(volatile int *)0 = 1;
+// }
+
+static inline void
+act_quit_print_pwd(struct fm *p)
+{
+ p->f |= FM_PRINT_PWD;
+ act_quit(p);
+}
+
+static inline void
+act_cd_home(struct fm *p)
+{
+ cut h = get_env("HOME", "");
+ if (!h.l) return;
+ fm_path_cd(p, h.d, h.l);
+}
+
+static inline void
+act_cd_mark_directory(struct fm *p)
+{
+ if (!p->vml) return;
+ fm_path_cd(p, p->mpwd.m, p->mpwd.l);
+}
+
+static inline void
+act_cd_last(struct fm *p)
+{
+ fm_path_cd(p, p->ppwd.m, p->ppwd.l);
+}
+
+static inline void
+act_copy_pwd(struct fm *p)
+{
+ int fd = fd_from_buf(p->pwd.m, p->pwd.l);
+ if (fd < 0)
+ fm_draw_err(p, S("PWD too large"), errno);
+ else {
+ const char *const a[] = { get_env("DFM_COPYER", DFM_COPYER).d, NULL };
+ fm_exec(p, fd, NULL, a, 1, 0);
+ close(fd);
+ fm_draw_msg(p, S("Copied PWD to clipboard"));
+ }
+}
+
+#define ACT_CD_BOOKMARK(N) \
+static inline void \
+act_cd_bookmark_##N(struct fm *p) \
+{ \
+ cut e = get_env("DFM_BOOKMARK_" #N, DFM_BOOKMARK_##N); \
+ if (e.l) fm_path_cd(p, e.d, e.l); \
+ else fm_draw_err(p, S("DFM_BOOKMARK_" #N " not set"), 0); \
+}
+ACT_CD_BOOKMARK(0) ACT_CD_BOOKMARK(1) ACT_CD_BOOKMARK(2) ACT_CD_BOOKMARK(3)
+ACT_CD_BOOKMARK(4) ACT_CD_BOOKMARK(5) ACT_CD_BOOKMARK(6) ACT_CD_BOOKMARK(7)
+ACT_CD_BOOKMARK(8) ACT_CD_BOOKMARK(9)
+
+static inline void
+act_cd_up(struct fm *p)
+{
+ if (p->f & FM_SEARCH) {
+ rl_clear(&p->r);
+ fm_filter_clear(p);
+ if (p->c == SIZE_MAX) {
+ fm_cursor_set(p, 0, 0);
+ return;
+ }
+ usize o = ent_next(p, 0);
+ if (o == SIZE_MAX) return;
+ cut c = fm_ent(p, o);
+ fm_scroll_to(p, c);
+ p->c = o;
+ return;
+ }
+
+ cut b = fm_path_cd_up(p);
+ if (!b.l) return;
+ fm_scroll_to(p, b);
+ fm_cursor_sync(p);
+}
+
+static inline void
+act_stat(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ cut e = fm_ent(p, p->c);
+
+ struct stat st;
+ if (fstatat(p->dfd, e.d, &st, AT_SYMLINK_NOFOLLOW) == -1) {
+ fm_draw_err(p, S("stat"), errno);
+ return;
+ }
+
+ STR_PUSH(&p->io, VT_ED2 VT_CUP1);
+
+ STR_PUSH(&p->io, "Name: ");
+ str_push(&p->io, e.d, e.l);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Type: ");
+ cut t = fm_file_type(st.st_mode);
+ str_push(&p->io, t.d, t.l);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ if (S_ISLNK(st.st_mode)) {
+ char b[PATH_MAX];
+ ssize_t r = readlinkat(p->dfd, e.d, b, sizeof(b) - 1);
+ if (r >= 0) {
+ b[r] = 0;
+ STR_PUSH(&p->io, "Target: ");
+ str_push_s(&p->io, b);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+ }
+ }
+
+ STR_PUSH(&p->io, "Size: ");
+ str_push_u64(&p->io, (u64)st.st_size);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Mode: 0");
+ str_push_u32_b(&p->io, st.st_mode & 07777, 8, 0, 0);
+ STR_PUSH(&p->io, ", ");
+ ent_perm_decode(&p->io, st.st_mode, 0);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "UID: ");
+ str_push_u32(&p->io, (u32)st.st_uid);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "GID: ");
+ str_push_u32(&p->io, (u32)st.st_gid);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Links: ");
+ str_push_u64(&p->io, (u64)st.st_nlink);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Blocks: ");
+ str_push_u64(&p->io, (u64)st.st_blocks);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Inode: ");
+ str_push_u64(&p->io, (u64)st.st_ino);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Device: ");
+ str_push_u64(&p->io, (u64)st.st_dev);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Access: ");
+ str_push_time(&p->io, p->tz, &st.st_atim);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Modify: ");
+ str_push_time(&p->io, p->tz, &st.st_mtim);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Change: ");
+ str_push_time(&p->io, p->tz, &st.st_ctim);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, VT_CR VT_LF "Press any key...");
+
+ fm_draw_flush(p);
+ term_key_read(p->t.fd, &p->k);
+ p->f |= FM_REDRAW;
+}
+
+static inline void
+act_open(struct fm *p)
+{
+ fm_open(p);
+}
+
+static inline void
+act_view_next(struct fm *p)
+{
+ switch (p->dv) {
+ default: p->dv = 's'; break;
+ case 's': p->dv = 'p'; break;
+ case 'p': p->dv = 't'; break;
+ case 't': p->dv = 'a'; break;
+ case 'a': p->dv = 'n'; break;
+ }
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_sort_next(struct fm *p)
+{
+ switch (p->ds) {
+ default: p->ds = 'N'; break;
+ case 'N': p->ds = 's'; break;
+ case 's': p->ds = 'S'; break;
+ case 'S': p->ds = 'd'; break;
+ case 'd': p->ds = 'D'; break;
+ case 'D': p->ds = 'e'; break;
+ case 'e': p->ds = 'n'; break;
+ }
+ fm_dir_sort(p);
+}
+
+static inline void
+act_redraw(struct fm *p)
+{
+ p->f |= FM_REDRAW;
+}
+
+static inline void
+act_refresh(struct fm *p)
+{
+ fm_dir_refresh(p);
+}
+
+static inline void
+act_scroll_top(struct fm *p)
+{
+ fm_cursor_set(p, 0, 0);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_scroll_bottom(struct fm *p)
+{
+ fm_cursor_set(p, p->vl - !!p->vl, p->row -1);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_page_down(struct fm *p)
+{
+ if (!p->vl) return;
+ usize ny = p->y + p->row;
+ if (ny >= p->vl) ny = p->vl - 1;
+ fm_cursor_set(p, ny, p->row - 1);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_page_up(struct fm *p)
+{
+ if (!p->vl) return;
+ usize ny = (p->y > p->row) ? (p->y - p->row) : 0;
+ fm_cursor_set(p, ny, 0);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_scroll_down(struct fm *p)
+{
+ if (unlikely(p->y + 1 >= p->vl)) return;
+ usize l = p->c;
+ p->y++;
+ p->o += p->o < p->row - 1;
+ usize n = ent_next(p, p->c + 1);
+ if (n == SIZE_MAX) return;
+ p->c = n;
+ fm_draw_ent(p, l);
+ STR_PUSH(&p->io, VT_LF);
+ fm_draw_ent(p, p->c);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+act_scroll_up(struct fm *p)
+{
+ if (unlikely(!p->y)) return;
+ usize l = p->c;
+ p->y--;
+ usize n = ent_prev(p, p->c - 1);
+ if (n == SIZE_MAX) return;
+ p->c = n;
+ fm_draw_ent(p, l);
+
+ if (!p->o) {
+ STR_PUSH(&p->io, VT_IL0);
+ } else {
+ p->o -= !!p->o;
+ STR_PUSH(&p->io, VT_CUU1);
+ }
+
+ fm_draw_ent(p, p->c);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+act_toggle_hidden(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ cut c = fm_ent(p, p->c);
+ if (!c.l) return;
+ p->f ^= FM_HIDDEN;
+ fm_filter_clear(p);
+ fm_scroll_to(p, c);
+ fm_cursor_sync(p);
+}
+
+static inline void
+act_search_startswith(struct fm *p)
+{
+ p->sf = fm_filter_startswith;
+ fm_filter_clear(p);
+ p->f |= FM_SEARCH;
+ fm_cursor_set(p, 0, 0);
+ fm_cmd(p, &(struct fm_cmd){
+ .prompt = CUT("/"),
+ .press = fm_cmd_search_press,
+ .enter = fm_cmd_search,
+ });
+}
+
+static inline void
+act_search_substring(struct fm *p)
+{
+ p->sf = fm_filter_substr;
+ fm_filter_clear(p);
+ p->f |= FM_SEARCH;
+ fm_cursor_set(p, 0, 0);
+ fm_cmd(p, &(struct fm_cmd){
+ .prompt = CUT("/*"),
+ .press = fm_cmd_search_press,
+ .enter = fm_cmd_search,
+ });
+}
+
+static inline void
+act_shell(struct fm *p)
+{
+ cut sh = get_env("SHELL", "/bin/sh");
+ const char *const a[] = { sh.d, NULL };
+ fm_exec(p, -1, NULL, a, 0, 1);
+}
+
+static inline void
+act_alt_buffer(struct fm *p)
+{
+ STR_PUSH(&p->io, VT_ALT_SCREEN_N);
+ fm_draw_flush(p);
+ term_key_read(p->t.fd, &p->k);
+ STR_PUSH(&p->io, VT_ALT_SCREEN_Y);
+ fm_draw_flush(p);
+ p->f &= ~FM_ERROR;
+ p->f |= FM_REDRAW;
+}
+
+static inline void
+act_mark_toggle(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ if (!(p->f & FM_MARK_PWD)) fm_mark_clear(p);
+ fm_mark_init(p);
+ if (!fm_mark_toggle_idx(p, p->c)) {
+ fm_draw_err(p, S("Not enough memory to mark"), 0);
+ return;
+ }
+ fm_mark_invalidate(p);
+ fm_draw_ent(p, p->c);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+act_mark_toggle_all(struct fm *p)
+{
+ usize i = ent_next(p, 0);
+ if (i == SIZE_MAX) return;
+ int pr = ent_v_geto(p, i, MARK);
+ fm_mark_clear(p);
+ if (pr) goto e;
+ fm_mark_init(p);
+ p->vml = 0;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ p->vm[b] = p->v[b];
+ p->vml += u64_popcount(p->vm[b]);
+ }
+ fm_mark_apply_bitset(p, p->vm);
+ p->ml = 0;
+e:
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_mark_clear(struct fm *p)
+{
+ fm_mark_clear(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_mark_next(struct fm *p)
+{
+ if (!p->vml || p->c == SIZE_MAX) return;
+ usize b = fm_mark_find(p, p->c, 1);
+ if (b == SIZE_MAX) return;
+ usize r = fm_filter_pct_rank(p, b);
+ size y = fm_scroll_to_rank(p, r);
+ while (y-- > 0) act_scroll_down(p);
+}
+
+static inline void
+act_mark_prev(struct fm *p)
+{
+ if (!p->vml || p->c == SIZE_MAX) return;
+ usize b = fm_mark_find(p, p->c, -1);
+ if (b == SIZE_MAX) return;
+ usize r = fm_filter_pct_rank(p, b);
+ size y = fm_scroll_to_rank(p, r);
+ while (y++ < 0) act_scroll_up(p);
+}
+
+static inline void
+act_mark_invert(struct fm *p)
+{
+ if (!p->vl) return;
+ if (!(p->f & FM_MARK_PWD)) {
+ fm_mark_clear(p);
+ fm_mark_init(p);
+ }
+ p->vml = 0;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ p->vm[b] = p->v[b] & ~p->vm[b];
+ p->vml += u64_popcount(p->vm[b]);
+ }
+ fm_mark_apply_bitset(p, p->vm);
+ for (usize b = 0; b < nw; b++) {
+ u64 cl = p->v[b] & ~p->vm[b];
+ while (cl) {
+ usize i = (b << 6) + u64_ctz(cl);
+ cl &= cl - 1;
+ if (i >= p->dl) break;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ }
+ }
+ fm_mark_invalidate(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+// }}}
+
+// Input {{{
+
+static inline void
+input_disabled(struct fm *p)
+{
+ (void) p;
+}
+
+static inline void
+input_move_beginning(struct fm *p)
+{
+ switch (rl_home(&p->r)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ STR_PUSH(&p->io, VT_CR);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_move_end(struct fm *p)
+{
+ switch (rl_end(&p->r)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ STR_PUSH(&p->io, VT_CR);
+ vt_cuf(&p->io, p->r.vx);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_move_left(struct fm *p)
+{
+ usize n;
+ switch (rl_left(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_cub(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_move_word_left(struct fm *p)
+{
+ if (rl_word_left(&p->r) != -1)
+ p->f |= FM_REDRAW_CMD;
+}
+
+static inline void
+input_move_word_right(struct fm *p)
+{
+ if (rl_word_right(&p->r) != -1)
+ p->f |= FM_REDRAW_CMD;
+}
+
+static inline void
+input_move_right(struct fm *p)
+{
+ usize n;
+ switch (rl_right(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_cuf(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_delete_to_end(struct fm *p)
+{
+ int r = rl_delete_right(&p->r);
+ if (r == RL_NONE) return;
+ STR_PUSH(&p->io, VT_EL0);
+ p->f |= FM_REDRAW_FLUSH;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete_to_beginning(struct fm *p)
+{
+ int r = rl_delete_left(&p->r);
+ if (r == RL_NONE) return;
+ p->f |= FM_REDRAW_CMD;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete(struct fm *p)
+{
+ usize n;
+ switch (rl_delete(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_dch(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ case RL_NONE: return;
+ }
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete_word_left(struct fm *p)
+{
+ int r = rl_delete_word_prev(&p->r);
+ if (r == RL_NONE) return;
+ p->f |= FM_REDRAW_CMD;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete_word_right(struct fm *p)
+{
+ int r = rl_delete_word_right(&p->r);
+ if (r == RL_NONE) return;
+ p->f |= FM_REDRAW_CMD;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_backspace(struct fm *p)
+{
+ usize n;
+ switch (rl_backspace(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_cub(&p->io, n);
+ vt_dch(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ case RL_NONE: return;
+ }
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_cancel(struct fm *p)
+{
+ rl_clear(&p->r);
+ p->kp = 0;
+ p->kd = 0;
+ STR_PUSH(&p->io, VT_EL2);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+input_submit(struct fm *p)
+{
+ rl_join(&p->r);
+ fm_cmd_exec(p);
+ p->r.vx = 0;
+ STR_PUSH(&p->io, VT_EL2);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+input_insert(struct fm *p)
+{
+ assert(!(p->k.c & KEY_TAG));
+ usize n;
+ switch (rl_insert(&p->r, p->k.c, p->k.b, p->k.l, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_ich(&p->io, n);
+ str_push(&p->io, (char *) p->k.b, p->k.l);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ case RL_NONE: return;
+ }
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_insert_paste(struct fm *p)
+{
+ for (bool s = false;;) {
+ if (!term_key_read(p->t.fd, &p->k)) return;
+ if (p->k.c == KEY_PASTE_END) return;
+ if (p->k.b[0] == '\r' || p->k.b[0] == '\n') {
+ if (!s) p->k.c = p->k.b[0] = ' ';
+ s = true;
+ } else
+ s = false;
+ if (KEY_GET_MOD(p->k.c) || KEY_IS_SYM(p->k.c) || p->k.c < 32)
+ continue;
+ input_insert(p);
+ }
+}
+
+// }}}
+
+#include "config_cmd.h"
+#include "config_key.h"
+
+// Init {{{
+
+static inline usize
+fm_io_flush(str *s, void *ctx, usize n)
+{
+ (void) n;
+ struct fm *p = ctx;
+ write_all(p->t.fd, s->m, s->l);
+ s->l = 0;
+ return 0;
+}
+
+static inline int
+fm_init(struct fm *p)
+{
+ if (fs_watch_init(&p->p) == -1)
+ return -1;
+ p->opener = get_env("DFM_OPENER", DFM_OPENER);
+ p->dfd = AT_FDCWD;
+ p->ds = DFM_DEFAULT_SORT;
+ p->dv = DFM_DEFAULT_VIEW;
+ p->sf = fm_filter_startswith;
+ p->dec = sizeof(p->de);
+ p->tz = tz_offset();
+#if DFM_SHOW_HIDDEN
+ p->f |= FM_HIDDEN;
+#endif
+ if (!geteuid()) p->f |= FM_ROOT;
+ fm_mark_clear_all(p);
+ STR_INIT(&p->pwd, DFM_PATH_MAX, 0, 0);
+ STR_INIT(&p->ppwd, DFM_PATH_MAX, 0, 0);
+ STR_INIT(&p->mpwd, DFM_PATH_MAX, 0, 0);
+ STR_INIT(&p->io, DFM_IO_MAX, fm_io_flush, p);
+ return 0;
+}
+
+static inline void
+fm_free(struct fm *p)
+{
+ fs_watch_free(&p->p);
+ close(p->dfd);
+ int fd = term_dead(&p->t) ? STDOUT_FILENO : STDERR_FILENO;
+ if (!p->pwd.l) return;
+ write_all(fd, p->pwd.m, p->pwd.l);
+ write_all(fd, S("\n"));
+}
+
+// }}}
+
+// Main {{{
+
+static inline void
+fm_watch_handle(struct fm *p)
+{
+ for (cut n = {0};;) {
+ switch (fs_watch_pump(&p->p, &n.d, &n.l)) {
+ case '!': fm_dir_refresh(p); return;
+ case '+': fm_dir_add(p, n); break;
+ case '-': fm_dir_del(p, n); break;
+ case '~': fm_dir_del(p, n); fm_dir_add(p, n); break;
+ default: return;
+ }
+ }
+}
+
+static inline void
+fm_update(struct fm *p)
+{
+ term_reap();
+ if (unlikely(term_resize(&p->t)))
+ if (fm_term_resize(p) < 0)
+ fm_draw_err(p, S("resize failed"), errno);
+ fm_watch_handle(p);
+ if (!(p->f & FM_DIRTY)) return;
+ p->f &= ~FM_DIRTY;
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+ fm_dir_sort(p);
+ fm_cursor_sync(p);
+ if (p->f & FM_DIRTY_WITHIN && p->st) {
+ u64 m = ent_load_off(p, p->st);
+ cut st = { p->de + p->st, ent_get(m, LEN) };
+ fm_scroll_to(p, st);
+ p->st = 0;
+ p->f &= ~FM_DIRTY_WITHIN;
+ }
+}
+
+static inline void
+fm_draw(struct fm *p)
+{
+ if ((p->f & FM_REDRAW) == FM_REDRAW) {
+ STR_PUSH(&p->io, VT_ED2);
+ fm_dir_ht_clear_cache(p);
+ }
+ if (p->f & FM_REDRAW_DIR)
+ fm_draw_dir(p);
+ if (p->f & FM_REDRAW_NAV)
+ fm_draw_nav(p);
+ if (p->f & FM_REDRAW_CMD)
+ fm_draw_cmd(p);
+ if (p->f & FM_REDRAW) {
+ if (p->kp || p->kd) {
+ vt_cup(&p->io, p->r.vx, p->row + DFM_MARGIN);
+ STR_PUSH(&p->io, VT_DECTCEM_Y);
+ } else {
+ vt_cup(&p->io, 0, p->o + 1);
+ STR_PUSH(&p->io, VT_DECTCEM_N);
+ }
+ fm_draw_flush(p);
+ }
+ p->f &= ~FM_REDRAW;
+}
+
+static inline void
+fm_input(struct fm *p)
+{
+ if (!term_key_read(p->t.fd, &p->k))
+ return;
+ if (p->r.pr.l) fm_key_input(p->k.c)(p);
+ else fm_key(p->k.c)(p);
+}
+
+static inline int
+fm_run(struct fm *p)
+{
+ if (fm_term_init(p) < 0) return -1;
+ rl_init(&p->r, p->col, CUT_NULL);
+ for (; likely(!term_dead(&p->t)); ) {
+ fm_update(p);
+ fm_draw(p);
+ fm_input(p);
+ }
+ fm_term_free(p);
+ if (!(p->f & FM_PRINT_PWD)) p->pwd.l = 0;
+ return 0;
+}
+
+int
+main(int argc, char *argv[])
+{
+ static struct fm p;
+ str *s = &p.pwd;
+
+ if (fm_init(&p) < 0) {
+ STR_PUSH(s, "error?: ");
+ str_push_s(s, strerror(errno));
+ goto e;
+ }
+
+ const char *pwd = ".";
+ struct argv A = arg_init(argc, argv);
+
+ for (struct arg a; (a = arg_next(&A)).sign != -1;) {
+ const char *n;
+ switch (a.name) {
+ case 'H':
+ p.f ^= (-(a.sign == '+') ^ p.f) & FM_HIDDEN;
+ continue;
+ case 'p':
+ p.f |= FM_PICKER;
+ continue;
+ case 'o':
+ n = arg_next_positional(&A);
+ if (!n) goto arg_no_val;
+ p.opener.d = n;
+ continue;
+ case 's':
+ n = arg_next_positional(&A);
+ if (!n) goto arg_no_val;
+ p.ds = fm_sort_fn(*n) ? *n : 'n';
+ continue;
+ case 'v':
+ n = arg_next_positional(&A);
+ if (!n) goto arg_no_val;
+ p.dv = *n;
+ continue;
+ case '-':
+ if (!strcmp(a.pos, "--help")) {
+ STR_PUSH(s, DFM_HELP);
+ term_set_dead(&p.t, 1);
+ goto e;
+ } else if (!strcmp(a.pos, "--version")) {
+ STR_PUSH(s, CFG_NAME " " CFG_VERSION " "
+ CC_COMMIT " (" CC_BRANCH ") " CC_DATE);
+ goto e;
+ }
+ STR_PUSH(s, "unknown arg ");
+ str_push_s(s, a.pos);
+ goto e;
+ default:
+ if (unlikely(a.name)) {
+ STR_PUSH(s, "unknown arg ");
+ str_push_c(s, a.sign);
+ str_push_c(s, a.name);
+ goto e;
+ }
+ pwd = a.pos;
+ continue;
+ }
+arg_no_val:
+ STR_PUSH(s, "arg ");
+ str_push_c(s, a.sign);
+ str_push_c(s, a.name);
+ STR_PUSH(s, " missing value");
+ goto e;
+ }
+
+ if (!fm_path_chdir(&p, pwd)) {
+ STR_PUSH(s, "cd: '");
+ str_push_s(s, pwd);
+ STR_PUSH(s, "': ");
+ str_push_s(s, strerror(errno));
+ goto e;
+ }
+
+ if (fm_run(&p) < 0) {
+ STR_PUSH(s, "term: '");
+ str_push_s(s, strerror(errno));
+ goto e;
+ }
+
+ fm_free(&p);
+ return EXIT_SUCCESS;
+e:
+ fm_free(&p);
+ return EXIT_FAILURE;
+}
+
+// }}}
+