diff --git a/LICENSE b/LICENSE index af5da61..43b140b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) dd86k 2017-2022 +Copyright (c) dd86k 2017-2024 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/dub.sdl b/dub.sdl index 3bd6ead..7354ffc 100644 --- a/dub.sdl +++ b/dub.sdl @@ -1,14 +1,9 @@ name "ddhx" description "Hexadecimal file viewer" authors "dd86k" -copyright "Copyright © 2017-2022 dd86k" +copyright "Copyright © 2017-2024 dd86k" license "MIT" -# NOTE: Somehow, the dmd package in the Alpine repo does not contain rdmd. -preBuildCommands "rdmd setup.d version" platform="dmd" -preBuildCommands "ldmd2 -run setup.d version" platform="ldc" -preBuildCommands "gdmd -run setup.d version" platform="gdc" - configuration "default" { targetType "executable" mainSourceFile "src/main.d" diff --git a/setup.d b/setup.d deleted file mode 100644 index 1ad9763..0000000 --- a/setup.d +++ /dev/null @@ -1,27 +0,0 @@ -#!rdmd - -import std.process; -import std.string : stripRight; -import std.file : write; -import std.path : dirSeparator; - -alias SEP = dirSeparator; - -enum GITINFO_PATH = "src" ~ SEP ~ "gitinfo.d"; - -int main(string[] args) -{ - final switch (args[1]) { - case "version": - auto describe = executeShell("git describe"); - if (describe.status) - return describe.status; - - string ver = stripRight(describe.output); - write(GITINFO_PATH, - "// NOTE: This file was automatically generated.\n"~ - "module gitinfo;\n"~ - "enum GIT_DESCRIPTION = \""~ver~"\";"); - return 0; - } -} \ No newline at end of file diff --git a/src/README.md b/src/README.md deleted file mode 100644 index 5698eb4..0000000 --- a/src/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Introduction - -This document explains the source file structure. - -# Structure - -| Entry | Description | -|--------------|-------------| -| os/ | OS utilities | -| utils/ | Generic utils | -| converter.d | Dynamic type converter and conversion routines | -| ddhx.d | Main interactive application | -| dump.d | Dump application mode | -| editor.d | Used by ddhx, represents an editor instance | -| error.d | Error facility | -| gitinfo.d | This file is automatically generated. | -| main.d | Program entry point | -| screen.d | Screen handling | -| searcher.d | Search engine | -| settings.d | User settings (to be removed) | -| transcoder.d | Character encoding transcoder | \ No newline at end of file diff --git a/src/converter.d b/src/converter.d deleted file mode 100644 index 8dc453c..0000000 --- a/src/converter.d +++ /dev/null @@ -1,177 +0,0 @@ -/// Type conversion routines. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module converter; - -import std.format : FormatSpec, singleSpec, unformatValue; -import std.encoding : transcode; -import error; - -// NOTE: template to(T) can turn string values into anything. - -//TODO: More types -// - FILETIME -// - GUID (little-endian)/UUID (big-endian) - -/// Convert data to a raw pointer dynamically. -/// Params: -/// data = Data pointer receiver. -/// len = Data length receiver. -/// val = Value to parse. -/// type = Name of expected type. -/// Returns: Error code. -int convertToRaw(ref void *data, ref size_t len, string val, string type) -{ - union TypeData { - ulong u64; - long i64; - uint u32; - int i32; - ushort u16; - short i16; - ubyte u8; - byte i8; - dstring s32; - wstring s16; - string s8; - } - __gshared TypeData types; - - version (Trace) trace("data=%s val=%s type=%s", data, val, type); - - //TODO: utf16le and all. - // bswap all wchars? - - int e = void; - with (types) switch (type) { - case "s32", "dstring": - e = convertToVal(s32, val); - if (e) return e; - data = cast(void*)s32.ptr; - len = s32.length * dchar.sizeof; - break; - case "s16", "wstring": - e = convertToVal(s16, val); - if (e) return e; - data = cast(void*)s16.ptr; - len = s16.length * wchar.sizeof; - break; - case "s8", "string": - data = cast(void*)val.ptr; - len = val.length; - break; - case "u64", "ulong": - e = convertToVal(u64, val); - if (e) return e; - data = &u64; - len = u64.sizeof; - break; - case "i64", "long": - e = convertToVal(i64, val); - if (e) return e; - data = &i64; - len = i64.sizeof; - break; - case "u32", "uint": - e = convertToVal(u32, val); - if (e) return e; - data = &u32; - len = u32.sizeof; - break; - case "i32", "int": - e = convertToVal(i32, val); - if (e) return e; - data = &i32; - len = i32.sizeof; - break; - case "u16", "ushort": - e = convertToVal(u16, val); - if (e) return e; - data = &u16; - len = u16.sizeof; - break; - case "i16", "short": - e = convertToVal(i16, val); - if (e) return e; - data = &i16; - len = i16.sizeof; - break; - case "u8", "ubyte": - e = convertToVal(u8, val); - if (e) return e; - data = &u8; - len = u8.sizeof; - break; - case "i8", "byte": - e = convertToVal(i8, val); - if (e) return e; - data = &i8; - len = i8.sizeof; - break; - default: - return errorSet(ErrorCode.invalidType); - } - - return ErrorCode.success; -} - -/// Convert data depending on the type supplied at compile-time. -/// Params: -/// v = Data reference. -/// val = String value. -/// Returns: Error code. -int convertToVal(T)(ref T v, string val) -{ - import std.conv : ConvException; - try { - //TODO: ubyte[] input - static if (is(T == wstring) || is(T == dstring)) - { - transcode(val, v); - } else { // Scalar - const size_t len = val.length; - FormatSpec!char fmt = void; - if (len >= 3 && val[0..2] == "0x") - { - fmt = singleSpec("%x"); - val = val[2..$]; - } - else if (len >= 2 && val[0] == '0') - { - fmt = singleSpec("%o"); - val = val[1..$]; - } else { - fmt = singleSpec("%d"); - } - v = unformatValue!T(val, fmt); - } - } - catch (Exception ex) - { - return errorSet(ex); - } - - return 0; -} - -/// -@system unittest { - int i; - assert(convertToVal(i, "256") == ErrorCode.success); - assert(i == 256); - assert(convertToVal(i, "0100") == ErrorCode.success); - assert(i == 64); - assert(convertToVal(i, "0x100") == ErrorCode.success); - assert(i == 0x100); - ulong l; - assert(convertToVal(l, "1000000000000000") == ErrorCode.success); - assert(l == 1000000000000000); - assert(convertToVal(l, "01000000000000000") == ErrorCode.success); - assert(l == 35184372088832); - assert(convertToVal(l, "0x1000000000000000") == ErrorCode.success); - assert(l == 0x1000000000000000); - wstring w; - assert(convertToVal(w, "hello") == ErrorCode.success); - assert(w == "hello"w); -} diff --git a/src/ddhx.d b/src/ddhx.d index 7eb08e5..7af3bd6 100644 --- a/src/ddhx.d +++ b/src/ddhx.d @@ -1,696 +1,429 @@ -/// Main application. +/// Main module, handling core TUI operations. +/// /// Copyright: dd86k /// License: MIT /// Authors: $(LINK2 https://github.com/dd86k, dd86k) module ddhx; -//TODO: When searching strings, maybe convert it to current charset? +import std.stdio; +import core.stdc.stdlib : exit; +import os.terminal : Key, Mod; +import editor; +import display; +import transcoder : CharacterSet; +import logger; -import gitinfo; -import os.terminal; -public import - editor, - searcher, - encoding, - error, - converter, - settings, - screen, - utils.args, - utils.format, - utils.memory; +//TODO: On terminal resize, set UHEADER -/// Copyright string -enum DDHX_COPYRIGHT = "Copyright (c) 2017-2022 dd86k "; -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 -//TODO: NumberType -> OffsetType -enum NumberType : ubyte { - hexadecimal, - decimal, - octal -} -//TODO: enum DataType { hex8, ... } etc. - -//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) +private enum { + // Update the cursor position + UCURSOR = 1, + // Update the current view + UVIEW = 1 << 1, + // Update the header + UHEADER = 1 << 2, + // Editing in progress + UEDIT = 1 << 3, + + // Clear the current message + UMESSAGE = 1 << 8, + + UINIT = 0xff, +} + +private __gshared +{ + Editor file; + + // Current file index (document selector) + //int currenfile; + + /// Total length of the file or data buffer, in bytes + long filesize; + /// Position of the view, in bytes + long viewpos; + /// Size of the view, in bytes + int viewsize; + + /// Number of columns desired, one per data group + int columns; + /// Address padding, in digits + int addrpad = 11; + + /// The type of format to use for rendering offsets + int offsetmode; + /// The type of format to use for rendering data + int datamode; + /// The size of one data item, in bytes + int groupsize; + /// The type of character set to use for rendering text + int charset; + + /// Number of digits per byte, in digits or nibbles + int digits; + + /// Position of the cursor in the file, in bytes + long curpos; + /// Position of the cursor editing a group of bytes, in digits + int editpos; // e.g., hex=nibble, dec=digit, etc. + /// + int editmode; + /// + enum EDITBFSZ = 8; + /// Value of edit input, as a digit + char[EDITBFSZ] editbuffer; + + /// System status, used in updating certains portions of the screen + int status; +} + +int ddhx_start(string path, bool readonly, + long skip, long length, + int cols, int ucharset) +{ + //TODO: Stream support + // With length, build up a buffer + if (path == null) + { + stderr.writeln("todo: Stdin"); + return 2; + } + + // Open file + if (file.open(path, true, readonly)) + { + stderr.writeln("error: Could not open file"); + return 3; + } + + // if (skip) { - //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; - } + viewpos = curpos = skip; } - if (editor.fileMode == FileMode.stream) + charset = ucharset; + + filesize = file.size(); + + // Init display in TUI mode + disp_init(true); + + // Set number of columns, or automatically get column count + // from terminal. + columns = cols ? cols : disp_hint_cols(); + + // Get "view" buffer size, in bytes + viewsize = disp_hint_view(columns); + + // Allocate buffer according to desired cols + trace("viewsize=%d", viewsize); + file.setbuffer( viewsize ); + + // Initially render everything + status = UINIT; + +Lread: + cast(void)update(); + int key = disp_readkey(); + + switch (key) { - editor.slurp(skip); - } + // Navigation keys + case Key.LeftArrow: move_left(); break; + case Key.RightArrow: move_right(); break; + case Key.DownArrow: move_down(); break; + case Key.UpArrow: move_up(); break; + case Key.PageDown: move_pg_down(); break; + case Key.PageUp: move_pg_up(); break; + case Key.Home: move_ln_start(); break; + case Key.End: move_ln_end(); break; + case Key.Home|Mod.ctrl: move_abs_start(); break; + case Key.End |Mod.ctrl: move_abs_end(); break; - if (screen.initiate) - panic; - - screen.onResize(&eventResize); - - refresh; - - inputLoop; - return 0; -} - -private: - -//TODO: rename to "readbuffer"? or "screenData"? -// screen.setData(...)? -__gshared ubyte[] readdata; - -void inputLoop() -{ - version (Trace) trace("loop"); - TerminalInput event; - -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: moveAbsStart; break; - case End: moveAlignEnd; break; - case End | ctrl: moveAbsEnd; break; - - // Actions/Shortcuts - - case '/': - menu(null, "/"); + // Search + case Key.W | Mod.ctrl: break; - case '?': - menu(null, "?"); + + // Reset screen + case Key.R | Mod.ctrl: + columns = disp_hint_cols(); + viewsize = disp_hint_view(columns); + file.setbuffer( viewsize ); + status = UINIT; break; - case N: - next; + + // + case Key.Q | Mod.ctrl: + quit(); break; - case Escape, Enter, ':': - menu; - break; - case G: - menu("g "); - break; - case I: - printFileInfo; - break; - case R, F5: - refresh; - break; - case A: - settingsColumns("a"); - refresh; - break; - case Q: exit; break; + default: + // Edit mode + /*if (_editkey(datamode, key)) + { + //TODO: When group size filled, add to edit history + trace("EDIT key=%c", cast(char)key); + editbuffer[editpos++] = cast(ubyte)key; + status |= UEDIT; + + if (editpos >= digits) { + //TODO: add byte+address to edits + editpos = 0; + _move_rel(1); + } + goto Lread; + }*/ } - goto L_INPUT; + goto Lread; } -void eventResize() +string prompt(string text) { - screen.updateTermSize(); - refresh; // temp + throw new Exception("Not implemented"); } -//TODO: revamp menu system -// char mode: character mode (':', '/', '?') -// string command: command shortcut (e.g., 'g' + ' ' default) -void menu(string prefix = ":", string prepend = null) +private +int _editkey(int type, int key) { - import std.stdio : readln; - import std.string : chomp, strip; - - // clear bar and command prepend - screen.clearOffsetBar; - - string line = screen.prompt(prefix, prepend); - - scope (exit) + switch (type) with (Format) { - editor.cursorBound; - updateCursor; - } - - if (line.length == 0) - return; - - if (command(line)) - screen.message(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("argv=%(%s %)", argv); - - string command = argv[0]; - - switch (command[0]) { // shortcuts - case '/': // Search - if (command.length < 2) - return errorSet(ErrorCode.missingType); - if (argc < 2) - return errorSet(ErrorCode.missingNeedle); - - return ddhx.lookup(command[1..$], argv[1], true, true); - case '?': // Search backwards - if (command.length < 2) - return errorSet(ErrorCode.missingType); - if (argc < 2) - return errorSet(ErrorCode.missingNeedle); - - return ddhx.lookup(command[1..$], argv[1], false, true); + case hex: + return (key <= '0' && key <= '9') || + (key <= 'A' && key <= 'F') || + (key <= 'a' && key <= 'f'); + case dec: + return key <= '0' && key <= '9'; + case oct: + return key <= '0' && key <= '7'; default: + throw new Exception(__FUNCTION__); } - - switch (argv[0]) { // regular commands - case "g", "goto": - if (argc < 2) - return errorSet(ErrorCode.missingValue); - - switch (argv[1]) { - case "e", "end": - moveAbsEnd; - break; - case "h", "home": - moveAbsStart; - 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.errorcode; - } - 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; - screen.message(C); - return 0; - case "version": - screen.message(DDHX_ABOUT); - return 0; - case "reset": - resetSettings(); - render; - return 0; - case "set": - if (argc < 2) - return errorSet(ErrorCode.missingOption); - - int e = settings.set(argv[1..$]); - if (e == 0) refresh; - return e; +} +private +int _editval(int type, int key) +{ + switch (type) with (Format) + { + case hex: + if (key <= '0' && key <= '9') + return key - '0'; + if (key <= 'A' && key <= 'F') + return key - 'A' + 0x10; + if (key <= 'a' && key <= 'f') + return key - 'a' + 0x10; + goto default; + case dec: + if (key <= '0' && key <= '9') + return key - '0'; + goto default; + case oct: + if (key <= '0' && key <= '7') + return key - '0'; + goto default; default: - return errorSet(ErrorCode.invalidCommand); + throw new Exception(__FUNCTION__); } } -// for more elaborate user inputs (commands invoke this) -// prompt="save as" -// "save as: " + user input -/*private -string input(string prompt, string include) +// Move the cursor relative to its position within the file +private +void _move_rel(long pos) { - terminalPos(0, 0); - screen.cwriteAt(0, 0, prompt, ": ", include); - return readln; -}*/ - -//TODO: When screen doesn't move: Only render lines affected - -/// Move the cursor to the start of the data -void moveAbsStart() -{ - if (editor.cursorAbsStart) - readRender; - else - render; - updateCursor; -} -/// Move the cursor to the end of the data -void moveAbsEnd() -{ - if (editor.cursorAbsEnd) - readRender; - else - render; - updateCursor; -} -/// Align cursor to start of row -void moveAlignStart() -{ - editor.cursorHome; - render; - updateCursor; -} -/// Align cursor to end of row -void moveAlignEnd() -{ - editor.cursorEnd; - render; - updateCursor; -} -/// Move cursor to one data group to the left (backwards) -void moveLeft() -{ - if (editor.cursorLeft) - readRender; - else - render; - updateCursor; -} -/// Move cursor to one data group to the right (forwards) -void moveRight() -{ - if (editor.cursorRight) - readRender; - else - render; - updateCursor; -} -/// Move cursor to one row size up (backwards) -void moveRowUp() -{ - if (editor.cursorUp) - readRender; - else - render; - updateCursor; -} -/// Move cursor to one row size down (forwards) -void moveRowDown() -{ - if (editor.cursorDown) - readRender; - else - render; - updateCursor; -} -/// Move cursor to one page size up (backwards) -void movePageUp() -{ - if (editor.cursorPageUp) - readRender; - else - render; - 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.columns; /// screen size - uint nsize = ssize >= fsz ? cast(uint)fsz : ssize; - - version (Trace) trace("fsz=%u ssz=%u nsize=%u", fsz, ssize, nsize); - - editor.setBuffer(nsize); -} - -// read at current position -void read() -{ - editor.seek(editor.position); - if (editor.err) panic; - - version (Trace) trace("editor.position=%u", editor.position); - - readdata = editor.read(); - if (editor.err) panic; - - version (Trace) trace("readdata.length=%u", readdata.length); -} - -//TODO: Consider render with multiple parameters to select what to render - -/// Render screen (all elements) -void renderAll() -{ - version (Trace) trace; - - updateOffset; - updateContent; - updateStatus; -} -void render() -{ - version (Trace) trace; - - updateContent; - updateStatus; -} -void readRender() -{ - read; - render; -} - -void updateOffset() -{ - screen.cursorOffset; - screen.renderOffset; -} - -void updateContent() -{ - screen.cursorContent; // Set pos for rendering - const int rows = screen.output( - editor.position, readdata, - editor.cursor.position); - - if (rows < 0) - { - message("Something bad happened"); + if (pos == 0) return; - } - screen.renderEmpty(rows); -} - -void updateStatus() -{ - import std.format : format; + long old = curpos; + curpos += pos; - long pos = editor.cursorTell; - char[12] offset = void; - size_t l = screen.offsetFormatter.offset(offset.ptr, pos); + if (pos > 0 && curpos >= filesize) + curpos = filesize; + else if (pos < 0 && curpos < 0) + curpos = 0; - screen.cursorStatusbar; - screen.renderStatusBar( - editor.editModeString, - screen.binaryFormatter.name, - transcoder.name, - formatBin(editor.readSize, setting.si), - offset[0..l], - format("%f%%", - ((cast(double)pos) / editor.fileSize) * 100)); -} - -void updateCursor() -{ - version (Trace) - with (editor.cursor) - trace("pos=%u n=%u", position, nibble); - - //with (editor.cursor) - // screen.cursor(position, nibble); -} - -/// 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) - { - screen.message("Navigation disabled, file too small"); + if (old == curpos) return; - } - //TODO: cursorTo - editor.seek(pos); - readRender; + status |= UCURSOR; + _adjust_viewpos(); } - -/// 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) +// Move the cursor to an absolute file position +private +void _move_abs(long pos) { - 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)) - { - screen.message("Could not parse number"); + long old = curpos; + curpos = pos; + + if (curpos >= filesize) + curpos = filesize; + else if (curpos < 0) + curpos = 0; + + if (old == curpos) return; - } - switch (seekmode) { - case '+': // Relative, add - safeSeek(editor.position + newPos); - break; - case '-': // Relative, substract - safeSeek(editor.position - newPos); - break; - default: // Absolute - if (newPos < 0) + + status |= UCURSOR; + _adjust_viewpos(); +} +// Adjust the view positon +void _adjust_viewpos() +{ + //TODO: Adjust view position algorithmically + + // Cursor is ahead the view + if (curpos >= viewpos + viewsize) + { + while (curpos >= viewpos + viewsize) { - screen.message("Range underflow: %d (0x%x)", newPos, newPos); + viewpos += columns; + if (viewpos >= filesize - viewsize) + break; } - else if (newPos >= editor.fileSize - editor.readSize) + status |= UVIEW; + } + // Cursor is behind the view + else if (curpos < viewpos) + { + while (curpos < viewpos) { - screen.message("Range overflow: %d (0x%x)", newPos, newPos); + viewpos -= columns; + if (viewpos <= 0) + break; } - else - { - seek(newPos); - } + status |= UVIEW; } } -/// Goes to the specified position in the file. -/// Checks bounds and calls Goto. -/// Params: pos = New position -void safeSeek(long pos) +void move_left() { - version (Trace) trace("pos=%s", pos); + if (curpos == 0) + return; - long fsize = editor.fileSize; + _move_rel(-1); +} +void move_right() +{ + if (curpos == filesize) + return; - if (pos + editor.readSize >= fsize) - pos = fsize - editor.readSize; - else if (pos < 0) - pos = 0; + _move_rel(1); +} +void move_up() +{ + if (curpos == 0) + return; - editor.cursorGoto(pos); + _move_rel(-columns); +} +void move_down() +{ + if (curpos == filesize) + return; + + _move_rel(columns); +} +void move_pg_up() +{ + if (curpos == 0) + return; + + _move_rel(-viewsize); +} +void move_pg_down() +{ + if (curpos == filesize) + return; + + _move_rel(viewsize); +} +void move_ln_start() +{ + _move_rel(-curpos % columns); +} +void move_ln_end() +{ + _move_rel((columns - (curpos % columns)) - 1); +} +void move_abs_start() +{ + _move_abs(0); +} +void move_abs_end() +{ + _move_abs(filesize); } -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() +// Update all elements on screen depending on status +// status global indicates what needs to be updated +void update() { - if (lastAvailable == false) + // Update header + if (status & UHEADER) + disp_header(columns); + + // Update the screen + if (status & UVIEW) + update_view(); + + // Update status + update_status(); + + // Update cursor position + // NOTE: Should always be updated due to frequent movement + // That includes messages, cursor naviation, menu invokes, etc. + int curdiff = cast(int)(curpos - viewpos); + trace("cur=%d", curdiff); + update_edit(curdiff, columns, addrpad); + + status = 0; +} + +void update_view() +{ + file.seek(viewpos); + ubyte[] data = file.read(); + trace("addr=%u data.length=%u", viewpos, data.length); + disp_update(viewpos, data, columns, + Format.hex, Format.hex, '.', + charset, + 11, + 1); +} + +// relative cursor position +void update_edit(int curpos, int columns, int addrpadd) +{ + enum hexsize = 3; + int row = 1 + (curpos / columns); + int col = (addrpadd + 2 + ((curpos % columns) * hexsize)); + disp_cursor(row, col); + + // Editing in progress + /*if (editbuf && editsz) { - return errorSet(ErrorCode.noLastItem); - } - - long pos = void; - int e = searchData(pos, lastItem.ptr, lastSize, lastForward); - - if (e) - { - screen.message("Not found"); - return e; - } - - safeSeek(pos); - //TODO: Format position found with current offset type - screen.message("Found at 0x%x", pos); - return 0; + disp_write(editbuf, editsz); + }*/ } -// Search data -int lookup(string type, string data, bool forward, bool save) +void update_status() { - void *p = void; - size_t len = void; - if (convertToRaw(p, len, data, type)) - return error.errorcode; - - 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; - } - - screen.message("Searching for %s...", type); - - long pos = void; - int e = searchData(pos, p, len, forward); - - if (e) - { - screen.message("Not found"); - return e; - } - - safeSeek(pos); - //TODO: Format position found with current offset type - screen.message("Found at 0x%x", pos); - return 0; + //TODO: Could limit write length by terminal width? + //TODO: check number of edits + enum STATBFSZ = 2 * 1024; + char[STATBFSZ] statbuf = void; + int statlen = snprintf(statbuf.ptr, STATBFSZ, "%s | %s", "test".ptr, "test2".ptr); + disp_message(statbuf.ptr, statlen); } -int skip(ubyte data) +void message(const(char)[] msg) { - screen.message("Skipping all 0x%x...", data); - - long pos = void; - const int e = searchSkip(data, pos); - - if (e) - { - screen.message("End of file reached"); - return e; - } - - safeSeek(pos); - //TODO: Format position found with current offset type - screen.message("Found at 0x%x", pos); - return 0; + disp_message(msg.ptr, msg.length); + status |= UMESSAGE; } -/// Print file information -void printFileInfo() +void quit() { - screen.message("%11s %s", - formatBin(editor.fileSize, setting.si), - editor.fileName); -} - -void exit() -{ - import core.stdc.stdlib : exit; - version (Trace) trace(); + trace("quit"); exit(0); -} - -void panic() -{ - import std.stdio : stderr; - import core.stdc.stdlib : exit; - version (Trace) trace("code=%u", errorcode); - - stderr.writeln("error: (", errorcode, ") ", errorMessage(errorcode)); - - exit(errorcode); } \ No newline at end of file diff --git a/src/display.d b/src/display.d new file mode 100644 index 0000000..886d645 --- /dev/null +++ b/src/display.d @@ -0,0 +1,301 @@ +/// Handles complex terminal operations. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module display; + +import std.range : chunks; +import std.conv : text; +import core.stdc.string : memset, memcpy; +import os.terminal; +import formatter; +import transcoder; +import utils.math; +import logger; + +enum Format +{ + hex, + dec, + oct, +} + +int selectFormat(string fmt) +{ + switch (fmt) with (Format) + { + case "hex": return hex; + case "dec": return dec; + case "oct": return oct; + default: + throw new Exception(text("Invalid format: ", fmt)); + } +} + +//TODO: cache rendered (data) lines +// dedicated char buffers + +private enum +{ + TUIMODE = 0x1, +} + +private __gshared +{ + /// Capabilities + int caps; +} + +void disp_init(bool tui) +{ + caps |= tui; + terminalInit(tui ? TermFeat.altScreen | TermFeat.inputSys : 0); +} + +int disp_readkey() +{ +Lread: + TermInput i = terminalRead(); + if (i.type != InputType.keyDown) goto Lread; + return i.key; +} + +// Given n columns, hint the optimal buffer size for the "view". +// If 0, calculated automatically +int disp_hint_cols() +{ + enum hexsize = 2; + enum decsize = 3; + enum octsize = 3; + TerminalSize w = terminalSize(); + return (w.columns - /*gofflen*/ 16) / (hexsize + 2); +} +int disp_hint_view(int cols) +{ + TerminalSize w = terminalSize(); + + enum hexsize = 2; + enum decsize = 3; + enum octsize = 3; + + // 16 - for text section? + return ((w.columns - cols /* 16 */) / (hexsize + 2)) * (w.rows - 2); +} + +void disp_enable_cursor() +{ + +} + +void disp_disable_cursor() +{ + +} + +void disp_cursor(int row, int col) +{ + terminalPos(col, row); +} +void disp_write(char* stuff, size_t sz) +{ + terminalWrite(stuff, sz); +} + +/// +void disp_header(int columns, + int addrfmt = Format.hex) +{ + //int max = int.max; + + if (caps & TUIMODE) + { + //max = terminalSize().columns; + disp_cursor(0, 0); + } + + enum BUFSZ = 2048; + __gshared char[BUFSZ] buffer; + + static immutable string prefix = "Offset("; + + string soff = void; + size_t function(char*, ubyte) format; + switch (addrfmt) with (Format) + { + case hex: + soff = "hex"; + format = &format8hex; + break; + case dec: + soff = "dec"; + //format = &; + break; + case oct: + soff = "oct"; + //format = &; + break; + default: + assert(false); + } + + memcpy(buffer.ptr, prefix.ptr, prefix.length); + memcpy(buffer.ptr + prefix.length, soff.ptr, soff.length); + + size_t i = prefix.length + 3; + buffer[i++] = ')'; + buffer[i++] = ' '; + + for (int col; col < columns; ++col) + { + buffer[i++] = ' '; + i += format(&buffer.ptr[i], cast(ubyte)col); + } + + buffer[i++] = '\n'; + + terminalWrite(buffer.ptr, i); +} + +/// +void disp_message(const(char)* msg, size_t len) +{ + TerminalSize w = terminalSize(); + disp_cursor(w.rows - 1, 0); + + //TODO: Invert colors for rest of space + terminalWrite(msg, min(len, w.columns)); +} + +// NOTE: Settings could be passed through a structure +/// +void disp_update(ulong base, ubyte[] data, + int columns, + int datafmt = Format.hex, int addrfmt = Format.hex, + char defaultchar = '.', int textfmt = CharacterSet.ascii, + int addrpadd = 11, int groupsize = 1) +{ + assert(columns > 0); + + enum BUFFERSZ = 1024 * 1024; + __gshared char[BUFFERSZ] buffer; + // Buffer size + size_t bufsz = BUFFERSZ; + // Buffer index + size_t bi = void; + + // Prepare data formatting functions + int elemsz = void; // Size of one data element, in characters, plus space + size_t function(char*, ubyte) formatdata; // Byte formatter + switch (datafmt) with (Format) + { + case hex: + formatdata = &format8hex; + elemsz = 3; + break; + case dec: + elemsz = 4; + break; + case oct: + elemsz = 4; + break; + default: + assert(false, "Invalid data format"); + } + + // Prepare address formatting functions + size_t function(char*, ulong) formataddr; + switch (addrfmt) with (Format) + { + case hex: + formataddr = &format64hex; + break; + case dec: + break; + case oct: + break; + default: + assert(false, "Invalid address format"); + } + + // Prepare transcoder + string function(ubyte) transcode = void; + switch (textfmt) with (CharacterSet) + { + case ascii: + transcode = &transcodeASCII; + break; + case cp437: + transcode = &transcodeCP437; + break; + case ebcdic: + transcode = &transcodeEBCDIC; + break; + case mac: + transcode = &transcodeMac; + break; + default: + assert(false, "Invalid character set"); + } + + // Starting position of text column + size_t cstart = addrpadd + 1 + (elemsz * columns) + 2; + + if (caps & TUIMODE) disp_cursor(1, 0); + + foreach (chunk; chunks(data, columns)) + { + // Format address, update address, add space + bi = formataddr(buffer.ptr, base); + base += columns; + buffer[bi++] = ' '; + + // Insert data and text bytes + size_t ci = cstart; + foreach (b, u8; chunk) + { + buffer[bi++] = ' '; + bi += formatdata(&buffer.ptr[bi], u8); + + // Transcode and insert it into buffer + immutable(char)[] units = transcode(u8); + if (units.length == 0) // No utf-8 codepoints, insert default char + { + buffer[ci++] = defaultchar; + continue; + } + foreach (codeunit; units) + buffer[ci++] = codeunit; + } + + // If row isn't entirely filled by column requirement + // NOTE: Text is filled as well to damage the display in TUI mode + if (chunk.length < columns) + { + size_t rem = columns - chunk.length; // remaining bytes + + // Fill empty data space + size_t sz = rem * elemsz; + memset(&buffer.ptr[bi], ' ', sz); bi += sz; + + // Fill empty text space + memset(&buffer.ptr[cstart + chunk.length], ' ', rem); + } + + // Add spaces between data and text columns + buffer[bi++] = ' '; + buffer[bi++] = ' '; + + // Add length of text column and terminate with newline + bi += ci - cstart; // text index - text start = text size in bytes + buffer[bi++] = '\n'; + + trace("out=%d", bi); + if (chunk.length < columns) + trace("buffer=%(-%02x%)", cast(ubyte[])buffer[0..bi]); + + terminalWrite(buffer.ptr, bi); + } +} + +private: + diff --git a/src/dump.d b/src/dump.d deleted file mode 100644 index d78d0f5..0000000 --- a/src/dump.d +++ /dev/null @@ -1,89 +0,0 @@ -/// Dumps binary data to stdout. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module dump; - -import os.terminal; -import error, editor, screen, settings; - -/*TODO: DumpOutput -// Then add to function as parameter - -// With custom "byte" formatter -// Default formatter has " " as prefix/suffix -// HTML formatter will have "" as prefix and "" as suffix - -enum DumpOutput { - text, - html -}*/ - -/// Dump to stdout, akin to xxd(1) or hexdump(1). -/// Params: -/// skip = If set, number of bytes to skip. -/// length = If set, maximum length to read. -/// Returns: Error code. -int start(long skip, long length) -{ - if (length < 0) - return errorPrint(1, "Length must be a positive value"); - - terminalInit(TermFeat.none); - - version (Trace) trace("skip=%d length=%d", skip, length); - - switch (editor.fileMode) with (FileMode) -{ - case file, mmfile: // Seekable - long fsize = editor.fileSize; - - // negative skip value: from end of file - if (skip < 0) - skip = fsize + skip; - - // adjust length if unset - if (length == 0) - length = fsize - skip; - else if (length < 0) - return errorPrint(1, "Length value must be positive"); - - // overflow check - //TODO: This shouldn't error and should take the size we get from file. - if (skip + length > fsize) - return errorPrint(1, "Specified length overflows file size"); - - break; - case stream: // Unseekable - if (skip < 0) - return errorPrint(1, "Skip value must be positive with stream"); - if (length == 0) - length = long.max; - break; - default: // Memory mode is never initiated from CLI - } - - // start skipping - if (skip) - editor.seek(skip); - - // top bar to stdout - screen.renderOffset; - - // mitigate unaligned reads/renders - size_t a = setting.columns * 16; - if (a > length) - a = cast(size_t)length; - editor.setBuffer(a); - - // read until EOF or length spec - long r; - do { - ubyte[] data = editor.read; - screen.output(r, data); - r += data.length; - //io.position = r; - } while (editor.eof == false && r < length); - - return 0; -} \ No newline at end of file diff --git a/src/dumper.d b/src/dumper.d new file mode 100644 index 0000000..4ac70cc --- /dev/null +++ b/src/dumper.d @@ -0,0 +1,61 @@ +/// Simple dumper UI, no interactive input. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module dumper; + +import std.stdio; +import std.file; +import core.stdc.stdlib : malloc; +import display; +import transcoder; +import utils.math; + +private enum CHUNKSIZE = 1024 * 1024; + +// NOTE: if path is null, then stdin is used +int dump(string path, int columns, + long skip, long length, + int charset) +{ + scope buffer = new ubyte[CHUNKSIZE]; + + // opAssign is bugged on ldc with optimizations + File file; + if (path) + { + file = File(path, "rb"); + + if (skip) file.seek(skip); + } + else + { + file = stdin; + + // Read blocks until length + if (skip) + { + Lskip: + size_t rdsz = min(skip, CHUNKSIZE); + if (file.rawRead(buffer[0..rdsz]).length == CHUNKSIZE) + goto Lskip; + } + } + + if (columns == 0) + columns = 16; + + disp_init(false); + + disp_header(columns); + + ulong address; + foreach (chunk; file.byChunk(CHUNKSIZE)) + { + disp_update(address, chunk, columns); + + address += chunk.length; + } + + return 0; +} diff --git a/src/editor.d b/src/editor.d index d9937b3..7443f44 100644 --- a/src/editor.d +++ b/src/editor.d @@ -1,664 +1,330 @@ +/// Implements a file buffer and file I/O. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) module editor; -import std.container.slist; -import std.stdio : File; -import std.path : baseName; -import std.file : getSize; -import core.stdc.stdio : FILE; -import settings, error; -import os.file, os.mmfile, os.terminal; -import utils.memory; +private import std.stdio : File; +private import os.file : OSFile, OFlags, Seek; +private import std.container.slist; +private import std.stdio : File; +private import std.path : baseName; +private import core.stdc.stdio : FILE; +private import utils.memory; +private import std.array : uninitializedArray; +private import core.stdc.stdlib : malloc, calloc, free; +private import core.stdc.string : memcmp; -// NOTE: Cursor management. -// -// +-> File position is at 0x10, at the start of the read buffer -// | -// | hex 01 02 03 04 -// 00000010 ab cd 11 22 -+ -// 00000014 33 44<55>66 +- Read buffer -// 00000018 77 88 99 ff -+ -// ^^ -// ++------ Cursor is at position 6 of read buffer - -// NOTE: Dependencies -// -// If the editor doesn't directly invoke screen handlers, the editor -// code could potentially be re-used in other projects. - -//TODO: Error mechanism -//TODO: Consider function hooks for basic events -// Like when the cursor has changed position, camera moved, etc. -// Trying to find a purpose for this... -//TODO: [0.5] Virtual change system. -// For editing/rendering/saving. -// Array!(Edit) or sorted dictionary? -// Obviously CTRL+Z for undo, CTRL+Y for redo. -//TODO: [0.5] ubyte[] view -// Copy screen buffer, modify depending on changes, send -// foreach edit -// if edit.position within current position + screen length -// modify screen result buffer -//TODO: File watcher -//TODO: File lock mechanic - -//TODO: Make editor hold more states and settings -// Lower module has internal function pointers set from received option - -/*private struct settings_t { - /// Bytes per row - //TODO: Rename to columns - ushort width = 16; - /// Current offset view type - NumberType offsetType; - /// Current data view type - NumberType dataType; - /// Default character to use for non-ascii characters - char defaultChar = '.'; - /// Use ISO base-10 prefixes over IEC base-2 - bool si; +/// Editor I/O mode. +enum IoMode : ushort +{ + file, /// Normal file. + mmfile, /// Memory-mapped file. + stream, /// Standard streaming I/O, often pipes. + memory, /// Typically from a stream buffered into memory. } -/// Current settings. -public __gshared settings_t settings2;*/ +// NOTE: Edition mode across files +// HxD2 keeps the edition mode on a per-file basis. +// While Notepad++ does not, it may have a dedicated read-only field. -// File properties - -/// FileMode for Io. -enum FileMode { - file, /// Normal file. - mmfile, /// Memory-mapped file. - stream, /// Standard streaming I/O, often pipes. - memory, /// Typically from a stream buffered into memory. -} - -/*enum SourceType { - file, - pipe, -}*/ - -/// Editor editing mode. -enum EditMode : ushort { - /// Incoming data will be overwritten at cursor position. - /// This is the default. - /// Editing: Enabled - /// Cursor: Enabled - overwrite, - /// Incoming data will be inserted at cursor position. - /// Editing: Enabled - /// Cursor: Enabled - insert, - /// The file cannot be edited. - /// Editing: Disabled - /// Cursor: Enabled - readOnly, - /// The file can only be viewed. - /// Editing: Disabled - /// Cursor: Disabled - view, +enum EditError +{ + none, + io, + memory, + bufferSize, } /// Represents a single edit -struct Edit { - EditMode mode; /// Which mode was used for edit - long position; /// Absolute offset of edit - ubyte value; /// Payload - // or ubyte[]? -} - -private union Source +struct Edit { - OSFile osfile; - OSMmFile mmfile; - File stream; - MemoryStream memory; + long position; /// Absolute offset of edit + int offset; /// Offset to byte group in digits + int value; /// Payload + // or ubyte[8]? } -private __gshared Source source; - -__gshared const(char)[] fileName; /// File base name. -__gshared FileMode fileMode; /// Current file mode. -__gshared long position; /// Last known set position. -//TODO: rename to viewSize -__gshared size_t readSize; /// For input size. -private __gshared ubyte[] readBuffer; /// For input data. -private __gshared uint vheight; /// -__gshared long fileSize; /// Last size of file - -// Editing stuff - -private __gshared size_t editIndex; /// Current edit position -private __gshared size_t editCount; /// Amount of edits in history -private __gshared SList!Edit editHistory; /// Temporary file edits -__gshared EditMode editMode; /// Current editing mode - -string editModeString(EditMode mode = editMode) -{ - final switch (mode) with (EditMode) { - case overwrite: return "ov"; - case insert: return "in"; - case readOnly: return "rd"; - case view: return "vw"; - } -} - -private struct cursor_t { - uint position; /// Screen cursor byte position - uint nibble; /// Data group nibble position -} -__gshared cursor_t cursor; /// Cursor state - -// View properties - -__gshared size_t viewSize; /// ? -__gshared ubyte[] viewBuffer; /// ? - -bool eof() -{ - final switch (fileMode) { - case FileMode.file: return source.osfile.eof; - case FileMode.mmfile: return source.mmfile.eof; - case FileMode.stream: return source.stream.eof; - case fileMode.memory: return source.memory.eof; - } -} - -bool err() -{ - final switch (fileMode) { - case FileMode.file: return source.osfile.err; - case FileMode.mmfile: return source.mmfile.err; - case FileMode.stream: return source.stream.error; - case fileMode.memory: return false; - } -} - -bool dirty() -{ - return editIndex > 0; -} - -// SECTION: File opening - -int openFile(string path) -{ - version (Trace) trace("path='%s'", path); - - if (source.osfile.open(path, OFlags.read | OFlags.exists)) - return errorSetOs; - - fileMode = FileMode.file; - fileName = baseName(path); - refreshFileSize; - return 0; -} - -int openMmfile(string path) -{ - version (Trace) trace("path='%s'", path); - - try - { - source.mmfile = new OSMmFile(path, MMFlags.read); - } - catch (Exception ex) - { - return errorSet(ex); - } - - fileMode = FileMode.mmfile; - fileName = baseName(path); - refreshFileSize; - return 0; -} - -int openStream(File file) -{ - version (Trace) trace(); - - source.stream = file; - fileMode = FileMode.stream; - fileName = null; - return 0; -} - -int openMemory(ubyte[] data) -{ - version (Trace) trace(); - - source.memory.open(data); - fileMode = FileMode.memory; - fileName = null; - refreshFileSize; - return 0; -} - -// !SECTION - -// SECTION: Buffer management - -void setBuffer(size_t size) -{ - readSize = size; - - switch (fileMode) with (FileMode) { - case file, stream: - readBuffer = new ubyte[size]; - return; - default: - } -} - -// !SECTION - -// -// SECTION: View position management -// - -long seek(long pos) -{ - version (Trace) trace("mode=%s", fileMode); - position = pos; - final switch (fileMode) with (FileMode) { - case file: return source.osfile.seek(Seek.start, pos); - case mmfile: return source.mmfile.seek(pos); - case memory: return source.memory.seek(pos); - case stream: - source.stream.seek(pos); - return source.stream.tell; - } -} - -long tell() -{ - version (Trace) trace("mode=%s", fileMode); - final switch (fileMode) with (FileMode) { - case file: return source.osfile.tell; - case mmfile: return source.mmfile.tell; - case stream: return source.stream.tell; - case memory: return source.memory.tell; - } -} - -// !SECTION - -// -// SECTION: Reading -// - -ubyte[] read() -{ - version (Trace) trace("mode=%s", fileMode); - final switch (fileMode) with (FileMode) { - case file: return source.osfile.read(readBuffer); - case mmfile: return source.mmfile.read(readSize); - case stream: return source.stream.rawRead(readBuffer); - case memory: return source.memory.read(readSize); - } -} -ubyte[] read(ubyte[] buffer) -{ - version (Trace) trace("mode=%s", fileMode); - final switch (fileMode) with (FileMode) { - case file: return source.osfile.read(buffer); - case mmfile: return source.mmfile.read(buffer.length); - case stream: return source.stream.rawRead(buffer); - case memory: return source.memory.read(buffer.length); - } -} - -// !SECTION - -// SECTION: Editing /// -ubyte[] peek() +struct Editor { - ubyte[] t = read().dup; + private enum DEFAULT_BUFSZ = 4096; + /// + //IoMode iomode; - - return null; -} - -void keydown(Key key) -{ - debug assert(editMode != EditMode.readOnly, - "Editor should not be getting edits in read-only mode"); - - //TODO: Check by panel (binary or text) - //TODO: nibble-awareness - -} - -/// Append change at current position -void appendEdit(ubyte data) -{ - debug assert(editMode != EditMode.readOnly, - "Editor should not be getting edits in read-only mode"); - - -} - -/// Write all changes to file. -/// Only edits [0..index] will be carried over. -/// When done, [0..index] edits will be cleared. -void writeEdits() -{ - -} - - -// !SECTION - -// -// SECTION View position management -// - -// NOTE: These return true if the position of the view changed. -// This is to determine if it's really necessary to update the -// view on screen, because pointlessly rendering content on-screen -// is not something I want to waste. - -private -bool viewStart() -{ - bool z = position != 0; - position = 0; - return z; -} -private -bool viewEnd() -{ - long old = position; - long npos = (fileSize - readSize) + setting.columns; - npos -= npos % setting.columns; // re-align to columns - position = npos; - return position != old; -} -private -bool viewUp() -{ - long npos = position - setting.columns; - if (npos < 0) - return false; - position = npos; - return true; -} -private -bool viewDown() -{ - long fsize = fileSize; - if (position + readSize > fsize) - return false; - position += setting.columns; - return true; -} -private -bool viewPageUp() -{ - long npos = position - readSize; - bool ok = npos >= 0; - position = ok ? npos : 0; - return ok; -} -private -bool viewPageDown() -{ - long npos = position + readSize; - bool ok = npos < fileSize; - if (ok) position = npos; - return ok; -} - -// !SECTION - -// -// SECTION Cursor position management -// - -void cursorBound() -{ - // NOTE: It is impossible for the cursor to be at a negative position - // Because it is unsigned and the cursor is 0-based - - long fsize = fileSize; - bool nok = cursorTell > fsize; - - if (nok) + private union { - int l = cast(int)(cursorTell - fsize); - cursor.position -= l; - return; + OSFile osfile; + //OSMmFile mmfile; + //File stream; + //MemoryStream memstream; } -} - -// NOTE: These return true if view has moved. - -/// Move cursor at the absolute start of the file. -/// Returns: True if the view moved. -bool cursorAbsStart() -{ - with (cursor) position = nibble = 0; - return viewStart; -} -/// Move cursor at the absolute end of the file. -/// Returns: True if the view moved. -bool cursorAbsEnd() -{ - uint base = cast(uint)(readSize < fileSize ? fileSize : readSize); - uint rem = cast(uint)(fileSize % setting.columns); - uint h = cast(uint)(base / setting.columns); - cursor.position = h + rem; - return viewEnd; -} -/// Move cursor at the start of the row. -/// Returns: True if the view moved. -bool cursorHome() // put cursor at the start of row -{ - cursor.position = cursor.position - (cursor.position % setting.columns); - cursor.nibble = 0; - return false; -} -/// Move cursor at the end of the row. -/// Returns: True if the view moved. -bool cursorEnd() // put cursor at the end of the row -{ - cursor.position = - (cursor.position - (cursor.position % setting.columns)) - + setting.columns - 1; - if (cursorTell > fileSize) + + struct Edits { - uint rem = cast(uint)(fileSize % setting.columns); - cursor.position = (cursor.position + setting.columns - rem); + size_t index; + size_t count; + Edit[long] list; + SList!long history; + const(char)[] name = "ov"; } - cursor.nibble = 0; - return false; -} -/// Move cursor up the file by a data group. -/// Returns: True if the view moved. -bool cursorLeft() -{ - if (cursor.position == 0) + Edits edits; + bool insert; + bool readonly; + + /// + private ubyte* rdbuf; + /// + private size_t rdbufsz; + /// File position + private long position; + + int lasterr; + + int seterr(int er) { - if (position == 0) - return false; - cursorEnd; - return viewUp; + return (lasterr = er); } - --cursor.position; - cursor.nibble = 0; - return false; -} -/// Move cursor down the file by a data group. -/// Returns: True if the view moved. -bool cursorRight() -{ - if (cursorTell >= fileSize) - return false; - - if (cursor.position == readSize - 1) + int open(string path, bool exists, bool viewonly) { - cursorHome; - return viewDown; + readonly = viewonly; + int flags = readonly ? OFlags.read : OFlags.readWrite; + if (exists) flags |= OFlags.exists; + + if (osfile.open(path, flags)) + return seterr(EditError.io); + + return 0; } - ++cursor.position; - cursor.nibble = 0; - return false; -} -/// Move cursor up the file by the number of columns. -/// Returns: True if the view moved. -bool cursorUp() -{ - if (cursor.position < setting.columns) + void close() { - return viewUp; + return osfile.close(); } - cursor.position -= setting.columns; - return false; -} -/// Move cursor down the file by the bymber of columns. -/// Returns: True if the view moved. -bool cursorDown() -{ - /// File size - long fsize = fileSize; - /// Normalized file size with last row (remaining) trimmed - long fsizenorm = fsize - (fsize % setting.columns); - /// Absolute cursor position - long acpos = cursorTell; - - bool bottom = cursor.position + setting.columns >= readSize; // cursor bottom - bool finalr = acpos >= fsizenorm; /// final row - - version (Trace) trace("bottom=%s final=%s", bottom, finalr); - - if (finalr) - return false; - - if (bottom) - return viewDown; - - if (acpos + setting.columns > fsize) - { - uint rem = cast(uint)(fsize % setting.columns); - cursor.position = cast(uint)(readSize - setting.columns + rem); - //cursor.position = cast(uint)((fsize + cursor.position) - fsize); - return false; - } - - cursor.position += setting.columns; - return false; -} -/// Move cursor by a page up the file. -/// Returns: True if the view moved. -bool cursorPageUp() -{ -// int v = readSize / setting.columns; - return viewPageUp; -} -/// Move cursor by a page down the file. -/// Returns: True if the view moved. -bool cursorPageDown() -{ - bool ok = viewPageDown; - cursorBound; - return ok; -} -/// Get cursor absolute position within the file. -/// Returns: Cursor absolute position in file. -long cursorTell() -{ - return position + cursor.position; -} -/// Make the cursor jump to an absolute position within the file. -/// This is used for search results. -/// Returns: True if the view moved. -void cursorGoto(long m) -{ - // Per view chunks, then per y chunks, then x - //long npos = - -} - -// !SECTION - -long refreshFileSize() -{ - final switch (fileMode) with (FileMode) { - case file: fileSize = source.osfile.size; break; - case mmfile: fileSize = source.mmfile.length; break; - case memory: fileSize = source.memory.size; break; - case stream: fileSize = source.stream.size; break; - } - - version (Trace) trace("sz=%u", fileSize); - - return fileSize; -} - -//TODO: from other types? -// or implement this via MemoryStream? -int slurp(long skip = 0, long length = 0) -{ - import std.array : uninitializedArray; - import core.stdc.stdio : fread; - import core.stdc.stdlib : malloc, free; - import std.algorithm.comparison : min; - import std.outbuffer : OutBuffer; - import std.typecons : scoped; - - enum READ_SIZE = 4096; - - version (Trace) trace("skip=%u length=%u", skip, length); - // - // Skiping + // // - ubyte *b = cast(ubyte*)malloc(READ_SIZE); - if (b == null) - return errorSetOs; + //TODO: Consider const(char)[] errorMsg() + // Clear message buffer on retrieval - FILE *_file = source.stream.getFP; - - if (skip) + bool error() { + return lasterr != 0; + } + void clearerr() + { + lasterr = 0; + } + bool eof() + { + return osfile.eof; + } + + void setbuffer(size_t newsize) + { + version (Trace) trace("newsize=%u", newsize); + + // If there is an exisiting size, must have been allocated before + if (rdbufsz) free(rdbuf); + + // Allocate read buffer + rdbuf = cast(ubyte*)malloc(newsize); + if (rdbuf == null) + { + lasterr = EditError.memory; + } + rdbufsz = newsize; + } + + // + // Editing + // + + bool dirty() + { + return edits.index > 0; + } + + // digit: true value (not a character) + int addEdit(long editpos, int digit, int groupmax) + { + int ndpos; // new digit position + edits.list.update(editpos, + // edit does not exist, add it to the list + // and update history + () { + Edit nedit; // New edit + return nedit; + }, + // edit exists at this position + // if so, increase the digit position when able + (ref Edit oldedit) { + } + ); + return ndpos; + } + + void removeEdit(long editpos) + { + if (readonly) + return; + + } + + void removeLastEdit() + { + if (readonly) + return; + + } + + /+void editMode(EditMode emode) + { + edits.mode = emode; + final switch (emode) with (EditMode) { + case overwrite: edits.modeString = "ov"; return; + case insert: edits.modeString = "in"; return; + case readOnly: edits.modeString = "rd"; return; + case view: edits.modeString = "vw"; return; + } + } + + ubyte[] peek() + { + return null; + }+/ + + // + // File operations + // + + bool seek(long pos, Seek seek = Seek.start) + { + version (Trace) trace("mode=%s", fileMode); + position = pos; + /*final switch (input.mode) with (FileMode) { + case file: + position = io.osfile.seek(Seek.start, pos); + return io.osfile.err; + case mmfile: + position = io.mmfile.seek(pos); + return io.mmfile.err; + case memory: + position = io.memory.seek(pos); + return io.memory.err; + case stream: + io.stream.seek(pos); + position = io.stream.tell; + return io.stream.error; + }*/ + osfile.seek(seek, pos); + return osfile.err; + } + + long tell() + { + version (Trace) trace("mode=%s", fileMode); + /*final switch (input.mode) with (FileMode) { + case file: return io.osfile.tell; + case mmfile: return io.mmfile.tell; + case stream: return io.stream.tell; + case memory: return io.memory.tell; + }*/ + return position; + } + + long size() + { + return osfile.size(); + } + + ubyte[] read() + { + version (Trace) trace("mode=%s", fileMode); + /*final switch (input.mode) with (FileMode) { + case file: return buffer.output = io.osfile.read(buffer.input); + case mmfile: return buffer.output = io.mmfile.read(buffer.size); + case stream: return buffer.output = io.stream.rawRead(buffer.input); + case memory: return buffer.output = io.memory.read(buffer.size); + }*/ + ubyte[] res = osfile.read(rdbuf, rdbufsz); + //TODO: Edit res + return res; + } + + //TODO: Turning into MemoryStream should be optional + // e.g., dump -> only read until skip + // ddhx -> read all into memory + // slurp -> only read skip amount + // readAll -> read all into MemoryStream + //TODO: Rename to convert or toMemoryStream + /// Read stream into memory. + /// Params: + /// skip = Number of bytes to skip. + /// length = Length of data to read into memory. + /// Returns: Error code. + /+int slurp(long skip = 0, long length = 0) + { + //NOTE: Can't close File, could be stdin + // Let attached Editor close stream if need be + + import core.stdc.stdio : fread; + import core.stdc.stdlib : malloc, free; + import std.algorithm.comparison : min; + import std.outbuffer : OutBuffer; + + enum READ_SIZE = 4096; + + version (Trace) trace("skip=%u length=%u", skip, length); + + // Skiping + + ubyte *tmpbuf = cast(ubyte*)malloc(READ_SIZE); + if (tmpbuf == null) + { + return seterr(EditError.memory); + } + scope(exit) free(tmpbuf); + + FILE *fp = io.stream.getFP; + + if (skip) + { + do + { + size_t bsize = cast(size_t)min(READ_SIZE, skip); + skip -= fread(tmpbuf, 1, bsize, fp); + } while (skip > 0); + } + + // Reading + + scope outbuf = new OutBuffer; + + // If no length set, just read as much as possible. + if (length == 0) length = long.max; + + // Loop ends when len (read length) is under the buffer's length + // or requested length. do { - size_t bsize = cast(size_t)min(READ_SIZE, skip); - skip -= fread(b, 1, bsize, _file); - } while (skip > 0); - } - - // - // Reading - // - - auto outbuf = scoped!OutBuffer; - - // If no length set, just read as much as possible. - if (length == 0) length = long.max; - - // Loop ends when len (read length) is under the buffer's length - // or requested length. - do { - size_t bsize = cast(size_t)min(READ_SIZE, length); - size_t len = fread(b, 1, bsize, _file); - if (len == 0) break; - outbuf.put(b[0..len]); - if (len < bsize) break; - length -= len; - } while (length > 0); - - free(b); - - version (Trace) trace("outbuf.offset=%u", outbuf.offset); - - source.memory.open(outbuf.toBytes); - - version (Trace) trace("source.memory.size=%u", source.memory.size); - - fileMode = FileMode.memory; - return 0; + size_t bsize = cast(size_t)min(READ_SIZE, length); + size_t len = fread(tmpbuf, 1, bsize, fp); + if (len == 0) break; + outbuf.put(b[0..len]); + if (len < bsize) break; + length -= len; + } while (length > 0); + + version (Trace) trace("outbuf.offset=%u", outbuf.offset); + + io.memory.copy(outbuf.toBytes); + + version (Trace) trace("io.memory.size=%u", io.memory.size); + + input.mode = FileMode.memory; + }+/ } \ No newline at end of file diff --git a/src/error.d b/src/error.d deleted file mode 100644 index 3492704..0000000 --- a/src/error.d +++ /dev/null @@ -1,223 +0,0 @@ -/// Error handling. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module error; - -import std.stdio; - -/// Error codes -enum ErrorCode { - success, - fileEmpty = 2, - negativeValue, - inputEmpty, - invalidCommand, - invalidSetting, - invalidParameter, - invalidNumber, - invalidType, - invalidCharset, - notFound, // ?? - overflow, - unparsable, - noLastItem, - eof, // ?? - unimplemented, - insufficientSpace, - missingOption, - missingValue, - missingType, - missingNeedle, - screenMinimumRows, - screenMinimumColumns, - - // Settings - - settingFileMissing = 100, - settingColumnsInvalid, - - // Special - - unknown = 0xf000, - exception, -} -/// Error source -enum ErrorSource { - code, - exception, - os, -} - -private struct last_t { - const(char)[] message; - const(char)[] file; - int line; - ErrorSource source; -} -private __gshared last_t last; -__gshared int errorcode; /// Last error code. - -/// Get system error message from an error code. -/// Params: code = System error code. -/// Returns: Error message. -const(char)[] systemMessage(int code) -{ - import std.string : fromStringz; - - version (Windows) - { - import core.sys.windows.winbase : - LocalFree, FormatMessageA, - FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM, - FORMAT_MESSAGE_IGNORE_INSERTS; - import core.sys.windows.winnt : - MAKELANGID, LANG_NEUTRAL, SUBLANG_DEFAULT; - - enum LANG = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); - - char *strerror; - - uint r = FormatMessageA( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - null, - code, - LANG, - cast(char*)&strerror, - 0, - null); - - assert(strerror, "FormatMessageA failed"); - - if (strerror[r - 1] == '\n') --r; - if (strerror[r - 1] == '\r') --r; - string msg = strerror[0..r].idup; - LocalFree(strerror); - return msg; - } else { - import core.stdc.string : strerror; - - return strerror(code).fromStringz; - } -} - -int errorSet(ErrorCode code, string file = __FILE__, int line = __LINE__) -{ - version (unittest) - { - import std.stdio : writefln; - writefln("%s:%u: %s", file, line, code); - } - version (Trace) trace("%s:%u: %s", file, line, code); - last.file = file; - last.line = line; - last.source = ErrorSource.code; - return (errorcode = code); -} - -int errorSet(Exception ex) -{ - version (unittest) - { - import std.stdio : writeln; - writeln(ex); - } - version (Trace) - { - debug trace("%s", ex); - else trace("%s", ex.msg); - } - last.file = ex.file; - last.line = cast(int)ex.line; - last.message = ex.msg; - last.source = ErrorSource.exception; - return (errorcode = ErrorCode.exception); -} - -int errorSetOs(string file = __FILE__, int line = __LINE__) -{ - version (Windows) - { - import core.sys.windows.winbase : GetLastError; - errorcode = GetLastError(); - } - else version (Posix) - { - import core.stdc.errno : errno; - errorcode = errno; - } - version (Trace) trace("errorcode=%u line=%s:%u", errorcode, file, line); - last.file = file; - last.line = line; - last.source = ErrorSource.os; - return errorcode; -} - -const(char)[] errorMessage(int code = errorcode) -{ - final switch (last.source) with (ErrorSource) { - case os: return systemMessage(errorcode); - case exception: return last.message; - case code: - } - - switch (code) with (ErrorCode) { - case fileEmpty: return "File is empty."; - case inputEmpty: return "Input is empty."; - case invalidCommand: return "Command not found."; - case invalidSetting: return "Invalid setting property."; - case invalidParameter: return "Parameter is invalid."; - case invalidType: return "Invalid type."; - case eof: return "Unexpected end of file (EOF)."; - case notFound: return "Data not found."; - case overflow: return "Integer overflow."; - case unparsable: return "Integer could not be parsed."; - case noLastItem: return "No previous search items saved."; - case insufficientSpace: return "Too little space left to search."; - - // Settings - - case settingFileMissing: return "Settings file does not exist at given path."; - case settingColumnsInvalid: return "Columns cannot be zero."; - - case success: return "No errors occured."; - default: return "Internal error occured."; - } -} - -int errorPrint() -{ - return errorPrint(errorcode, errorMessage()); -} -int errorPrint(A...)(int code, const(char)[] fmt, A args) -{ - stderr.write("error: "); - stderr.writefln(fmt, args); - return code; -} - -version (Trace) -{ - public import std.datetime.stopwatch; - - private __gshared File log; - - void traceInit(string n) - { - log.open("ddhx.log", "w"); - trace(n); - } - void trace(string func = __FUNCTION__, int line = __LINE__, A...)() - { - log.writefln("TRACE:%s:%u", func, line); - log.flush; - } - void trace(string func = __FUNCTION__, int line = __LINE__, A...)(string fmt, A args) - { - log.writef("TRACE:%s:%u: ", func, line); - log.writefln(fmt, args); - log.flush; - } -} diff --git a/src/formatter.d b/src/formatter.d new file mode 100644 index 0000000..d8392d9 --- /dev/null +++ b/src/formatter.d @@ -0,0 +1,511 @@ +/// Handles data formatting. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module formatter; + +//TODO: format function for int + +size_t format8hex(char *buffer, ubyte v) +{ + buffer[1] = hexMap[v & 15]; + buffer[0] = hexMap[v >> 4]; + return 2; +} +@system unittest +{ + char[2] c = void; + format02x(c.ptr, 0x01); + assert(c[] == "01", c); + format02x(c.ptr, 0x20); + assert(c[] == "20", c); + format02x(c.ptr, 0xff); + assert(c[] == "ff", c); +} + +size_t format64hex(char *buffer, ulong 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 >= 44) + { + continue; // cut + } + else if (pad && shift >= 4) + { + buffer[pos++] = pad ? ' ' : '0'; + continue; // pad + } + } + else // Padding no longer acceptable + pad = false; + buffer[pos++] = hexMap[b]; + } + return pos; +} +@system unittest +{ + char[32] b = void; + char *p = b.ptr; + assert(b[0..format64hex(p, 0)] == " 0"); + assert(b[0..format64hex(p, 1)] == " 1"); + assert(b[0..format64hex(p, 0x10)] == " 10"); + assert(b[0..format64hex(p, 0x100)] == " 100"); + assert(b[0..format64hex(p, 0x1000)] == " 1000"); + assert(b[0..format64hex(p, 0x10000)] == " 10000"); + assert(b[0..format64hex(p, 0x100000)] == " 100000"); + assert(b[0..format64hex(p, 0x1000000)] == " 1000000"); + assert(b[0..format64hex(p, 0x10000000)] == " 10000000"); + assert(b[0..format64hex(p, 0x100000000)] == " 100000000"); + assert(b[0..format64hex(p, 0x1000000000)] == " 1000000000"); + assert(b[0..format64hex(p, 0x10000000000)] == "10000000000"); + assert(b[0..format64hex(p, 0x100000000000)] == "100000000000"); + assert(b[0..format64hex(p, 0x1000000000000)] == "1000000000000"); + assert(b[0..format64hex(p, ubyte.max)] == " ff"); + assert(b[0..format64hex(p, ushort.max)] == " ffff"); + assert(b[0..format64hex(p, uint.max)] == " ffffffff"); + assert(b[0..format64hex(p, ulong.max)] == "ffffffffffffffff"); + assert(b[0..format64hex(p, 0x1010)] == " 1010"); + assert(b[0..format64hex(p, 0x10101010)] == " 10101010"); + assert(b[0..format64hex(p, 0x1010101010101010)] == "1010101010101010"); +} + +private: + +immutable string hexMap = "0123456789abcdef"; + +size_t format02x(char *buffer, ubyte v) +{ + buffer[1] = hexMap[v & 15]; + buffer[0] = hexMap[v >> 4]; + return 2; +} +@system unittest +{ + char[2] c = void; + format02x(c.ptr, 0x01); + assert(c[] == "01", c); + format02x(c.ptr, 0x20); + assert(c[] == "20", c); + format02x(c.ptr, 0xff); + assert(c[] == "ff", c); +} + +size_t format11x(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 >= 44) + { + continue; // cut + } + else if (pad && shift >= 4) + { + buffer[pos++] = pad ? ' ' : '0'; + continue; // pad + } + } else pad = false; + buffer[pos++] = hexMap[b]; + } + return pos; +} +/// +@system unittest +{ + char[32] b = void; + char *p = b.ptr; + assert(b[0..format11x(p, 0)] == " 0"); + assert(b[0..format11x(p, 1)] == " 1"); + assert(b[0..format11x(p, 0x10)] == " 10"); + assert(b[0..format11x(p, 0x100)] == " 100"); + assert(b[0..format11x(p, 0x1000)] == " 1000"); + assert(b[0..format11x(p, 0x10000)] == " 10000"); + assert(b[0..format11x(p, 0x100000)] == " 100000"); + assert(b[0..format11x(p, 0x1000000)] == " 1000000"); + assert(b[0..format11x(p, 0x10000000)] == " 10000000"); + assert(b[0..format11x(p, 0x100000000)] == " 100000000"); + assert(b[0..format11x(p, 0x1000000000)] == " 1000000000"); + assert(b[0..format11x(p, 0x10000000000)] == "10000000000"); + assert(b[0..format11x(p, 0x100000000000)] == "100000000000"); + assert(b[0..format11x(p, 0x1000000000000)] == "1000000000000"); + assert(b[0..format11x(p, ubyte.max)] == " ff"); + assert(b[0..format11x(p, ushort.max)] == " ffff"); + assert(b[0..format11x(p, uint.max)] == " ffffffff"); + assert(b[0..format11x(p, ulong.max)] == "ffffffffffffffff"); + assert(b[0..format11x(p, 0x1010)] == " 1010"); + assert(b[0..format11x(p, 0x10101010)] == " 10101010"); + assert(b[0..format11x(p, 0x1010101010101010)] == "1010101010101010"); +} + +immutable static string decMap = "0123456789"; +size_t format03d(char *buffer, ubyte v) +{ + buffer[2] = (v % 10) + '0'; + buffer[1] = (v / 10 % 10) + '0'; + buffer[0] = (v / 100 % 10) + '0'; + return 3; +} +@system unittest +{ + char[3] c = void; + format03d(c.ptr, 1); + assert(c[] == "001", c); + format03d(c.ptr, 10); + assert(c[] == "010", c); + format03d(c.ptr, 111); + assert(c[] == "111", c); +} + +size_t format11d(char *buffer, long v) +{ + debug import std.conv : text; + enum ulong I64MAX = 10_000_000_000_000_000_000UL; + 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_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++] = decMap[r]; + } + return pos; +} +/// +@system unittest +{ + char[32] b = void; + char *p = b.ptr; + assert(b[0..format11d(p, 0)] == " 0"); + assert(b[0..format11d(p, 1)] == " 1"); + assert(b[0..format11d(p, 10)] == " 10"); + assert(b[0..format11d(p, 100)] == " 100"); + assert(b[0..format11d(p, 1000)] == " 1000"); + assert(b[0..format11d(p, 10_000)] == " 10000"); + assert(b[0..format11d(p, 100_000)] == " 100000"); + assert(b[0..format11d(p, 1000_000)] == " 1000000"); + assert(b[0..format11d(p, 10_000_000)] == " 10000000"); + assert(b[0..format11d(p, 100_000_000)] == " 100000000"); + assert(b[0..format11d(p, 1000_000_000)] == " 1000000000"); + assert(b[0..format11d(p, 10_000_000_000)] == "10000000000"); + assert(b[0..format11d(p, 100_000_000_000)] == "100000000000"); + assert(b[0..format11d(p, 1000_000_000_000)] == "1000000000000"); + assert(b[0..format11d(p, ubyte.max)] == " 255"); + assert(b[0..format11d(p, ushort.max)] == " 65535"); + assert(b[0..format11d(p, uint.max)] == " 4294967295"); + assert(b[0..format11d(p, ulong.max)] == "18446744073709551615"); + assert(b[0..format11d(p, 1010)] == " 1010"); +} + +size_t format03o(char *buffer, ubyte v) +{ + buffer[2] = (v % 8) + '0'; + buffer[1] = (v / 8 % 8) + '0'; + buffer[0] = (v / 64 % 8) + '0'; + return 3; +} +@system unittest +{ + import std.conv : octal; + char[3] c = void; + format03o(c.ptr, 1); + assert(c[] == "001", c); + format03o(c.ptr, octal!20); + assert(c[] == "020", c); + format03o(c.ptr, octal!133); + assert(c[] == "133", c); +} + +size_t format11o(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 >= 33) + { + continue; // cut + } + else if (pad && shift >= 3) + { + buffer[pos++] = pad ? ' ' : '0'; + continue; + } + } else pad = false; + buffer[pos++] = hexMap[b]; + } + return pos; +} +/// +@system unittest +{ + import std.conv : octal; + char[32] b = void; + char *p = b.ptr; + assert(b[0..format11o(p, 0)] == " 0"); + assert(b[0..format11o(p, 1)] == " 1"); + assert(b[0..format11o(p, octal!10)] == " 10"); + assert(b[0..format11o(p, octal!20)] == " 20"); + assert(b[0..format11o(p, octal!100)] == " 100"); + assert(b[0..format11o(p, octal!1000)] == " 1000"); + assert(b[0..format11o(p, octal!10_000)] == " 10000"); + assert(b[0..format11o(p, octal!100_000)] == " 100000"); + assert(b[0..format11o(p, octal!1000_000)] == " 1000000"); + assert(b[0..format11o(p, octal!10_000_000)] == " 10000000"); + assert(b[0..format11o(p, octal!100_000_000)] == " 100000000"); + assert(b[0..format11o(p, octal!1000_000_000)] == " 1000000000"); + assert(b[0..format11o(p, octal!10_000_000_000)] == "10000000000"); + assert(b[0..format11o(p, octal!100_000_000_000)] == "100000000000"); + assert(b[0..format11o(p, ubyte.max)] == " 377"); + assert(b[0..format11o(p, ushort.max)] == " 177777"); + assert(b[0..format11o(p, uint.max)] == "37777777777"); + assert(b[0..format11o(p, ulong.max)] == "1777777777777777777777"); + assert(b[0..format11o(p, octal!101_010)] == " 101010"); +} + +// !SECTION + +version (none): + +//int outputLine(long base, ubyte[] data, int row, int cursor = -1) + +//TODO: Add int param for data at cursor (placeholder) +/// Render multiple lines on screen with optional cursor. +/// Params: +/// base = Offset base. +/// data = data to render. +/// cursor = Position of cursor. +/// Returns: Number of rows printed. Negative numbers indicate error. +int output(long base, ubyte[] data, int cursor = -1) +{ + int crow = void, ccol = void; + + if (data.length == 0) + return 0; + + if (cursor < 0) + crow = ccol = -1; + else + { + crow = cursor / setting.columns; + ccol = cursor % setting.columns; + } + + version (Trace) + { + trace("base=%u D=%u crow=%d ccol=%d", + base, data.length, crow, ccol); + StopWatch sw = StopWatch(AutoStart.yes); + } + + size_t buffersz = // minimum anyway + OFFSET_SPACE + 2 + // offset + spacer + ((binaryFormatter.size + 1) * setting.columns) + // binary + spacer * cols + (1 + (setting.columns * 3)); // spacer + text (utf-8) + + char *buffer = cast(char*)malloc(buffersz); + if (buffer == null) return -1; + + int lines; + foreach (chunk; chunks(data, setting.columns)) + { + const bool cur_row = lines == crow; + + Row row = makerow(buffer, buffersz, chunk, base, ccol); + + if (cur_row) + { + version (Trace) trace( + "row.length=%u cbi=%u cbl=%u cti=%u ctl=%u bl=%u tl=%u", + row.result.length, + row.cursorBinaryIndex, + row.cursorBinaryLength, + row.cursorTextIndex, + row.cursorTextLength, + row.binaryLength, + row.textLength); + + // between binary and text cursors + size_t distance = row.cursorTextIndex - + row.cursorBinaryIndex - + row.cursorBinaryLength; + + char *p = buffer; + // offset + pre-cursor binary + p += cwrite(p, row.cursorBinaryIndex); + // binary cursor + terminalInvertColor; + p += cwrite(p, row.cursorBinaryLength); + terminalResetColor; + // post-cursor binary + pre-cursor text (minus spacer) + p += cwrite(p, distance); + // text cursor + terminalHighlight; + p += cwrite(p, row.cursorTextLength); + terminalResetColor; + // post-cursor text + size_t rem = row.result.length - (p - buffer); + p += cwrite(p, rem); + + version (Trace) trace("d=%u r=%u l=%u", distance, rem, p - buffer); + } + else + cwrite(row.result.ptr, row.result.length); + + cwrite('\n'); + + ++lines; + base += setting.columns; + } + + free(buffer); + + version (Trace) + { + sw.stop; + trace("time='%s µs'", sw.peek.total!"usecs"); + } + + return lines; +} + +//TODO: Consider moving to this ddhx +void renderEmpty(uint rows, int w) +{ + version (Trace) + { + trace("lines=%u rows=%u cols=%u", lines, rows, w); + StopWatch sw = StopWatch(AutoStart.yes); + } + + char *p = cast(char*)malloc(w); + assert(p); //TODO: Soft asserts + memset(p, ' ', w); + + //TODO: Output to scoped OutBuffer + for (int i; i < rows; ++i) + cwrite(p, w); + + free(p); + + version (Trace) + { + sw.stop; + trace("time='%s µs'", sw.peek.total!"usecs"); + } +} + +private +struct Row +{ + char[] result; + + size_t cursorBinaryIndex; + size_t cursorBinaryLength; + size_t cursorTextIndex; + size_t cursorTextLength; + + size_t binaryLength; + size_t textLength; +} + +private +Row makerow(char *buffer, size_t bufferlen, + ubyte[] chunk, long pos, + int cursor_col) +{ + Row row = void; + + // Insert OFFSET + size_t indexData = offsetFormatter.offset(buffer, pos); + buffer[indexData++] = ' '; // index: OFFSET + space + + const uint dataLen = (setting.columns * (binaryFormatter.size + 1)); /// data row character count + size_t indexChar = indexData + dataLen; // Position for character column + + *(cast(ushort*)(buffer + indexChar)) = 0x2020; // DATA-CHAR spacer + indexChar += 2; // indexChar: indexData + dataLen + spacer + + // Format DATA and CHAR + // NOTE: Smaller loops could fit in cache... + // And would separate data/text logic + size_t bi0 = indexData, ti0 = indexChar; + int currentCol; + foreach (data; chunk) + { + const bool curhit = currentCol == cursor_col; // cursor hit column + //TODO: Maybe binary data formatter should include space? + // Data translation + buffer[indexData++] = ' '; + if (curhit) + { + row.cursorBinaryIndex = indexData; + row.cursorBinaryLength = binaryFormatter.size; + } + indexData += binaryFormatter.data(buffer + indexData, data); + // Character translation + immutable(char)[] units = transcoder.transform(data); + if (curhit) + { + row.cursorTextIndex = indexChar; + row.cursorTextLength = units.length ? units.length : 1; + } + if (units.length) // Has utf-8 codepoints + { + foreach (codeunit; units) + buffer[indexChar++] = codeunit; + } else // Invalid character, insert default character + buffer[indexChar++] = setting.defaultChar; + + ++currentCol; + } + + row.binaryLength = dataLen - bi0; + row.textLength = indexChar - ti0; + + size_t end = indexChar; + + // data length < minimum row requirement = in-fill data and text columns + if (chunk.length < setting.columns) + { + // In-fill characters: left = Columns - ChunkLength + size_t leftchar = (setting.columns - chunk.length); // Bytes left + memset(buffer + indexChar, ' ', leftchar); + row.textLength += leftchar; + // In-fill binary data: left = CharactersLeft * (DataSize + 1) + size_t leftdata = leftchar * (binaryFormatter.size + 1); + memset(buffer + indexData, ' ', leftdata); + row.binaryLength += leftdata; + + end += leftchar; + } + + row.result = buffer[0..end]; + + return row; +} \ No newline at end of file diff --git a/src/logger.d b/src/logger.d new file mode 100644 index 0000000..e97041a --- /dev/null +++ b/src/logger.d @@ -0,0 +1,30 @@ +/// Basic logger. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module logger; + +import std.stdio; +import std.datetime.stopwatch; + +private __gshared File tracefile; +private __gshared StopWatch sw; +private __gshared bool tracing; + +void traceInit() +{ + tracing = true; + tracefile = File("ddhx.log", "w"); + tracefile.setvbuf(0, _IONBF); + sw.start(); +} + +void trace(A...)(string fmt, A args) +{ + if (tracing == false) + return; + + double ms = sw.peek().total!"msecs"() / 1_000.0; + tracefile.writef("[%08.3f] ", ms); + tracefile.writefln(fmt, args); +} \ No newline at end of file diff --git a/src/main.d b/src/main.d index 42b8028..fb6028f 100644 --- a/src/main.d +++ b/src/main.d @@ -1,241 +1,198 @@ /// Command-line interface. /// -/// Some of these functions are private for linter reasons /// Copyright: dd86k /// License: MIT /// Authors: $(LINK2 https://github.com/dd86k, dd86k) module main; +import std.compiler : version_major, version_minor; import std.stdio, std.mmfile, std.format, std.getopt; +import std.conv : text; import core.stdc.stdlib : exit; -import ddhx, editor, dump, reverser; +import ddhx, dumper; +import utils.strings; +import display : Format, selectFormat; +import transcoder : CharacterSet; +import logger; -//TODO: --only=n -// text: only display translated text -// data: only display hex data -//TODO: --memory -// read all into memory -// current seeing no need for this +private: // Shuts up the linter -private: +/// Copyright string +immutable string DDHX_COPYRIGHT = "Copyright (c) 2017-2024 dd86k "; +/// App version +immutable string DDHX_VERSION = "0.5.0"; +/// Version line +immutable string DDHX_ABOUT = "ddhx "~DDHX_VERSION~" (built: "~__TIMESTAMP__~")"; immutable string SECRET = q"SECRET - +---------------------------------+ - __ | Heard you need help editing | - / \ | data, can I help you with that? | - - - | [ Yes ] [ No ] [ Go away ] | - O O +-. .-----------------------------+ + +----------------------------+ + __ | Heard you need help. | + / \ | Can I help you with that? | + _ _ | [ Yes ] [ No ] [ Go away ] | + O O +-. .------------------------+ || |/ |/ | V | \_/ SECRET"; -// CLI command options +static immutable string COMPILER_VERSION = + format("%d.%03d", version_major, version_minor); +static immutable string PAGE_VERSION = + DDHX_ABOUT~"\n"~ + DDHX_COPYRIGHT~"\n"~ + "License: MIT \n"~ + "Homepage: \n"~ + "Compiler: "~__VENDOR__~" "~COMPILER_VERSION; -immutable string OPT_COLUMNS = "c|"~COMMAND_COLUMNS; -immutable string OPT_INSERT = COMMAND_INSERT; -immutable string OPT_OVERWRITE = COMMAND_OVERWRITE; -immutable string OPT_READONLY = "R|"~COMMAND_READONLY; -immutable string OPT_VIEW = COMMAND_VIEW; -immutable string OPT_SI = COMMAND_SI; -immutable string OPT_IEC = COMMAND_IEC; -immutable string OPT_OFFSET = "o|"~COMMAND_OFFSET; -immutable string OPT_DATA = "d|"~COMMAND_DATA; -immutable string OPT_FILLER = "F|"~COMMAND_FILLER; -immutable string OPT_CHARSET = "C|"~COMMAND_CHARSET; - -// CLI common options - -immutable string OPT_VERSION = "version"; -immutable string OPT_VER = "ver"; -immutable string OPT_SECRET = "assistant"; - -bool askingHelp(string v) { return v == "help"; } - -void cliList(string opt) +void cliPage(string key) { - writeln("Available values for ",opt,":"); - import std.traits : EnumMembers; - switch (opt) { - case OPT_OFFSET, OPT_DATA: - foreach (m; EnumMembers!NumberType) - writeln("\t=", m); - break; - case OPT_CHARSET: - foreach (m; EnumMembers!CharacterSet) - writeln("\t=", m); - break; - default: + final switch (key) { + case "ver": key = DDHX_VERSION; break; + case "version": key = PAGE_VERSION; break; + case "assistant": key = SECRET; break; } + writeln(key); exit(0); } -void cliOption(string opt, string val) +string argstdin(string filename) { - final switch (opt) { - case OPT_INSERT: - editor.editMode = EditMode.insert; - return; - case OPT_OVERWRITE: - editor.editMode = EditMode.overwrite; - return; - case OPT_READONLY: - editor.editMode = EditMode.readOnly; - return; - case OPT_VIEW: - editor.editMode = EditMode.view; - return; - case OPT_COLUMNS: - if (settingsColumns(val)) - break; - return; - case OPT_OFFSET: - if (askingHelp(val)) - cliList(opt); - if (settingsOffset(val)) - break; - return; - case OPT_DATA: - if (askingHelp(val)) - cliList(opt); - if (settingsData(val)) - break; - return; - case OPT_FILLER: - if (settingsFiller(val)) - break; - return; - case OPT_CHARSET: - if (askingHelp(val)) - cliList(opt); - if (settingsCharset(val)) - break; - return; - } - errorPrint(1, "Invalid value for %s: %s", opt, val); - exit(1); + return filename == "-" ? null : filename; +} +long lparse(string v) +{ + if (v[0] != '+') + throw new Exception(text("Missing '+' prefix to argument: ", v)); + + return cparse(v[1..$]); } -void page(string opt) -{ - import std.compiler : version_major, version_minor; - static immutable string COMPILER_VERSION = format("%d.%03d", version_major, version_minor); - static immutable string VERSTR = - DDHX_ABOUT~"\n"~ - DDHX_COPYRIGHT~"\n"~ - "License: MIT \n"~ - "Homepage: \n"~ - "Compiler: "~__VENDOR__~" "~COMPILER_VERSION; - final switch (opt) { - case OPT_VERSION: opt = VERSTR; break; - case OPT_VER: opt = DDHX_VERSION; break; - case OPT_SECRET: opt = SECRET; break; - } - writeln(opt); - exit(0); -} +debug enum TRACE = true; +else enum TRACE = false; int main(string[] args) { - bool cliMmfile, cliFile, cliDump, cliStdin; - bool cliNoRC; - string cliSeek, cliLength, cliRC, cliReverse; + bool otrace = TRACE; + bool odump; + bool oreadonly; + int ofmtdata = Format.hex; + int ofmtaddr = Format.hex; + int ocolumns = 16; + int ocharset = CharacterSet.ascii; + long oskip; + long olength; + GetoptResult res = void; - try { - //TODO: Change &cliOption to {} + try + { res = args.getopt(config.caseSensitive, - OPT_COLUMNS, "Set column size (automatic='a', default=16)", &cliOption, - OPT_OFFSET, "Set offset mode (decimal, hex, or octal)", &cliOption, - OPT_DATA, "Set binary mode (decimal, hex, or octal)", &cliOption, - OPT_FILLER, "Set non-printable default character (default='.')", &cliOption, - OPT_CHARSET, "Set character translation (default=ascii)", &cliOption, - OPT_INSERT, "Open file in insert editing mode", &cliOption, - OPT_OVERWRITE, "Open file in overwrite editing mode", &cliOption, - OPT_READONLY, "Open file in read-only editing mode", &cliOption, - OPT_VIEW, "Open file in view editing mode", &cliOption, - OPT_SI, "Use SI binary suffixes instead of IEC", &setting.si, - "m|mmfile", "Open file as mmfile (memory-mapped)", &cliMmfile, - "f|file", "Force opening file as regular", &cliFile, - "stdin", "Open stdin instead of file", &cliStdin, - "s|seek", "Seek at position", &cliSeek, - "D|dump", "Dump file non-interactive onto screen", &cliDump, - "l|length", "Dump: Length of data to read", &cliLength, - "I|norc", "Use detaults and ignore user configuration files", &cliNoRC, - "rc", "Use supplied RC file", &cliRC, - "r|reverse", "Reverse operation: From hex, output binary to this file", &cliReverse, - OPT_VERSION, "Print the version screen and exit", &page, - OPT_VER, "Print only the version and exit", &page, - OPT_SECRET, "", &page + "assistant", "", &cliPage, + // Editor option + "c|columns", "Set column length (default=16, auto=0)", &ocolumns, + "o|offset", "Set offset mode ('hex', 'dec', or 'oct')", + (string _, string fmt) { + ofmtaddr = selectFormat(fmt); + }, + "data", "Set data mode ('hex', 'dec', or 'oct')", + (string _, string fmt) { + ofmtdata = selectFormat(fmt); + }, + //"filler", "Set non-printable default character (default='.')", &cliOption, + "C|charset", "Set character translation (default=ascii)", + (string _, string charset) { + switch (charset) with (CharacterSet) + { + case "cp437": ocharset = cp437; break; + case "ebcdic": ocharset = ebcdic; break; + case "mac": ocharset = mac; break; + case "ascii": ocharset = ascii; break; + default: + throw new Exception(text("Invalid charset: ", charset)); + } + }, + "r|readonly", "Open file in read-only editing mode", &oreadonly, + // Editor input mode + // Application options + "dump", "Dump file non-interactively", &odump, + "s|seek", "Seek at position", + (string _, string len) { oskip = cparse(len); }, + "l|length", "Maximum amount of data to read", + (string _, string len) { olength = cparse(len); }, + //"I|norc", "Use defaults and ignore user configuration files", &cliNoRC, + //"rc", "Use supplied RC file", &cliRC, + // Pages + "version", "Print the version screen and exit", &cliPage, + "ver", "Print only the version and exit", &cliPage, + "trace", "Enable tracing. Used in debugging", &otrace, ); } catch (Exception ex) { - return errorPrint(1, ex.msg); + stderr.writeln("error: ", ex.msg); + return 1; } if (res.helpWanted) { // Replace default help line res.options[$-1].help = "Print this help screen and exit"; - writeln("ddhx - Interactive hexadecimal file viewer\n"~ - " Usage: ddhx [OPTIONS] [FILE|--stdin]\n"~ + + writeln("ddhx - Hex editor\n"~ + "USAGE\n"~ + " ddhx [OPTIONS] [+POSITION] [+LENGTH] [FILE|-]\n"~ + " ddhx {-h|--help|--version|--ver]\n"~ "\n"~ "OPTIONS"); - foreach (opt; res.options) with (opt) + foreach (opt; res.options[1..$]) with (opt) { - if (help == "") continue; if (optShort) - writefln("%s, %-14s %s", optShort, optLong, help); + write(' ', optShort, ','); else - writefln(" %-14s %s", optLong, help); + write(" "); + writefln(" %-14s %s", optLong, help); } + return 0; } - version (Trace) - traceInit(DDHX_ABOUT); - - long skip, length; - - // Convert skip value - if (cliSeek) + // Positional arguments + string filename = void; + try switch (args.length) { - version (Trace) trace("seek=%s", cliSeek); - - if (convertToVal(skip, cliSeek)) - return errorPrint(); - - if (skip < 0) - return errorPrint(1, "Skip value must be positive"); + case 1: // Missing filename, implicit stdin + filename = null; + break; + case 2: // Has filename or - + filename = argstdin(args[1]); + break; + case 3: // Has position and filename + oskip = lparse(args[1]); + filename = argstdin(args[2]); + break; + case 4: // Has position, length, and filename + oskip = lparse(args[1]); + olength = lparse(args[2]); + filename = argstdin(args[3]); + break; + default: + throw new Exception("Too many arguments"); + } + catch (Exception ex) + { + stderr.writeln("error: ", ex.msg); + return 2; } - // Open file or stream - //TODO: Open MemoryStream - if ((args.length <= 1 || cliStdin) && editor.openStream(stdin)) - return errorPrint; - else if (cliFile ? false : cliMmfile && editor.openMmfile(args[1])) - return errorPrint; - else if (args.length > 1 && editor.openFile(args[1])) - return errorPrint; - - // Parse settings - if (cliNoRC == false && loadSettings(cliRC)) - return errorPrint; + if (otrace) traceInit(); + trace("version=%s args=%u", DDHX_VERSION, args.length); // App: dump - if (cliDump) + if (odump) { - if (cliLength && convertToVal(length, cliLength)) - return errorPrint; - return dump.start(skip, length); - } - - // App: reverse - if (cliReverse) - { - return reverser.start(cliReverse); + //TODO: If args.length < 2 -> open stream + return dump(filename, ocolumns, oskip, olength, ocharset); } // App: interactive - return ddhx.start(skip); + return ddhx_start(filename, oreadonly, oskip, olength, ocolumns, ocharset); } \ No newline at end of file diff --git a/src/os/file.d b/src/os/file.d index c2ed9b1..962d74a 100644 --- a/src/os/file.d +++ b/src/os/file.d @@ -83,9 +83,9 @@ /// File seek origin. enum Seek { - start = SEEK_SET, /// Seek since start of file. - current = SEEK_CUR, /// Seek since current position in file. - end = SEEK_END /// Seek since end of file. + start = SEEK_SET, /// Seek since start of file. + current = SEEK_CUR, /// Seek since current position in file. + end = SEEK_END, /// Seek since end of file. } /// Open file flags @@ -102,7 +102,8 @@ // useful when writing all changes to file // Win32: Seek + SetEndOfFile // others: ftruncate -struct OSFile { +struct OSFile +{ private OSHANDLE handle; bool eof, err; @@ -130,11 +131,9 @@ { version (Windows) { - uint dwAccess; - uint dwCreation; + uint dwCreation = flags & OFlags.exists ? OPEN_EXISTING : OPEN_ALWAYS; - if (flags & OFlags.exists) dwCreation |= OPEN_EXISTING; - else dwCreation |= OPEN_ALWAYS; + uint dwAccess; if (flags & OFlags.read) dwAccess |= GENERIC_READ; if (flags & OFlags.write) dwAccess |= GENERIC_WRITE; @@ -156,7 +155,7 @@ else version (Posix) { int oflags; - if (!(flags & OFlags.exists)) oflags |= O_CREAT; + if ((flags & OFlags.exists) == 0) oflags |= O_CREAT; if ((flags & OFlags.readWrite) == OFlags.readWrite) oflags |= O_RDWR; else if (flags & OFlags.write) diff --git a/src/os/mmfile.d b/src/os/mmfile.d deleted file mode 100644 index 5c4c1c6..0000000 --- a/src/os/mmfile.d +++ /dev/null @@ -1,685 +0,0 @@ -/// A re-imagination and updated version of std.mmfile that features -/// some minor enhancements, such as APIs similar to File. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module os.mmfile; - -import std.stdio : File; -import std.mmfile : MmFile; - -enum MMFlags -{ - exists = 1, /// File must exist. - read = 1 << 1, /// Read access. - write = 1 << 2, /// Write access. -} - -// temp -public class OSMmFile : MmFile -{ - private void *address; - - this(string path, int flags) - { - super(path, - flags & MMFlags.read ? MmFile.Mode.read : - flags & MMFlags.exists ? - MmFile.Mode.readWrite : MmFile.Mode.readWriteNew, - 0, - address); - } - - bool eof, err; - private long position; - long seek(long pos) // only do seek_set for now - { - return position = pos; - } - long tell() - { - return position; - } - ubyte[] read(size_t size) - { - long sz = cast(long)this.length; - long p2 = position+size; - eof = p2 > sz; - if (eof) p2 = sz; - return cast(ubyte[])this[position..p2]; - } - /*void seek(long pos) - { - final switch (origin) with (Seek) - { - case start: - position = pos; - return 0; - case current: - position += pos; - return 0; - case end: - position = size - pos; - return 0; - } - }*/ -} - -/// The mode the memory mapped file is opened with. -/+enum MmFileMode { - read, /// Read existing file - readWriteNew, /// Delete existing file, write new file (overwrite) - readWrite, /// Read/Write existing file, create if not existing - readCopyOnWrite, /// Read/Write existing file, copy on write -} - -/// Memory-mapped file. -struct OSMmFile2 { - /// Open a memory-mapped file. - /// Params: - /// filename = File path. - /// mode = mmfile operating mode. - /// size = - this(string filename, MmFileMode mode = Mode.read, ulong size = 0, - void* address = null, size_t window = 0) -{ - - version (Windows) -{ - } else version (linux) -{ - int oflag; - int fmode; - - final switch (mode) with (MmFileMode) -{ - case read: - flags = MAP_SHARED; - prot = PROT_READ; - oflag = O_RDONLY; - fmode = 0; - break; - case Mode.readWriteNew: - assert(size != 0); - flags = MAP_SHARED; - prot = PROT_READ | PROT_WRITE; - oflag = O_CREAT | O_RDWR | O_TRUNC; - fmode = octal!660; - break; - case Mode.readWrite: - flags = MAP_SHARED; - prot = PROT_READ | PROT_WRITE; - oflag = O_CREAT | O_RDWR; - fmode = octal!660; - break; - case Mode.readCopyOnWrite: - flags = MAP_PRIVATE; - prot = PROT_READ | PROT_WRITE; - oflag = O_RDWR; - fmode = 0; - break; - } - - fd = fildes; - - // Adjust size - stat_t statbuf = void; - errnoEnforce(fstat(fd, &statbuf) == 0); - if (prot & PROT_WRITE && size > statbuf.st_size) - { - // Need to make the file size bytes big - lseek(fd, cast(off_t)(size - 1), SEEK_SET); - char c = 0; - core.sys.posix.unistd.write(fd, &c, 1); - } - else if (prot & PROT_READ && size == 0) - size = statbuf.st_size; - this.size = size; - - // Map the file into memory! - size_t initial_map = (window && 2*window statbuf.st_size) - { - // Need to make the file size bytes big - lseek(fd, cast(off_t)(size - 1), SEEK_SET); - char c = 0; - core.sys.posix.unistd.write(fd, &c, 1); - } - else if (prot & PROT_READ && size == 0) - size = statbuf.st_size; - this.size = size; - - // Map the file into memory! - size_t initial_map = (window && 2*window> 32); - hFileMap = CreateFileMappingW(hFile, null, flProtect, - hi, cast(uint) size, null); - wenforce(hFileMap, "CreateFileMapping"); - scope(failure) - { - CloseHandle(hFileMap); - hFileMap = null; - } - - if (size == 0 && filename != null) - { - uint sizehi; - uint sizelow = GetFileSize(hFile, &sizehi); - wenforce(sizelow != INVALID_FILE_SIZE || GetLastError() != ERROR_SUCCESS, - "GetFileSize"); - size = (cast(ulong) sizehi << 32) + sizelow; - } - this.size = size; - - size_t initial_map = (window && 2*window statbuf.st_size) - { - // Need to make the file size bytes big - .lseek(fd, cast(off_t)(size - 1), SEEK_SET); - char c = 0; - core.sys.posix.unistd.write(fd, &c, 1); - } - else if (prot & PROT_READ && size == 0) - size = statbuf.st_size; - } - else - { - fd = -1; - flags |= MAP_ANON; - } - this.size = size; - size_t initial_map = (window && 2*window= start && i < start+data.length; - } - - // unmap the current range - private void unmap() - { - debug (MMFILE) printf("MmFile.unmap()\n"); - version (Windows) - { - wenforce(!data.ptr || UnmapViewOfFile(data.ptr) != FALSE, "UnmapViewOfFile"); - } - else - { - errnoEnforce(!data.ptr || munmap(cast(void*) data, data.length) == 0, - "munmap failed"); - } - data = null; - } - - // map range - private void map(ulong start, size_t len) - { - debug (MMFILE) printf("MmFile.map(%lld, %d)\n", start, len); - void* p; - if (start+len > size) - len = cast(size_t)(size-start); - version (Windows) - { - uint hi = cast(uint)(start >> 32); - p = MapViewOfFileEx(hFileMap, dwDesiredAccess, hi, cast(uint) start, len, address); - wenforce(p, "MapViewOfFileEx"); - } - else - { - p = mmap(address, len, prot, flags, fd, cast(off_t) start); - errnoEnforce(p != MAP_FAILED); - } - data = p[0 .. len]; - this.start = start; - } - - // ensure a given position is mapped - private void ensureMapped(ulong i) - { - debug (MMFILE) printf("MmFile.ensureMapped(%lld)\n", i); - if (!mapped(i)) - { - unmap(); - if (window == 0) - { - map(0,cast(size_t) size); - } - else - { - ulong block = i/window; - if (block == 0) - map(0,2*window); - else - map(window*(block-1),3*window); - } - } - } - - // ensure a given range is mapped - private void ensureMapped(ulong i, ulong j) - { - debug (MMFILE) printf("MmFile.ensureMapped(%lld, %lld)\n", i, j); - if (!mapped(i) || !mapped(j-1)) - { - unmap(); - if (window == 0) - { - map(0,cast(size_t) size); - } - else - { - ulong iblock = i/window; - ulong jblock = (j-1)/window; - if (iblock == 0) - { - map(0,cast(size_t)(window*(jblock+2))); - } - else - { - map(window*(iblock-1),cast(size_t)(window*(jblock-iblock+3))); - } - } - } - } - -private: - string filename; - void[] data; - ulong start; - size_t window; - ulong size; - Mode mMode; - void* address; - version (linux) File file; - - version (Windows) - { - HANDLE hFile = INVALID_HANDLE_VALUE; - HANDLE hFileMap = null; - uint dwDesiredAccess; - } - else version (Posix) - { - int fd; - int prot; - int flags; - int fmode; - } else { - static assert(0); - } -}+/ \ No newline at end of file diff --git a/src/os/path.d b/src/os/path.d index a819899..934c02d 100644 --- a/src/os/path.d +++ b/src/os/path.d @@ -37,7 +37,13 @@ { version (Windows) { - // 1. SHGetFolderPath + // 1. %USERPROFILE% + if ("USERPROFILE" in environment) + return environment["USERPROFILE"]; + // 2. %HOMEDRIVE% and %HOMEPATH% + if ("HOMEDRIVE" in environment && "HOMEPATH" in environment) + return environment["HOMEDRIVE"] ~ environment["HOMEPATH"]; + // 3. SHGetFolderPath wchar *buffer = cast(wchar*)malloc(1024); if (SHGetFolderPathW(null, CSIDL_PROFILE, null, 0, buffer) == S_OK) { @@ -47,12 +53,6 @@ return path; } free(buffer); - // 2. %USERPROFILE% - if ("USERPROFILE" in environment) - return environment["USERPROFILE"]; - // 3. %HOMEDRIVE% and %HOMEPATH% - if ("HOMEDRIVE" in environment && "HOMEPATH" in environment) - return environment["HOMEDRIVE"] ~ environment["HOMEPATH"]; } else version (Posix) { @@ -77,16 +77,19 @@ /// Get the path to the current user data folder. /// This does not verify if the path exists. -/// Windows: Typically C:\\Users\\%USERNAME%\\AppData\\Roaming +/// Windows: Typically C:\\Users\\%USERNAME%\\AppData\\Local /// Posix: Typically /home/$USERNAME/.config /// Returns: Path or null on failure. -string getUserDataFolder() +string getUserConfigFolder() { version (Windows) { - // 1. SHGetFolderPath + // 1. %LOCALAPPDATA% + if ("LOCALAPPDATA" in environment) + return environment["LOCALAPPDATA"]; + // 2. SHGetFolderPath wchar *buffer = cast(wchar*)malloc(1024); - if (SHGetFolderPathW(null, CSIDL_APPDATA, null, 0, buffer) == S_OK) + if (SHGetFolderPathW(null, CSIDL_LOCAL_APPDATA, null, 0, buffer) == S_OK) { string path; transcode(buffer[0..wcslen(buffer)], path); @@ -94,15 +97,11 @@ return path; } free(buffer); - // 2. %APPDATA% - // Is is the exact same with CSIDL_APPDATA but anything can go wrong. - if ("APPDATA" in environment) - return environment["APPDATA"]; } else version (Posix) { - if ("XDG_CONFIG_HOME" in environment) - return environment["XDG_CONFIG_HOME"]; + if (const(string) *xdg_config_home = "XDG_CONFIG_HOME" in environment) + return *xdg_config_home; } // Fallback @@ -148,7 +147,7 @@ /// Returns: Path or null on failure. string buildUserAppFile(string appname, string filename) { - string base = getUserDataFolder; + string base = getUserConfigFolder; if (base is null) return null; diff --git a/src/os/terminal.d b/src/os/terminal.d index 02e1084..2648fca 100644 --- a/src/os/terminal.d +++ b/src/os/terminal.d @@ -7,44 +7,43 @@ //TODO: readline // automatically pause input, stdio.readln, resume input //TODO: Switch key input depending on $TERM -// xterm, xterm-color, xterm-256color, linux, vt100 -//TODO: Invert color support +// xterm (xterm, xterm-color, xterm-256color), linux, vt100 +//TODO: BetterC coverage (using D_BetterC version) // NOTE: Useful links for escape codes // https://man7.org/linux/man-pages/man0/termios.h.0p.html // https://man7.org/linux/man-pages/man3/tcsetattr.3.html // https://man7.org/linux/man-pages/man4/console_codes.4.html -/// -private extern (C) int putchar(int); - private import std.stdio : _IONBF, _IOLBF, _IOFBF, stdin, stdout; -version (TestInput) private import std.stdio : printf; private import core.stdc.stdlib : system, atexit; +version (unittest) +{ + private import core.stdc.stdio : printf; + private extern (C) int putchar(int); +} version (Windows) { - //TODO: To reduce output binary, import only modules necessary - private import core.sys.windows.windows; - private import std.windows.syserror : WindowsException; - private enum ALT_PRESSED = RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED; - private enum CTRL_PRESSED = RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED; - private enum DEFAULT_COLOR = - FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; + import core.sys.windows.winbase; + import core.sys.windows.wincon; + import core.sys.windows.windef; // HANDLE, USHORT, DWORD + import std.windows.syserror : WindowsException; private enum CP_UTF8 = 65_001; private __gshared HANDLE hIn, hOut; - private __gshared USHORT defaultColor = DEFAULT_COLOR; private __gshared DWORD oldCP; private __gshared ushort oldAttr; } else version (Posix) { - private import core.stdc.stdio : snprintf; - private import core.sys.posix.sys.stat; - private import core.sys.posix.sys.ioctl; - private import core.sys.posix.unistd; - private import core.sys.posix.termios; - private import core.sys.posix.signal; + import core.stdc.stdio : snprintf; + import core.sys.posix.sys.stat; + import core.sys.posix.sys.ioctl; + import core.sys.posix.unistd; + import core.sys.posix.termios; + import core.sys.posix.signal; + import core.sys.posix.unistd : write, STDOUT_FILENO; + import core.sys.posix.sys.types : ssize_t; private enum NULL_SIGACTION = cast(sigaction_t*)0; private enum SIGWINCH = 28; @@ -107,67 +106,67 @@ int value; } immutable KeyInfo[] keyInputsVTE = [ - // text Key value - { "\033[A", Key.UpArrow }, - { "\033[1;2A", Key.UpArrow | Mod.shift }, - { "\033[1;3A", Key.UpArrow | Mod.alt }, - { "\033[1;5A", Key.UpArrow | Mod.ctrl }, - { "\033[A:4A", Key.UpArrow | Mod.shift | Mod.alt }, - { "\033[B", Key.DownArrow }, - { "\033[1;2B", Key.DownArrow | Mod.shift }, - { "\033[1;3B", Key.DownArrow | Mod.alt }, - { "\033[1;5B", Key.DownArrow | Mod.ctrl }, - { "\033[A:4B", Key.DownArrow | Mod.shift | Mod.alt }, - { "\033[C", Key.RightArrow }, - { "\033[1;2C", Key.RightArrow | Mod.shift }, - { "\033[1;3C", Key.RightArrow | Mod.alt }, - { "\033[1;5C", Key.RightArrow | Mod.ctrl }, - { "\033[A:4C", Key.RightArrow | Mod.shift | Mod.alt }, - { "\033[D", Key.LeftArrow }, - { "\033[1;2D", Key.LeftArrow | Mod.shift }, - { "\033[1;3D", Key.LeftArrow | Mod.alt }, - { "\033[1;5D", Key.LeftArrow | Mod.ctrl }, - { "\033[A:4D", Key.LeftArrow | Mod.shift | Mod.alt }, + // text Key value + { "\033[A", Key.UpArrow }, + { "\033[1;2A", Key.UpArrow | Mod.shift }, + { "\033[1;3A", Key.UpArrow | Mod.alt }, + { "\033[1;5A", Key.UpArrow | Mod.ctrl }, + { "\033[A:4A", Key.UpArrow | Mod.shift | Mod.alt }, + { "\033[B", Key.DownArrow }, + { "\033[1;2B", Key.DownArrow | Mod.shift }, + { "\033[1;3B", Key.DownArrow | Mod.alt }, + { "\033[1;5B", Key.DownArrow | Mod.ctrl }, + { "\033[A:4B", Key.DownArrow | Mod.shift | Mod.alt }, + { "\033[C", Key.RightArrow }, + { "\033[1;2C", Key.RightArrow | Mod.shift }, + { "\033[1;3C", Key.RightArrow | Mod.alt }, + { "\033[1;5C", Key.RightArrow | Mod.ctrl }, + { "\033[A:4C", Key.RightArrow | Mod.shift | Mod.alt }, + { "\033[D", Key.LeftArrow }, + { "\033[1;2D", Key.LeftArrow | Mod.shift }, + { "\033[1;3D", Key.LeftArrow | Mod.alt }, + { "\033[1;5D", Key.LeftArrow | Mod.ctrl }, + { "\033[A:4D", Key.LeftArrow | Mod.shift | Mod.alt }, { "\033[2~", Key.Insert }, - { "\033[2;3~", Key.Insert | Mod.alt }, + { "\033[2;3~", Key.Insert | Mod.alt }, { "\033[3~", Key.Delete }, - { "\033[3;5~", Key.Delete | Mod.ctrl }, - { "\033[H", Key.Home }, - { "\033[1;3H", Key.Home | Mod.alt }, - { "\033[1;5H", Key.Home | Mod.ctrl }, - { "\033[F", Key.End }, - { "\033[1;3F", Key.End | Mod.alt }, - { "\033[1;5F", Key.End | Mod.ctrl }, + { "\033[3;5~", Key.Delete | Mod.ctrl }, + { "\033[H", Key.Home }, + { "\033[1;3H", Key.Home | Mod.alt }, + { "\033[1;5H", Key.Home | Mod.ctrl }, + { "\033[F", Key.End }, + { "\033[1;3F", Key.End | Mod.alt }, + { "\033[1;5F", Key.End | Mod.ctrl }, { "\033[5~", Key.PageUp }, - { "\033[5;5~", Key.PageUp | Mod.ctrl }, + { "\033[5;5~", Key.PageUp | Mod.ctrl }, { "\033[6~", Key.PageDown }, - { "\033[6;5~", Key.PageDown | Mod.ctrl }, - { "\033OP", Key.F1 }, - { "\033[1;2P", Key.F1 | Mod.shift, }, - { "\033[1;3R", Key.F1 | Mod.alt, }, - { "\033[1;5P", Key.F1 | Mod.ctrl, }, - { "\033OQ", Key.F2 }, - { "\033[1;2Q", Key.F2 | Mod.shift }, - { "\033OR", Key.F3 }, - { "\033[1;2R", Key.F3 | Mod.shift }, - { "\033OS", Key.F4 }, - { "\033[1;2S", Key.F4 | Mod.shift }, - { "\033[15~", Key.F5 }, - { "\033[15;2~", Key.F5 | Mod.shift }, - { "\033[17~", Key.F6 }, - { "\033[17;2~", Key.F6 | Mod.shift }, - { "\033[18~", Key.F7 }, - { "\033[18;2~", Key.F7 | Mod.shift }, - { "\033[19~", Key.F8 }, - { "\033[19;2~", Key.F8 | Mod.shift }, - { "\033[20~", Key.F9 }, - { "\033[20;2~", Key.F9 | Mod.shift }, - { "\033[21~", Key.F10 }, - { "\033[21;2~", Key.F10 | Mod.shift }, - { "\033[23~", Key.F11 }, - { "\033[23;2~", Key.F11 | Mod.shift}, - { "\033[24~", Key.F12 }, - { "\033[24;2~", Key.F12 | Mod.shift }, + { "\033[6;5~", Key.PageDown | Mod.ctrl }, + { "\033OP", Key.F1 }, + { "\033[1;2P", Key.F1 | Mod.shift, }, + { "\033[1;3R", Key.F1 | Mod.alt, }, + { "\033[1;5P", Key.F1 | Mod.ctrl, }, + { "\033OQ", Key.F2 }, + { "\033[1;2Q", Key.F2 | Mod.shift }, + { "\033OR", Key.F3 }, + { "\033[1;2R", Key.F3 | Mod.shift }, + { "\033OS", Key.F4 }, + { "\033[1;2S", Key.F4 | Mod.shift }, + { "\033[15~", Key.F5 }, + { "\033[15;2~", Key.F5 | Mod.shift }, + { "\033[17~", Key.F6 }, + { "\033[17;2~", Key.F6 | Mod.shift }, + { "\033[18~", Key.F7 }, + { "\033[18;2~", Key.F7 | Mod.shift }, + { "\033[19~", Key.F8 }, + { "\033[19;2~", Key.F8 | Mod.shift }, + { "\033[20~", Key.F9 }, + { "\033[20;2~", Key.F9 | Mod.shift }, + { "\033[21~", Key.F10 }, + { "\033[21;2~", Key.F10 | Mod.shift }, + { "\033[23~", Key.F11 }, + { "\033[23;2~", Key.F11 | Mod.shift}, + { "\033[24~", Key.F12 }, + { "\033[24;2~", Key.F12 | Mod.shift }, ]; private __gshared termios old_ios, new_ios; @@ -177,23 +176,24 @@ //TODO: captureCtrlC: Block CTRL+C enum TermFeat : ushort { /// Initiate only the basic. - none = 0, + none = 0, /// Initiate the input system. inputSys = 1, /// Initiate the alternative screen buffer. - altScreen = 1 << 1, + altScreen = 1 << 1, /// Initiate everything. - all = 0xffff, + all = 0xffff, } -private __gshared TermFeat current_features; +private __gshared int current_features; /// Initiate terminal. /// Params: features = Feature bits to initiate. /// Throws: (Windows) WindowsException on OS exception -void terminalInit(TermFeat features) +void terminalInit(int features) { current_features = features; + version (Windows) { CONSOLE_SCREEN_BUFFER_INFO csbi = void; @@ -252,7 +252,9 @@ if (SetConsoleActiveScreenBuffer(hOut) == FALSE) throw new WindowsException(GetLastError); - } else { + } + else + { hOut = GetStdHandle(STD_OUTPUT_HANDLE); } @@ -322,21 +324,21 @@ private extern (C) void terminalQuit() { - terminalRestore; + terminalRestore(); } /// Restore CP and other settings void terminalRestore() { version (Windows) -{ + { SetConsoleOutputCP(oldCP); // unconditionally } else version (Posix) { // restore main screen buffer if (current_features & TermFeat.altScreen) - terminalOutput2("\033[?1049l"); + terminalOut("\033[?1049l"); terminalShowCursor; } if (current_features & TermFeat.inputSys) @@ -393,14 +395,15 @@ FillConsoleOutputAttribute(hOut, csbi.wAttributes, size, c, &num) == 0*/) { terminalPos(0, 0); - } else // If that fails, run cls. + } + else // If that fails, run cls. system("cls"); } else version (Posix) { // \033c is a Reset // \033[2J is "Erase whole display" - terminalOutput2("\033[2J"); + terminalOut("\033[2J"); } else static assert(0, "Clear: Not implemented"); } @@ -413,8 +416,8 @@ { CONSOLE_SCREEN_BUFFER_INFO c = void; GetConsoleScreenBufferInfo(hOut, &c); - size.height = c.srWindow.Bottom - c.srWindow.Top + 1; - size.width = c.srWindow.Right - c.srWindow.Left + 1; + size.rows = c.srWindow.Bottom - c.srWindow.Top + 1; + size.columns = c.srWindow.Right - c.srWindow.Left + 1; } else version (Posix) { @@ -424,8 +427,8 @@ // Reply: ESC [ 8 ; ROWS ; COLUMNS t winsize ws = void; ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); - size.height = ws.ws_row; - size.width = ws.ws_col; + size.rows = ws.ws_row; + size.columns = ws.ws_col; } else static assert(0, "terminalSize: Not implemented"); return size; } @@ -449,7 +452,7 @@ char[16] b = void; int r = snprintf(b.ptr, 16, "\033[%d;%dH", ++y, ++x); assert(r > 0); - terminalOutput(b.ptr, r); + terminalOut(b.ptr, r); } } @@ -465,7 +468,7 @@ } else version (Posix) { - terminalOutput2("\033[?25l"); + terminalOut("\033[?25l"); } } /// Show the terminal cursor. @@ -480,7 +483,7 @@ } else version (Posix) { - terminalOutput2("\033[?25h"); + terminalOut("\033[?25h"); } } @@ -492,7 +495,7 @@ } else version (Posix) { - terminalOutput2("\033[41m"); + terminalOut("\033[41m"); } } /// Invert color. @@ -504,7 +507,7 @@ } else version (Posix) { - terminalOutput2("\033[7m"); + terminalOut("\033[7m"); } } /// Underline. @@ -517,7 +520,7 @@ } else version (Posix) { - terminalOutput2("\033[4m"); + terminalOut("\033[4m"); } } /// Reset color. @@ -529,13 +532,13 @@ } else version (Posix) { - terminalOutput2("\033[0m"); + terminalWrite("\033[0m"); } } -size_t terminalOutput2(const(void)[] data) +size_t terminalWrite(const(void)[] data) { - return terminalOutput(data.ptr, data.length); + return terminalWrite(data.ptr, data.length); } /// Directly write to output. @@ -543,49 +546,56 @@ /// data = Character data. /// size = Amount in bytes. /// Returns: Number of bytes written. -size_t terminalOutput(const(void) *data, size_t size) +size_t terminalWrite(const(void) *data, size_t size) { version (Windows) { - import core.sys.windows.winbase : STD_OUTPUT_HANDLE, - WriteFile, GetStdHandle; uint r = void; assert(WriteFile(hOut, data, cast(uint)size, &r, null)); return r; } else version (Posix) { - import core.sys.posix.unistd : write, STDOUT_FILENO; - import core.sys.posix.sys.types : ssize_t; ssize_t r = write(STDOUT_FILENO, data, size); assert(r >= 0); return r; } } -/// Read an input event. This function is blocking. -/// Params: -/// event = TerminalInfo struct -/// Throws: (Windows) WindowsException on OS error -void terminalInput(ref TerminalInput event) +// Commented until terminalRead() improves +/*template CTRL(int c) { enum CTRL = c & 0x1f; } +unittest { + assert(CTRL!('a') == 0xa); +}*/ + +/// Read an input event. This function is blocking. +/// Throws: (Windows) WindowsException on OS error. +/// Returns: Terminal input. +TermInput terminalRead() +{ + TermInput event; + version (Windows) { + enum ALT_PRESSED = RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED; + enum CTRL_PRESSED = RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED; + INPUT_RECORD ir = void; DWORD num = void; -L_READ: +Lread: if (ReadConsoleInputA(hIn, &ir, 1, &num) == 0) throw new WindowsException(GetLastError); if (num == 0) - goto L_READ; + goto Lread; switch (ir.EventType) { case KEY_EVENT: if (ir.KeyEvent.bKeyDown == FALSE) - goto L_READ; + goto Lread; - version (TestInput) + version (unittest) { printf( "KeyEvent: AsciiChar=%d wVirtualKeyCode=%d dwControlKeyState=%x\n", @@ -599,7 +609,7 @@ // Filter out single modifier key events switch (keycode) { - case 16, 17, 18: goto L_READ; // shift,ctrl,alt + case 16, 17, 18: goto Lread; // shift,ctrl,alt default: } @@ -610,7 +620,7 @@ if (ascii >= 'a' && ascii <= 'z') { event.key = ascii - 32; - return; + return event; } else if (ascii >= 0x20 && ascii < 0x7f) { @@ -621,7 +631,7 @@ if (ascii >= 'A' && ascii <= 'Z') event.key = Mod.shift; - return; + return event; } event.key = keycode; @@ -629,7 +639,7 @@ if (state & ALT_PRESSED) event.key |= Mod.alt; if (state & CTRL_PRESSED) event.key |= Mod.ctrl; if (state & SHIFT_PRESSED) event.key |= Mod.shift; - return; + return event; /*case MOUSE_EVENT: if (ir.MouseEvent.dwEventFlags & MOUSE_WHEELED) { @@ -640,8 +650,8 @@ case WINDOW_BUFFER_SIZE_EVENT: if (terminalOnResizeEvent) terminalOnResizeEvent(); - goto L_READ; - default: goto L_READ; + goto Lread; + default: goto Lread; } } else version (Posix) @@ -684,31 +694,33 @@ enum BLEN = 8; char[BLEN] b = void; - L_READ: + Lread: ssize_t r = read(STDIN_FILENO, b.ptr, BLEN); event.type = InputType.keyDown; // Assuming for now event.key = 0; // clear as safety measure switch (r) { - case -1: assert(0, "read(2) failed"); + case -1: + assert(0, "read(2) failed"); + goto Lread; case 0: // How even - version (TestInput) printf("stdin: empty\n"); - goto L_READ; + version (unittest) printf("stdin: empty\n"); + goto Lread; case 1: char c = b[0]; - version (TestInput) printf("stdin: \\0%o (%d)\n", c, c); + version (unittest) printf("stdin: \\0%o (%d)\n", c, c); // Filtering here adjusts the value only if necessary. switch (c) { case 0: // Ctrl+Space event.key = Key.Spacebar | Mod.ctrl; - return; + return event; case 13: event.key = Key.Enter; - return; + return event; case 8, 127: // ^H event.key = Key.Backspace; - return; + return event; default: } if (c >= 'a' && c <= 'z') @@ -723,7 +735,7 @@ default: } - version (TestInput) + version (unittest) { printf("stdin:"); for (size_t i; i < r; ++i) @@ -750,31 +762,60 @@ if (r != ki.text.length) continue; if (inputString != ki.text) continue; event.key = ki.value; - return; + return event; } // Matched to nothing - goto L_READ; + goto Lread; } // version posix } + /// Terminal input type. -enum InputType { +enum InputType +{ keyDown, keyUp, mouseDown, mouseUp, } +//TODO: Redo input encoding +// 15: 0 key value (ascii or special key) +// 23:16 flags +// 0000 0000 +// |||| ||+-- Alt +// |||| |+--- Ctrl +// |||+------ Special key +// 31:24 input type +// 0 keydown +// 1 mousedown +// 128 keyup +// 129 mouseup + +/*private enum specialbit = 1 << 20; +enum Special +{ + rightArrow = 1 | specialbit, + leftArrow = 2 | specialbit, + upArrow = 3 | specialbit, + Arrow = 4 | specialbit, + + +}*/ + /// Key modifier -enum Mod { - ctrl = 1 << 16, - shift = 1 << 17, - alt = 1 << 18, +enum Mod +{ + ctrl = 1 << 24, + shift = 1 << 25, + alt = 1 << 26, } + /// Key codes map. //TODO: Consider mapping these to os/ascii-specific -enum Key { +enum Key +{ Undefined = 0, Backspace = 8, Tab = 9, @@ -925,10 +966,13 @@ } /// Terminal input structure -struct TerminalInput { - union { +struct TermInput +{ + union + { int key; /// Keyboard input with possible Mod flags. - struct { + struct + { ushort mouseX; /// Mouse column coord ushort mouseY; /// Mouse row coord } @@ -937,15 +981,10 @@ } /// Terminal size structure -struct TerminalSize { - union { - /// Terminal width in character columns - int width; - int columns; /// Ditto - } - union { - /// Terminal height in character rows - int height; - int rows; /// Ditto - } +struct TerminalSize +{ + /// Terminal width in character columns + int columns; + /// Terminal height in character rows + int rows; } diff --git a/src/reverser.d b/src/reverser.d deleted file mode 100644 index 8b6eecf..0000000 --- a/src/reverser.d +++ /dev/null @@ -1,79 +0,0 @@ -module reverser; - -import std.stdio; -import editor; -import os.file; -import error; - -int start(string outpath) -{ - // editor: hex input - // outfile: binary output - enum BUFSZ = 4096; - ubyte[BUFSZ] data = void; - - OSFile binfd = void; - if (binfd.open(outpath, OFlags.write)) - { - stderr.writefln("error: %s", systemMessage(binfd.syscode())); - return 2; - } - -L_READ: - ubyte[] r = editor.read(data); - - if (editor.err) - { - return 3; - } - - foreach (ubyte b; r) - { - if (b >= '0' && b <= '9') - { - outnibble(binfd, b - 0x30); - } - else if (b >= 'a' && b <= 'f') - { - outnibble(binfd, b - 0x57); - } - else if (b >= 'A' && b <= 'F') - { - outnibble(binfd, b - 0x37); - } - } - - if (editor.eof) - { - outfinish(binfd); - return 0; - } - - goto L_READ; -} - -private: - -__gshared bool low; -__gshared ubyte data; - -void outnibble(ref OSFile file, int nibble) -{ - if (low == false) - { - data = cast(ubyte)(nibble << 4); - low = true; - return; - } - - low = false; - ubyte b = cast(ubyte)(data | nibble); - file.write(&b, 1); -} - -void outfinish(ref OSFile file) -{ - if (low == false) return; - - file.write(&data, 1); -} \ No newline at end of file diff --git a/src/screen.d b/src/screen.d deleted file mode 100644 index d8edd8c..0000000 --- a/src/screen.d +++ /dev/null @@ -1,775 +0,0 @@ -/// Terminal screen handling. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module screen; - -import std.range : chunks; -import ddhx; // for setting, NumberType -import os.terminal, os.file; -import core.stdc.string : memset; -import core.stdc.stdlib : malloc, free; -import std.outbuffer : OutBuffer; - -//TODO: Consider renaming module to render -// And rename all functions to remove "render" prefix -//TODO: Data viewer groups -// hex: h8, h16, h32, h64 -// signed decimal: i8, i16, i32, i64 -// unsigned decimal: u8, u16, u32, u64 -// signed octal: oi8, oi16, oi32, oi64 -// unsigned octal: ou8, ou16, ou32, ou64 -// float: f32, f64 -//TODO: Group endianness (when >1) -// native (default), little, big -//TODO: View display mode (data+text, data, text) -// Currently very low priority -//TODO: Unaligned rendering. -// Rendering engine should be capable to take off whereever it stopped -// or be able to specify/toggle seperate regardless of column length. -// Probably useful for dump app. - -private -{ - /// Length of offset space taken on screen - enum OFFSET_SPACE = 11; // Not currently a setting, but should be -} - -private struct NumberFormatter -{ - string name; /// Short offset name - char fmtchar; /// Format character for printf-like functions - ubyte size; /// Size for formatted byte (excluding space) - size_t function(char*,long) offset; /// Function to format offset - size_t function(char*,ubyte) data; /// Function to format data -} - -//TODO: Replace with OffsetFormatter and BinaryFormatter structures -// XXXFormatter.select() - -private immutable NumberFormatter[3] formatters = [ - { "hex", 'x', 2, &format11x, &format02x }, - { "dec", 'u', 3, &format11d, &format03d }, - { "oct", 'o', 3, &format11o, &format03o }, -]; - -/// Last known terminal size. -__gshared TerminalSize termSize; -/// -__gshared uint maxLine = uint.max; -/// Offset formatter. -__gshared NumberFormatter offsetFormatter = formatters[0]; -/// Binary data formatter. -__gshared NumberFormatter binaryFormatter = formatters[0]; - -int initiate() -{ - terminalInit(TermFeat.all); - - updateTermSize; - - if (termSize.height < 3) - return errorSet(ErrorCode.screenMinimumRows); - if (termSize.width < 20) - return errorSet(ErrorCode.screenMinimumColumns); - - terminalHideCursor; - - return 0; -} - -void updateTermSize() -{ - termSize = terminalSize; - maxLine = termSize.height - 2; -} - -void onResize(void function() func) -{ - terminalOnResize(func); -} - -void setOffsetFormat(NumberType type) -{ - offsetFormatter = formatters[type]; -} -void setBinaryFormat(NumberType type) -{ - binaryFormatter = formatters[type]; -} - -/// Clear entire terminal screen -void clear() -{ - terminalClear; -} - -/*void clearStatusBar() -{ - screen.cwritefAt(0,0,"%*s", termSize.width - 1, " "); -}*/ - -/// Display a formatted message at the bottom of the screen. -/// Params: -/// fmt = Formatting message string. -/// args = Arguments. -void message(A...)(const(char)[] fmt, A args) -{ - //TODO: Consider using a scoped outbuffer + private message(outbuf) - import std.format : format; - message(format(fmt, args)); -} -/// Display a message at the bottom of the screen. -/// Params: str = Message. -void message(const(char)[] str) -{ - terminalPos(0, termSize.height - 1); - cwritef("%-*s", termSize.width - 1, str); -} - -string prompt(string prefix, string include) -{ - import std.stdio : readln; - import std.string : chomp; - - scope (exit) - { - cursorOffset; - renderOffset; - } - - clearOffsetBar; - - terminalPos(0, 0); - - cwrite(prefix); - if (include) cwrite(include); - - terminalShowCursor; - terminalPauseInput; - - string line = include ~ chomp(readln()); - - terminalHideCursor; - terminalResumeInput; - - return line; -} - -// -// SECTION Rendering -// - -//TODO: Move formatting stuff to module format. - -private immutable string hexMap = "0123456789abcdef"; - -private -size_t format02x(char *buffer, ubyte v) -{ - buffer[1] = hexMap[v & 15]; - buffer[0] = hexMap[v >> 4]; - return 2; -} -@system unittest { - char[2] c = void; - format02x(c.ptr, 0x01); - assert(c[] == "01", c); - format02x(c.ptr, 0x20); - assert(c[] == "20", c); - format02x(c.ptr, 0xff); - assert(c[] == "ff", c); -} -private -size_t format11x(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 >= 44) - { - continue; // cut - } - else if (pad && shift >= 4) - { - buffer[pos++] = pad ? ' ' : '0'; - continue; // pad - } - } else pad = false; - buffer[pos++] = hexMap[b]; - } - return pos; -} -/// -@system unittest { - char[32] b = void; - char *p = b.ptr; - assert(b[0..format11x(p, 0)] == " 0"); - assert(b[0..format11x(p, 1)] == " 1"); - assert(b[0..format11x(p, 0x10)] == " 10"); - assert(b[0..format11x(p, 0x100)] == " 100"); - assert(b[0..format11x(p, 0x1000)] == " 1000"); - assert(b[0..format11x(p, 0x10000)] == " 10000"); - assert(b[0..format11x(p, 0x100000)] == " 100000"); - assert(b[0..format11x(p, 0x1000000)] == " 1000000"); - assert(b[0..format11x(p, 0x10000000)] == " 10000000"); - assert(b[0..format11x(p, 0x100000000)] == " 100000000"); - assert(b[0..format11x(p, 0x1000000000)] == " 1000000000"); - assert(b[0..format11x(p, 0x10000000000)] == "10000000000"); - assert(b[0..format11x(p, 0x100000000000)] == "100000000000"); - assert(b[0..format11x(p, 0x1000000000000)] == "1000000000000"); - assert(b[0..format11x(p, ubyte.max)] == " ff"); - assert(b[0..format11x(p, ushort.max)] == " ffff"); - assert(b[0..format11x(p, uint.max)] == " ffffffff"); - assert(b[0..format11x(p, ulong.max)] == "ffffffffffffffff"); - assert(b[0..format11x(p, 0x1010)] == " 1010"); - assert(b[0..format11x(p, 0x10101010)] == " 10101010"); - assert(b[0..format11x(p, 0x1010101010101010)] == "1010101010101010"); -} - -private immutable static string decMap = "0123456789"; -private -size_t format03d(char *buffer, ubyte v) -{ - buffer[2] = (v % 10) + '0'; - buffer[1] = (v / 10 % 10) + '0'; - buffer[0] = (v / 100 % 10) + '0'; - return 3; -} -@system unittest { - char[3] c = void; - format03d(c.ptr, 1); - assert(c[] == "001", c); - format03d(c.ptr, 10); - assert(c[] == "010", c); - format03d(c.ptr, 111); - assert(c[] == "111", c); -} -private -size_t format11d(char *buffer, long v) -{ - debug import std.conv : text; - enum ulong I64MAX = 10_000_000_000_000_000_000UL; - 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_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++] = decMap[r]; - } - return pos; -} -/// -@system unittest { - char[32] b = void; - char *p = b.ptr; - assert(b[0..format11d(p, 0)] == " 0"); - assert(b[0..format11d(p, 1)] == " 1"); - assert(b[0..format11d(p, 10)] == " 10"); - assert(b[0..format11d(p, 100)] == " 100"); - assert(b[0..format11d(p, 1000)] == " 1000"); - assert(b[0..format11d(p, 10_000)] == " 10000"); - assert(b[0..format11d(p, 100_000)] == " 100000"); - assert(b[0..format11d(p, 1000_000)] == " 1000000"); - assert(b[0..format11d(p, 10_000_000)] == " 10000000"); - assert(b[0..format11d(p, 100_000_000)] == " 100000000"); - assert(b[0..format11d(p, 1000_000_000)] == " 1000000000"); - assert(b[0..format11d(p, 10_000_000_000)] == "10000000000"); - assert(b[0..format11d(p, 100_000_000_000)] == "100000000000"); - assert(b[0..format11d(p, 1000_000_000_000)] == "1000000000000"); - assert(b[0..format11d(p, ubyte.max)] == " 255"); - assert(b[0..format11d(p, ushort.max)] == " 65535"); - assert(b[0..format11d(p, uint.max)] == " 4294967295"); - assert(b[0..format11d(p, ulong.max)] == "18446744073709551615"); - assert(b[0..format11d(p, 1010)] == " 1010"); -} - -private -size_t format03o(char *buffer, ubyte v) -{ - buffer[2] = (v % 8) + '0'; - buffer[1] = (v / 8 % 8) + '0'; - buffer[0] = (v / 64 % 8) + '0'; - return 3; -} -@system unittest { - import std.conv : octal; - char[3] c = void; - format03o(c.ptr, 1); - assert(c[] == "001", c); - format03o(c.ptr, octal!20); - assert(c[] == "020", c); - format03o(c.ptr, octal!133); - assert(c[] == "133", c); -} -private -size_t format11o(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 >= 33) - { - continue; // cut - } - else if (pad && shift >= 3) - { - buffer[pos++] = pad ? ' ' : '0'; - continue; - } - } else pad = false; - buffer[pos++] = hexMap[b]; - } - return pos; -} -/// -@system unittest { - import std.conv : octal; - char[32] b = void; - char *p = b.ptr; - assert(b[0..format11o(p, 0)] == " 0"); - assert(b[0..format11o(p, 1)] == " 1"); - assert(b[0..format11o(p, octal!10)] == " 10"); - assert(b[0..format11o(p, octal!20)] == " 20"); - assert(b[0..format11o(p, octal!100)] == " 100"); - assert(b[0..format11o(p, octal!1000)] == " 1000"); - assert(b[0..format11o(p, octal!10_000)] == " 10000"); - assert(b[0..format11o(p, octal!100_000)] == " 100000"); - assert(b[0..format11o(p, octal!1000_000)] == " 1000000"); - assert(b[0..format11o(p, octal!10_000_000)] == " 10000000"); - assert(b[0..format11o(p, octal!100_000_000)] == " 100000000"); - assert(b[0..format11o(p, octal!1000_000_000)] == " 1000000000"); - assert(b[0..format11o(p, octal!10_000_000_000)] == "10000000000"); - assert(b[0..format11o(p, octal!100_000_000_000)] == "100000000000"); - assert(b[0..format11o(p, ubyte.max)] == " 377"); - assert(b[0..format11o(p, ushort.max)] == " 177777"); - assert(b[0..format11o(p, uint.max)] == "37777777777"); - assert(b[0..format11o(p, ulong.max)] == "1777777777777777777777"); - assert(b[0..format11o(p, octal!101_010)] == " 101010"); -} - -// !SECTION - -// -// SECTION Rendering -// - -void cursorOffset() -{ - terminalPos(0, 0); -} -void cursorContent() -{ - terminalPos(0, 1); -} -void cursorStatusbar() -{ - terminalPos(0, termSize.height - 1); -} - -void clearOffsetBar() -{ - screen.cwritefAt(0, 0, "%*s", termSize.width - 1, " "); -} -/// -//TODO: Add "edited" or '*' to end if file edited -void renderOffset() -{ - import std.conv : octal; - - version (Trace) - { - StopWatch sw = StopWatch(AutoStart.yes); - } - - // Setup index formatting - int datasize = binaryFormatter.size; - __gshared char[4] offsetFmt = " %__"; - offsetFmt[2] = cast(char)(datasize + '0'); - offsetFmt[3] = formatters[setting.offsetType].fmtchar; - - scope outbuf = new OutBuffer(); - outbuf.reserve(16 + (setting.columns * datasize)); - outbuf.write("Offset("); - outbuf.write(formatters[setting.offsetType].name); - outbuf.write(") "); - - // Add offsets - uint i; - for (; i < setting.columns; ++i) - outbuf.writef(offsetFmt, i); - // Fill rest of terminal width if in interactive mode - if (termSize.width) - { - for (i = cast(uint)outbuf.offset; i < termSize.width; ++i) - outbuf.put(' '); - } - - version (Trace) - { - Duration a = sw.peek; - } - - // OutBuffer.toString duplicates it, what a waste! - cwriteln(cast(const(char)[])outbuf.toBytes); - - version (Trace) - { - Duration b = sw.peek; - trace("gen='%s µs' print='%s µs'", - a.total!"usecs", - (b - a).total!"usecs"); - } -} - -/// -void renderStatusBar(const(char)[][] items ...) -{ - version (Trace) - { - StopWatch sw = StopWatch(AutoStart.yes); - } - - int w = termSize.width; - bool done; - - scope outbuf = new OutBuffer(); - outbuf.reserve(w); - outbuf.put(' '); - foreach (item; items) - { - if (outbuf.offset > 1) outbuf.put(" | "); - if (outbuf.offset + item.length >= w) - { - size_t r = outbuf.offset + item.length - w; - outbuf.put(item[0..r]); - done = true; - break; - } - outbuf.put(item); - } - - if (done == false) - { - // Fill rest by space - outbuf.data[outbuf.offset..w] = ' '; - outbuf.offset = w; // used in .toBytes - } - - version (Trace) - { - Duration a = sw.peek; - } - - cwrite(cast(const(char)[])outbuf.toBytes); - - version (Trace) - { - sw.stop; - Duration b = sw.peek; - trace("gen='%s µs' print='%s µs'", - a.total!"usecs", - (b - a).total!"usecs"); - } -} - -//int outputLine(long base, ubyte[] data, int row, int cursor = -1) - -/// Render multiple lines on screen with optional cursor. -/// Params: -/// base = Offset base. -/// data = data to render. -/// cursor = Position of cursor. -/// Returns: Number of rows printed. Negative numbers indicate error. -int output(long base, ubyte[] data, int cursor = -1) -{ - uint crow = void, ccol = void; - - if (data.length == 0) - return 0; - - if (cursor < 0) - crow = ccol = -1; - else - { - crow = cursor / setting.columns; - ccol = cursor % setting.columns; - } - - version (Trace) - { - trace("P=%u D=%u R=%u C=%u", - position, data.length, crow, ccol); - StopWatch sw = StopWatch(AutoStart.yes); - } - - size_t buffersz = // minimum anyway - OFFSET_SPACE + 2 + // offset + spacer - ((binaryFormatter.size + 1) * setting.columns) + // binary + spacer * cols - (1 + (setting.columns * 3)); // spacer + text (utf-8) - - char *buffer = cast(char*)malloc(buffersz); - if (buffer == null) return -1; - - int lines; - foreach (chunk; chunks(data, setting.columns)) - { - const bool cur_row = lines == crow; - - Row row = makerow(buffer, buffersz, chunk, base, ccol); - - if (cur_row) - { - version (Trace) trace("row.length=%u cbi=%u cbl=%u cti=%u ctl=%u bl=%u tl=%u", - row.result.length, - row.cursorBinaryIndex, - row.cursorBinaryLength, - row.cursorTextIndex, - row.cursorTextLength, - row.binaryLength, - row.textLength); - - // between binary and text cursors - size_t distance = row.cursorTextIndex - - row.cursorBinaryIndex - - row.cursorBinaryLength; - - char *p = buffer; - // offset + pre-cursor binary - p += cwrite(p, row.cursorBinaryIndex); - // binary cursor - terminalInvertColor; - p += cwrite(p, row.cursorBinaryLength); - terminalResetColor; - // post-cursor binary + pre-cursor text (minus spacer) - p += cwrite(p, distance); - // text cursor - terminalHighlight; - p += cwrite(p, row.cursorTextLength); - terminalResetColor; - // post-cursor text - size_t rem = row.result.length - (p - buffer); - p += cwrite(p, rem); - - version (Trace) trace("d=%u r=%u l=%u", distance, rem, p - buffer); - } - else - with (row) cwrite(result.ptr, result.length); - - cwrite('\n'); - - ++lines; - base += setting.columns; - } - - free(buffer); - - version (Trace) - { - sw.stop; - trace("time='%s µs'", sw.peek.total!"usecs"); - } - - return lines; -} - -void renderEmpty(uint rows) -{ - debug assert(termSize.rows); - debug assert(termSize.columns); - - uint lines = maxLine - rows; - - version (Trace) - { - trace("lines=%u rows=%u cols=%u", lines, termSize.rows, termSize.columns); - StopWatch sw = StopWatch(AutoStart.yes); - } - - char *p = cast(char*)malloc(termSize.columns); - assert(p); //TODO: Soft asserts - int w = termSize.columns; - memset(p, ' ', w); - - //TODO: Output to scoped OutBuffer - for (int i; i < lines; ++i) - cwrite(p, w); - - free(p); - - version (Trace) - { - sw.stop; - trace("time='%s µs'", sw.peek.total!"usecs"); - } -} - -private -struct Row -{ - char[] result; - - size_t cursorBinaryIndex; - size_t cursorBinaryLength; - size_t cursorTextIndex; - size_t cursorTextLength; - - size_t binaryLength; - size_t textLength; -} - -private -Row makerow(char *buffer, size_t bufferlen, - ubyte[] chunk, long pos, - int cursor_col) -{ - Row row = void; - - // Insert OFFSET - size_t indexData = offsetFormatter.offset(buffer, pos); - buffer[indexData++] = ' '; // index: OFFSET + space - - const uint dataLen = (setting.columns * (binaryFormatter.size + 1)); /// data row character count - size_t indexChar = indexData + dataLen; // Position for character column - - *(cast(ushort*)(buffer + indexChar)) = 0x2020; // DATA-CHAR spacer - indexChar += 2; // indexChar: indexData + dataLen + spacer - - // Format DATA and CHAR - // NOTE: Smaller loops could fit in cache... - // And would separate data/text logic - size_t bi0 = indexData, ti0 = indexChar; - int currentCol; - foreach (data; chunk) - { - const bool curhit = currentCol == cursor_col; // cursor hit column - //TODO: Maybe binary data formatter should include space? - // Data translation - buffer[indexData++] = ' '; - if (curhit) - { - row.cursorBinaryIndex = indexData; - row.cursorBinaryLength = binaryFormatter.size; - } - indexData += binaryFormatter.data(buffer + indexData, data); - // Character translation - immutable(char)[] units = transcoder.transform(data); - if (curhit) - { - row.cursorTextIndex = indexChar; - row.cursorTextLength = units.length ? units.length : 1; - } - if (units.length) // Has utf-8 codepoints - { - foreach (codeunit; units) - buffer[indexChar++] = codeunit; - } else // Invalid character, insert default character - buffer[indexChar++] = setting.defaultChar; - - ++currentCol; - } - - row.binaryLength = dataLen - bi0; - row.textLength = indexChar - ti0; - - size_t end = indexChar; - - // data length < minimum row requirement = in-fill data and text columns - if (chunk.length < setting.columns) - { - // In-fill characters: left = Columns - ChunkLength - size_t leftchar = (setting.columns - chunk.length); // Bytes left - memset(buffer + indexChar, ' ', leftchar); - row.textLength += leftchar; - // In-fill binary data: left = CharactersLeft * (DataSize + 1) - size_t leftdata = leftchar * (binaryFormatter.size + 1); - memset(buffer + indexData, ' ', leftdata); - row.binaryLength += leftdata; - - end += leftchar; - } - - row.result = buffer[0..end]; - - return row; -} - -// !SECTION - -// SECTION Console Write functions - -size_t cwrite(char c) -{ - return terminalOutput(&c, 1); -} -size_t cwrite(const(char)[] _str) -{ - return terminalOutput(_str.ptr, _str.length); -} -size_t cwrite(char *_str, size_t size) -{ - return terminalOutput(_str, size); -} -size_t cwriteln(const(char)[] _str) -{ - return cwrite(_str) + cwrite('\n'); -} -size_t cwritef(A...)(const(char)[] fmt, A args) -{ - import std.format : sformat; - char[256] buf = void; - return cwrite(sformat(buf, fmt, args)); -} -size_t cwritefln(A...)(const(char)[] fmt, A args) -{ - return cwritef(fmt, args) + cwrite('\n'); -} -size_t cwriteAt(int x, int y, char c) -{ - terminalPos(x, y); - return cwrite(c); -} -size_t cwriteAt(int x, int y, const(char)[] str) -{ - terminalPos(x, y); - return cwrite(str); -} -size_t cwritelnAt(int x, int y, const(char)[] str) -{ - terminalPos(x, y); - return cwriteln(str); -} -size_t cwritefAt(A...)(int x, int y, const(char)[] fmt, A args) -{ - terminalPos(x, y); - return cwritef(fmt, args); -} -size_t cwriteflnAt(A...)(int x, int y, const(char)[] fmt, A args) -{ - terminalPos(x, y); - return cwritefln(fmt, args); -} - -// !SECTION \ No newline at end of file diff --git a/src/searcher.d b/src/searcher.d deleted file mode 100644 index 06e1cfc..0000000 --- a/src/searcher.d +++ /dev/null @@ -1,255 +0,0 @@ -/// Search module. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module searcher; - -import std.array : uninitializedArray; -import core.stdc.string : memcmp; -import editor, error; - -alias compare = memcmp; // avoids confusion with memcpy/memcmp - -/// Search buffer size. -private enum BUFFER_SIZE = 4 * 1024; - -/*int data(ref Editor editor, out long pos, const(void) *data, size_t len, bool dir) -{ -}*/ - -/// Binary search. -/// Params: -/// pos = Found position. -/// data = Needle pointer. -/// len = Needle length. -/// dir = Search direction. If set, forwards. If unset, backwards. -/// Returns: Error code if set. -int searchData(out long pos, const(void) *data, size_t len, bool dir) -{ - int function(out long, const(void)*, size_t) F = dir ? &forward : &backward; - return F(pos, data, len); -} - -/// Search for binary data forward. -/// Params: -/// pos = Found position in file. -/// data = Data pointer. -/// len = Data length. -/// Returns: Error code if set. -private -int forward(out long newPos, const(void) *data, size_t len) -{ - version (Trace) trace("data=%s len=%u", data, len); - - ubyte *needle = cast(ubyte*)data; - /// First byte for data to compare with haystack. - const ubyte firstByte = needle[0]; - const bool firstByteOnly = len == 1; - /// File buffer. - ubyte[] fileBuffer = uninitializedArray!(ubyte[])(BUFFER_SIZE); - /// Allocated if size is higher than one. - /// Used to read file data to compare with needle if needle extends - /// across haystack chunks. - ubyte[] needleBuffer; - - if (firstByteOnly == false) - needleBuffer = uninitializedArray!(ubyte[])(len); - - // Setup - const long oldPos = editor.position; - long pos = oldPos + 1; /// New haystack position - editor.seek(pos); - - version (Trace) trace("start=%u", pos); - - size_t haystackIndex = void; /// haystack chunk index - -L_CONTINUE: - ubyte[] haystack = editor.read(fileBuffer); - const size_t haystackLen = haystack.length; - - // For every byte - for (haystackIndex = 0; haystackIndex < haystackLen; ++haystackIndex) - { - // Check first byte - if (haystack[haystackIndex] != firstByte) continue; - - // If first byte is indifferent and length is of 1, then - // we're done. - if (firstByteOnly) goto L_FOUND; - - // In-haystack or out-haystack check - // Determined if needle fits within haystack (chunk) - if (haystackIndex + len < haystackLen) // fits inside haystack - { - if (compare(haystack.ptr + haystackIndex, needle, len) == 0) - goto L_FOUND; - } else { // needle spans across haystacks - const long t = pos + haystackIndex; // temporary seek - editor.seek(t); // Go at chunk start + index - ubyte[] tc = editor.read(needleBuffer); // Read needle length - if (editor.eof) // Already hit past the end with needle length - goto L_NOTFOUND; // No more chunks - if (compare(tc.ptr, needle, len) == 0) - goto L_FOUND; - editor.seek(pos); // Negative, return to chunk start - } - } - - // Increase (search) position with chunk length. - pos += haystackLen; - - // Check if last haystack. - if (editor.eof == false) goto L_CONTINUE; - - // Not found -L_NOTFOUND: - editor.seek(oldPos); - return errorSet(ErrorCode.notFound); - -L_FOUND: // Found - newPos = pos + haystackIndex; // Position + Chunk index = Found position - return 0; -} - -/// Search for binary data backward. -/// Params: -/// pos = Found position in file. -/// data = Data pointer. -/// len = Data length. -/// Returns: Error code if set. -private -int backward(out long newPos, const(void) *data, size_t len) -{ - version (Trace) trace("data=%s len=%u", data, len); - - if (editor.position < 2) - return errorSet(ErrorCode.insufficientSpace); - - ubyte *needle = cast(ubyte*)data; - /// First byte for data to compare with haystack. - const ubyte lastByte = needle[len - 1]; - const bool lastByteOnly = len == 1; - /// File buffer. - ubyte[] fileBuffer = uninitializedArray!(ubyte[])(BUFFER_SIZE); - /// Allocated if size is higher than one. - /// Used to read file data to compare with needle if needle extends - /// across haystack chunks. - ubyte[] needleBuffer; - - if (lastByteOnly == false) // No need for buffer if needle is a byte - needleBuffer = uninitializedArray!(ubyte[])(len); - - // Setup - const long oldPos = editor.position; - long pos = oldPos - 1; /// Chunk position - editor.seek(pos); - - version (Trace) trace("start=%u", pos); - - size_t haystackIndex = void; - size_t haystackLen = BUFFER_SIZE; - ptrdiff_t diff = void; - -L_CONTINUE: - pos -= haystackLen; - - // Adjusts buffer size to read chunk if it goes past start of haystack. - if (pos < 0) - { - haystackLen += pos; - fileBuffer = uninitializedArray!(ubyte[])(haystackLen); - pos = 0; - } - - editor.seek(pos); - ubyte[] haystack = editor.read; - - // For every byte - for (haystackIndex = haystackLen; haystackIndex-- > 0;) - { - // Check last byte - if (haystack[haystackIndex] != lastByte) continue; - - // Fix when needle is one byte - diff = 0; - - // If first byte is indifferent and length is of 1, then - // we're done. - if (lastByteOnly) goto L_FOUND; - - // Go at needle - diff = haystackIndex - len + 1; // Go at needle[0] - - // In-haystack or out-haystack check - // Determined if needle fits within haystack (chunk) - if (diff >= 0) // fits inside haystack - { - if (compare(haystack.ptr + diff, needle, len) == 0) - goto L_FOUND; - } else { // needle spans across haystacks - editor.seek(pos + diff); // temporary seek - ubyte[] tc = editor.read(needleBuffer); // Read needle length - if (compare(tc.ptr, needle, len) == 0) - goto L_FOUND; - editor.seek(pos); // Go back we where last - } - } - - // Acts like EOF. - if (pos > 0) goto L_CONTINUE; - - // Not found - editor.seek(oldPos); - return errorSet(ErrorCode.notFound); - -L_FOUND: // Found - newPos = pos + diff; // Position + Chunk index = Found position - return 0; -} - -/// Finds the next position indifferent to specified byte. -/// Params: -/// data = Byte. -/// newPos = Found position. -/// Returns: Error code. -int searchSkip(ubyte data, out long newPos) -{ - version (Trace) trace("data=0x%x", data); - - /// File buffer. - ubyte[] fileBuffer = uninitializedArray!(ubyte[])(BUFFER_SIZE); - size_t haystackIndex = void; - - const long oldPos = editor.position; - long pos = oldPos + 1; - -L_CONTINUE: - editor.seek(pos); - ubyte[] haystack = editor.read(fileBuffer); - const size_t haystackLen = haystack.length; - - // For every byte - for (haystackIndex = 0; haystackIndex < haystackLen; ++haystackIndex) - { - if (haystack[haystackIndex] != data) - goto L_FOUND; - } - - // Increase (search) position with chunk length. - pos += haystackLen; - - // Check if last haystack. - // If haystack length is lower than the default size, - // this simply means it's the last haystack since it reached - // OEF (by having read less data). - if (haystackLen == BUFFER_SIZE) goto L_CONTINUE; - - // Not found - editor.seek(oldPos); - return errorSet(ErrorCode.notFound); - -L_FOUND: // Found - newPos = pos + haystackIndex; // Position + Chunk index = Found position - return 0; -} diff --git a/src/settings.d b/src/settings.d deleted file mode 100644 index 8d491f8..0000000 --- a/src/settings.d +++ /dev/null @@ -1,309 +0,0 @@ -/// Settings handler. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module settings; - -import ddhx; // for NumberType -import os.terminal : TerminalSize, terminalSize; - -//TODO: Save config file on parameter change? - -private -enum MAX_STATUS_ITEMS = 10; - -enum StatusItem : ubyte { - /// Empty. - empty, - /// Shows editing mode, like insertion. - editMode, - /// Shows binary data display formatting type. - dataMode, - /// Shows current character translation. - charMode, - /// Shows size of the view buffer in size. - viewSize, - /// Shows absolute file position following offset setting. - absolutePosition, - /// Shows absolute file position that its type can be changed. - absolutePositionAlt, - /// Shows absolute file position relative to file size in percentage. - absolutePercentage, -} - -immutable string COMMAND_COLUMNS = "columns"; -immutable string COMMAND_OFFSET = "offset"; -immutable string COMMAND_DATA = "data"; -immutable string COMMAND_FILLER = "filler"; -immutable string COMMAND_SI = "si"; -immutable string COMMAND_IEC = "iec"; -immutable string COMMAND_CHARSET = "charset"; -// Editing modes -immutable string COMMAND_INSERT = "insert"; -immutable string COMMAND_OVERWRITE = "overwrite"; -immutable string COMMAND_READONLY = "readonly"; -immutable string COMMAND_VIEW = "view"; - -//TODO: Consider having statusbar offset type seperate offset - -private struct settings_t { - /// Bytes per row. - /// Default: 16 - int columns = 16; - /// Offset number number formatting type. - /// Default: hexadecimal - NumberType offsetType; - /// Binary data number formatting type. - /// Default: hexadecimal - NumberType dataType; - // Number formatting type for absolute offset in statusbar. - // Default: hexadecimal -// NumberType statusType; - /// Default character to use for non-ascii characters - /// Default: Period ('.') - char defaultChar = '.'; - /// Use ISO base-10 prefixes over IEC base-2 - /// Default: false - bool si; - /// - /// Default: As presented - StatusItem[MAX_STATUS_ITEMS] statusItems = [ - StatusItem.editMode, - StatusItem.dataMode, - StatusItem.charMode, - StatusItem.viewSize, - StatusItem.absolutePosition, - StatusItem.empty, - StatusItem.empty, - StatusItem.empty, - StatusItem.empty, - StatusItem.empty, - ]; -} - -/// Current settings. -public __gshared settings_t setting; - -/// Reset all settings to default. -void resetSettings() -{ - setting = setting.init; - transcoderSelect(CharacterSet.ascii); -} - -/// Determines the optimal column width given terminal width. -int optimalWidth() -{ - TerminalSize termsize = terminalSize; - int dataSize = void; - final switch (setting.dataType) with (NumberType) - { - case hexadecimal: dataSize = 2; break; - case decimal, octal: dataSize = 3; break; - } - dataSize += 2; // Account for space - //TODO: +groups - //width = cast(ushort)((w - 12) / 4); - return (termsize.width - 16) / dataSize; -} - -int settingsColumns(string val) -{ - if (val == null || val.length == 0) - return errorSet(ErrorCode.invalidParameter); - - version (Trace) trace("value='%s'", val); - - switch (val[0]) { - case 'a': // Automatic (fit terminal width) - setting.columns = optimalWidth; - break; - case 'd': // Default - setting.columns = 16; - break; - default: - ushort l = void; - if (convertToVal(l, val)) - return errorSet(ErrorCode.invalidNumber); - if (l == 0) - return errorSet(ErrorCode.settingColumnsInvalid); - setting.columns = l; - } - return 0; -} - -int settingsOffset(string val) -{ - if (val == null || val.length == 0) - return errorSet(ErrorCode.invalidParameter); - - version (Trace) trace("value='%s'", val); - - switch (val[0]) { - case 'o','O': setting.offsetType = NumberType.octal; break; - case 'd','D': setting.offsetType = NumberType.decimal; break; - case 'h','H': setting.offsetType = NumberType.hexadecimal; break; - default: return errorSet(ErrorCode.invalidParameter); - } - screen.setOffsetFormat(setting.offsetType); - return 0; -} - -int settingsData(string val) -{ - if (val == null || val.length == 0) - return errorSet(ErrorCode.invalidParameter); - - version (Trace) trace("value='%s'", val); - - switch (val[0]) { - case 'o','O': setting.dataType = NumberType.octal; break; - case 'd','D': setting.dataType = NumberType.decimal; break; - case 'h','H': setting.dataType = NumberType.hexadecimal; break; - default: return errorSet(ErrorCode.invalidParameter); - } - screen.setBinaryFormat(setting.dataType); - return 0; -} - -int settingsFiller(string val) -{ - if (val == null || val.length == 0) - return errorSet(ErrorCode.invalidParameter); - - version (Trace) trace("value='%s'", val); - - switch (val) { // aliases - case "space": setting.defaultChar = ' '; break; - case "dot": setting.defaultChar = '.'; break; - default: setting.defaultChar = val[0]; - } - return 0; -} - -int settingsCharset(string val) -{ - if (val == null || val.length == 0) - return errorSet(ErrorCode.invalidParameter); - - version (Trace) trace("value='%s'", val); - - switch (val) { - case "ascii": transcoderSelect(CharacterSet.ascii); break; - case "cp437": transcoderSelect(CharacterSet.cp437); break; - case "ebcdic": transcoderSelect(CharacterSet.ebcdic); break; - case "mac": transcoderSelect(CharacterSet.mac); break; - default: return errorSet(ErrorCode.invalidCharset); - } - return 0; -} - -//TODO: Consider doing an AA with string[]... functions -// or enum size_t HASH_FILLER = "filler".hashOf(); -// Low priority - -int set(string[] args) -{ - const size_t argc = args.length; - - if (argc == 0) - return errorSet(ErrorCode.missingOption); - - switch (args[0]) { - case COMMAND_FILLER: - if (argc < 2) - return errorSet(ErrorCode.missingValue); - return settingsFiller(args[1]); - case COMMAND_COLUMNS: - if (argc < 2) - return errorSet(ErrorCode.missingValue); - return settingsColumns(args[1]); - case COMMAND_OFFSET: - if (argc < 2) - return errorSet(ErrorCode.missingValue); - return settingsOffset(args[1]); - case COMMAND_DATA: - if (argc < 2) - return errorSet(ErrorCode.missingValue); - return settingsData(args[1]); - case COMMAND_SI: - setting.si = true; - return 0; - case COMMAND_IEC: - setting.si = false; - return 0; - case COMMAND_CHARSET: - if (argc < 2) - return errorSet(ErrorCode.missingValue); - return settingsCharset(args[1]); - // Editing modes - case COMMAND_INSERT: - - return 0; - case COMMAND_OVERWRITE: - - return 0; - case COMMAND_READONLY: - - return 0; - case COMMAND_VIEW: - - return 0; - default: - } - - return errorSet(ErrorCode.invalidSetting); -} - -int loadSettings(string rc) -{ - import std.stdio : File; - import std.file : exists; - import os.path : buildUserFile, buildUserAppFile; - import std.format.read : formattedRead; - import std.string : chomp, strip; - import utils.args : arguments; - - static immutable string cfgname = ".ddhxrc"; - - if (rc == null) - { - rc = buildUserFile(cfgname); - - if (rc is null) - goto L_APPCONFIG; - if (rc.exists) - goto L_SELECTED; - -L_APPCONFIG: - rc = buildUserAppFile("ddhx", cfgname); - - if (rc is null) - return 0; - if (rc.exists == false) - return 0; - } else { - if (exists(rc) == false) - return errorSet(ErrorCode.settingFileMissing); - } - -L_SELECTED: - version (Trace) trace("rc='%s'", rc); - - File file; - file.open(rc); - int linenum; - foreach (line; file.byLine()) - { - ++linenum; - if (line.length == 0) continue; - if (line[0] == '#') continue; - - string[] args = arguments(cast(string)line.chomp); - - if (set(args)) - return errorcode; - } - - return 0; -} \ No newline at end of file diff --git a/src/transcoder.d b/src/transcoder.d index 8cca392..549ca86 100644 --- a/src/transcoder.d +++ b/src/transcoder.d @@ -1,21 +1,48 @@ /// Transcoder module. /// -/// Translates single-byte characters into utf-8. +/// Translates single-byte characters into utf-8 strings. /// Copyright: dd86k /// License: MIT /// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module encoding; +module transcoder; import std.encoding : codeUnits, CodeUnits; /// Character set. -enum CharacterSet : ubyte { +enum CharacterSet +{ ascii, /// 7-bit US-ASCII cp437, /// IBM PC CP-437 - ebcdic, /// IBM EBCDIC Code Page 37 + ebcdic, /// IBM EBCDIC Code Page 37 (CCSID 37) mac, /// Mac OS Roman (Windows 10000) -// t61, /// ITU T.61 -// gsm, /// GSM 03.38 + //t61, /// ITU T.61 + //gsm, /// GSM 03.38 +} + +string charsetName(int charset) +{ + switch (charset) with (CharacterSet) + { + case ascii: return "ascii"; + case cp437: return "cp437"; + case ebcdic: return "ascii"; + case mac: return "ascii"; + default: + assert(false); + } +} + +string charsetFullname(int charset) +{ + switch (charset) with (CharacterSet) + { + case ascii: return "ASCII"; + case cp437: return "IBM PC Code Page 437"; + case ebcdic: return "IBM EBCDIC Code Page 37"; + case mac: return "Mac OS Roman (Windows 10000)"; + default: + assert(false); + } } //TODO: Consider registering encoders to EncodingScheme @@ -39,47 +66,21 @@ // - GSM 03.38 "gsm" (technically multibyte) // https://www.unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT -private struct Transcoder { - alias transform this; - string name = "ascii"; - immutable(char)[] function(ubyte) transform = &transcodeASCII; -} - -private immutable(char)[] emptychar = []; - -private immutable Transcoder[4] transcoders = [ - { "ascii", &transcodeASCII }, - { "cp437", &transcodeCP437 }, - { "ebcdic", &transcodeEBCDIC }, - { "mac", &transcodeMac }, -]; - -/// Current transcoder -public __gshared Transcoder transcoder; - -/// Select a new transcoder. -/// Params: charset = Character set. -void transcoderSelect(CharacterSet charset) -{ - debug assert(charset <= CharacterSet.max); - transcoder = transcoders[charset]; -} - private alias U = char[]; private template C(dchar c) { enum C = cast(immutable)codeUnits!char(c).s; } +private immutable immutable(char)[] emptychar; -private -immutable(char)[] transcodeASCII(ubyte data) +immutable(char)[] transcodeASCII(ubyte data) @trusted { - __gshared immutable(char)[] empty; __gshared char[1] c; if (data > 0x7E || data < 0x20) - return empty; + return emptychar; c[0] = data; return c; } -unittest { +@trusted unittest +{ assert(transcodeASCII(0) == []); assert(transcodeASCII('a') == [ 'a' ]); assert(transcodeASCII(0x7f) == []); @@ -87,45 +88,45 @@ private immutable U[256] mapCP437 = [ // 0 1 2 3 4 5 6 7 -/*00*/ [], C!'☺', C!'☻', C!'♥', C!'♦', C!'♣', C!'♠', C!'•', -/*08*/ C!'◘', C!'○', C!'◙', C!'♂', C!'♀', C!'♪', C!'♫', C!'☼', -/*10*/ C!'►', C!'◄', C!'↕', C!'‼', C!'¶', C!'§', C!'▬', C!'↨', -/*18*/ C!'↑', C!'↓', C!'→', C!'←', C!'∟', C!'↔', C!'▲', C!'▼', -/*20*/ C!' ', C!'!', C!'"', C!'#', C!'$', C!'%', C!'&',C!'\'', -/*28*/ C!'(', C!')', C!'*', C!'+', C!',', C!'-', C!'.', C!'/', -/*30*/ C!'0', C!'1', C!'2', C!'3', C!'4', C!'5', C!'6', C!'7', -/*38*/ C!'8', C!'9', C!':', C!';', C!'<', C!'>', C!'=', C!'?', -/*40*/ C!'@', C!'A', C!'B', C!'C', C!'D', C!'E', C!'F', C!'G', -/*48*/ C!'H', C!'I', C!'J', C!'K', C!'M', C!'N', C!'L', C!'O', -/*50*/ C!'P', C!'Q', C!'R', C!'S', C!'T', C!'U', C!'V', C!'W', -/*58*/ C!'X', C!'Y', C!'Z', C!'[',C!'\\', C!']', C!'^', C!'_', -/*60*/ C!'`', C!'a', C!'b', C!'c', C!'d', C!'e', C!'f', C!'g', -/*68*/ C!'h', C!'i', C!'j', C!'k', C!'l', C!'m', C!'n', C!'o', -/*70*/ C!'p', C!'q', C!'r', C!'s', C!'t', C!'u', C!'v', C!'w', -/*78*/ C!'x', C!'y', C!'z', C!'{', C!'|', C!'}', C!'~', C!'⌂', -/*80*/ C!'Ç', C!'ü', C!'é', C!'â', C!'ä', C!'à', C!'å', C!'ç', -/*88*/ C!'ê', C!'ë', C!'è', C!'ï', C!'î', C!'ì', C!'Ä', C!'Å', -/*90*/ C!'É', C!'æ', C!'Æ', C!'ô', C!'ö', C!'ò', C!'û', C!'ù', -/*98*/ C!'ÿ', C!'Ö', C!'Ü', C!'¢', C!'£', C!'¥', C!'₧', C!'ƒ', -/*a0*/ C!'á', C!'í', C!'ó', C!'ú', C!'ñ', C!'Ñ', C!'ª', C!'º', -/*a8*/ C!'¿', C!'⌐', C!'¬', C!'½', C!'¼', C!'¡', C!'«', C!'»', -/*b0*/ C!'░', C!'▒', C!'▓', C!'│', C!'┤', C!'╡', C!'╢', C!'╖', -/*b8*/ C!'╕', C!'╣', C!'║', C!'╗', C!'╝', C!'╜', C!'╛', C!'┐', -/*c0*/ C!'└', C!'┴', C!'┬', C!'├', C!'─', C!'┼', C!'╞', C!'╟', -/*c8*/ C!'╚', C!'╔', C!'╩', C!'╦', C!'╠', C!'═', C!'╬', C!'╧', -/*d0*/ C!'╨', C!'╤', C!'╥', C!'╙', C!'╘', C!'╒', C!'╓', C!'╫', -/*d8*/ C!'╪', C!'┘', C!'┌', C!'█', C!'▄', C!'▌', C!'▐', C!'▀', -/*e0*/ C!'α', C!'β', C!'Γ', C!'π', C!'Σ', C!'σ', C!'µ', C!'τ', -/*e8*/ C!'Φ', C!'Θ', C!'Ω', C!'δ', C!'∞', C!'φ', C!'ε', C!'∩', -/*f0*/ C!'≡', C!'±', C!'≥', C!'≤', C!'⌠', C!'⌡', C!'÷', C!'≈', -/*f8*/ C!'°', C!'∙', C!'·', C!'√', C!'ⁿ', C!'²', C!'■', C!' ' +/*00*/ [], C!'☺', C!'☻', C!'♥', C!'♦', C!'♣', C!'♠', C!'•', +/*08*/ C!'◘', C!'○', C!'◙', C!'♂', C!'♀', C!'♪', C!'♫', C!'☼', +/*10*/ C!'►', C!'◄', C!'↕', C!'‼', C!'¶', C!'§', C!'▬', C!'↨', +/*18*/ C!'↑', C!'↓', C!'→', C!'←', C!'∟', C!'↔', C!'▲', C!'▼', +/*20*/ C!' ', C!'!', C!'"', C!'#', C!'$', C!'%', C!'&',C!'\'', +/*28*/ C!'(', C!')', C!'*', C!'+', C!',', C!'-', C!'.', C!'/', +/*30*/ C!'0', C!'1', C!'2', C!'3', C!'4', C!'5', C!'6', C!'7', +/*38*/ C!'8', C!'9', C!':', C!';', C!'<', C!'>', C!'=', C!'?', +/*40*/ C!'@', C!'A', C!'B', C!'C', C!'D', C!'E', C!'F', C!'G', +/*48*/ C!'H', C!'I', C!'J', C!'K', C!'M', C!'N', C!'L', C!'O', +/*50*/ C!'P', C!'Q', C!'R', C!'S', C!'T', C!'U', C!'V', C!'W', +/*58*/ C!'X', C!'Y', C!'Z', C!'[',C!'\\', C!']', C!'^', C!'_', +/*60*/ C!'`', C!'a', C!'b', C!'c', C!'d', C!'e', C!'f', C!'g', +/*68*/ C!'h', C!'i', C!'j', C!'k', C!'l', C!'m', C!'n', C!'o', +/*70*/ C!'p', C!'q', C!'r', C!'s', C!'t', C!'u', C!'v', C!'w', +/*78*/ C!'x', C!'y', C!'z', C!'{', C!'|', C!'}', C!'~', C!'⌂', +/*80*/ C!'Ç', C!'ü', C!'é', C!'â', C!'ä', C!'à', C!'å', C!'ç', +/*88*/ C!'ê', C!'ë', C!'è', C!'ï', C!'î', C!'ì', C!'Ä', C!'Å', +/*90*/ C!'É', C!'æ', C!'Æ', C!'ô', C!'ö', C!'ò', C!'û', C!'ù', +/*98*/ C!'ÿ', C!'Ö', C!'Ü', C!'¢', C!'£', C!'¥', C!'₧', C!'ƒ', +/*a0*/ C!'á', C!'í', C!'ó', C!'ú', C!'ñ', C!'Ñ', C!'ª', C!'º', +/*a8*/ C!'¿', C!'⌐', C!'¬', C!'½', C!'¼', C!'¡', C!'«', C!'»', +/*b0*/ C!'░', C!'▒', C!'▓', C!'│', C!'┤', C!'╡', C!'╢', C!'╖', +/*b8*/ C!'╕', C!'╣', C!'║', C!'╗', C!'╝', C!'╜', C!'╛', C!'┐', +/*c0*/ C!'└', C!'┴', C!'┬', C!'├', C!'─', C!'┼', C!'╞', C!'╟', +/*c8*/ C!'╚', C!'╔', C!'╩', C!'╦', C!'╠', C!'═', C!'╬', C!'╧', +/*d0*/ C!'╨', C!'╤', C!'╥', C!'╙', C!'╘', C!'╒', C!'╓', C!'╫', +/*d8*/ C!'╪', C!'┘', C!'┌', C!'█', C!'▄', C!'▌', C!'▐', C!'▀', +/*e0*/ C!'α', C!'β', C!'Γ', C!'π', C!'Σ', C!'σ', C!'µ', C!'τ', +/*e8*/ C!'Φ', C!'Θ', C!'Ω', C!'δ', C!'∞', C!'φ', C!'ε', C!'∩', +/*f0*/ C!'≡', C!'±', C!'≥', C!'≤', C!'⌠', C!'⌡', C!'÷', C!'≈', +/*f8*/ C!'°', C!'∙', C!'·', C!'√', C!'ⁿ', C!'²', C!'■', C!' ' ]; -private immutable(char)[] transcodeCP437(ubyte data) { return mapCP437[data]; } -unittest { +unittest +{ assert(transcodeCP437(0) == []); assert(transcodeCP437('r') == [ 'r' ]); assert(transcodeCP437(1) == [ '\xe2', '\x98', '\xba' ]); @@ -133,37 +134,37 @@ private immutable U[192] mapEBCDIC = [ // 256 - 64 (0x40) just unprintable // 0 1 2 3 4 5 6 7 -/*40*/ C!' ', C!' ', C!'â', C!'ä', C!'à', C!'á', C!'ã', C!'å', -/*48*/ C!'ç', C!'ñ', C!'¢', C!'.', C!'<', C!'(', C!'+', C!'|', -/*50*/ C!'&', C!'é', C!'ê', C!'ë', C!'è', C!'í', C!'î', C!'ï', -/*58*/ C!'ì', C!'ß', C!'!', C!'$', C!'*', C!')', C!';', C!'¬', -/*60*/ C!'-', C!'/', C!'Â', C!'Ä', C!'À', C!'Á', C!'Ã', C!'Å', -/*68*/ C!'Ç', C!'Ñ', C!'¦', C!',', C!'%', C!'_', C!'>', C!'?', -/*70*/ C!'ø', C!'É', C!'Ê', C!'Ë', C!'È', C!'Í', C!'Î', C!'Ï', -/*78*/ C!'Ì', C!'`', C!':', C!'#', C!'@', C!'\'',C!'=', C!'"', -/*80*/ C!'Ø', C!'a', C!'b', C!'c', C!'d', C!'e', C!'f', C!'g', -/*88*/ C!'h', C!'i', C!'«', C!'»', C!'ð', C!'ý', C!'þ', C!'±', -/*90*/ C!'°', C!'j', C!'k', C!'l', C!'m', C!'n', C!'o', C!'p', -/*98*/ C!'q', C!'r', C!'ª', C!'º', C!'æ', C!'¸', C!'Æ', C!'¤', -/*a0*/ C!'µ', C!'~', C!'s', C!'t', C!'u', C!'v', C!'w', C!'x', -/*a8*/ C!'y', C!'z', C!'¡', C!'¿', C!'Ð', C!'Ý', C!'Þ', C!'®', -/*b0*/ C!'^', C!'£', C!'¥', C!'·', C!'©', C!'§', C!'¶', C!'¼', -/*b8*/ C!'½', C!'¾', C!'[', C!']', C!'¯', C!'¨', C!'´', C!'×', -/*c0*/ C!'{', C!'A', C!'B', C!'C', C!'D', C!'E', C!'F', C!'G', -/*c8*/ C!'H', C!'I', [], C!'ô', C!'ö', C!'ò', C!'ó', C!'õ', -/*d0*/ C!'}', C!'J', C!'K', C!'L', C!'M', C!'N', C!'O', C!'P', -/*d8*/ C!'Q', C!'R', C!'¹', C!'û', C!'ü', C!'ù', C!'ú', C!'ÿ', -/*e0*/ C!'\\',C!'÷', C!'S', C!'T', C!'U', C!'V', C!'W', C!'X', -/*e8*/ C!'Y', C!'Z', C!'²', C!'Ô', C!'Ö', C!'Ò', C!'Ó', C!'Õ', -/*f0*/ C!'0', C!'1', C!'2', C!'3', C!'4', C!'5', C!'6', C!'7', -/*f8*/ C!'8', C!'9', C!'³', C!'Û', C!'Ü', C!'Ù', C!'Ú', [] +/*40*/ C!' ', C!' ', C!'â', C!'ä', C!'à', C!'á', C!'ã', C!'å', +/*48*/ C!'ç', C!'ñ', C!'¢', C!'.', C!'<', C!'(', C!'+', C!'|', +/*50*/ C!'&', C!'é', C!'ê', C!'ë', C!'è', C!'í', C!'î', C!'ï', +/*58*/ C!'ì', C!'ß', C!'!', C!'$', C!'*', C!')', C!';', C!'¬', +/*60*/ C!'-', C!'/', C!'Â', C!'Ä', C!'À', C!'Á', C!'Ã', C!'Å', +/*68*/ C!'Ç', C!'Ñ', C!'¦', C!',', C!'%', C!'_', C!'>', C!'?', +/*70*/ C!'ø', C!'É', C!'Ê', C!'Ë', C!'È', C!'Í', C!'Î', C!'Ï', +/*78*/ C!'Ì', C!'`', C!':', C!'#', C!'@', C!'\'',C!'=', C!'"', +/*80*/ C!'Ø', C!'a', C!'b', C!'c', C!'d', C!'e', C!'f', C!'g', +/*88*/ C!'h', C!'i', C!'«', C!'»', C!'ð', C!'ý', C!'þ', C!'±', +/*90*/ C!'°', C!'j', C!'k', C!'l', C!'m', C!'n', C!'o', C!'p', +/*98*/ C!'q', C!'r', C!'ª', C!'º', C!'æ', C!'¸', C!'Æ', C!'¤', +/*a0*/ C!'µ', C!'~', C!'s', C!'t', C!'u', C!'v', C!'w', C!'x', +/*a8*/ C!'y', C!'z', C!'¡', C!'¿', C!'Ð', C!'Ý', C!'Þ', C!'®', +/*b0*/ C!'^', C!'£', C!'¥', C!'·', C!'©', C!'§', C!'¶', C!'¼', +/*b8*/ C!'½', C!'¾', C!'[', C!']', C!'¯', C!'¨', C!'´', C!'×', +/*c0*/ C!'{', C!'A', C!'B', C!'C', C!'D', C!'E', C!'F', C!'G', +/*c8*/ C!'H', C!'I', [], C!'ô', C!'ö', C!'ò', C!'ó', C!'õ', +/*d0*/ C!'}', C!'J', C!'K', C!'L', C!'M', C!'N', C!'O', C!'P', +/*d8*/ C!'Q', C!'R', C!'¹', C!'û', C!'ü', C!'ù', C!'ú', C!'ÿ', +/*e0*/ C!'\\',C!'÷', C!'S', C!'T', C!'U', C!'V', C!'W', C!'X', +/*e8*/ C!'Y', C!'Z', C!'²', C!'Ô', C!'Ö', C!'Ò', C!'Ó', C!'Õ', +/*f0*/ C!'0', C!'1', C!'2', C!'3', C!'4', C!'5', C!'6', C!'7', +/*f8*/ C!'8', C!'9', C!'³', C!'Û', C!'Ü', C!'Ù', C!'Ú', [] ]; -private immutable(char)[] transcodeEBCDIC(ubyte data) { return data >= 0x40 ? mapEBCDIC[data-0x40] : emptychar; } -unittest { +unittest +{ assert(transcodeEBCDIC(0) == [ ]); assert(transcodeEBCDIC(0x42) == [ '\xc3', '\xa2' ]); assert(transcodeEBCDIC(0x7c) == [ '@' ]); @@ -174,41 +175,41 @@ // NOTE: 0xF0 is the apple logo and that's obviously not in Unicode private immutable U[224] mapMac = [ // 256 - 32 (0x20) // 0 1 2 3 4 5 6 7 -/*20*/ C!' ', C!'!', C!'"', C!'#', C!'$', C!'%', C!'&',C!'\'', -/*28*/ C!'(', C!')', C!'*', C!'+', C!',', C!'-', C!'.', C!'/', -/*30*/ C!'0', C!'1', C!'2', C!'3', C!'4', C!'5', C!'6', C!'7', -/*38*/ C!'8', C!'9', C!':', C!';', C!'<', C!'=', C!'>', C!'?', -/*40*/ C!'@', C!'A', C!'B', C!'C', C!'D', C!'E', C!'F', C!'G', -/*48*/ C!'H', C!'I', C!'J', C!'K', C!'L', C!'M', C!'N', C!'O', -/*50*/ C!'P', C!'Q', C!'R', C!'S', C!'T', C!'U', C!'V', C!'W', -/*58*/ C!'X', C!'Y', C!'Z', C!'[',C!'\\', C!']', C!'^', C!'_', -/*60*/ C!'`', C!'a', C!'b', C!'c', C!'d', C!'e', C!'f', C!'g', -/*68*/ C!'h', C!'i', C!'j', C!'k', C!'l', C!'m', C!'n', C!'o', -/*70*/ C!'p', C!'q', C!'r', C!'s', C!'t', C!'u', C!'v', C!'w', -/*78*/ C!'x', C!'y', C!'z', C!'{', C!'|', C!'}', C!'~', [], -/*80*/ C!'Ä', C!'Å', C!'Ç', C!'É', C!'Ñ', C!'Ö', C!'Ü', C!'á', -/*88*/ C!'à', C!'â', C!'ä', C!'ã', C!'å', C!'ç', C!'é', C!'è', -/*90*/ C!'ê', C!'ë', C!'í', C!'ì', C!'î', C!'ï', C!'ñ', C!'ó', -/*98*/ C!'ò', C!'ô', C!'ö', C!'õ', C!'ú', C!'ù', C!'û', C!'ü', -/*a0*/ C!'†', C!'°', C!'¢', C!'£', C!'§', C!'•', C!'¶', C!'ß', -/*a8*/ C!'®', C!'©', C!'™', C!'´', C!'¨', C!'≠', C!'Æ', C!'Ø', -/*b0*/ C!'∞', C!'±', C!'≤', C!'≥', C!'¥', C!'µ', C!'∂', C!'∑', -/*b8*/ C!'∏', C!'π', C!'∫', C!'ª', C!'º', C!'Ω', C!'æ', C!'ø', -/*c0*/ C!'¿', C!'¡', C!'¬', C!'√', C!'ƒ', C!'≈', C!'∆', C!'«', -/*c8*/ C!'»', C!'…', C!' ', C!'À', C!'Ã', C!'Õ', C!'Œ', C!'œ', -/*d0*/ C!'–', C!'—', C!'“', C!'”', C!'‘', C!'’', C!'÷', C!'◊', -/*d8*/ C!'ÿ', C!'Ÿ', C!'⁄', C!'€', C!'‹', C!'›', C!'fi', C!'fl', -/*e0*/ C!'‡', C!'·', C!'‚', C!'„', C!'‰', C!'Â', C!'Ê', C!'Á', -/*e8*/ C!'Ë', C!'È', C!'Í', C!'Î', C!'Ï', C!'Ì', C!'Ó', C!'Ô', -/*f0*/ [], C!'Ò', C!'Ú', C!'Û', C!'Ù', C!'ı', C!'ˆ', C!'˜', -/*f8*/ C!'¯', C!'˘', C!'˙', C!'˚', C!'¸', C!'˝', C!'˛', C!'ˇ', +/*20*/ C!' ', C!'!', C!'"', C!'#', C!'$', C!'%', C!'&',C!'\'', +/*28*/ C!'(', C!')', C!'*', C!'+', C!',', C!'-', C!'.', C!'/', +/*30*/ C!'0', C!'1', C!'2', C!'3', C!'4', C!'5', C!'6', C!'7', +/*38*/ C!'8', C!'9', C!':', C!';', C!'<', C!'=', C!'>', C!'?', +/*40*/ C!'@', C!'A', C!'B', C!'C', C!'D', C!'E', C!'F', C!'G', +/*48*/ C!'H', C!'I', C!'J', C!'K', C!'L', C!'M', C!'N', C!'O', +/*50*/ C!'P', C!'Q', C!'R', C!'S', C!'T', C!'U', C!'V', C!'W', +/*58*/ C!'X', C!'Y', C!'Z', C!'[',C!'\\', C!']', C!'^', C!'_', +/*60*/ C!'`', C!'a', C!'b', C!'c', C!'d', C!'e', C!'f', C!'g', +/*68*/ C!'h', C!'i', C!'j', C!'k', C!'l', C!'m', C!'n', C!'o', +/*70*/ C!'p', C!'q', C!'r', C!'s', C!'t', C!'u', C!'v', C!'w', +/*78*/ C!'x', C!'y', C!'z', C!'{', C!'|', C!'}', C!'~', [], +/*80*/ C!'Ä', C!'Å', C!'Ç', C!'É', C!'Ñ', C!'Ö', C!'Ü', C!'á', +/*88*/ C!'à', C!'â', C!'ä', C!'ã', C!'å', C!'ç', C!'é', C!'è', +/*90*/ C!'ê', C!'ë', C!'í', C!'ì', C!'î', C!'ï', C!'ñ', C!'ó', +/*98*/ C!'ò', C!'ô', C!'ö', C!'õ', C!'ú', C!'ù', C!'û', C!'ü', +/*a0*/ C!'†', C!'°', C!'¢', C!'£', C!'§', C!'•', C!'¶', C!'ß', +/*a8*/ C!'®', C!'©', C!'™', C!'´', C!'¨', C!'≠', C!'Æ', C!'Ø', +/*b0*/ C!'∞', C!'±', C!'≤', C!'≥', C!'¥', C!'µ', C!'∂', C!'∑', +/*b8*/ C!'∏', C!'π', C!'∫', C!'ª', C!'º', C!'Ω', C!'æ', C!'ø', +/*c0*/ C!'¿', C!'¡', C!'¬', C!'√', C!'ƒ', C!'≈', C!'∆', C!'«', +/*c8*/ C!'»', C!'…', C!' ', C!'À', C!'Ã', C!'Õ', C!'Œ', C!'œ', +/*d0*/ C!'–', C!'—', C!'“', C!'”', C!'‘', C!'’', C!'÷', C!'◊', +/*d8*/ C!'ÿ', C!'Ÿ', C!'⁄', C!'€', C!'‹', C!'›', C!'fi', C!'fl', +/*e0*/ C!'‡', C!'·', C!'‚', C!'„', C!'‰', C!'Â', C!'Ê', C!'Á', +/*e8*/ C!'Ë', C!'È', C!'Í', C!'Î', C!'Ï', C!'Ì', C!'Ó', C!'Ô', +/*f0*/ [], C!'Ò', C!'Ú', C!'Û', C!'Ù', C!'ı', C!'ˆ', C!'˜', +/*f8*/ C!'¯', C!'˘', C!'˙', C!'˚', C!'¸', C!'˝', C!'˛', C!'ˇ', ]; -private immutable(char)[] transcodeMac(ubyte data) { return data >= 0x20 ? mapMac[data-0x20] : emptychar; } -unittest { +unittest +{ assert(transcodeMac(0) == [ ]); assert(transcodeMac(0x20) == [ ' ' ]); assert(transcodeMac(0x22) == [ '"' ]); diff --git a/src/utils/args.d b/src/utils/args.d index b6cebb2..b1aa877 100644 --- a/src/utils/args.d +++ b/src/utils/args.d @@ -1,10 +1,13 @@ +/// Utilities to handle arguments. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) module utils.args; - /// Separate buffer into arguments (akin to argv). /// Params: buffer = String buffer. /// Returns: Argv-like array. -string[] arguments(string buffer) +string[] arguments(const(char)[] buffer) { import std.string : strip; import std.ascii : isControl, isWhite; @@ -38,7 +41,7 @@ break; } - results ~= buffer[start..(index++)]; + results ~= cast(string)buffer[start..(index++)]; break; default: for (start = index, ++index; index < buflen; ++index) @@ -48,15 +51,14 @@ break; } - results ~= buffer[start..index]; + results ~= cast(string)buffer[start..index]; } } return results; } - -/// -@system unittest { +@system unittest +{ //TODO: Test embedded string quotes assert(arguments("") == []); assert(arguments("\n") == []); @@ -74,4 +76,6 @@ assert(arguments(`a "b c" d`) == [ "a", "b c", "d" ]); assert(arguments(`/type 'yes string'`) == [ "/type", "yes string" ]); assert(arguments(`/type "yes string"`) == [ "/type", "yes string" ]); + assert(arguments(`A B`) == [ "A", "B" ]); + //assert(arguments(`tab\tmoment`) == [ "tab", "moment" ]); } diff --git a/src/utils/format.d b/src/utils/format.d deleted file mode 100644 index d96b382..0000000 --- a/src/utils/format.d +++ /dev/null @@ -1,83 +0,0 @@ -/// Bit utilities. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module utils.format; - -private enum : float { - // SI (base-10) - SI = 1000, /// SI base - kB = SI, /// Represents one KiloByte - MB = kB * SI, /// Represents one MegaByte - GB = MB * SI, /// Represents one GigaByte - TB = GB * SI, /// Represents one TeraByte - PB = TB * SI, /// Represents one PetaByte - EB = PB * SI, /// Represents one ExaByte - // IEC (base-2) - IEC = 1024, /// IEC base - KiB = IEC, /// Represents one KibiByte (1024) - MiB = KiB * IEC, /// Represents one MebiByte (1024²) - GiB = MiB * IEC, /// Represents one GibiByte (1024³) - TiB = GiB * IEC, /// Represents one TebiByte (1024⁴) - PiB = TiB * IEC, /// Represents one PebiByte (1024⁵) - EiB = PiB * IEC, /// Represents one PebiByte (1024⁶) -} - -/// Format byte size. -/// Params: -/// size = Binary number. -/// b10 = Use SI suffixes instead of IEC suffixes. -/// Returns: Character slice using sformat -const(char)[] formatBin(long size, bool b10 = false) -{ - import std.format : format; - - // NOTE: ulong.max = (2^64)-1 Bytes = 16 EiB - 1 = 16 * 1024⁵ - - //TODO: Pretty this up with some clever math - // Not great, not terrible... - - if (b10) // base 1000 - { - if (size >= EB) - return format("%0.2f EB", size / EB); - if (size >= PB) - return format("%0.2f TB", size / PB); - if (size >= TB) - return format("%0.2f TB", size / TB); - if (size >= GB) - return format("%0.2f GB", size / GB); - if (size >= MB) - return format("%0.1f MB", size / MB); - if (size >= kB) - return format("%0.1f kB", size / kB); - } - else // base 1024 - { - if (size >= EiB) - return format("%0.2f EiB", size / EiB); - if (size >= PiB) - return format("%0.2f TiB", size / PiB); - if (size >= TiB) - return format("%0.2f TiB", size / TiB); - if (size >= GiB) - return format("%0.2f GiB", size / GiB); - if (size >= MiB) - return format("%0.1f MiB", size / MiB); - if (size >= KiB) - return format("%0.1f KiB", size / KiB); - } - - return format("%u B", size); -} - -unittest { - assert(formatBin(0) == "0 B"); - assert(formatBin(1) == "1 B"); - assert(formatBin(1023) == "1023 B"); - assert(formatBin(1024) == "1.0 KiB"); - // Wouldn't this more exactly 7.99 EiB? Precision limitation? - assert(formatBin(long.max) == "8.00 EiB"); - assert(formatBin(999, true) == "999 B"); - assert(formatBin(1000, true) == "1.0 kB"); -} diff --git a/src/utils/math.d b/src/utils/math.d new file mode 100644 index 0000000..38539b7 --- /dev/null +++ b/src/utils/math.d @@ -0,0 +1,27 @@ +/// Mathematic utilities. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module utils.math; + +T min(T)(T a, T b) pure @safe +{ + return a < b ? a : b; +} +@safe unittest +{ + assert(min(1, 2) == 1); + assert(min(2, 2) == 2); + assert(min(3, 2) == 2); +} + +T max(T)(T a, T b) pure @safe +{ + return a > b ? a : b; +} +@safe unittest +{ + assert(max(1, 2) == 2); + assert(max(2, 2) == 2); + assert(max(3, 2) == 3); +} \ No newline at end of file diff --git a/src/utils/memory.d b/src/utils/memory.d index 876fb68..4ec5155 100644 --- a/src/utils/memory.d +++ b/src/utils/memory.d @@ -1,10 +1,15 @@ +/// Memory utilities. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) module utils.memory; import std.stdio : File; import std.container.array; //TODO: Use OutBuffer or Array!ubyte when writing changes? -struct MemoryStream { +struct MemoryStream +{ private ubyte[] buffer; private long position; @@ -19,16 +24,12 @@ err = false; } - // read file into memory - /*void open(string path) - { - }*/ - void open(ubyte[] data) + void copy(ubyte[] data) { buffer = new ubyte[data.length]; buffer[0..$] = data[0..$]; } - void open(File stream) + void copy(File stream) { buffer = buffer.init; //TODO: use OutBuffer+reserve (if possible to get filesize) @@ -36,7 +37,6 @@ { buffer ~= a; } - } long seek(long pos) diff --git a/src/utils/strings.d b/src/utils/strings.d new file mode 100644 index 0000000..72064cf --- /dev/null +++ b/src/utils/strings.d @@ -0,0 +1,135 @@ +/// Formatting utilities. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module utils.strings; + +import core.stdc.stdio : sscanf; +import core.stdc.string : memcpy; +import std.string : toStringz; +import std.conv : text; +import utils.math; + +private enum : double +{ + // SI (base-10) + SI = 1000, /// SI base + kB = SI, /// Represents one KiloByte (1000) + MB = kB * SI, /// Represents one MegaByte (1000²) + GB = MB * SI, /// Represents one GigaByte (1000³) + TB = GB * SI, /// Represents one TeraByte (1000⁴) + PB = TB * SI, /// Represents one PetaByte (1000⁵) + EB = PB * SI, /// Represents one ExaByte (1000⁶) + // IEC (base-2) + IEC = 1024, /// IEC base + KiB = IEC, /// Represents one KibiByte (1024) + MiB = KiB * IEC, /// Represents one MebiByte (1024²) + GiB = MiB * IEC, /// Represents one GibiByte (1024³) + TiB = GiB * IEC, /// Represents one TebiByte (1024⁴) + PiB = TiB * IEC, /// Represents one PebiByte (1024⁵) + EiB = PiB * IEC, /// Represents one PebiByte (1024⁶) +} + +/// Format byte size. +/// Params: +/// size = Binary number. +/// b10 = Use SI suffixes instead of IEC suffixes. +/// Returns: Character slice using sformat +const(char)[] formatBin(long size, bool b10 = false) @safe +{ + import std.format : format; + + // NOTE: ulong.max = (2^64)-1 Bytes = 16 EiB - 1 = 16 * 1024⁵ + + //TODO: Consider table+index + + static immutable string[] formatsIEC = [ + "%0.0f B", + "%0.1f KiB", + "%0.1f MiB", + "%0.2f GiB", + "%0.2f TiB", + "%0.2f PiB", + "%0.2f EiB", + ]; + static immutable string[] formatsSI = [ + "%0.0f B", + "%0.1f kB", + "%0.1f MB", + "%0.2f GB", + "%0.2f TB", + "%0.2f PB", + "%0.2f EB", + ]; + + size_t i; + double base = void; + + if (b10) // base 1000 + { + base = 1000.0; + if (size >= EB) i = 6; + else if (size >= PB) i = 5; + else if (size >= TB) i = 4; + else if (size >= GB) i = 3; + else if (size >= MB) i = 2; + else if (size >= kB) i = 1; + } + else // base 1024 + { + base = 1024.0; + if (size >= EiB) i = 6; + else if (size >= PiB) i = 5; + else if (size >= TiB) i = 4; + else if (size >= GiB) i = 3; + else if (size >= MiB) i = 2; + else if (size >= KiB) i = 1; + } + + return format(b10 ? formatsSI[i] : formatsIEC[i], size / (base ^^ i)); +} +@safe unittest +{ + assert(formatBin(0) == "0 B"); + assert(formatBin(1) == "1 B"); + assert(formatBin(1023) == "1023 B"); + assert(formatBin(1024) == "1.0 KiB"); + // Wouldn't this more exactly 7.99 EiB? Precision limitation? + assert(formatBin(long.max) == "8.00 EiB"); + assert(formatBin(999, true) == "999 B"); + assert(formatBin(1000, true) == "1.0 kB"); + assert(formatBin(1_000_000, true) == "1.0 MB"); +} + +long cparse(string arg) @trusted +{ + // NOTE: Since toStringz can allocate, and I use this function a lot, + // a regular static buffer is used instead. + enum BUFSZ = 64; + char[BUFSZ] buf = void; + size_t sz = min(arg.length+1, BUFSZ-1); // Account for null terminator + memcpy(buf.ptr, arg.ptr, sz); + buf[sz] = 0; + + long r = void; + if (sscanf(arg.toStringz, "%lli", &r) != 1) + throw new Exception("Could not parse: ".text(arg)); + return r; +} +@safe unittest +{ + import std.conv : octal; + assert(cparse("0") == 0); + assert(cparse("1") == 1); + assert(cparse("10") == 10); + assert(cparse("20") == 20); + assert(cparse("0x1") == 0x1); + assert(cparse("0x10") == 0x10); + assert(cparse("0x20") == 0x20); + // NOTE: Signed numbers cannot be over 0x8000_0000_0000_000 + assert(cparse("0x1bcd1234ffffaaaa") == 0x1bcd_1234_ffff_aaaa); + assert(cparse("0") == 0); + assert(cparse("01") == 1); + assert(cparse("010") == octal!"010"); + assert(cparse("020") == octal!"020"); +}