Newer
Older
ddhx / editor / app.d
/// Main module, handling core TUI operations.
/// 
/// Copyright: dd86k <dd@dax.moe>
/// 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);
}