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; }
-}