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 core.stdc.stdlib : exit;
import editor.file;
import ddhx.display;
import ddhx.transcoder : CharacterSet;
import ddhx.formatter : Format;
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 additionall
//         per-digit basis.
//       View
//         Position and length of the "view" of the view buffer within file or memory buffer.

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,
    
    // Clear the current message
    UMESSAGE    = 1 << 8,
    
    //
    URESET = UCURSOR | UVIEW | UHEADER,
}

private __gshared
{
    FileEditor _efile;
    
    BUFFER *dispbuffer;
    
    int _etrows;
    int _etcols;
    
    long _efilesize;
    
    /// Position of the view, in bytes
    long _eviewpos;
    /// Size of the view, in bytes
    int _eviewsize;
    
    /// 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;
}

int start(string path)
{
    trace("ddhx starting");
    
    //TODO: Stream support
    //      With length, build up a memory buffer
    if (path == null)
    {
        trace("todo: Stdin");
        stderr.writeln("todo: Stdin");
        return 2;
    }
    
    // Open file
    if (_efile.open(path, true, _oreadonly))
    {
        trace("error: Could not open file");
        stderr.writeln("error: Could not open file");
        return 3;
    }
    
    _efilesize = _efile.size();
    trace("filesize=%d", _efilesize);
    
    // 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;
}

void setupscreen()
{
    disp_size(_etrows, _etcols);
    if (_etrows < 4 || _etcols < 20)
    {
        stderr.writeln("error: Terminal too small");
        exit(4);
    }
    trace("term.rows=%d term.cols=%d", _etrows, _etcols);
    
    // Set number of columns, or automatically get column count
    // from terminal.
    _ocolumns = _ocolumns > 0 ? _ocolumns : optimalcols();
    trace("hintcols=%d", _ocolumns);
    
    // Get "view" buffer size, in bytes
    _eviewsize = optimalvwsz(_ocolumns);
    
    // Create buffer
    dispbuffer = disp_create(_etrows - 2, _ocolumns, 0);
    if (dispbuffer == null)
    {
        stderr.writeln("error: Unknown error creating display");
        exit(5);
    }
    
    // Allocate buffer according to desired cols
    trace("viewsize=%d", _eviewsize);
    _efile.setbuffer( _eviewsize );
    
    // Initially render these things
    _estatus = URESET;
}

// Get optimal column count for current terminal size
int optimalcols()
{
    int esize = disp_elem_msize(_odatafmt);
    return (_etcols - _oaddrpad) / (esize + 1); // 16 was address size?
}

// Get optimal view size for the view buffer
int optimalvwsz(int cols)
{
    int esize = disp_elem_msize(_odatafmt);
    return ((_etcols - cols) / (esize + 1)) * (_etrows - 2);
}

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 >= _efilesize)
        tmp = _efilesize;
    else 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 >= _efilesize)
        pos = _efilesize;
    else if (pos < 0)
        pos = 0;

    if (pos == _ecurpos)
        return;
    
    _ecurpos = pos;
    _estatus |= UCURSOR;
    _adjust_viewpos();
}
// Adjust the view positon
void _adjust_viewpos()
{
    //TODO: Adjust view position algorithmically
    
    // Cursor is ahead the view
    if (_ecurpos >= _eviewpos + _eviewsize)
    {
        while (_ecurpos >= _eviewpos + _eviewsize)
        {
            _eviewpos += _ocolumns;
            if (_eviewpos >= _efilesize - _eviewsize)
                break;
        }
        _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()
{
    if (_ecurpos == _efilesize)
        return;
    
    moverel(1);
}
void move_up()
{
    if (_ecurpos == 0)
        return;
    
    moverel(-_ocolumns);
}
void move_down()
{
    if (_ecurpos == _efilesize)
        return;
    
    moverel(_ocolumns);
}
void move_pg_up()
{
    if (_ecurpos == 0)
        return;
    
    moverel(-_eviewsize);
}
void move_pg_down()
{
    if (_ecurpos == _efilesize)
        return;
    
    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()
{
    moveabs(_efilesize);
}

// 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 status
    update_status();
    
    // Update cursor position back into view (data) section
    // NOTE: Should always be updated due to frequent movement
    //       That includes messages, cursor naviation, menu invokes, etc.
    int curdiff = cast(int)(_ecurpos - _eviewpos);
    trace("cur=%d", curdiff);
    update_cursor(curdiff, _ocolumns, _oaddrpad);
    
    _estatus = 0;
}

void update_header()
{
    disp_cursor(0, 0);
    disp_header(_ocolumns);
}

void update_view()
{
    _efile.seek(_eviewpos);
    ubyte[] data = _efile.read();
    trace("addr=%u data.length=%u", _eviewpos, data.length);
    
    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);
}

// relative cursor position
//TODO: Should be: byte pos + digit pos
void update_cursor(int curpos, int columns, int addrpadd)
{
    enum hexsize = 3;
    int row = 1 + (curpos / columns);
    int col = (addrpadd + 2 + ((curpos % columns) * hexsize));
    disp_cursor(row, col);
    
    // Editing in progress
    /*if (editbuf && editsz)
    {
        disp_write(editbuf, editsz);
    }*/
}

void update_status()
{
    //TODO: Could limit write length by terminal width?
    //TODO: check number of edits
    enum STATBFSZ = 2 * 1024;
    char[STATBFSZ] statbuf = void;
    int statlen = snprintf(statbuf.ptr, STATBFSZ, "%s | %s", "test".ptr, "test2".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);
}