Hex viewer
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.

hex.c 34KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295
  1. /*
  2. * hex -- a very simple hex viewer
  3. *
  4. * Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
  5. * All rights reserved.
  6. *
  7. * Permission to use, copy, modify, and/or distribute this software for any
  8. * purpose with or without fee is hereby granted, provided that the above
  9. * copyright notice and this permission notice appear in all copies.
  10. *
  11. * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  12. * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  13. * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
  14. * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  15. * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
  16. * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  17. * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  18. *
  19. */
  20. #include "config.h"
  21. // We "need" to have an enum for attributes before including liberty.
  22. // Avoiding colours in the defaults here in order to support dumb terminals.
  23. #define ATTRIBUTE_TABLE(XX) \
  24. XX( HEADER, "header", -1, -1, 0 ) \
  25. XX( HIGHLIGHT, "highlight", -1, -1, A_BOLD ) \
  26. /* Bar */ \
  27. XX( BAR, "bar", -1, -1, A_REVERSE ) \
  28. XX( BAR_ACTIVE, "bar_active", -1, -1, A_UNDERLINE ) \
  29. /* Listview */ \
  30. XX( EVEN, "even", -1, -1, 0 ) \
  31. XX( ODD, "odd", -1, -1, 0 ) \
  32. XX( SELECTION, "selection", -1, -1, A_REVERSE ) \
  33. /* These are for debugging only */ \
  34. XX( WARNING, "warning", 3, -1, 0 ) \
  35. XX( ERROR, "error", 1, -1, 0 )
  36. enum
  37. {
  38. #define XX(name, config, fg_, bg_, attrs_) ATTRIBUTE_ ## name,
  39. ATTRIBUTE_TABLE (XX)
  40. #undef XX
  41. ATTRIBUTE_COUNT
  42. };
  43. // My battle-tested C framework acting as a GLib replacement. Its one big
  44. // disadvantage is missing support for i18n but that can eventually be added
  45. // as an optional feature. Localised applications look super awkward, though.
  46. // User data for logger functions to enable formatted logging
  47. #define print_fatal_data ((void *) ATTRIBUTE_ERROR)
  48. #define print_error_data ((void *) ATTRIBUTE_ERROR)
  49. #define print_warning_data ((void *) ATTRIBUTE_WARNING)
  50. #define LIBERTY_WANT_POLLER
  51. #define LIBERTY_WANT_ASYNC
  52. #define LIBERTY_WANT_PROTO_HTTP
  53. #include "liberty/liberty.c"
  54. #include <locale.h>
  55. #include <termios.h>
  56. #ifndef TIOCGWINSZ
  57. #include <sys/ioctl.h>
  58. #endif // ! TIOCGWINSZ
  59. #include <ncurses.h>
  60. // ncurses is notoriously retarded for input handling, we need something
  61. // different if only to receive mouse events reliably.
  62. #include "termo.h"
  63. // It is surprisingly hard to find a good library to handle Unicode shenanigans,
  64. // and there's enough of those for it to be impractical to reimplement them.
  65. //
  66. // GLib ICU libunistring utf8proc
  67. // Decently sized . . x x
  68. // Grapheme breaks . x . x
  69. // Character width x . x x
  70. // Locale handling . . x .
  71. // Liberal license . x . x
  72. //
  73. // Also note that the ICU API is icky and uses UTF-16 for its primary encoding.
  74. //
  75. // Currently we're chugging along with libunistring but utf8proc seems viable.
  76. // Non-Unicode locales can mostly be handled with simple iconv like in sdtui.
  77. // Similarly grapheme breaks can be guessed at using character width (a basic
  78. // test here is Zalgo text).
  79. //
  80. // None of this is ever going to work too reliably anyway because terminals
  81. // and Unicode don't go awfully well together. In particular, character cell
  82. // devices have some problems with double-wide characters.
  83. #include <unistr.h>
  84. #include <uniwidth.h>
  85. #include <uniconv.h>
  86. #include <unicase.h>
  87. #define APP_TITLE PROGRAM_NAME ///< Left top corner
  88. // --- Utilities ---------------------------------------------------------------
  89. // The standard endwin/refresh sequence makes the terminal flicker
  90. static void
  91. update_curses_terminal_size (void)
  92. {
  93. #if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ)
  94. struct winsize size;
  95. if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
  96. {
  97. char *row = getenv ("LINES");
  98. char *col = getenv ("COLUMNS");
  99. unsigned long tmp;
  100. resizeterm (
  101. (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row,
  102. (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col);
  103. }
  104. #else // HAVE_RESIZETERM && TIOCGWINSZ
  105. endwin ();
  106. refresh ();
  107. #endif // HAVE_RESIZETERM && TIOCGWINSZ
  108. }
  109. static char *
  110. latin1_to_utf8 (const char *latin1)
  111. {
  112. struct str converted;
  113. str_init (&converted);
  114. while (*latin1)
  115. {
  116. uint8_t c = *latin1++;
  117. if (c < 0x80)
  118. str_append_c (&converted, c);
  119. else
  120. {
  121. str_append_c (&converted, 0xC0 | (c >> 6));
  122. str_append_c (&converted, 0x80 | (c & 0x3F));
  123. }
  124. }
  125. return str_steal (&converted);
  126. }
  127. // --- Application -------------------------------------------------------------
  128. // Function names are prefixed mostly because of curses which clutters the
  129. // global namespace and makes it harder to distinguish what functions relate to.
  130. struct attrs
  131. {
  132. short fg; ///< Foreground colour index
  133. short bg; ///< Background colour index
  134. chtype attrs; ///< Other attributes
  135. };
  136. // Basically a container for most of the globals; no big sense in handing
  137. // around a pointer to this, hence it is a simple global variable as well.
  138. // There is enough global state as it is.
  139. static struct app_context
  140. {
  141. // Event loop:
  142. struct poller poller; ///< Poller
  143. bool quitting; ///< Quit signal for the event loop
  144. bool polling; ///< The event loop is running
  145. struct poller_fd tty_event; ///< Terminal input event
  146. struct poller_fd signal_event; ///< Signal FD event
  147. // Data:
  148. struct config config; ///< Program configuration
  149. char *filename; ///< Target filename
  150. uint8_t *data; ///< Target data
  151. uint64_t data_len; ///< Length of the data
  152. uint64_t data_offset; ///< Offset of the data within the file
  153. uint64_t data_cursor; ///< Current position within the data
  154. // TODO: get rid of this as it can be computed from "data*"
  155. size_t item_count; ///< Total item count
  156. int item_top; ///< Index of the topmost item
  157. int item_selected; ///< Index of the selected item
  158. // Emulated widgets:
  159. // TODO: make this the footer;
  160. // remove this, we know how high the footer is
  161. int header_height; ///< Height of the header
  162. struct poller_idle refresh_event; ///< Refresh the screen
  163. // Terminal:
  164. termo_t *tk; ///< termo handle
  165. struct poller_timer tk_timer; ///< termo timeout timer
  166. bool locale_is_utf8; ///< The locale is Unicode
  167. struct attrs attrs[ATTRIBUTE_COUNT];
  168. }
  169. g_ctx;
  170. /// Shortcut to retrieve named terminal attributes
  171. #define APP_ATTR(name) g_ctx.attrs[ATTRIBUTE_ ## name].attrs
  172. // --- Configuration -----------------------------------------------------------
  173. static struct config_schema g_config_colors[] =
  174. {
  175. #define XX(name_, config, fg_, bg_, attrs_) \
  176. { .name = config, .type = CONFIG_ITEM_STRING },
  177. ATTRIBUTE_TABLE (XX)
  178. #undef XX
  179. {}
  180. };
  181. static const char *
  182. get_config_string (struct config_item *root, const char *key)
  183. {
  184. struct config_item *item = config_item_get (root, key, NULL);
  185. hard_assert (item);
  186. if (item->type == CONFIG_ITEM_NULL)
  187. return NULL;
  188. hard_assert (config_item_type_is_string (item->type));
  189. return item->value.string.str;
  190. }
  191. /// Load configuration for a color using a subset of git config colors
  192. static void
  193. app_load_color (struct config_item *subtree, const char *name, int id)
  194. {
  195. const char *value = get_config_string (subtree, name);
  196. if (!value)
  197. return;
  198. struct str_vector v;
  199. str_vector_init (&v);
  200. cstr_split (value, " ", true, &v);
  201. int colors = 0;
  202. struct attrs attrs = { -1, -1, 0 };
  203. for (char **it = v.vector; *it; it++)
  204. {
  205. char *end = NULL;
  206. long n = strtol (*it, &end, 10);
  207. if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX)
  208. {
  209. if (colors == 0) attrs.fg = n;
  210. if (colors == 1) attrs.bg = n;
  211. colors++;
  212. }
  213. else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD;
  214. else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM;
  215. else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE;
  216. else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK;
  217. else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE;
  218. #ifdef A_ITALIC
  219. else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC;
  220. #endif // A_ITALIC
  221. }
  222. str_vector_free (&v);
  223. g_ctx.attrs[id] = attrs;
  224. }
  225. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  226. static void
  227. load_config_colors (struct config_item *subtree, void *user_data)
  228. {
  229. config_schema_apply_to_object (g_config_colors, subtree, user_data);
  230. // The attributes cannot be changed dynamically right now, so it doesn't
  231. // make much sense to make use of "on_change" callbacks either.
  232. // For simplicity, we should reload the entire table on each change anyway.
  233. #define XX(name, config, fg_, bg_, attrs_) \
  234. app_load_color (subtree, config, ATTRIBUTE_ ## name);
  235. ATTRIBUTE_TABLE (XX)
  236. #undef XX
  237. }
  238. static void
  239. app_load_configuration (void)
  240. {
  241. struct config *config = &g_ctx.config;
  242. config_register_module (config, "colors", load_config_colors, NULL);
  243. // Bootstrap configuration, so that we can access schema items at all
  244. config_load (config, config_item_object ());
  245. char *filename = resolve_filename
  246. (PROGRAM_NAME ".conf", resolve_relative_config_filename);
  247. if (!filename)
  248. return;
  249. struct error *e = NULL;
  250. struct config_item *root = config_read_from_file (filename, &e);
  251. free (filename);
  252. if (e)
  253. {
  254. print_error ("error loading configuration: %s", e->message);
  255. error_free (e);
  256. exit (EXIT_FAILURE);
  257. }
  258. if (root)
  259. {
  260. config_load (&g_ctx.config, root);
  261. config_schema_call_changed (g_ctx.config.root);
  262. }
  263. }
  264. // --- Application -------------------------------------------------------------
  265. static void
  266. app_init_attributes (void)
  267. {
  268. #define XX(name, config, fg_, bg_, attrs_) \
  269. g_ctx.attrs[ATTRIBUTE_ ## name].fg = fg_; \
  270. g_ctx.attrs[ATTRIBUTE_ ## name].bg = bg_; \
  271. g_ctx.attrs[ATTRIBUTE_ ## name].attrs = attrs_;
  272. ATTRIBUTE_TABLE (XX)
  273. #undef XX
  274. }
  275. static void
  276. app_init_context (void)
  277. {
  278. poller_init (&g_ctx.poller);
  279. config_init (&g_ctx.config);
  280. // This is also approximately what libunistring does internally,
  281. // since the locale name is canonicalized by locale_charset().
  282. // Note that non-Unicode locales are handled pretty inefficiently.
  283. g_ctx.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8");
  284. app_init_attributes ();
  285. }
  286. static void
  287. app_init_terminal (void)
  288. {
  289. TERMO_CHECK_VERSION;
  290. if (!(g_ctx.tk = termo_new (STDIN_FILENO, NULL, 0)))
  291. abort ();
  292. if (!initscr () || nonl () == ERR)
  293. abort ();
  294. // Disable cursor, we're not going to use it most of the time
  295. curs_set (0);
  296. // By default we don't use any colors so they're not required...
  297. if (start_color () == ERR
  298. || use_default_colors () == ERR
  299. || COLOR_PAIRS <= ATTRIBUTE_COUNT)
  300. return;
  301. for (int a = 0; a < ATTRIBUTE_COUNT; a++)
  302. {
  303. // ...thus we can reset back to defaults even after initializing some
  304. if (g_ctx.attrs[a].fg >= COLORS || g_ctx.attrs[a].fg < -1
  305. || g_ctx.attrs[a].bg >= COLORS || g_ctx.attrs[a].bg < -1)
  306. {
  307. app_init_attributes ();
  308. return;
  309. }
  310. init_pair (a + 1, g_ctx.attrs[a].fg, g_ctx.attrs[a].bg);
  311. g_ctx.attrs[a].attrs |= COLOR_PAIR (a + 1);
  312. }
  313. }
  314. static void
  315. app_free_context (void)
  316. {
  317. config_free (&g_ctx.config);
  318. poller_free (&g_ctx.poller);
  319. free (g_ctx.filename);
  320. free (g_ctx.data);
  321. if (g_ctx.tk)
  322. termo_destroy (g_ctx.tk);
  323. }
  324. static void
  325. app_quit (void)
  326. {
  327. g_ctx.quitting = true;
  328. g_ctx.polling = false;
  329. }
  330. static bool
  331. app_is_character_in_locale (ucs4_t ch)
  332. {
  333. // Avoid the overhead joined with calling iconv() for all characters.
  334. if (g_ctx.locale_is_utf8)
  335. return true;
  336. // The library really creates a new conversion object every single time
  337. // and doesn't provide any smarter APIs. Luckily, most users use UTF-8.
  338. size_t len;
  339. char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error,
  340. &ch, 1, NULL, NULL, &len);
  341. if (!tmp)
  342. return false;
  343. free (tmp);
  344. return true;
  345. }
  346. // --- Terminal output ---------------------------------------------------------
  347. // Necessary abstraction to simplify aligned, formatted character output
  348. struct row_char
  349. {
  350. ucs4_t c; ///< Unicode codepoint
  351. chtype attrs; ///< Special attributes
  352. int width; ///< How many cells this takes
  353. };
  354. struct row_buffer
  355. {
  356. struct row_char *chars; ///< Characters
  357. size_t chars_len; ///< Character count
  358. size_t chars_alloc; ///< Characters allocated
  359. int total_width; ///< Total width of all characters
  360. };
  361. static void
  362. row_buffer_init (struct row_buffer *self)
  363. {
  364. memset (self, 0, sizeof *self);
  365. self->chars = xcalloc (sizeof *self->chars, (self->chars_alloc = 256));
  366. }
  367. static void
  368. row_buffer_free (struct row_buffer *self)
  369. {
  370. free (self->chars);
  371. }
  372. /// Replace invalid chars and push all codepoints to the array w/ attributes.
  373. static void
  374. row_buffer_append (struct row_buffer *self, const char *str, chtype attrs)
  375. {
  376. // The encoding is only really used internally for some corner cases
  377. const char *encoding = locale_charset ();
  378. // Note that this function is a hotspot, try to keep it decently fast
  379. struct row_char current = { .attrs = attrs };
  380. struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 };
  381. const uint8_t *next = (const uint8_t *) str;
  382. while ((next = u8_next (&current.c, next)))
  383. {
  384. if (self->chars_len >= self->chars_alloc)
  385. self->chars = xreallocarray (self->chars,
  386. sizeof *self->chars, (self->chars_alloc <<= 1));
  387. current.width = uc_width (current.c, encoding);
  388. if (current.width < 0 || !app_is_character_in_locale (current.c))
  389. current = invalid;
  390. self->chars[self->chars_len++] = current;
  391. self->total_width += current.width;
  392. }
  393. }
  394. static void
  395. row_buffer_addv (struct row_buffer *self, const char *s, ...)
  396. ATTRIBUTE_SENTINEL;
  397. static void
  398. row_buffer_addv (struct row_buffer *self, const char *s, ...)
  399. {
  400. va_list ap;
  401. va_start (ap, s);
  402. while (s)
  403. {
  404. row_buffer_append (self, s, va_arg (ap, chtype));
  405. s = va_arg (ap, const char *);
  406. }
  407. va_end (ap);
  408. }
  409. /// Pop as many codepoints as needed to free up "space" character cells.
  410. /// Given the suffix nature of combining marks, this should work pretty fine.
  411. static int
  412. row_buffer_pop_cells (struct row_buffer *self, int space)
  413. {
  414. int made = 0;
  415. while (self->chars_len && made < space)
  416. made += self->chars[--self->chars_len].width;
  417. self->total_width -= made;
  418. return made;
  419. }
  420. static void
  421. row_buffer_space (struct row_buffer *self, int width, chtype attrs)
  422. {
  423. if (width < 0)
  424. return;
  425. while (self->chars_len + width >= self->chars_alloc)
  426. self->chars = xreallocarray (self->chars,
  427. sizeof *self->chars, (self->chars_alloc <<= 1));
  428. struct row_char space = { .attrs = attrs, .c = ' ', .width = 1 };
  429. self->total_width += width;
  430. while (width-- > 0)
  431. self->chars[self->chars_len++] = space;
  432. }
  433. static void
  434. row_buffer_ellipsis (struct row_buffer *self, int target)
  435. {
  436. if (self->total_width <= target
  437. || !row_buffer_pop_cells (self, self->total_width - target))
  438. return;
  439. // We use attributes from the last character we've removed,
  440. // assuming that we don't shrink the array (and there's no real need)
  441. ucs4_t ellipsis = L'…';
  442. if (app_is_character_in_locale (ellipsis))
  443. {
  444. if (self->total_width >= target)
  445. row_buffer_pop_cells (self, 1);
  446. if (self->total_width + 1 <= target)
  447. row_buffer_append (self, "…", self->chars[self->chars_len].attrs);
  448. }
  449. else if (target >= 3)
  450. {
  451. if (self->total_width >= target)
  452. row_buffer_pop_cells (self, 3);
  453. if (self->total_width + 3 <= target)
  454. row_buffer_append (self, "...", self->chars[self->chars_len].attrs);
  455. }
  456. }
  457. static void
  458. row_buffer_align (struct row_buffer *self, int target, chtype attrs)
  459. {
  460. row_buffer_ellipsis (self, target);
  461. row_buffer_space (self, target - self->total_width, attrs);
  462. }
  463. static void
  464. row_buffer_print (uint32_t *ucs4, chtype attrs)
  465. {
  466. // This assumes that we can reset the attribute set without consequences
  467. char *str = u32_strconv_to_locale (ucs4);
  468. if (str)
  469. {
  470. attrset (attrs);
  471. addstr (str);
  472. attrset (0);
  473. free (str);
  474. }
  475. }
  476. static void
  477. row_buffer_flush (struct row_buffer *self)
  478. {
  479. if (!self->chars_len)
  480. return;
  481. // We only NUL-terminate the chunks because of the libunistring API
  482. uint32_t chunk[self->chars_len + 1], *insertion_point = chunk;
  483. for (size_t i = 0; i < self->chars_len; i++)
  484. {
  485. struct row_char *iter = self->chars + i;
  486. if (i && iter[0].attrs != iter[-1].attrs)
  487. {
  488. row_buffer_print (chunk, iter[-1].attrs);
  489. insertion_point = chunk;
  490. }
  491. *insertion_point++ = iter->c;
  492. *insertion_point = 0;
  493. }
  494. row_buffer_print (chunk, self->chars[self->chars_len - 1].attrs);
  495. }
  496. // --- Rendering ---------------------------------------------------------------
  497. static void
  498. app_invalidate (void)
  499. {
  500. poller_idle_set (&g_ctx.refresh_event);
  501. }
  502. static void
  503. app_flush_buffer (struct row_buffer *buf, int width, chtype attrs)
  504. {
  505. row_buffer_align (buf, width, attrs);
  506. row_buffer_flush (buf);
  507. row_buffer_free (buf);
  508. }
  509. /// Write the given UTF-8 string padded with spaces.
  510. /// @param[in] attrs Text attributes for the text, including padding.
  511. static void
  512. app_write_line (const char *str, chtype attrs)
  513. {
  514. struct row_buffer buf;
  515. row_buffer_init (&buf);
  516. row_buffer_append (&buf, str, attrs);
  517. app_flush_buffer (&buf, COLS, attrs);
  518. }
  519. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  520. static void
  521. app_flush_header (struct row_buffer *buf, chtype attrs)
  522. {
  523. move (g_ctx.header_height++, 0);
  524. app_flush_buffer (buf, COLS, attrs);
  525. }
  526. static void
  527. app_draw_status (void)
  528. {
  529. // XXX: can we get rid of this and still make it look acceptable?
  530. chtype a_normal = APP_ATTR (HEADER);
  531. chtype a_highlight = APP_ATTR (HIGHLIGHT);
  532. struct row_buffer buf;
  533. row_buffer_init (&buf);
  534. // ...
  535. app_flush_header (&buf, a_normal);
  536. }
  537. static void
  538. app_draw_header (void)
  539. {
  540. // TODO: call app_fix_view_range() if it changes from the previous value
  541. g_ctx.header_height = 0;
  542. if (true)
  543. app_draw_status ();
  544. else
  545. {
  546. move (g_ctx.header_height++, 0);
  547. app_write_line ("Connecting to MPD...", APP_ATTR (HEADER));
  548. }
  549. // XXX: can we get rid of this and still make it look acceptable?
  550. chtype a_normal = APP_ATTR (BAR);
  551. chtype a_active = APP_ATTR (BAR_ACTIVE);
  552. struct row_buffer buf;
  553. row_buffer_init (&buf);
  554. // TODO: print the filename here instead
  555. row_buffer_append (&buf, APP_TITLE, a_normal);
  556. row_buffer_append (&buf, " ", a_normal);
  557. // TODO: endian indication, position indication
  558. app_flush_header (&buf, a_normal);
  559. }
  560. static int
  561. app_visible_items (void)
  562. {
  563. // This may eventually include a header bar and/or a status bar
  564. return MAX (0, LINES - g_ctx.header_height);
  565. }
  566. static void
  567. app_draw_view (void)
  568. {
  569. move (g_ctx.header_height, 0);
  570. clrtobot ();
  571. int view_width = COLS;
  572. int to_show = MIN (LINES - g_ctx.header_height,
  573. (int) g_ctx.item_count - g_ctx.item_top);
  574. for (int row = 0; row < to_show; row++)
  575. {
  576. int item_index = g_ctx.item_top + row;
  577. int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
  578. if (item_index == g_ctx.item_selected)
  579. row_attrs = APP_ATTR (SELECTION);
  580. struct row_buffer buf;
  581. row_buffer_init (&buf);
  582. // TODO: draw the row using view_width
  583. // Combine attributes used by the handler with the defaults.
  584. // Avoiding attrset() because of row_buffer_flush().
  585. for (size_t i = 0; i < buf.chars_len; i++)
  586. {
  587. chtype *attrs = &buf.chars[i].attrs;
  588. if (item_index == g_ctx.item_selected)
  589. *attrs = (*attrs & ~(A_COLOR | A_REVERSE)) | row_attrs;
  590. else if ((*attrs & A_COLOR) && (row_attrs & A_COLOR))
  591. *attrs |= (row_attrs & ~A_COLOR);
  592. else
  593. *attrs |= row_attrs;
  594. }
  595. move (g_ctx.header_height + row, 0);
  596. app_flush_buffer (&buf, view_width, row_attrs);
  597. }
  598. }
  599. static void
  600. app_on_refresh (void *user_data)
  601. {
  602. (void) user_data;
  603. poller_idle_reset (&g_ctx.refresh_event);
  604. app_draw_header ();
  605. app_draw_view ();
  606. refresh ();
  607. }
  608. // --- Actions -----------------------------------------------------------------
  609. /// Checks what items are visible and returns if fixes were needed
  610. static bool
  611. app_fix_view_range (void)
  612. {
  613. if (g_ctx.item_top < 0)
  614. {
  615. g_ctx.item_top = 0;
  616. app_invalidate ();
  617. return false;
  618. }
  619. // If the contents are at least as long as the screen, always fill it
  620. int max_item_top = (int) g_ctx.item_count - app_visible_items ();
  621. // But don't let that suggest a negative offset
  622. max_item_top = MAX (max_item_top, 0);
  623. if (g_ctx.item_top > max_item_top)
  624. {
  625. g_ctx.item_top = max_item_top;
  626. app_invalidate ();
  627. return false;
  628. }
  629. return true;
  630. }
  631. /// Scroll down (positive) or up (negative) @a n items
  632. static bool
  633. app_scroll (int n)
  634. {
  635. g_ctx.item_top += n;
  636. app_invalidate ();
  637. return app_fix_view_range ();
  638. }
  639. static void
  640. app_ensure_selection_visible (void)
  641. {
  642. if (g_ctx.item_selected < 0)
  643. return;
  644. int too_high = g_ctx.item_top - g_ctx.item_selected;
  645. if (too_high > 0)
  646. app_scroll (-too_high);
  647. int too_low = g_ctx.item_selected
  648. - (g_ctx.item_top + app_visible_items () - 1);
  649. if (too_low > 0)
  650. app_scroll (too_low);
  651. }
  652. static bool
  653. app_move_selection (int diff)
  654. {
  655. int fixed = g_ctx.item_selected += diff;
  656. fixed = MAX (fixed, 0);
  657. fixed = MIN (fixed, (int) g_ctx.item_count - 1);
  658. bool result = g_ctx.item_selected != fixed;
  659. g_ctx.item_selected = fixed;
  660. app_invalidate ();
  661. app_ensure_selection_visible ();
  662. return result;
  663. }
  664. // --- User input handling -----------------------------------------------------
  665. enum action
  666. {
  667. ACTION_NONE,
  668. ACTION_QUIT,
  669. ACTION_REDRAW,
  670. ACTION_CHOOSE,
  671. ACTION_DELETE,
  672. ACTION_SCROLL_UP,
  673. ACTION_SCROLL_DOWN,
  674. ACTION_GOTO_TOP,
  675. ACTION_GOTO_BOTTOM,
  676. ACTION_GOTO_ITEM_PREVIOUS,
  677. ACTION_GOTO_ITEM_NEXT,
  678. ACTION_GOTO_PAGE_PREVIOUS,
  679. ACTION_GOTO_PAGE_NEXT,
  680. ACTION_COUNT
  681. };
  682. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  683. static bool
  684. app_process_action (enum action action)
  685. {
  686. switch (action)
  687. {
  688. case ACTION_QUIT:
  689. app_quit ();
  690. break;
  691. case ACTION_REDRAW:
  692. clear ();
  693. app_invalidate ();
  694. break;
  695. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  696. // XXX: these should rather be parametrized
  697. case ACTION_SCROLL_UP:
  698. app_scroll (-3);
  699. break;
  700. case ACTION_SCROLL_DOWN:
  701. app_scroll (3);
  702. break;
  703. case ACTION_GOTO_TOP:
  704. if (g_ctx.item_count)
  705. {
  706. g_ctx.item_selected = 0;
  707. app_ensure_selection_visible ();
  708. app_invalidate ();
  709. }
  710. break;
  711. case ACTION_GOTO_BOTTOM:
  712. if (g_ctx.item_count)
  713. {
  714. g_ctx.item_selected = (int) g_ctx.item_count - 1;
  715. app_ensure_selection_visible ();
  716. app_invalidate ();
  717. }
  718. break;
  719. case ACTION_GOTO_ITEM_PREVIOUS:
  720. app_move_selection (-1);
  721. break;
  722. case ACTION_GOTO_ITEM_NEXT:
  723. app_move_selection (1);
  724. break;
  725. case ACTION_GOTO_PAGE_PREVIOUS:
  726. app_scroll ((int) g_ctx.header_height - LINES);
  727. app_move_selection ((int) g_ctx.header_height - LINES);
  728. break;
  729. case ACTION_GOTO_PAGE_NEXT:
  730. app_scroll (LINES - (int) g_ctx.header_height);
  731. app_move_selection (LINES - (int) g_ctx.header_height);
  732. break;
  733. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  734. case ACTION_NONE:
  735. break;
  736. default:
  737. beep ();
  738. return false;
  739. }
  740. return true;
  741. }
  742. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  743. static bool
  744. app_process_left_mouse_click (int line, int column)
  745. {
  746. if (line == g_ctx.header_height - 1)
  747. {
  748. }
  749. else
  750. {
  751. int row_index = line - g_ctx.header_height;
  752. if (row_index < 0
  753. || row_index >= (int) g_ctx.item_count - g_ctx.item_top)
  754. return false;
  755. g_ctx.item_selected = row_index + g_ctx.item_top;
  756. app_invalidate ();
  757. }
  758. return true;
  759. }
  760. static bool
  761. app_process_mouse (termo_mouse_event_t type, int line, int column, int button)
  762. {
  763. if (type != TERMO_MOUSE_PRESS)
  764. return true;
  765. if (button == 1)
  766. return app_process_left_mouse_click (line, column);
  767. else if (button == 4)
  768. return app_process_action (ACTION_SCROLL_UP);
  769. else if (button == 5)
  770. return app_process_action (ACTION_SCROLL_DOWN);
  771. return false;
  772. }
  773. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  774. static struct binding
  775. {
  776. const char *key; ///< Key definition
  777. enum action action; ///< Action to take
  778. }
  779. g_default_bindings[] =
  780. {
  781. { "Escape", ACTION_QUIT },
  782. { "q", ACTION_QUIT },
  783. { "C-l", ACTION_REDRAW },
  784. // TODO: Tab switches endianity
  785. { "Home", ACTION_GOTO_TOP },
  786. { "End", ACTION_GOTO_BOTTOM },
  787. { "M-<", ACTION_GOTO_TOP },
  788. { "M->", ACTION_GOTO_BOTTOM },
  789. { "Up", ACTION_GOTO_ITEM_PREVIOUS },
  790. { "Down", ACTION_GOTO_ITEM_NEXT },
  791. { "k", ACTION_GOTO_ITEM_PREVIOUS },
  792. { "j", ACTION_GOTO_ITEM_NEXT },
  793. { "PageUp", ACTION_GOTO_PAGE_PREVIOUS },
  794. { "PageDown", ACTION_GOTO_PAGE_NEXT },
  795. { "C-p", ACTION_GOTO_ITEM_PREVIOUS },
  796. { "C-n", ACTION_GOTO_ITEM_NEXT },
  797. { "C-b", ACTION_GOTO_PAGE_PREVIOUS },
  798. { "C-f", ACTION_GOTO_PAGE_NEXT },
  799. // Not sure how to set these up, they're pretty arbitrary so far
  800. { "Enter", ACTION_CHOOSE },
  801. { "Delete", ACTION_DELETE },
  802. };
  803. static bool
  804. app_process_termo_event (termo_key_t *event)
  805. {
  806. // TODO: pre-parse the keys, order them by termo_keycmp() and binary search
  807. for (size_t i = 0; i < N_ELEMENTS (g_default_bindings); i++)
  808. {
  809. struct binding *binding = &g_default_bindings[i];
  810. termo_key_t key;
  811. hard_assert (!*termo_strpkey_utf8 (g_ctx.tk, binding->key, &key,
  812. TERMO_FORMAT_ALTISMETA));
  813. if (!termo_keycmp (g_ctx.tk, event, &key))
  814. return app_process_action (binding->action);
  815. }
  816. // TODO: use 0-9 a-f to overwrite nibbles
  817. return false;
  818. }
  819. // --- Signals -----------------------------------------------------------------
  820. static int g_signal_pipe[2]; ///< A pipe used to signal... signals
  821. /// Program termination has been requested by a signal
  822. static volatile sig_atomic_t g_termination_requested;
  823. /// The window has changed in size
  824. static volatile sig_atomic_t g_winch_received;
  825. static void
  826. signals_postpone_handling (char id)
  827. {
  828. int original_errno = errno;
  829. if (write (g_signal_pipe[1], &id, 1) == -1)
  830. soft_assert (errno == EAGAIN);
  831. errno = original_errno;
  832. }
  833. static void
  834. signals_superhandler (int signum)
  835. {
  836. switch (signum)
  837. {
  838. case SIGWINCH:
  839. g_winch_received = true;
  840. signals_postpone_handling ('w');
  841. break;
  842. case SIGINT:
  843. case SIGTERM:
  844. g_termination_requested = true;
  845. signals_postpone_handling ('t');
  846. break;
  847. default:
  848. hard_assert (!"unhandled signal");
  849. }
  850. }
  851. static void
  852. signals_setup_handlers (void)
  853. {
  854. if (pipe (g_signal_pipe) == -1)
  855. exit_fatal ("%s: %s", "pipe", strerror (errno));
  856. set_cloexec (g_signal_pipe[0]);
  857. set_cloexec (g_signal_pipe[1]);
  858. // So that the pipe cannot overflow; it would make write() block within
  859. // the signal handler, which is something we really don't want to happen.
  860. // The same holds true for read().
  861. set_blocking (g_signal_pipe[0], false);
  862. set_blocking (g_signal_pipe[1], false);
  863. signal (SIGPIPE, SIG_IGN);
  864. struct sigaction sa;
  865. sa.sa_flags = SA_RESTART;
  866. sa.sa_handler = signals_superhandler;
  867. sigemptyset (&sa.sa_mask);
  868. if (sigaction (SIGWINCH, &sa, NULL) == -1
  869. || sigaction (SIGINT, &sa, NULL) == -1
  870. || sigaction (SIGTERM, &sa, NULL) == -1)
  871. exit_fatal ("sigaction: %s", strerror (errno));
  872. }
  873. // --- Initialisation, event handling ------------------------------------------
  874. static void
  875. app_on_tty_readable (const struct pollfd *fd, void *user_data)
  876. {
  877. (void) user_data;
  878. if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
  879. print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
  880. poller_timer_reset (&g_ctx.tk_timer);
  881. termo_advisereadable (g_ctx.tk);
  882. termo_key_t event;
  883. termo_result_t res;
  884. while ((res = termo_getkey (g_ctx.tk, &event)) == TERMO_RES_KEY)
  885. {
  886. int y, x, button;
  887. termo_mouse_event_t type;
  888. if (termo_interpret_mouse (g_ctx.tk, &event, &type, &button, &y, &x))
  889. {
  890. if (!app_process_mouse (type, y, x, button))
  891. beep ();
  892. }
  893. else if (!app_process_termo_event (&event))
  894. beep ();
  895. }
  896. if (res == TERMO_RES_AGAIN)
  897. poller_timer_set (&g_ctx.tk_timer, termo_get_waittime (g_ctx.tk));
  898. else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF)
  899. app_quit ();
  900. }
  901. static void
  902. app_on_key_timer (void *user_data)
  903. {
  904. (void) user_data;
  905. termo_key_t event;
  906. if (termo_getkey_force (g_ctx.tk, &event) == TERMO_RES_KEY)
  907. if (!app_process_termo_event (&event))
  908. app_quit ();
  909. }
  910. static void
  911. app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
  912. {
  913. (void) user_data;
  914. char id = 0;
  915. (void) read (fd->fd, &id, 1);
  916. if (g_termination_requested && !g_ctx.quitting)
  917. app_quit ();
  918. if (g_winch_received)
  919. {
  920. update_curses_terminal_size ();
  921. app_fix_view_range ();
  922. app_invalidate ();
  923. g_winch_received = false;
  924. }
  925. }
  926. static void
  927. app_log_handler (void *user_data, const char *quote, const char *fmt,
  928. va_list ap)
  929. {
  930. // We certainly don't want to end up in a possibly infinite recursion
  931. static bool in_processing;
  932. if (in_processing)
  933. return;
  934. in_processing = true;
  935. struct str message;
  936. str_init (&message);
  937. str_append (&message, quote);
  938. str_append_vprintf (&message, fmt, ap);
  939. // If the standard error output isn't redirected, try our best at showing
  940. // the message to the user
  941. if (!isatty (STDERR_FILENO))
  942. fprintf (stderr, "%s\n", message.str);
  943. else
  944. {
  945. // TODO: think of a location to print this, maybe over decoding fields
  946. // TODO: remember the position and restore it
  947. move (LINES - 1, 0);
  948. app_write_line (message.str, A_REVERSE);
  949. }
  950. str_free (&message);
  951. in_processing = false;
  952. }
  953. static void
  954. app_init_poller_events (void)
  955. {
  956. poller_fd_init (&g_ctx.signal_event, &g_ctx.poller, g_signal_pipe[0]);
  957. g_ctx.signal_event.dispatcher = app_on_signal_pipe_readable;
  958. poller_fd_set (&g_ctx.signal_event, POLLIN);
  959. poller_fd_init (&g_ctx.tty_event, &g_ctx.poller, STDIN_FILENO);
  960. g_ctx.tty_event.dispatcher = app_on_tty_readable;
  961. poller_fd_set (&g_ctx.tty_event, POLLIN);
  962. poller_timer_init (&g_ctx.tk_timer, &g_ctx.poller);
  963. g_ctx.tk_timer.dispatcher = app_on_key_timer;
  964. poller_idle_init (&g_ctx.refresh_event, &g_ctx.poller);
  965. g_ctx.refresh_event.dispatcher = app_on_refresh;
  966. }
  967. /// Decode size arguments according to similar rules to those that dd(1) uses;
  968. /// we support octal and hexadecimal numbers but they clash with suffixes
  969. static bool
  970. decode_size (const char *s, uint64_t *out)
  971. {
  972. char *end;
  973. errno = 0;
  974. uint64_t n = strtoul (s, &end, 0);
  975. if (errno != 0 || end == s)
  976. return false;
  977. uint64_t f = 1;
  978. switch (*end)
  979. {
  980. case 'c': f = 1 << 0; end++; break;
  981. case 'w': f = 1 << 1; end++; break;
  982. case 'b': f = 1 << 9; end++; break;
  983. case 'K': f = 1 << 10; if (*++end == 'B') { f = 1e3; end++; } break;
  984. case 'M': f = 1 << 20; if (*++end == 'B') { f = 1e6; end++; } break;
  985. case 'G': f = 1 << 30; if (*++end == 'B') { f = 1e9; end++; } break;
  986. }
  987. if (*end || n > UINT64_MAX / f)
  988. return false;
  989. *out = n * f;
  990. return true;
  991. }
  992. int
  993. main (int argc, char *argv[])
  994. {
  995. static const struct opt opts[] =
  996. {
  997. { 'd', "debug", NULL, 0, "run in debug mode" },
  998. { 'h', "help", NULL, 0, "display this help and exit" },
  999. { 'V', "version", NULL, 0, "output version information and exit" },
  1000. { 'o', "offset", NULL, 0, "offset within the file" },
  1001. { 's', "size", NULL, 0, "size limit (1G by default)" },
  1002. { 0, NULL, NULL, 0, NULL }
  1003. };
  1004. struct opt_handler oh;
  1005. opt_handler_init (&oh, argc, argv, opts, "[FILE]", "Hex viewer.");
  1006. uint64_t size_limit = 1 << 30;
  1007. int c;
  1008. while ((c = opt_handler_get (&oh)) != -1)
  1009. switch (c)
  1010. {
  1011. case 'd':
  1012. g_debug_mode = true;
  1013. break;
  1014. case 'h':
  1015. opt_handler_usage (&oh, stdout);
  1016. exit (EXIT_SUCCESS);
  1017. case 'V':
  1018. printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
  1019. exit (EXIT_SUCCESS);
  1020. case 'o':
  1021. if (!decode_size (optarg, &g_ctx.data_offset))
  1022. exit_fatal ("invalid offset specified");
  1023. break;
  1024. case 's':
  1025. if (!decode_size (optarg, &size_limit))
  1026. exit_fatal ("invalid size limit specified");
  1027. break;
  1028. default:
  1029. print_error ("wrong options");
  1030. opt_handler_usage (&oh, stderr);
  1031. exit (EXIT_FAILURE);
  1032. }
  1033. argc -= optind;
  1034. argv += optind;
  1035. // When no filename is given, read from stdin and replace it with the tty
  1036. int input_fd;
  1037. if (argc == 0)
  1038. {
  1039. if ((input_fd = dup (STDIN_FILENO)) < 0)
  1040. exit_fatal ("cannot read input: %s", strerror (errno));
  1041. close (STDIN_FILENO);
  1042. if (open ("/dev/tty", O_RDWR))
  1043. exit_fatal ("cannot open the terminal: %s", strerror (errno));
  1044. }
  1045. else if (argc == 1)
  1046. {
  1047. g_ctx.filename = xstrdup (argv[0]);
  1048. if (!(input_fd = open (argv[0], O_RDONLY)))
  1049. exit_fatal ("cannot open `%s': %s", argv[0], strerror (errno));
  1050. }
  1051. else
  1052. {
  1053. opt_handler_usage (&oh, stderr);
  1054. exit (EXIT_FAILURE);
  1055. }
  1056. opt_handler_free (&oh);
  1057. // Seek in the file or pipe however we can
  1058. static char seek_buf[8192];
  1059. if (lseek (input_fd, g_ctx.data_offset, SEEK_SET) == (off_t) -1)
  1060. for (uint64_t remaining = g_ctx.data_offset; remaining; )
  1061. {
  1062. ssize_t n_read = read (input_fd,
  1063. seek_buf, MIN (remaining, sizeof seek_buf));
  1064. if (n_read <= 0)
  1065. exit_fatal ("cannot seek: %s", strerror (errno));
  1066. remaining -= n_read;
  1067. }
  1068. // Read up to "size_limit" bytes of data into a buffer
  1069. struct str buf;
  1070. str_init (&buf);
  1071. while (buf.len < size_limit)
  1072. {
  1073. str_ensure_space (&buf, 8192);
  1074. ssize_t n_read = read (input_fd, buf.str + buf.len,
  1075. MIN (size_limit - buf.len, buf.alloc - buf.len));
  1076. if (!n_read)
  1077. break;
  1078. if (n_read == -1)
  1079. exit_fatal ("cannot read input: %s", strerror (errno));
  1080. buf.len += n_read;
  1081. }
  1082. g_ctx.data = (uint8_t *) buf.str;
  1083. g_ctx.data_len = buf.len;
  1084. // We only need to convert to and from the terminal encoding
  1085. if (!setlocale (LC_CTYPE, ""))
  1086. print_warning ("failed to set the locale");
  1087. app_init_context ();
  1088. app_load_configuration ();
  1089. app_init_terminal ();
  1090. signals_setup_handlers ();
  1091. app_init_poller_events ();
  1092. // Redirect all messages from liberty so that they don't disrupt display
  1093. g_log_message_real = app_log_handler;
  1094. g_ctx.polling = true;
  1095. while (g_ctx.polling)
  1096. poller_run (&g_ctx.poller);
  1097. endwin ();
  1098. g_log_message_real = log_message_stdio;
  1099. app_free_context ();
  1100. return 0;
  1101. }