From 9606c2d6b9de869e27f9f0eef83a8bda8d8a5029 Mon Sep 17 00:00:00 2001 From: Lephenixnoir Date: Thu, 23 Jun 2022 19:40:23 +0100 Subject: [PATCH] add histogram, discard confirmation, and lazy files --- CMakeLists.txt | 1 + src/heditor.c | 4 +- src/main.c | 130 ++++++++++++++++++++++++++++++++------- src/source-lazy-file.c | 112 +++++++++++++++++++++++++++++++++ src/source-lazy-file.h | 19 +++++- src/source-loaded-file.c | 3 +- src/source.h | 13 ++-- src/util.c | 45 ++++++++++++-- src/util.h | 4 ++ 9 files changed, 292 insertions(+), 39 deletions(-) create mode 100644 src/source-lazy-file.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f5cd69..52a5c05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ set(SOURCES src/heditor.c src/main.c src/source.c + src/source-lazy-file.c src/source-loaded-file.c src/util.c ) diff --git a/src/heditor.c b/src/heditor.c index 976afbc..ab0daba 100644 --- a/src/heditor.c +++ b/src/heditor.c @@ -164,7 +164,7 @@ bool heditor_save(heditor *e) void heditor_set_insert_mode(heditor *e, bool insert) { e->insert = false; - if(e->source && e->source->intf->cap & SOURCE_EXPAND) + if(e->source && e->source->intf->cap & SOURCE_RESIZE) e->insert = insert; e->widget.update = true; } @@ -339,7 +339,7 @@ static bool heditor_poly_event(void *e0, jevent ev) if(!(e->source->intf->cap & SOURCE_WRITE)) return false; - if(e->insert && !(e->source->intf->cap & SOURCE_EXPAND)) + if(e->insert && !(e->source->intf->cap & SOURCE_RESIZE)) return false; int hexdigit = get_hexdigit(key); diff --git a/src/main.c b/src/main.c index 88a8ef6..b45b1e3 100644 --- a/src/main.c +++ b/src/main.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -27,8 +28,9 @@ struct { heditor *editor; /* Source info widget. */ jlabel *source_info; - /* Source stats widget. */ + /* Source stats widget and histogram. */ jlabel *source_stats; + jpainted *source_histogram; /* Current data source. */ source_t *source; @@ -39,6 +41,9 @@ struct { int view; /* Current position in the menus. */ int fkmenu; + + /* Histogram data. */ + int32_t histogram[256]; } app; enum { @@ -73,14 +78,14 @@ static void update_fkeys(void) if(!app.source) jfkeys_set(app.fkeys, "/OPEN;;;;;"); else { - bool can_insert = (app.source->intf->cap & SOURCE_EXPAND) != 0; + bool can_insert = (app.source->intf->cap & SOURCE_RESIZE) != 0; fkeys_sprintf(app.fkeys, "/OPEN;/SOURCE;%s%s;@GOTO;;#VIEW", can_insert ? "@" : "#", app.insert ? "INSERT" : "OVERW"); } } else if(app.fkmenu == FK_EDITOR_SOURCE) { - jfkeys_set(app.fkeys, "@SAVE;@SAVE AS;@FRONT;;;"); + jfkeys_set(app.fkeys, "@SAVE;@SAVE AS;;;;"); } else if(app.fkmenu == FK_EDITOR_OPEN) { jfkeys_set(app.fkeys, "@FILE;@LAZY;@MEMORY;@NEW"); @@ -99,6 +104,11 @@ static void update_fkeys(void) } } +static bool has_unsaved_changes(void) +{ + return app.editor->front_buffer_dirty || source_dirty(app.source); +} + static void update_status(void) { source_t *s = app.source; @@ -131,7 +141,6 @@ static void update_status(void) static void update_source_info(void) { source_t *s = app.source; - if(!s) { jlabel_set_text(app.source_info, "No source."); return; @@ -142,31 +151,95 @@ static void update_source_info(void) if(s->intf->cap & SOURCE_WRITE) strcat(caps, " WRITE"); - if(s->intf->cap & SOURCE_EXPAND) - strcat(caps, " EXPAND"); - if(s->intf->cap & SOURCE_SHRINK) - strcat(caps, " SHRINK"); + if(s->intf->cap & SOURCE_RESIZE) + strcat(caps, " RESIZE"); jlabel_asprintf(app.source_info, "%s\n" "Size: %d bytes\n" "Front buffer: %p, size %d/%d (x%d)\n" "Front buffer maps to: @%d, size %d\n" + "Source type: %s\n" "Capabilities: %s", s->origin ? s->origin : "", source_size(s), s->buf->mem, s->buf->data_size, s->buf->mem_size, s->buf->block_size, s->buf_offset, s->buf_segment_size, + s->intf->name, caps + 1); } +#define HIST_W 256 +#define HIST_H 150 + +static void update_stats_label(char const *message) +{ + jlabel_set_text(app.source_stats, message); +} + +static void update_stats(void) +{ + memset(app.histogram, 0, sizeof app.histogram); + + source_t *s = app.source; + if(!s) return update_stats_label("No source."); + + int const BUFFER_SIZE = 4096; + uint8_t *buf = malloc(BUFFER_SIZE); + if(!buf) return update_stats_label("Out of memory!"); + + int size = source_size(s); + for(off_t offset = 0; offset < size;) { + int rc = s->intf->read(s->cookie, buf, offset, BUFFER_SIZE); + if(rc <= 0) return update_stats_label("Read error!"); + + for(int i = 0; i < rc; i++) + app.histogram[buf[i]]++; + offset += rc; + } + app.source_histogram->widget.update = true; + + free(buf); + jlabel_asprintf(app.source_stats, "Bytes processed: %d", size); +} + +static void render_stats_histogram(int x, int y) +{ + int M = 0; + for(int i = 0; i < 256; i++) + M = max(M, app.histogram[i]); + + int bars_h = HIST_H - 20; + int bars_y = y + bars_h; + + dline(x, bars_y+1, x + HIST_W - 1, bars_y+1, C_BLACK); + dtext_opt(x, bars_y+4, C_BLACK, C_NONE, DTEXT_LEFT, DTEXT_TOP, "0x00", -1); + dtext_opt(x + HIST_W - 1, bars_y+4, C_BLACK, C_NONE, DTEXT_RIGHT, + DTEXT_TOP, "0xff", -1); + if(!M) return; + + for(int i = 0; i < 256; i++) { + if(app.histogram[i] == 0) + continue; + + int bar_height = max(1, app.histogram[i] * bars_h / M); + int color = (i & 1) ? C_RGB(4, 4, 4) : C_RGB(16, 16, 16); + dline(x+i, bars_y-1, x+i, bars_y-bar_height, color); + } +} + void hex_view(void) { + memset(&app, 0, sizeof app); app.source = NULL; app.insert = false; app.view = VIEW_EDITOR; app.fkmenu = FK_EDITOR; + bool file_open_lazy = false; + + // + jscene *scene = jscene_create_fullscreen(NULL); jlabel *title = jlabel_create("Hex Editor", scene); jwidget *stack = jwidget_create(scene); @@ -237,9 +310,16 @@ void hex_view(void) jwidget_set_stretch(tab_stats, 1, 1, false); jlayout_set_vbox(tab_stats)->spacing = 3; - jlabel *source_stats = jlabel_create("TODO", tab_stats); + jlabel *source_stats = jlabel_create("", tab_stats); + jlabel_set_block_alignment(source_stats, J_ALIGN_LEFT, J_ALIGN_TOP); + jwidget_set_stretch(source_stats, 1, 1, false); app.source_stats = source_stats; + jpainted *source_histogram = jpainted_create(render_stats_histogram, + NULL, HIST_W, HIST_H, tab_stats); + jwidget_set_margin(source_histogram, 8, 8, 8, 8); + app.source_histogram = source_histogram; + // Initial state // update_status(); @@ -281,7 +361,10 @@ void hex_view(void) if(file) { if(app.source) source_free(app.source); - app.source = source_loaded_file_open(file); + if(file_open_lazy) + app.source = source_lazy_file_open(file); + else + app.source = source_loaded_file_open(file); app.fkmenu = FK_EDITOR; app.insert = false; source_load(app.source, 0, 2048); @@ -314,7 +397,7 @@ void hex_view(void) update_fkeys(); } else if(key == KEY_F3 && app.source) { - if(app.source->intf->cap & SOURCE_EXPAND) { + if(app.source->intf->cap & SOURCE_RESIZE) { app.insert = !app.insert; heditor_set_insert_mode(mem, app.insert); update_fkeys(); @@ -339,12 +422,6 @@ void hex_view(void) else if(key == KEY_F2 && app.source) { /* TODO: Source save as */ } - else if(key == KEY_F3 && app.source) { - if(heditor_save_front_buffer(mem)) { - app.fkmenu = FK_EDITOR; - update_fkeys(); - } - } else if(key == KEY_EXIT) { app.fkmenu = FK_EDITOR; update_fkeys(); @@ -352,14 +429,20 @@ void hex_view(void) } else if(app.fkmenu == FK_EDITOR_OPEN) { if(key == KEY_F1) { - /* TODO: Check if current source is unsaved? */ - jfileselect_browse(fileselect, "/"); - jscene_show_and_focus(scene, fileselect); + if(!has_unsaved_changes() || confirm_discard()) { + file_open_lazy = false; + jfileselect_browse(fileselect, "/"); + jscene_show_and_focus(scene, fileselect); + } + scene->widget.update = true; } else if(key == KEY_F2) { - /* TODO: Check if current source is unsaved? */ - jfileselect_browse(fileselect, "/"); - jscene_show_and_focus(scene, fileselect); + if(!has_unsaved_changes() || confirm_discard()) { + file_open_lazy = true; + jfileselect_browse(fileselect, "/"); + jscene_show_and_focus(scene, fileselect); + } + scene->widget.update = true; } else if(key == KEY_F3) { /* TODO: Memory browser */ @@ -402,6 +485,7 @@ void hex_view(void) jscene_show_and_focus(scene, source_stats); app.fkmenu = FK_STATS; app.view = VIEW_STATS; + update_stats(); update_fkeys(); } else if(key == KEY_EXIT) { diff --git a/src/source-lazy-file.c b/src/source-lazy-file.c new file mode 100644 index 0000000..1858168 --- /dev/null +++ b/src/source-lazy-file.c @@ -0,0 +1,112 @@ +#include "source.h" +#include +#include +#include +#include +#include + +static void lz_free(lazy_file_t *lz) +{ + if(lz && lz->fd >= 0) + close(lz->fd); + free(lz); +} + +static ssize_t lz_read(void *_cookie, void *buf, off_t offset, size_t size) +{ + lazy_file_t *lz = _cookie; + + /* Here we need to be careful. We can't just lseek() or pread() because the + file is open in read/Write mode, so that would extend the file. */ + if(offset < 0 || offset >= lz->size) + return -1; + size = min(size, lz->size - offset); + + int rc = lseek(lz->fd, offset, SEEK_SET); + if(rc < 0) + return -1; + + return read(lz->fd, buf, size); +} + +static bool lz_write(void *_cookie, void *data, size_t data_size, off_t offset, + size_t segment_size) +{ + lazy_file_t *lz = _cookie; + if(data_size != segment_size) + return false; + + if(offset < 0 || offset >= lz->size) + return -1; + data_size = min(data_size, lz->size - offset); + + int rc = lseek(lz->fd, offset, SEEK_SET); + if(rc < 0) + return -1; + + return write(lz->fd, data, data_size); +} + +static size_t lz_size(void *_cookie) +{ + lazy_file_t *lz = _cookie; + return lz->size; +} + +static bool lz_dirty(void *_cookie) +{ + /* The file itself cannot be dirty, it's *the* file in Flash */ + (void)_cookie; + return false; +} + +static bool lz_save(void *_cookie, char const *destination) +{ + /* When destination is NULL we have nothing to do since your backing is the + live file. When it's not then we don't support the save because it would + need to do a chunked copy, which I don't want to write yet */ + (void)_cookie; + return (destination == NULL); +} + +static void lz_close(void *_cookie) +{ + lazy_file_t *lz = _cookie; + lz_free(lz); +} + +static source_intf_t lz_intf = { + .name = "Lazy File", + .cap = SOURCE_WRITE, + .read = lz_read, + .write = lz_write, + .size = lz_size, + .dirty = lz_dirty, + .save = lz_save, + .close = lz_close, +}; + +source_t *source_lazy_file_open(char const *path) +{ + source_t *source = NULL; + lazy_file_t *lz = NULL; + + lz = malloc(sizeof *lz); + if(!lz) goto fail; + + lz->fd = open(path, O_RDWR); + if(lz->fd < 0) goto fail; + + lz->size = lseek(lz->fd, 0, SEEK_END); + if(lz->size <= 0) goto fail; + + source = source_open(&lz_intf, lz, path); + if(!source) goto fail; + + return source; + +fail: + free(source); + lz_close(lz); + return NULL; +} diff --git a/src/source-lazy-file.h b/src/source-lazy-file.h index 70b786d..ba0ceca 100644 --- a/src/source-lazy-file.h +++ b/src/source-lazy-file.h @@ -1 +1,18 @@ -// TODO +// source-lazy-file: A file lazily loaded to memory +// +// This data source is a regular file that is not loaded to RAM; only the front +// buffer is loaded. Because we don't control the entire file, this source does +// not support any size-changing capability. + +#pragma once +#include + +typedef struct { + /* Open file descriptor */ + int fd; + /* File size */ + int size; + +} lazy_file_t; + +source_t *source_lazy_file_open(char const *path); diff --git a/src/source-loaded-file.c b/src/source-loaded-file.c index 2e3b5e3..4eafb70 100644 --- a/src/source-loaded-file.c +++ b/src/source-loaded-file.c @@ -86,7 +86,8 @@ static void lf_close(void *_cookie) } static source_intf_t lf_intf = { - .cap = SOURCE_WRITE | SOURCE_EXPAND | SOURCE_SHRINK, + .name = "Loaded File", + .cap = SOURCE_WRITE | SOURCE_RESIZE, .read = lf_read, .write = lf_write, .size = lf_size, diff --git a/src/source.h b/src/source.h index 4ace289..a78f3e0 100644 --- a/src/source.h +++ b/src/source.h @@ -14,22 +14,21 @@ /* The data source is writable and can be modified by API. */ #define SOURCE_WRITE 0x01 -/* The data source can expand, adding new bytes at the end. */ -#define SOURCE_EXPAND 0x02 -/* The data source can shrink, removing bytes at the end. */ -#define SOURCE_SHRINK 0x03 +/* The data source can by resized, adding or removing new bytes. */ +#define SOURCE_RESIZE 0x02 // Interface to data sources // typedef struct { + /* Interface name */ + char const *name; /* Source capabilities */ int cap; /* Load a chunk from the data source into a buffer. */ ssize_t (*read)(void *_cookie, void *buf, off_t offset, size_t size); /* Write from a buffer into a segment of the data source. If the interface - has the SOURCE_EXPAND capability, the buffer might be larger than the - segment. Similarly, if the interface has the SOURCE_SHRINK capability, - the buffer might be smaller than the segment. */ + has the SOURCE_RESIZE capability, the buffer might be of a different + size than the segment. */ bool (*write)(void *_cookie, void *buf, size_t buf_size, off_t offset, size_t segment_size); /* Current source data size. */ diff --git a/src/util.c b/src/util.c index 1f37325..fd1d1de 100644 --- a/src/util.c +++ b/src/util.c @@ -1,13 +1,10 @@ #include "util.h" #include +#include #include -void please_wait(void) +void popup(int w, int h, int *x_ptr, int *y_ptr) { - int w, h; - char const *message = "Please wait..."; - dsize(message, NULL, &w, &h); - int x = DWIDTH / 2; int y = DHEIGHT / 2; @@ -17,10 +14,48 @@ void please_wait(void) drect(x - w/2 - mx, y - h/2 - my, x + w/2 + mx, y + h/2 + my, C_WHITE); drect_border(x - w/2 - bx, y - h/2 - by, x + w/2 + bx, y + h/2 + by, C_WHITE, 1, C_BLACK); + + *x_ptr = x; + *y_ptr = y; +} + +void please_wait(void) +{ + int w, h, x, y; + char const *message = "Please wait..."; + dsize(message, NULL, &w, &h); + popup(w, h, &x, &y); + dtext_opt(x, y, C_BLACK, C_NONE, DTEXT_CENTER, DTEXT_MIDDLE, message, -1); dupdate(); } +bool confirm_discard(void) +{ + int w=210, h=42, x, y; + popup(w, h, &x, &y); + + dtext_opt(x-w/2, y-15, C_BLACK, C_NONE, DTEXT_LEFT, DTEXT_MIDDLE, + "There are unsaved changes.", -1); + dtext_opt(x-w/2, y, C_BLACK, C_NONE, DTEXT_LEFT, DTEXT_MIDDLE, + "[EXE]: Discard them", -1); + dtext_opt(x-w/2, y+15, C_BLACK, C_NONE, DTEXT_LEFT, DTEXT_MIDDLE, + "[EXIT]: Go back to editor", -1); + + dupdate(); + + while(1) { + key_event_t ev = getkey(); + if(ev.type != KEYEV_DOWN) + continue; + + if(ev.key == KEY_EXE) + return true; + if(ev.key == KEY_ACON || ev.key == KEY_EXIT) + return false; + } +} + char const *human_size(int size, char *str) { static char default_buffer[64]; diff --git a/src/util.h b/src/util.h index 435f4e4..84e126e 100644 --- a/src/util.h +++ b/src/util.h @@ -1,11 +1,15 @@ // util: Global utility functions #pragma once +#include /* Show the "please wait" popup message over the current display. This gets overridden at the next application frame. Does draw to VRAM. */ void please_wait(void); +/* Show the "unsaved changes" popup. */ +bool confirm_discard(void); + /* Human version of file sizes. If str=NULL, returns a static buffer that is overridden by further calls. */ char const *human_size(int size, char *str);