forked from Lephenixnoir/hex-editor
add histogram, discard confirmation, and lazy files
This commit is contained in:
parent
f3578e112d
commit
9606c2d6b9
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
|
|
130
src/main.c
130
src/main.c
|
@ -11,6 +11,7 @@
|
|||
#include <justui/jfkeys.h>
|
||||
#include <justui/jinput.h>
|
||||
#include <justui/jfileselect.h>
|
||||
#include <justui/jpainted.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
@ -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 : "<unnamed source>",
|
||||
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) {
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
#include "source.h"
|
||||
#include <gint/gint.h>
|
||||
#include <gint/defs/util.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
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;
|
||||
}
|
|
@ -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 <stddef.h>
|
||||
|
||||
typedef struct {
|
||||
/* Open file descriptor */
|
||||
int fd;
|
||||
/* File size */
|
||||
int size;
|
||||
|
||||
} lazy_file_t;
|
||||
|
||||
source_t *source_lazy_file_open(char const *path);
|
||||
|
|
|
@ -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,
|
||||
|
|
13
src/source.h
13
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. */
|
||||
|
|
45
src/util.c
45
src/util.c
|
@ -1,13 +1,10 @@
|
|||
#include "util.h"
|
||||
#include <gint/display.h>
|
||||
#include <gint/keyboard.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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];
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
// util: Global utility functions
|
||||
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
|
||||
/* 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);
|
||||
|
|
Loading…
Reference in New Issue