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