aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2023-06-17 15:50:56 +0200
committerPřemysl Eric Janouch <p@janouch.name>2023-06-19 13:19:48 +0200
commit00d6c5ede909c45d44c904a02f658256130b84ff (patch)
treef8e4ae321ee6e0067f2f26dd435042114f9459b0
parentb87206bb931f10da622a95f8bd2f01b1ad6be706 (diff)
downloadhex-00d6c5ede909c45d44c904a02f658256130b84ff.tar.gz
hex-00d6c5ede909c45d44c904a02f658256130b84ff.tar.xz
hex-00d6c5ede909c45d44c904a02f658256130b84ff.zip
Bump liberty, move the UI to liberty-xui.c
This deduplicates code between nncmpp and hex, while adding functionality.
-rw-r--r--CMakeLists.txt20
-rw-r--r--LICENSE2
-rw-r--r--README.adoc3
-rw-r--r--config.h.in3
-rw-r--r--hex.adoc4
-rw-r--r--hex.c738
-rw-r--r--hex.desktop9
m---------liberty0
8 files changed, 398 insertions, 381 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 732fb15..b54b21d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,7 +20,6 @@ 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")
@@ -45,7 +44,6 @@ set (project_libraries ${Unistring_LIBRARIES}
pkg_search_module (lua lua53 lua5.3 lua-5.3 lua>=5.3)
option (WITH_LUA "Enable support for Lua plugins" ${lua_FOUND})
-
if (WITH_LUA)
if (NOT lua_FOUND)
message (FATAL_ERROR "Lua library not found")
@@ -65,6 +63,18 @@ if (WITH_LUA)
endif ()
endif ()
+pkg_check_modules (x11 x11 xrender xft fontconfig)
+option (WITH_X11 "Build with X11 support" ${x11_FOUND})
+if (WITH_X11)
+ if (NOT x11_FOUND)
+ message (FATAL_ERROR "Some X11 libraries were not found")
+ endif ()
+
+ list (APPEND project_libraries ${x11_LIBRARIES})
+ include_directories (${x11_INCLUDE_DIRS})
+ link_directories (${x11_LIBRARY_DIRS})
+endif ()
+
include_directories (${Unistring_INCLUDE_DIRS}
${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS})
@@ -74,8 +84,6 @@ set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
# Generate a configuration file
-set (HAVE_LUA "${WITH_LUA}")
-
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
${PROJECT_BINARY_DIR}/config.h)
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
@@ -89,6 +97,10 @@ add_threads (${PROJECT_NAME})
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
+if (WITH_X11)
+ install (FILES ${PROJECT_NAME}.desktop
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
+endif ()
# Generate documentation from text markup
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
diff --git a/LICENSE b/LICENSE
index ddfaee0..7b6617a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2016 - 2017, Přemysl Eric Janouch <p@janouch.name>
+Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
diff --git a/README.adoc b/README.adoc
index f2b23d1..32436c6 100644
--- a/README.adoc
+++ b/README.adoc
@@ -3,7 +3,7 @@ hex
'hex' is yet another hex viewer. It automatically interprets fields within
files using a set of Lua scripts, colorizing them and showing descriptions on
-the side.
+the side. It also runs equally well in the terminal, or as an X11 client.
At the moment there aren't that many features and we only have a few decoders.
@@ -24,6 +24,7 @@ Building and Running
Build dependencies: CMake, pkg-config, awk, liberty (included),
termo (included), asciidoctor or asciidoc (recommended but optional) +
Runtime dependencies: ncursesw, libunistring, Lua >= 5.3 (for highlighting)
+Optional runtime dependencies: x11, xft
$ git clone --recursive https://git.janouch.name/p/hex.git
$ mkdir hex/build
diff --git a/config.h.in b/config.h.in
index ba7991d..2551a7f 100644
--- a/config.h.in
+++ b/config.h.in
@@ -5,7 +5,8 @@
#define PROGRAM_VERSION "${PROJECT_VERSION}"
#cmakedefine HAVE_RESIZETERM
-#cmakedefine HAVE_LUA
+#cmakedefine WITH_LUA
+#cmakedefine WITH_X11
#endif // ! CONFIG_H
diff --git a/hex.adoc b/hex.adoc
index d422283..eb5f55d 100644
--- a/hex.adoc
+++ b/hex.adoc
@@ -38,6 +38,10 @@ _G_=1024M, and _GB_=1000M. The default value is 1G.
*-d*, *--debug*::
Run in debug mode.
+*-x*, *--x11*::
+ Use an X11 interface even when run from a terminal.
+ Note that the application may be built with this feature disabled.
+
*-h*, *--help*::
Display a help message and exit.
diff --git a/hex.c b/hex.c
index 0c57aff..3b6d4a9 100644
--- a/hex.c
+++ b/hex.c
@@ -1,7 +1,7 @@
/*
* hex -- hex viewer
*
- * Copyright (c) 2016 - 2017, Přemysl Eric Janouch <p@janouch.name>
+ * Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -20,6 +20,8 @@
// We "need" to have an enum for attributes before including liberty.
// Avoiding colours in the defaults here in order to support dumb terminals.
+// FIXME: we need a 256color default palette that fails gracefully
+// to something like underlined fields
#define ATTRIBUTE_TABLE(XX) \
XX( FOOTER, "footer", -1, -1, 0 ) \
XX( FOOTER_HL, "footer_hl", -1, -1, A_BOLD ) \
@@ -55,49 +57,24 @@ enum
#define LIBERTY_WANT_POLLER
#define LIBERTY_WANT_ASYNC
#include "liberty/liberty.c"
-#include "liberty/liberty-tui.c"
+
+#ifdef WITH_X11
+#define LIBERTY_XUI_WANT_X11
+#endif // WITH_X11
+#include "liberty/liberty-xui.c"
#include <locale.h>
-#include <termios.h>
-#ifndef TIOCGWINSZ
-#include <sys/ioctl.h>
-#endif // ! TIOCGWINSZ
-#include "termo.h"
+#ifdef WITH_LUA
+#include <dirent.h>
-#ifdef HAVE_LUA
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
-
-#include <dirent.h>
-#endif // HAVE_LUA
+#endif // WITH_LUA
#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
-}
-
// --- Application -------------------------------------------------------------
enum
@@ -136,23 +113,18 @@ 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
-#ifdef HAVE_LUA
+#ifdef WITH_LUA
lua_State *L; ///< Lua state
int ref_format; ///< Reference to "string.format"
struct str_map coders; ///< Map of coders by name
-#endif // HAVE_LUA
+#endif // WITH_LUA
// Data:
- char *message; ///< Last logged message
- int message_attr; ///< Attributes for the logged message
-
struct config config; ///< Program configuration
char *filename; ///< Target filename
@@ -176,15 +148,12 @@ static struct app_context
enum endianity endianity; ///< Endianity
- // Emulated widgets:
-
- struct poller_idle refresh_event; ///< Refresh the screen
+ // User interface:
- // Terminal:
+ struct poller_timer message_timer; ///< Message timeout
+ char *message; ///< Last logged message
- termo_t *tk; ///< termo handle
- struct poller_timer tk_timer; ///< termo timeout timer
- bool locale_is_utf8; ///< The locale is Unicode
+ int digitw; ///< Width of a single digit
struct attrs attrs[ATTRIBUTE_COUNT];
}
@@ -277,6 +246,13 @@ app_init_attributes (void)
#undef XX
}
+static bool
+app_on_insufficient_color (void)
+{
+ app_init_attributes ();
+ return true;
+}
+
static void
app_init_context (void)
{
@@ -288,47 +264,10 @@ app_init_context (void)
ARRAY_INIT (g_ctx.marks_by_offset);
ARRAY_INIT (g_ctx.offset_entries);
- // 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 ();
-
- // 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)
- {
- // FIXME: we need a 256color default palette that fails gracefully
- // to something like underlined fields
- 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);
@@ -343,36 +282,14 @@ app_free_context (void)
cstr_set (&g_ctx.filename, NULL);
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;
-}
-
// --- Field marking -----------------------------------------------------------
/// Find the "marks_by_offset" span covering the offset (if any)
@@ -495,30 +412,58 @@ app_flatten_marks (void)
free (current);
}
-// --- Rendering ---------------------------------------------------------------
+// --- Layouting ---------------------------------------------------------------
-static void
-app_invalidate (void)
+enum
+{
+ WIDGET_NONE = 0, WIDGET_HEX, WIDGET_ASCII, WIDGET_ENDIANITY,
+};
+
+struct layout
+{
+ struct widget *head;
+ struct widget *tail;
+};
+
+static struct widget *
+app_label (chtype attrs, const char *label)
{
- poller_idle_set (&g_ctx.refresh_event);
+ return g_xui.ui->label (attrs, 0, label);
}
-static void
-app_flush_buffer (struct row_buffer *buf, int width, chtype attrs)
+static struct widget *
+app_mono_label (chtype attrs, const char *label)
{
- row_buffer_align (buf, width, attrs);
- row_buffer_flush (buf);
- row_buffer_free (buf);
+ return g_xui.ui->label (attrs, XUI_ATTR_MONOSPACE, label);
}
-/// 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)
+static struct widget *
+app_mono_padding (chtype attrs, float width, float height)
+{
+ struct widget *w = g_xui.ui->padding (attrs, width, height);
+ w->width = width * g_ctx.digitw;
+ return w;
+}
+
+static struct widget *
+app_push (struct layout *l, struct widget *w)
+{
+ LIST_APPEND_WITH_TAIL (l->head, l->tail, w);
+ return w;
+}
+
+static struct widget *
+app_push_hfill (struct layout *l, struct widget *w)
{
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, str, attrs);
- app_flush_buffer (&buf, COLS, attrs);
+ w->width = -1;
+ return app_push (l, w);
+}
+
+static struct widget *
+app_push_vfill (struct layout *l, struct widget *w)
+{
+ w->height = -1;
+ return app_push (l, w);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -526,104 +471,130 @@ app_write_line (const char *str, chtype attrs)
static int
app_visible_rows (void)
{
- return MAX (0, LINES - 1 /* bar */ - 3 /* decoder */ - !!g_ctx.message);
+ int occupied = 1 /* bar */ + 3 /* decoder */;
+ return MAX (0, g_xui.height - occupied * g_xui.vunit) / g_xui.vunit;
}
-static void
-app_make_row (struct row_buffer *buf, int64_t addr, int attrs)
+static inline void
+app_layout_cell (struct layout *hex, struct layout *ascii, int attrs,
+ int64_t addr)
{
+ const char *hexa = "0123456789abcdef";
+
+ struct marks_by_offset *marks = app_marks_at_offset (addr);
+ int attrs_mark = attrs;
+ if (marks && marks->color >= 0)
+ attrs_mark = g_ctx.attrs[marks->color].attrs;
+
+ if (addr >= g_ctx.view_cursor
+ && addr < g_ctx.view_cursor + 8)
+ {
+ attrs |= A_UNDERLINE;
+ attrs_mark |= A_UNDERLINE;
+ }
+
+ // TODO: leave it up to the user to decide what should be colored
+ uint8_t cell = g_ctx.data[addr - g_ctx.data_offset];
+ if (addr != g_ctx.view_cursor)
+ {
+ char s[] = { hexa[cell >> 4], hexa[cell & 0xf], 0 };
+ app_push (hex, app_mono_label (attrs, s));
+ }
+ else if (g_ctx.view_skip_nibble)
+ {
+ char s1[] = { hexa[cell >> 4], 0 }, s2[] = { hexa[cell & 0xf], 0 };
+ app_push (hex, app_mono_label (attrs, s1));
+ app_push (hex, app_mono_label (attrs ^ A_REVERSE, s2));
+ }
+ else
+ {
+ char s1[] = { hexa[cell >> 4], 0 }, s2[] = { hexa[cell & 0xf], 0 };
+ app_push (hex, app_mono_label (attrs ^ A_REVERSE, s1));
+ app_push (hex, app_mono_label (attrs, s2));
+ }
+
+ char s[] = { (cell >= 32 && cell < 127) ? cell : '.', 0 };
+ app_push (ascii, app_mono_label (attrs_mark, s));
+}
+
+// XXX: This per-character layouting is very inefficient, but not extremely so.
+static struct widget *
+app_layout_row (int64_t addr, int y, int attrs)
+{
+ struct layout l = {};
char *row_addr_str = xstrdup_printf ("%08" PRIx64, addr);
- row_buffer_append (buf, row_addr_str, attrs);
+ app_push (&l, app_mono_label (attrs, row_addr_str));
free (row_addr_str);
- struct row_buffer ascii = row_buffer_make ();
- row_buffer_append (&ascii, " ", attrs);
+ struct layout hex = {};
+ struct layout ascii = {};
+ app_push (&ascii, app_mono_padding (attrs, 2, 1));
int64_t end_addr = g_ctx.data_offset + g_ctx.data_len;
- const char *hexa = "0123456789abcdef";
for (int x = 0; x < ROW_SIZE; x++)
{
- if (x % 8 == 0) row_buffer_append (buf, " ", attrs);
- if (x % 2 == 0) row_buffer_append (buf, " ", attrs);
+ if (x % 8 == 0) app_push (&hex, app_mono_padding (attrs, 1, 1));
+ if (x % 2 == 0) app_push (&hex, app_mono_padding (attrs, 1, 1));
int64_t cell_addr = addr + x;
if (cell_addr < g_ctx.data_offset
|| cell_addr >= end_addr)
{
- row_buffer_append (buf, " ", attrs);
- row_buffer_append (&ascii, " ", attrs);
+ app_push (&hex, app_mono_padding (attrs, 2, 1));
+ app_push (&ascii, app_mono_padding (attrs, 1, 1));
}
else
- {
- int attrs_mark = attrs;
- struct marks_by_offset *marks = app_marks_at_offset (cell_addr);
- if (marks && marks->color >= 0)
- attrs_mark = g_ctx.attrs[marks->color].attrs;
-
- int highlight = 0;
- if (cell_addr >= g_ctx.view_cursor
- && cell_addr < g_ctx.view_cursor + 8)
- highlight = A_UNDERLINE;
-
- // TODO: leave it up to the user to decide what should be colored
- uint8_t cell = g_ctx.data[cell_addr - g_ctx.data_offset];
- row_buffer_append (buf,
- (char[3]) { hexa[cell >> 4], hexa[cell & 0xf], 0 },
- attrs | highlight);
-
- char s[2] = { (cell >= 32 && cell < 127) ? cell : '.', 0 };
- row_buffer_append (&ascii, s, attrs_mark | highlight);
- }
+ app_layout_cell (&hex, &ascii, attrs, cell_addr);
}
- row_buffer_append_buffer (buf, &ascii);
- row_buffer_free (&ascii);
+
+ struct widget *w = NULL;
+ app_push (&l, (w = xui_hbox (hex.head)))->id = WIDGET_HEX;
+ w->userdata = y;
+ app_push (&l, (w = xui_hbox (ascii.head)))->id = WIDGET_ASCII;
+ w->userdata = y;
+ return xui_hbox (l.head);
}
-static void
-app_draw_view (void)
+static struct widget *
+app_layout_view (void)
{
- move (0, 0);
-
+ struct layout l = {};
int64_t end_addr = g_ctx.data_offset + g_ctx.data_len;
- for (int y = 0; y < app_visible_rows (); y++)
+ for (int y = 0; y <= app_visible_rows (); y++)
{
int64_t addr = g_ctx.view_top + y * ROW_SIZE;
if (addr >= end_addr)
break;
int attrs = (addr / ROW_SIZE & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
-
- struct row_buffer buf = row_buffer_make ();
- app_make_row (&buf, addr, attrs);
- app_flush_buffer (&buf, COLS, attrs);
+ app_push (&l, app_layout_row (addr, y, attrs));
}
+ return xui_vbox (l.head);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static void
-app_draw_info (void)
+static struct widget *
+app_layout_info (void)
{
const struct marks_by_offset *marks;
+ struct layout l = {};
if (!(marks = app_marks_at_offset (g_ctx.view_cursor)))
- return;
+ goto out;
- int x_offset = 70;
struct mark *mark, **iter = g_ctx.offset_entries + marks->marks;
- for (int y = 0; y < app_visible_rows (); y++)
+ for (int y = 0; y <= app_visible_rows (); y++)
{
// TODO: we can use the field background
// TODO: we can keep going through subsequent fields to fill the column
if (!(mark = *iter++))
break;
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf,
- g_ctx.mark_strings.str + mark->description, 0);
-
- move (y, x_offset);
- app_flush_buffer (&buf, COLS - x_offset, 0);
+ const char *description = g_ctx.mark_strings.str + mark->description;
+ app_push (&l, app_label (0, description));
}
+out:
+ return xui_vbox (l.head);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -641,64 +612,93 @@ app_decode (const uint8_t *p, size_t len, enum endianity endianity)
return val;
}
-static void
-app_write_footer (struct row_buffer *b, char id, int len, const char *fmt, ...)
- ATTRIBUTE_PRINTF (4, 5);
+static struct widget *
+app_footer_field (char id, int size, const char *fmt, ...)
+ ATTRIBUTE_PRINTF (3, 4);
-static void
-app_footer_field (struct row_buffer *b, char id, int len, const char *fmt, ...)
+static struct widget *
+app_footer_field (char id, int size, const char *fmt, ...)
{
const char *coding = "";
- if (len <= 1)
+ if (size <= 1)
;
else if (g_ctx.endianity == ENDIANITY_LE)
coding = "le";
else if (g_ctx.endianity == ENDIANITY_BE)
coding = "be";
- char *key = xstrdup_printf ("%c%d%s", id, len * 8, coding);
- row_buffer_append (b, key, APP_ATTR (FOOTER_HL));
+ struct layout l = {};
+ char *key = xstrdup_printf ("%c%d%s", id, size * 8, coding);
+ app_push (&l, app_mono_label (APP_ATTR (FOOTER_HL), key));
free (key);
+ app_push (&l, app_mono_padding (0, 1, 1));
+
va_list ap;
va_start (ap, fmt);
struct str value = str_make ();
str_append_vprintf (&value, fmt, ap);
va_end (ap);
- row_buffer_append (b, value.str, APP_ATTR (FOOTER));
+ // Right-aligned
+ app_push_hfill (&l, g_xui.ui->padding (0, 1, 1));
+ app_push (&l, app_mono_label (APP_ATTR (FOOTER), value.str));
str_free (&value);
+ return xui_hbox (l.head);
}
static void
-app_draw_footer (void)
+app_footer_group (struct layout *out, int size, int64_t u, int64_t s, int align)
{
- move (app_visible_rows (), 0);
+ struct layout l = {};
+ app_push (&l, app_footer_field ('x', size, "%0*" PRIx64, size * 2, u));
+ app_push (&l, app_footer_field ('u', size, "%" PRIu64, u));
+ app_push (&l, app_footer_field ('s', size, "%" PRId64, s));
+ l.head->width = MAX (l.head->width,
+ g_ctx.digitw * align /* sign + ceil(log10(U/INT*_MAX)) */);
+ if (out->head)
+ app_push (out, app_mono_padding (APP_ATTR (FOOTER), 2, 1));
+ app_push (out, xui_vbox (l.head));
+}
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, APP_TITLE, APP_ATTR (BAR));
+static struct widget *
+app_layout_footer (void)
+{
+ struct layout statusl = {};
+ app_push (&statusl, app_label (APP_ATTR (BAR), APP_TITLE));
+ app_push (&statusl, g_xui.ui->padding (APP_ATTR (BAR), 1, 1));
- if (g_ctx.filename)
+ if (g_ctx.message)
+ app_push (&statusl, app_label (APP_ATTR (BAR_HL), g_ctx.message));
+ else if (g_ctx.filename)
{
- row_buffer_append (&buf, " ", APP_ATTR (BAR));
char *filename = (char *) u8_strconv_from_locale (g_ctx.filename);
- row_buffer_append (&buf, filename, APP_ATTR (BAR_HL));
+ app_push (&statusl, app_label (APP_ATTR (BAR_HL), filename));
free (filename);
+ app_push (&statusl, g_xui.ui->padding (APP_ATTR (BAR), 1, 1));
}
- struct str right = str_make ();
- str_append_printf (&right, " %08" PRIx64, g_ctx.view_cursor);
- str_append (&right, g_ctx.endianity == ENDIANITY_LE ? " LE " : " BE ");
+ app_push_hfill (&statusl, g_xui.ui->padding (APP_ATTR (BAR), 1, 1));
+
+ char *address = xstrdup_printf ("%08" PRIx64, g_ctx.view_cursor);
+ app_push (&statusl, app_mono_label (APP_ATTR (BAR), address));
+ free (address);
+ app_push (&statusl, g_xui.ui->padding (APP_ATTR (BAR), 1, 1));
+
+ app_push (&statusl, app_mono_label (APP_ATTR (BAR),
+ g_ctx.endianity == ENDIANITY_LE ? "LE" : "BE"))->id = WIDGET_ENDIANITY;
+ app_push (&statusl, g_xui.ui->padding (APP_ATTR (BAR), 1, 1));
int64_t top = g_ctx.view_top;
int64_t bot = g_ctx.view_top + app_visible_rows () * ROW_SIZE;
+ struct str where = str_make ();
if (top <= g_ctx.data_offset
&& bot >= g_ctx.data_offset + g_ctx.data_len)
- str_append (&right, "All");
+ str_append (&where, "All");
else if (top <= g_ctx.data_offset)
- str_append (&right, "Top");
+ str_append (&where, "Top");
else if (bot >= g_ctx.data_offset + g_ctx.data_len)
- str_append (&right, "Bot");
+ str_append (&where, "Bot");
else
{
int64_t end_addr = g_ctx.data_offset + g_ctx.data_len;
@@ -707,90 +707,67 @@ app_draw_footer (void)
cur -= g_ctx.data_offset / ROW_SIZE;
max -= g_ctx.data_offset / ROW_SIZE;
- str_append_printf (&right, "%2d%%", (int) (100 * cur / max));
+ str_append_printf (&where, "%2d%%", (int) (100 * cur / max));
}
- row_buffer_align (&buf, COLS - right.len, APP_ATTR (BAR));
- row_buffer_append (&buf, right.str, APP_ATTR (BAR));
- app_flush_buffer (&buf, COLS, APP_ATTR (BAR));
- str_free (&right);
+ app_push (&statusl, app_mono_label (APP_ATTR (BAR), where.str));
+ str_free (&where);
int64_t end_addr = g_ctx.data_offset + g_ctx.data_len;
if (g_ctx.view_cursor < g_ctx.data_offset
|| g_ctx.view_cursor >= end_addr)
- return;
+ return xui_hbox (statusl.head);
int64_t len = end_addr - g_ctx.view_cursor;
uint8_t *p = g_ctx.data + (g_ctx.view_cursor - g_ctx.data_offset);
- struct row_buffer x = row_buffer_make ();
- struct row_buffer u = row_buffer_make ();
- struct row_buffer s = row_buffer_make ();
-
+ // TODO: The entire bottom part perhaps should be pre-painted
+ // with APP_ATTR (FOOTER).
+ struct layout groupl = {};
if (len >= 1)
- {
- app_footer_field (&x, 'x', 1, " %02x ", p[0]);
- app_footer_field (&u, 'u', 1, " %4u ", p[0]);
- app_footer_field (&s, 's', 1, " %4d ", (int8_t) p[0]);
- }
+ app_footer_group (&groupl, 1, p[0], (int8_t) p[0], 3 + 4);
if (len >= 2)
{
- uint16_t val = app_decode (p, 2, g_ctx.endianity);
- app_footer_field (&x, 'x', 2, " %04x ", val);
- app_footer_field (&u, 'u', 2, " %6u ", val);
- app_footer_field (&s, 's', 2, " %6d ", (int16_t) val);
+ uint16_t value = app_decode (p, 2, g_ctx.endianity);
+ app_footer_group (&groupl, 2, value, (int16_t) value, 6 + 6);
}
if (len >= 4)
{
- uint32_t val = app_decode (p, 4, g_ctx.endianity);
- app_footer_field (&x, 'x', 4, " %08x ", val);
- app_footer_field (&u, 'u', 4, " %11u ", val);
- app_footer_field (&s, 's', 4, " %11d ", (int32_t) val);
+ uint32_t value = app_decode (p, 4, g_ctx.endianity);
+ app_footer_group (&groupl, 4, value, (int32_t) value, 6 + 11);
}
if (len >= 8)
{
- uint64_t val = app_decode (p, 8, g_ctx.endianity);
- app_footer_field (&x, 'x', 8, " %016" PRIx64, val);
- app_footer_field (&u, 'u', 8, " %20" PRIu64, val);
- app_footer_field (&s, 's', 8, " %20" PRId64, (int64_t) val);
+ uint64_t value = app_decode (p, 8, g_ctx.endianity);
+ app_footer_group (&groupl, 8, value, (int64_t) value, 6 + 20);
}
- app_flush_buffer (&x, COLS, APP_ATTR (FOOTER));
- app_flush_buffer (&u, COLS, APP_ATTR (FOOTER));
- app_flush_buffer (&s, COLS, APP_ATTR (FOOTER));
-
- if (g_ctx.message)
- app_write_line (g_ctx.message, g_ctx.attrs[g_ctx.message_attr].attrs);
+ struct layout lll = {};
+ app_push (&lll, xui_hbox (statusl.head));
+ app_push (&lll, xui_hbox (groupl.head));
+ return xui_vbox (lll.head);
}
static void
-app_on_refresh (void *user_data)
+app_layout (void)
{
- (void) user_data;
- poller_idle_reset (&g_ctx.refresh_event);
-
- erase ();
- app_draw_view ();
- app_draw_info ();
- app_draw_footer ();
+ struct layout topl = {};
+ app_push (&topl, app_layout_view ());
+ app_push (&topl, g_xui.ui->padding (0, 1, 1));
+ app_push_hfill (&topl, app_layout_info ());
- int64_t diff = g_ctx.view_cursor - g_ctx.view_top;
- int64_t y = diff / ROW_SIZE;
- int64_t x = diff % ROW_SIZE;
- if (diff >= 0 && y < app_visible_rows ())
- {
- curs_set (1);
- move (y, 10 + x*2 + g_ctx.view_skip_nibble + x/8 + x/2);
- }
- else
- curs_set (0);
+ struct layout l = {};
+ app_push_vfill (&l, xui_hbox (topl.head));
+ app_push (&l, app_layout_footer ());
- refresh ();
+ struct widget *root = g_xui.widgets = xui_vbox (l.head);
+ root->width = g_xui.width;
+ root->height = g_xui.height;
}
// --- Lua ---------------------------------------------------------------------
-#ifdef HAVE_LUA
+#ifdef WITH_LUA
static void *
app_lua_alloc (void *ud, void *ptr, size_t o_size, size_t n_size)
@@ -1284,7 +1261,7 @@ app_lua_init (void)
strv_free (&v);
}
-#endif // HAVE_LUA
+#endif // WITH_LUA
// --- Actions -----------------------------------------------------------------
@@ -1296,7 +1273,7 @@ app_fix_view_range (void)
if (g_ctx.view_top < data_view_start)
{
g_ctx.view_top = data_view_start;
- app_invalidate ();
+ xui_invalidate ();
return false;
}
@@ -1310,7 +1287,7 @@ app_fix_view_range (void)
if (g_ctx.view_top > max_view_top)
{
g_ctx.view_top = max_view_top;
- app_invalidate ();
+ xui_invalidate ();
return false;
}
return true;
@@ -1321,7 +1298,7 @@ static bool
app_scroll (int n)
{
g_ctx.view_top += n * ROW_SIZE;
- app_invalidate ();
+ xui_invalidate ();
return app_fix_view_range ();
}
@@ -1348,7 +1325,7 @@ app_move_cursor_by_rows (int diff)
bool result = g_ctx.view_cursor == fixed;
g_ctx.view_cursor = fixed;
- app_invalidate ();
+ xui_invalidate ();
app_ensure_selection_visible ();
return result;
@@ -1362,7 +1339,7 @@ app_jump_to_marks (ssize_t i)
g_ctx.view_cursor = g_ctx.marks_by_offset[i].offset;
g_ctx.view_skip_nibble = false;
- app_invalidate ();
+ xui_invalidate ();
app_ensure_selection_visible ();
return true;
}
@@ -1398,7 +1375,7 @@ app_process_action (enum action action)
g_ctx.view_cursor = g_ctx.data_offset;
g_ctx.view_skip_nibble = false;
app_ensure_selection_visible ();
- app_invalidate ();
+ xui_invalidate ();
break;
case ACTION_GOTO_BOTTOM:
if (!g_ctx.data_len)
@@ -1407,7 +1384,7 @@ app_process_action (enum action action)
g_ctx.view_cursor = g_ctx.data_offset + g_ctx.data_len - 1;
g_ctx.view_skip_nibble = false;
app_ensure_selection_visible ();
- app_invalidate ();
+ xui_invalidate ();
break;
case ACTION_GOTO_PAGE_PREVIOUS:
@@ -1434,7 +1411,7 @@ app_process_action (enum action action)
g_ctx.view_cursor--;
app_ensure_selection_visible ();
}
- app_invalidate ();
+ xui_invalidate ();
break;
case ACTION_RIGHT:
if (!g_ctx.view_skip_nibble)
@@ -1448,7 +1425,7 @@ app_process_action (enum action action)
g_ctx.view_cursor++;
app_ensure_selection_visible ();
}
- app_invalidate ();
+ xui_invalidate ();
break;
case ACTION_ROW_START:
@@ -1459,7 +1436,7 @@ app_process_action (enum action action)
g_ctx.view_cursor = new;
g_ctx.view_skip_nibble = false;
- app_invalidate ();
+ xui_invalidate ();
break;
}
case ACTION_ROW_END:
@@ -1470,7 +1447,7 @@ app_process_action (enum action action)
g_ctx.view_cursor = new;
g_ctx.view_skip_nibble = false;
- app_invalidate ();
+ xui_invalidate ();
break;
}
@@ -1491,13 +1468,13 @@ app_process_action (enum action action)
break;
case ACTION_REDRAW:
clear ();
- app_invalidate ();
+ xui_invalidate ();
break;
case ACTION_TOGGLE_ENDIANITY:
g_ctx.endianity = (g_ctx.endianity == ENDIANITY_LE)
? ENDIANITY_BE : ENDIANITY_LE;
- app_invalidate ();
+ xui_invalidate ();
break;
default:
return false;
@@ -1508,59 +1485,76 @@ app_process_action (enum action action)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
-app_process_left_mouse_click (int line, int column)
+app_process_left_mouse_click (struct widget *w, int x, int y)
{
- if (line < 0)
- return false;
+ if (w->id == WIDGET_ENDIANITY)
+ return app_process_action (ACTION_TOGGLE_ENDIANITY);
- if (line == app_visible_rows ())
+ // XXX: This is really ugly.
+ x = x / g_ctx.digitw - 2;
+ y = w->userdata;
+ switch (w->id)
{
- if (column < COLS - 7
- || column >= COLS - 5)
- return false;
- return app_process_action (ACTION_TOGGLE_ENDIANITY);
+ case WIDGET_HEX:
+ x -= x/5 + x/21;
+ g_ctx.view_skip_nibble = x % 2;
+ x /= 2;
+ break;
+ case WIDGET_ASCII:
+ g_ctx.view_skip_nibble = false;
+ break;
+ default:
+ return true;
}
- else if (line < app_visible_rows ())
- {
- // TODO: when holding a mouse button over a mark string,
- // go to a locked mode that highlights that entire mark
- // (probably by inverting colors)
- // TODO: employ strict checking here before the autofix
- int offset;
- if (column >= 10 && column < 50)
- {
- offset = column - 10;
- offset -= offset/5 + offset/21;
- g_ctx.view_skip_nibble = offset % 2;
- offset /= 2;
- }
- else if (column >= 52 && column < 68)
- {
- offset = column - 52;
- g_ctx.view_skip_nibble = false;
- }
- else
- return false;
+ g_ctx.view_cursor = g_ctx.view_top + y * ROW_SIZE + x;
+ return app_move_cursor_by_rows (0);
+}
+
+/// Returns the deepest child at the cursor that has a non-zero ID, if any.
+static struct widget *
+app_find_widget (struct widget *list, int x, int y)
+{
+ struct widget *target = NULL;
+ LIST_FOR_EACH (struct widget, w, list)
+ {
+ if (x < w->x || x >= w->x + w->width
+ || y < w->y || y >= w->y + w->height)
+ continue;
- g_ctx.view_cursor = g_ctx.view_top + line * ROW_SIZE + offset;
- return app_move_cursor_by_rows (0);
+ struct widget *child = app_find_widget (w->children, x, y);
+ if (child)
+ target = child;
+ else if (w->id)
+ target = w;
}
- return true;
+ return target;
}
static bool
-app_process_mouse (termo_mouse_event_t type, int line, int column, int button)
+app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
+ int modifiers)
{
+ (void) modifiers;
+
+ // TODO: when holding a mouse button over a mark string,
+ // go to a locked mode that highlights that entire mark
+ // (probably by inverting colors)
if (type != TERMO_MOUSE_PRESS)
return true;
-
- if (button == 1)
- return app_process_left_mouse_click (line, column);
- else if (button == 4)
+ if (button == 4)
return app_process_action (ACTION_SCROLL_UP);
- else if (button == 5)
+ if (button == 5)
return app_process_action (ACTION_SCROLL_DOWN);
+
+ struct widget *target = app_find_widget (g_xui.widgets, x, y);
+ if (!target)
+ return false;
+
+ x -= target->x;
+ y -= target->y;
+ if (button == 1)
+ return app_process_left_mouse_click (target, x, y);
return false;
}
@@ -1615,7 +1609,7 @@ g_default_bindings[] =
static int
app_binding_cmp (const void *a, const void *b)
{
- return termo_keycmp (g_ctx.tk,
+ return termo_keycmp (g_xui.tk,
&((struct binding *) a)->decoded, &((struct binding *) b)->decoded);
}
@@ -1625,7 +1619,7 @@ app_init_bindings (void)
for (size_t i = 0; i < N_ELEMENTS (g_default_bindings); i++)
{
struct binding *binding = &g_default_bindings[i];
- hard_assert (!*termo_strpkey_utf8 (g_ctx.tk,
+ hard_assert (!*termo_strpkey_utf8 (g_xui.tk,
binding->key, &binding->decoded, TERMO_FORMAT_ALTISMETA));
}
qsort (g_default_bindings, N_ELEMENTS (g_default_bindings),
@@ -1713,72 +1707,58 @@ signals_setup_handlers (void)
// --- Initialisation, event handling ------------------------------------------
static void
-app_on_tty_readable (const struct pollfd *fd, void *user_data)
+app_on_signal_pipe_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);
+ char id = 0;
+ (void) read (fd->fd, &id, 1);
- 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;
- bool success;
- if (termo_interpret_mouse (g_ctx.tk, &event, &type, &button, &y, &x))
- success = app_process_mouse (type, y, x, button);
- else
- success = app_process_termo_event (&event);
+ if (g_termination_requested)
+ app_quit ();
- if (!success)
- beep ();
+ if (g_winch_received)
+ {
+ g_winch_received = false;
+ if (g_xui.ui->winch)
+ g_xui.ui->winch ();
+ app_fix_view_range ();
}
+}
- 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_show_message (char *message)
+{
+ cstr_set (&g_ctx.message, message);
+ poller_timer_set (&g_ctx.message_timer, 5000);
+ xui_invalidate ();
}
static void
-app_on_key_timer (void *user_data)
+app_hide_message (void)
{
- (void) user_data;
+ if (!g_ctx.message)
+ return;
- termo_key_t event;
- if (termo_getkey_force (g_ctx.tk, &event) == TERMO_RES_KEY)
- if (!app_process_termo_event (&event))
- app_quit ();
+ cstr_set (&g_ctx.message, NULL);
+ poller_timer_reset (&g_ctx.message_timer);
+ xui_invalidate ();
}
static void
-app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
+app_on_message_timer (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)
- {
- g_winch_received = false;
- update_curses_terminal_size ();
- app_fix_view_range ();
- app_invalidate ();
- }
+ app_hide_message ();
}
static void
app_log_handler (void *user_data, const char *quote, const char *fmt,
va_list ap)
{
+ (void) user_data;
+
// We certainly don't want to end up in a possibly infinite recursion
static bool in_processing;
if (in_processing)
@@ -1790,37 +1770,33 @@ app_log_handler (void *user_data, const char *quote, const char *fmt,
str_append (&message, quote);
str_append_vprintf (&message, fmt, ap);
+ app_show_message (xstrdup (message.str));
+
// If the standard error output isn't redirected, try our best at showing
// the message to the user
- if (!isatty (STDERR_FILENO))
+ if (g_debug_mode && !isatty (STDERR_FILENO))
fprintf (stderr, "%s\n", message.str);
- else
- {
- cstr_set (&g_ctx.message, xstrdup (message.str));
- g_ctx.message_attr = (intptr_t) user_data;
- app_invalidate ();
- }
str_free (&message);
in_processing = false;
}
static void
+app_on_clipboard_copy (const char *text)
+{
+ // TODO: Resolve encoding.
+ print_status ("Text copied to clipboard: %s", text);
+}
+
+static void
app_init_poller_events (void)
{
g_ctx.signal_event = poller_fd_make (&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);
- g_ctx.tty_event = poller_fd_make (&g_ctx.poller, STDIN_FILENO);
- g_ctx.tty_event.dispatcher = app_on_tty_readable;
- poller_fd_set (&g_ctx.tty_event, POLLIN);
-
- g_ctx.tk_timer = poller_timer_make (&g_ctx.poller);
- g_ctx.tk_timer.dispatcher = app_on_key_timer;
-
- g_ctx.refresh_event = poller_idle_make (&g_ctx.poller);
- g_ctx.refresh_event.dispatcher = app_on_refresh;
+ g_ctx.message_timer = poller_timer_make (&g_ctx.poller);
+ g_ctx.message_timer.dispatcher = app_on_message_timer;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -1860,17 +1836,21 @@ main (int argc, char *argv[])
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
+#ifdef WITH_X11
+ { 'x', "x11", NULL, 0, "use X11 even when run from a terminal" },
+#endif // WITH_X11
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 'o', "offset", "OFFSET", 0, "offset within the file" },
{ 's', "size", "SIZE", 0, "size limit (1G by default)" },
-#ifdef HAVE_LUA
+#ifdef WITH_LUA
{ 't', "type", "TYPE", 0, "force interpretation as the given type" },
-#endif // HAVE_LUA
+#endif // WITH_LUA
{ 0, NULL, NULL, 0, NULL }
};
+ bool requested_x11 = false;
struct opt_handler oh = opt_handler_make (argc, argv, opts, "[FILE]",
"Interpreting hex viewer.");
int64_t size_limit = 1 << 30;
@@ -1883,6 +1863,9 @@ main (int argc, char *argv[])
case 'd':
g_debug_mode = true;
break;
+ case 'x':
+ requested_x11 = true;
+ break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
@@ -1910,7 +1893,7 @@ main (int argc, char *argv[])
argc -= optind;
argv += optind;
-#ifdef HAVE_LUA
+#ifdef WITH_LUA
// We do it at this questionable location to catch plugin failure before
// we read potentially hundreds of megabytes of data in
app_lua_init ();
@@ -1922,7 +1905,7 @@ main (int argc, char *argv[])
puts (iter.link->key);
exit (EXIT_SUCCESS);
}
-#endif // HAVE_LUA
+#endif // WITH_LUA
// When no filename is given, read from stdin and replace it with the tty
int input_fd;
@@ -1984,7 +1967,7 @@ main (int argc, char *argv[])
print_warning ("failed to set the locale");
app_init_context ();
-#ifdef HAVE_LUA
+#ifdef WITH_LUA
// TODO: eventually we should do this in a separate thread after load
// as it may take a long time (-> responsivity) and once we allow the user
// to edit the file, each change will need a background rescan
@@ -2003,15 +1986,22 @@ main (int argc, char *argv[])
exit_fatal ("Lua: decoding failed: %s", lua_tostring (g_ctx.L, -1));
lua_pop (g_ctx.L, 1);
-#endif // HAVE_LUA
+#endif // WITH_LUA
app_flatten_marks ();
app_load_configuration ();
- app_init_terminal ();
signals_setup_handlers ();
app_init_poller_events ();
- app_invalidate ();
+
+ xui_preinit ();
app_init_bindings ();
+ xui_start (&g_ctx.poller,
+ requested_x11, g_ctx.attrs, N_ELEMENTS (g_ctx.attrs));
+ xui_invalidate ();
+
+ struct widget *w = g_xui.ui->label (0, XUI_ATTR_MONOSPACE, "8");
+ g_ctx.digitw = w->width;
+ widget_destroy (w);
// Redirect all messages from liberty so that they don't disrupt display
g_log_message_real = app_log_handler;
@@ -2020,14 +2010,14 @@ main (int argc, char *argv[])
while (g_ctx.polling)
poller_run (&g_ctx.poller);
- endwin ();
+ xui_stop ();
g_log_message_real = log_message_stdio;
app_free_context ();
-#ifdef HAVE_LUA
+#ifdef WITH_LUA
str_map_free (&g_ctx.coders);
lua_close (g_ctx.L);
-#endif // HAVE_LUA
+#endif // WITH_LUA
return 0;
}
diff --git a/hex.desktop b/hex.desktop
new file mode 100644
index 0000000..d4351a6
--- /dev/null
+++ b/hex.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Name=hex
+GenericName=Interpreting hex viewer
+Icon=hex
+Exec=hex %f
+NoDisplay=true
+StartupNotify=false
+Categories=Utility;
diff --git a/liberty b/liberty
-Subproject 34f86651f6220038c0ee07d3f422a52d9b081f0
+Subproject d01a1ff0348174f91bb2d3ba53145cc2c9f50a7