aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Janouch <p.janouch@gmail.com>2016-12-27 15:26:51 +0100
committerPřemysl Janouch <p.janouch@gmail.com>2016-12-29 16:48:56 +0100
commitd0eee678b59c703cec15431b66cfcac9e5741815 (patch)
tree33943c9a9599b4566ddacac9094cc8c73743c5df
downloadhex-d0eee678b59c703cec15431b66cfcac9e5741815.tar.gz
hex-d0eee678b59c703cec15431b66cfcac9e5741815.tar.xz
hex-d0eee678b59c703cec15431b66cfcac9e5741815.zip
Initial commit
This is essentially a gutted fork of nncmpp that doesn't do anything.
-rw-r--r--.gitignore9
-rw-r--r--.gitmodules6
-rw-r--r--CMakeLists.txt116
-rw-r--r--LICENSE15
-rw-r--r--README.adoc86
-rw-r--r--cmake/FindNcursesw.cmake17
-rw-r--r--cmake/FindUnistring.cmake10
-rw-r--r--config.h.in10
-rw-r--r--hex.c1295
m---------liberty0
m---------termo0
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)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ce263ae
--- /dev/null
+++ b/LICENSE
@@ -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
+
diff --git a/hex.c b/hex.c
new file mode 100644
index 0000000..2491fc0
--- /dev/null
+++ b/hex.c
@@ -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 (&current.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