/// Terminal screen handling. /// Copyright: dd86k <dd@dax.moe> /// License: MIT /// Authors: $(LINK2 github.com/dd86k, dd86k) module screen; import std.range : chunks; import std.stdio : stdout; // for cwrite family import ddhx; // for setting, NumberType import os.terminal, os.file; version (Trace) import std.datetime.stopwatch; //TODO: Data grouping (1, 2, 4, 8, 16) // e.g., cd ab -> abcd, 128 64 -> 192 // cast(uint[]) is probably possible on a ubyte[] range //TODO: Group endianness (when >1) // native (default), little, big //TODO: View display mode (data+text, data, text) // Currently very low priority //TODO: Consider hiding cursor when drawing // + save/restore position // terminalCursorHide() // windows: SetConsoleCursorInfo // https://docs.microsoft.com/en-us/windows/console/setconsolecursorinfo // posix: \033[?25l // terminalCursorShow() // windows: SetConsoleCursorInfo // posix: \033[?25h //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. //TODO: Consider buffer strategy full for terminal-altscreen mode // + manual flushes here /// Last known terminal size. __gshared TerminalSize termSize; // Internal buffer filling character. // For dump, that should be spaces and 0, // For interactive, that should be spaces and spaces. //__gshared char binaryFiller; //__gshared char textFiller; void initiate() { terminalInit(TermFeat.all); } void updateTermSize() { termSize = terminalSize; } //string screenPrompt(string prompt) /// Update cursor position on the terminal screen void cursor(uint pos, uint nibble) { //TODO: (x * 3) -> x * datawidth uint y = pos / setting.width; uint x = pos % setting.width; terminalPos(13 + (x * 3) + nibble, 1 + y); } /// Clear entire terminal screen void clear() { terminalClear; } string name() { return dataFmt.name; } /*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 screenMessage(A...)(const(char)[] fmt, A args) { //TODO: Consider using a scoped outbuffer + private screenMessage(outbuf) import std.format : format; screenMessage(format(fmt, args)); } /// Display a message at the bottom of the screen. /// Params: str = Message. void screenMessage(const(char)[] str) { //TODO: Consider using a scoped outbuffer + private screenMessage(outbuf) terminalPos(0, termSize.height - 1); cwritef("%-*s", termSize.width - 1, str); } private struct NumberFormatter { string name; /// Short offset name align(2) char fmtchar; /// Format character for printf-like functions uint size; /// Size for formatted byte size_t function(char*,long) offset; /// Function to format offset size_t function(char*,ubyte) data; /// Function to format data } private immutable NumberFormatter[3] numbers = [ { "hex", 'x', 2, &format11x, &format02x }, { "dec", 'u', 3, &format11d, &format03d }, { "oct", 'o', 3, &format11o, &format03o }, ]; // // SECTION Formatting // //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.outbuffer : OutBuffer; import std.typecons : scoped; import std.conv : octal; version (Trace) { StopWatch sw = StopWatch(AutoStart.yes); } // Setup index formatting //TODO: Consider SingleSpec or "maker" function int dsz = numbers[setting.dataType].size; __gshared char[4] offsetFmt = " %__"; offsetFmt[2] = cast(char)(dsz + '0'); offsetFmt[3] = numbers[setting.offsetType].fmtchar; auto outbuf = scoped!OutBuffer(); outbuf.reserve(16 + (setting.width * dsz)); outbuf.write("Offset("); outbuf.write(numbers[setting.offsetType].name); outbuf.write(") "); // Add offsets uint i; for (; i < setting.width; ++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 ...) { import std.outbuffer : OutBuffer; import std.typecons : scoped; version (Trace) { StopWatch sw = StopWatch(AutoStart.yes); } int w = termSize.width; auto outbuf = scoped!OutBuffer(); outbuf.reserve(w); outbuf.put(' '); foreach (item; items) { if (outbuf.offset > 1) outbuf.put(" | "); outbuf.put(item); if (outbuf.offset >= w) { } } // Fill rest by space outbuf.data[outbuf.offset..w] = ' '; outbuf.offset = w; // used in .toBytes version (Trace) { Duration a = sw.peek; } L_WRITE: 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"); } } /// Update display from buffer. /// Returns: Numbers of row written. uint renderContent(long position, ubyte[] data) { version (Trace) { trace("position=%u data.len=%u", position, data.length); StopWatch swtotal = StopWatch(AutoStart.yes); } // Setup formatting related stuff prepareView; // print lines in bulk (for entirety of view buffer) uint lines; foreach (chunk; chunks(data, setting.width)) { cwriteln(renderRow(chunk, position)); position += setting.width; ++lines; } version (Trace) { swtotal.stop; trace("totaltime='%s µs'", swtotal.peek.total!"usecs"); } return lines; } private void prepareView(NumberType offset = setting.offsetType, NumberType data = setting.dataType) { offsetFmt = &numbers[offset]; dataFmt = &numbers[data]; } private __gshared immutable(NumberFormatter) *offsetFmt; private __gshared immutable(NumberFormatter) *dataFmt; //TODO: bool insertPosition? //TODO: bool insertData? //TODO: bool insertText? private char[] renderRow(ubyte[] chunk, long pos) { import core.stdc.string : memset; //TODO: Consider realloc on terminal width // In screen.initiate enum BUFFER_SIZE = 2048; __gshared char[BUFFER_SIZE] buffer; __gshared char *bufferptr = buffer.ptr; // Insert OFFSET size_t indexData = offsetFmt.offset(bufferptr, pos); bufferptr[indexData++] = ' '; // index: OFFSET + space const uint dataLen = (setting.width * (dataFmt.size + 1)); /// data row character count size_t indexChar = indexData + dataLen; // Position for character column *(cast(ushort*)(bufferptr + 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 foreach (data; chunk) { //TODO: Maybe binary data formatter should include space // Data translation bufferptr[indexData++] = ' '; indexData += dataFmt.data(bufferptr + indexData, data); // Character translation immutable(char)[] units = transcoder.transform(data); if (units.length) { // Has utf-8 codepoints foreach (codeunit; units) bufferptr[indexChar++] = codeunit; } else // Invalid character, insert default character bufferptr[indexChar++] = setting.defaultChar; } size_t end = indexChar; // data length < minimum row requirement = in-fill data and text columns if (chunk.length < setting.width) { // In-fill characters: left = Columns - ChunkLength size_t leftchar = (setting.width - chunk.length); // Bytes left memset(bufferptr + indexChar, ' ', leftchar); // In-fill binary data: left = CharactersLeft * (DataSize + 1) size_t leftdata = leftchar * (dataFmt.size + 1); memset(bufferptr + indexData, ' ', leftdata); end += leftchar; } return buffer[0..end]; } //TODO: More renderRow unittests //TODO: Maybe split rendering components? unittest { // With defaults prepareView; // Offset(hex) 0 1 2 3 4 5 6 7 8 9 a b c d e f assert(renderRow([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf ], 0) == " 0 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ................"); assert(renderRow([ 0 ], 0x10) == " 10 00 . "); } // !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 cwriteln(const(char)[] _str) { return cwrite(_str) + cwrite('\n'); } size_t cwritef(A...)(const(char)[] fmt, A args) { import std.format : sformat; char[128] 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