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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587
  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(PAGE_PREVIOUS) XX(PAGE_NEXT) \
  341. XX(SCROLL_UP) XX(SCROLL_DOWN) XX(CHDIR) XX(GO_START) XX(GO_HOME) \
  342. XX(SEARCH) XX(RENAME) XX(RENAME_PREFILL) \
  343. XX(TOGGLE_FULL) XX(REVERSE_SORT) XX(SHOW_HIDDEN) XX(REDRAW) XX(RELOAD) \
  344. XX(INPUT_ABORT) XX(INPUT_CONFIRM) XX(INPUT_B_DELETE)
  345. #define XX(name) ACTION_ ## name,
  346. enum action { ACTIONS(XX) ACTION_COUNT };
  347. #undef XX
  348. #define XX(name) #name,
  349. static const char *g_action_names[] = {ACTIONS(XX)};
  350. #undef XX
  351. static map<wint_t, action> g_normal_actions {
  352. {ALT | '\r', ACTION_CHOOSE_FULL}, {ALT | KEY (ENTER), ACTION_CHOOSE_FULL},
  353. {'\r', ACTION_CHOOSE}, {KEY (ENTER), ACTION_CHOOSE},
  354. {KEY (F (3)), ACTION_VIEW}, {KEY (F (4)), ACTION_EDIT}, {'h', ACTION_HELP},
  355. {'q', ACTION_QUIT}, {ALT | 'q', ACTION_QUIT_NO_CHDIR},
  356. // M-o ought to be the same shortcut the navigator is launched with
  357. {ALT | 'o', ACTION_QUIT},
  358. {'<', ACTION_SORT_LEFT}, {'>', ACTION_SORT_RIGHT},
  359. {'k', ACTION_UP}, {CTRL 'p', ACTION_UP}, {KEY (UP), ACTION_UP},
  360. {'j', ACTION_DOWN}, {CTRL 'n', ACTION_DOWN}, {KEY (DOWN), ACTION_DOWN},
  361. {'g', ACTION_TOP}, {ALT | '<', ACTION_TOP}, {KEY (HOME), ACTION_TOP},
  362. {'G', ACTION_BOTTOM}, {ALT | '>', ACTION_BOTTOM}, {KEY(END), ACTION_BOTTOM},
  363. {KEY (PPAGE), ACTION_PAGE_PREVIOUS}, {KEY (NPAGE), ACTION_PAGE_NEXT},
  364. {CTRL 'y', ACTION_SCROLL_UP}, {CTRL 'e', ACTION_SCROLL_DOWN},
  365. {'c', ACTION_CHDIR}, {'&', ACTION_GO_START}, {'~', ACTION_GO_HOME},
  366. {'/', ACTION_SEARCH}, {'s', ACTION_SEARCH},
  367. {ALT | 'e', ACTION_RENAME_PREFILL}, {'e', ACTION_RENAME},
  368. {'t', ACTION_TOGGLE_FULL}, {ALT | 't', ACTION_TOGGLE_FULL},
  369. {'R', ACTION_REVERSE_SORT}, {ALT | '.', ACTION_SHOW_HIDDEN},
  370. {CTRL 'L', ACTION_REDRAW}, {'r', ACTION_RELOAD},
  371. };
  372. static map<wint_t, action> g_input_actions {
  373. {27, ACTION_INPUT_ABORT}, {CTRL 'g', ACTION_INPUT_ABORT},
  374. {L'\r', ACTION_INPUT_CONFIRM}, {KEY (ENTER), ACTION_INPUT_CONFIRM},
  375. {KEY (BACKSPACE), ACTION_INPUT_B_DELETE},
  376. };
  377. static const map<string, map<wint_t, action>*> g_binding_contexts {
  378. {"normal", &g_normal_actions}, {"input", &g_input_actions},
  379. };
  380. #define LS(XX) XX(NORMAL, "no") XX(FILE, "fi") XX(RESET, "rs") \
  381. XX(DIRECTORY, "di") XX(SYMLINK, "ln") XX(MULTIHARDLINK, "mh") \
  382. XX(FIFO, "pi") XX(SOCKET, "so") XX(DOOR, "do") XX(BLOCK, "bd") \
  383. XX(CHARACTER, "cd") XX(ORPHAN, "or") XX(MISSING, "mi") XX(SETUID, "su") \
  384. XX(SETGID, "sg") XX(CAPABILITY, "ca") XX(STICKY_OTHER_WRITABLE, "tw") \
  385. XX(OTHER_WRITABLE, "ow") XX(STICKY, "st") XX(EXECUTABLE, "ex")
  386. #define XX(id, name) LS_ ## id,
  387. enum { LS(XX) LS_COUNT };
  388. #undef XX
  389. #define XX(id, name) name,
  390. static const char *g_ls_colors[] = {LS(XX)};
  391. #undef XX
  392. struct stringcaseless {
  393. bool operator () (const string &a, const string &b) const {
  394. const auto &c = locale::classic();
  395. return lexicographical_compare (begin (a), end (a), begin (b), end (b),
  396. [&](char m, char n) { return tolower (m, c) < tolower (n, c); });
  397. }
  398. };
  399. struct entry {
  400. string filename, target_path;
  401. struct stat info = {}, target_info = {};
  402. enum { MODES, USER, GROUP, SIZE, MTIME, FILENAME, COLUMNS };
  403. ncstring cols[COLUMNS];
  404. };
  405. struct level {
  406. int offset, cursor; ///< Scroll offset and cursor position
  407. string path, filename; ///< Level path and filename at cursor
  408. };
  409. static struct {
  410. string cwd; ///< Current working directory
  411. string start_dir; ///< Starting directory
  412. vector<entry> entries; ///< Current directory entries
  413. vector<level> levels; ///< Upper directory levels
  414. int offset, cursor; ///< Scroll offset and cursor position
  415. bool full_view; ///< Show extended information
  416. bool gravity; ///< Entries are shoved to the bottom
  417. bool reverse_sort; ///< Reverse sort
  418. bool show_hidden; ///< Show hidden files
  419. int max_widths[entry::COLUMNS]; ///< Column widths
  420. int sort_column = entry::FILENAME; ///< Sorting column
  421. int sort_flash_ttl; ///< Sorting column flash TTL
  422. wstring message; ///< Message for the user
  423. int message_ttl; ///< Time to live for the message
  424. string chosen; ///< Chosen item for the command line
  425. bool no_chdir; ///< Do not tell the shell to chdir
  426. bool quitting; ///< Whether we should quit already
  427. int inotify_fd, inotify_wd = -1; ///< File watch
  428. bool out_of_date; ///< Entries may be out of date
  429. const wchar_t *editor; ///< Prompt string for editing
  430. wstring editor_line; ///< Current user input
  431. void (*editor_on_change) (); ///< Callback on editor change
  432. void (*editor_on_confirm) (); ///< Callback on editor confirmation
  433. enum { AT_CURSOR, AT_BAR, AT_CWD, AT_INPUT, AT_COUNT };
  434. chtype attrs[AT_COUNT] = {A_REVERSE, 0, A_BOLD, 0};
  435. const char *attr_names[AT_COUNT] = {"cursor", "bar", "cwd", "input"};
  436. map<int, chtype> ls_colors; ///< LS_COLORS decoded
  437. map<string, chtype> ls_exts; ///< LS_COLORS file extensions
  438. bool ls_symlink_as_target; ///< ln=target in dircolors
  439. map<string, wint_t, stringcaseless> name_to_key;
  440. map<wint_t, string> key_to_name;
  441. string action_names[ACTION_COUNT]; ///< Stylized action names
  442. // Refreshed by reload():
  443. map<uid_t, string> unames; ///< User names by UID
  444. map<gid_t, string> gnames; ///< Group names by GID
  445. struct tm now; ///< Current local time for display
  446. } g;
  447. // The coloring logic has been more or less exactly copied from GNU ls,
  448. // simplified and rewritten to reflect local implementation specifics
  449. fun ls_is_colored (int type) -> bool {
  450. auto i = g.ls_colors.find (type);
  451. return i != g.ls_colors.end () && i->second != 0;
  452. }
  453. fun ls_format (const entry &e, bool for_target) -> chtype {
  454. int type = LS_ORPHAN;
  455. auto set = [&](int t) { if (ls_is_colored (t)) type = t; };
  456. const auto &name = for_target
  457. ? e.target_path : e.filename;
  458. const auto &info =
  459. (for_target || (g.ls_symlink_as_target && e.target_info.st_mode))
  460. ? e.target_info : e.info;
  461. if (for_target && info.st_mode == 0) {
  462. // This differs from GNU ls: we use ORPHAN when MISSING is not set,
  463. // but GNU ls colors by dirent::d_type
  464. set (LS_MISSING);
  465. } else if (S_ISREG (info.st_mode)) {
  466. type = LS_FILE;
  467. if (info.st_nlink > 1)
  468. set (LS_MULTIHARDLINK);
  469. if ((info.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
  470. set (LS_EXECUTABLE);
  471. if (lgetxattr (name.c_str (), "security.capability", NULL, 0) >= 0)
  472. set (LS_CAPABILITY);
  473. if ((info.st_mode & S_ISGID))
  474. set (LS_SETGID);
  475. if ((info.st_mode & S_ISUID))
  476. set (LS_SETUID);
  477. } else if (S_ISDIR (info.st_mode)) {
  478. type = LS_DIRECTORY;
  479. if ((info.st_mode & S_ISVTX))
  480. set (LS_STICKY);
  481. if ((info.st_mode & S_IWOTH))
  482. set (LS_OTHER_WRITABLE);
  483. if ((info.st_mode & S_ISVTX) && (info.st_mode & S_IWOTH))
  484. set (LS_STICKY_OTHER_WRITABLE);
  485. } else if (S_ISLNK (info.st_mode)) {
  486. type = LS_SYMLINK;
  487. if (!e.target_info.st_mode
  488. && (ls_is_colored (LS_ORPHAN) || g.ls_symlink_as_target))
  489. type = LS_ORPHAN;
  490. } else if (S_ISFIFO (info.st_mode)) {
  491. type = LS_FIFO;
  492. } else if (S_ISSOCK (info.st_mode)) {
  493. type = LS_SOCKET;
  494. } else if (S_ISBLK (info.st_mode)) {
  495. type = LS_BLOCK;
  496. } else if (S_ISCHR (info.st_mode)) {
  497. type = LS_CHARACTER;
  498. }
  499. chtype format = 0;
  500. const auto x = g.ls_colors.find (type);
  501. if (x != g.ls_colors.end ())
  502. format = x->second;
  503. auto dot = name.find_last_of ('.');
  504. if (dot != string::npos && type == LS_FILE) {
  505. const auto x = g.ls_exts.find (name.substr (++dot));
  506. if (x != g.ls_exts.end ())
  507. format = x->second;
  508. }
  509. return format;
  510. }
  511. fun make_entry (const struct dirent *f) -> entry {
  512. entry e;
  513. e.filename = f->d_name;
  514. e.info.st_mode = DTTOIF (f->d_type);
  515. auto &info = e.info;
  516. // TODO: benchmark just readdir() vs. lstat(), also on dead mounts;
  517. // it might make sense to stat asynchronously in threads
  518. // http://lkml.iu.edu/hypermail//linux/kernel/0804.3/1616.html
  519. if (lstat (f->d_name, &info)) {
  520. e.cols[entry::MODES] = apply_attrs ({ decode_type (info.st_mode),
  521. L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?', L'?' }, 0);
  522. e.cols[entry::USER] = e.cols[entry::GROUP] =
  523. e.cols[entry::SIZE] = e.cols[entry::MTIME] = apply_attrs (L"?", 0);
  524. e.cols[entry::FILENAME] =
  525. apply_attrs (to_wide (e.filename), ls_format (e, false));
  526. return e;
  527. }
  528. if (S_ISLNK (info.st_mode)) {
  529. char buf[PATH_MAX] = {};
  530. auto len = readlink (f->d_name, buf, sizeof buf);
  531. if (len < 0 || size_t (len) >= sizeof buf) {
  532. e.target_path = "?";
  533. } else {
  534. e.target_path = buf;
  535. // If a symlink links to another symlink, we follow all the way
  536. (void) stat (buf, &e.target_info);
  537. }
  538. }
  539. auto mode = decode_mode (info.st_mode);
  540. // This is a Linux-only extension
  541. if (acl_extended_file_nofollow (f->d_name) > 0)
  542. mode += L"+";
  543. e.cols[entry::MODES] = apply_attrs (mode, 0);
  544. auto usr = g.unames.find (info.st_uid);
  545. e.cols[entry::USER] = (usr != g.unames.end ())
  546. ? apply_attrs (to_wide (usr->second), 0)
  547. : apply_attrs (to_wstring (info.st_uid), 0);
  548. auto grp = g.gnames.find (info.st_gid);
  549. e.cols[entry::GROUP] = (grp != g.gnames.end ())
  550. ? apply_attrs (to_wide (grp->second), 0)
  551. : apply_attrs (to_wstring (info.st_gid), 0);
  552. auto size = to_wstring (info.st_size);
  553. if (info.st_size >> 40) size = to_wstring (info.st_size >> 40) + L"T";
  554. else if (info.st_size >> 30) size = to_wstring (info.st_size >> 30) + L"G";
  555. else if (info.st_size >> 20) size = to_wstring (info.st_size >> 20) + L"M";
  556. else if (info.st_size >> 10) size = to_wstring (info.st_size >> 10) + L"K";
  557. e.cols[entry::SIZE] = apply_attrs (size, 0);
  558. char buf[32] = "";
  559. auto tm = localtime (&info.st_mtime);
  560. strftime (buf, sizeof buf,
  561. (tm->tm_year == g.now.tm_year) ? "%b %e %H:%M" : "%b %e %Y", tm);
  562. e.cols[entry::MTIME] = apply_attrs (to_wide (buf), 0);
  563. auto &fn = e.cols[entry::FILENAME] =
  564. apply_attrs (to_wide (e.filename), ls_format (e, false));
  565. if (!e.target_path.empty ()) {
  566. fn.append (apply_attrs (to_wide (" -> "), 0));
  567. fn.append (apply_attrs (to_wide (e.target_path), ls_format (e, true)));
  568. }
  569. return e;
  570. }
  571. fun inline visible_lines () -> int { return max (0, LINES - 2); }
  572. fun update () {
  573. int start_column = g.full_view ? 0 : entry::FILENAME;
  574. static int alignment[entry::COLUMNS] = { -1, -1, -1, 1, 1, -1 };
  575. erase ();
  576. int available = visible_lines ();
  577. int used = min (available, int (g.entries.size ()) - g.offset);
  578. for (int i = 0; i < used; i++) {
  579. auto index = g.offset + i;
  580. bool selected = index == g.cursor;
  581. attrset (selected ? g.attrs[g.AT_CURSOR] : 0);
  582. move (g.gravity ? (available - used + i) : i, 0);
  583. auto used = 0;
  584. for (int col = start_column; col < entry::COLUMNS; col++) {
  585. const auto &field = g.entries[index].cols[col];
  586. auto aligned = align (field, alignment[col] * g.max_widths[col]);
  587. if (g.sort_flash_ttl && col == g.sort_column)
  588. for_each (begin (aligned), end (aligned), invert);
  589. if (selected)
  590. for_each (begin (aligned), end (aligned), decolor);
  591. used += print (aligned + apply_attrs (L" ", 0), COLS - used);
  592. }
  593. hline (' ', COLS - used);
  594. }
  595. auto bar = apply_attrs (to_wide (g.cwd), g.attrs[g.AT_CWD]);
  596. if (!g.show_hidden)
  597. bar += apply_attrs (L" (hidden)", 0);
  598. if (g.out_of_date)
  599. bar += apply_attrs (L" [+]", 0);
  600. move (LINES - 2, 0);
  601. attrset (g.attrs[g.AT_BAR]);
  602. hline (' ', COLS - print (bar, COLS));
  603. attrset (g.attrs[g.AT_INPUT]);
  604. curs_set (0);
  605. if (g.editor) {
  606. move (LINES - 1, 0);
  607. auto p = apply_attrs (wstring (g.editor) + L": ", 0);
  608. move (LINES - 1, print (p + apply_attrs (g.editor_line, 0), COLS - 1));
  609. curs_set (1);
  610. } else if (!g.message.empty ()) {
  611. move (LINES - 1, 0);
  612. print (apply_attrs (g.message, 0), COLS);
  613. }
  614. refresh ();
  615. }
  616. fun operator< (const entry &e1, const entry &e2) -> bool {
  617. auto t1 = make_tuple (e1.filename != "..",
  618. !S_ISDIR (e1.info.st_mode) && !S_ISDIR (e1.target_info.st_mode));
  619. auto t2 = make_tuple (e2.filename != "..",
  620. !S_ISDIR (e2.info.st_mode) && !S_ISDIR (e2.target_info.st_mode));
  621. if (t1 != t2)
  622. return t1 < t2;
  623. const auto &a = g.reverse_sort ? e2 : e1;
  624. const auto &b = g.reverse_sort ? e1 : e2;
  625. switch (g.sort_column) {
  626. case entry::MODES:
  627. if (a.info.st_mode != b.info.st_mode)
  628. return a.info.st_mode < b.info.st_mode;
  629. break;
  630. case entry::USER:
  631. if (a.info.st_uid != b.info.st_uid)
  632. return a.info.st_uid < b.info.st_uid;
  633. break;
  634. case entry::GROUP:
  635. if (a.info.st_gid != b.info.st_gid)
  636. return a.info.st_gid < b.info.st_gid;
  637. break;
  638. case entry::SIZE:
  639. if (a.info.st_size != b.info.st_size)
  640. return a.info.st_size < b.info.st_size;
  641. break;
  642. case entry::MTIME:
  643. if (a.info.st_mtime != b.info.st_mtime)
  644. return a.info.st_mtime < b.info.st_mtime;
  645. break;
  646. }
  647. return a.filename < b.filename;
  648. }
  649. fun reload (const string &old_cwd) {
  650. g.unames.clear();
  651. while (auto *ent = getpwent ())
  652. g.unames.emplace (ent->pw_uid, ent->pw_name);
  653. endpwent();
  654. g.gnames.clear();
  655. while (auto *ent = getgrent ())
  656. g.gnames.emplace (ent->gr_gid, ent->gr_name);
  657. endgrent();
  658. string anchor;
  659. if (!g.entries.empty ())
  660. anchor = g.entries[g.cursor].filename;
  661. auto now = time (NULL); g.now = *localtime (&now);
  662. auto dir = opendir (".");
  663. g.entries.clear ();
  664. while (auto f = readdir (dir)) {
  665. string name = f->d_name;
  666. // Two dots are for navigation but this ain't as useful
  667. if (name == ".")
  668. continue;
  669. if (name == ".." ? g.cwd != "/" : (name[0] != '.' || g.show_hidden))
  670. g.entries.push_back (make_entry (f));
  671. }
  672. closedir (dir);
  673. sort (begin (g.entries), end (g.entries));
  674. g.out_of_date = false;
  675. if (g.cwd == old_cwd && !anchor.empty ()) {
  676. for (size_t i = 0; i < g.entries.size (); i++)
  677. if (g.entries[i].filename == anchor)
  678. g.cursor = i;
  679. }
  680. for (int col = 0; col < entry::COLUMNS; col++) {
  681. auto &longest = g.max_widths[col] = 0;
  682. for (const auto &entry : g.entries)
  683. longest = max (longest, compute_width (entry.cols[col]));
  684. }
  685. g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
  686. g.offset = min (g.offset, int (g.entries.size ()) - 1);
  687. if (g.inotify_wd != -1)
  688. inotify_rm_watch (g.inotify_fd, g.inotify_wd);
  689. // We don't show atime, so access and open are merely spam
  690. char buf[PATH_MAX];
  691. g.inotify_wd = inotify_add_watch (g.inotify_fd, buf,
  692. (IN_ALL_EVENTS | IN_ONLYDIR | IN_EXCL_UNLINK) & ~(IN_ACCESS | IN_OPEN));
  693. }
  694. fun show_message (const string &message, int ttl = 30) {
  695. g.message = to_wide (message);
  696. g.message_ttl = ttl;
  697. }
  698. fun run_program (initializer_list<const char*> list, const string &filename) {
  699. endwin ();
  700. switch (pid_t child = fork ()) {
  701. int status;
  702. case -1:
  703. break;
  704. case 0:
  705. // Put the child in a new foreground process group...
  706. setpgid (0, 0);
  707. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  708. for (auto pager : list)
  709. if (pager) execl ("/bin/sh", "/bin/sh", "-c", (string (pager)
  710. + " " + shell_escape (filename)).c_str (), NULL);
  711. _exit (EXIT_FAILURE);
  712. default:
  713. // ...and make sure of it in the parent as well
  714. (void) setpgid (child, child);
  715. waitpid (child, &status, 0);
  716. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  717. }
  718. refresh ();
  719. update ();
  720. }
  721. fun view (const string &filename) {
  722. run_program ({(const char *) getenv ("PAGER"), "pager", "cat"}, filename);
  723. }
  724. fun edit (const string &filename) {
  725. run_program ({(const char *) getenv ("VISUAL"),
  726. (const char *) getenv ("EDITOR"), "vi"}, filename);
  727. }
  728. fun run_pager (FILE *contents) {
  729. // We don't really need to set O_CLOEXEC, so we're not going to
  730. rewind (contents);
  731. endwin ();
  732. switch (pid_t child = fork ()) {
  733. int status;
  734. case -1:
  735. break;
  736. case 0:
  737. // Put the child in a new foreground process group...
  738. setpgid (0, 0);
  739. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  740. dup2 (fileno (contents), STDIN_FILENO);
  741. // Behaviour copies man-db's man(1), similar to POSIX man(1)
  742. for (auto pager : {(const char *) getenv ("PAGER"), "pager", "cat"})
  743. if (pager) execl ("/bin/sh", "/bin/sh", "-c", pager, NULL);
  744. _exit (EXIT_FAILURE);
  745. default:
  746. // ...and make sure of it in the parent as well
  747. (void) setpgid (child, child);
  748. waitpid (child, &status, 0);
  749. tcsetpgrp (STDOUT_FILENO, getpgid (0));
  750. }
  751. refresh ();
  752. update ();
  753. }
  754. fun encode_key (wint_t key) -> string {
  755. string encoded;
  756. if (key & ALT)
  757. encoded.append ("M-");
  758. wchar_t bare = key & ~ALT;
  759. if (g.key_to_name.count (bare))
  760. encoded.append (capitalize (g.key_to_name.at (bare)));
  761. else if (bare < 32)
  762. encoded.append ("C-").append ({char (tolower (bare + 64))});
  763. else
  764. encoded.append (to_mb ({bare}));
  765. return encoded;
  766. }
  767. fun show_help () {
  768. FILE *contents = tmpfile ();
  769. if (!contents)
  770. return;
  771. for (const auto &kv : g_binding_contexts) {
  772. fprintf (contents, "%s\n",
  773. underline (capitalize (kv.first + " key bindings")).c_str ());
  774. for (const auto &kv : *kv.second) {
  775. auto key = encode_key (kv.first);
  776. key.append (max (0, 10 - compute_width (to_wide (key))), ' ');
  777. fprintf (contents, "%s %s\n",
  778. key.c_str (), g.action_names[kv.second].c_str ());
  779. }
  780. fprintf (contents, "\n");
  781. }
  782. run_pager (contents);
  783. fclose (contents);
  784. }
  785. fun search (const wstring &needle) {
  786. int best = g.cursor, best_n = 0;
  787. for (int i = 0; i < int (g.entries.size ()); i++) {
  788. auto o = (i + g.cursor) % g.entries.size ();
  789. int n = prefix_length (to_wide (g.entries[o].filename), needle);
  790. if (n > best_n) {
  791. best = o;
  792. best_n = n;
  793. }
  794. }
  795. g.cursor = best;
  796. }
  797. fun fix_cursor_and_offset () {
  798. g.cursor = max (g.cursor, 0);
  799. g.cursor = min (g.cursor, int (g.entries.size ()) - 1);
  800. // Decrease the offset when more items can suddenly fit
  801. int pushable = visible_lines () - (int (g.entries.size ()) - g.offset);
  802. g.offset -= max (pushable, 0);
  803. // Make sure cursor is visible
  804. g.offset = max (g.offset, 0);
  805. g.offset = min (g.offset, int (g.entries.size ()) - 1);
  806. if (g.offset > g.cursor)
  807. g.offset = g.cursor;
  808. if (g.cursor - g.offset >= visible_lines ())
  809. g.offset = g.cursor - visible_lines () + 1;
  810. }
  811. fun is_ancestor_dir (const string &ancestor, const string &of) -> bool {
  812. if (strncmp (ancestor.c_str (), of.c_str (), ancestor.length ()))
  813. return false;
  814. return of[ancestor.length ()] == '/' || (ancestor == "/" && ancestor != of);
  815. }
  816. fun pop_levels () {
  817. string anchor; auto i = g.levels.rbegin ();
  818. while (i != g.levels.rend () && !is_ancestor_dir (i->path, g.cwd)) {
  819. if (i->path == g.cwd) {
  820. g.offset = i->offset;
  821. g.cursor = i->cursor;
  822. anchor = i->filename;
  823. }
  824. i++;
  825. g.levels.pop_back ();
  826. }
  827. fix_cursor_and_offset ();
  828. if (!anchor.empty () && g.entries[g.cursor].filename != anchor)
  829. search (to_wide (anchor));
  830. }
  831. fun explode_path (const string &path, vector<string> &out) {
  832. size_t mark = 0, p = path.find ("/");
  833. for (; p != string::npos; p = path.find ("/", (mark = p + 1)))
  834. out.push_back (path.substr (mark, p - mark));
  835. if (mark < path.length ())
  836. out.push_back (path.substr (mark));
  837. }
  838. fun serialize_path (const vector<string> &components) -> string {
  839. string result;
  840. for (const auto &i : components)
  841. result.append (i).append ("/");
  842. auto n = result.find_last_not_of ('/');
  843. if (n != result.npos)
  844. return result.erase (n + 1);
  845. return result;
  846. }
  847. fun absolutize (const string &abs_base, const string &path) -> string {
  848. if (path[0] == '/')
  849. return path;
  850. if (!abs_base.empty () && abs_base.back () == '/')
  851. return abs_base + path;
  852. return abs_base + "/" + path;
  853. }
  854. fun relativize (string current, const string &path) -> string {
  855. if (current == path)
  856. return ".";
  857. if (current.back () != '/')
  858. current += '/';
  859. if (!strncmp (current.c_str (), path.c_str (), current.length ()))
  860. return path.substr (current.length ());
  861. return path;
  862. }
  863. // Roughly follows the POSIX description of `cd -L` because of symlinks.
  864. // HOME and CDPATH handling is ommitted.
  865. fun change_dir (const string &path) {
  866. if (g.cwd[0] != '/') {
  867. show_message ("cannot figure out absolute path");
  868. beep ();
  869. return;
  870. }
  871. vector<string> in, out;
  872. explode_path (absolutize (g.cwd, path), in);
  873. // Paths with exactly two leading slashes may get special treatment
  874. size_t startempty = 1;
  875. if (in.size () >= 2 && in[1] == "" && (in.size () < 3 || in[2] != ""))
  876. startempty = 2;
  877. struct stat s{};
  878. for (size_t i = 0; i < in.size (); i++)
  879. if (in[i] == "..") {
  880. auto parent = relativize (g.cwd, serialize_path (out));
  881. if (errno = 0, !stat (parent.c_str (), &s) && !S_ISDIR (s.st_mode))
  882. errno = ENOTDIR;
  883. if (errno) {
  884. show_message (parent + ": " + strerror (errno));
  885. beep ();
  886. return;
  887. }
  888. if (!out.back().empty ())
  889. out.pop_back ();
  890. } else if (in[i] != "." && (!in[i].empty () || i < startempty)) {
  891. out.push_back (in[i]);
  892. }
  893. auto full_path = serialize_path (out);
  894. if (chdir (relativize (g.cwd, full_path).c_str ())) {
  895. show_message (strerror (errno));
  896. beep ();
  897. return;
  898. }
  899. auto old_cwd = g.cwd;
  900. level last {g.offset, g.cursor, old_cwd, g.entries[g.cursor].filename};
  901. g.cwd = full_path;
  902. reload (old_cwd);
  903. if (is_ancestor_dir (last.path, g.cwd)) {
  904. g.levels.push_back (last);
  905. g.offset = g.cursor = 0;
  906. } else {
  907. pop_levels ();
  908. }
  909. }
  910. // Roughly follows the POSIX description of the PWD environment variable
  911. fun initial_cwd () -> string {
  912. char cwd[4096] = ""; getcwd (cwd, sizeof cwd);
  913. const char *pwd = getenv ("PWD");
  914. if (!pwd || pwd[0] != '/' || strlen (pwd) >= PATH_MAX)
  915. return cwd;
  916. // Extra slashes shouldn't break anything for us
  917. vector<string> components;
  918. explode_path (pwd, components);
  919. for (const auto &i : components) {
  920. if (i == "." || i == "..")
  921. return cwd;
  922. }
  923. // Check if it "is an absolute pathname of the current working directory."
  924. // This particular method won't match on bind mounts, which is desired.
  925. char *real = realpath (pwd, nullptr);
  926. bool ok = real && !strcmp (cwd, real);
  927. free (real);
  928. return ok ? pwd : cwd;
  929. }
  930. fun choose (const entry &entry) {
  931. // Dive into directories and accessible symlinks to them
  932. if (!S_ISDIR (entry.info.st_mode)
  933. && !S_ISDIR (entry.target_info.st_mode)) {
  934. g.chosen = entry.filename;
  935. g.quitting = true;
  936. } else {
  937. change_dir (entry.filename);
  938. }
  939. }
  940. fun handle_editor (wint_t c) {
  941. auto i = g_input_actions.find (c);
  942. switch (i == g_input_actions.end () ? ACTION_NONE : i->second) {
  943. case ACTION_INPUT_CONFIRM:
  944. if (g.editor_on_confirm)
  945. g.editor_on_confirm ();
  946. // Fall-through
  947. case ACTION_INPUT_ABORT:
  948. g.editor_line.clear ();
  949. g.editor = 0;
  950. g.editor_on_change = nullptr;
  951. g.editor_on_confirm = nullptr;
  952. break;
  953. case ACTION_INPUT_B_DELETE:
  954. if (!g.editor_line.empty ())
  955. g.editor_line.erase (g.editor_line.length () - 1);
  956. break;
  957. default:
  958. if (c & (ALT | SYM)) {
  959. beep ();
  960. } else {
  961. g.editor_line += c;
  962. if (g.editor_on_change)
  963. g.editor_on_change ();
  964. }
  965. }
  966. }
  967. fun handle (wint_t c) -> bool {
  968. // If an editor is active, let it handle the key instead and eat it
  969. if (g.editor) {
  970. handle_editor (c);
  971. c = WEOF;
  972. }
  973. const auto &current = g.entries[g.cursor];
  974. auto i = g_normal_actions.find (c);
  975. switch (i == g_normal_actions.end () ? ACTION_NONE : i->second) {
  976. case ACTION_CHOOSE_FULL:
  977. g.chosen = g.cwd + "/" + current.filename;
  978. g.no_chdir = true;
  979. g.quitting = true;
  980. break;
  981. case ACTION_CHOOSE:
  982. choose (current);
  983. break;
  984. case ACTION_VIEW:
  985. view (current.filename);
  986. break;
  987. case ACTION_EDIT:
  988. edit (current.filename);
  989. break;
  990. case ACTION_HELP:
  991. show_help ();
  992. break;
  993. case ACTION_QUIT_NO_CHDIR:
  994. g.no_chdir = true;
  995. // Fall-through
  996. case ACTION_QUIT:
  997. g.quitting = true;
  998. break;
  999. case ACTION_SORT_LEFT:
  1000. g.sort_column = (g.sort_column + entry::COLUMNS - 1) % entry::COLUMNS;
  1001. g.sort_flash_ttl = 2;
  1002. reload (g.cwd);
  1003. break;
  1004. case ACTION_SORT_RIGHT:
  1005. g.sort_column = (g.sort_column + entry::COLUMNS + 1) % entry::COLUMNS;
  1006. g.sort_flash_ttl = 2;
  1007. reload (g.cwd);
  1008. break;
  1009. case ACTION_UP:
  1010. g.cursor--;
  1011. break;
  1012. case ACTION_DOWN:
  1013. g.cursor++;
  1014. break;
  1015. case ACTION_TOP:
  1016. g.cursor = 0;
  1017. break;
  1018. case ACTION_BOTTOM:
  1019. g.cursor = int (g.entries.size ()) - 1;
  1020. break;
  1021. case ACTION_PAGE_PREVIOUS:
  1022. g.cursor -= LINES;
  1023. break;
  1024. case ACTION_PAGE_NEXT:
  1025. g.cursor += LINES;
  1026. break;
  1027. case ACTION_SCROLL_DOWN:
  1028. g.offset++;
  1029. break;
  1030. case ACTION_SCROLL_UP:
  1031. g.offset--;
  1032. break;
  1033. case ACTION_CHDIR:
  1034. g.editor = L"chdir";
  1035. g.editor_on_confirm = [] {
  1036. change_dir (to_mb (g.editor_line));
  1037. };
  1038. break;
  1039. case ACTION_GO_START:
  1040. change_dir (g.start_dir);
  1041. break;
  1042. case ACTION_GO_HOME:
  1043. if (const auto *home = getenv ("HOME"))
  1044. change_dir (home);
  1045. else if (const auto *pw = getpwuid (getuid ()))
  1046. change_dir (pw->pw_dir);
  1047. break;
  1048. case ACTION_SEARCH:
  1049. g.editor = L"search";
  1050. g.editor_on_change = [] {
  1051. search (g.editor_line);
  1052. };
  1053. g.editor_on_confirm = [] {
  1054. choose (g.entries[g.cursor]);
  1055. };
  1056. break;
  1057. case ACTION_RENAME_PREFILL:
  1058. g.editor_line = to_wide (current.filename);
  1059. // Fall-through
  1060. case ACTION_RENAME:
  1061. g.editor = L"rename";
  1062. g.editor_on_confirm = [] {
  1063. auto mb = to_mb (g.editor_line);
  1064. rename (g.entries[g.cursor].filename.c_str (), mb.c_str ());
  1065. reload (g.cwd);
  1066. };
  1067. break;
  1068. case ACTION_TOGGLE_FULL:
  1069. g.full_view = !g.full_view;
  1070. break;
  1071. case ACTION_REVERSE_SORT:
  1072. g.reverse_sort = !g.reverse_sort;
  1073. reload (g.cwd);
  1074. break;
  1075. case ACTION_SHOW_HIDDEN:
  1076. g.show_hidden = !g.show_hidden;
  1077. reload (g.cwd);
  1078. break;
  1079. case ACTION_REDRAW:
  1080. clear ();
  1081. break;
  1082. case ACTION_RELOAD:
  1083. reload (g.cwd);
  1084. break;
  1085. default:
  1086. if (c != KEY (RESIZE) && c != WEOF)
  1087. beep ();
  1088. }
  1089. fix_cursor_and_offset ();
  1090. update ();
  1091. return !g.quitting;
  1092. }
  1093. fun inotify_check () {
  1094. // Only provide simple indication that contents might have changed
  1095. char buf[4096]; ssize_t len;
  1096. bool changed = false;
  1097. while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) {
  1098. const inotify_event *e;
  1099. for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) {
  1100. e = (const inotify_event *) buf;
  1101. if (e->wd == g.inotify_wd)
  1102. changed = g.out_of_date = true;
  1103. }
  1104. }
  1105. if (changed)
  1106. update ();
  1107. }
  1108. fun decode_ansi_sgr (const vector<string> &v) -> chtype {
  1109. vector<int> args;
  1110. for (const auto &arg : v) {
  1111. char *end; unsigned long ul = strtoul (arg.c_str (), &end, 10);
  1112. if (*end != '\0' || ul > 255)
  1113. return 0;
  1114. args.push_back (ul);
  1115. }
  1116. chtype result = 0; int fg = -1, bg = -1;
  1117. for (size_t i = 0; i < args.size (); i++) {
  1118. auto arg = args[i];
  1119. if (arg == 0) {
  1120. result = 0; fg = -1; bg = -1;
  1121. } else if (arg == 1) {
  1122. result |= A_BOLD;
  1123. } else if (arg == 4) {
  1124. result |= A_UNDERLINE;
  1125. } else if (arg == 5) {
  1126. result |= A_BLINK;
  1127. } else if (arg == 7) {
  1128. result |= A_REVERSE;
  1129. } else if (arg >= 30 && arg <= 37) {
  1130. fg = arg - 30;
  1131. } else if (arg >= 40 && arg <= 47) {
  1132. bg = arg - 40;
  1133. // Anything other than indexed colours will be rejected completely
  1134. } else if (arg == 38 && (i += 2) < args.size ()) {
  1135. if (args[i - 1] != 5 || (fg = args[i]) >= COLORS)
  1136. return 0;
  1137. } else if (arg == 48 && (i += 2) < args.size ()) {
  1138. if (args[i - 1] != 5 || (bg = args[i]) >= COLORS)
  1139. return 0;
  1140. }
  1141. }
  1142. if (fg != -1 || bg != -1)
  1143. result |= COLOR_PAIR (allocate_pair (fg, bg));
  1144. return result;
  1145. }
  1146. fun load_ls_colors (vector<string> colors) {
  1147. map<string, chtype> attrs;
  1148. for (const auto &pair : colors) {
  1149. auto equal = pair.find ('=');
  1150. if (equal == string::npos)
  1151. continue;
  1152. auto key = pair.substr (0, equal), value = pair.substr (equal + 1);
  1153. if (key != g_ls_colors[LS_SYMLINK]
  1154. || !(g.ls_symlink_as_target = value == "target"))
  1155. attrs[key] = decode_ansi_sgr (split (value, ";"));
  1156. }
  1157. for (int i = 0; i < LS_COUNT; i++) {
  1158. auto m = attrs.find (g_ls_colors[i]);
  1159. if (m != attrs.end ())
  1160. g.ls_colors[i] = m->second;
  1161. }
  1162. for (const auto &pair : attrs) {
  1163. if (pair.first.substr (0, 2) == "*.")
  1164. g.ls_exts[pair.first.substr (2)] = pair.second;
  1165. }
  1166. }
  1167. fun load_colors () {
  1168. // Bail out on dumb terminals, there's not much one can do about them
  1169. if (!has_colors () || start_color () == ERR || use_default_colors () == ERR)
  1170. return;
  1171. if (const char *colors = getenv ("LS_COLORS"))
  1172. load_ls_colors (split (colors, ":"));
  1173. auto config = xdg_config_find ("look");
  1174. if (!config)
  1175. return;
  1176. vector<string> tokens;
  1177. while (parse_line (*config, tokens)) {
  1178. if (tokens.empty ())
  1179. continue;
  1180. auto name = shift (tokens);
  1181. for (int i = 0; i < g.AT_COUNT; i++)
  1182. if (name == g.attr_names[i])
  1183. g.attrs[i] = decode_attrs (tokens);
  1184. }
  1185. }
  1186. fun read_key (wint_t &c) -> bool {
  1187. int res = get_wch (&c);
  1188. if (res == ERR)
  1189. return false;
  1190. wint_t metafied{};
  1191. if (c == 27 && (res = get_wch (&metafied)) != ERR)
  1192. c = ALT | metafied;
  1193. if (res == KEY_CODE_YES)
  1194. c |= SYM;
  1195. return true;
  1196. }
  1197. fun parse_key (const string &key_name) -> wint_t {
  1198. wint_t c{};
  1199. auto p = key_name.c_str ();
  1200. if (!strncmp (p, "M-", 2)) {
  1201. c |= ALT;
  1202. p += 2;
  1203. }
  1204. if (!strncmp (p, "C-", 2)) {
  1205. p += 2;
  1206. if (*p < 32) {
  1207. cerr << "bindings: invalid combination: " << key_name << endl;
  1208. return WEOF;
  1209. }
  1210. c |= CTRL *p;
  1211. p += 1;
  1212. } else if (g.name_to_key.count (p)) {
  1213. return c | g.name_to_key.at (p);
  1214. } else {
  1215. wchar_t w; mbstate_t mb {};
  1216. auto len = strlen (p) + 1, res = mbrtowc (&w, p, len, &mb);
  1217. if (res == 0) {
  1218. cerr << "bindings: missing key name: " << key_name << endl;
  1219. return WEOF;
  1220. }
  1221. if (res == size_t (-1) || res == size_t (-2)) {
  1222. cerr << "bindings: invalid encoding: " << key_name << endl;
  1223. return WEOF;
  1224. }
  1225. c |= w;
  1226. p += res;
  1227. }
  1228. if (*p) {
  1229. cerr << "key name has unparsable trailing part: " << key_name << endl;
  1230. return WEOF;
  1231. }
  1232. return c;
  1233. }
  1234. fun learn_named_key (const string &name, wint_t key) {
  1235. g.name_to_key[g.key_to_name[key] = name] = key;
  1236. }
  1237. fun load_bindings () {
  1238. learn_named_key ("space", ' ');
  1239. learn_named_key ("escape", 0x1b);
  1240. for (int kc = KEY_MIN; kc < KEY_MAX; kc++) {
  1241. const char *name = keyname (kc);
  1242. if (!name)
  1243. continue;
  1244. if (!strncmp (name, "KEY_", 4))
  1245. name += 4;
  1246. string filtered;
  1247. for (; *name; name++) {
  1248. if (*name != '(' && *name != ')')
  1249. filtered += *name;
  1250. }
  1251. learn_named_key (filtered, SYM | kc);
  1252. }
  1253. auto config = xdg_config_find ("bindings");
  1254. if (!config)
  1255. return;
  1256. // Stringization in the preprocessor is a bit limited, we want lisp-case
  1257. map<string, action> actions;
  1258. int a = 0;
  1259. for (auto p : g_action_names) {
  1260. string name;
  1261. for (; *p; p++)
  1262. name += *p == '_' ? '-' : *p + 'a' - 'A';
  1263. g.action_names[a] = name;
  1264. actions[name] = action (a++);
  1265. }
  1266. vector<string> tokens;
  1267. while (parse_line (*config, tokens)) {
  1268. if (tokens.empty ())
  1269. continue;
  1270. if (tokens.size () < 3) {
  1271. cerr << "bindings: expected: context binding action";
  1272. continue;
  1273. }
  1274. auto context = tokens[0], key_name = tokens[1], action = tokens[2];
  1275. auto m = g_binding_contexts.find (context);
  1276. if (m == g_binding_contexts.end ()) {
  1277. cerr << "bindings: invalid context: " << context << endl;
  1278. continue;
  1279. }
  1280. wint_t c = parse_key (key_name);
  1281. if (c == WEOF)
  1282. continue;
  1283. auto i = actions.find (action);
  1284. if (i == actions.end ()) {
  1285. cerr << "bindings: invalid action: " << action << endl;
  1286. continue;
  1287. }
  1288. (*m->second)[c] = i->second;
  1289. }
  1290. }
  1291. fun load_history_level (const vector<string> &v) {
  1292. if (v.size () != 7)
  1293. return;
  1294. // Not checking the hostname and parent PID right now since we can't merge
  1295. g.levels.push_back ({stoi (v.at (4)), stoi (v.at (5)), v.at (3), v.at (6)});
  1296. }
  1297. fun load_config () {
  1298. auto config = xdg_config_find ("config");
  1299. if (!config)
  1300. return;
  1301. vector<string> tokens;
  1302. while (parse_line (*config, tokens)) {
  1303. if (tokens.empty ())
  1304. continue;
  1305. if (tokens.front () == "full-view" && tokens.size () > 1)
  1306. g.full_view = tokens.at (1) == "1";
  1307. else if (tokens.front () == "gravity" && tokens.size () > 1)
  1308. g.gravity = tokens.at (1) == "1";
  1309. else if (tokens.front () == "reverse-sort" && tokens.size () > 1)
  1310. g.reverse_sort = tokens.at (1) == "1";
  1311. else if (tokens.front () == "show-hidden" && tokens.size () > 1)
  1312. g.show_hidden = tokens.at (1) == "1";
  1313. else if (tokens.front () == "sort-column" && tokens.size () > 1)
  1314. g.sort_column = stoi (tokens.at (1));
  1315. else if (tokens.front () == "history")
  1316. load_history_level (tokens);
  1317. }
  1318. }
  1319. fun save_config () {
  1320. auto config = xdg_config_write ("config");
  1321. if (!config)
  1322. return;
  1323. write_line (*config, {"full-view", g.full_view ? "1" : "0"});
  1324. write_line (*config, {"gravity", g.gravity ? "1" : "0"});
  1325. write_line (*config, {"reverse-sort", g.reverse_sort ? "1" : "0"});
  1326. write_line (*config, {"show-hidden", g.show_hidden ? "1" : "0"});
  1327. write_line (*config, {"sort-column", to_string (g.sort_column)});
  1328. char hostname[256];
  1329. if (gethostname (hostname, sizeof hostname))
  1330. *hostname = 0;
  1331. auto ppid = std::to_string (getppid ());
  1332. for (auto i = g.levels.begin (); i != g.levels.end (); i++)
  1333. write_line (*config, {"history", hostname, ppid, i->path,
  1334. to_string (i->offset), to_string (i->cursor), i->filename});
  1335. write_line (*config, {"history", hostname, ppid, g.cwd,
  1336. to_string (g.offset), to_string (g.cursor),
  1337. g.entries[g.cursor].filename});
  1338. }
  1339. int main (int argc, char *argv[]) {
  1340. (void) argc;
  1341. (void) argv;
  1342. // That bitch zle closes stdin before exec without redirection
  1343. (void) close (STDIN_FILENO);
  1344. if (open ("/dev/tty", O_RDWR)) {
  1345. cerr << "cannot open tty" << endl;
  1346. return 1;
  1347. }
  1348. // Save the original stdout and force ncurses to use the terminal directly
  1349. auto output_fd = dup (STDOUT_FILENO);
  1350. dup2 (STDIN_FILENO, STDOUT_FILENO);
  1351. // So that the neither us nor our children stop on tcsetpgrp()
  1352. signal (SIGTTOU, SIG_IGN);
  1353. if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) {
  1354. cerr << "cannot initialize inotify" << endl;
  1355. return 1;
  1356. }
  1357. locale::global (locale (""));
  1358. load_bindings ();
  1359. load_config ();
  1360. if (!initscr () || cbreak () == ERR || noecho () == ERR || nonl () == ERR) {
  1361. cerr << "cannot initialize screen" << endl;
  1362. return 1;
  1363. }
  1364. load_colors ();
  1365. g.start_dir = g.cwd = initial_cwd ();
  1366. reload (g.cwd);
  1367. pop_levels ();
  1368. update ();
  1369. // Invoking keypad() earlier would make ncurses flush its output buffer,
  1370. // which would worsen start-up flickering
  1371. if (halfdelay (1) == ERR || keypad (stdscr, TRUE) == ERR) {
  1372. endwin ();
  1373. cerr << "cannot configure input" << endl;
  1374. return 1;
  1375. }
  1376. wint_t c;
  1377. while (!read_key (c) || handle (c)) {
  1378. inotify_check ();
  1379. if (g.sort_flash_ttl && !--g.sort_flash_ttl)
  1380. update ();
  1381. if (g.message_ttl && !--g.message_ttl) {
  1382. g.message.clear ();
  1383. update ();
  1384. }
  1385. }
  1386. endwin ();
  1387. save_config ();
  1388. // Presumably it is going to end up as an argument, so quote it
  1389. if (!g.chosen.empty ())
  1390. g.chosen = shell_escape (g.chosen);
  1391. // We can't portably create a standard stream from an FD, so modify the FD
  1392. dup2 (output_fd, STDOUT_FILENO);
  1393. if (g.cwd != g.start_dir && !g.no_chdir)
  1394. cout << "local cd=" << shell_escape (g.cwd) << endl;
  1395. else
  1396. cout << "local cd=" << endl;
  1397. cout << "local insert=" << shell_escape (g.chosen) << endl;
  1398. return 0;
  1399. }