Newer
Older
ddhx / src / main.d
/// Command-line interface.
///
/// Copyright: dd86k <dd@dax.moe>
/// License: MIT
/// Authors: $(LINK2 https://github.com/dd86k, dd86k)
module main;

import std.compiler : version_major, version_minor;
import std.stdio, std.mmfile, std.format, std.getopt;
import std.conv : text;
import core.stdc.stdlib : exit;
import ddhx, dumper;
import utils.strings;
import display : Format, selectFormat;
import transcoder : CharacterSet;
import logger;

private: // Shuts up the linter

/// Copyright string
immutable string DDHX_COPYRIGHT = "Copyright (c) 2017-2024 dd86k <dd@dax.moe>";
/// 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 <https://mit-license.org/>\n"~
    "Homepage: <https://git.dd86k.space/dd86k/ddhx>\n"~
    "Compiler: "~__VENDOR__~" "~COMPILER_VERSION;

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

string argstdin(string filename)
{
    return filename == "-" ? null : filename;
}
long lparse(string v)
{
    if (v[0] != '+')
        throw new Exception(text("Missing '+' prefix to argument: ", v));
    
    return cparse(v[1..$]);
}

debug enum TRACE = true;
else  enum TRACE = false;

int main(string[] args)
{
    bool otrace = TRACE;
    bool odump;
    bool oreadonly;
    int ofmtdata = Format.hex;
    int ofmtaddr = Format.hex;
    int ocolumns = 16;
    int ocharset = CharacterSet.ascii;
    long oskip;
    long olength;
    
    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) {
                ofmtaddr = selectFormat(fmt);
            },
        "data",         "Set data mode ('hex', 'dec', or 'oct')",
            (string _, string fmt) {
                ofmtdata = selectFormat(fmt);
            },
        //"filler",       "Set non-printable default character (default='.')", &cliOption,
        "C|charset",    "Set character translation (default=ascii)",
            (string _, string charset) {
                switch (charset) with (CharacterSet)
                {
                case "cp437":   ocharset = cp437; break;
                case "ebcdic":  ocharset = ebcdic; break;
                case "mac":     ocharset = mac; break;
                case "ascii":   ocharset = ascii; break;
                default:
                    throw new Exception(text("Invalid charset: ", charset));
                }
            },
        "r|readonly",   "Open file in read-only editing mode", &oreadonly,
        // Editor input mode
        // Application options
        "dump",         "Dump file non-interactively", &odump,
        "s|seek",       "Seek at position",
            (string _, string len) { oskip = 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. Used in debugging", &otrace,
        );
    }
    catch (Exception ex)
    {
        stderr.writeln("error: ", ex.msg);
        return 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 [OPTIONS] [+POSITION] [+LENGTH] [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);
        }
        
        return 0;
    }
    
    // Positional arguments
    string filename = void;
    try switch (args.length)
    {
    case 1: // Missing filename, implicit stdin
        filename = null;
        break;
    case 2: // Has filename or -
        filename = argstdin(args[1]);
        break;
    case 3: // Has position and filename
        oskip = lparse(args[1]);
        filename = argstdin(args[2]);
        break;
    case 4: // Has position, length, and filename
        oskip = lparse(args[1]);
        olength = lparse(args[2]);
        filename = argstdin(args[3]);
        break;
    default:
        throw new Exception("Too many arguments");
    }
    catch (Exception ex)
    {
        stderr.writeln("error: ", ex.msg);
        return 2;
    }
    
    if (otrace) traceInit();
    trace("version=%s args=%u", DDHX_VERSION, args.length);
    
    // App: dump
    if (odump)
    {
        //TODO: If args.length < 2 -> open stream
        return dump(filename, ocolumns, oskip, olength, ocharset);
    }
    
    // App: interactive
    return ddhx_start(filename, oreadonly, oskip, olength, ocolumns, ocharset);
}