add histogram, discard confirmation, and lazy files

This commit is contained in:
Lephenixnoir 2022-06-23 19:40:23 +01:00
parent f3578e112d
commit 9606c2d6b9
Signed by untrusted user: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
9 changed files with 292 additions and 39 deletions

View File

@ -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
)

View File

@ -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);

View File

@ -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) {

112
src/source-lazy-file.c Normal file
View File

@ -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;
}

View File

@ -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);

View File

@ -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,

View File

@ -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. */

View File

@ -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];

View File

@ -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);