diff options
-rw-r--r-- | .gitignore | 9 | ||||
-rw-r--r-- | .gitmodules | 6 | ||||
-rw-r--r-- | CMakeLists.txt | 116 | ||||
-rw-r--r-- | LICENSE | 15 | ||||
-rw-r--r-- | README.adoc | 86 | ||||
-rw-r--r-- | cmake/FindNcursesw.cmake | 17 | ||||
-rw-r--r-- | cmake/FindUnistring.cmake | 10 | ||||
-rw-r--r-- | config.h.in | 10 | ||||
-rw-r--r-- | hex.c | 1295 | ||||
m--------- | liberty | 0 | ||||
m--------- | termo | 0 |
11 files changed, 1564 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5a835a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Build files +/build + +# Qt Creator files +/CMakeLists.txt.user* +/hex.config +/hex.files +/hex.creator* +/hex.includes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4acc2dd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "termo"] + path = termo + url = git://github.com/pjanouch/termo.git +[submodule "liberty"] + path = liberty + url = git://github.com/pjanouch/liberty.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f4aad0d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,116 @@ +project (hex C) +cmake_minimum_required (VERSION 2.8.5) + +# Moar warnings +if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) + set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99") + set (CMAKE_C_FLAGS_DEBUG + "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra -Wno-unused-function") +endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) + +# Version +set (project_VERSION_MAJOR "0") +set (project_VERSION_MINOR "1") +set (project_VERSION_PATCH "0") + +set (project_VERSION "${project_VERSION_MAJOR}") +set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}") +set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}") + +# For custom modules +set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + +# Dependencies +find_package (Ncursesw REQUIRED) +find_package (PkgConfig REQUIRED) +find_package (Unistring REQUIRED) + +set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake) +include (AddThreads) + +find_package (Termo QUIET NO_MODULE) +option (USE_SYSTEM_TERMO + "Don't compile our own termo library, use the system one" ${Termo_FOUND}) + +if (USE_SYSTEM_TERMO) + if (NOT Termo_FOUND) + message (FATAL_ERROR "System termo library not found") + endif (NOT Termo_FOUND) +else (USE_SYSTEM_TERMO) + add_subdirectory (termo EXCLUDE_FROM_ALL) + # We don't have many good choices when we don't want to install it and want + # to support older versions of CMake; this is a relatively clean approach + # (other possibilities: setting a variable in the parent scope, using a + # cache variable, writing a special config file with build paths in it and + # including it here, or setting a custom property on the targets). + get_directory_property (Termo_INCLUDE_DIRS + DIRECTORY termo INCLUDE_DIRECTORIES) + set (Termo_LIBRARIES termo-static) +endif (USE_SYSTEM_TERMO) + +include_directories (${UNISTRING_INCLUDE_DIRS} + ${NCURSESW_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS}) + +# Configuration +include (CheckFunctionExists) +set (CMAKE_REQUIRED_LIBRARIES ${NCURSESW_LIBRARIES}) +CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM) + +# Generate a configuration file +configure_file (${PROJECT_SOURCE_DIR}/config.h.in + ${PROJECT_BINARY_DIR}/config.h) +include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}) + +# Build the main executable and link it +add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c) +target_link_libraries (${PROJECT_NAME} ${UNISTRING_LIBRARIES} + ${NCURSESW_LIBRARIES} termo-static) +add_threads (${PROJECT_NAME}) + +# Installation +include (GNUInstallDirs) +install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) + +# Generate documentation from program help +find_program (HELP2MAN_EXECUTABLE help2man) +if (NOT HELP2MAN_EXECUTABLE) + message (FATAL_ERROR "help2man not found") +endif (NOT HELP2MAN_EXECUTABLE) + +foreach (page ${PROJECT_NAME}) + set (page_output "${PROJECT_BINARY_DIR}/${page}.1") + list (APPEND project_MAN_PAGES "${page_output}") + add_custom_command (OUTPUT ${page_output} + COMMAND ${HELP2MAN_EXECUTABLE} -N + "${PROJECT_BINARY_DIR}/${page}" -o ${page_output} + DEPENDS ${page} + COMMENT "Generating man page for ${page}" VERBATIM) +endforeach (page) + +add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES}) + +foreach (page ${project_MAN_PAGES}) + string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}") + install (FILES "${page}" + DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") +endforeach (page) + +# CPack +set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Hex viewer") +set (CPACK_PACKAGE_VENDOR "Premysl Janouch") +set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p.janouch@gmail.com>") +set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") +set (CPACK_PACKAGE_VERSION_MAJOR ${project_VERSION_MAJOR}) +set (CPACK_PACKAGE_VERSION_MINOR ${project_VERSION_MINOR}) +set (CPACK_PACKAGE_VERSION_PATCH ${project_VERSION_PATCH}) +set (CPACK_GENERATOR "TGZ;ZIP") +set (CPACK_PACKAGE_FILE_NAME + "${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") +set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}") +set (CPACK_SOURCE_GENERATOR "TGZ;ZIP") +set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user") +set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}") + +set (CPACK_SET_DESTDIR TRUE) +include (CPack) @@ -0,0 +1,15 @@ + Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com> + All rights reserved. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..0c2a3b7 --- /dev/null +++ b/README.adoc @@ -0,0 +1,86 @@ +hex +=== + +'hex' is yet another hex viewer. As of now, there are no advantages to it. + +Plans +----- +In the future, it should be able to automatically interpret fields within files +via a set of Lua scripts. + +Packages +-------- +Regular releases are sporadic. git master should be stable enough. You can get +a package with the latest development version from Archlinux's AUR, or from +openSUSE Build Service for the rest of mainstream distributions. Consult the +list of repositories and their respective links at: + +https://build.opensuse.org/project/repositories/home:pjanouch:git + +Building and Running +-------------------- +Build dependencies: CMake, pkg-config, help2man, liberty (included), + termo (included) + +Runtime dependencies: ncursesw, libunistring + + $ git clone --recursive https://github.com/pjanouch/hex.git + $ mkdir hex/build + $ cd hex/build + $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug + $ make + +To install the application, you can do either the usual: + + # make install + +Or you can try telling CMake to make a package for you. For Debian it is: + + $ cpack -G DEB + # dpkg -i hex-*.deb + +Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with +`fakeroot` or file ownership will end up wrong. + +Having the program installed, optionally create a configuration file and run it. + +Configuration +------------- +Create _~/.config/hex/hex.conf_ with contents like the following: + +.... +colors = { + header = "" + highlight = "bold" + bar = "reverse" + bar_active = "ul" + even = "" + odd = "" + selection = "reverse" +} +.... + +Terminal caveats +---------------- +This application aspires to be as close to a GUI as possible. It expects you +to use the mouse (though it's not required). Terminals are, however, somewhat +tricky to get consistent results on, so be aware of the following: + + - use a UTF-8 locale to get finer resolution progress bars and scrollbars + - Xterm needs `XTerm*metaSendsEscape: true` for the default bindings to work + - urxvt's 'vtwheel' plugin sabotages scrolling + +Contributing and Support +------------------------ +Use this project's GitHub to report any bugs, request features, or submit pull +requests. If you want to discuss this project, or maybe just hang out with +the developer, feel free to join me at irc://irc.janouch.name, channel #dev. + +License +------- +'hex' is written by Přemysl Janouch <p.janouch@gmail.com>. + +You may use the software under the terms of the ISC license, the text of which +is included within the package, or, at your option, you may relicense the work +under the MIT or the Modified BSD License, as listed at the following site: + +http://www.gnu.org/licenses/license-list.html diff --git a/cmake/FindNcursesw.cmake b/cmake/FindNcursesw.cmake new file mode 100644 index 0000000..88c1d01 --- /dev/null +++ b/cmake/FindNcursesw.cmake @@ -0,0 +1,17 @@ +# Public Domain + +find_package (PkgConfig REQUIRED) +pkg_check_modules (NCURSESW QUIET ncursesw) + +# OpenBSD doesn't provide a pkg-config file +set (required_vars NCURSESW_LIBRARIES) +if (NOT NCURSESW_FOUND) + find_library (NCURSESW_LIBRARIES NAMES ncursesw) + find_path (NCURSESW_INCLUDE_DIRS ncurses.h) + list (APPEND required_vars NCURSESW_INCLUDE_DIRS) +endif (NOT NCURSESW_FOUND) + +include (FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS (NCURSESW DEFAULT_MSG ${required_vars}) + +mark_as_advanced (NCURSESW_LIBRARIES NCURSESW_INCLUDE_DIRS) diff --git a/cmake/FindUnistring.cmake b/cmake/FindUnistring.cmake new file mode 100644 index 0000000..6b74efb --- /dev/null +++ b/cmake/FindUnistring.cmake @@ -0,0 +1,10 @@ +# Public Domain + +find_path (UNISTRING_INCLUDE_DIRS unistr.h) +find_library (UNISTRING_LIBRARIES NAMES unistring libunistring) + +include (FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS (UNISTRING DEFAULT_MSG + UNISTRING_INCLUDE_DIRS UNISTRING_LIBRARIES) + +mark_as_advanced (UNISTRING_LIBRARIES UNISTRING_INCLUDE_DIRS) diff --git a/config.h.in b/config.h.in new file mode 100644 index 0000000..b61ed66 --- /dev/null +++ b/config.h.in @@ -0,0 +1,10 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define PROGRAM_NAME "${CMAKE_PROJECT_NAME}" +#define PROGRAM_VERSION "${project_VERSION}" + +#cmakedefine HAVE_RESIZETERM + +#endif // ! CONFIG_H + @@ -0,0 +1,1295 @@ +/* + * hex -- a very simple hex viewer + * + * Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#include "config.h" + +// We "need" to have an enum for attributes before including liberty. +// Avoiding colours in the defaults here in order to support dumb terminals. +#define ATTRIBUTE_TABLE(XX) \ + XX( HEADER, "header", -1, -1, 0 ) \ + XX( HIGHLIGHT, "highlight", -1, -1, A_BOLD ) \ + /* Bar */ \ + XX( BAR, "bar", -1, -1, A_REVERSE ) \ + XX( BAR_ACTIVE, "bar_active", -1, -1, A_UNDERLINE ) \ + /* Listview */ \ + XX( EVEN, "even", -1, -1, 0 ) \ + XX( ODD, "odd", -1, -1, 0 ) \ + XX( SELECTION, "selection", -1, -1, A_REVERSE ) \ + /* These are for debugging only */ \ + XX( WARNING, "warning", 3, -1, 0 ) \ + XX( ERROR, "error", 1, -1, 0 ) + +enum +{ +#define XX(name, config, fg_, bg_, attrs_) ATTRIBUTE_ ## name, + ATTRIBUTE_TABLE (XX) +#undef XX + ATTRIBUTE_COUNT +}; + +// My battle-tested C framework acting as a GLib replacement. Its one big +// disadvantage is missing support for i18n but that can eventually be added +// as an optional feature. Localised applications look super awkward, though. + +// User data for logger functions to enable formatted logging +#define print_fatal_data ((void *) ATTRIBUTE_ERROR) +#define print_error_data ((void *) ATTRIBUTE_ERROR) +#define print_warning_data ((void *) ATTRIBUTE_WARNING) + +#define LIBERTY_WANT_POLLER +#define LIBERTY_WANT_ASYNC +#define LIBERTY_WANT_PROTO_HTTP +#include "liberty/liberty.c" + +#include <locale.h> +#include <termios.h> +#ifndef TIOCGWINSZ +#include <sys/ioctl.h> +#endif // ! TIOCGWINSZ +#include <ncurses.h> + +// ncurses is notoriously retarded for input handling, we need something +// different if only to receive mouse events reliably. + +#include "termo.h" + +// It is surprisingly hard to find a good library to handle Unicode shenanigans, +// and there's enough of those for it to be impractical to reimplement them. +// +// GLib ICU libunistring utf8proc +// Decently sized . . x x +// Grapheme breaks . x . x +// Character width x . x x +// Locale handling . . x . +// Liberal license . x . x +// +// Also note that the ICU API is icky and uses UTF-16 for its primary encoding. +// +// Currently we're chugging along with libunistring but utf8proc seems viable. +// Non-Unicode locales can mostly be handled with simple iconv like in sdtui. +// Similarly grapheme breaks can be guessed at using character width (a basic +// test here is Zalgo text). +// +// None of this is ever going to work too reliably anyway because terminals +// and Unicode don't go awfully well together. In particular, character cell +// devices have some problems with double-wide characters. + +#include <unistr.h> +#include <uniwidth.h> +#include <uniconv.h> +#include <unicase.h> + +#define APP_TITLE PROGRAM_NAME ///< Left top corner + +// --- Utilities --------------------------------------------------------------- + +// The standard endwin/refresh sequence makes the terminal flicker +static void +update_curses_terminal_size (void) +{ +#if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ) + struct winsize size; + if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) + { + char *row = getenv ("LINES"); + char *col = getenv ("COLUMNS"); + unsigned long tmp; + resizeterm ( + (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row, + (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col); + } +#else // HAVE_RESIZETERM && TIOCGWINSZ + endwin (); + refresh (); +#endif // HAVE_RESIZETERM && TIOCGWINSZ +} + +static char * +latin1_to_utf8 (const char *latin1) +{ + struct str converted; + str_init (&converted); + while (*latin1) + { + uint8_t c = *latin1++; + if (c < 0x80) + str_append_c (&converted, c); + else + { + str_append_c (&converted, 0xC0 | (c >> 6)); + str_append_c (&converted, 0x80 | (c & 0x3F)); + } + } + return str_steal (&converted); +} + +// --- Application ------------------------------------------------------------- + +// Function names are prefixed mostly because of curses which clutters the +// global namespace and makes it harder to distinguish what functions relate to. + +struct attrs +{ + short fg; ///< Foreground colour index + short bg; ///< Background colour index + chtype attrs; ///< Other attributes +}; + +// Basically a container for most of the globals; no big sense in handing +// around a pointer to this, hence it is a simple global variable as well. +// There is enough global state as it is. + +static struct app_context +{ + // Event loop: + + struct poller poller; ///< Poller + bool quitting; ///< Quit signal for the event loop + bool polling; ///< The event loop is running + + struct poller_fd tty_event; ///< Terminal input event + struct poller_fd signal_event; ///< Signal FD event + + // Data: + + struct config config; ///< Program configuration + char *filename; ///< Target filename + + uint8_t *data; ///< Target data + uint64_t data_len; ///< Length of the data + uint64_t data_offset; ///< Offset of the data within the file + uint64_t data_cursor; ///< Current position within the data + + // TODO: get rid of this as it can be computed from "data*" + size_t item_count; ///< Total item count + int item_top; ///< Index of the topmost item + int item_selected; ///< Index of the selected item + + // Emulated widgets: + + // TODO: make this the footer; + // remove this, we know how high the footer is + int header_height; ///< Height of the header + + struct poller_idle refresh_event; ///< Refresh the screen + + // Terminal: + + termo_t *tk; ///< termo handle + struct poller_timer tk_timer; ///< termo timeout timer + bool locale_is_utf8; ///< The locale is Unicode + + struct attrs attrs[ATTRIBUTE_COUNT]; +} +g_ctx; + +/// Shortcut to retrieve named terminal attributes +#define APP_ATTR(name) g_ctx.attrs[ATTRIBUTE_ ## name].attrs + +// --- Configuration ----------------------------------------------------------- + +static struct config_schema g_config_colors[] = +{ +#define XX(name_, config, fg_, bg_, attrs_) \ + { .name = config, .type = CONFIG_ITEM_STRING }, + ATTRIBUTE_TABLE (XX) +#undef XX + {} +}; + +static const char * +get_config_string (struct config_item *root, const char *key) +{ + struct config_item *item = config_item_get (root, key, NULL); + hard_assert (item); + if (item->type == CONFIG_ITEM_NULL) + return NULL; + hard_assert (config_item_type_is_string (item->type)); + return item->value.string.str; +} + +/// Load configuration for a color using a subset of git config colors +static void +app_load_color (struct config_item *subtree, const char *name, int id) +{ + const char *value = get_config_string (subtree, name); + if (!value) + return; + + struct str_vector v; + str_vector_init (&v); + cstr_split (value, " ", true, &v); + + int colors = 0; + struct attrs attrs = { -1, -1, 0 }; + for (char **it = v.vector; *it; it++) + { + char *end = NULL; + long n = strtol (*it, &end, 10); + if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX) + { + if (colors == 0) attrs.fg = n; + if (colors == 1) attrs.bg = n; + colors++; + } + else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD; + else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM; + else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE; + else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK; + else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE; +#ifdef A_ITALIC + else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC; +#endif // A_ITALIC + } + str_vector_free (&v); + g_ctx.attrs[id] = attrs; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +load_config_colors (struct config_item *subtree, void *user_data) +{ + config_schema_apply_to_object (g_config_colors, subtree, user_data); + + // The attributes cannot be changed dynamically right now, so it doesn't + // make much sense to make use of "on_change" callbacks either. + // For simplicity, we should reload the entire table on each change anyway. +#define XX(name, config, fg_, bg_, attrs_) \ + app_load_color (subtree, config, ATTRIBUTE_ ## name); + ATTRIBUTE_TABLE (XX) +#undef XX +} + +static void +app_load_configuration (void) +{ + struct config *config = &g_ctx.config; + config_register_module (config, "colors", load_config_colors, NULL); + + // Bootstrap configuration, so that we can access schema items at all + config_load (config, config_item_object ()); + + char *filename = resolve_filename + (PROGRAM_NAME ".conf", resolve_relative_config_filename); + if (!filename) + return; + + struct error *e = NULL; + struct config_item *root = config_read_from_file (filename, &e); + free (filename); + + if (e) + { + print_error ("error loading configuration: %s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + if (root) + { + config_load (&g_ctx.config, root); + config_schema_call_changed (g_ctx.config.root); + } +} + +// --- Application ------------------------------------------------------------- + +static void +app_init_attributes (void) +{ +#define XX(name, config, fg_, bg_, attrs_) \ + g_ctx.attrs[ATTRIBUTE_ ## name].fg = fg_; \ + g_ctx.attrs[ATTRIBUTE_ ## name].bg = bg_; \ + g_ctx.attrs[ATTRIBUTE_ ## name].attrs = attrs_; + ATTRIBUTE_TABLE (XX) +#undef XX +} + +static void +app_init_context (void) +{ + poller_init (&g_ctx.poller); + config_init (&g_ctx.config); + + // This is also approximately what libunistring does internally, + // since the locale name is canonicalized by locale_charset(). + // Note that non-Unicode locales are handled pretty inefficiently. + g_ctx.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8"); + + app_init_attributes (); +} + +static void +app_init_terminal (void) +{ + TERMO_CHECK_VERSION; + if (!(g_ctx.tk = termo_new (STDIN_FILENO, NULL, 0))) + abort (); + if (!initscr () || nonl () == ERR) + abort (); + + // Disable cursor, we're not going to use it most of the time + curs_set (0); + + // By default we don't use any colors so they're not required... + if (start_color () == ERR + || use_default_colors () == ERR + || COLOR_PAIRS <= ATTRIBUTE_COUNT) + return; + + for (int a = 0; a < ATTRIBUTE_COUNT; a++) + { + // ...thus we can reset back to defaults even after initializing some + if (g_ctx.attrs[a].fg >= COLORS || g_ctx.attrs[a].fg < -1 + || g_ctx.attrs[a].bg >= COLORS || g_ctx.attrs[a].bg < -1) + { + app_init_attributes (); + return; + } + + init_pair (a + 1, g_ctx.attrs[a].fg, g_ctx.attrs[a].bg); + g_ctx.attrs[a].attrs |= COLOR_PAIR (a + 1); + } +} + +static void +app_free_context (void) +{ + config_free (&g_ctx.config); + poller_free (&g_ctx.poller); + + free (g_ctx.filename); + free (g_ctx.data); + + if (g_ctx.tk) + termo_destroy (g_ctx.tk); +} + +static void +app_quit (void) +{ + g_ctx.quitting = true; + g_ctx.polling = false; +} + +static bool +app_is_character_in_locale (ucs4_t ch) +{ + // Avoid the overhead joined with calling iconv() for all characters. + if (g_ctx.locale_is_utf8) + return true; + + // The library really creates a new conversion object every single time + // and doesn't provide any smarter APIs. Luckily, most users use UTF-8. + size_t len; + char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error, + &ch, 1, NULL, NULL, &len); + if (!tmp) + return false; + free (tmp); + return true; +} + +// --- Terminal output --------------------------------------------------------- + +// Necessary abstraction to simplify aligned, formatted character output + +struct row_char +{ + ucs4_t c; ///< Unicode codepoint + chtype attrs; ///< Special attributes + int width; ///< How many cells this takes +}; + +struct row_buffer +{ + struct row_char *chars; ///< Characters + size_t chars_len; ///< Character count + size_t chars_alloc; ///< Characters allocated + int total_width; ///< Total width of all characters +}; + +static void +row_buffer_init (struct row_buffer *self) +{ + memset (self, 0, sizeof *self); + self->chars = xcalloc (sizeof *self->chars, (self->chars_alloc = 256)); +} + +static void +row_buffer_free (struct row_buffer *self) +{ + free (self->chars); +} + +/// Replace invalid chars and push all codepoints to the array w/ attributes. +static void +row_buffer_append (struct row_buffer *self, const char *str, chtype attrs) +{ + // The encoding is only really used internally for some corner cases + const char *encoding = locale_charset (); + + // Note that this function is a hotspot, try to keep it decently fast + struct row_char current = { .attrs = attrs }; + struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 }; + const uint8_t *next = (const uint8_t *) str; + while ((next = u8_next (¤t.c, next))) + { + if (self->chars_len >= self->chars_alloc) + self->chars = xreallocarray (self->chars, + sizeof *self->chars, (self->chars_alloc <<= 1)); + + current.width = uc_width (current.c, encoding); + if (current.width < 0 || !app_is_character_in_locale (current.c)) + current = invalid; + + self->chars[self->chars_len++] = current; + self->total_width += current.width; + } +} + +static void +row_buffer_addv (struct row_buffer *self, const char *s, ...) + ATTRIBUTE_SENTINEL; + +static void +row_buffer_addv (struct row_buffer *self, const char *s, ...) +{ + va_list ap; + va_start (ap, s); + + while (s) + { + row_buffer_append (self, s, va_arg (ap, chtype)); + s = va_arg (ap, const char *); + } + va_end (ap); +} + +/// Pop as many codepoints as needed to free up "space" character cells. +/// Given the suffix nature of combining marks, this should work pretty fine. +static int +row_buffer_pop_cells (struct row_buffer *self, int space) +{ + int made = 0; + while (self->chars_len && made < space) + made += self->chars[--self->chars_len].width; + self->total_width -= made; + return made; +} + +static void +row_buffer_space (struct row_buffer *self, int width, chtype attrs) +{ + if (width < 0) + return; + + while (self->chars_len + width >= self->chars_alloc) + self->chars = xreallocarray (self->chars, + sizeof *self->chars, (self->chars_alloc <<= 1)); + + struct row_char space = { .attrs = attrs, .c = ' ', .width = 1 }; + self->total_width += width; + while (width-- > 0) + self->chars[self->chars_len++] = space; +} + +static void +row_buffer_ellipsis (struct row_buffer *self, int target) +{ + if (self->total_width <= target + || !row_buffer_pop_cells (self, self->total_width - target)) + return; + + // We use attributes from the last character we've removed, + // assuming that we don't shrink the array (and there's no real need) + ucs4_t ellipsis = L'…'; + if (app_is_character_in_locale (ellipsis)) + { + if (self->total_width >= target) + row_buffer_pop_cells (self, 1); + if (self->total_width + 1 <= target) + row_buffer_append (self, "…", self->chars[self->chars_len].attrs); + } + else if (target >= 3) + { + if (self->total_width >= target) + row_buffer_pop_cells (self, 3); + if (self->total_width + 3 <= target) + row_buffer_append (self, "...", self->chars[self->chars_len].attrs); + } +} + +static void +row_buffer_align (struct row_buffer *self, int target, chtype attrs) +{ + row_buffer_ellipsis (self, target); + row_buffer_space (self, target - self->total_width, attrs); +} + +static void +row_buffer_print (uint32_t *ucs4, chtype attrs) +{ + // This assumes that we can reset the attribute set without consequences + char *str = u32_strconv_to_locale (ucs4); + if (str) + { + attrset (attrs); + addstr (str); + attrset (0); + free (str); + } +} + +static void +row_buffer_flush (struct row_buffer *self) +{ + if (!self->chars_len) + return; + + // We only NUL-terminate the chunks because of the libunistring API + uint32_t chunk[self->chars_len + 1], *insertion_point = chunk; + for (size_t i = 0; i < self->chars_len; i++) + { + struct row_char *iter = self->chars + i; + if (i && iter[0].attrs != iter[-1].attrs) + { + row_buffer_print (chunk, iter[-1].attrs); + insertion_point = chunk; + } + *insertion_point++ = iter->c; + *insertion_point = 0; + } + row_buffer_print (chunk, self->chars[self->chars_len - 1].attrs); +} + +// --- Rendering --------------------------------------------------------------- + +static void +app_invalidate (void) +{ + poller_idle_set (&g_ctx.refresh_event); +} + +static void +app_flush_buffer (struct row_buffer *buf, int width, chtype attrs) +{ + row_buffer_align (buf, width, attrs); + row_buffer_flush (buf); + row_buffer_free (buf); +} + +/// Write the given UTF-8 string padded with spaces. +/// @param[in] attrs Text attributes for the text, including padding. +static void +app_write_line (const char *str, chtype attrs) +{ + struct row_buffer buf; + row_buffer_init (&buf); + row_buffer_append (&buf, str, attrs); + app_flush_buffer (&buf, COLS, attrs); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +app_flush_header (struct row_buffer *buf, chtype attrs) +{ + move (g_ctx.header_height++, 0); + app_flush_buffer (buf, COLS, attrs); +} + +static void +app_draw_status (void) +{ + // XXX: can we get rid of this and still make it look acceptable? + chtype a_normal = APP_ATTR (HEADER); + chtype a_highlight = APP_ATTR (HIGHLIGHT); + + struct row_buffer buf; + row_buffer_init (&buf); + // ... + app_flush_header (&buf, a_normal); +} + +static void +app_draw_header (void) +{ + // TODO: call app_fix_view_range() if it changes from the previous value + g_ctx.header_height = 0; + + if (true) + app_draw_status (); + else + { + move (g_ctx.header_height++, 0); + app_write_line ("Connecting to MPD...", APP_ATTR (HEADER)); + } + + // XXX: can we get rid of this and still make it look acceptable? + chtype a_normal = APP_ATTR (BAR); + chtype a_active = APP_ATTR (BAR_ACTIVE); + + struct row_buffer buf; + row_buffer_init (&buf); + + // TODO: print the filename here instead + row_buffer_append (&buf, APP_TITLE, a_normal); + row_buffer_append (&buf, " ", a_normal); + + // TODO: endian indication, position indication + app_flush_header (&buf, a_normal); +} + +static int +app_visible_items (void) +{ + // This may eventually include a header bar and/or a status bar + return MAX (0, LINES - g_ctx.header_height); +} + +static void +app_draw_view (void) +{ + move (g_ctx.header_height, 0); + clrtobot (); + + int view_width = COLS; + + int to_show = MIN (LINES - g_ctx.header_height, + (int) g_ctx.item_count - g_ctx.item_top); + for (int row = 0; row < to_show; row++) + { + int item_index = g_ctx.item_top + row; + int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN); + if (item_index == g_ctx.item_selected) + row_attrs = APP_ATTR (SELECTION); + + struct row_buffer buf; + row_buffer_init (&buf); + // TODO: draw the row using view_width + + // Combine attributes used by the handler with the defaults. + // Avoiding attrset() because of row_buffer_flush(). + for (size_t i = 0; i < buf.chars_len; i++) + { + chtype *attrs = &buf.chars[i].attrs; + if (item_index == g_ctx.item_selected) + *attrs = (*attrs & ~(A_COLOR | A_REVERSE)) | row_attrs; + else if ((*attrs & A_COLOR) && (row_attrs & A_COLOR)) + *attrs |= (row_attrs & ~A_COLOR); + else + *attrs |= row_attrs; + } + + move (g_ctx.header_height + row, 0); + app_flush_buffer (&buf, view_width, row_attrs); + } +} + +static void +app_on_refresh (void *user_data) +{ + (void) user_data; + poller_idle_reset (&g_ctx.refresh_event); + + app_draw_header (); + app_draw_view (); + + refresh (); +} + +// --- Actions ----------------------------------------------------------------- + +/// Checks what items are visible and returns if fixes were needed +static bool +app_fix_view_range (void) +{ + if (g_ctx.item_top < 0) + { + g_ctx.item_top = 0; + app_invalidate (); + return false; + } + + // If the contents are at least as long as the screen, always fill it + int max_item_top = (int) g_ctx.item_count - app_visible_items (); + // But don't let that suggest a negative offset + max_item_top = MAX (max_item_top, 0); + + if (g_ctx.item_top > max_item_top) + { + g_ctx.item_top = max_item_top; + app_invalidate (); + return false; + } + return true; +} + +/// Scroll down (positive) or up (negative) @a n items +static bool +app_scroll (int n) +{ + g_ctx.item_top += n; + app_invalidate (); + return app_fix_view_range (); +} + +static void +app_ensure_selection_visible (void) +{ + if (g_ctx.item_selected < 0) + return; + + int too_high = g_ctx.item_top - g_ctx.item_selected; + if (too_high > 0) + app_scroll (-too_high); + + int too_low = g_ctx.item_selected + - (g_ctx.item_top + app_visible_items () - 1); + if (too_low > 0) + app_scroll (too_low); +} + +static bool +app_move_selection (int diff) +{ + int fixed = g_ctx.item_selected += diff; + fixed = MAX (fixed, 0); + fixed = MIN (fixed, (int) g_ctx.item_count - 1); + + bool result = g_ctx.item_selected != fixed; + g_ctx.item_selected = fixed; + app_invalidate (); + + app_ensure_selection_visible (); + return result; +} + +// --- User input handling ----------------------------------------------------- + +enum action +{ + ACTION_NONE, + ACTION_QUIT, + ACTION_REDRAW, + ACTION_CHOOSE, + ACTION_DELETE, + ACTION_SCROLL_UP, + ACTION_SCROLL_DOWN, + ACTION_GOTO_TOP, + ACTION_GOTO_BOTTOM, + ACTION_GOTO_ITEM_PREVIOUS, + ACTION_GOTO_ITEM_NEXT, + ACTION_GOTO_PAGE_PREVIOUS, + ACTION_GOTO_PAGE_NEXT, + ACTION_COUNT +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +app_process_action (enum action action) +{ + switch (action) + { + case ACTION_QUIT: + app_quit (); + break; + case ACTION_REDRAW: + clear (); + app_invalidate (); + break; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // XXX: these should rather be parametrized + case ACTION_SCROLL_UP: + app_scroll (-3); + break; + case ACTION_SCROLL_DOWN: + app_scroll (3); + break; + + case ACTION_GOTO_TOP: + if (g_ctx.item_count) + { + g_ctx.item_selected = 0; + app_ensure_selection_visible (); + app_invalidate (); + } + break; + case ACTION_GOTO_BOTTOM: + if (g_ctx.item_count) + { + g_ctx.item_selected = (int) g_ctx.item_count - 1; + app_ensure_selection_visible (); + app_invalidate (); + } + break; + + case ACTION_GOTO_ITEM_PREVIOUS: + app_move_selection (-1); + break; + case ACTION_GOTO_ITEM_NEXT: + app_move_selection (1); + break; + + case ACTION_GOTO_PAGE_PREVIOUS: + app_scroll ((int) g_ctx.header_height - LINES); + app_move_selection ((int) g_ctx.header_height - LINES); + break; + case ACTION_GOTO_PAGE_NEXT: + app_scroll (LINES - (int) g_ctx.header_height); + app_move_selection (LINES - (int) g_ctx.header_height); + break; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + case ACTION_NONE: + break; + default: + beep (); + return false; + } + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +app_process_left_mouse_click (int line, int column) +{ + if (line == g_ctx.header_height - 1) + { + } + else + { + int row_index = line - g_ctx.header_height; + if (row_index < 0 + || row_index >= (int) g_ctx.item_count - g_ctx.item_top) + return false; + + g_ctx.item_selected = row_index + g_ctx.item_top; + app_invalidate (); + } + return true; +} + +static bool +app_process_mouse (termo_mouse_event_t type, int line, int column, int button) +{ + if (type != TERMO_MOUSE_PRESS) + return true; + + if (button == 1) + return app_process_left_mouse_click (line, column); + else if (button == 4) + return app_process_action (ACTION_SCROLL_UP); + else if (button == 5) + return app_process_action (ACTION_SCROLL_DOWN); + return false; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct binding +{ + const char *key; ///< Key definition + enum action action; ///< Action to take +} +g_default_bindings[] = +{ + { "Escape", ACTION_QUIT }, + { "q", ACTION_QUIT }, + { "C-l", ACTION_REDRAW }, + // TODO: Tab switches endianity + + { "Home", ACTION_GOTO_TOP }, + { "End", ACTION_GOTO_BOTTOM }, + { "M-<", ACTION_GOTO_TOP }, + { "M->", ACTION_GOTO_BOTTOM }, + { "Up", ACTION_GOTO_ITEM_PREVIOUS }, + { "Down", ACTION_GOTO_ITEM_NEXT }, + { "k", ACTION_GOTO_ITEM_PREVIOUS }, + { "j", ACTION_GOTO_ITEM_NEXT }, + { "PageUp", ACTION_GOTO_PAGE_PREVIOUS }, + { "PageDown", ACTION_GOTO_PAGE_NEXT }, + { "C-p", ACTION_GOTO_ITEM_PREVIOUS }, + { "C-n", ACTION_GOTO_ITEM_NEXT }, + { "C-b", ACTION_GOTO_PAGE_PREVIOUS }, + { "C-f", ACTION_GOTO_PAGE_NEXT }, + + // Not sure how to set these up, they're pretty arbitrary so far + { "Enter", ACTION_CHOOSE }, + { "Delete", ACTION_DELETE }, +}; + +static bool +app_process_termo_event (termo_key_t *event) +{ + // TODO: pre-parse the keys, order them by termo_keycmp() and binary search + for (size_t i = 0; i < N_ELEMENTS (g_default_bindings); i++) + { + struct binding *binding = &g_default_bindings[i]; + termo_key_t key; + hard_assert (!*termo_strpkey_utf8 (g_ctx.tk, binding->key, &key, + TERMO_FORMAT_ALTISMETA)); + if (!termo_keycmp (g_ctx.tk, event, &key)) + return app_process_action (binding->action); + } + // TODO: use 0-9 a-f to overwrite nibbles + return false; +} + +// --- Signals ----------------------------------------------------------------- + +static int g_signal_pipe[2]; ///< A pipe used to signal... signals + +/// Program termination has been requested by a signal +static volatile sig_atomic_t g_termination_requested; +/// The window has changed in size +static volatile sig_atomic_t g_winch_received; + +static void +signals_postpone_handling (char id) +{ + int original_errno = errno; + if (write (g_signal_pipe[1], &id, 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +signals_superhandler (int signum) +{ + switch (signum) + { + case SIGWINCH: + g_winch_received = true; + signals_postpone_handling ('w'); + break; + case SIGINT: + case SIGTERM: + g_termination_requested = true; + signals_postpone_handling ('t'); + break; + default: + hard_assert (!"unhandled signal"); + } +} + +static void +signals_setup_handlers (void) +{ + if (pipe (g_signal_pipe) == -1) + exit_fatal ("%s: %s", "pipe", strerror (errno)); + + set_cloexec (g_signal_pipe[0]); + set_cloexec (g_signal_pipe[1]); + + // So that the pipe cannot overflow; it would make write() block within + // the signal handler, which is something we really don't want to happen. + // The same holds true for read(). + set_blocking (g_signal_pipe[0], false); + set_blocking (g_signal_pipe[1], false); + + signal (SIGPIPE, SIG_IGN); + + struct sigaction sa; + sa.sa_flags = SA_RESTART; + sa.sa_handler = signals_superhandler; + sigemptyset (&sa.sa_mask); + + if (sigaction (SIGWINCH, &sa, NULL) == -1 + || sigaction (SIGINT, &sa, NULL) == -1 + || sigaction (SIGTERM, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); +} + +// --- Initialisation, event handling ------------------------------------------ + +static void +app_on_tty_readable (const struct pollfd *fd, void *user_data) +{ + (void) user_data; + if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + poller_timer_reset (&g_ctx.tk_timer); + termo_advisereadable (g_ctx.tk); + + termo_key_t event; + termo_result_t res; + while ((res = termo_getkey (g_ctx.tk, &event)) == TERMO_RES_KEY) + { + int y, x, button; + termo_mouse_event_t type; + if (termo_interpret_mouse (g_ctx.tk, &event, &type, &button, &y, &x)) + { + if (!app_process_mouse (type, y, x, button)) + beep (); + } + else if (!app_process_termo_event (&event)) + beep (); + } + + if (res == TERMO_RES_AGAIN) + poller_timer_set (&g_ctx.tk_timer, termo_get_waittime (g_ctx.tk)); + else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF) + app_quit (); +} + +static void +app_on_key_timer (void *user_data) +{ + (void) user_data; + + termo_key_t event; + if (termo_getkey_force (g_ctx.tk, &event) == TERMO_RES_KEY) + if (!app_process_termo_event (&event)) + app_quit (); +} + +static void +app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data) +{ + (void) user_data; + + char id = 0; + (void) read (fd->fd, &id, 1); + + if (g_termination_requested && !g_ctx.quitting) + app_quit (); + + if (g_winch_received) + { + update_curses_terminal_size (); + app_fix_view_range (); + app_invalidate (); + + g_winch_received = false; + } +} + +static void +app_log_handler (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + // We certainly don't want to end up in a possibly infinite recursion + static bool in_processing; + if (in_processing) + return; + + in_processing = true; + + struct str message; + str_init (&message); + str_append (&message, quote); + str_append_vprintf (&message, fmt, ap); + + // If the standard error output isn't redirected, try our best at showing + // the message to the user + if (!isatty (STDERR_FILENO)) + fprintf (stderr, "%s\n", message.str); + else + { + // TODO: think of a location to print this, maybe over decoding fields + // TODO: remember the position and restore it + move (LINES - 1, 0); + app_write_line (message.str, A_REVERSE); + } + str_free (&message); + + in_processing = false; +} + +static void +app_init_poller_events (void) +{ + poller_fd_init (&g_ctx.signal_event, &g_ctx.poller, g_signal_pipe[0]); + g_ctx.signal_event.dispatcher = app_on_signal_pipe_readable; + poller_fd_set (&g_ctx.signal_event, POLLIN); + + poller_fd_init (&g_ctx.tty_event, &g_ctx.poller, STDIN_FILENO); + g_ctx.tty_event.dispatcher = app_on_tty_readable; + poller_fd_set (&g_ctx.tty_event, POLLIN); + + poller_timer_init (&g_ctx.tk_timer, &g_ctx.poller); + g_ctx.tk_timer.dispatcher = app_on_key_timer; + + poller_idle_init (&g_ctx.refresh_event, &g_ctx.poller); + g_ctx.refresh_event.dispatcher = app_on_refresh; +} + +/// Decode size arguments according to similar rules to those that dd(1) uses; +/// we support octal and hexadecimal numbers but they clash with suffixes +static bool +decode_size (const char *s, uint64_t *out) +{ + char *end; + errno = 0; + uint64_t n = strtoul (s, &end, 0); + if (errno != 0 || end == s) + return false; + + uint64_t f = 1; + switch (*end) + { + case 'c': f = 1 << 0; end++; break; + case 'w': f = 1 << 1; end++; break; + case 'b': f = 1 << 9; end++; break; + + case 'K': f = 1 << 10; if (*++end == 'B') { f = 1e3; end++; } break; + case 'M': f = 1 << 20; if (*++end == 'B') { f = 1e6; end++; } break; + case 'G': f = 1 << 30; if (*++end == 'B') { f = 1e9; end++; } break; + } + if (*end || n > UINT64_MAX / f) + return false; + + *out = n * f; + return true; +} + +int +main (int argc, char *argv[]) +{ + static const struct opt opts[] = + { + { 'd', "debug", NULL, 0, "run in debug mode" }, + { 'h', "help", NULL, 0, "display this help and exit" }, + { 'V', "version", NULL, 0, "output version information and exit" }, + + { 'o', "offset", NULL, 0, "offset within the file" }, + { 's', "size", NULL, 0, "size limit (1G by default)" }, + { 0, NULL, NULL, 0, NULL } + }; + + struct opt_handler oh; + opt_handler_init (&oh, argc, argv, opts, "[FILE]", "Hex viewer."); + uint64_t size_limit = 1 << 30; + + int c; + while ((c = opt_handler_get (&oh)) != -1) + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + opt_handler_usage (&oh, stdout); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + + case 'o': + if (!decode_size (optarg, &g_ctx.data_offset)) + exit_fatal ("invalid offset specified"); + break; + case 's': + if (!decode_size (optarg, &size_limit)) + exit_fatal ("invalid size limit specified"); + break; + default: + print_error ("wrong options"); + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + argc -= optind; + argv += optind; + + // When no filename is given, read from stdin and replace it with the tty + int input_fd; + if (argc == 0) + { + if ((input_fd = dup (STDIN_FILENO)) < 0) + exit_fatal ("cannot read input: %s", strerror (errno)); + close (STDIN_FILENO); + if (open ("/dev/tty", O_RDWR)) + exit_fatal ("cannot open the terminal: %s", strerror (errno)); + } + else if (argc == 1) + { + g_ctx.filename = xstrdup (argv[0]); + if (!(input_fd = open (argv[0], O_RDONLY))) + exit_fatal ("cannot open `%s': %s", argv[0], strerror (errno)); + } + else + { + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + opt_handler_free (&oh); + + // Seek in the file or pipe however we can + static char seek_buf[8192]; + if (lseek (input_fd, g_ctx.data_offset, SEEK_SET) == (off_t) -1) + for (uint64_t remaining = g_ctx.data_offset; remaining; ) + { + ssize_t n_read = read (input_fd, + seek_buf, MIN (remaining, sizeof seek_buf)); + if (n_read <= 0) + exit_fatal ("cannot seek: %s", strerror (errno)); + remaining -= n_read; + } + + // Read up to "size_limit" bytes of data into a buffer + struct str buf; + str_init (&buf); + + while (buf.len < size_limit) + { + str_ensure_space (&buf, 8192); + ssize_t n_read = read (input_fd, buf.str + buf.len, + MIN (size_limit - buf.len, buf.alloc - buf.len)); + if (!n_read) + break; + if (n_read == -1) + exit_fatal ("cannot read input: %s", strerror (errno)); + buf.len += n_read; + } + + g_ctx.data = (uint8_t *) buf.str; + g_ctx.data_len = buf.len; + + // We only need to convert to and from the terminal encoding + if (!setlocale (LC_CTYPE, "")) + print_warning ("failed to set the locale"); + + app_init_context (); + app_load_configuration (); + app_init_terminal (); + signals_setup_handlers (); + app_init_poller_events (); + + // Redirect all messages from liberty so that they don't disrupt display + g_log_message_real = app_log_handler; + + g_ctx.polling = true; + while (g_ctx.polling) + poller_run (&g_ctx.poller); + + endwin (); + g_log_message_real = log_message_stdio; + app_free_context (); + return 0; +} diff --git a/liberty b/liberty new file mode 160000 +Subproject f53b717f3bba27ca1c42486d3742c91967f3fe9 diff --git a/termo b/termo new file mode 160000 +Subproject a9b41e41b7789924465a7e5596a463ed93d8fc2 |