Newer
Older
ddhx / src / ddhx.d
/**
 * Main rendering engine.
 */
module ddhx;

import std.stdio : write, writeln, writef, writefln;
import std.file : getSize;
import std.mmfile;
import core.stdc.stdio : printf, fflush, puts, snprintf;
import core.stdc.string : memset;
import menu, ddcon;
import utils : formatsize, unformat;

__gshared:

/// Copyright string
enum COPYRIGHT = "Copyright (c) dd86k <dd@dax.moe> 2017-2021";

/// App version
enum APP_VERSION = "0.2.2";

/// Offset type (hex, dec, etc.)
enum OffsetType {
	Hex, Decimal, Octal
}

/// 
enum DisplayMode {
	Default, Text, Data
}

/// Default character for non-displayable characters
enum DEFAULT_CHAR = '.';

/// Character table for header row
private immutable char[] offsetTable = [
	'h', 'd', 'o'
];
/// Character table for the main panel for printf
private immutable char[] formatTable = [
	'X', 'u', 'o'
];

//
// User settings
//

//TODO: Mark these as private and have a function to set them

/// How many bytes are shown per row
ushort g_rowwidth = 16;
/// Current offset view type
OffsetType g_offsettype = void;
/// Current display view type
DisplayMode g_displaymode = void;

MmFile g_fhandle;	/// Current file handle
ubyte* g_fmmbuf;	/// mmfile buffer address
uint g_screenl;	/// screen size in bytes, 1 dimensional buffer
string g_fname;	/// filename
long g_fpos;	/// Current file position
long g_fsize;	/// File size

private char[32] g_fsizebuf;	/// total formatted size buffer
private char[] g_fsizeout;	/// total formatted size (slice)
private Exception lastEx;	/// Last exception

/// Load file
/// Params: path = Path of file to open
/// Returns: true on error
bool ddhx_file(string path) {
	try {
		// NOTE: zero-length file errors are weird so manual check here
		if ((g_fsize = getSize(path)) == 0)
			throw new Exception("Empty file");
		
		g_fhandle = new MmFile((g_fname = path), MmFile.Mode.read, 0, g_fmmbuf);
	} catch (Exception ex) {
		lastEx = ex;
		return true;
	}
	
	return false;
}

/// Print lastException to stderr. Useful for command-line.
/// Params: mod = Module or function name
void ddhx_error(string mod) {
	import std.stdio : stderr;
	debug stderr.writefln("%s: (%s L%d) %s",
		mod, lastEx.file, lastEx.line, lastEx.msg);
	else  stderr.writefln("%s: %s",
		mod, lastEx.msg);
}

/// Get last exception.
/// Returns: Last exception
Exception ddhx_exception() {
	return lastEx;
}

/// Main app entry point
void ddhx_main() {
	g_fsizeout = formatsize(g_fsizebuf, g_fsize);
	conclear;
	ddhx_prep;
	ddhx_update_offsetbar;
	
	if (ddhx_render_raw < conheight - 2)
		ddhx_update_infobar;
	else
		ddhx_update_infobar_raw;

	InputInfo k;
KEY:
	coninput(k);
	switch (k.value) {

	//
	// Navigation
	//

	case Key.UpArrow, Key.K:
		if (g_fpos - g_rowwidth >= 0)
			ddhx_seek_unsafe(g_fpos - g_rowwidth);
		else
			ddhx_seek_unsafe(0);
		break;
	case Key.DownArrow, Key.J:
		if (g_fpos + g_screenl + g_rowwidth <= g_fsize)
			ddhx_seek_unsafe(g_fpos + g_rowwidth);
		else
			ddhx_seek_unsafe(g_fsize - g_screenl);
		break;
	case Key.LeftArrow, Key.H:
		if (g_fpos - 1 >= 0) // Else already at 0
			ddhx_seek_unsafe(g_fpos - 1);
		break;
	case Key.RightArrow, Key.L:
		if (g_fpos + g_screenl + 1 <= g_fsize)
			ddhx_seek_unsafe(g_fpos + 1);
		else
			ddhx_seek_unsafe(g_fsize - g_screenl);
		break;
	case Key.PageUp, Mouse.ScrollUp:
		if (g_fpos - cast(long)g_screenl >= 0)
			ddhx_seek_unsafe(g_fpos - g_screenl);
		else
			ddhx_seek_unsafe(0);
		break;
	case Key.PageDown, Mouse.ScrollDown:
		if (g_fpos + g_screenl + g_screenl <= g_fsize)
			ddhx_seek_unsafe(g_fpos + g_screenl);
		else
			ddhx_seek_unsafe(g_fsize - g_screenl);
		break;
	case Key.Home:
		ddhx_seek_unsafe(k.key.ctrl ? 0 : g_fpos - (g_fpos % g_rowwidth));
		break;
	case Key.End:
		if (k.key.ctrl) {
			ddhx_seek_unsafe(g_fsize - g_screenl);
		} else {
			const long np = g_fpos +
				(g_rowwidth - g_fpos % g_rowwidth);
			ddhx_seek_unsafe(np + g_screenl <= g_fsize ? np : g_fsize - g_screenl);
		}
		break;

	//
	// Actions/Shortcuts
	//

	case Key.Escape, Key.Enter, Key.Colon:
		hxmenu;
		break;
	case Key.G:
		hxmenu("g ");
		ddhx_update_offsetbar();
		break;
	case Key.I:
		ddhx_fileinfo;
		break;
	case Key.R, Key.F5:
		ddhx_refresh;
		break;
	case Key.A:
		ddhx_setting_width("a");
		ddhx_refresh;
		break;
	case Key.Q: ddhx_exit; break;
	default:
	}
	goto KEY;
}

/// Set the ouput mode.
/// Params: v = Setting value
/// Returns: true on error
bool ddhx_setting_output(string v) {
	switch (v[0]) {
	case 'o', 'O': g_offsettype = OffsetType.Octal; break;
	case 'd', 'D': g_offsettype = OffsetType.Decimal; break;
	case 'h', 'H': g_offsettype = OffsetType.Hex; break;
	default: return true;
	}
	return false;
}

/// Set the column width in bytes
/// Params: v = Value
/// Returns: true on error
bool ddhx_setting_width(string v) {
	switch (v[0]) {
	case 'a': // Automatic
		final switch (g_displaymode)
		{
		case DisplayMode.Default:
			g_rowwidth = cast(ushort)((conwidth - 11) / 4);
			break;
		case DisplayMode.Text, DisplayMode.Data:
			g_rowwidth = cast(ushort)((conwidth - 11) / 3);
			break;
		}
		break;
	case 'd': // Default
		g_rowwidth = 16;
		break;
	default:
		long l;
		if (unformat(v, l) == false) {
			lastEx = new Exception("width: Number could not be formatted");
			return true;
		}
		if (l < 1 || l > ushort.max) {
			lastEx = new Exception("width: Number out of range");
			return true;
		}
		g_rowwidth = cast(ushort)l;
	}
	return false;
}

/// Refresh the entire screen
void ddhx_refresh() {
	ddhx_prep;
	conclear;
	ddhx_update_offsetbar;
	if (ddhx_render_raw < conheight - 2)
		ddhx_update_infobar;
	else
		ddhx_update_infobar_raw;
}

/**
 * Update the upper offset bar.
 */
void ddhx_update_offsetbar() {
	char [8]format = cast(char[8])" %02X"; // default
	format[4] = formatTable[g_offsettype];
	conpos(0, 0);
	printf("Offset %c ", offsetTable[g_offsettype]);
	for (ushort i; i < g_rowwidth; ++i)
		printf(cast(char*)format, i);
	putchar('\n');
}

/// Update the bottom current information bar.
void ddhx_update_infobar() {
	conpos(0, conheight - 1);
	ddhx_update_infobar_raw;
}

/// Updates information bar without cursor position call.
void ddhx_update_infobar_raw() {
	char[32] bl = void, cp = void;
	writef(" %*s | %*s/%*s | %7.3f%%",
		7,  formatsize(bl, g_screenl), // Buffer size
		10, formatsize(cp, g_fpos), // Formatted position
		10, g_fsizeout, // Total file size
		((cast(float)g_fpos + g_screenl) / g_fsize) * 100 // Pos/filesize%
	);
}

/// Determine screensize
void ddhx_prep() {
	const int bufs = (conheight - 2) * g_rowwidth; // Proposed buffer size
	g_screenl = g_fsize >= bufs ? bufs : cast(uint)g_fsize;
}

/**
 * Goes to the specified position in the file.
 * Ignores bounds checking for performance reasons.
 * Sets CurrentPosition.
 * Params: pos = New position
 */
void ddhx_seek_unsafe(long pos) {
	if (g_screenl < g_fsize) {
		g_fpos = pos;
		if (ddhx_render < conheight - 2)
			ddhx_update_infobar;
		else
			ddhx_update_infobar_raw;
	} else
		ddhx_msglow("Navigation disabled, buffer too small");
}

/**
 * Goes to the specified position in the file.
 * Checks bounds and calls Goto.
 * Params: pos = New position
 */
void ddhx_seek(long pos) {
	if (pos + g_screenl > g_fsize)
		ddhx_seek_unsafe(g_fsize - g_screenl);
	else
		ddhx_seek_unsafe(pos);
}

/**
 * Parses the string as a long and navigates to the file location.
 * Includes offset checking (+/- notation).
 * Params: str = String as a number
 */
void ddhx_seek(string str) {
	byte rel = void; // Lazy code
	if (str[0] == '+') { // relative position
		rel = 1;
		str = str[1..$];
	} else if (str[0] == '-') {
		rel = 2;
		str = str[1..$];
	}
	long l = void;
	if (unformat(str, l) == false) {
		ddhx_msglow("Could not parse number");
		return;
	}
	switch (rel) {
	case 1:
		if (g_fpos + l - g_screenl < g_fsize)
			ddhx_seek_unsafe(g_fpos + l);
		break;
	case 2:
		if (g_fpos - l >= 0)
			ddhx_seek_unsafe(g_fpos - l);
		break;
	default:
		if (l >= 0 && l < g_fsize - g_screenl) {
			ddhx_seek_unsafe(l);
		} else {
			import std.format : format;
			ddhx_msglow(format("Range too far or negative: %d (%XH)", l, l));
		}
	}
}

/// Update display from buffer
/// Returns: See ddhx_render_raw
uint ddhx_render() {
	conpos(0, 1);
	return ddhx_render_raw;
}

/// Update display from buffer without setting cursor
/// Returns: The number of lines printed on screen
uint ddhx_render_raw() {
	__gshared char[] hexTable = [
		'0', '1', '2', '3', '4', '5', '6', '7',
		'8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
	];

	uint linesp; /// Lines printed
	char[2048] buf = void;

	size_t viewpos = cast(size_t)g_fpos;
	size_t viewend = viewpos + g_screenl; /// window length
	ubyte *filebuf = cast(ubyte*)g_fhandle[viewpos..viewend].ptr;

	const(char) *fposfmt = void;
	with (OffsetType)
	final switch (g_offsettype) {
	case Hex:	fposfmt = "%8zX "; break;
	case Octal:	fposfmt = "%8zo "; break;
	case Decimal:	fposfmt = "%8zd "; break;
	}

	// vi: view index
	for (size_t vi; vi < g_screenl; viewpos += g_rowwidth) {
		// Offset column: Cannot be negative since the buffer will
		// always be large enough
		size_t bufindex = snprintf(buf.ptr, 32, fposfmt, viewpos);

		// data bytes left to be treated for the row
		size_t left = g_screenl - vi;

		if (left >= g_rowwidth) {
			left = g_rowwidth;
		} else { // left < g_rowwidth
			memset(buf.ptr + bufindex, ' ', 2048);
		}

		// Data buffering (hexadecimal and ascii)
		// hi: hex buffer offset
		// ai: ascii buffer offset
		size_t hi = bufindex;
		size_t ai = bufindex + (g_rowwidth * 3);
		buf[ai] = ' ';
		buf[ai+1] = ' ';
		for (ai += 2; left > 0; --left, hi += 3, ++ai) {
			ubyte b = filebuf[vi++];
			buf[hi] = ' ';
			buf[hi+1] = hexTable[b >> 4];
			buf[hi+2] = hexTable[b & 15];
			buf[ai] = b > 0x7E || b < 0x20 ? DEFAULT_CHAR : b;
		}

		// null terminator
		buf[ai] = 0;

		// Output
		puts(buf.ptr);
		++linesp;
	}

	return linesp;
}

/**
 * Message once (upper bar)
 * Params: msg = Message string
 */
void ddhx_msgtop(string msg) {
	conpos(0, 0);
	writef("%s%*s", msg, (conwidth - 1) - msg.length, " ");
}

/**
 * Message once (bottom bar)
 * Params: msg = Message string
 */
void ddhx_msglow(string msg) {
	conpos(0, conheight - 1);
	writef("%s%*s", msg, (conwidth - 1) - msg.length, " ");
}

/**
 * Bottom bar message.
 * Params:
 *   f = Format
 *   arg = String argument
 */
void ddhx_msglow(string f, string arg) {
	//TODO: (string format, ...) format, remove other (string) func
	import std.format : format;
	ddhx_msglow(format(f, arg));
}

/// Print some file information at the bottom bar
void ddhx_fileinfo() {
	import std.format : sformat;
	import std.path : baseName;
	char[256] b = void;
	//TODO: Use ddhx_msglow(string fmt, ...) whenever available
	ddhx_msglow(cast(string)b.sformat!"%s  %s"(g_fsizeout, g_fname.baseName));
}

/// Exits ddhx
void ddhx_exit() {
	import core.stdc.stdlib : exit;
	conclear;
	exit(0);
}