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..33e5c6a 100644 --- a/dub.sdl +++ b/dub.sdl @@ -1,22 +1,21 @@ 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" { +configuration "editor" { targetType "executable" - mainSourceFile "src/main.d" + mainSourceFile "editor/main.d" + sourcePaths "editor" + importPaths "editor" } -configuration "trace" { +configuration "dumper" { targetType "executable" - versions "Trace" - mainSourceFile "src/main.d" + targetName "ddhxdump" + mainSourceFile "dumper/main.d" + sourcePaths "dumper" + importPaths "dumper" } # @@ -27,7 +26,9 @@ dflags "-vgc" "-vtls" platform="dmd" dflags "--vgc" "--vtls" platform="ldc" } - +buildType "trace" { + versions "Trace" +} # # Tests diff --git a/dumper/app.d b/dumper/app.d new file mode 100644 index 0000000..65a7091 --- /dev/null +++ b/dumper/app.d @@ -0,0 +1,67 @@ +/// Simple dumper UI, no interactive input. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module dumper.app; + +import std.stdio; +import std.file; +import core.stdc.stdlib : malloc; +import ddhx.common; +import ddhx.transcoder; +import ddhx.utils.math; +import ddhx.os.terminal; +import ddhx.display; + +private enum CHUNKSIZE = 16 * 1024; + +// NOTE: if path is null, then stdin is used +int dump(string path) +{ + scope buffer = new ubyte[CHUNKSIZE]; + + // opAssign is bugged on ldc with optimizations + File file; + if (path) + { + file = File(path, "rb"); + + if (_opos) file.seek(_opos); + } + else + { + file = stdin; + + // Read blocks until length + if (_opos) + { + Lskip: + size_t rdsz = min(_opos, CHUNKSIZE); + if (file.rawRead(buffer[0..rdsz]).length == CHUNKSIZE) + goto Lskip; + } + } + + disp_init(false); + + BUFFER *dispbuf = disp_create(CHUNKSIZE / _ocolumns, _ocolumns, 0); + if (dispbuf == null) + { + stderr.writeln("error: Unknown error creating display"); + return 10; + } + + disp_header(_ocolumns); + long address; + foreach (chunk; file.byChunk(CHUNKSIZE)) + { + //disp_update(address, chunk, _ocolumns); + disp_render_buffer(dispbuf, address, chunk, _ocolumns, + _odatafmt, _oaddrfmt, _ofillchar, _ocharset, _oaddrpad, 1); + disp_print_buffer(dispbuf); + + address += chunk.length; + } + + return 0; +} diff --git a/dumper/main.d b/dumper/main.d new file mode 100644 index 0000000..8ebffec --- /dev/null +++ b/dumper/main.d @@ -0,0 +1,18 @@ +module dumper.main; + +import dumper.app; +import ddhx.common; +import ddhx.logger; + +private: + +int main(string[] args) +{ + args = commonopts(args); + + // If file not mentioned, app will assume stdin + string filename = args.length > 1 ? args[1] : null; + trace("filename=%s", filename); + + return dump(filename); +} \ No newline at end of file diff --git a/editor/app.d b/editor/app.d new file mode 100644 index 0000000..6e9ae1b --- /dev/null +++ b/editor/app.d @@ -0,0 +1,482 @@ +/// Main module, handling core TUI operations. +/// +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module editor.app; + +import std.stdio; +import std.string; +import core.stdc.stdlib; +import core.stdc.string; +import core.stdc.errno; +import ddhx.document; +import ddhx.display; +import ddhx.transcoder; +import ddhx.formatter; +import ddhx.logger; +import ddhx.os.terminal : Key, Mod; +import ddhx.common; + +// NOTE: Glossary +// Cursor +// Visible on-screen cursor, positioned on a per-byte and additionally +// per-digit basis when editing. +// View/Camera +// The "camera" that follows the cursor. Contains a read buffer that +// the document is read from, and is used for rendering. + +private enum // Update flags +{ + // 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, + + // Message was sent, clear later + UMESSAGE = 1 << 8, + + // + URESET = UCURSOR | UVIEW | UHEADER, +} + +//TODO: EditorConfig? + +private __gshared +{ + Document document; + + BUFFER *dispbuffer; + + /// Last read count in bytes, for limiting the cursor offsets + size_t _elrdsz; + + /// Camera buffer + void *_eviewbuffer; + /// Camera buffer size + size_t _eviewsize; + /// Position of the camera, in bytes + long _eviewpos; + + /// Position of the cursor in the file, in bytes + long _ecurpos; + /// Position of the cursor editing a group of bytes, in digits + int _edgtpos; // e.g., hex=nibble, dec=digit, etc. + /// Cursor edit mode (insert, overwrite, etc.) + int _emode; + /// + enum EDITBFSZ = 8; + /// Value of edit input, as a digit + char[EDITBFSZ] _ebuffer; + + /// System status, used in updating certains portions of the screen + int _estatus; +} + +void startEditor(Document doc) +{ + document = doc; + + // Init display in TUI mode + disp_init(true); + + setupscreen(); + +Lread: + cast(void)update(); + int key = disp_readkey(); + + switch (key) { + // 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; + + // Search + case Key.W | Mod.ctrl: + break; + + // Reset screen + case Key.R | Mod.ctrl: + setupscreen(); + break; + + // Quit + case Key.Q: + quit(); + break; + + default: + // Edit mode + if (_editkey(_odatafmt, key)) + { + // 1. Check if key can be inserted into group + // 2. + + //TODO: When group size filled, add to edit history + trace("EDIT key=%c", cast(char)key); + _ebuffer[_edgtpos++] = cast(ubyte)key; + _estatus |= UEDIT; + + /*if (_edgtpos >= _odigits) { + //TODO: add byte+address to edits + editpos = 0; + _move_rel(1); + }*/ + goto Lread; + } + } + goto Lread; +} + +// Setup screen and buffers +void setupscreen() +{ + int tcols = void, trows = void; + disp_size(tcols, trows); + trace("tcols=%d trows=%d", tcols, trows); + if (tcols < 20 || trows < 4) + { + stderr.writeln("error: Terminal too small, needs 20x4"); + exit(4); + } + + // Set number of columns, or automatically get column count + // from terminal. + _ocolumns = _ocolumns > 0 ? _ocolumns : optimalElemsPerRow(tcols, _oaddrpad); + trace("hintcols=%d", _ocolumns); + + // Get "view" buffer size, in bytes + _eviewsize = optimalCamSize(_ocolumns, tcols, trows, _odatafmt); + trace("readsize=%u", _eviewsize); + + // Create display buffer + dispbuffer = disp_create(trows - 2, _ocolumns, 0); + if (dispbuffer == null) + { + stderr.writeln("error: Unknown error creating display"); + exit(5); + } + trace("disprows=%d dispcols=%d", dispbuffer.rows, dispbuffer.columns); + + // Allocate read buffer + _eviewbuffer = malloc(_eviewsize); + if (_eviewbuffer == null) + { + stderr.writeln("error: ", fromStringz(strerror(errno))); + exit(6); + } + + // Initially render these things + _estatus = URESET; +} + +// Given desired bytes/elements per row (ucols) and terminal size, +// get optimal size for the view buffer +int optimalCamSize(int ucols, int tcols, int trows, int datafmt) +{ + return ucols * (trows - 2); +} +unittest +{ + assert(optimalCamSize(16, 80, 24, Format.hex) == 352); +} + +// Given number of terminal columns and the padding of the address field, +// get the optimal number of elements (bytes for now) per row to fit on screen +int optimalElemsPerRow(int tcols, int addrpad) +{ + FormatInfo info = formatInfo(_odatafmt); + return (tcols - addrpad) / (info.size1 + 1); // +space +} + +// Invoke command prompt +string prompt(string text) +{ + throw new Exception("Not implemented"); +} + +//TODO: Merge _editkey and _editval +// Could return a struct +private +int _editkey(int type, int key) +{ + switch (type) with (Format) + { + 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: + } + return 0; +} +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: + throw new Exception(__FUNCTION__); + } +} + +// Move the cursor relative to its position within the file +private +void moverel(long pos) +{ + if (pos == 0) + return; + + long tmp = _ecurpos + pos; + if (pos < 0 && tmp < 0) + tmp = 0; + + if (tmp == _ecurpos) + return; + + _ecurpos = tmp; + _estatus |= UCURSOR; + _adjust_viewpos(); +} + +// Move the cursor to an absolute file position +private +void moveabs(long pos) +{ + if (pos < 0) + pos = 0; + + if (pos == _ecurpos) + return; + + _ecurpos = pos; + _estatus |= UCURSOR; + _adjust_viewpos(); +} + +// Adjust the camera positon to the cursor +void _adjust_viewpos() +{ + //TODO: Adjust view position algorithmically + + // Cursor is ahead the view + if (_ecurpos >= _eviewpos + _eviewsize) + { + while (_ecurpos >= _eviewpos + _eviewsize) + { + _eviewpos += _ocolumns; + } + _estatus |= UVIEW; + } + // Cursor is behind the view + else if (_ecurpos < _eviewpos) + { + while (_ecurpos < _eviewpos) + { + _eviewpos -= _ocolumns; + if (_eviewpos <= 0) + break; + } + _estatus |= UVIEW; + } +} + +void move_left() +{ + if (_ecurpos == 0) + return; + + moverel(-1); +} +void move_right() +{ + moverel(1); +} +void move_up() +{ + if (_ecurpos == 0) + return; + + moverel(-_ocolumns); +} +void move_down() +{ + moverel(_ocolumns); +} +void move_pg_up() +{ + if (_ecurpos == 0) + return; + + moverel(-_eviewsize); +} +void move_pg_down() +{ + moverel(_eviewsize); +} +void move_ln_start() +{ + moverel(-_ecurpos % _ocolumns); +} +void move_ln_end() +{ + moverel((_ocolumns - (_ecurpos % _ocolumns)) - 1); +} +void move_abs_start() +{ + moveabs(0); +} +void move_abs_end() +{ + long size = document.size(); + if (size < 0) + message("Don't know end of document"); + moveabs(size); +} + +// Update all elements on screen depending on status +// status global indicates what needs to be updated +void update() +{ + // Update header + if (_estatus & UHEADER) + update_header(); + + // Update the screen + if (_estatus & UVIEW) + update_view(); + + // Update statusbar if no messages + if ((_estatus & UMESSAGE) == 0) + update_status(); + + // Update cursor + // NOTE: Should always be updated due to frequent movement + // That includes messages, cursor naviation, menu invokes, etc. + update_cursor(); + + // Clear all + _estatus = 0; +} + +void update_header() +{ + disp_cursor(0, 0); + disp_header(_ocolumns); +} + +// Adjust camera offset +void update_view() +{ + static long oldpos; + + // Seek to camera position and read + ubyte[] data = document.readAt(_eviewpos, _eviewbuffer, _eviewsize); + trace("_eviewpos=%d addr=%u data.length=%u _eviewbuffer=%s _eviewsize=%u", + _eviewpos, _eviewpos, data.length, _eviewbuffer, _eviewsize); + + // If unsuccessful, reset & ignore + if (data == null || data.length == 0) + { + _eviewpos = oldpos; + return; + } + + // Success + _elrdsz = data.length; + oldpos = _eviewpos; + + disp_render_buffer(dispbuffer, _eviewpos, data, + _ocolumns, Format.hex, Format.hex, _ofillchar, + _ocharset, _oaddrpad, 1); + + //TODO: Editor applies previous edits in BUFFER + //TODO: Editor applies current edit in BUFFER + + disp_cursor(1, 0); + disp_print_buffer(dispbuffer); +} + +// Adjust cursor position if outside bounds +void update_cursor() +{ + // If absolute cursor position is further than view pos + last read length + long avail = _eviewpos + _elrdsz; + if (_ecurpos > avail) + _ecurpos = avail; + + + // Cursor position in camera + long curview = _ecurpos - _eviewpos; + + // Get 2D coords + int elemsz = formatInfo(_odatafmt).size1 + 1; + int row = 1 + (cast(int)curview / _ocolumns); + int col = (_oaddrpad + 2 + ((cast(int)curview % _ocolumns) * elemsz)); + trace("_eviewpos=%d _ecurpos=%d _elrdsz=%d row=%d col=%d", _eviewpos, _ecurpos, _elrdsz, row, col); + disp_cursor(row, col); + + // Editing in progress + /*if (editbuf && editsz) + { + disp_write(editbuf, editsz); + }*/ +} + +void update_status() +{ + //TODO: check number of edits + enum STATBFSZ = 2 * 1024; + char[STATBFSZ] statbuf = void; + + FormatInfo finfo = formatInfo(_odatafmt); + string charset = charsetName(_ocharset); + + int statlen = snprintf(statbuf.ptr, STATBFSZ, "%.*s | %.*s", + cast(int)finfo.name.length, finfo.name.ptr, + cast(int)charset.length, charset.ptr); + disp_message(statbuf.ptr, statlen); +} + +void message(const(char)[] msg) +{ + disp_message(msg.ptr, msg.length); + _estatus |= UMESSAGE; +} + +void quit() +{ + //TODO: Ask confirmation + trace("quit"); + exit(0); +} \ No newline at end of file diff --git a/editor/edits.d b/editor/edits.d new file mode 100644 index 0000000..c9add0a --- /dev/null +++ b/editor/edits.d @@ -0,0 +1,55 @@ +/// Implements edits. +/// +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module edits; + +import std.container.slist; + +enum WriteMode +{ + readOnly, + insert, + overwrite +} + +/// Represents a single edit +struct Edit +{ + WriteMode mode; + long position; /// Absolute offset of edit + long value; /// + int size; /// Size of payload in bytes +} + +struct EditHistory +{ + size_t index; + size_t count; + SList!long history; + const(char)[] name = "ov"; + int status; + + bool dirty() + { + return status != 0; + } + + void markSave() + { + status = 1; + } + + void add(long value, long address, WriteMode mode) + { + + } + + void undo() + { + + } + + //Edit[] get(long low, long high) +} \ No newline at end of file diff --git a/editor/main.d b/editor/main.d new file mode 100644 index 0000000..c0446ff --- /dev/null +++ b/editor/main.d @@ -0,0 +1,36 @@ +module editor.main; + +import std.stdio; +import editor.app; +import ddhx.common; +import ddhx.logger; +import ddhx.document; + +private: + +int main(string[] args) +{ + args = commonopts(args); + + // If file not mentioned, app will assume stdin + string filename = args.length > 1 ? args[1] : null; + trace("filename=%s", filename); + + // TODO: Support streams (for editor, that's slurping all of stdin) + if (filename == null) + { + stderr.writeln("error: Filename required. No current support for streams."); + return 0; + } + + Document doc; + try doc.openFile(filename, _oreadonly); + catch (Exception ex) + { + stderr.writeln("error: ", ex.msg); + return 1; + } + + startEditor(doc); + return 0; +} \ No newline at end of file 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 deleted file mode 100644 index 7eb08e5..0000000 --- a/src/ddhx.d +++ /dev/null @@ -1,696 +0,0 @@ -/// Main application. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module ddhx; - -//TODO: When searching strings, maybe convert it to current charset? - -import gitinfo; -import os.terminal; -public import - editor, - searcher, - encoding, - error, - converter, - settings, - screen, - utils.args, - utils.format, - utils.memory; - -/// Copyright string -enum DDHX_COPYRIGHT = "Copyright (c) 2017-2022 dd86k "; -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) -{ - 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; - } - } - - if (editor.fileMode == FileMode.stream) - { - editor.slurp(skip); - } - - 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, "/"); - break; - case '?': - menu(null, "?"); - break; - case N: - next; - 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: - } - goto L_INPUT; -} - -void eventResize() -{ - screen.updateTermSize(); - refresh; // temp -} - -//TODO: revamp menu system -// char mode: character mode (':', '/', '?') -// string command: command shortcut (e.g., 'g' + ' ' default) -void menu(string prefix = ":", string prepend = null) -{ - import std.stdio : readln; - import std.string : chomp, strip; - - // clear bar and command prepend - screen.clearOffsetBar; - - string line = screen.prompt(prefix, prepend); - - scope (exit) - { - 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); - default: - } - - 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; - default: - return errorSet(ErrorCode.invalidCommand); - } -} - -// for more elaborate user inputs (commands invoke this) -// prompt="save as" -// "save as: " + user input -/*private -string input(string prompt, string include) -{ - 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"); - return; - } - - screen.renderEmpty(rows); -} - -void updateStatus() -{ - import std.format : format; - - long pos = editor.cursorTell; - char[12] offset = void; - size_t l = screen.offsetFormatter.offset(offset.ptr, pos); - - 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"); - return; - } - - //TODO: cursorTo - editor.seek(pos); - readRender; -} - -/// Parses the string as a long and navigates to the file location. -/// Includes offset checking (+/- notation). -/// Params: str = String as a number -void seek(string str) -{ - version (Trace) trace("str=%s", str); - - const char seekmode = str[0]; - if (seekmode == '+' || seekmode == '-') // relative input.position - { - str = str[1..$]; - } - long newPos = void; - if (convertToVal(newPos, str)) - { - screen.message("Could not parse number"); - return; - } - switch (seekmode) { - case '+': // Relative, add - safeSeek(editor.position + newPos); - break; - case '-': // Relative, substract - safeSeek(editor.position - newPos); - break; - default: // Absolute - if (newPos < 0) - { - screen.message("Range underflow: %d (0x%x)", newPos, newPos); - } - else if (newPos >= editor.fileSize - editor.readSize) - { - screen.message("Range overflow: %d (0x%x)", newPos, newPos); - } - else - { - seek(newPos); - } - } -} - -/// Goes to the specified position in the file. -/// Checks bounds and calls Goto. -/// Params: pos = New position -void safeSeek(long pos) -{ - version (Trace) trace("pos=%s", pos); - - long fsize = editor.fileSize; - - if (pos + editor.readSize >= fsize) - pos = fsize - editor.readSize; - else if (pos < 0) - pos = 0; - - editor.cursorGoto(pos); -} - -enum LAST_BUFFER_SIZE = 128; -__gshared ubyte[LAST_BUFFER_SIZE] lastItem; -__gshared size_t lastSize; -__gshared string lastType; -__gshared bool lastForward; -__gshared bool lastAvailable; - -/// Search last item. -/// Returns: Error code if set. -//TODO: I don't think the return code is ever used... -int next() -{ - if (lastAvailable == false) - { - return errorSet(ErrorCode.noLastItem); - } - - long pos = void; - int e = searchData(pos, lastItem.ptr, lastSize, lastForward); - - if (e) - { - 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; -} - -// Search data -int lookup(string type, string data, bool forward, bool save) -{ - void *p = void; - size_t len = void; - if (convertToRaw(p, len, data, type)) - return error.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; -} - -int skip(ubyte data) -{ - 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; -} - -/// Print file information -void printFileInfo() -{ - screen.message("%11s %s", - formatBin(editor.fileSize, setting.si), - editor.fileName); -} - -void exit() -{ - import core.stdc.stdlib : exit; - version (Trace) trace(); - 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/ddhx/common.d b/src/ddhx/common.d new file mode 100644 index 0000000..f8909e6 --- /dev/null +++ b/src/ddhx/common.d @@ -0,0 +1,171 @@ +module ddhx.common; + +import std.stdio, std.format, std.compiler; +import std.conv, std.getopt; +import core.stdc.stdlib : exit; +import ddhx.formatter : Format, selectFormat; +import ddhx.transcoder : CharacterSet, selectCharacterSet; +import ddhx.utils.strings : cparse; +import ddhx.logger; + +__gshared: + +debug enum TRACE = true; +else enum TRACE = false; + +/// 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. | + / \ | Can I help you with that? | + _ _ | [ Yes ] [ No ] [ Go away ] | + O O +-. .------------------------+ + || |/ |/ + | V | + \_/ +SECRET"; + +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; + +//TODO: Config struct + +/// +bool _otrace = TRACE; +/// Read-only buffer +bool _oreadonly; +/// Group size of one element in bytes +int _ogrpsize = 1; +/// Data formatting +int _odatafmt = Format.hex; +/// Address formatting +int _oaddrfmt = Format.hex; +/// Address space padding in digits +int _oaddrpad = 11; +/// Size of column (one row) in bytes +int _ocolumns = 16; +/// Character set +int _ocharset = CharacterSet.ascii; +/// +char _ofillchar = '.'; +/// Skip/seek position +long _opos; +/// Total length to read +long _olength; + +void cliPage(string key) +{ + final switch (key) { + case "ver": key = DDHX_VERSION; break; + case "version": key = PAGE_VERSION; break; + case "assistant": key = SECRET; break; + } + writeln(key); + exit(0); +} + +bool cliTestStdin(string path) +{ + return path == "-"; +} +long cliParsePos(string v) +{ + if (v[0] != '+') + throw new Exception(text("Missing '+' prefix to argument: ", v)); + + return cparse(v[1..$]); +} + +void cliOptColumn(string v) +{ + +} + +// TODO: Do not follow symlink +// TODO: --process - Memory edit by PID +string[] commonopts(string[] args) +{ + GetoptResult res = void; + try + { + res = args.getopt(config.caseSensitive, + "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) { + _oaddrfmt = selectFormat(fmt); + }, + "data", "Set data mode ('hex', 'dec', or 'oct')", + (string _, string fmt) { + _odatafmt = selectFormat(fmt); + }, + //"filler", "Set non-printable default character (default='.')", &cliOption, + "C|charset", "Set character translation (default=ascii)", + (string _, string charset) { + _ocharset = selectCharacterSet(charset); + }, + "r|readonly", "Open file in read-only editing mode", &_oreadonly, + // Editor input mode + // Application options + "s|seek", "Seek at position", + (string _, string len) { + _opos = 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 log file", &_otrace, + ); + } + catch (Exception ex) + { + stderr.writeln("error: ", ex.msg); + exit(1); + } + + if (res.helpWanted) + { + // Replace default help line + res.options[$-1].help = "Print this help screen and exit"; + + writeln("ddhx - Hex editor\n"~ + "USAGE\n"~ + " ddhx [FILE|-]\n"~ + " ddhx {-h|--help|--version|--ver]\n"~ + "\n"~ + "OPTIONS"); + foreach (opt; res.options[1..$]) with (opt) + { + if (optShort) + write(' ', optShort, ','); + else + write(" "); + writefln(" %-14s %s", optLong, help); + } + exit(0); + } + + if (_otrace) traceInit(); + trace("version=%s args=%u", DDHX_VERSION, args.length); + + return args; +} diff --git a/src/ddhx/display.d b/src/ddhx/display.d new file mode 100644 index 0000000..580194c --- /dev/null +++ b/src/ddhx/display.d @@ -0,0 +1,305 @@ +/// Handles complex terminal operations. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.display; + +import std.range : chunks; +import std.conv : text; +import core.stdc.string : memset, memcpy; +import core.stdc.stdlib : malloc, free; +import ddhx.os.terminal; +import ddhx.formatter; +import ddhx.transcoder; +import ddhx.utils.math; +import ddhx.logger; + +//TODO: cache rendered (data) lines +// dedicated char buffers + +// Display allocates buffer and returns it +// + +//TODO: ELEMENT +// union { long _; char[8] data; } +// int digits; + +struct LINE +{ + long base; + + int baselen; + char[24] basestr; + + int datalen; /// Number of characters printed for data section + char *data; + + int textlen; /// Number of characters printed for text section + char *text; +} + +struct BUFFER +{ + int flags; + + int rows; /// Initial requested row count + int columns; /// Initial requested column count + + int datacap; /// Data buffer size (capacity) + int textcap; /// Text buffer size (capacity) + + int lncount; /// Number of lines rendered (obviously not over rows count) + LINE* lines; +} + +void disp_init(bool tui) +{ + terminalInit(tui ? TermFeat.altScreen | TermFeat.inputSys : 0); +} + +BUFFER* disp_create(int rows, int cols, int flags) +{ + // Assuming max oct 377 + 1 space for one LINE of formatted data + int databfsz = cols * 4; + // Assuming max 3 bytes per UTF-8 character, no emoji support, for one LINE of text data + int textbfsz = cols * 3; + // + int linesz = cast(int)LINE.sizeof + databfsz + textbfsz; + // + int totalsz = cast(int)BUFFER.sizeof + (rows * linesz); + + trace("rows=%d cols=%d flags=%x dsz=%d tsz=%d lsz=%d total=%d", rows, cols, flags, databfsz, textbfsz, linesz, totalsz); + + BUFFER* buffer = cast(BUFFER*)malloc(totalsz); + if (buffer == null) + return buffer; + + // Layout: + // BUFFER struct + // LINE... structures + // Data and text... buffers per-LINE + + buffer.flags = flags; + buffer.rows = rows; + buffer.columns = cols; + buffer.datacap = databfsz; + buffer.textcap = textbfsz; + buffer.lines = cast(LINE*)(cast(void*)buffer + BUFFER.sizeof); + char* bp = cast(char*)buffer + BUFFER.sizeof + (cast(int)LINE.sizeof * rows); + for (int i; i < rows; ++i) + { + LINE *line = &buffer.lines[i]; + line.base = 0; + line.datalen = line.textlen = 0; + line.data = bp; + line.text = bp + databfsz; + bp += databfsz + textbfsz; + } + + return buffer; +} + +void disp_set_datafmt(int datafmt) +{ +} +void disp_set_addrfmt(int addrfmt) +{ +} +void disp_set_character(int charset) +{ +} + +int disp_readkey() +{ +Lread: + TermInput i = terminalRead(); + if (i.type != InputType.keyDown) goto Lread; + return i.key; +} + +void disp_cursor_enable(bool enabled) +{ + +} + +void disp_cursor(int row, int col) +{ + terminalPos(col, row); +} +void disp_write(char* stuff, size_t sz) +{ + terminalWrite(stuff, sz); +} +void disp_size(ref int cols, ref int rows) +{ + TerminalSize ts = terminalSize(); + rows = ts.rows; + cols = ts.columns; +} + +/// +void disp_header(int columns, + int addrfmt = Format.hex) +{ + enum BUFSZ = 2048; + __gshared char[BUFSZ] buffer; + + static immutable string prefix = "Offset("; + + FormatInfo finfo = formatInfo(addrfmt); + + memcpy(buffer.ptr, prefix.ptr, prefix.length); + memcpy(buffer.ptr + prefix.length, finfo.name.ptr, finfo.name.length); + + size_t i = prefix.length + 3; + buffer[i++] = ')'; + buffer[i++] = ' '; + + for (int col; col < columns; ++col) + { + buffer[i++] = ' '; + i += formatval(buffer.ptr + i, 24, finfo.size1, col, addrfmt); + } + + 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)); +} + +LINE* disp_find_line(long pos) +{ + return null; +} + +void disp_render_line(LINE *line, + long base, ubyte[] data, + int columns, + int datafmt, int addrfmt, + char defaultchar, int encoding, + int addrpad, int groupsize) +{ + // Prepare data formatting functions + FormatInfo info = formatInfo(datafmt); + int elemsz = info.size1; + + // Prepare transcoder + string function(ubyte) transcode = getTranscoder(encoding); + + line.base = base; + //line.baselen = cast(int)formataddr(line.basestr.ptr, base); + line.baselen = cast(int)formatval(line.basestr.ptr, 24, addrpad, base, addrfmt); + + // Insert data and text bytes + int di, ci, cnt; + foreach (u8; data) + { + ++cnt; // Number of bytes processed + + // Format data element into data buffer + line.data[di++] = ' '; + //TODO: Fix buffer length + di += formatval(line.data + di, 24, elemsz, u8, datafmt | F_ZEROPAD); + + // Transcode character and insert it into text buffer + immutable(char)[] units = transcode(u8); + if (units.length == 0) // No utf-8 codepoints, insert default char + { + line.text[ci++] = defaultchar; + continue; + } + foreach (codeunit; units) + line.text[ci++] = codeunit; + } + + // If row is incomplete, in-fill with spaces + if (cnt < columns) + { + // Remaining length in bytes + int rem = columns - cnt; + + // Fill empty data space and adjust data index + int datsz = rem * (elemsz + 1); + memset(line.data + di, ' ', datsz); di += datsz; + + // Fill empty text space and adjust text index + memset(line.text + ci, ' ', rem); ci += rem; + } + + line.datalen = di; + line.textlen = ci; +} + +void disp_render_buffer(BUFFER *buffer, + long base, ubyte[] data, + int columns, + int datafmt, int addrfmt, + char defaultchar, int textfmt, + int addrpad, int groupsize) +{ + assert(buffer); + assert(columns > 0); + assert(datafmt >= 0); + assert(addrfmt >= 0); + assert(defaultchar); + assert(textfmt >= 0); + assert(addrpad > 0); + assert(groupsize > 0); + + //TODO: Check columns vs. buffer's? + + // Render lines + int lncnt; + size_t lnidx; + foreach (chunk; chunks(data, columns)) + { + if (lnidx >= buffer.rows) + { + trace("Line index exceeded buffer row capacity (%d rows configured)", buffer.rows); + break; + } + + LINE *line = &buffer.lines[lnidx++]; + + disp_render_line(line, base, chunk, + columns, datafmt, addrfmt, + defaultchar, textfmt, + addrpad, groupsize); + + base += columns; + ++lncnt; + } + buffer.lncount = lncnt; +} + +void disp_print_buffer(BUFFER *buffer) +{ + assert(buffer, "Buffer pointer null"); + + for (int l; l < buffer.lncount; ++l) + disp_print_line(&buffer.lines[l]); +} + +void disp_print_line(LINE *line) +{ + assert(line, "Line pointer null"); + + static immutable string space = " "; + static immutable string newln = "\n"; + + terminalWrite(line.basestr.ptr, line.baselen); + terminalWrite(space.ptr, space.length - 1); + terminalWrite(line.data, line.datalen); + terminalWrite(space.ptr, space.length); + terminalWrite(line.text, line.textlen); + terminalWrite(newln.ptr, newln.length); +} diff --git a/src/ddhx/document.d b/src/ddhx/document.d new file mode 100644 index 0000000..46d8147 --- /dev/null +++ b/src/ddhx/document.d @@ -0,0 +1,74 @@ +/// Handles multiple types of +/// +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.document; + +import ddhx.os.file; +import ddhx.os.error; +import std.file; + +private enum DocType +{ + none, + disk, + process, +} + +struct Document +{ + private union + { + OSFile file; + } + + private int doctype; + + void openFile(string path, bool readOnly) + { + if (isDir(path)) + throw new Exception("Is a directory"); + int e = file.open(path, readOnly ? OFlags.read : OFlags.readWrite); + if (e) + throw new Exception(messageFromCode(e)); + doctype = DocType.disk; + } + + // + + //void openProcess(int pid, bool readOnly) + + ubyte[] read(void *buffer, size_t size) + { + switch (doctype) with (DocType) { + case disk: + return file.read(cast(ubyte*)buffer, size); + default: + throw new Exception("Not implemented: "~__FUNCTION__); + } + } + + ubyte[] readAt(long location, void *buffer, size_t size) + { + switch (doctype) with (DocType) { + case disk: + file.seek(Seek.start, location); + return file.read(cast(ubyte*)buffer, size); + default: + throw new Exception("Not implemented: "~__FUNCTION__); + } + } + + long size() + { + switch (doctype) with (DocType) { + case disk: + return file.size(); + case process: + return -1; + default: + throw new Exception("Not implemented: "~__FUNCTION__); + } + } +} \ No newline at end of file diff --git a/src/ddhx/formatter.d b/src/ddhx/formatter.d new file mode 100644 index 0000000..ba5eb31 --- /dev/null +++ b/src/ddhx/formatter.d @@ -0,0 +1,175 @@ +/// Handles data formatting. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.formatter; + +import core.stdc.string : memset; +import std.conv : text; + +// TODO: Add hex upper? +enum Format +{ + hex, + dec, + oct, +} + +int selectFormat(string format) +{ + switch (format) with (Format) { + case "hex": return hex; + case "dec": return dec; + case "oct": return oct; + default: + throw new Exception(text("Invalid format: ", format)); + } +} + +struct FormatInfo +{ + string name; + int size1; +} + +FormatInfo formatInfo(int format) +{ + switch (format) with (Format) { + case hex: return FormatInfo("hex", 2); + case dec: return FormatInfo("dec", 3); + case oct: return FormatInfo("oct", 3); + default: + throw new Exception(text("Invalid format: ", format)); + } +} + +enum +{ + // NOTE: lowest byte is format type + F_ZEROPAD = 0x100, + // Add "0x", "0", or nothing to format + //F_PREPEND = 0x200, +} + +int getdigit16lsb(ref long value) +{ + int ret = value & 0xf; + value >>= 4; + return ret; +} +unittest +{ + long test = 0x1234_5678_90ab_cdef; + assert(getdigit16lsb(test) == 0xf); + assert(getdigit16lsb(test) == 0xe); + assert(getdigit16lsb(test) == 0xd); + assert(getdigit16lsb(test) == 0xc); + assert(getdigit16lsb(test) == 0xb); + assert(getdigit16lsb(test) == 0xa); + assert(getdigit16lsb(test) == 0); + assert(getdigit16lsb(test) == 9); + assert(getdigit16lsb(test) == 8); + assert(getdigit16lsb(test) == 7); + assert(getdigit16lsb(test) == 6); + assert(getdigit16lsb(test) == 5); + assert(getdigit16lsb(test) == 4); + assert(getdigit16lsb(test) == 3); + assert(getdigit16lsb(test) == 2); + assert(getdigit16lsb(test) == 1); +} + +private immutable char[16] hexmap = "0123456789abcdef"; + +private +char formatdigit16(int digit) { return hexmap[digit]; } +unittest +{ + assert(formatdigit16(0) == '0'); + assert(formatdigit16(1) == '1'); + assert(formatdigit16(2) == '2'); + assert(formatdigit16(3) == '3'); + assert(formatdigit16(4) == '4'); + assert(formatdigit16(5) == '5'); + assert(formatdigit16(6) == '6'); + assert(formatdigit16(7) == '7'); + assert(formatdigit16(8) == '8'); + assert(formatdigit16(9) == '9'); + assert(formatdigit16(0xa) == 'a'); + assert(formatdigit16(0xb) == 'b'); + assert(formatdigit16(0xc) == 'c'); + assert(formatdigit16(0xd) == 'd'); + assert(formatdigit16(0xe) == 'e'); + assert(formatdigit16(0xf) == 'f'); +} + +// Input : 0x123 with 11 padding (size=char count, e.g., the num of characters to fulfill) +// Output: " 123" +size_t formatval(char *buffer, size_t buffersize, int width, long value, int options) +{ + assert(buffer); + assert(buffersize); + assert(width); + assert(width <= buffersize); + + // Setup + int format = options & 0xf; + int function(ref long) getdigit = void; + char function(int) formatdigit = void; + final switch (format) with (Format) { + case hex: + getdigit = &getdigit16lsb; + formatdigit = &formatdigit16; + break; + case dec: + case oct: assert(0, "implement"); + } + + // Example: 0x0000_0000_0000_1200 with a width of 10 characters + // Expected: " 1200" or + // "0000001200" + // 1. While formatting, record position of last non-zero 'digit' (one-based) + // 0x0000_0000_0000_1200 + // ^ + // i=4 + // 2. Substract width with position + // width(10) - i(4) = 6 Number of padding chars + // 3. Fill padding with the number of characters from string[0] + + int b; // Position of highest non-zero digit + int i; // Number of characters written + for (; i < width; ++i) + { + int digit = getdigit(value); + buffer[width - i - 1] = formatdigit16(digit); + if (digit) b = i; // Save position + } + + // If we want space padding and more than one characters written + if ((options & F_ZEROPAD) == 0 && i > 1) + memset(buffer, ' ', width - (b + 1)); + + return width; +} +unittest +{ + char[16] b; + + void testformat(string result, int size, long value, int options) + { + char[] t = b[0..formatval(b.ptr, 16, size, value, options)]; + //import std.stdio : writeln, stderr; + //writeln(`test: "`, result, `", result: "`, t, `"`); + if (t != result) + assert(false); + } + + testformat("0", 1, 0, Format.hex); + testformat("00", 2, 0, Format.hex | F_ZEROPAD); + testformat(" 0", 2, 0, Format.hex); + testformat("dd", 2, 0xdd, Format.hex); + testformat("c0ffee", 6, 0xc0ffee, Format.hex); + testformat(" c0ffee", 8, 0xc0ffee, Format.hex); + testformat("00c0ffee", 8, 0xc0ffee, Format.hex | F_ZEROPAD); + testformat(" 123", 11, 0x123, Format.hex); + testformat("00000000123", 11, 0x123, Format.hex | F_ZEROPAD); +} diff --git a/src/ddhx/logger.d b/src/ddhx/logger.d new file mode 100644 index 0000000..929716d --- /dev/null +++ b/src/ddhx/logger.d @@ -0,0 +1,30 @@ +/// Basic logger. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.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(string func = __FUNCTION__, A...)(string fmt, A args) +{ + if (tracing == false) + return; + + double ms = sw.peek().total!"msecs"() / 1_000.0; + tracefile.writef("[%08.3f] %s: ", ms, func); + tracefile.writefln(fmt, args); +} \ No newline at end of file diff --git a/src/ddhx/os/error.d b/src/ddhx/os/error.d new file mode 100644 index 0000000..7d5a93b --- /dev/null +++ b/src/ddhx/os/error.d @@ -0,0 +1,44 @@ +/// OS error handling. +/// +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.os.error; + +version (Windows) +{ + import core.sys.windows.winbase; + import std.format; +} +else +{ + import core.stdc.string; + import std.string; +} + +string messageFromCode(int code) +{ +version (Windows) +{ + // TODO: Get console codepage + enum BUFSZ = 1024; + __gshared char[BUFSZ] buffer; + uint len = FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_MAX_WIDTH_MASK, + null, + code, + 0, // Default + buffer.ptr, + BUFSZ, + null); + + if (len == 0) + return cast(string)sformat(buffer, "FormatMessageA returned code %#x", GetLastError()); + + return cast(string)buffer[0..len]; +} +else +{ + return fromStringz( strerror(code) ); +} +} \ No newline at end of file diff --git a/src/ddhx/os/file.d b/src/ddhx/os/file.d new file mode 100644 index 0000000..9554fae --- /dev/null +++ b/src/ddhx/os/file.d @@ -0,0 +1,283 @@ +/// File handling. +/// +/// This exists because 32-bit runtimes suffer from the 32-bit file size limit. +/// Despite the MS C runtime having _open and _fseeki64, the DM C runtime does +/// not have these functions. +/// +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.os.file; + +version (Windows) +{ + import core.sys.windows.winnt; + import core.sys.windows.winbase; + import std.utf : toUTF16z; + + private alias OSHANDLE = HANDLE; + private alias SEEK_SET = FILE_BEGIN; + private alias SEEK_CUR = FILE_CURRENT; + private alias SEEK_END = FILE_END; + + private enum OFLAG_OPENONLY = OPEN_EXISTING; +} +else version (Posix) +{ + import core.sys.posix.unistd; + import core.sys.posix.sys.types; + import core.sys.posix.sys.stat; + import core.sys.posix.fcntl; + import core.stdc.errno; + import core.stdc.stdio : SEEK_SET, SEEK_CUR, SEEK_END; + import std.string : toStringz; + + // BLKGETSIZE64 missing from dmd 2.098.1 and ldc 1.24.0 + // ldc 1.24 missing core.sys.linux.fs + // source musl 1.2.0 and glibc 2.25 has roughly same settings. + + private enum _IOC_NRBITS = 8; + private enum _IOC_TYPEBITS = 8; + private enum _IOC_SIZEBITS = 14; + private enum _IOC_NRSHIFT = 0; + private enum _IOC_TYPESHIFT = _IOC_NRSHIFT+_IOC_NRBITS; + private enum _IOC_SIZESHIFT = _IOC_TYPESHIFT+_IOC_TYPEBITS; + private enum _IOC_DIRSHIFT = _IOC_SIZESHIFT+_IOC_SIZEBITS; + private enum _IOC_READ = 2; + private enum _IOC(int dir,int type,int nr,size_t size) = + (dir << _IOC_DIRSHIFT) | + (type << _IOC_TYPESHIFT) | + (nr << _IOC_NRSHIFT) | + (size << _IOC_SIZESHIFT); + //TODO: _IOR!(0x12,114,size_t.sizeof) results in ulong.max + // I don't know why, so I'm casting it to int to let it compile. + // Fix later. + private enum _IOR(int type,int nr,size_t size) = + cast(int)_IOC!(_IOC_READ,type,nr,size); + + private enum BLKGETSIZE64 = cast(int)_IOR!(0x12,114,size_t.sizeof); + private alias BLOCKSIZE = BLKGETSIZE64; + + version (Android) + alias off_t = int; + else + alias off_t = long; + + private extern (C) int ioctl(int,off_t,...); + + private alias OSHANDLE = int; +} +else +{ + static assert(0, "Implement file I/O"); +} + +//TODO: FileType GetType(string) +// Pipe, device, etc. + +/// 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. +} + +/// Open file flags +enum OFlags +{ + exists = 1, /// File must exist. + read = 1 << 1, /// Read access. + write = 1 << 2, /// Write access. + readWrite = read | write, /// Read and write access. + share = 1 << 5, /// [TODO] Share file with read access to other programs. +} + +//TODO: Set file size (to extend or truncate file, allocate size) +// useful when writing all changes to file +// Win32: Seek + SetEndOfFile +// others: ftruncate +struct OSFile +{ + private OSHANDLE handle; + + //TODO: Share file. + // By default, at least on Windows, files aren't shared. Enabling + // sharing would allow refreshing view (manually) when a program + // writes to file. + int open(string path, int flags = OFlags.readWrite) + { + version (Windows) + { + uint dwCreation = flags & OFlags.exists ? OPEN_EXISTING : OPEN_ALWAYS; + + uint dwAccess; + if (flags & OFlags.read) dwAccess |= GENERIC_READ; + if (flags & OFlags.write) dwAccess |= GENERIC_WRITE; + + // NOTE: toUTF16z/tempCStringW + // Phobos internally uses tempCStringW from std.internal + // but I doubt it's meant for us to use so... + // Legacy baggage? + handle = CreateFileW( + path.toUTF16z, // lpFileName + dwAccess, // dwDesiredAccess + 0, // dwShareMode + null, // lpSecurityAttributes + dwCreation, // dwCreationDisposition + 0, // dwFlagsAndAttributes + null, // hTemplateFile + ); + return handle == INVALID_HANDLE_VALUE ? GetLastError : 0; + } + else version (Posix) + { + int oflags; + if ((flags & OFlags.exists) == 0) oflags |= O_CREAT; + if ((flags & OFlags.readWrite) == OFlags.readWrite) + oflags |= O_RDWR; + else if (flags & OFlags.write) + oflags |= O_WRONLY; + else if (flags & OFlags.read) + oflags |= O_RDONLY; + handle = .open(path.toStringz, oflags); + return handle < 0 ? errno : 0; + } + } + + int seek(Seek origin, long pos) + { + version (Windows) + { + LARGE_INTEGER i = void; + i.QuadPart = pos; + return SetFilePointerEx(handle, i, &i, origin) == FALSE ? + GetLastError() : 0; + } + else version (OSX) + { + // NOTE: Darwin has set off_t as long + // and doesn't have lseek64 + return lseek(handle, pos, origin) < 0 ? errno : 0; + } + else version (Posix) // Should cover glibc and musl + { + return lseek64(handle, pos, origin) < 0 ? errno : 0; + } + } + + long tell() + { + version (Windows) + { + LARGE_INTEGER i; // .init + SetFilePointerEx(handle, i, &i, FILE_CURRENT); + return i.QuadPart; + } + else version (OSX) + { + return lseek(handle, 0, SEEK_CUR); + } + else version (Posix) + { + return lseek64(handle, 0, SEEK_CUR); + } + } + + long size() + { + version (Windows) + { + LARGE_INTEGER li = void; + return GetFileSizeEx(handle, &li) ? li.QuadPart : -1; + } + else version (Posix) + { + // TODO: macOS + stat_t stats = void; + if (fstat(handle, &stats) == -1) + return -1; + // NOTE: fstat(2) sets st_size to 0 on block devices + switch (stats.st_mode & S_IFMT) { + case S_IFREG: // File + case S_IFLNK: // Link + return stats.st_size; + case S_IFBLK: // Block devices (like a disk) + //TODO: BSD variants + long s = void; + return ioctl(handle, BLOCKSIZE, &s) == -1 ? -1 : s; + default: + return -1; + } + } + } + + ubyte[] read(ubyte[] buffer) + { + return read(buffer.ptr, buffer.length); + } + + ubyte[] read(ubyte *buffer, size_t size) + { + version (Windows) + { + uint len = cast(uint)size; + if (ReadFile(handle, buffer, len, &len, null) == FALSE) + return null; + return buffer[0..len]; + } + else version (Posix) + { + ssize_t len = .read(handle, buffer, size); + if (len < 0) + return null; + return buffer[0..len]; + } + } + + size_t write(ubyte[] data) + { + return write(data.ptr, data.length); + } + + size_t write(ubyte *data, size_t size) + { + version (Windows) + { + uint len = cast(uint)size; + if (WriteFile(handle, data, len, &len, null) == FALSE) + return 0; + return len; // 0 on error anyway + } + else version (Posix) + { + ssize_t len = .write(handle, data, size); + err = len < 0; + return len < 0 ? 0 : len; + } + } + + void flush() + { + version (Windows) + { + FlushFileBuffers(handle); + } + else version (Posix) + { + .fsync(handle); + } + } + + void close() + { + version (Windows) + { + CloseHandle(handle); + } + else version (Posix) + { + .close(handle); + } + } +} diff --git a/src/ddhx/os/path.d b/src/ddhx/os/path.d new file mode 100644 index 0000000..0f7bb31 --- /dev/null +++ b/src/ddhx/os/path.d @@ -0,0 +1,156 @@ +/// OS path utilities. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.os.path; + +version (Windows) +{ + import core.sys.windows.windef : S_OK; + import core.sys.windows.shlobj : + CSIDL_PROFILE, CSIDL_LOCAL_APPDATA, CSIDL_APPDATA, + SHGetFolderPathA, SHGetFolderPathW; + import core.stdc.stdlib : malloc, free; + import core.stdc.wchar_ : wcslen; + import std.encoding : transcode; +} +else version (Posix) +{ + import core.sys.posix.unistd : getuid, uid_t; + import core.sys.posix.pwd : getpwuid, passwd; + import core.stdc.string : strlen; +} + +import std.process : environment; +import std.path : dirSeparator, buildPath; +import std.file : exists; + +// NOTE: As of Windows Vista, the SHGetSpecialFolderPathW function is a wrapper +// for SHGetKnownFolderPath. The latter not defined in the shlobj module. + +/// Get the path to the current user's home folder. +/// This does not verify if the path exists. +/// Windows: Typically C:\\Users\\%USERNAME% +/// Posix: Typically /home/$USERNAME +/// Returns: Path or null on failure. +string getHomeFolder() +{ + version (Windows) + { + // 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) + { + string path; + transcode(buffer[0..wcslen(buffer)], path); + free(buffer); // since transcode allocated + return path; + } + free(buffer); + } + else version (Posix) + { + // 1. $HOME + if ("HOME" in environment) + return environment["HOME"]; + // 2. getpwuid+getuid + uid_t uid = getuid(); + if (uid >= 0) + { + passwd *wd = getpwuid(uid); + if (wd) + { + return cast(immutable(char)[]) + wd.pw_dir[0..strlen(wd.pw_dir)]; + } + } + } + + return null; +} + +/// Get the path to the current user data folder. +/// This does not verify if the path exists. +/// Windows: Typically C:\\Users\\%USERNAME%\\AppData\\Local +/// Posix: Typically /home/$USERNAME/.config +/// Returns: Path or null on failure. +string getUserConfigFolder() +{ + version (Windows) + { + // 1. %LOCALAPPDATA% + if ("LOCALAPPDATA" in environment) + return environment["LOCALAPPDATA"]; + // 2. SHGetFolderPath + wchar *buffer = cast(wchar*)malloc(1024); + if (SHGetFolderPathW(null, CSIDL_LOCAL_APPDATA, null, 0, buffer) == S_OK) + { + string path; + transcode(buffer[0..wcslen(buffer)], path); + free(buffer); // transcode allocates + return path; + } + free(buffer); + } + else version (Posix) + { + if (const(string) *xdg_config_home = "XDG_CONFIG_HOME" in environment) + return *xdg_config_home; + } + + // Fallback + + string base = getHomeFolder; + + if (base is null) + return null; + + version (Windows) + { + return buildPath(base, "AppData", "Local"); + } + else version (Posix) + { + return buildPath(base, ".config"); + } +} + +/// Build the path for a given file name with the user home folder. +/// This does not verify if the path exists. +/// Windows: Typically C:\\Users\\%USERNAME%\\{filename} +/// Posix: Typically /home/$USERNAME/{filename} +/// Params: filename = Name of a file. +/// Returns: Path or null on failure. +string buildUserFile(string filename) +{ + string base = getHomeFolder; + + if (base is null) + return null; + + return buildPath(base, filename); +} + +/// Build the path for a given file name and parent folder with the user data folder. +/// This does not verify if the path exists. +/// Windows: Typically C:\\Users\\%USERNAME%\\AppData\\Roaming\\{appname}\\{filename} +/// Posix: Typically /home/$USERNAME/.config/{appname}/{filename} +/// Params: +/// appname = Name of the app. This acts as the parent folder. +/// filename = Name of a file. +/// Returns: Path or null on failure. +string buildUserAppFile(string appname, string filename) +{ + string base = getUserConfigFolder; + + if (base is null) + return null; + + return buildPath(base, appname, filename); +} \ No newline at end of file diff --git a/src/ddhx/os/terminal.d b/src/ddhx/os/terminal.d new file mode 100644 index 0000000..4d7730f --- /dev/null +++ b/src/ddhx/os/terminal.d @@ -0,0 +1,990 @@ +/// Terminal/console handling. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.os.terminal; + +//TODO: readline +// automatically pause input, stdio.readln, resume input +//TODO: Switch key input depending on $TERM +// 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 import std.stdio : _IONBF, _IOLBF, _IOFBF, stdin, stdout; +private import core.stdc.stdlib : system, atexit; +version (unittest) +{ + private import core.stdc.stdio : printf; + private extern (C) int putchar(int); +} + +version (Windows) +{ + 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 DWORD oldCP; + private __gshared ushort oldAttr; +} +else version (Posix) +{ + 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; + + // Bionic depends on the Linux system it's compiled on. + // But Glibc and Musl have the same settings, so does Bionic. + // ...And uClibc, at least on Linux. + // D are missing the following bindings. + version (CRuntime_Musl) + version = IncludeTermiosLinux; + version (CRuntime_Bionic) + version = IncludeTermiosLinux; + version (CRuntime_UClibc) + version = IncludeTermiosLinux; + + version (IncludeTermiosLinux) + { + //siginfo_t + // termios.h, bits/termios.h + private alias uint tcflag_t; + private alias uint speed_t; + private alias char cc_t; + private enum TCSANOW = 0; + private enum NCCS = 32; + private enum ICANON = 2; + private enum ECHO = 10; + private enum BRKINT = 2; + private enum INPCK = 20; + private enum ISTRIP = 40; + private enum ICRNL = 400; + private enum IXON = 2000; + private enum IEXTEN = 100000; + private enum CS8 = 60; + private enum TCSAFLUSH = 2; + private struct termios { + tcflag_t c_iflag; + tcflag_t c_oflag; + tcflag_t c_cflag; + tcflag_t c_lflag; + cc_t c_line; + cc_t[NCCS] c_cc; + speed_t __c_ispeed; + speed_t __c_ospeed; + } + private extern (C) int tcgetattr(int fd, termios *termios_p); + private extern (C) int tcsetattr(int fd, int a, termios *termios_p); + // ioctl.h + private enum TIOCGWINSZ = 0x5413; + private struct winsize { + ushort ws_row; + ushort ws_col; + ushort ws_xpixel; + ushort ws_ypixel; + } + private extern (C) int ioctl(int fd, ulong request, ...); + } + + struct KeyInfo { + string text; + 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 }, + { "\033[2~", Key.Insert }, + { "\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[5~", Key.PageUp }, + { "\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 }, + ]; + + private __gshared termios old_ios, new_ios; +} + +/// Flags for terminalInit. +//TODO: captureCtrlC: Block CTRL+C +enum TermFeat : ushort { + /// Initiate only the basic. + none = 0, + /// Initiate the input system. + inputSys = 1, + /// Initiate the alternative screen buffer. + altScreen = 1 << 1, + /// Initiate everything. + all = 0xffff, +} + +private __gshared int current_features; + +/// Initiate terminal. +/// Params: features = Feature bits to initiate. +/// Throws: (Windows) WindowsException on OS exception +void terminalInit(int features) +{ + current_features = features; + + version (Windows) + { + CONSOLE_SCREEN_BUFFER_INFO csbi = void; + + if (features & TermFeat.inputSys) + { + //NOTE: Re-opening stdin before new screen fixes quite a few things + // - usage with CreateConsoleScreenBuffer + // - readln (for menu) + // - receiving key input when stdin was used for reading a buffer + hIn = CreateFileA("CONIN$", GENERIC_READ, 0, null, OPEN_EXISTING, 0, null); + if (hIn == INVALID_HANDLE_VALUE) + throw new WindowsException(GetLastError); + SetConsoleMode(hIn, ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT); + stdin.windowsHandleOpen(hIn, "r"); + SetStdHandle(STD_INPUT_HANDLE, hIn); + } + else + { + hIn = GetStdHandle(STD_INPUT_HANDLE); + } + + if (features & TermFeat.altScreen) + { + // + // Setting up stdout + // + + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hIn == INVALID_HANDLE_VALUE) + throw new WindowsException(GetLastError); + + if (GetConsoleScreenBufferInfo(hOut, &csbi) == FALSE) + throw new WindowsException(GetLastError); + + DWORD attr = void; + if (GetConsoleMode(hOut, &attr) == FALSE) + throw new WindowsException(GetLastError); + + hOut = CreateConsoleScreenBuffer( + GENERIC_READ | GENERIC_WRITE, // dwDesiredAccess + FILE_SHARE_READ | FILE_SHARE_WRITE, // dwShareMode + null, // lpSecurityAttributes + CONSOLE_TEXTMODE_BUFFER, // dwFlags + null, // lpScreenBufferData + ); + if (hOut == INVALID_HANDLE_VALUE) + throw new WindowsException(GetLastError); + + stdout.flush; + stdout.windowsHandleOpen(hOut, "wb"); // fixes using write functions + + SetStdHandle(STD_OUTPUT_HANDLE, hOut); + SetConsoleScreenBufferSize(hOut, csbi.dwSize); + SetConsoleMode(hOut, attr | ENABLE_PROCESSED_OUTPUT); + + if (SetConsoleActiveScreenBuffer(hOut) == FALSE) + throw new WindowsException(GetLastError); + } + else + { + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + } + + stdout.setvbuf(0, _IONBF); // fixes weird cursor positions with alt buffer + + // NOTE: While Windows supports UTF-16LE (1200) and UTF-32LE, + // it's only for "managed applications" (.NET). + // LINK: https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers + oldCP = GetConsoleOutputCP(); + if (SetConsoleOutputCP(CP_UTF8) == FALSE) + throw new WindowsException(GetLastError); + + //TODO: Get active (or default) colors + GetConsoleScreenBufferInfo(hOut, &csbi); + oldAttr = csbi.wAttributes; + } + else version (Posix) + { + stdout.setvbuf(0, _IONBF); + if (features & TermFeat.inputSys) + { + // Should it re-open tty by default? + stat_t s = void; + fstat(STDIN_FILENO, &s); + if (S_ISFIFO(s.st_mode)) + stdin.reopen("/dev/tty", "r"); + tcgetattr(STDIN_FILENO, &old_ios); + new_ios = old_ios; + // NOTE: input modes + // - IXON enables ^S and ^Q + // - ICRNL enables ^M + // - BRKINT causes SIGINT (^C) on break conditions + // - INPCK enables parity checking + // - ISTRIP strips the 8th bit + new_ios.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP); + // NOTE: output modes + // - OPOST turns on output post-processing + //new_ios.c_oflag &= ~(OPOST); + // NOTE: local modes + // - ICANON turns on canonical mode (per-line instead of per-byte) + // - ECHO turns on character echo + // - ISIG enables ^C and ^Z signals + // - IEXTEN enables ^V + new_ios.c_lflag &= ~(ICANON | ECHO | IEXTEN); + // NOTE: control modes + // - CS8 sets Character Size to 8-bit + new_ios.c_cflag |= CS8; + // minimum amount of bytes to read, + // 0 being return as soon as there is data + //new_ios.c_cc[VMIN] = 0; + // maximum amount of time to wait for input, + // 1 being 1/10 of a second (100 milliseconds) + //new_ios.c_cc[VTIME] = 0; + terminalResumeInput; + } + + if (features & TermFeat.altScreen) + { + // change to alternative screen buffer + stdout.write("\033[?1049h"); + } + } + + atexit(&terminalQuit); +} + +private extern (C) +void terminalQuit() +{ + 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) + terminalOut("\033[?1049l"); + terminalShowCursor; + } + if (current_features & TermFeat.inputSys) + terminalPauseInput; +} + +private __gshared void function() terminalOnResizeEvent; + +void terminalOnResize(void function() func) +{ + version (Posix) + { + sigaction_t sa = void; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = &terminalResized; + assert(sigaction(SIGWINCH, &sa, NULL_SIGACTION) != -1); + } + terminalOnResizeEvent = func; +} + +version (Posix) +private extern (C) +void terminalResized(int signo, siginfo_t *info, void *content) +{ + if (terminalOnResizeEvent) + terminalOnResizeEvent(); +} + +void terminalPauseInput() +{ + version (Posix) + tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_ios); +} + +void terminalResumeInput() +{ + version (Posix) + tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_ios); +} + +/// Clear screen +void terminalClear() +{ + version (Windows) + { + CONSOLE_SCREEN_BUFFER_INFO csbi = void; + COORD c; + GetConsoleScreenBufferInfo(hOut, &csbi); + const int size = csbi.dwSize.X * csbi.dwSize.Y; + DWORD num; + if (FillConsoleOutputCharacterA(hOut, ' ', size, c, &num) == 0 + /*|| + FillConsoleOutputAttribute(hOut, csbi.wAttributes, size, c, &num) == 0*/) + { + terminalPos(0, 0); + } + else // If that fails, run cls. + system("cls"); + } + else version (Posix) + { + // \033c is a Reset + // \033[2J is "Erase whole display" + terminalOut("\033[2J"); + } else static assert(0, "Clear: Not implemented"); +} + +/// Get terminal window size in characters. +/// Returns: Size +TerminalSize terminalSize() +{ + TerminalSize size = void; + version (Windows) + { + CONSOLE_SCREEN_BUFFER_INFO c = void; + GetConsoleScreenBufferInfo(hOut, &c); + size.rows = c.srWindow.Bottom - c.srWindow.Top + 1; + size.columns = c.srWindow.Right - c.srWindow.Left + 1; + } + else version (Posix) + { + //TODO: Consider using LINES and COLUMNS environment variables + // as fallback if ioctl returns -1. + //TODO: Consider ESC [ 18 t for fallback of environment. + // Reply: ESC [ 8 ; ROWS ; COLUMNS t + winsize ws = void; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + size.rows = ws.ws_row; + size.columns = ws.ws_col; + } else static assert(0, "terminalSize: Not implemented"); + return size; +} + +/// Set cursor position x and y position respectively from the top left corner, +/// 0-based. +/// Params: +/// x = X position (horizontal) +/// y = Y position (vertical) +void terminalPos(int x, int y) +{ + version (Windows) // 0-based + { + COORD c = void; + c.X = cast(short)x; + c.Y = cast(short)y; + SetConsoleCursorPosition(hOut, c); + } + else version (Posix) // 1-based, so 0,0 needs to be output as 1,1 + { + char[16] b = void; + int r = snprintf(b.ptr, 16, "\033[%d;%dH", ++y, ++x); + assert(r > 0); + terminalOut(b.ptr, r); + } +} + +/// Hide the terminal cursor. +void terminalHideCursor() +{ + version (Windows) + { + CONSOLE_CURSOR_INFO cci = void; + GetConsoleCursorInfo(hOut, &cci); + cci.bVisible = FALSE; + SetConsoleCursorInfo(hOut, &cci); + } + else version (Posix) + { + terminalOut("\033[?25l"); + } +} +/// Show the terminal cursor. +void terminalShowCursor() +{ + version (Windows) + { + CONSOLE_CURSOR_INFO cci = void; + GetConsoleCursorInfo(hOut, &cci); + cci.bVisible = TRUE; + SetConsoleCursorInfo(hOut, &cci); + } + else version (Posix) + { + terminalOut("\033[?25h"); + } +} + +void terminalHighlight() +{ + version (Windows) + { + SetConsoleTextAttribute(hOut, oldAttr | BACKGROUND_RED); + } + else version (Posix) + { + terminalOut("\033[41m"); + } +} +/// Invert color. +void terminalInvertColor() +{ + version (Windows) + { + SetConsoleTextAttribute(hOut, oldAttr | COMMON_LVB_REVERSE_VIDEO); + } + else version (Posix) + { + terminalOut("\033[7m"); + } +} +/// Underline. +/// Bugs: Does not work on Windows Terminal. See https://github.com/microsoft/terminal/issues/8037 +void terminalUnderline() +{ + version (Windows) + { + SetConsoleTextAttribute(hOut, oldAttr | COMMON_LVB_UNDERSCORE); + } + else version (Posix) + { + terminalOut("\033[4m"); + } +} +/// Reset color. +void terminalResetColor() +{ + version (Windows) + { + SetConsoleTextAttribute(hOut, oldAttr); + } + else version (Posix) + { + terminalWrite("\033[0m"); + } +} + +size_t terminalWrite(const(void)[] data) +{ + return terminalWrite(data.ptr, data.length); +} + +/// Directly write to output. +/// Params: +/// data = Character data. +/// size = Amount in bytes. +/// Returns: Number of bytes written. +size_t terminalWrite(const(void) *data, size_t size) +{ + version (Windows) + { + uint r = void; + assert(WriteFile(hOut, data, cast(uint)size, &r, null)); + return r; + } + else version (Posix) + { + ssize_t r = write(STDOUT_FILENO, data, size); + assert(r >= 0); + return r; + } +} + +// 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; +Lread: + if (ReadConsoleInputA(hIn, &ir, 1, &num) == 0) + throw new WindowsException(GetLastError); + + if (num == 0) + goto Lread; + + switch (ir.EventType) { + case KEY_EVENT: + if (ir.KeyEvent.bKeyDown == FALSE) + goto Lread; + + version (unittest) + { + printf( + "KeyEvent: AsciiChar=%d wVirtualKeyCode=%d dwControlKeyState=%x\n", + ir.KeyEvent.AsciiChar, + ir.KeyEvent.wVirtualKeyCode, + ir.KeyEvent.dwControlKeyState, + ); + } + + const ushort keycode = ir.KeyEvent.wVirtualKeyCode; + + // Filter out single modifier key events + switch (keycode) { + case 16, 17, 18: goto Lread; // shift,ctrl,alt + default: + } + + event.type = InputType.keyDown; + + const char ascii = ir.KeyEvent.AsciiChar; + + if (ascii >= 'a' && ascii <= 'z') + { + event.key = ascii - 32; + return event; + } + else if (ascii >= 0x20 && ascii < 0x7f) + { + event.key = ascii; + + // '?' on a fr-ca kb is technically shift+6, + // breaking app input since expecting no modifiers + if (ascii >= 'A' && ascii <= 'Z') + event.key = Mod.shift; + + return event; + } + + event.key = keycode; + const DWORD state = ir.KeyEvent.dwControlKeyState; + 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 event; + /*case MOUSE_EVENT: + if (ir.MouseEvent.dwEventFlags & MOUSE_WHEELED) + { + // Up=0x00780000 Down=0xFF880000 + event.type = ir.MouseEvent.dwButtonState > 0xFF_0000 ? + Mouse.ScrollDown : Mouse.ScrollUp; + }*/ + case WINDOW_BUFFER_SIZE_EVENT: + if (terminalOnResizeEvent) + terminalOnResizeEvent(); + goto Lread; + default: goto Lread; + } + } + else version (Posix) + { + //TODO: Mouse reporting in Posix terminals + // * X10 compatbility mode (mouse-down only) + // Enable: ESC [ ? 9 h + // Disable: ESC [ ? 9 l + // "sends ESC [ M bxy (6 characters)" + // - ESC [ M button column row (1-based) + // - 0,0 click: ESC [ M ! ! + // ! is 0x21, so '!' - 0x21 = 0 + // - end,end click: ESC [ M q ; + // q is 0x71, so 'q' - 0x21 = 0x50 (column 80) + // ; is 0x3b, so ';' - 0x21 = 0x1a (row 26) + // - button left: ' ' + // - button right: '"' + // - button middle: '!' + // * Normal tracking mode + // Enable: ESC [ ? 1000 h + // Disable: ESC [ ? 1000 l + // b bits[1:0] 0=MB1 pressed, 1=MB2 pressed, 2=MB3 pressed, 3=release + // b bits[7:2] 4=Shift (bit 3), 8=Meta (bit 4), 16=Control (bit 5) + //TODO: Faster scanning + // So we have a few choices: + // - string table (current, works alright) + // - string[string] + // - needs active init though + // - string decoding (slower?) + // [ -> escape + // 1;2 -> shift (optional) + // B -> right arrow + // - template char[8] to long + // - very cursed + // - screwed if there are keys more than 8 bytes + // - template should do endianness + // - Manually hash it + // - Allows static arrays + // - std.digest.murmurhash already available + + enum BLEN = 8; + char[BLEN] b = void; + 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"); + goto Lread; + case 0: // How even + version (unittest) printf("stdin: empty\n"); + goto Lread; + case 1: + char c = b[0]; + 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 event; + case 13: + event.key = Key.Enter; + return event; + case 8, 127: // ^H + event.key = Key.Backspace; + return event; + default: + } + if (c >= 'a' && c <= 'z') + event.key = cast(ushort)(c - 32); + else if (c >= 'A' && c <= 'Z') + event.key = c | Mod.shift; + else if (c < 32) + event.key = (c + 64) | Mod.ctrl; + else + event.key = c; + return; + default: + } + + version (unittest) + { + printf("stdin:"); + for (size_t i; i < r; ++i) + { + char c = b[i]; + if (c < 32 || c > 126) + printf(" \\0%o", c); + else + putchar(b[i]); + } + putchar('\n'); + stdout.flush(); + } + + // Make a slice of misc. input. + const(char)[] inputString = b[0..r]; + + //TODO: Checking for mouse inputs + // Starts with \033[M + + // Checking for other key inputs + foreach (ki; keyInputsVTE) + { + if (r != ki.text.length) continue; + if (inputString != ki.text) continue; + event.key = ki.value; + return event; + } + + // Matched to nothing + goto Lread; + } // version posix +} + + +/// Terminal input type. +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 << 24, + shift = 1 << 25, + alt = 1 << 26, +} + +/// Key codes map. +//TODO: Consider mapping these to os/ascii-specific +enum Key +{ + Undefined = 0, + Backspace = 8, + Tab = 9, + Clear = 12, + Enter = 13, + Pause = 19, + Escape = 27, + Spacebar = 32, + PageUp = 33, + PageDown = 34, + End = 35, + Home = 36, + LeftArrow = 37, + UpArrow = 38, + RightArrow = 39, + DownArrow = 40, + Select = 41, + Print = 42, + Execute = 43, + PrintScreen = 44, + Insert = 45, + Delete = 46, + Help = 47, + D0 = 48, + D1 = 49, + D2 = 50, + D3 = 51, + D4 = 52, + D5 = 53, + D6 = 54, + D7 = 55, + D8 = 56, + D9 = 57, + Colon = 58, + SemiColon = 59, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LeftMeta = 91, + RightMeta = 92, + Applications = 93, + Sleep = 95, + NumPad0 = 96, + NumPad1 = 97, + NumPad2 = 98, + NumPad3 = 99, + NumPad4 = 100, + NumPad5 = 101, + NumPad6 = 102, + NumPad7 = 103, + NumPad8 = 104, + NumPad9 = 105, + Multiply = 106, + Add = 107, + Separator = 108, + Subtract = 109, + Decimal = 110, + Divide = 111, + F1 = 112, + F2 = 113, + F3 = 114, + F4 = 115, + F5 = 116, + F6 = 117, + F7 = 118, + F8 = 119, + F9 = 120, + F10 = 121, + F11 = 122, + F12 = 123, + F13 = 124, + F14 = 125, + F15 = 126, + F16 = 127, + F17 = 128, + F18 = 129, + F19 = 130, + F20 = 131, + F21 = 132, + F22 = 133, + F23 = 134, + F24 = 135, + BrowserBack = 166, + BrowserForward = 167, + BrowserRefresh = 168, + BrowserStop = 169, + BrowserSearch = 170, + BrowserFavorites = 171, + BrowserHome = 172, + VolumeMute = 173, + VolumeDown = 174, + VolumeUp = 175, + MediaNext = 176, + MediaPrevious = 177, + MediaStop = 178, + MediaPlay = 179, + LaunchMail = 180, + LaunchMediaSelect = 181, + LaunchApp1 = 182, + LaunchApp2 = 183, + Oem1 = 186, + OemPlus = 187, + OemComma = 188, + OemMinus = 189, + OemPeriod = 190, + Oem2 = 191, + Oem3 = 192, + Oem4 = 219, + Oem5 = 220, + Oem6 = 221, + Oem7 = 222, + Oem8 = 223, + Oem102 = 226, + Process = 229, + Packet = 231, + Attention = 246, + CrSel = 247, + ExSel = 248, + EraseEndOfFile = 249, + Play = 250, + Zoom = 251, + NoName = 252, + Pa1 = 253, + OemClear = 254 +} + +/// Terminal input structure +struct TermInput +{ + union + { + int key; /// Keyboard input with possible Mod flags. + struct + { + ushort mouseX; /// Mouse column coord + ushort mouseY; /// Mouse row coord + } + } + int type; /// Terminal input event type +} + +/// Terminal size structure +struct TerminalSize +{ + /// Terminal width in character columns + int columns; + /// Terminal height in character rows + int rows; +} diff --git a/src/ddhx/transcoder.d b/src/ddhx/transcoder.d new file mode 100644 index 0000000..f2d6e21 --- /dev/null +++ b/src/ddhx/transcoder.d @@ -0,0 +1,239 @@ +/// Transcoder module. +/// +/// Translates single-byte characters into utf-8 strings. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.transcoder; + +import std.encoding : codeUnits, CodeUnits; +import std.conv : text; + +/// Character set. +enum CharacterSet +{ + ascii, /// 7-bit US-ASCII + cp437, /// IBM PC CP-437 + ebcdic, /// IBM EBCDIC Code Page 37 (CCSID 37) + mac, /// Mac OS Roman (Windows 10000) + //t61, /// ITU T.61 + //gsm, /// GSM 03.38 +} + +int selectCharacterSet(string charset) +{ + switch (charset) with (CharacterSet) + { + case "ascii": return ascii; + case "cp437": return cp437; + case "ebcdic": return ebcdic; + case "mac": return mac; + default: throw new Exception(text("Invalid charset: ", charset)); + } +} + +string charsetName(int charset) +{ + switch (charset) with (CharacterSet) + { + case ascii: return "ascii"; + case cp437: return "cp437"; + case ebcdic: return "ebcdic"; + case mac: return "mac"; + 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); + } +} + +string function(ubyte) getTranscoder(int charset) +{ + switch (charset) with (CharacterSet) { + case ascii: return &transcodeASCII; + case cp437: return &transcodeCP437; + case ebcdic: return &transcodeEBCDIC; + case mac: return &transcodeMac; + default: assert(false); + } +} + +//TODO: Consider registering encoders to EncodingScheme +// to transcode to other charsets other than UTF-8 +//TODO: Translation function could return something specific if it needs another byte +// With static param, internally cleared when successful +//TODO: Other single-byte character sets +// - ISO/IEC 8859-1 "iso8859-1" +// https://en.wikipedia.org/wiki/ISO/IEC_8859-1 +// - Windows-1251 "win1251" +// https://en.wikipedia.org/wiki/Windows-1251 +// - Windows-1252 "win1252" +// https://en.wikipedia.org/wiki/Windows-1252 +// - Windows-932 "win932" +// https://en.wikipedia.org/wiki/Code_page_932_(Microsoft_Windows) +// - ITU T.61 "t61" (technically multibyte) +// Also called Code page 1036, CP1036, or IBM 01036. +// https://en.wikipedia.org/wiki/ITU_T.61 +// NOTE: T.51 specifies how accents are used. +// 0xc0..0xcf are accent prefixes. +// - GSM 03.38 "gsm" (technically multibyte) +// https://www.unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT + +private alias U = char[]; +private template C(dchar c) { enum C = cast(immutable)codeUnits!char(c).s; } +private immutable immutable(char)[] emptychar; + +immutable(char)[] transcodeASCII(ubyte data) @trusted +{ + __gshared char[1] c; + if (data > 0x7E || data < 0x20) + return emptychar; + + c[0] = data; + return c; +} +@trusted unittest +{ + assert(transcodeASCII(0) == []); + assert(transcodeASCII('a') == [ 'a' ]); + assert(transcodeASCII(0x7f) == []); +} + +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!' ' +]; +immutable(char)[] transcodeCP437(ubyte data) +{ + return mapCP437[data]; +} +unittest +{ + assert(transcodeCP437(0) == []); + assert(transcodeCP437('r') == [ 'r' ]); + assert(transcodeCP437(1) == [ '\xe2', '\x98', '\xba' ]); +} + +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!'Ú', [] +]; +immutable(char)[] transcodeEBCDIC(ubyte data) +{ + return data >= 0x40 ? mapEBCDIC[data-0x40] : emptychar; +} +unittest +{ + assert(transcodeEBCDIC(0) == [ ]); + assert(transcodeEBCDIC(0x42) == [ '\xc3', '\xa2' ]); + assert(transcodeEBCDIC(0x7c) == [ '@' ]); +} + +// Mac OS Roman (Windows-10000) "mac" +// https://en.wikipedia.org/wiki/Mac_OS_Roman +// 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!'ˇ', +]; +immutable(char)[] transcodeMac(ubyte data) +{ + return data >= 0x20 ? mapMac[data-0x20] : emptychar; +} +unittest +{ + assert(transcodeMac(0) == [ ]); + assert(transcodeMac(0x20) == [ ' ' ]); + assert(transcodeMac(0x22) == [ '"' ]); + assert(transcodeMac(0xaa) == [ '\xe2', '\x84', '\xa2' ]); +} \ No newline at end of file diff --git a/src/ddhx/utils/args.d b/src/ddhx/utils/args.d new file mode 100644 index 0000000..cc30188 --- /dev/null +++ b/src/ddhx/utils/args.d @@ -0,0 +1,81 @@ +/// Utilities to handle arguments. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.utils.args; + +/// Separate buffer into arguments (akin to argv). +/// Params: buffer = String buffer. +/// Returns: Argv-like array. +string[] arguments(const(char)[] buffer) +{ + import std.string : strip; + import std.ascii : isControl, isWhite; + // NOTE: Using split/splitter would destroy quoted arguments + + //TODO: Escape characters (with '\\') + + buffer = strip(buffer); + + if (buffer.length == 0) return []; + + string[] results; + const size_t buflen = buffer.length; + char delim = void; + + for (size_t index, start; index < buflen; ++index) + { + char c = buffer[index]; + + if (isControl(c) || isWhite(c)) + continue; + + switch (c) { + case '"', '\'': + delim = c; + + for (start = ++index, ++index; index < buflen; ++index) + { + c = buffer[index]; + if (c == delim) + break; + } + + results ~= cast(string)buffer[start..(index++)]; + break; + default: + for (start = index, ++index; index < buflen; ++index) + { + c = buffer[index]; + if (isControl(c) || isWhite(c)) + break; + } + + results ~= cast(string)buffer[start..index]; + } + } + + return results; +} +@system unittest +{ + //TODO: Test embedded string quotes + assert(arguments("") == []); + assert(arguments("\n") == []); + assert(arguments("a") == [ "a" ]); + assert(arguments("simple") == [ "simple" ]); + assert(arguments("simple a b c") == [ "simple", "a", "b", "c" ]); + assert(arguments("simple test\n") == [ "simple", "test" ]); + assert(arguments("simple test\r\n") == [ "simple", "test" ]); + assert(arguments("/simple/ /test/") == [ "/simple/", "/test/" ]); + assert(arguments(`simple 'test extreme'`) == [ "simple", "test extreme" ]); + assert(arguments(`simple "test extreme"`) == [ "simple", "test extreme" ]); + assert(arguments(`simple ' hehe '`) == [ "simple", " hehe " ]); + assert(arguments(`simple " hehe "`) == [ "simple", " hehe " ]); + assert(arguments(`a 'b c' d`) == [ "a", "b c", "d" ]); + 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/ddhx/utils/math.d b/src/ddhx/utils/math.d new file mode 100644 index 0000000..f6d0747 --- /dev/null +++ b/src/ddhx/utils/math.d @@ -0,0 +1,27 @@ +/// Mathematic utilities. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.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/ddhx/utils/memory.d b/src/ddhx/utils/memory.d new file mode 100644 index 0000000..732918f --- /dev/null +++ b/src/ddhx/utils/memory.d @@ -0,0 +1,76 @@ +/// Memory utilities. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.utils.memory; + +import std.stdio : File; +import std.container.array; + +//TODO: Use OutBuffer or Array!ubyte when writing changes? +struct MemoryStream +{ + private ubyte[] buffer; + private long position; + + bool err, eof; + + void cleareof() + { + eof = false; + } + void clearerr() + { + err = false; + } + + void copy(ubyte[] data) + { + buffer = new ubyte[data.length]; + buffer[0..$] = data[0..$]; + } + void copy(File stream) + { + buffer = buffer.init; + //TODO: use OutBuffer+reserve (if possible to get filesize) + foreach (ubyte[] a; stream.byChunk(4096)) + { + buffer ~= a; + } + } + + long seek(long pos) + { + /*final switch (origin) with (Seek) { + case start:*/ + return position = pos; + /* return 0; + case current: + position += pos; + return 0; + case end: + position = size - pos; + return 0; + }*/ + } + + ubyte[] read(size_t size) + { + long p2 = position + size; + + if (p2 > buffer.length) + return buffer[position..$]; + + return buffer[position..p2]; + } + + // not inout ref, just want to read + ubyte[] opSlice(size_t n1, size_t n2) + { + return buffer[n1..n2]; + } + + long size() { return buffer.length; } + + long tell() { return position; } +} diff --git a/src/ddhx/utils/strings.d b/src/ddhx/utils/strings.d new file mode 100644 index 0000000..3995490 --- /dev/null +++ b/src/ddhx/utils/strings.d @@ -0,0 +1,135 @@ +/// Formatting utilities. +/// Copyright: dd86k +/// License: MIT +/// Authors: $(LINK2 https://github.com/dd86k, dd86k) +module ddhx.utils.strings; + +import core.stdc.stdio : sscanf; +import core.stdc.string : memcpy; +import std.string : toStringz; +import std.conv : text; +import ddhx.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"); +} 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/editor.d b/src/editor.d deleted file mode 100644 index d9937b3..0000000 --- a/src/editor.d +++ /dev/null @@ -1,664 +0,0 @@ -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; - -// 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; -} - -/// Current settings. -public __gshared settings_t settings2;*/ - -// 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, -} - -/// 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 -{ - OSFile osfile; - OSMmFile mmfile; - File stream; - MemoryStream memory; -} -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() -{ - ubyte[] t = read().dup; - - - - 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) - { - int l = cast(int)(cursorTell - fsize); - cursor.position -= l; - return; - } -} - -// 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) - { - uint rem = cast(uint)(fileSize % setting.columns); - cursor.position = (cursor.position + setting.columns - rem); - } - 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) - { - if (position == 0) - return false; - cursorEnd; - return viewUp; - } - - --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) - { - cursorHome; - return viewDown; - } - - ++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) - { - return viewUp; - } - - 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; - - FILE *_file = source.stream.getFP; - - if (skip) - { - 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; -} \ 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/main.d b/src/main.d deleted file mode 100644 index 42b8028..0000000 --- a/src/main.d +++ /dev/null @@ -1,241 +0,0 @@ -/// 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.stdio, std.mmfile, std.format, std.getopt; -import core.stdc.stdlib : exit; -import ddhx, editor, dump, reverser; - -//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: - -immutable string SECRET = q"SECRET - +---------------------------------+ - __ | Heard you need help editing | - / \ | data, can I help you with that? | - - - | [ Yes ] [ No ] [ Go away ] | - O O +-. .-----------------------------+ - || |/ |/ - | V | - \_/ -SECRET"; - -// CLI command options - -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) -{ - 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: - } - exit(0); -} - -void cliOption(string opt, string val) -{ - 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); -} - -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); -} - -int main(string[] args) -{ - bool cliMmfile, cliFile, cliDump, cliStdin; - bool cliNoRC; - string cliSeek, cliLength, cliRC, cliReverse; - GetoptResult res = void; - try { - //TODO: Change &cliOption to {} - 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 - ); - } - catch (Exception ex) - { - return errorPrint(1, ex.msg); - } - - 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"~ - "\n"~ - "OPTIONS"); - foreach (opt; res.options) with (opt) - { - if (help == "") continue; - if (optShort) - writefln("%s, %-14s %s", optShort, optLong, help); - else - writefln(" %-14s %s", optLong, help); - } - return 0; - } - - version (Trace) - traceInit(DDHX_ABOUT); - - long skip, length; - - // Convert skip value - if (cliSeek) - { - version (Trace) trace("seek=%s", cliSeek); - - if (convertToVal(skip, cliSeek)) - return errorPrint(); - - if (skip < 0) - return errorPrint(1, "Skip value must be positive"); - } - - // 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; - - // App: dump - if (cliDump) - { - if (cliLength && convertToVal(length, cliLength)) - return errorPrint; - return dump.start(skip, length); - } - - // App: reverse - if (cliReverse) - { - return reverser.start(cliReverse); - } - - // App: interactive - return ddhx.start(skip); -} \ No newline at end of file diff --git a/src/os/file.d b/src/os/file.d deleted file mode 100644 index c2ed9b1..0000000 --- a/src/os/file.d +++ /dev/null @@ -1,310 +0,0 @@ -/// File handling. -/// -/// This exists because 32-bit runtimes suffer from the 32-bit file size limit. -/// Despite the MS C runtime having _open and _fseeki64, the DM C runtime does -/// not have these functions. -/// -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module os.file; - -version (Windows) -{ - import core.sys.windows.winnt : - DWORD, HANDLE, LARGE_INTEGER, FALSE, TRUE, - GENERIC_ALL, GENERIC_READ, GENERIC_WRITE; - import core.sys.windows.winbase : - GetLastError, - CreateFileA, CreateFileW, - SetFilePointerEx, GetFileSizeEx, - ReadFile, ReadFileEx, - WriteFile, - FlushFileBuffers, - CloseHandle, - OPEN_ALWAYS, OPEN_EXISTING, INVALID_HANDLE_VALUE, - FILE_BEGIN, FILE_CURRENT, FILE_END; - - private alias OSHANDLE = HANDLE; - private alias SEEK_SET = FILE_BEGIN; - private alias SEEK_CUR = FILE_CURRENT; - private alias SEEK_END = FILE_END; - - private enum OFLAG_OPENONLY = OPEN_EXISTING; -} -else version (Posix) -{ - import core.sys.posix.unistd; - import core.sys.posix.sys.types; - import core.sys.posix.sys.stat; - import core.sys.posix.fcntl; - import core.stdc.errno; - import core.stdc.stdio : SEEK_SET, SEEK_CUR, SEEK_END; - - // BLKGETSIZE64 missing from dmd 2.098.1 and ldc 1.24.0 - // ldc 1.24 missing core.sys.linux.fs - // source musl 1.2.0 and glibc 2.25 has roughly same settings. - - private enum _IOC_NRBITS = 8; - private enum _IOC_TYPEBITS = 8; - private enum _IOC_SIZEBITS = 14; - private enum _IOC_NRSHIFT = 0; - private enum _IOC_TYPESHIFT = _IOC_NRSHIFT+_IOC_NRBITS; - private enum _IOC_SIZESHIFT = _IOC_TYPESHIFT+_IOC_TYPEBITS; - private enum _IOC_DIRSHIFT = _IOC_SIZESHIFT+_IOC_SIZEBITS; - private enum _IOC_READ = 2; - private enum _IOC(int dir,int type,int nr,size_t size) = - (dir << _IOC_DIRSHIFT) | - (type << _IOC_TYPESHIFT) | - (nr << _IOC_NRSHIFT) | - (size << _IOC_SIZESHIFT); - //TODO: _IOR!(0x12,114,size_t.sizeof) results in ulong.max - // I don't know why, so I'm casting it to int to let it compile. - // Fix later. - private enum _IOR(int type,int nr,size_t size) = - cast(int)_IOC!(_IOC_READ,type,nr,size); - - private enum BLKGETSIZE64 = cast(int)_IOR!(0x12,114,size_t.sizeof); - private alias BLOCKSIZE = BLKGETSIZE64; - - private extern (C) int ioctl(int,long,...); - - private alias OSHANDLE = int; -} else { - static assert(0, "Implement file I/O"); -} - -//TODO: FileType GetType(string) -// Pipe, device, etc. - -import std.string : toStringz; -import std.utf : toUTF16z; - -/// 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. -} - -/// Open file flags -enum OFlags -{ - exists = 1, /// File must exist. - read = 1 << 1, /// Read access. - write = 1 << 2, /// Write access. - readWrite = read | write, /// Read and write access. - share = 1 << 5, /// [TODO] Share file with read access to other programs. -} - -//TODO: Set file size (to extend or truncate file, allocate size) -// useful when writing all changes to file -// Win32: Seek + SetEndOfFile -// others: ftruncate -struct OSFile { - private OSHANDLE handle; - bool eof, err; - - void cleareof() - { - eof = false; - } - void clearerr() - { - err = false; - } - int syscode() - { - version (Windows) - return GetLastError(); - else - return errno; - } - - //TODO: Share file. - // By default, at least on Windows, files aren't shared. Enabling - // sharing would allow refreshing view (manually) when a program - // writes to file. - bool open(string path, int flags = OFlags.readWrite) - { - version (Windows) - { - uint dwAccess; - uint dwCreation; - - if (flags & OFlags.exists) dwCreation |= OPEN_EXISTING; - else dwCreation |= OPEN_ALWAYS; - if (flags & OFlags.read) dwAccess |= GENERIC_READ; - if (flags & OFlags.write) dwAccess |= GENERIC_WRITE; - - // NOTE: toUTF16z/tempCStringW - // Phobos internally uses tempCStringW from std.internal - // but I doubt it's meant for us to use so... - // Legacy baggage? - handle = CreateFileW( - path.toUTF16z, // lpFileName - dwAccess, // dwDesiredAccess - 0, // dwShareMode - null, // lpSecurityAttributes - dwCreation, // dwCreationDisposition - 0, // dwFlagsAndAttributes - null, // hTemplateFile - ); - return err = handle == INVALID_HANDLE_VALUE; - } - else version (Posix) - { - int oflags; - if (!(flags & OFlags.exists)) oflags |= O_CREAT; - if ((flags & OFlags.readWrite) == OFlags.readWrite) - oflags |= O_RDWR; - else if (flags & OFlags.write) - oflags |= O_WRONLY; - else if (flags & OFlags.read) - oflags |= O_RDONLY; - handle = .open(path.toStringz, oflags); - return err = handle == -1; - } - } - - long seek(Seek origin, long pos) - { - version (Windows) - { - LARGE_INTEGER i = void; - i.QuadPart = pos; - err = SetFilePointerEx(handle, i, &i, origin) == FALSE; - return i.QuadPart; - } - else version (OSX) - { - // NOTE: Darwin has set off_t as long - // and doesn't have lseek64 - pos = lseek(handle, pos, origin); - err = pos == -1; - return pos; - } - else version (Posix) // Should cover glibc and musl - { - pos = lseek64(handle, pos, origin); - err = pos == -1; - return pos; - } - } - - long tell() - { - version (Windows) - { - LARGE_INTEGER i; // .init - SetFilePointerEx(handle, i, &i, FILE_CURRENT); - return i.QuadPart; - } - else version (OSX) - { - return lseek(handle, 0, SEEK_CUR); - } - else version (Posix) - { - return lseek64(handle, 0, SEEK_CUR); - } - } - - long size() - { - version (Windows) - { - LARGE_INTEGER li = void; - err = GetFileSizeEx(handle, &li) == 0; - return err ? -1 : li.QuadPart; - } - else version (Posix) - { - stat_t stats = void; - if (fstat(handle, &stats) == -1) - return -1; - // NOTE: fstat(2) sets st_size to 0 on block devices - switch (stats.st_mode & S_IFMT) { - case S_IFREG: // File - case S_IFLNK: // Link - return stats.st_size; - case S_IFBLK: // Block devices (like a disk) - //TODO: BSD variants - long s = void; - return ioctl(handle, BLOCKSIZE, &s) == -1 ? -1 : s; - default: - return -1; - } - } - } - - ubyte[] read(ubyte[] buffer) - { - return read(buffer.ptr, buffer.length); - } - - ubyte[] read(ubyte *buffer, size_t size) - { - version (Windows) - { - uint len = cast(uint)size; - err = ReadFile(handle, buffer, len, &len, null) == FALSE; - if (err) return null; - eof = len < size; - return buffer[0..len]; - } - else version (Posix) - { - ssize_t len = .read(handle, buffer, size); - if ((err = len < 0) == true) return null; - eof = len < size; - return buffer[0..len]; - } - } - - size_t write(ubyte[] data) - { - return write(data.ptr, data.length); - } - - size_t write(ubyte *data, size_t size) - { - version (Windows) - { - uint len = cast(uint)size; - err = WriteFile(handle, data, len, &len, null) == FALSE; - return len; // 0 on error anyway - } - else version (Posix) - { - ssize_t len = .write(handle, data, size); - err = len < 0; - return err ? 0 : len; - } - } - - void flush() - { - version (Windows) - { - FlushFileBuffers(handle); - } - else version (Posix) - { - .fsync(handle); - } - } - - void close() - { - version (Windows) - { - CloseHandle(handle); - } - else version (Posix) - { - .close(handle); - } - } -} 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 deleted file mode 100644 index a819899..0000000 --- a/src/os/path.d +++ /dev/null @@ -1,157 +0,0 @@ -/// OS path utilities. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module os.path; - -version (Windows) -{ - import core.sys.windows.windef : S_OK; - import core.sys.windows.shlobj : - CSIDL_PROFILE, CSIDL_LOCAL_APPDATA, CSIDL_APPDATA, - SHGetFolderPathA, SHGetFolderPathW; - import core.stdc.stdlib : malloc, free; - import core.stdc.wchar_ : wcslen; - import std.encoding : transcode; -} -else version (Posix) -{ - import core.sys.posix.unistd : getuid, uid_t; - import core.sys.posix.pwd : getpwuid, passwd; - import core.stdc.string : strlen; -} - -import std.process : environment; -import std.path : dirSeparator, buildPath; -import std.file : exists; - -// NOTE: As of Windows Vista, the SHGetSpecialFolderPathW function is a wrapper -// for SHGetKnownFolderPath. The latter not defined in the shlobj module. - -/// Get the path to the current user's home folder. -/// This does not verify if the path exists. -/// Windows: Typically C:\\Users\\%USERNAME% -/// Posix: Typically /home/$USERNAME -/// Returns: Path or null on failure. -string getHomeFolder() -{ - version (Windows) - { - // 1. SHGetFolderPath - wchar *buffer = cast(wchar*)malloc(1024); - if (SHGetFolderPathW(null, CSIDL_PROFILE, null, 0, buffer) == S_OK) - { - string path; - transcode(buffer[0..wcslen(buffer)], path); - free(buffer); // since transcode allocated - 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) - { - // 1. $HOME - if ("HOME" in environment) - return environment["HOME"]; - // 2. getpwuid+getuid - uid_t uid = getuid(); - if (uid >= 0) - { - passwd *wd = getpwuid(uid); - if (wd) - { - return cast(immutable(char)[]) - wd.pw_dir[0..strlen(wd.pw_dir)]; - } - } - } - - return null; -} - -/// Get the path to the current user data folder. -/// This does not verify if the path exists. -/// Windows: Typically C:\\Users\\%USERNAME%\\AppData\\Roaming -/// Posix: Typically /home/$USERNAME/.config -/// Returns: Path or null on failure. -string getUserDataFolder() -{ - version (Windows) - { - // 1. SHGetFolderPath - wchar *buffer = cast(wchar*)malloc(1024); - if (SHGetFolderPathW(null, CSIDL_APPDATA, null, 0, buffer) == S_OK) - { - string path; - transcode(buffer[0..wcslen(buffer)], path); - free(buffer); // transcode allocates - 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"]; - } - - // Fallback - - string base = getHomeFolder; - - if (base is null) - return null; - - version (Windows) - { - return buildPath(base, "AppData", "Local"); - } - else version (Posix) - { - return buildPath(base, ".config"); - } -} - -/// Build the path for a given file name with the user home folder. -/// This does not verify if the path exists. -/// Windows: Typically C:\\Users\\%USERNAME%\\{filename} -/// Posix: Typically /home/$USERNAME/{filename} -/// Params: filename = Name of a file. -/// Returns: Path or null on failure. -string buildUserFile(string filename) -{ - string base = getHomeFolder; - - if (base is null) - return null; - - return buildPath(base, filename); -} - -/// Build the path for a given file name and parent folder with the user data folder. -/// This does not verify if the path exists. -/// Windows: Typically C:\\Users\\%USERNAME%\\AppData\\Roaming\\{appname}\\{filename} -/// Posix: Typically /home/$USERNAME/.config/{appname}/{filename} -/// Params: -/// appname = Name of the app. This acts as the parent folder. -/// filename = Name of a file. -/// Returns: Path or null on failure. -string buildUserAppFile(string appname, string filename) -{ - string base = getUserDataFolder; - - if (base is null) - return null; - - return buildPath(base, appname, filename); -} \ No newline at end of file diff --git a/src/os/terminal.d b/src/os/terminal.d deleted file mode 100644 index 02e1084..0000000 --- a/src/os/terminal.d +++ /dev/null @@ -1,951 +0,0 @@ -/// Terminal/console handling. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module os.terminal; - -//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 - -// 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 (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; - 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; - - private enum NULL_SIGACTION = cast(sigaction_t*)0; - private enum SIGWINCH = 28; - - // Bionic depends on the Linux system it's compiled on. - // But Glibc and Musl have the same settings, so does Bionic. - // ...And uClibc, at least on Linux. - // D are missing the following bindings. - version (CRuntime_Musl) - version = IncludeTermiosLinux; - version (CRuntime_Bionic) - version = IncludeTermiosLinux; - version (CRuntime_UClibc) - version = IncludeTermiosLinux; - - version (IncludeTermiosLinux) - { - //siginfo_t - // termios.h, bits/termios.h - private alias uint tcflag_t; - private alias uint speed_t; - private alias char cc_t; - private enum TCSANOW = 0; - private enum NCCS = 32; - private enum ICANON = 2; - private enum ECHO = 10; - private enum BRKINT = 2; - private enum INPCK = 20; - private enum ISTRIP = 40; - private enum ICRNL = 400; - private enum IXON = 2000; - private enum IEXTEN = 100000; - private enum CS8 = 60; - private enum TCSAFLUSH = 2; - private struct termios { - tcflag_t c_iflag; - tcflag_t c_oflag; - tcflag_t c_cflag; - tcflag_t c_lflag; - cc_t c_line; - cc_t[NCCS] c_cc; - speed_t __c_ispeed; - speed_t __c_ospeed; - } - private extern (C) int tcgetattr(int fd, termios *termios_p); - private extern (C) int tcsetattr(int fd, int a, termios *termios_p); - // ioctl.h - private enum TIOCGWINSZ = 0x5413; - private struct winsize { - ushort ws_row; - ushort ws_col; - ushort ws_xpixel; - ushort ws_ypixel; - } - private extern (C) int ioctl(int fd, ulong request, ...); - } - - struct KeyInfo { - string text; - 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 }, - { "\033[2~", Key.Insert }, - { "\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[5~", Key.PageUp }, - { "\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 }, - ]; - - private __gshared termios old_ios, new_ios; -} - -/// Flags for terminalInit. -//TODO: captureCtrlC: Block CTRL+C -enum TermFeat : ushort { - /// Initiate only the basic. - none = 0, - /// Initiate the input system. - inputSys = 1, - /// Initiate the alternative screen buffer. - altScreen = 1 << 1, - /// Initiate everything. - all = 0xffff, -} - -private __gshared TermFeat current_features; - -/// Initiate terminal. -/// Params: features = Feature bits to initiate. -/// Throws: (Windows) WindowsException on OS exception -void terminalInit(TermFeat features) -{ - current_features = features; - version (Windows) - { - CONSOLE_SCREEN_BUFFER_INFO csbi = void; - - if (features & TermFeat.inputSys) - { - //NOTE: Re-opening stdin before new screen fixes quite a few things - // - usage with CreateConsoleScreenBuffer - // - readln (for menu) - // - receiving key input when stdin was used for reading a buffer - hIn = CreateFileA("CONIN$", GENERIC_READ, 0, null, OPEN_EXISTING, 0, null); - if (hIn == INVALID_HANDLE_VALUE) - throw new WindowsException(GetLastError); - SetConsoleMode(hIn, ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT); - stdin.windowsHandleOpen(hIn, "r"); - SetStdHandle(STD_INPUT_HANDLE, hIn); - } - else - { - hIn = GetStdHandle(STD_INPUT_HANDLE); - } - - if (features & TermFeat.altScreen) - { - // - // Setting up stdout - // - - hOut = GetStdHandle(STD_OUTPUT_HANDLE); - if (hIn == INVALID_HANDLE_VALUE) - throw new WindowsException(GetLastError); - - if (GetConsoleScreenBufferInfo(hOut, &csbi) == FALSE) - throw new WindowsException(GetLastError); - - DWORD attr = void; - if (GetConsoleMode(hOut, &attr) == FALSE) - throw new WindowsException(GetLastError); - - hOut = CreateConsoleScreenBuffer( - GENERIC_READ | GENERIC_WRITE, // dwDesiredAccess - FILE_SHARE_READ | FILE_SHARE_WRITE, // dwShareMode - null, // lpSecurityAttributes - CONSOLE_TEXTMODE_BUFFER, // dwFlags - null, // lpScreenBufferData - ); - if (hOut == INVALID_HANDLE_VALUE) - throw new WindowsException(GetLastError); - - stdout.flush; - stdout.windowsHandleOpen(hOut, "wb"); // fixes using write functions - - SetStdHandle(STD_OUTPUT_HANDLE, hOut); - SetConsoleScreenBufferSize(hOut, csbi.dwSize); - SetConsoleMode(hOut, attr | ENABLE_PROCESSED_OUTPUT); - - if (SetConsoleActiveScreenBuffer(hOut) == FALSE) - throw new WindowsException(GetLastError); - } else { - hOut = GetStdHandle(STD_OUTPUT_HANDLE); - } - - stdout.setvbuf(0, _IONBF); // fixes weird cursor positions with alt buffer - - // NOTE: While Windows supports UTF-16LE (1200) and UTF-32LE, - // it's only for "managed applications" (.NET). - // LINK: https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers - oldCP = GetConsoleOutputCP(); - if (SetConsoleOutputCP(CP_UTF8) == FALSE) - throw new WindowsException(GetLastError); - - //TODO: Get active (or default) colors - GetConsoleScreenBufferInfo(hOut, &csbi); - oldAttr = csbi.wAttributes; - } - else version (Posix) - { - stdout.setvbuf(0, _IONBF); - if (features & TermFeat.inputSys) - { - // Should it re-open tty by default? - stat_t s = void; - fstat(STDIN_FILENO, &s); - if (S_ISFIFO(s.st_mode)) - stdin.reopen("/dev/tty", "r"); - tcgetattr(STDIN_FILENO, &old_ios); - new_ios = old_ios; - // NOTE: input modes - // - IXON enables ^S and ^Q - // - ICRNL enables ^M - // - BRKINT causes SIGINT (^C) on break conditions - // - INPCK enables parity checking - // - ISTRIP strips the 8th bit - new_ios.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP); - // NOTE: output modes - // - OPOST turns on output post-processing - //new_ios.c_oflag &= ~(OPOST); - // NOTE: local modes - // - ICANON turns on canonical mode (per-line instead of per-byte) - // - ECHO turns on character echo - // - ISIG enables ^C and ^Z signals - // - IEXTEN enables ^V - new_ios.c_lflag &= ~(ICANON | ECHO | IEXTEN); - // NOTE: control modes - // - CS8 sets Character Size to 8-bit - new_ios.c_cflag |= CS8; - // minimum amount of bytes to read, - // 0 being return as soon as there is data - //new_ios.c_cc[VMIN] = 0; - // maximum amount of time to wait for input, - // 1 being 1/10 of a second (100 milliseconds) - //new_ios.c_cc[VTIME] = 0; - terminalResumeInput; - } - - if (features & TermFeat.altScreen) - { - // change to alternative screen buffer - stdout.write("\033[?1049h"); - } - } - - atexit(&terminalQuit); -} - -private extern (C) -void terminalQuit() -{ - 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"); - terminalShowCursor; - } - if (current_features & TermFeat.inputSys) - terminalPauseInput; -} - -private __gshared void function() terminalOnResizeEvent; - -void terminalOnResize(void function() func) -{ - version (Posix) - { - sigaction_t sa = void; - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_SIGINFO; - sa.sa_sigaction = &terminalResized; - assert(sigaction(SIGWINCH, &sa, NULL_SIGACTION) != -1); - } - terminalOnResizeEvent = func; -} - -version (Posix) -private extern (C) -void terminalResized(int signo, siginfo_t *info, void *content) -{ - if (terminalOnResizeEvent) - terminalOnResizeEvent(); -} - -void terminalPauseInput() -{ - version (Posix) - tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_ios); -} - -void terminalResumeInput() -{ - version (Posix) - tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_ios); -} - -/// Clear screen -void terminalClear() -{ - version (Windows) - { - CONSOLE_SCREEN_BUFFER_INFO csbi = void; - COORD c; - GetConsoleScreenBufferInfo(hOut, &csbi); - const int size = csbi.dwSize.X * csbi.dwSize.Y; - DWORD num; - if (FillConsoleOutputCharacterA(hOut, ' ', size, c, &num) == 0 - /*|| - FillConsoleOutputAttribute(hOut, csbi.wAttributes, size, c, &num) == 0*/) - { - terminalPos(0, 0); - } else // If that fails, run cls. - system("cls"); - } - else version (Posix) - { - // \033c is a Reset - // \033[2J is "Erase whole display" - terminalOutput2("\033[2J"); - } else static assert(0, "Clear: Not implemented"); -} - -/// Get terminal window size in characters. -/// Returns: Size -TerminalSize terminalSize() -{ - TerminalSize size = void; - version (Windows) - { - 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; - } - else version (Posix) - { - //TODO: Consider using LINES and COLUMNS environment variables - // as fallback if ioctl returns -1. - //TODO: Consider ESC [ 18 t for fallback of environment. - // Reply: ESC [ 8 ; ROWS ; COLUMNS t - winsize ws = void; - ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); - size.height = ws.ws_row; - size.width = ws.ws_col; - } else static assert(0, "terminalSize: Not implemented"); - return size; -} - -/// Set cursor position x and y position respectively from the top left corner, -/// 0-based. -/// Params: -/// x = X position (horizontal) -/// y = Y position (vertical) -void terminalPos(int x, int y) -{ - version (Windows) // 0-based - { - COORD c = void; - c.X = cast(short)x; - c.Y = cast(short)y; - SetConsoleCursorPosition(hOut, c); - } - else version (Posix) // 1-based, so 0,0 needs to be output as 1,1 - { - char[16] b = void; - int r = snprintf(b.ptr, 16, "\033[%d;%dH", ++y, ++x); - assert(r > 0); - terminalOutput(b.ptr, r); - } -} - -/// Hide the terminal cursor. -void terminalHideCursor() -{ - version (Windows) - { - CONSOLE_CURSOR_INFO cci = void; - GetConsoleCursorInfo(hOut, &cci); - cci.bVisible = FALSE; - SetConsoleCursorInfo(hOut, &cci); - } - else version (Posix) - { - terminalOutput2("\033[?25l"); - } -} -/// Show the terminal cursor. -void terminalShowCursor() -{ - version (Windows) - { - CONSOLE_CURSOR_INFO cci = void; - GetConsoleCursorInfo(hOut, &cci); - cci.bVisible = TRUE; - SetConsoleCursorInfo(hOut, &cci); - } - else version (Posix) - { - terminalOutput2("\033[?25h"); - } -} - -void terminalHighlight() -{ - version (Windows) - { - SetConsoleTextAttribute(hOut, oldAttr | BACKGROUND_RED); - } - else version (Posix) - { - terminalOutput2("\033[41m"); - } -} -/// Invert color. -void terminalInvertColor() -{ - version (Windows) - { - SetConsoleTextAttribute(hOut, oldAttr | COMMON_LVB_REVERSE_VIDEO); - } - else version (Posix) - { - terminalOutput2("\033[7m"); - } -} -/// Underline. -/// Bugs: Does not work on Windows Terminal. See https://github.com/microsoft/terminal/issues/8037 -void terminalUnderline() -{ - version (Windows) - { - SetConsoleTextAttribute(hOut, oldAttr | COMMON_LVB_UNDERSCORE); - } - else version (Posix) - { - terminalOutput2("\033[4m"); - } -} -/// Reset color. -void terminalResetColor() -{ - version (Windows) - { - SetConsoleTextAttribute(hOut, oldAttr); - } - else version (Posix) - { - terminalOutput2("\033[0m"); - } -} - -size_t terminalOutput2(const(void)[] data) -{ - return terminalOutput(data.ptr, data.length); -} - -/// Directly write to output. -/// Params: -/// data = Character data. -/// size = Amount in bytes. -/// Returns: Number of bytes written. -size_t terminalOutput(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) -{ - version (Windows) - { - INPUT_RECORD ir = void; - DWORD num = void; -L_READ: - if (ReadConsoleInputA(hIn, &ir, 1, &num) == 0) - throw new WindowsException(GetLastError); - - if (num == 0) - goto L_READ; - - switch (ir.EventType) { - case KEY_EVENT: - if (ir.KeyEvent.bKeyDown == FALSE) - goto L_READ; - - version (TestInput) - { - printf( - "KeyEvent: AsciiChar=%d wVirtualKeyCode=%d dwControlKeyState=%x\n", - ir.KeyEvent.AsciiChar, - ir.KeyEvent.wVirtualKeyCode, - ir.KeyEvent.dwControlKeyState, - ); - } - - const ushort keycode = ir.KeyEvent.wVirtualKeyCode; - - // Filter out single modifier key events - switch (keycode) { - case 16, 17, 18: goto L_READ; // shift,ctrl,alt - default: - } - - event.type = InputType.keyDown; - - const char ascii = ir.KeyEvent.AsciiChar; - - if (ascii >= 'a' && ascii <= 'z') - { - event.key = ascii - 32; - return; - } - else if (ascii >= 0x20 && ascii < 0x7f) - { - event.key = ascii; - - // '?' on a fr-ca kb is technically shift+6, - // breaking app input since expecting no modifiers - if (ascii >= 'A' && ascii <= 'Z') - event.key = Mod.shift; - - return; - } - - event.key = keycode; - const DWORD state = ir.KeyEvent.dwControlKeyState; - 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; - /*case MOUSE_EVENT: - if (ir.MouseEvent.dwEventFlags & MOUSE_WHEELED) - { - // Up=0x00780000 Down=0xFF880000 - event.type = ir.MouseEvent.dwButtonState > 0xFF_0000 ? - Mouse.ScrollDown : Mouse.ScrollUp; - }*/ - case WINDOW_BUFFER_SIZE_EVENT: - if (terminalOnResizeEvent) - terminalOnResizeEvent(); - goto L_READ; - default: goto L_READ; - } - } - else version (Posix) - { - //TODO: Mouse reporting in Posix terminals - // * X10 compatbility mode (mouse-down only) - // Enable: ESC [ ? 9 h - // Disable: ESC [ ? 9 l - // "sends ESC [ M bxy (6 characters)" - // - ESC [ M button column row (1-based) - // - 0,0 click: ESC [ M ! ! - // ! is 0x21, so '!' - 0x21 = 0 - // - end,end click: ESC [ M q ; - // q is 0x71, so 'q' - 0x21 = 0x50 (column 80) - // ; is 0x3b, so ';' - 0x21 = 0x1a (row 26) - // - button left: ' ' - // - button right: '"' - // - button middle: '!' - // * Normal tracking mode - // Enable: ESC [ ? 1000 h - // Disable: ESC [ ? 1000 l - // b bits[1:0] 0=MB1 pressed, 1=MB2 pressed, 2=MB3 pressed, 3=release - // b bits[7:2] 4=Shift (bit 3), 8=Meta (bit 4), 16=Control (bit 5) - //TODO: Faster scanning - // So we have a few choices: - // - string table (current, works alright) - // - string[string] - // - needs active init though - // - string decoding (slower?) - // [ -> escape - // 1;2 -> shift (optional) - // B -> right arrow - // - template char[8] to long - // - very cursed - // - screwed if there are keys more than 8 bytes - // - template should do endianness - // - Manually hash it - // - Allows static arrays - // - std.digest.murmurhash already available - - enum BLEN = 8; - char[BLEN] b = void; - L_READ: - 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 0: // How even - version (TestInput) printf("stdin: empty\n"); - goto L_READ; - case 1: - char c = b[0]; - version (TestInput) 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; - case 13: - event.key = Key.Enter; - return; - case 8, 127: // ^H - event.key = Key.Backspace; - return; - default: - } - if (c >= 'a' && c <= 'z') - event.key = cast(ushort)(c - 32); - else if (c >= 'A' && c <= 'Z') - event.key = c | Mod.shift; - else if (c < 32) - event.key = (c + 64) | Mod.ctrl; - else - event.key = c; - return; - default: - } - - version (TestInput) - { - printf("stdin:"); - for (size_t i; i < r; ++i) - { - char c = b[i]; - if (c < 32 || c > 126) - printf(" \\0%o", c); - else - putchar(b[i]); - } - putchar('\n'); - stdout.flush(); - } - - // Make a slice of misc. input. - const(char)[] inputString = b[0..r]; - - //TODO: Checking for mouse inputs - // Starts with \033[M - - // Checking for other key inputs - foreach (ki; keyInputsVTE) - { - if (r != ki.text.length) continue; - if (inputString != ki.text) continue; - event.key = ki.value; - return; - } - - // Matched to nothing - goto L_READ; - } // version posix -} - -/// Terminal input type. -enum InputType { - keyDown, - keyUp, - mouseDown, - mouseUp, -} - -/// Key modifier -enum Mod { - ctrl = 1 << 16, - shift = 1 << 17, - alt = 1 << 18, -} -/// Key codes map. -//TODO: Consider mapping these to os/ascii-specific -enum Key { - Undefined = 0, - Backspace = 8, - Tab = 9, - Clear = 12, - Enter = 13, - Pause = 19, - Escape = 27, - Spacebar = 32, - PageUp = 33, - PageDown = 34, - End = 35, - Home = 36, - LeftArrow = 37, - UpArrow = 38, - RightArrow = 39, - DownArrow = 40, - Select = 41, - Print = 42, - Execute = 43, - PrintScreen = 44, - Insert = 45, - Delete = 46, - Help = 47, - D0 = 48, - D1 = 49, - D2 = 50, - D3 = 51, - D4 = 52, - D5 = 53, - D6 = 54, - D7 = 55, - D8 = 56, - D9 = 57, - Colon = 58, - SemiColon = 59, - A = 65, - B = 66, - C = 67, - D = 68, - E = 69, - F = 70, - G = 71, - H = 72, - I = 73, - J = 74, - K = 75, - L = 76, - M = 77, - N = 78, - O = 79, - P = 80, - Q = 81, - R = 82, - S = 83, - T = 84, - U = 85, - V = 86, - W = 87, - X = 88, - Y = 89, - Z = 90, - LeftMeta = 91, - RightMeta = 92, - Applications = 93, - Sleep = 95, - NumPad0 = 96, - NumPad1 = 97, - NumPad2 = 98, - NumPad3 = 99, - NumPad4 = 100, - NumPad5 = 101, - NumPad6 = 102, - NumPad7 = 103, - NumPad8 = 104, - NumPad9 = 105, - Multiply = 106, - Add = 107, - Separator = 108, - Subtract = 109, - Decimal = 110, - Divide = 111, - F1 = 112, - F2 = 113, - F3 = 114, - F4 = 115, - F5 = 116, - F6 = 117, - F7 = 118, - F8 = 119, - F9 = 120, - F10 = 121, - F11 = 122, - F12 = 123, - F13 = 124, - F14 = 125, - F15 = 126, - F16 = 127, - F17 = 128, - F18 = 129, - F19 = 130, - F20 = 131, - F21 = 132, - F22 = 133, - F23 = 134, - F24 = 135, - BrowserBack = 166, - BrowserForward = 167, - BrowserRefresh = 168, - BrowserStop = 169, - BrowserSearch = 170, - BrowserFavorites = 171, - BrowserHome = 172, - VolumeMute = 173, - VolumeDown = 174, - VolumeUp = 175, - MediaNext = 176, - MediaPrevious = 177, - MediaStop = 178, - MediaPlay = 179, - LaunchMail = 180, - LaunchMediaSelect = 181, - LaunchApp1 = 182, - LaunchApp2 = 183, - Oem1 = 186, - OemPlus = 187, - OemComma = 188, - OemMinus = 189, - OemPeriod = 190, - Oem2 = 191, - Oem3 = 192, - Oem4 = 219, - Oem5 = 220, - Oem6 = 221, - Oem7 = 222, - Oem8 = 223, - Oem102 = 226, - Process = 229, - Packet = 231, - Attention = 246, - CrSel = 247, - ExSel = 248, - EraseEndOfFile = 249, - Play = 250, - Zoom = 251, - NoName = 252, - Pa1 = 253, - OemClear = 254 -} - -/// Terminal input structure -struct TerminalInput { - union { - int key; /// Keyboard input with possible Mod flags. - struct { - ushort mouseX; /// Mouse column coord - ushort mouseY; /// Mouse row coord - } - } - int type; /// Terminal input event type -} - -/// 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 - } -} 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 deleted file mode 100644 index 8cca392..0000000 --- a/src/transcoder.d +++ /dev/null @@ -1,216 +0,0 @@ -/// Transcoder module. -/// -/// Translates single-byte characters into utf-8. -/// Copyright: dd86k -/// License: MIT -/// Authors: $(LINK2 https://github.com/dd86k, dd86k) -module encoding; - -import std.encoding : codeUnits, CodeUnits; - -/// Character set. -enum CharacterSet : ubyte { - ascii, /// 7-bit US-ASCII - cp437, /// IBM PC CP-437 - ebcdic, /// IBM EBCDIC Code Page 37 - mac, /// Mac OS Roman (Windows 10000) -// t61, /// ITU T.61 -// gsm, /// GSM 03.38 -} - -//TODO: Consider registering encoders to EncodingScheme -// to transcode to other charsets other than UTF-8 -//TODO: Translation function could return something specific if it needs another byte -// With static param, internally cleared when successful -//TODO: Other single-byte character sets -// - ISO/IEC 8859-1 "iso8859-1" -// https://en.wikipedia.org/wiki/ISO/IEC_8859-1 -// - Windows-1251 "win1251" -// https://en.wikipedia.org/wiki/Windows-1251 -// - Windows-1252 "win1252" -// https://en.wikipedia.org/wiki/Windows-1252 -// - Windows-932 "win932" -// https://en.wikipedia.org/wiki/Code_page_932_(Microsoft_Windows) -// - ITU T.61 "t61" (technically multibyte) -// Also called Code page 1036, CP1036, or IBM 01036. -// https://en.wikipedia.org/wiki/ITU_T.61 -// NOTE: T.51 specifies how accents are used. -// 0xc0..0xcf are accent prefixes. -// - 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(char)[] transcodeASCII(ubyte data) -{ - __gshared immutable(char)[] empty; - __gshared char[1] c; - if (data > 0x7E || data < 0x20) - return empty; - - c[0] = data; - return c; -} -unittest { - assert(transcodeASCII(0) == []); - assert(transcodeASCII('a') == [ 'a' ]); - assert(transcodeASCII(0x7f) == []); -} - -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!' ' -]; -private -immutable(char)[] transcodeCP437(ubyte data) -{ - return mapCP437[data]; -} -unittest { - assert(transcodeCP437(0) == []); - assert(transcodeCP437('r') == [ 'r' ]); - assert(transcodeCP437(1) == [ '\xe2', '\x98', '\xba' ]); -} - -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!'Ú', [] -]; -private -immutable(char)[] transcodeEBCDIC(ubyte data) -{ - return data >= 0x40 ? mapEBCDIC[data-0x40] : emptychar; -} -unittest { - assert(transcodeEBCDIC(0) == [ ]); - assert(transcodeEBCDIC(0x42) == [ '\xc3', '\xa2' ]); - assert(transcodeEBCDIC(0x7c) == [ '@' ]); -} - -// Mac OS Roman (Windows-10000) "mac" -// https://en.wikipedia.org/wiki/Mac_OS_Roman -// 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!'ˇ', -]; -private -immutable(char)[] transcodeMac(ubyte data) -{ - return data >= 0x20 ? mapMac[data-0x20] : emptychar; -} -unittest { - assert(transcodeMac(0) == [ ]); - assert(transcodeMac(0x20) == [ ' ' ]); - assert(transcodeMac(0x22) == [ '"' ]); - assert(transcodeMac(0xaa) == [ '\xe2', '\x84', '\xa2' ]); -} \ No newline at end of file diff --git a/src/utils/args.d b/src/utils/args.d deleted file mode 100644 index b6cebb2..0000000 --- a/src/utils/args.d +++ /dev/null @@ -1,77 +0,0 @@ -module utils.args; - - -/// Separate buffer into arguments (akin to argv). -/// Params: buffer = String buffer. -/// Returns: Argv-like array. -string[] arguments(string buffer) -{ - import std.string : strip; - import std.ascii : isControl, isWhite; - // NOTE: Using split/splitter would destroy quoted arguments - - //TODO: Escape characters (with '\\') - - buffer = strip(buffer); - - if (buffer.length == 0) return []; - - string[] results; - const size_t buflen = buffer.length; - char delim = void; - - for (size_t index, start; index < buflen; ++index) - { - char c = buffer[index]; - - if (isControl(c) || isWhite(c)) - continue; - - switch (c) { - case '"', '\'': - delim = c; - - for (start = ++index, ++index; index < buflen; ++index) - { - c = buffer[index]; - if (c == delim) - break; - } - - results ~= buffer[start..(index++)]; - break; - default: - for (start = index, ++index; index < buflen; ++index) - { - c = buffer[index]; - if (isControl(c) || isWhite(c)) - break; - } - - results ~= buffer[start..index]; - } - } - - return results; -} - -/// -@system unittest { - //TODO: Test embedded string quotes - assert(arguments("") == []); - assert(arguments("\n") == []); - assert(arguments("a") == [ "a" ]); - assert(arguments("simple") == [ "simple" ]); - assert(arguments("simple a b c") == [ "simple", "a", "b", "c" ]); - assert(arguments("simple test\n") == [ "simple", "test" ]); - assert(arguments("simple test\r\n") == [ "simple", "test" ]); - assert(arguments("/simple/ /test/") == [ "/simple/", "/test/" ]); - assert(arguments(`simple 'test extreme'`) == [ "simple", "test extreme" ]); - assert(arguments(`simple "test extreme"`) == [ "simple", "test extreme" ]); - assert(arguments(`simple ' hehe '`) == [ "simple", " hehe " ]); - assert(arguments(`simple " hehe "`) == [ "simple", " hehe " ]); - assert(arguments(`a 'b c' d`) == [ "a", "b c", "d" ]); - 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" ]); -} 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/memory.d b/src/utils/memory.d deleted file mode 100644 index 876fb68..0000000 --- a/src/utils/memory.d +++ /dev/null @@ -1,76 +0,0 @@ -module utils.memory; - -import std.stdio : File; -import std.container.array; - -//TODO: Use OutBuffer or Array!ubyte when writing changes? -struct MemoryStream { - private ubyte[] buffer; - private long position; - - bool err, eof; - - void cleareof() - { - eof = false; - } - void clearerr() - { - err = false; - } - - // read file into memory - /*void open(string path) - { - }*/ - void open(ubyte[] data) - { - buffer = new ubyte[data.length]; - buffer[0..$] = data[0..$]; - } - void open(File stream) - { - buffer = buffer.init; - //TODO: use OutBuffer+reserve (if possible to get filesize) - foreach (ubyte[] a; stream.byChunk(4096)) - { - buffer ~= a; - } - - } - - long seek(long pos) - { - /*final switch (origin) with (Seek) { - case start:*/ - return position = pos; - /* return 0; - case current: - position += pos; - return 0; - case end: - position = size - pos; - return 0; - }*/ - } - - ubyte[] read(size_t size) - { - long p2 = position + size; - - if (p2 > buffer.length) - return buffer[position..$]; - - return buffer[position..p2]; - } - - // not inout ref, just want to read - ubyte[] opSlice(size_t n1, size_t n2) - { - return buffer[n1..n2]; - } - - long size() { return buffer.length; } - - long tell() { return position; } -}