Newer
Older
ddhx / src / ddhx / ddhx.d
@dd86k dd86k on 27 Nov 2021 18 KB Bump 0.3.3
/**
 * Main rendering engine.
 */
module ddhx.ddhx;

import std.stdio;
import std.file : getSize;
import core.stdc.stdio : printf, fflush, puts, snprintf;
import core.stdc.string : memset;
import ddhx.utils : formatSize, unformat;
import ddhx.input, ddhx.menu, ddhx.terminal, ddhx.settings, ddhx.error;
import ddhx.searcher : searchLast;

//TODO: View display mode (hex+ascii, hex, ascii)
//TODO: Data display mode (hex, octal, dec)

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

/// App version
enum DDHX_VERSION = "0.3.3";

/// Version line
enum DDHX_VERSION_LINE = "ddhx " ~ DDHX_VERSION ~ " (built: " ~ __TIMESTAMP__~")";

private extern (C) int putchar(int);

/// Character table for header row
private immutable char[3] offsetTable = [ 'h', 'd', 'o' ];
/// Character table for the main panel for printf
private immutable char[3] formatTable = [ 'x', 'u', 'o' ];
/// Offset format functions
private immutable size_t function(char*,long)[3] offsetFuncs =
	[ &format8lux, &format8lud, &format8luo ];
/// Data format functions
//private immutable size_t function(char*,long)[] dataFuncs =
//	[ &format2x, &format3d, &format3o ];


//
// User settings
//

//TODO: --no-header: bool
//TODO: --no-offset: bool
//TODO: --no-status: bool
/// Global definitions and default values
struct Globals {
	// Settings
	ushort rowWidth = 16;	/// How many bytes are shown per row
	OffsetType offset;	/// Current offset view type
	DisplayMode display;	/// Current display view type
	char defaultChar = '.';	/// Default character to use for non-ascii characters
	// File
	string fileName;	/// 
	const(ubyte)[] buffer;	/// 
//	bool omitHeader;	/// 
//	bool omitOffsetBar;	/// 
//	bool omitOffset;	/// 
	// Internals
	int termHeight;	/// Last known terminal height
	int termWidth;	/// Last known terminal width
	const(char)[] fileSizeString;	/// Formatted binary size
}

__gshared Globals globals; /// Single-instance of globals.
__gshared Input   input;   /// Input file/stream

int printError(int code = 1, A...)(string fmt, A args) {
	stderr.write("error: ");
	stderr.writefln(fmt, args);
	return code;
}

int ddhxOpenFile(string path) {
	version (Trace) trace("path=%s", path);
	
	import std.path : baseName;
	globals.fileName = baseName(path);
	return input.openFile(path);
}
int ddhxOpenMmfile(string path) {
	version (Trace) trace("path=%s", path);
	
	import std.path : baseName;
	globals.fileName = baseName(path);
	return input.openMmfile(path);
}
int ddhxOpenStdin() {
	version (Trace) trace("-");
	
	globals.fileName = "-";
	return input.openStdin();
}

/// Main app entry point
int ddhxInteractive(long skip = 0) {
	//TODO: Consider hiding terminal cursor
	//TODO: Consider changing the buffering strategy
	//      e.g., flush+setvbuf/puts+flush
	
	//TODO: negative should be starting from end of file (if not stdin)
	if (skip < 0)
		skip = +skip;
	
	if (input.mode == InputMode.stdin) {
		version (Trace) trace("slurp skip=%u", skip);
		input.slurpStdin(skip);
	}
	
	input.position = skip;
	globals.fileSizeString = input.binarySize();
	
	version (Trace) trace("coninit");
	coninit;
	version (Trace) trace("conclear");
	conclear;
	version (Trace) trace("conheight");
	globals.termHeight = conheight;
	ddhxPrepBuffer(true);
	globals.buffer = input.read();
	version (Trace) trace("buffer+read=%u", globals.buffer.length);
	ddhxRender();
	
	InputInfo k;
	version (Trace) trace("loop");
L_KEY:
	coninput(k);
	version (Trace) trace("key=%d", k.value);
	
	with (globals) switch (k.value) {
	
	//
	// Navigation
	//
	
	case Key.UpArrow, Key.K:
		if (input.position - rowWidth >= 0)
			ddhxSeek(input.position - rowWidth);
		else
			ddhxSeek(0);
		break;
	case Key.DownArrow, Key.J:
		if (input.position + input.bufferSize + rowWidth <= input.size)
			ddhxSeek(input.position + rowWidth);
		else
			ddhxSeek(input.size - input.bufferSize);
		break;
	case Key.LeftArrow, Key.H:
		if (input.position - 1 >= 0) // Else already at 0
			ddhxSeek(input.position - 1);
		break;
	case Key.RightArrow, Key.L:
		if (input.position + input.bufferSize + 1 <= input.size)
			ddhxSeek(input.position + 1);
		else
			ddhxSeek(input.size - input.bufferSize);
		break;
	case Key.PageUp, Mouse.ScrollUp:
		if (input.position - cast(long)input.bufferSize >= 0)
			ddhxSeek(input.position - input.bufferSize);
		else
			ddhxSeek(0);
		break;
	case Key.PageDown, Mouse.ScrollDown:
		if (input.position + input.bufferSize + input.bufferSize <= input.size)
			ddhxSeek(input.position + input.bufferSize);
		else
			ddhxSeek(input.size - input.bufferSize);
		break;
	case Key.Home:
		ddhxSeek(k.key.ctrl ? 0 : input.position - (input.position % rowWidth));
		break;
	case Key.End:
		if (k.key.ctrl) {
			ddhxSeek(input.size - input.bufferSize);
		} else {
			const long np = input.position + (rowWidth - input.position % rowWidth);
			ddhxSeek(np + input.bufferSize <= input.size ? np : input.size - input.bufferSize);
		}
		break;

	//
	// Actions/Shortcuts
	//

	case Key.N:
		if (searchLast())
			ddhxMsgLow(ddhxErrorMsg);
		break;
	case Key.Escape, Key.Enter, Key.Colon:
		hxmenu;
		break;
	case Key.G:
		hxmenu("g ");
		ddhxUpdateOffsetbar;
		break;
	case Key.I:
		ddhxMsgFileInfo;
		break;
	case Key.R, Key.F5:
		ddhxRefresh;
		break;
	case Key.A:
		optionWidth("a");
		ddhxRefresh;
		break;
	case Key.Q: ddhxExit; break;
	default:
		version (Trace) trace("unknown key=%u", k.value);
	}
	goto L_KEY;
}

/// 
int ddhxDump(long skip, long length) {
	if (length < 0)
		return printError!2("length negative");
	
	version (Trace) trace("skip=%d length=%d", skip, length);
	
	final switch (input.mode) {
	case InputMode.file, InputMode.mmfile:
		if (skip < 0) {
			skip = input.size + skip;
		}
		if (skip + length > input.size)
			return printError!2("length overflow");
		if (length == 0)
			length = input.size - skip;
		if (skip)
			input.seek(skip);
		
		ddhxUpdateOffsetbarRaw;
		
		if (length >= DEFAULT_BUFFER_SIZE) {
			input.adjust(DEFAULT_BUFFER_SIZE);
			do {
				globals.buffer = input.read();
				ddhxDrawRaw;
				input.position += DEFAULT_BUFFER_SIZE;
			} while (length -= DEFAULT_BUFFER_SIZE > 0);
		}
		
		if (length > 0) {
			input.adjust(cast(uint)length);
			globals.buffer = input.read();
			ddhxDrawRaw;
		}
	
		break;
	case InputMode.stdin:
		if (skip < 0)
			return printError!2("skip value negative in stdin mode");
		
		size_t l = void;
		if (skip) {
			if (skip > DEFAULT_BUFFER_SIZE) {
				input.adjust(DEFAULT_BUFFER_SIZE);
			} else {
				input.adjust(cast(uint)(skip));
			}
			do {
				l = input.read().length;
			} while (l >= DEFAULT_BUFFER_SIZE);
		}
		
		input.adjust(DEFAULT_BUFFER_SIZE);
		ddhxUpdateOffsetbarRaw;
		
		do {
			globals.buffer = input.read();
			ddhxDrawRaw;
			input.position += DEFAULT_BUFFER_SIZE;
			l = globals.buffer.length;
		} while (l);
		break;
	}
	return 0;
}

/// Refresh the entire screen
void ddhxRefresh() {
	ddhxPrepBuffer();
	input.seek(input.position);
	globals.buffer = input.read();
	conclear();
	ddhxRender();
}

/// Render all
void ddhxRender() {
	ddhxUpdateOffsetbar();
	if (ddhxDrawRaw() < conheight - 2)
		ddhxUpdateStatusbar;
	else
		ddhxUpdateStatusbarRaw;
}

/// Update the upper offset bar.
void ddhxUpdateOffsetbar() {
	conpos(0, 0);
	ddhxUpdateOffsetbarRaw;
}

/// 
void ddhxUpdateOffsetbarRaw() {
	//TODO: Redo ddhxUpdateOffsetbarRaw
	/*enum OFFSET = "Offset ";
	__gshared char[512] line = "Offset ";
	size_t lineindex = OFFSET.sizeof;
	
	line[lineindex] = offsetTable[globals.offset];
	line[lineindex+1] = ' ';
	lineindex += 2;
	
	for (ushort i; i < globals.rowWidth; ++i) {
		line[lineindex] = ' ';
	}*/
	
	static char[8] fmt = " %02x";
	fmt[4] = formatTable[globals.offset];
	printf("Offset %c ", offsetTable[globals.offset]);
	//TODO: Better rendering for large positions
	if (input.position > 0xffff_ffff) putchar(' ');
	for (ushort i; i < globals.rowWidth; ++i)
		printf(cast(char*)fmt, i);
	putchar('\n');
}

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

/// Updates information bar without cursor position call.
void ddhxUpdateStatusbarRaw() {
	import std.format : sformat;
	__gshared size_t last;
	char[32] c = void, t = void;
	char[128] b = void;
	char[] f = sformat!" %*s | %*s/%*s | %7.4f%%"(b,
		7,  formatSize(c, input.bufferSize), // Buffer size
		10, formatSize(t, input.position), // Formatted position
		10, globals.fileSizeString, // Total file size
		((cast(float)input.position + input.bufferSize) / input.size) * 100 // Pos/input.size%
	);
	if (last > f.length) {
		int p = cast(int)(f.length + (last - f.length));
		writef("%*s", -p, f);
	} else {
		write(f);
	}
	last = f.length;
	version (CRuntime_DigitalMars) stdout.flush();
	version (CRuntime_Musl) stdout.flush();
}

/// Determine input.bufferSize and buffer size
void ddhxPrepBuffer(bool skipTerm = false) {
	version (Trace) trace("skip=%s", skipTerm);
	
	debug import std.conv : text;
	const int h = (skipTerm ? globals.termHeight : conheight) - 2;
	debug assert(h > 0);
	debug assert(h < conheight, "h="~h.text~" >= conheight="~conheight.text);
	int newSize = h * globals.rowWidth; // Proposed buffer size
	if (newSize >= input.size)
		newSize = cast(uint)(input.size - input.position);
	version (Trace) trace("newSize=%u", newSize);
	input.adjust(newSize);
}

/**
 * Goes to the specified position in the file.
 * Ignores bounds checking for performance reasons.
 * Sets CurrentPosition.
 * Params: pos = New position
 */
void ddhxSeek(long pos) {
	version (Trace) trace("pos=%d", pos);
	
	if (input.bufferSize < input.size) {
		input.seek(pos);
		globals.buffer = input.read();
		ddhxRender();
	} else
		ddhxMsgLow("Navigation disabled, buffer too small");
}

/**
 * Parses the string as a long and navigates to the file location.
 * Includes offset checking (+/- notation).
 * Params: str = String as a number
 */
void ddhxSeek(string str) {
	version (Trace) trace("str=%s", str);
	
	const char seekmode = str[0];
	if (seekmode == '+' || seekmode == '-') { // relative input.position
		str = str[1..$];
	}
	long newPos = void;
	if (unformat(str, newPos) == false) {
		ddhxMsgLow("Could not parse number");
		return;
	}
	with (globals) switch (seekmode) {
	case '+':
		newPos = input.position + newPos;
		if (newPos - input.bufferSize < input.size)
			ddhxSeek(newPos);
		break;
	case '-':
		newPos = input.position - newPos;
		if (newPos >= 0)
			ddhxSeek(newPos);
		break;
	default:
		if (newPos < 0) {
			ddhxMsgLow("Range underflow: %d (0x%x)", newPos, newPos);
		} else if (newPos >= input.size - input.bufferSize) {
			ddhxMsgLow("Range overflow: %d (0x%x)", newPos, newPos);
		} else {
			ddhxSeek(newPos);
		}
	}
}

/**
 * Goes to the specified position in the file.
 * Checks bounds and calls Goto.
 * Params: pos = New position
 */
void ddhxSeekSafe(long pos) {
	version (Trace) trace("pos=%s", pos);
	
	if (pos + input.bufferSize > input.size)
		ddhxSeek(input.size - input.bufferSize);
	else
		ddhxSeek(pos);
}

private immutable static string hexTable = "0123456789abcdef";
private
size_t format8lux(char *buffer, long v) {
	size_t pos;
	bool pad = true;
	for (int shift = 60; shift >= 0; shift -= 4) {
		const ubyte b = (v >> shift) & 15;
		if (b == 0) {
			if (pad && shift >= 32) {
				continue; // cut
			} else if (pad && shift >= 4) {
				buffer[pos++] = pad ? ' ' : '0';
				continue;
			}
		} else pad = false;
		buffer[pos++] = hexTable[b];
	}
	return pos;
}
/// 
@system unittest {
	char[32] b = void;
	char *p = b.ptr;
	assert(b[0..format8lux(p, 0)]               ==      "       0");
	assert(b[0..format8lux(p, 1)]               ==      "       1");
	assert(b[0..format8lux(p, 0x10)]            ==      "      10");
	assert(b[0..format8lux(p, 0x100)]           ==      "     100");
	assert(b[0..format8lux(p, 0x1000)]          ==      "    1000");
	assert(b[0..format8lux(p, 0x10000)]         ==      "   10000");
	assert(b[0..format8lux(p, 0x100000)]        ==      "  100000");
	assert(b[0..format8lux(p, 0x1000000)]       ==      " 1000000");
	assert(b[0..format8lux(p, 0x10000000)]      ==      "10000000");
	assert(b[0..format8lux(p, 0x100000000)]     ==     "100000000");
	assert(b[0..format8lux(p, 0x1000000000)]    ==    "1000000000");
	assert(b[0..format8lux(p, 0x10000000000)]   ==   "10000000000");
	assert(b[0..format8lux(p, 0x100000000000)]  ==  "100000000000");
	assert(b[0..format8lux(p, 0x1000000000000)] == "1000000000000");
	assert(b[0..format8lux(p, ubyte.max)]  ==         "      ff");
	assert(b[0..format8lux(p, ushort.max)] ==         "    ffff");
	assert(b[0..format8lux(p, uint.max)]   ==         "ffffffff");
	assert(b[0..format8lux(p, ulong.max)]  == "ffffffffffffffff");
	assert(b[0..format8lux(p, 0x1010)]             ==         "    1010");
	assert(b[0..format8lux(p, 0x10101010)]         ==         "10101010");
	assert(b[0..format8lux(p, 0x1010101010101010)] == "1010101010101010");
}
private
size_t format8lud(char *buffer, long v) {
	debug import std.conv : text;
	enum ulong I64MAX = 10_000_000_000_000_000_000UL;
	immutable static string decTable = "0123456789";
	size_t pos;
	bool pad = true;
	for (ulong d = I64MAX; d > 0; d /= 10) {
		const long r = (v / d) % 10;
		if (r == 0) {
			if (pad && d >= 100_000_000) {
				continue; // cut
			} else if (pad && d >= 10) {
				buffer[pos++] = pad ? ' ' : '0';
				continue;
			}
		} else pad = false;
		debug assert(r >= 0 && r < 10, "r="~r.text);
		buffer[pos++] = decTable[r];
	}
	return pos;
}
/// 
@system unittest {
	char[32] b = void;
	char *p = b.ptr;
	assert(b[0..format8lud(p, 0)]                 ==      "       0");
	assert(b[0..format8lud(p, 1)]                 ==      "       1");
	assert(b[0..format8lud(p, 10)]                ==      "      10");
	assert(b[0..format8lud(p, 100)]               ==      "     100");
	assert(b[0..format8lud(p, 1000)]              ==      "    1000");
	assert(b[0..format8lud(p, 10_000)]            ==      "   10000");
	assert(b[0..format8lud(p, 100_000)]           ==      "  100000");
	assert(b[0..format8lud(p, 1000_000)]          ==      " 1000000");
	assert(b[0..format8lud(p, 10_000_000)]        ==      "10000000");
	assert(b[0..format8lud(p, 100_000_000)]       ==     "100000000");
	assert(b[0..format8lud(p, 1000_000_000)]      ==    "1000000000");
	assert(b[0..format8lud(p, 10_000_000_000)]    ==   "10000000000");
	assert(b[0..format8lud(p, 100_000_000_000)]   ==  "100000000000");
	assert(b[0..format8lud(p, 1000_000_000_000)]  == "1000000000000");
	assert(b[0..format8lud(p, ubyte.max)]  ==             "     255");
	assert(b[0..format8lud(p, ushort.max)] ==             "   65535");
	assert(b[0..format8lud(p, uint.max)]   ==           "4294967295");
	assert(b[0..format8lud(p, ulong.max)]  == "18446744073709551615");
	assert(b[0..format8lud(p, 1010)]       ==             "    1010");
}
private
size_t format8luo(char *buffer, long v) {
	size_t pos;
	if (v >> 63) buffer[pos++] = '1'; // ulong.max coverage
	bool pad = true;
	for (int shift = 60; shift >= 0; shift -= 3) {
		const ubyte b = (v >> shift) & 7;
		if (b == 0) {
			if (pad && shift >= 24) {
				continue; // cut
			} else if (pad && shift >= 3) {
				buffer[pos++] = pad ? ' ' : '0';
				continue;
			}
		} else pad = false;
		buffer[pos++] = hexTable[b];
	}
	return pos;
}
/// 
@system unittest {
	import std.conv : octal;
	char[32] b = void;
	char *p = b.ptr;
	assert(b[0..format8luo(p, 0)]                     ==     "       0");
	assert(b[0..format8luo(p, 1)]                     ==     "       1");
	assert(b[0..format8luo(p, octal!10)]              ==     "      10");
	assert(b[0..format8luo(p, octal!20)]              ==     "      20");
	assert(b[0..format8luo(p, octal!100)]             ==     "     100");
	assert(b[0..format8luo(p, octal!1000)]            ==     "    1000");
	assert(b[0..format8luo(p, octal!10_000)]          ==     "   10000");
	assert(b[0..format8luo(p, octal!100_000)]         ==     "  100000");
	assert(b[0..format8luo(p, octal!1000_000)]        ==     " 1000000");
	assert(b[0..format8luo(p, octal!10_000_000)]      ==     "10000000");
	assert(b[0..format8luo(p, octal!100_000_000)]     ==    "100000000");
	assert(b[0..format8luo(p, octal!1000_000_000)]    ==   "1000000000");
	assert(b[0..format8luo(p, octal!10_000_000_000)]  ==  "10000000000");
	assert(b[0..format8luo(p, octal!100_000_000_000)] == "100000000000");
	assert(b[0..format8luo(p, ubyte.max)]   ==               "     377");
	assert(b[0..format8luo(p, ushort.max)]  ==               "  177777");
	assert(b[0..format8luo(p, uint.max)]    ==            "37777777777");
	assert(b[0..format8luo(p, ulong.max)]   == "1777777777777777777777");
	assert(b[0..format8luo(p, octal!101_010)]             == "  101010");
}

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

/// Write to stdout from file buffer
/// Returns: The number of lines printed
uint ddhxDrawRaw() {
	// data
	const(ubyte) *b    = globals.buffer.ptr;
	int           bsz  = cast(int)globals.buffer.length;
	size_t        bpos;
	
	// line buffer
	size_t     lpos = void;
	char[2048] lbuf   = void;	/// line buffer
	char      *l      = lbuf.ptr;
	uint       ls;	/// lines printed
	
	// formatting
	const int row = globals.rowWidth;
	size_t function(char*, long) formatOffset = offsetFuncs[globals.offset];
	//size_t function(char*, ubyte) formatData = dataFuncs[globals.dataMode];
	
	// print lines in bulk
	long pos = input.position;
	for (int left = bsz; left > 0; left -= row, pos += row, ++ls) {
		lpos = formatOffset(l, pos);
		l[lpos++] = ' ';
		
		const bool leftOvers = left < row;
		int bytesLeft = leftOvers ? left : row;
		
		size_t apos = (lpos + (row * 3)) + 2;
		for (ushort r; r < bytesLeft; ++r, ++apos, ++bpos) {
			const ubyte bt = b[bpos];
			l[lpos] = ' ';
			l[lpos+1] = hexTable[bt >> 4];
			l[lpos+2] = hexTable[bt & 15];
			lpos += 3; // += formatData(bt);
			l[apos] = bt > 0x7E || bt < 0x20 ? globals.defaultChar : bt;
		}
		
		lbuf[lpos] = ' ';	// hex + ' ' + ascii
		lbuf[lpos+1] = ' ';	// hex + ' ' + ascii
		lpos += 2;
		
		if (leftOvers) {
			bytesLeft = row - left;
			l[apos] = 0;
			do {
				l[lpos]   = ' ';
				l[lpos+1] = ' ';
				l[lpos+2] = ' ';
				lpos += 3;
			} while (--bytesLeft > 0);
			left = 0;
		} else lbuf[lpos + globals.rowWidth] = 0;
		
		puts(l);	// out with it + newline
	}
	
	return ls;
}

/**
 * Message once (upper bar)
 * Params: msg = Message string
 */
void ddhxMsgTop(A...)(string fmt, A args) {
	conpos(0, 0);
	ddhxMsg(fmt, args);
}

void ddhxMsgLow(A...)(string fmt, A args) {
	conpos(0, conheight - 1);
	ddhxMsg(fmt, args);
}

private void ddhxMsg(A...)(string fmt, A args) {
	import std.format : sformat;
	char[256] outbuf = void;
	char[] outs = outbuf[].sformat(fmt, args);
	writef("%s%*s", outs, (conwidth - 1) - outs.length, " ");
	stdout.flush();
}

/// Print some file information at the bottom bar
void ddhxMsgFileInfo() {
	with (globals)
	ddhxMsgLow("%s  %s", fileSizeString, fileName);
}

void ddhxExit(int code = 0) {
	import core.stdc.stdlib : exit;
	conclear;
	exit(code);
}