Directory navigator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sdn.cpp 45KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612
  1. //
  2. // sdn: simple directory navigator
  3. //
  4. // Copyright (c) 2017 - 2018, Přemysl Janouch <p@janouch.name>
  5. //
  6. // Permission to use, copy, modify, and/or distribute this software for any
  7. // purpose with or without fee is hereby granted.
  8. //
  9. // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  10. // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  11. // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
  12. // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  13. // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
  14. // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  15. // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. //
  17. // May be required for ncursesw and we generally want it all anyway
  18. #define _XOPEN_SOURCE_EXTENDED
  19. #include <string>
  20. #include <vector>
  21. #include <locale>
  22. #include <iostream>
  23. #include <algorithm>
  24. #include <cwchar>
  25. #include <climits>
  26. #include <cstdlib>
  27. #include <cstring>
  28. #include <fstream>
  29. #include <map>
  30. #include <tuple>
  31. #include <memory>
  32. #include <unistd.h>
  33. #include <dirent.h>
  34. #include <sys/stat.h>
  35. #include <sys/types.h>
  36. #include <sys/acl.h>
  37. #include <fcntl.h>
  38. #include <pwd.h>
  39. #include <grp.h>
  40. #include <libgen.h>
  41. #include <sys/inotify.h>
  42. #include <sys/xattr.h>
  43. #include <sys/types.h>
  44. #include <sys/wait.h>
  45. #include <acl/libacl.h>
  46. #include <ncurses.h>
  47. // Unicode is complex enough already and we might make assumptions
  48. #ifndef __STDC_ISO_10646__
  49. #error Unicode required for wchar_t
  50. #endif
  51. // Trailing return types make C++ syntax suck considerably less
  52. #define fun static auto
  53. #ifndef A_ITALIC
  54. #define A_ITALIC 0
  55. #endif
  56. using namespace std;
  57. // For some reason handling of encoding in C and C++ is extremely annoying
  58. // and C++17 ironically obsoletes C++11 additions that made it less painful
  59. fun to_wide (const string &multi) -> wstring {
  60. wstring wide; wchar_t w; mbstate_t mb {};
  61. size_t n = 0, len = multi.length () + 1;
  62. while (auto res = mbrtowc (&w, multi.c_str () + n, len - n, &mb)) {
  63. if (res == size_t (-1) || res == size_t (-2))
  64. return L"/invalid encoding/";
  65. n += res;
  66. wide += w;
  67. }
  68. return wide;
  69. }
  70. fun to_mb (const wstring &wide) -> string {
  71. string mb; char buf[MB_LEN_MAX + 1]; mbstate_t mbs {};
  72. for (size_t n = 0; n <= wide.length (); n++) {
  73. auto res = wcrtomb (buf, wide.c_str ()[n], &mbs);
  74. if (res == size_t (-1))
  75. throw invalid_argument ("invalid encoding");
  76. mb.append (buf, res);
  77. }
  78. // There's one extra NUL character added by wcrtomb()
  79. mb.erase (mb.length () - 1);
  80. return mb;
  81. }
  82. fun prefix_length (const wstring &in, const wstring &of) -> int {
  83. int score = 0;
  84. for (size_t i = 0; i < of.size () && in.size () >= i && in[i] == of[i]; i++)
  85. score++;
  86. return score;
  87. }
  88. // TODO: this omits empty elements, check usages
  89. fun split (const string &s, const string &sep, vector<string> &out) {
  90. size_t mark = 0, p = s.find (sep);
  91. for (; p != string::npos; p = s.find (sep, (mark = p + sep.length ())))
  92. if (mark < p)
  93. out.push_back (s.substr (mark, p - mark));
  94. if (mark < s.length ())
  95. out.push_back (s.substr (mark));
  96. }
  97. fun split (const string &s, const string &sep) -> vector<string> {
  98. vector<string> result; split (s, sep, result); return result;
  99. }
  100. fun needs_shell_quoting (const string &v) -> bool {
  101. // IEEE Std 1003.1 sh + the exclamation mark because of csh/bash
  102. // history expansion, implicitly also the NUL character
  103. for (auto c : v)
  104. if (strchr ("|&;<>()$`\\\"' \t\n" "*?[#˜=%" "!", c))
  105. return true;
  106. return v.empty ();
  107. }
  108. fun shell_escape (const string &v) -> string {
  109. if (!needs_shell_quoting (v))
  110. return v;
  111. string result;
  112. for (auto c : v)
  113. if (c == '\'')
  114. result += "'\\''";
  115. else
  116. result += c;
  117. return "'" + result + "'";
  118. }
  119. fun parse_line (istream &is, vector<string> &out) -> bool {
  120. enum {STA, DEF, COM, ESC, WOR, QUO, STATES};
  121. enum {TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6};
  122. enum {TWOR = TAKE | WOR};
  123. // We never transition back to the start state, so it can stay as a noop
  124. static char table[STATES][7] = {
  125. // state EOF SP, TAB ' # \ LF default
  126. /* STA */ {ERROR, DEF, QUO, COM, ESC, STOP, TWOR},
  127. /* DEF */ {STOP, 0, QUO, COM, ESC, STOP, TWOR},
  128. /* COM */ {STOP, 0, 0, 0, 0, STOP, 0},
  129. /* ESC */ {ERROR, TWOR, TWOR, TWOR, TWOR, TWOR, TWOR},
  130. /* WOR */ {STOP | PUSH, DEF | PUSH, QUO, TAKE, ESC, STOP | PUSH, TAKE},
  131. /* QUO */ {ERROR, TAKE, WOR, TAKE, TAKE, TAKE, TAKE},
  132. };
  133. out.clear (); string token; int state = STA;
  134. constexpr auto eof = istream::traits_type::eof ();
  135. while (1) {
  136. int ch = is.get (), edge = 0;
  137. switch (ch) {
  138. case eof: edge = table[state][0]; break;
  139. case '\t':
  140. case ' ': edge = table[state][1]; break;
  141. case '\'': edge = table[state][2]; break;
  142. case '#': edge = table[state][3]; break;
  143. case '\\': edge = table[state][4]; break;
  144. case '\n': edge = table[state][5]; break;
  145. default: edge = table[state][6]; break;
  146. }
  147. if (edge & TAKE)
  148. token += ch;
  149. if (edge & PUSH) {
  150. out.push_back (token);
  151. token.clear ();
  152. }
  153. if (edge & STOP)
  154. return true;
  155. if (edge & ERROR)
  156. return false;
  157. if (edge &= 7)
  158. state = edge;
  159. }
  160. }
  161. fun write_line (ostream &os, const vector<string> &in) {
  162. if (!in.empty ())
  163. os << shell_escape (in.at (0));
  164. for (size_t i = 1; i < in.size (); i++)
  165. os << " " << shell_escape (in.at (i));
  166. os << endl;
  167. }
  168. fun decode_type (mode_t m) -> wchar_t {
  169. if (S_ISDIR (m)) return L'd'; if (S_ISBLK (m)) return L'b';
  170. if (S_ISCHR (m)) return L'c'; if (S_ISLNK (m)) return L'l';
  171. if (S_ISFIFO (m)) return L'p'; if (S_ISSOCK (m)) return L's';
  172. if (S_ISREG (m)) return L'-';
  173. return L'?';
  174. }
  175. /// Return the modes of a file in the usual stat/ls format
  176. fun decode_mode (mode_t m) -> wstring {
  177. return {decode_type (m),
  178. L"r-"[!(m & S_IRUSR)],
  179. L"w-"[!(m & S_IWUSR)],
  180. ((m & S_ISUID) ? L"sS" : L"x-")[!(m & S_IXUSR)],
  181. L"r-"[!(m & S_IRGRP)],
  182. L"w-"[!(m & S_IWGRP)],
  183. ((m & S_ISGID) ? L"sS" : L"x-")[!(m & S_IXGRP)],
  184. L"r-"[!(m & S_IROTH)],
  185. L"w-"[!(m & S_IWOTH)],
  186. ((m & S_ISVTX) ? L"tT" : L"x-")[!(m & S_IXOTH)]};
  187. }
  188. template<class T> fun shift (vector<T> &v) -> T {
  189. auto front = v.front (); v.erase (begin (v)); return front;
  190. }
  191. fun capitalize (const string &s) -> string {
  192. string result;
  193. for (auto c : s)
  194. result += result.empty () ? toupper (c) : tolower (c);
  195. return result;
  196. }
  197. /// Underlining for teletypes, also imitated in more(1) and less(1)
  198. fun underline (const string& s) -> string {
  199. string result;
  200. for (auto c : s)
  201. result.append ({c, 8, '_'});
  202. return result;
  203. }
  204. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  205. fun xdg_config_home () -> string {
  206. const char *user_dir = getenv ("XDG_CONFIG_HOME");
  207. if (user_dir && user_dir[0] == '/')
  208. return user_dir;
  209. const char *home_dir = getenv ("HOME");
  210. return string (home_dir ? home_dir : "") + "/.config";
  211. }
  212. // In C++17 we will get <optional> but until then there's unique_ptr
  213. fun xdg_config_find (const string &suffix) -> unique_ptr<ifstream> {
  214. vector<string> dirs {xdg_config_home ()};
  215. const char *system_dirs = getenv ("XDG_CONFIG_DIRS");
  216. split (system_dirs ? system_dirs : "/etc/xdg", ":", dirs);
  217. for (const auto &dir : dirs) {
  218. if (dir[0] != '/')
  219. continue;
  220. auto ifs = make_unique<ifstream>
  221. (dir + "/" PROJECT_NAME "/" + suffix);
  222. if (*ifs)
  223. return ifs;
  224. }
  225. return nullptr;
  226. }
  227. fun xdg_config_write (const string &suffix) -> unique_ptr<fstream> {
  228. auto dir = xdg_config_home ();
  229. if (dir[0] == '/') {
  230. auto path = dir + "/" PROJECT_NAME "/" + suffix;
  231. if (!fork ())
  232. _exit (-execlp ("mkdir", "mkdir", "-p",
  233. dirname (strdup (path.c_str ())), NULL));
  234. auto fs = make_unique<fstream>
  235. (path, fstream::in | fstream::out | fstream::trunc);
  236. if (*fs)
  237. return fs;
  238. }
  239. return nullptr;
  240. }
  241. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  242. using ncstring = basic_string<cchar_t>;
  243. fun cchar (chtype attrs, wchar_t c) -> cchar_t {
  244. cchar_t ch {}; wchar_t ws[] = {c, 0};
  245. setcchar (&ch, ws, attrs, PAIR_NUMBER (attrs), nullptr);
  246. return ch;
  247. }
  248. fun decolor (cchar_t &ch) {
  249. wchar_t c[CCHARW_MAX]; attr_t attrs; short pair;
  250. getcchar (&ch, c, &attrs, &pair, nullptr);
  251. setcchar (&ch, c, attrs &~ A_REVERSE, 0, nullptr);
  252. }
  253. fun invert (cchar_t &ch) {
  254. wchar_t c[CCHARW_MAX]; attr_t attrs; short pair;
  255. getcchar (&ch, c, &attrs, &pair, nullptr);
  256. setcchar (&ch, c, attrs ^ A_REVERSE, 0, nullptr);
  257. }
  258. fun apply_attrs (const wstring &w, attr_t attrs) -> ncstring {
  259. ncstring res;
  260. for (auto c : w)
  261. res += cchar (attrs, c);
  262. return res;
  263. }
  264. fun sanitize_char (chtype attrs, wchar_t c) -> ncstring {
  265. if (c < 32)
  266. return {cchar (attrs | A_REVERSE, L'^'),
  267. cchar (attrs | A_REVERSE, c + 64)};
  268. if (!iswprint (c))
  269. return {cchar (attrs | A_REVERSE, L'?')};
  270. return {cchar (attrs, c)};
  271. }
  272. fun sanitize (const ncstring &nc) -> ncstring {
  273. ncstring out;
  274. for (cchar_t c : nc)
  275. for (size_t i = 0; i < CCHARW_MAX && c.chars[i]; i++)
  276. out += sanitize_char (c.attr, c.chars[i]);
  277. return out;
  278. }
  279. fun print (const ncstring &nc, int limit) -> int {
  280. int total_width = 0;
  281. for (cchar_t c : sanitize (nc)) {
  282. int width = wcwidth (c.chars[0]);
  283. if (total_width + width > limit)
  284. break;
  285. add_wch (&c);
  286. total_width += width;
  287. }
  288. return total_width;
  289. }
  290. fun compute_width (const wstring &w) -> int {
  291. int total = 0;
  292. for (const auto &c : w)
  293. total += wcwidth (c);
  294. return total;
  295. }
  296. fun compute_width (const ncstring &nc) -> int {
  297. int total = 0;
  298. for (const auto &c : nc)
  299. total += wcwidth (c.chars[0]);
  300. return total;
  301. }
  302. // TODO: maybe we need formatting for the padding passed in?
  303. fun align (const ncstring &nc, int target) -> ncstring {
  304. auto current = compute_width (nc);
  305. auto missing = abs (target) - current;
  306. if (missing <= 0)
  307. return nc;
  308. return target < 0
  309. ? nc + apply_attrs (wstring (missing, L' '), 0)
  310. : apply_attrs (wstring (missing, L' '), 0) + nc;
  311. }
  312. fun allocate_pair (short fg, short bg) -> short {
  313. static short counter = 1; init_pair (counter, fg, bg); return counter++;
  314. }
  315. fun decode_attrs (const vector<string> &attrs) -> chtype {
  316. chtype result = 0; int fg = -1, bg = -1, colors = 0;
  317. for (const auto &s : attrs) {
  318. char *end; auto color = strtol (s.c_str (), &end, 10);
  319. if (!*end && color >= -1 && color < COLORS) {
  320. if (++colors == 1) fg = color;
  321. else if (colors == 2) bg = color;
  322. }
  323. else if (s == "bold") result |= A_BOLD;
  324. else if (s == "dim") result |= A_DIM;
  325. else if (s == "ul") result |= A_UNDERLINE;
  326. else if (s == "blink") result |= A_BLINK;
  327. else if (s == "reverse") result |= A_REVERSE;
  328. else if (s == "italic") result |= A_ITALIC;
  329. }
  330. if (fg != -1 || bg != -1)
  331. result |= COLOR_PAIR (allocate_pair (fg, bg));
  332. return result;
  333. }
  334. // --- Application -------------------------------------------------------------
  335. enum { ALT = 1 << 24, SYM = 1 << 25 }; // Outside the range of Unicode
  336. #define KEY(name) (SYM | KEY_ ## name)
  337. #define CTRL 31 &
  338. #define ACTIONS(XX) XX(NONE) XX(HELP) XX(QUIT) XX(QUIT_NO_CHDIR) \
  339. XX(CHOOSE) XX(CHOOSE_FULL) XX(VIEW) XX(EDIT) XX(SORT_LEFT) XX(SORT_RIGHT) \
  340. XX(UP) XX(DOWN) XX(TOP) XX(BOTTOM) XX(HIGH) XX(MIDDLE) XX(LOW) \
  341. XX(PAGE_PREVIOUS) XX(PAGE_NEXT) \
  342. XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CHDIR) XX(GO_START) XX(GO_HOME) \
  343. XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) \
  344. XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \
  345. XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE)
  346. #define XX(name) ACTION_ ## name,
  347. enum action { ACTIONS(XX) ACTION_COUNT };
  348. #undef XX
  349. #define XX(name) #name,
  350. static const char *g_action_names[] = {ACTIONS(XX)};
  351. #undef XX
  352. static map<wint_t, action> g_normal_actions {
  353. {ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
  354. {'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
  355. {KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT}, {'h', ACTION_HELP},
  356. {'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
  357. // M-o ought to be the same shortcut the navigator is launched with
  358. {ALT | 'o', ACTION_QUIT},
  359. {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
  360. {'k', ACTION_UP}, {CTRL 'p', ACTION_UP}, {KEY (UP), ACTION_UP},
  361. {'j', ACTION_DOWN}, {CTRL 'n', ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
  362. {'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
  363. {'G', ACTION_BOTTOM}, {ALT | '>', ACTION_BOTTOM}, {KEY(END), ACTION_BOTTOM},
  364. {'H', ACTION_HIGH}, {'M', ACTION_MIDDLE}, {'L', ACTION_LOW},
  365. {KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
  366. {CTRL 'y', ACTION_SCROLL_UP}, {CTRL 'e', ACTION_SCROLL_DOWN},
  367. {'c', ACTION_CHDIR}, {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
  368. {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH},
  369. {ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
  370. {'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL},
  371. {'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
  372. {CTRL 'L', ACTION_REDRAW}, {'r', ACTION_RELOAD},
  373. };
  374. static map<wint_t, action> g_input_actions {
  375. {27, ACTION_INPUT_ABORT}, {CTRL 'g', ACTION_INPUT_ABORT},
  376. {L'\r', ACTION_INPUT_CONFIRM}, {KEY (ENTER), ACTION_INPUT_CONFIRM},
  377. {KEY (BACKSPACE), ACTION_INPUT_B_DELETE},
  378. };
  379. static const map<string, map<wint_t, action>*> g_binding_contexts {
  380. {"normal", &g_normal_actions}, {"input", &g_input_actions},
  381. };
  382. #define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \
  383. XX(DIRECTORY, "di") XX(SYMLINK, "ln") XX(MULTIHARDLINK, "mh") \
  384. XX(FIFO, "pi") XX(SOCKET, "so") XX(DOOR, "do") XX(BLOCK, "bd") \
  385. XX(CHARACTER, "cd") XX(ORPHAN, "or") XX(MISSING, "mi") XX(SETUID, "su") \
  386. XX(SETGID, "sg") XX(CAPABILITY, "ca") XX(STICKY_OTHER_WRITABLE, "tw") \
  387. XX(OTHER_WRITABLE, "ow") XX(STICKY, "st") XX(EXECUTABLE, "ex")
  388. #define XX(id, name) LS_ ## id,
  389. enum { LS(XX) LS_COUNT };
  390. #undef XX
  391. #define XX(id, name) name,
  392. static const char *g_ls_colors[] = {LS(XX)};
  393. #undef XX
  394. struct stringcaseless {
  395. bool operator () (const string &a, const string &b) const {
  396. const auto &c = locale::classic();
  397. return lexicographical_compare (begin (a), end (a), begin (b), end (b),
  398. [&](char m, char n) { return tolower (m, c) < tolower (n, c); });
  399. }
  400. };
  401. struct entry {
  402. string filename, target_path;
  403. struct stat info = {}, target_info = {};
  404. enum { MODES, USER, GROUP, SIZE, MTIME, FILENAME, COLUMNS };
  405. ncstring cols[COLUMNS];
  406. };
  407. struct level {
  408. int offset, cursor; ///< Scroll offset and cursor position
  409. string path, filename; ///< Level path and filename at cursor
  410. };
  411. static struct {
  412. string cwd; ///< Current working directory
  413. string start_dir; ///< Starting directory
  414. vector<entry> entries; ///< Current directory entries
  415. vector<level> levels; ///< Upper directory levels
  416. int offset, cursor; ///< Scroll offset and cursor position
  417. bool full_view; ///< Show extended information
  418. bool gravity; ///< Entries are shoved to the bottom
  419. bool reverse_sort; ///< Reverse sort
  420. bool show_hidden; ///< Show hidden files
  421. int max_widths[entry::COLUMNS]; ///< Column widths
  422. int sort_column = entry::FILENAME; ///< Sorting column
  423. int sort_flash_ttl; ///< Sorting column flash TTL
  424. wstring message; ///< Message for the user
  425. int message_ttl; ///< Time to live for the message
  426. string chosen; ///< Chosen item for the command line
  427. bool no_chdir; ///< Do not tell the shell to chdir
  428. bool quitting; ///< Whether we should quit already
  429. int inotify_fd, inotify_wd = -1; ///< File watch
  430. bool out_of_date; ///< Entries may be out of date
  431. const wchar_t *editor; ///< Prompt string for editing
  432. wstring editor_line; ///< Current user input
  433. void (*editor_on_change) (); ///< Callback on editor change
  434. void (*editor_on_confirm) (); ///< Callback on editor confirmation
  435. enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_COUNT };
  436. chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0};
  437. const char *attr_names[AT_COUNT] = {"cursor", "bar", "cwd", "input"};
  438. map<int, chtype> ls_colors; ///< LS_COLORS decoded
  439. map<string, chtype> ls_exts; ///< LS_COLORS file extensions
  440. bool ls_symlink_as_target; ///< ln=target in dircolors
  441. map<string, wint_t, stringcaseless> name_to_key;
  442. map<wint_t, string> key_to_name;
  443. string action_names[ACTION_COUNT]; ///< Stylized action names
  444. // Refreshed by reload():
  445. map<uid_t, string> unames; ///< User names by UID
  446. map<gid_t, string> gnames; ///< Group names by GID
  447. struct tm now; ///< Current local time for display
  448. } g;
  449. // The coloring logic has been more or less exactly copied from GNU ls,
  450. // simplified and rewritten to reflect local implementation specifics
  451. fun ls_is_colored (int type) -> bool {
  452. auto i = g.ls_colors.find (type);
  453. return i != g.ls_colors.end () && i->second != 0;
  454. }
  455. fun ls_format (const entry &e, bool for_target) -> chtype {
  456. int type = LS_ORPHAN;
  457. auto set = [&](int t) { if (ls_is_colored (t)) type = t; };
  458. const auto &name = for_target
  459. ? e.target_path : e.filename;
  460. const auto &info =
  461. (for_target || (g.ls_symlink_as_target && e.target_info.st_mode))
  462. ? e.target_info : e.info;
  463. if (for_target && info.st_mode == 0) {
  464. // This differs from GNU ls: we use ORPHAN when MISSING is not set,
  465. // but GNU ls colors by dirent::d_type
  466. set (LS_MISSING);
  467. } else if (S_ISREG (info.st_mode)) {
  468. type = LS_FILE;
  469. if (info.st_nlink > 1)
  470. set (LS_MULTIHARDLINK);
  471. if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
  472. set (LS_EXECUTABLE);
  473. if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
  474. set (LS_CAPABILITY);
  475. if ((info.st_mode & S_ISGID))
  476. set (LS_SETGID);
  477. if ((info.st_mode & S_ISUID))
  478. set (LS_SETUID);
  479. } else if (S_ISDIR (info.st_mode)) {
  480. type = LS_DIRECTORY;
  481. if ((info.st_mode & S_ISVTX))
  482. set (LS_STICKY);
  483. if ((info.st_mode & S_IWOTH))
  484. set (LS_OTHER_WRITABLE);
  485. if ((info.st_mode & S_ISVTX) && (info.st_mode & S_IWOTH))
  486. set (LS_STICKY_OTHER_WRITABLE);
  487. } else if (S_ISLNK (info.st_mode)) {
  488. type = LS_SYMLINK;
  489. if (!e.target_info.st_mode
  490. && (ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
  491. type = LS_ORPHAN;
  492. } else if (S_ISFIFO (info.st_mode)) {
  493. type = LS_FIFO;
  494. } else if (S_ISSOCK (info.st_mode)) {
  495. type = LS_SOCKET;
  496. } else if (S_ISBLK (info.st_mode)) {
  497. type = LS_BLOCK;
  498. } else if (S_ISCHR (info.st_mode)) {
  499. type = LS_CHARACTER;
  500. }
  501. chtype format = 0;
  502. const auto x = g.ls_colors.find (type);
  503. if (x != g.ls_colors.end ())
  504. format = x->second;
  505. auto dot = name.find_last_of ('.');
  506. if (dot != string::npos && type == LS_FILE) {
  507. const auto x = g.ls_exts.find (name.substr (++dot));
  508. if (x != g.ls_exts.end ())
  509. format = x->second;
  510. }
  511. return format;
  512. }
  513. fun make_entry (const struct dirent *f) -> entry {
  514. entry e;
  515. e.filename = f->d_name;
  516. e.info.st_mode = DTTOIF (f->d_type);
  517. auto &info = e.info;
  518. // TODO: benchmark just readdir() vs. lstat(), also on dead mounts;
  519. // it might make sense to stat asynchronously in threads
  520. // http://lkml.iu.edu/hypermail//linux/kernel/0804.3/1616.html
  521. if (lstat (f->d_name, &info)) {
  522. e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode),
  523. L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0);
  524. e.cols[entry::USER] = e.cols[entry::GROUP] =
  525. e.cols[entry::SIZE] = e.cols[entry::MTIME] = apply_attrs (L"?", 0);
  526. e.cols[entry::FILENAME] =
  527. apply_attrs (to_wide (e.filename), ls_format (e, false));
  528. return e;
  529. }
  530. if (S_ISLNK (info.st_mode)) {
  531. char buf[PATH_MAX] = {};
  532. auto len = readlink (f->d_name, buf, sizeof buf);
  533. if (len < 0 || size_t (len) >= sizeof buf) {
  534. e.target_path = "?";
  535. } else {
  536. e.target_path = buf;
  537. // If a symlink links to another symlink, we follow all the way
  538. (void) stat (buf, &e.target_info);
  539. }
  540. }
  541. auto mode = decode_mode (info.st_mode);
  542. // This is a Linux-only extension
  543. if (acl_extended_file_nofollow (f->d_name) > 0)
  544. mode += L"+";
  545. e.cols[entry::MODES] = apply_attrs (mode, 0);
  546. auto usr = g.unames.find (info.st_uid);
  547. e.cols[entry::USER] = (usr != g.unames.end ())
  548. ? apply_attrs (to_wide (usr->second), 0)
  549. : apply_attrs (to_wstring (info.st_uid), 0);
  550. auto grp = g.gnames.find (info.st_gid);
  551. e.cols[entry::GROUP] = (grp != g.gnames.end ())
  552. ? apply_attrs (to_wide (grp->second), 0)
  553. : apply_attrs (to_wstring (info.st_gid), 0);
  554. auto size = to_wstring (info.st_size);
  555. if (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T";
  556. else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G";
  557. else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M";
  558. else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
  559. e.cols[entry::SIZE] = apply_attrs (size, 0);
  560. char buf[32] = "";
  561. auto tm = localtime (&info.st_mtime);
  562. strftime (buf, sizeof buf,
  563. (tm->tm_year == g.now.tm_year) ? "%b %e %H:%M" : "%b %e %Y", tm);
  564. e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0);
  565. auto &fn = e.cols[entry::FILENAME] =
  566. apply_attrs (to_wide (e.filename), ls_format (e, false));
  567. if (!e.target_path.empty ()) {
  568. fn.append (apply_attrs (to_wide (" -> "), 0));
  569. fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
  570. }
  571. return e;
  572. }
  573. fun inline visible_lines () -> int { return max (0, LINES - 2); }
  574. fun update () {
  575. int start_column = g.full_view ? 0 : entry::FILENAME;
  576. static int alignment[entry::COLUMNS] = { -1, -1, -1, 1, 1, -1 };
  577. erase ();
  578. int available = visible_lines ();
  579. int all = g.entries.size ();
  580. int used = min (available, all - g.offset);
  581. for (int i = 0; i < used; i++) {
  582. auto index = g.offset + i;
  583. bool selected = index == g.cursor;
  584. attrset (selected ? g.attrs[g.AT_CURSOR] : 0);
  585. move (g.gravity ? (available - used + i) : i, 0);
  586. auto used = 0;
  587. for (int col = start_column; col < entry::COLUMNS; col++) {
  588. const auto &field = g.entries[index].cols[col];
  589. auto aligned = align (field, alignment[col] * g.max_widths[col]);
  590. if (g.sort_flash_ttl && col == g.sort_column)
  591. for_each (begin (aligned), end (aligned), invert);
  592. if (selected)
  593. for_each (begin (aligned), end (aligned), decolor);
  594. used += print (aligned + apply_attrs (L" ", 0), COLS - used);
  595. }
  596. hline (' ', COLS - used);
  597. }
  598. auto bar = apply_attrs (to_wide (g.cwd), g.attrs[g.AT_CWD]);
  599. if (!g.show_hidden)
  600. bar += apply_attrs (L" (hidden)", 0);
  601. if (g.out_of_date)
  602. bar += apply_attrs (L" [+]", 0);
  603. move (LINES - 2, 0);
  604. attrset (g.attrs[g.AT_BAR]);
  605. int unused = COLS - print (bar, COLS);
  606. hline (' ', unused);
  607. auto pos = to_wstring (int (double (g.offset) / all * 100)) + L"%";
  608. if (used == all)
  609. pos = L"All";
  610. else if (g.offset == 0)
  611. pos = L"Top";
  612. else if (g.offset + used == all)
  613. pos = L"Bot";
  614. if (int (pos.size ()) < unused)
  615. mvaddwstr (LINES - 2, COLS - pos.size (), pos.c_str ());
  616. attrset (g.attrs[g.AT_INPUT]);
  617. curs_set (0);
  618. if (g.editor) {
  619. move (LINES - 1, 0);
  620. auto p = apply_attrs (wstring (g.editor) + L": ", 0);
  621. move (LINES - 1, print (p + apply_attrs (g.editor_line, 0), COLS - 1));
  622. curs_set (1);
  623. } else if (!g.message.empty ()) {
  624. move (LINES - 1, 0);
  625. print (apply_attrs (g.message, 0), COLS);
  626. }
  627. refresh ();
  628. }
  629. fun operator< (const entry &e1, const entry &e2) -> bool {
  630. auto t1 = make_tuple (e1.filename != "..",
  631. !S_ISDIR (e1.info.st_mode) && !S_ISDIR (e1.target_info.st_mode));
  632. auto t2 = make_tuple (e2.filename != "..",
  633. !S_ISDIR (e2.info.st_mode) && !S_ISDIR (e2.target_info.st_mode));
  634. if (t1 != t2)
  635. return t1 < t2;
  636. const auto &a = g.reverse_sort ? e2 : e1;
  637. const auto &b = g.reverse_sort ? e1 : e2;
  638. switch (g.sort_column) {
  639. case entry::MODES:
  640. if (a.info.st_mode != b.info.st_mode)
  641. return a.info.st_mode < b.info.st_mode;
  642. break;
  643. case entry::USER:
  644. if (a.info.st_uid != b.info.st_uid)
  645. return a.info.st_uid < b.info.st_uid;
  646. break;
  647. case entry::GROUP:
  648. if (a.info.st_gid != b.info.st_gid)
  649. return a.info.st_gid < b.info.st_gid;
  650. break;
  651. case entry::SIZE:
  652. if (a.info.st_size != b.info.st_size)
  653. return a.info.st_size < b.info.st_size;
  654. break;
  655. case entry::MTIME:
  656. if (a.info.st_mtime != b.info.st_mtime)
  657. return a.info.st_mtime < b.info.st_mtime;
  658. break;
  659. }
  660. return a.filename < b.filename;
  661. }
  662. fun reload (const string &old_cwd) {
  663. g.unames.clear();
  664. while (auto *ent = getpwent ())
  665. g.unames.emplace (ent->pw_uid, ent->pw_name);
  666. endpwent();
  667. g.gnames.clear();
  668. while (auto *ent = getgrent ())
  669. g.gnames.emplace (ent->gr_gid, ent->gr_name);
  670. endgrent();
  671. string anchor;
  672. if (!g.entries.empty ())
  673. anchor = g.entries[g.cursor].filename;
  674. auto now = time (NULL); g.now = *localtime (&now);
  675. auto dir = opendir (".");
  676. g.entries.clear ();
  677. while (auto f = readdir (dir)) {
  678. string name = f->d_name;
  679. // Two dots are for navigation but this ain't as useful
  680. if (name == ".")
  681. continue;
  682. if (name == ".." ? g.cwd != "/" : (name[0] != '.' || g.show_hidden))
  683. g.entries.push_back (make_entry (f));
  684. }
  685. closedir (dir);
  686. sort (begin (g.entries), end (g.entries));
  687. g.out_of_date = false;
  688. if (g.cwd == old_cwd && !anchor.empty ()) {
  689. for (size_t i = 0; i < g.entries.size (); i++)
  690. if (g.entries[i].filename == anchor)
  691. g.cursor = i;
  692. }
  693. for (int col = 0; col < entry::COLUMNS; col++) {
  694. auto &longest = g.max_widths[col] = 0;
  695. for (const auto &entry : g.entries)
  696. longest = max (longest, compute_width (entry.cols[col]));
  697. }
  698. g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
  699. g.offset = min (g.offset, int (g.entries.size ()) - 1);
  700. if (g.inotify_wd != -1)
  701. inotify_rm_watch (g.inotify_fd, g.inotify_wd);
  702. // We don't show atime, so access and open are merely spam
  703. g.inotify_wd = inotify_add_watch (g.inotify_fd, ".",
  704. (IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
  705. }
  706. fun show_message (const string &message, int ttl = 30) {
  707. g.message = to_wide (message);
  708. g.message_ttl = ttl;
  709. }
  710. fun run_program (initializer_list<const char*> list, const string &filename) {
  711. endwin ();
  712. switch (pid_t child = fork ()) {
  713. int status;
  714. case -1:
  715. break;
  716. case 0:
  717. // Put the child in a new foreground process group...
  718. setpgid (0, 0);
  719. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  720. for (auto pager : list)
  721. if (pager) execl ("/bin/sh", "/bin/sh", "-c", (string (pager)
  722. + " " + shell_escape (filename)).c_str (), NULL);
  723. _exit (EXIT_FAILURE);
  724. default:
  725. // ...and make sure of it in the parent as well
  726. (void) setpgid (child, child);
  727. waitpid (child, &status, 0);
  728. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  729. }
  730. refresh ();
  731. update ();
  732. }
  733. fun view (const string &filename) {
  734. run_program ({(const char *) getenv ("PAGER"), "pager", "cat"}, filename);
  735. }
  736. fun edit (const string &filename) {
  737. run_program ({(const char *) getenv ("VISUAL"),
  738. (const char *) getenv ("EDITOR"), "vi"}, filename);
  739. }
  740. fun run_pager (FILE *contents) {
  741. // We don't really need to set O_CLOEXEC, so we're not going to
  742. rewind (contents);
  743. endwin ();
  744. switch (pid_t child = fork ()) {
  745. int status;
  746. case -1:
  747. break;
  748. case 0:
  749. // Put the child in a new foreground process group...
  750. setpgid (0, 0);
  751. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  752. dup2 (fileno (contents), STDIN_FILENO);
  753. // Behaviour copies man-db's man(1), similar to POSIX man(1)
  754. for (auto pager : {(const char *) getenv ("PAGER"), "pager", "cat"})
  755. if (pager) execl ("/bin/sh", "/bin/sh", "-c", pager, NULL);
  756. _exit (EXIT_FAILURE);
  757. default:
  758. // ...and make sure of it in the parent as well
  759. (void) setpgid (child, child);
  760. waitpid (child, &status, 0);
  761. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  762. }
  763. refresh ();
  764. update ();
  765. }
  766. fun encode_key (wint_t key) -> string {
  767. string encoded;
  768. if (key & ALT)
  769. encoded.append ("M-");
  770. wchar_t bare = key & ~ALT;
  771. if (g.key_to_name.count (bare))
  772. encoded.append (capitalize (g.key_to_name.at (bare)));
  773. else if (bare < 32)
  774. encoded.append ("C-").append ({char (tolower (bare + 64))});
  775. else
  776. encoded.append (to_mb ({bare}));
  777. return encoded;
  778. }
  779. fun show_help () {
  780. FILE *contents = tmpfile ();
  781. if (!contents)
  782. return;
  783. for (const auto &kv : g_binding_contexts) {
  784. fprintf (contents, "%s\n",
  785. underline (capitalize (kv.first + " key bindings")).c_str ());
  786. for (const auto &kv : *kv.second) {
  787. auto key = encode_key (kv.first);
  788. key.append (max (0, 10 - compute_width (to_wide (key))), ' ');
  789. fprintf (contents, "%s %s\n",
  790. key.c_str (), g.action_names[kv.second].c_str ());
  791. }
  792. fprintf (contents, "\n");
  793. }
  794. run_pager (contents);
  795. fclose (contents);
  796. }
  797. fun search (const wstring &needle) {
  798. int best = g.cursor, best_n = 0;
  799. for (int i = 0; i < int (g.entries.size ()); i++) {
  800. auto o = (i + g.cursor) % g.entries.size ();
  801. int n = prefix_length (to_wide (g.entries[o].filename), needle);
  802. if (n > best_n) {
  803. best = o;
  804. best_n = n;
  805. }
  806. }
  807. g.cursor = best;
  808. }
  809. fun fix_cursor_and_offset () {
  810. g.cursor = max (g.cursor, 0);
  811. g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
  812. // Decrease the offset when more items can suddenly fit
  813. int pushable = visible_lines () - (int (g.entries.size ()) - g.offset);
  814. g.offset -= max (pushable, 0);
  815. // Make sure the cursor is visible
  816. g.offset = max (g.offset, 0);
  817. g.offset = min (g.offset, int (g.entries.size ()) - 1);
  818. if (g.offset > g.cursor)
  819. g.offset = g.cursor;
  820. if (g.cursor - g.offset >= visible_lines ())
  821. g.offset = g.cursor - visible_lines () + 1;
  822. }
  823. fun is_ancestor_dir (const string &ancestor, const string &of) -> bool {
  824. if (strncmp (ancestor.c_str (), of.c_str (), ancestor.length ()))
  825. return false;
  826. return of[ancestor.length ()] == '/' || (ancestor == "/" && ancestor != of);
  827. }
  828. fun pop_levels () {
  829. string anchor; auto i = g.levels.rbegin ();
  830. while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) {
  831. if (i->path == g.cwd) {
  832. g.offset = i->offset;
  833. g.cursor = i->cursor;
  834. anchor = i->filename;
  835. }
  836. i++;
  837. g.levels.pop_back ();
  838. }
  839. fix_cursor_and_offset ();
  840. if (!anchor.empty () && g.entries[g.cursor].filename != anchor)
  841. search (to_wide (anchor));
  842. }
  843. fun explode_path (const string &path, vector<string> &out) {
  844. size_t mark = 0, p = path.find ("/");
  845. for (; p != string::npos; p = path.find ("/", (mark = p + 1)))
  846. out.push_back (path.substr (mark, p - mark));
  847. if (mark < path.length ())
  848. out.push_back (path.substr (mark));
  849. }
  850. fun serialize_path (const vector<string> &components) -> string {
  851. string result;
  852. for (const auto &i : components)
  853. result.append (i).append ("/");
  854. auto n = result.find_last_not_of ('/');
  855. if (n != result.npos)
  856. return result.erase (n + 1);
  857. return result;
  858. }
  859. fun absolutize (const string &abs_base, const string &path) -> string {
  860. if (path[0] == '/')
  861. return path;
  862. if (!abs_base.empty () && abs_base.back () == '/')
  863. return abs_base + path;
  864. return abs_base + "/" + path;
  865. }
  866. fun relativize (string current, const string &path) -> string {
  867. if (current == path)
  868. return ".";
  869. if (current.back () != '/')
  870. current += '/';
  871. if (!strncmp (current.c_str (), path.c_str (), current.length ()))
  872. return path.substr (current.length ());
  873. return path;
  874. }
  875. // Roughly follows the POSIX description of `cd -L` because of symlinks.
  876. // HOME and CDPATH handling is ommitted.
  877. fun change_dir (const string &path) {
  878. if (g.cwd[0] != '/') {
  879. show_message ("cannot figure out absolute path");
  880. beep ();
  881. return;
  882. }
  883. vector<string> in, out;
  884. explode_path (absolutize (g.cwd, path), in);
  885. // Paths with exactly two leading slashes may get special treatment
  886. size_t startempty = 1;
  887. if (in.size () >= 2 && in[1] == "" && (in.size () < 3 || in[2] != ""))
  888. startempty = 2;
  889. struct stat s{};
  890. for (size_t i = 0; i < in.size (); i++)
  891. if (in[i] == "..") {
  892. auto parent = relativize (g.cwd, serialize_path (out));
  893. if (errno = 0, !stat (parent.c_str (), &s) && !S_ISDIR (s.st_mode))
  894. errno = ENOTDIR;
  895. if (errno) {
  896. show_message (parent + ": " + strerror (errno));
  897. beep ();
  898. return;
  899. }
  900. if (!out.back().empty ())
  901. out.pop_back ();
  902. } else if (in[i] != "." && (!in[i].empty () || i < startempty)) {
  903. out.push_back (in[i]);
  904. }
  905. auto full_path = serialize_path (out);
  906. if (chdir (relativize (g.cwd, full_path).c_str ())) {
  907. show_message (strerror (errno));
  908. beep ();
  909. return;
  910. }
  911. auto old_cwd = g.cwd;
  912. level last {g.offset, g.cursor, old_cwd, g.entries[g.cursor].filename};
  913. g.cwd = full_path;
  914. reload (old_cwd);
  915. if (is_ancestor_dir (last.path, g.cwd)) {
  916. g.levels.push_back (last);
  917. g.offset = g.cursor = 0;
  918. } else {
  919. pop_levels ();
  920. }
  921. }
  922. // Roughly follows the POSIX description of the PWD environment variable
  923. fun initial_cwd () -> string {
  924. char cwd[4096] = ""; getcwd (cwd, sizeof cwd);
  925. const char *pwd = getenv ("PWD");
  926. if (!pwd || pwd[0] != '/' || strlen (pwd) >= PATH_MAX)
  927. return cwd;
  928. // Extra slashes shouldn't break anything for us
  929. vector<string> components;
  930. explode_path (pwd, components);
  931. for (const auto &i : components) {
  932. if (i == "." || i == "..")
  933. return cwd;
  934. }
  935. // Check if it "is an absolute pathname of the current working directory."
  936. // This particular method won't match on bind mounts, which is desired.
  937. char *real = realpath (pwd, nullptr);
  938. bool ok = real && !strcmp (cwd, real);
  939. free (real);
  940. return ok ? pwd : cwd;
  941. }
  942. fun choose (const entry &entry) {
  943. // Dive into directories and accessible symlinks to them
  944. if (!S_ISDIR (entry.info.st_mode)
  945. && !S_ISDIR (entry.target_info.st_mode)) {
  946. g.chosen = entry.filename;
  947. g.quitting = true;
  948. } else {
  949. change_dir (entry.filename);
  950. }
  951. }
  952. fun handle_editor (wint_t c) {
  953. auto i = g_input_actions.find (c);
  954. switch (i == g_input_actions.end () ? ACTION_NONE : i->second) {
  955. case ACTION_INPUT_CONFIRM:
  956. if (g.editor_on_confirm)
  957. g.editor_on_confirm ();
  958. // Fall-through
  959. case ACTION_INPUT_ABORT:
  960. g.editor_line.clear ();
  961. g.editor = 0;
  962. g.editor_on_change = nullptr;
  963. g.editor_on_confirm = nullptr;
  964. break;
  965. case ACTION_INPUT_B_DELETE:
  966. if (!g.editor_line.empty ())
  967. g.editor_line.erase (g.editor_line.length () - 1);
  968. break;
  969. default:
  970. if (c & (ALT | SYM)) {
  971. beep ();
  972. } else {
  973. g.editor_line += c;
  974. if (g.editor_on_change)
  975. g.editor_on_change ();
  976. }
  977. }
  978. }
  979. fun handle (wint_t c) -> bool {
  980. // If an editor is active, let it handle the key instead and eat it
  981. if (g.editor) {
  982. handle_editor (c);
  983. c = WEOF;
  984. }
  985. const auto &current = g.entries[g.cursor];
  986. auto i = g_normal_actions.find (c);
  987. switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
  988. case ACTION_CHOOSE_FULL:
  989. g.chosen = g.cwd + "/" + current.filename;
  990. g.no_chdir = true;
  991. g.quitting = true;
  992. break;
  993. case ACTION_CHOOSE:
  994. choose (current);
  995. break;
  996. case ACTION_VIEW:
  997. view (current.filename);
  998. break;
  999. case ACTION_EDIT:
  1000. edit (current.filename);
  1001. break;
  1002. case ACTION_HELP:
  1003. show_help ();
  1004. break;
  1005. case ACTION_QUIT_NO_CHDIR:
  1006. g.no_chdir = true;
  1007. // Fall-through
  1008. case ACTION_QUIT:
  1009. g.quitting = true;
  1010. break;
  1011. case ACTION_SORT_LEFT:
  1012. g.sort_column = (g.sort_column + entry::COLUMNS - 1) % entry::COLUMNS;
  1013. g.sort_flash_ttl = 2;
  1014. reload (g.cwd);
  1015. break;
  1016. case ACTION_SORT_RIGHT:
  1017. g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS;
  1018. g.sort_flash_ttl = 2;
  1019. reload (g.cwd);
  1020. break;
  1021. case ACTION_UP:
  1022. g.cursor--;
  1023. break;
  1024. case ACTION_DOWN:
  1025. g.cursor++;
  1026. break;
  1027. case ACTION_TOP:
  1028. g.cursor = 0;
  1029. break;
  1030. case ACTION_BOTTOM:
  1031. g.cursor = int (g.entries.size ()) - 1;
  1032. break;
  1033. case ACTION_HIGH:
  1034. g.cursor = g.offset;
  1035. break;
  1036. case ACTION_MIDDLE:
  1037. g.cursor = g.offset + (min (int (g.entries.size ()) - g.offset,
  1038. visible_lines ()) - 1) / 2;
  1039. break;
  1040. case ACTION_LOW:
  1041. g.cursor = g.offset + visible_lines () - 1;
  1042. break;
  1043. case ACTION_PAGE_PREVIOUS:
  1044. g.cursor -= LINES;
  1045. break;
  1046. case ACTION_PAGE_NEXT:
  1047. g.cursor += LINES;
  1048. break;
  1049. case ACTION_SCROLL_DOWN:
  1050. g.offset++;
  1051. break;
  1052. case ACTION_SCROLL_UP:
  1053. g.offset--;
  1054. break;
  1055. case ACTION_CHDIR:
  1056. g.editor = L"chdir";
  1057. g.editor_on_confirm = [] {
  1058. change_dir (to_mb (g.editor_line));
  1059. };
  1060. break;
  1061. case ACTION_GO_START:
  1062. change_dir (g.start_dir);
  1063. break;
  1064. case ACTION_GO_HOME:
  1065. if (const auto *home = getenv ("HOME"))
  1066. change_dir (home);
  1067. else if (const auto *pw = getpwuid (getuid ()))
  1068. change_dir (pw->pw_dir);
  1069. break;
  1070. case ACTION_SEARCH:
  1071. g.editor = L"search";
  1072. g.editor_on_change = [] {
  1073. search (g.editor_line);
  1074. };
  1075. g.editor_on_confirm = [] {
  1076. choose (g.entries[g.cursor]);
  1077. };
  1078. break;
  1079. case ACTION_RENAME_PREFILL:
  1080. g.editor_line = to_wide (current.filename);
  1081. // Fall-through
  1082. case ACTION_RENAME:
  1083. g.editor = L"rename";
  1084. g.editor_on_confirm = [] {
  1085. auto mb = to_mb (g.editor_line);
  1086. rename (g.entries[g.cursor].filename.c_str (), mb.c_str ());
  1087. reload (g.cwd);
  1088. };
  1089. break;
  1090. case ACTION_TOGGLE_FULL:
  1091. g.full_view = !g.full_view;
  1092. break;
  1093. case ACTION_REVERSE_SORT:
  1094. g.reverse_sort = !g.reverse_sort;
  1095. reload (g.cwd);
  1096. break;
  1097. case ACTION_SHOW_HIDDEN:
  1098. g.show_hidden = !g.show_hidden;
  1099. reload (g.cwd);
  1100. break;
  1101. case ACTION_REDRAW:
  1102. clear ();
  1103. break;
  1104. case ACTION_RELOAD:
  1105. reload (g.cwd);
  1106. break;
  1107. default:
  1108. if (c != KEY (RESIZE) && c != WEOF)
  1109. beep ();
  1110. }
  1111. fix_cursor_and_offset ();
  1112. update ();
  1113. return !g.quitting;
  1114. }
  1115. fun inotify_check () {
  1116. // Only provide simple indication that contents might have changed
  1117. char buf[4096]; ssize_t len;
  1118. bool changed = false;
  1119. while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) {
  1120. const inotify_event *e;
  1121. for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
  1122. e = (const inotify_event *) buf;
  1123. if (e->wd == g.inotify_wd)
  1124. changed = g.out_of_date = true;
  1125. }
  1126. }
  1127. if (changed)
  1128. update ();
  1129. }
  1130. fun decode_ansi_sgr (const vector<string> &v) -> chtype {
  1131. vector<int> args;
  1132. for (const auto &arg : v) {
  1133. char *end; unsigned long ul = strtoul (arg.c_str (), &end, 10);
  1134. if (*end != '\0' || ul > 255)
  1135. return 0;
  1136. args.push_back (ul);
  1137. }
  1138. chtype result = 0; int fg = -1, bg = -1;
  1139. for (size_t i = 0; i < args.size (); i++) {
  1140. auto arg = args[i];
  1141. if (arg == 0) {
  1142. result = 0; fg = -1; bg = -1;
  1143. } else if (arg == 1) {
  1144. result |= A_BOLD;
  1145. } else if (arg == 4) {
  1146. result |= A_UNDERLINE;
  1147. } else if (arg == 5) {
  1148. result |= A_BLINK;
  1149. } else if (arg == 7) {
  1150. result |= A_REVERSE;
  1151. } else if (arg >= 30 && arg <= 37) {
  1152. fg = arg - 30;
  1153. } else if (arg >= 40 && arg <= 47) {
  1154. bg = arg - 40;
  1155. // Anything other than indexed colours will be rejected completely
  1156. } else if (arg == 38 && (i += 2) < args.size ()) {
  1157. if (args[i - 1] != 5 || (fg = args[i]) >= COLORS)
  1158. return 0;
  1159. } else if (arg == 48 && (i += 2) < args.size ()) {
  1160. if (args[i - 1] != 5 || (bg = args[i]) >= COLORS)
  1161. return 0;
  1162. }
  1163. }
  1164. if (fg != -1 || bg != -1)
  1165. result |= COLOR_PAIR (allocate_pair (fg, bg));
  1166. return result;
  1167. }
  1168. fun load_ls_colors (vector<string> colors) {
  1169. map<string, chtype> attrs;
  1170. for (const auto &pair : colors) {
  1171. auto equal = pair.find ('=');
  1172. if (equal == string::npos)
  1173. continue;
  1174. auto key = pair.substr (0, equal), value = pair.substr (equal + 1);
  1175. if (key != g_ls_colors[LS_SYMLINK]
  1176. || !(g.ls_symlink_as_target = value == "target"))
  1177. attrs[key] = decode_ansi_sgr (split (value, ";"));
  1178. }
  1179. for (int i = 0; i < LS_COUNT; i++) {
  1180. auto m = attrs.find (g_ls_colors[i]);
  1181. if (m != attrs.end ())
  1182. g.ls_colors[i] = m->second;
  1183. }
  1184. for (const auto &pair : attrs) {
  1185. if (pair.first.substr (0, 2) == "*.")
  1186. g.ls_exts[pair.first.substr (2)] = pair.second;
  1187. }
  1188. }
  1189. fun load_colors () {
  1190. // Bail out on dumb terminals, there's not much one can do about them
  1191. if (!has_colors () || start_color () == ERR || use_default_colors () == ERR)
  1192. return;
  1193. if (const char *colors = getenv ("LS_COLORS"))
  1194. load_ls_colors (split (colors, ":"));
  1195. auto config = xdg_config_find ("look");
  1196. if (!config)
  1197. return;
  1198. vector<string> tokens;
  1199. while (parse_line (*config, tokens)) {
  1200. if (tokens.empty ())
  1201. continue;
  1202. auto name = shift (tokens);
  1203. for (int i = 0; i < g.AT_COUNT; i++)
  1204. if (name == g.attr_names[i])
  1205. g.attrs[i] = decode_attrs (tokens);
  1206. }
  1207. }
  1208. fun read_key (wint_t &c) -> bool {
  1209. int res = get_wch (&c);
  1210. if (res == ERR)
  1211. return false;
  1212. wint_t metafied{};
  1213. if (c == 27 && (res = get_wch (&metafied)) != ERR)
  1214. c = ALT | metafied;
  1215. if (res == KEY_CODE_YES)
  1216. c |= SYM;
  1217. return true;
  1218. }
  1219. fun parse_key (const string &key_name) -> wint_t {
  1220. wint_t c{};
  1221. auto p = key_name.c_str ();
  1222. if (!strncmp (p, "M-", 2)) {
  1223. c |= ALT;
  1224. p += 2;
  1225. }
  1226. if (!strncmp (p, "C-", 2)) {
  1227. p += 2;
  1228. if (*p < 32) {
  1229. cerr << "bindings: invalid combination: " << key_name << endl;
  1230. return WEOF;
  1231. }
  1232. c |= CTRL *p;
  1233. p += 1;
  1234. } else if (g.name_to_key.count (p)) {
  1235. return c | g.name_to_key.at (p);
  1236. } else {
  1237. wchar_t w; mbstate_t mb {};
  1238. auto len = strlen (p) + 1, res = mbrtowc (&w, p, len, &mb);
  1239. if (res == 0) {
  1240. cerr << "bindings: missing key name: " << key_name << endl;
  1241. return WEOF;
  1242. }
  1243. if (res == size_t (-1) || res == size_t (-2)) {
  1244. cerr << "bindings: invalid encoding: " << key_name << endl;
  1245. return WEOF;
  1246. }
  1247. c |= w;
  1248. p += res;
  1249. }
  1250. if (*p) {
  1251. cerr << "key name has unparsable trailing part: " << key_name << endl;
  1252. return WEOF;
  1253. }
  1254. return c;
  1255. }
  1256. fun learn_named_key (const string &name, wint_t key) {
  1257. g.name_to_key[g.key_to_name[key] = name] = key;
  1258. }
  1259. fun load_bindings () {
  1260. learn_named_key ("space", ' ');
  1261. learn_named_key ("escape", 0x1b);
  1262. for (int kc = KEY_MIN; kc < KEY_MAX; kc++) {
  1263. const char *name = keyname (kc);
  1264. if (!name)
  1265. continue;
  1266. if (!strncmp (name, "KEY_", 4))
  1267. name += 4;
  1268. string filtered;
  1269. for (; *name; name++) {
  1270. if (*name != '(' && *name != ')')
  1271. filtered += *name;
  1272. }
  1273. learn_named_key (filtered, SYM | kc);
  1274. }
  1275. auto config = xdg_config_find ("bindings");
  1276. if (!config)
  1277. return;
  1278. // Stringization in the preprocessor is a bit limited, we want lisp-case
  1279. map<string, action> actions;
  1280. int a = 0;
  1281. for (auto p : g_action_names) {
  1282. string name;
  1283. for (; *p; p++)
  1284. name += *p == '_' ? '-' : *p + 'a' - 'A';
  1285. g.action_names[a] = name;
  1286. actions[name] = action (a++);
  1287. }
  1288. vector<string> tokens;
  1289. while (parse_line (*config, tokens)) {
  1290. if (tokens.empty ())
  1291. continue;
  1292. if (tokens.size () < 3) {
  1293. cerr << "bindings: expected: context binding action";
  1294. continue;
  1295. }
  1296. auto context = tokens[0], key_name = tokens[1], action = tokens[2];
  1297. auto m = g_binding_contexts.find (context);
  1298. if (m == g_binding_contexts.end ()) {
  1299. cerr << "bindings: invalid context: " << context << endl;
  1300. continue;
  1301. }
  1302. wint_t c = parse_key (key_name);
  1303. if (c == WEOF)
  1304. continue;
  1305. auto i = actions.find (action);
  1306. if (i == actions.end ()) {
  1307. cerr << "bindings: invalid action: " << action << endl;
  1308. continue;
  1309. }
  1310. (*m->second)[c] = i->second;
  1311. }
  1312. }
  1313. fun load_history_level (const vector<string> &v) {
  1314. if (v.size () != 7)
  1315. return;
  1316. // Not checking the hostname and parent PID right now since we can't merge
  1317. g.levels.push_back ({stoi (v.at (4)), stoi (v.at (5)), v.at (3), v.at (6)});
  1318. }
  1319. fun load_config () {
  1320. auto config = xdg_config_find ("config");
  1321. if (!config)
  1322. return;
  1323. vector<string> tokens;
  1324. while (parse_line (*config, tokens)) {
  1325. if (tokens.empty ())
  1326. continue;
  1327. if (tokens.front () == "full-view" && tokens.size () > 1)
  1328. g.full_view = tokens.at (1) == "1";
  1329. else if (tokens.front () == "gravity" && tokens.size () > 1)
  1330. g.gravity = tokens.at (1) == "1";
  1331. else if (tokens.front () == "reverse-sort" && tokens.size () > 1)
  1332. g.reverse_sort = tokens.at (1) == "1";
  1333. else if (tokens.front () == "show-hidden" && tokens.size () > 1)
  1334. g.show_hidden = tokens.at (1) == "1";
  1335. else if (tokens.front () == "sort-column" && tokens.size () > 1)
  1336. g.sort_column = stoi (tokens.at (1));
  1337. else if (tokens.front () == "history")
  1338. load_history_level (tokens);
  1339. }
  1340. }
  1341. fun save_config () {
  1342. auto config = xdg_config_write ("config");
  1343. if (!config)
  1344. return;
  1345. write_line (*config, {"full-view", g.full_view ? "1" : "0"});
  1346. write_line (*config, {"gravity", g.gravity ? "1" : "0"});
  1347. write_line (*config, {"reverse-sort", g.reverse_sort ? "1" : "0"});
  1348. write_line (*config, {"show-hidden", g.show_hidden ? "1" : "0"});
  1349. write_line (*config, {"sort-column", to_string (g.sort_column)});
  1350. char hostname[256];
  1351. if (gethostname (hostname, sizeof hostname))
  1352. *hostname = 0;
  1353. auto ppid = std::to_string (getppid ());
  1354. for (auto i = g.levels.begin (); i != g.levels.end (); i++)
  1355. write_line (*config, {"history", hostname, ppid, i->path,
  1356. to_string (i->offset), to_string (i->cursor), i->filename});
  1357. write_line (*config, {"history", hostname, ppid, g.cwd,
  1358. to_string (g.offset), to_string (g.cursor),
  1359. g.entries[g.cursor].filename});
  1360. }
  1361. int main (int argc, char *argv[]) {
  1362. (void) argc;
  1363. (void) argv;
  1364. // That bitch zle closes stdin before exec without redirection
  1365. (void) close (STDIN_FILENO);
  1366. if (open ("/dev/tty", O_RDWR)) {
  1367. cerr << "cannot open tty" << endl;
  1368. return 1;
  1369. }
  1370. // Save the original stdout and force ncurses to use the terminal directly
  1371. auto output_fd = dup (STDOUT_FILENO);
  1372. dup2 (STDIN_FILENO, STDOUT_FILENO);
  1373. // So that the neither us nor our children stop on tcsetpgrp()
  1374. signal (SIGTTOU, SIG_IGN);
  1375. if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
  1376. cerr << "cannot initialize inotify" << endl;
  1377. return 1;
  1378. }
  1379. locale::global (locale (""));
  1380. load_bindings ();
  1381. load_config ();
  1382. if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR) {
  1383. cerr << "cannot initialize screen" << endl;
  1384. return 1;
  1385. }
  1386. load_colors ();
  1387. g.start_dir = g.cwd = initial_cwd ();
  1388. reload (g.cwd);
  1389. pop_levels ();
  1390. update ();
  1391. // Invoking keypad() earlier would make ncurses flush its output buffer,
  1392. // which would worsen start-up flickering
  1393. if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
  1394. endwin ();
  1395. cerr << "cannot configure input" << endl;
  1396. return 1;
  1397. }
  1398. wint_t c;
  1399. while (!read_key (c) || handle (c)) {
  1400. inotify_check ();
  1401. if (g.sort_flash_ttl && !--g.sort_flash_ttl)
  1402. update ();
  1403. if (g.message_ttl && !--g.message_ttl) {
  1404. g.message.clear ();
  1405. update ();
  1406. }
  1407. }
  1408. endwin ();
  1409. save_config ();
  1410. // Presumably it is going to end up as an argument, so quote it
  1411. if (!g.chosen.empty ())
  1412. g.chosen = shell_escape (g.chosen);
  1413. // We can't portably create a standard stream from an FD, so modify the FD
  1414. dup2 (output_fd, STDOUT_FILENO);
  1415. if (g.cwd != g.start_dir && !g.no_chdir)
  1416. cout << "local cd=" << shell_escape (g.cwd) << endl;
  1417. else
  1418. cout << "local cd=" << endl;
  1419. cout << "local insert=" << shell_escape (g.chosen) << endl;
  1420. return 0;
  1421. }