Newer
Older
ddhx / src / ddhx.d
/// Main application.
/// Copyright: dd86k <dd@dax.moe>
/// License: MIT
/// Authors: $(LINK2 github.com/dd86k, dd86k)
module ddhx;

//TODO: Consider moving all bits into editor module.

import gitinfo;
import os.terminal;
public import
	editor,
	searcher,
	encoding,
	error,
	converter,
	settings,
	screen,
	utils.args,
	utils.format,
	utils.memory;

/// Copyright string
enum DDHX_COPYRIGHT = "Copyright (c) 2017-2022 dd86k <dd@dax.moe>";
private enum DESCRIPTION = GIT_DESCRIPTION[1..$];
/// App version
debug enum DDHX_VERSION = DESCRIPTION~"+debug";
else  enum DDHX_VERSION = DESCRIPTION; /// Ditto
/// Version line
enum DDHX_ABOUT = "ddhx "~DDHX_VERSION~" (built: "~__TIMESTAMP__~")";

/// Number type to render either for offset or data
enum NumberType : ubyte {
	hexadecimal,
	decimal,
	octal
}

//TODO: Seprate start functions into their own modules

/// Interactive application.
/// Params: skip = Seek to file/data position.
/// Returns: Error code.
int start(long skip = 0) {
	screen.initiate;
	
	version (Trace) trace("terminalSize");
	TerminalSize termsize = terminalSize;
	
	if (termsize.height < 3)
		return errorPrint(1, "Need at least 3 lines to display properly");
	if (termsize.width < 20)
		return errorPrint(1, "Need at least 20 columns to display properly");
	
	//TODO: negative should be starting from end of file (if not stdin)
	//      stdin: use seek
	if (skip < 0)
		return errorPrint(1, "Skip value must be positive");
	
	if (editor.fileMode == FileMode.stream) {
		version (Trace) trace("slurp skip=%u", skip);
		if (editor.slurp(skip, 0))
			return errorPrint;
	} else if (skip) {
		version (Trace) trace("seek skip=%u", skip);
		editor.seek(skip);
		if (editor.err)
			return errorPrint;
	}
	
	refresh;
	
	version (Trace) trace("loop");
	TerminalInput event;
	
	//
	// Input
	//
	
L_INPUT:
	terminalInput(event);
	version (Trace) trace("type=%d", event.type);
	
	switch (event.type) with (InputType) {
	case keyDown:	goto L_KEYDOWN;
	default:	goto L_INPUT; // unknown
	}
	
	//
	// Keyboard
	//
	
L_KEYDOWN:
	version (Trace) trace("key=%d", event.key);
	
	switch (event.key) with (Key) with (Mod) {
	
	// Navigation
	
	//TODO: ctrl+(up|down) = move view only
	//TODO: ctrl+(left|right) = move to next word
	
	case UpArrow, K:    moveRowUp; break;
	case DownArrow, J:  moveRowDown; break;
	case LeftArrow, H:  moveLeft; break;
	case RightArrow, L: moveRight; break;
	case PageUp:        movePageUp; break;
	case PageDown:      movePageDown; break;
	case Home:          moveAlignStart; break;
	case Home | ctrl:   moveStart; break;
	case End:           moveAlignEnd; break;
	case End | ctrl:    moveEnd; break;
	
	// Actions/Shortcuts
	
	case '/':
		menu(null, "/");
		break;
	case '?':
		menu(null, "?");
		break;
	case N:
		next;
		break;
	case Escape, Enter, Colon:
		terminalPauseInput;
		menu;
		terminalResumeInput;
		break;
	case G:
		menu("g ");
		break;
	case I:
		printFileInfo;
		break;
	case R, F5:
		refresh;
		break;
	case A:
		settingsWidth("a");
		refresh;
		break;
	case Q: exit; break;
	default:
	}
	goto L_INPUT;
}

private:

__gshared ubyte[] readdata;

//TODO: revamp menu system
//      char mode: character mode (':', '/', '?')
//      string command: command shortcut (e.g., 'g' + ' ' default)
void menu(string cmdPrepend = null, string cmdAlias = null) {
	import std.stdio : readln;
	
	// clear bar and command prepend
	screen.clearOffsetBar;
	
	// write prompt
	terminalPos(0, 0);
	if (cmdAlias == null) screen.cwrite(":");
	if (cmdAlias) screen.cwrite(cmdAlias);
	if (cmdPrepend) screen.cwrite(cmdPrepend);
	
	// read command
	string line = cmdPrepend ~ cmdAlias ~ readln();
	
	// draw upper bar, clearing input
	screen.cursorOffset;
	screen.renderOffset;
	
	if (command(line))
		screenMessage(errorMessage());
}

int command(string line) {
	return command(arguments(line));
}

int command(string[] argv) {
	const size_t argc = argv.length;
	if (argc == 0) return 0;
	
	version (Trace) trace("%(%s %)", argv);
	
	string command = argv[0];
	
	switch (command[0]) { // shortcuts
	case '/': // Search
		if (command.length <= 1)
			return errorSet(ErrorCode.missingArgumentType);
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentNeedle);
		
		return ddhx.lookup(command[1..$], argv[1], true, true);
	case '?': // Search backwards
		if (command.length <= 1)
			return errorSet(ErrorCode.missingArgumentType);
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentNeedle);
		
		return ddhx.lookup(command[1..$], argv[1], false, true);
	default:
	}
	
	switch (argv[0]) { // regular commands
	case "g", "goto":
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentPosition);
		
		switch (argv[1])
		{
		case "e", "end":
			moveEnd;
			break;
		case "h", "home":
			moveStart;
			break;
		default:
			seek(argv[1]);
		}
		return 0;
	case "skip":
		ubyte byte_ = void;
		if (argc <= 1) {
			byte_ = readdata[editor.cursor.position];
		} else {
			if (argv[1] == "zero")
				byte_ = 0;
			else if (convertToVal(byte_, argv[1]))
				return error.ecode;
		}
		return skip(byte_);
	case "i", "info":
		printFileInfo;
		return 0;
	case "refresh":
		refresh;
		return 0;
	case "q", "quit":
		exit;
		return 0;
	case "about":
		enum C = "Written by dd86k. " ~ DDHX_COPYRIGHT;
		screenMessage(C);
		return 0;
	case "version":
		screenMessage(DDHX_ABOUT);
		return 0;
	//
	// Settings
	//
	case "w", "width":
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentWidth);
		
		if (settingsWidth(argv[1]))
			return error.ecode;
		
		refresh;
		return 0;
	case "o", "offset":
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentType);
		
		if (settingsOffset(argv[1]))
			return error.ecode;
		
		screen.cursorOffset;
		screen.renderOffset;
		render;
		return 0;
	case "d", "data":
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentType);
		
		if (settingsData(argv[1]))
			return error.ecode;
		
		render;
		return 0;
	case "C", "defaultchar":
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentCharacter);
		
		if (settingsDefaultChar(argv[1]))
			return error.ecode;
		
		render;
		return 0;
	case "cp", "charset":
		if (argc <= 1)
			return errorSet(ErrorCode.missingArgumentCharset);
		
		if (settingsCharset(argv[1]))
			return error.ecode;
		
		render;
		return 0;
	case "reset":
		resetSettings();
		render;
		return 0;
	default:
		return errorSet(ErrorCode.invalidCommand);
	}
}

// for more elaborate user inputs (commands invoke this)
// prompt="save as"
// "save as: " + user input
/*private
string input(string prompt) {
	terminalPos(0, 0);
	screen.cwriteAt(0,0,prompt, ": ");
	return readln;
}*/

/// Move the cursor to the start of the data
void moveStart() {
	if (editor.cursorFileStart)
		readRender;
	updateStatus;
	updateCursor;
}
/// Move the cursor to the end of the data
void moveEnd() {
	if (editor.cursorFileEnd)
		readRender;
	updateStatus;
	updateCursor;
}
/// Align cursor to start of row
void moveAlignStart() {
	editor.cursorHome;
	updateStatus;
	updateCursor;
}
/// Align cursor to end of row
void moveAlignEnd() {
	editor.cursorEnd;
	updateStatus;
	updateCursor;
}
/// Move cursor to one data group to the left (backwards)
void moveLeft() {
	if (editor.cursorLeft)
		readRender;
	updateStatus;
	updateCursor;
}
/// Move cursor to one data group to the right (forwards)
void moveRight() {
	if (editor.cursorRight)
		readRender;
	updateStatus;
	updateCursor;
}
/// Move cursor to one row size up (backwards)
void moveRowUp() {
	if (editor.cursorUp)
		readRender;
	updateStatus;
	updateCursor;
}
/// Move cursor to one row size down (forwards)
void moveRowDown() {
	if (editor.cursorDown)
		readRender;
	updateStatus;
	updateCursor;
}
/// Move cursor to one page size up (backwards)
void movePageUp() {
	if (editor.cursorPageUp)
		readRender;
	updateStatus;
	updateCursor;
}
/// Move view to one page size down (forwards)
void movePageDown() {
	if (editor.cursorPageDown)
		readRender;
	updateStatus;
	updateCursor;
}

/// Initiate screen buffer
void initiate() {
	screen.updateTermSize;
	
	long fsz = editor.fileSize; /// data size
	int ssize = (screen.termSize.height - 2) * setting.width; /// screen size
	
	version (Trace) trace("fsz=%u ssz=%u", fsz, ssize);
	
	uint nsize = ssize >= fsz ? cast(uint)fsz : ssize;
	editor.setBuffer(nsize);
}

// read at current position
int read() {
	version (Trace) trace;
	
	editor.seek(editor.position);
//	if (editor.err)
//		return errorSet(ErrorCode.os);
	readdata = editor.read();
//	if (editor.err)
//		return errorSet(ErrorCode.os);
	return 0;
}

//TODO: Consider render with multiple parameters to select what to render

/// Render screen (all elements)
void render() {
	version (Trace) trace;
	
	updateContent;
	updateStatus;
}

void updateOffset() {
	screen.cursorOffset;
	screen.renderOffset;
}

void updateContent() {
	screen.cursorContent;
	screen.renderContent(editor.position, readdata);
}

void updateStatus() {
	import std.format : format;
	
	long c = editor.cursorTell + 1;
	
	screen.cursorStatusbar;
	screen.renderStatusBar(
		editor.editModeString,
		screen.name,
		transcoder.name,
		formatBin(editor.readSize, setting.si),
		format("%s (%f%%)",
			formatBin(c, setting.si),
			((cast(double)c) / editor.fileSize) * 100));
}

void updateCursor() {
	version (Trace)
		with (editor.cursor)
			trace("pos=%u n=%u", position, nibble);
	
	with (editor.cursor)
		screen.cursor(position, nibble);
}

void readRender() {
	read;
	render;
}

/// Refresh the entire screen by:
/// 1. Clearing the terminal
/// 2. Automatically resizing the view buffer
/// 3. Re-seeking to the current position (failsafe)
/// 4. Read buffer
/// 5. Render
void refresh() {
	version (Trace) trace;
	
	screen.clear;
	initiate;
	read;
	updateOffset;
	render;
	updateCursor;
}

/// Seek to position in data, reads view's worth, and display that.
/// Ignores bounds checking for performance reasons.
/// Sets CurrentPosition.
/// Params: pos = New position
void seek(long pos) {
	version (Trace) trace("pos=%d", pos);
	
	if (editor.readSize >= editor.fileSize) {
		screenMessage("Navigation disabled, file too small");
		return;
	}
	
	//TODO: cursorTo
	editor.seek(pos);
	readRender;
}

/// Parses the string as a long and navigates to the file location.
/// Includes offset checking (+/- notation).
/// Params: str = String as a number
void seek(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 (convertToVal(newPos, str)) {
		screenMessage("Could not parse number");
		return;
	}
	switch (seekmode) {
	case '+': // Relative, add
		safeSeek(editor.position + newPos);
		break;
	case '-': // Relative, substract
		safeSeek(editor.position - newPos);
		break;
	default: // Absolute
		if (newPos < 0) {
			screenMessage("Range underflow: %d (0x%x)", newPos, newPos);
		} else if (newPos >= editor.fileSize - editor.readSize) {
			screenMessage("Range overflow: %d (0x%x)", newPos, newPos);
		} else {
			seek(newPos);
		}
	}
}

/// Goes to the specified position in the file.
/// Checks bounds and calls Goto.
/// Params: pos = New position
void safeSeek(long pos) {
	version (Trace) trace("pos=%s", pos);
	
	long fsize = editor.fileSize;
	
	if (pos + editor.readSize >= fsize)
		pos = fsize - editor.readSize;
	else if (pos < 0)
		pos = 0;
	
	editor.cursorJump(pos, true);
}

enum LAST_BUFFER_SIZE = 128;
__gshared ubyte[LAST_BUFFER_SIZE] lastItem;
__gshared size_t lastSize;
__gshared string lastType;
__gshared bool lastForward;
__gshared bool lastAvailable;

/// Search last item.
/// Returns: Error code if set.
//TODO: I don't think the return code is ever used...
int next() {
	if (lastAvailable == false) {
		return errorSet(ErrorCode.noLastItem);
	}
	
	long pos = void;
	int e = searchData(pos, lastItem.ptr, lastSize, lastForward);
	
	if (e) {
		screenMessage("Not found");
		return e;
	}
	
	safeSeek(pos);
	//TODO: Format position found with current offset type
	screenMessage("Found at 0x%x", pos);
	return 0;
}

// Search data
int lookup(string type, string data, bool forward, bool save) {
	void *p = void;
	size_t len = void;
	if (convertToRaw(p, len, data, type))
		return error.ecode;
	
	if (save) {
		import core.stdc.string : memcpy;
		lastType = type;
		lastSize = len;
		lastForward = forward;
		//TODO: Check length against LAST_BUFFER_SIZE
		memcpy(lastItem.ptr, p, len);
		lastAvailable = true;
	}
	
	screenMessage("Searching for %s...", type);
	
	long pos = void;
	int e = searchData(pos, p, len, forward);
	
	if (e) {
		screenMessage("Not found");
		return e;
	}
	
	safeSeek(pos);
	//TODO: Format position found with current offset type
	screenMessage("Found at 0x%x", pos);
	return 0;
}

int skip(ubyte data) {
	screenMessage("Skipping all 0x%x...", data);
	
	long pos = void;
	const int e = searchSkip(data, pos);
	
	if (e) {
		screenMessage("End of file reached");
		return e;
	}
	
	safeSeek(pos);
	//TODO: Format position found with current offset type
	screenMessage("Found at 0x%x", pos);
	return 0;
}

/// Print file information
void printFileInfo() {
	screenMessage("%11s  %s",
		formatBin(editor.fileSize, setting.si),
		editor.fileName);
}

/// Exit ddhx.
/// Params: code = Exit code.
void exit(int code = 0) {
	import core.stdc.stdlib : exit;
	version (Trace) trace("code=%u", code);
	exit(code);
}