From 79a3028f8ae348829f0eb38519900beaa235e8a5 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 11 Feb 2026 07:22:55 -0500 Subject: [PATCH 01/63] CLI version using inline --- src/CMakeLists.txt | 2 +- src/cli.c | 286 +++--- src/cli.h | 21 +- src/debugger.c | 25 +- src/debugger.h | 6 +- src/help.c | 33 +- src/help.h | 4 +- src/inline.c | 2135 ++++++++++++++++++++++++++++++++++++++++++++ src/inline.h | 223 +++++ src/linedit.c | 1927 --------------------------------------- src/linedit.h | 285 ------ src/main.c | 1 + 12 files changed, 2517 insertions(+), 2431 deletions(-) create mode 100644 src/inline.c create mode 100644 src/inline.h delete mode 100644 src/linedit.c delete mode 100644 src/linedit.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 720827f..8a5085d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,6 @@ target_sources(morpho6 cli.c cli.h debugger.c debugger.h help.c help.h - linedit.c linedit.h + inline.c inline.h main.c ) \ No newline at end of file diff --git a/src/cli.c b/src/cli.c index fdcf0ea..1deb161 100644 --- a/src/cli.c +++ b/src/cli.c @@ -6,6 +6,7 @@ #include #include +#include #include "cli.h" @@ -31,46 +32,62 @@ char *cli_globalsrc=NULL; #define GRY "\x1B[38;2;128;128;128m" #define RESET "\x1B[0m" +#define BOLD "\x1B[1m" +#define ITALIC "\x1B[3m" +#define UNDERLINE "\x1B[4m" + +void inline_setutf8(void); +void inline_emitcolor(int color); +void inline_emit(const char *seq); + +void cli_emitemphasis(int emph) { + switch (emph) { + case CLI_NOEMPHASIS: inline_emit(RESET); break; + case CLI_BOLD: inline_emit(BOLD); break; + case CLI_UNDERLINE: inline_emit(UNDERLINE); break; + case CLI_ITALIC: inline_emit(ITALIC); break; + default: break; + } +} + /** Displays several strings with a specified style using linedit */ -void cli_displaywithstyle(lineditor *edit, linedit_color col, linedit_emphasis emph, int n, ...) { +void cli_displaywithstyle(int col, int emph, int n, ...) { va_list args; va_start(args, n); for (int i=0; icat!=ERROR_NONE) { - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, "Error '", err->id, "'"); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, "Error '", err->id, "'"); if (ERROR_ISRUNTIMEERROR(*err)) { - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); morpho_stacktrace(v); } else { if (err->line!=ERROR_POSNUNIDENTIFIABLE && err->posn!=ERROR_POSNUNIDENTIFIABLE) { char posnbuffer[CLI_BUFFERSIZE]; snprintf(posnbuffer, CLI_BUFFERSIZE, " [line %u char %u", err->line, err->posn+1); - linedit_displaywithstyle(&linedit, posnbuffer, CLI_ERRORCOLOR, CLI_NOEMPHASIS); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 1, posnbuffer); if (err->file) { - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, " in module '", err->file, "'"); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, " in module '", err->file, "'"); } - linedit_displaywithstyle(&linedit, "] ", CLI_ERRORCOLOR, CLI_NOEMPHASIS); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 1, "] "); } - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); } } - - linedit_clear(&linedit); } /* ********************************************************************** @@ -79,9 +96,8 @@ void cli_reporterror(error *err, vm *v) { /** Print callback */ void cli_printcallbackfn(vm *v, void *ref, char *string) { - lineditor *l = (lineditor *) ref; - - cli_displaywithstyle(l, CLI_DEFAULTCOLOR, LINEDIT_BOLD, 1, string); + inline_editor *l = (inline_editor *) ref; + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_BOLD, 1, string); } /** Input callback */ @@ -90,20 +106,16 @@ void cli_inputcallbackfn(vm *v, void *ref, morphoinputmode mode, varray_char *st int key = getchar(); if (key!=EOF) varray_charwrite(str, (char) key); } else { - lineditor line; - linedit_init(&line); - linedit_setprompt(&line, ""); - char *out=linedit(&line); + inline_editor *line=inline_new(""); + char *out=inline_readline(line); if (out) varray_charadd(str, out, (int) strlen(out)); - linedit_clear(&line); + inline_free(line); } } /** Warning callback */ void cli_warningcallbackfn(vm *v, void *ref, error *err) { - lineditor *l = (lineditor *) ref; - - cli_displaywithstyle(l, CLI_WARNINGCOLOR, CLI_NOEMPHASIS, 5, "Warning '", err->id, "': ", err->msg, "\n"); + cli_displaywithstyle(CLI_WARNINGCOLOR, CLI_NOEMPHASIS, 5, "Warning '", err->id, "': ", err->msg, "\n"); } /** Warning callback */ @@ -116,109 +128,49 @@ void cli_debuggercallbackfn(vm *v, void *ref) { * ********************************************************************** */ /** Define colors for different token types */ -linedit_colormap cli_tokencolors[] = { - { TOKEN_NEWLINE, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_QUESTION, LINEDIT_YELLOW }, - - { TOKEN_STRING, LINEDIT_BLUE }, - { TOKEN_INTERPOLATION, LINEDIT_BLUE }, - { TOKEN_INTEGER, LINEDIT_BLUE }, - { TOKEN_NUMBER, LINEDIT_BLUE }, - { TOKEN_SYMBOL, LINEDIT_CYAN }, - - { TOKEN_LEFTPAREN, LINEDIT_DEFAULTCOLOR }, - { TOKEN_RIGHTPAREN, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LEFTSQBRACKET, LINEDIT_DEFAULTCOLOR }, - { TOKEN_RIGHTSQBRACKET, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LEFTCURLYBRACKET, LINEDIT_DEFAULTCOLOR }, - { TOKEN_RIGHTCURLYBRACKET, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_COLON, LINEDIT_DEFAULTCOLOR }, - { TOKEN_SEMICOLON, LINEDIT_DEFAULTCOLOR }, - { TOKEN_COMMA, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_PLUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_MINUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_STAR, LINEDIT_DEFAULTCOLOR }, - { TOKEN_SLASH, LINEDIT_DEFAULTCOLOR }, - { TOKEN_CIRCUMFLEX, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_PLUSPLUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_MINUSMINUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_PLUSEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_MINUSEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_STAREQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_SLASHEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_HASH, LINEDIT_DEFAULTCOLOR }, - { TOKEN_AT, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_QUOTE, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DOT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DOTDOT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DOTDOTDOT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EXCLAMATION, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_AMP, LINEDIT_DEFAULTCOLOR }, - { TOKEN_VBAR, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DBLAMP, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DBLVBAR, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EQUAL, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_NEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_GT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LTEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_GTEQ, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_TRUE, LINEDIT_MAGENTA }, - { TOKEN_FALSE, LINEDIT_MAGENTA }, - { TOKEN_NIL, LINEDIT_MAGENTA }, - { TOKEN_SELF, LINEDIT_MAGENTA }, - { TOKEN_SUPER, LINEDIT_MAGENTA }, - { TOKEN_IMAG, LINEDIT_BLUE }, - - { TOKEN_PRINT, LINEDIT_MAGENTA }, - { TOKEN_VAR, LINEDIT_MAGENTA }, - { TOKEN_IF, LINEDIT_MAGENTA }, - { TOKEN_ELSE, LINEDIT_MAGENTA }, - { TOKEN_IN, LINEDIT_MAGENTA }, - { TOKEN_WHILE, LINEDIT_MAGENTA }, - { TOKEN_FOR, LINEDIT_MAGENTA }, - { TOKEN_DO, LINEDIT_MAGENTA }, - { TOKEN_BREAK, LINEDIT_MAGENTA }, - { TOKEN_CONTINUE, LINEDIT_MAGENTA }, - { TOKEN_FUNCTION, LINEDIT_MAGENTA }, - { TOKEN_RETURN, LINEDIT_MAGENTA }, - { TOKEN_CLASS, LINEDIT_MAGENTA }, - { TOKEN_IMPORT, LINEDIT_MAGENTA }, - { TOKEN_AS, LINEDIT_MAGENTA }, - { TOKEN_IS, LINEDIT_MAGENTA }, - { TOKEN_WITH, LINEDIT_MAGENTA }, - { TOKEN_TRY, LINEDIT_MAGENTA }, - { TOKEN_CATCH, LINEDIT_MAGENTA }, - - { TOKEN_SHEBANG, LINEDIT_DEFAULTCOLOR }, - { TOKEN_INCOMPLETE, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EOF, LINEDIT_DEFAULTCOLOR }, - { LINEDIT_ENDCOLORMAP, LINEDIT_DEFAULTCOLOR } + +int palette[] = { + CLI_DEFAULTCOLOR, // 0 default + INLINE_YELLOW, // 1 help + INLINE_BLUE, // 2 string/integer/number literals + INLINE_CYAN, // 3 symbol + INLINE_MAGENTA // 4 keyword }; +tokentype help[] = { TOKEN_QUESTION }; +tokentype literal[] = { TOKEN_STRING, TOKEN_INTERPOLATION, TOKEN_INTEGER, TOKEN_NUMBER, TOKEN_IMAG }; +tokentype symbols[] = { TOKEN_SYMBOL }; +tokentype keywords[] = { TOKEN_TRUE, TOKEN_FALSE, TOKEN_NIL, TOKEN_SELF, TOKEN_SUPER, TOKEN_PRINT, TOKEN_VAR, TOKEN_IF, TOKEN_ELSE, TOKEN_IN, TOKEN_WHILE, TOKEN_FOR, TOKEN_DO, TOKEN_BREAK, TOKEN_CONTINUE, TOKEN_FUNCTION, + TOKEN_RETURN, TOKEN_CLASS, TOKEN_IMPORT, TOKEN_AS, TOKEN_IS, TOKEN_VAR, TOKEN_WITH, TOKEN_TRY, TOKEN_CATCH }; + +/** Checks if match matches any tokentype in a given list */ +static bool matchtokentype(tokentype match, size_t n, tokentype *list) { + for (size_t i=0; istart=(char *) tok.start; - out->length=tok.length; - out->type=(linedit_tokentype) tok.type; + out->color=0; + if (tok.start>in+offset) { // Token began after offset + out->byte_end=tok.start-in; + } else { // A real token + out->byte_end=offset+tok.length; + if (matchtokentype(tok.type, sizeof(help)/sizeof(help[0]), help)) out->color=1; + else if (matchtokentype(tok.type, sizeof(literal)/sizeof(literal[0]), literal)) out->color=2; + else if (matchtokentype(tok.type, sizeof(symbols)/sizeof(symbols[0]), symbols)) out->color=3; + else if (matchtokentype(tok.type, sizeof(keywords)/sizeof(keywords[0]), keywords)) out->color=4; + } success=(tok.type!=TOKEN_EOF); } @@ -227,12 +179,14 @@ bool cli_lex(char *in, void *ref, linedit_token *out) { return success; } +static char *words[] = {"as", "and", "break", "class", "continue", "do", "else", "for", "false", "fn", "help", "if", "in", "import", "nil", "or", "print", "return", "true", "var", "while", "quit", "self", "super", "this", "try", "catch", NULL}; + /** Autocomplete function */ -bool cli_complete(char *in, void *ref, linedit_stringlist *c) { +const char *cli_complete(const char *in, void *ref, size_t *index) { size_t len=strlen(in); /* First find the last token in the input */ - char *tok = in+len; + const char *tok = in+len; /* Scan backwards from end of string over alphanumeric tokens */ while (tok>in && !isspace(*(tok-1))) tok--; @@ -241,33 +195,29 @@ bool cli_complete(char *in, void *ref, linedit_stringlist *c) { /* Now try to match the token against a library of words */ len=strlen(tok); - char *words[] = {"as", "and", "break", "class", "continue", "do", "else", "for", "false", "fn", "help", "if", "in", "import", "nil", "or", "print", "return", "true", "var", "while", "quit", "self", "super", "this", "try", "catch", NULL}; + int success=false; - for (unsigned int i=0; words[i]!=NULL; i++) { + for (size_t i=*index; words[i]!=NULL; i++) { if ( (lencat!=ERROR_NONE) { @@ -311,23 +261,15 @@ size_t libgrapheme_graphemefn(const char *in, const char *end) { /** @brief Provide a command line interface */ void cli(clioptions opt) { - bool tty=linedit_checktty(); + bool tty=inline_checktty(); version morphoversion; morpho_version(&morphoversion); char morphoversionstring[VERSION_MAXSTRINGLENGTH]; version_tostring(&morphoversion, VERSION_MAXSTRINGLENGTH, morphoversionstring); if (tty) { - linedit_setutf8(); - #ifdef MORPHO_LONG_BANNER - // Original ASCII art source - https://www.asciiart.eu/animals/insects/butterflies - printf(BLU " ___ ___ \n" RESET); - printf(BLU "(" CYN " @ " GRY"\\Y/" CYN " @ " BLU ") " RESET " | morpho %s | \U0001F44B Type 'help' or '?' for help\n", morphoversionstring); - printf(BLU " \\" CYN"__" GRY"+|+" CYN"__" BLU"/ " RESET " | Documentation: https://morpho-lang.readthedocs.io/en/latest/ \n"); - printf(BLU" {" CYN"_" BLU "/ \\" CYN "_" BLU"} " RESET " | Code: https://github.com/Morpho-lang/morpho \n\n"); - #else + inline_setutf8(); printf("\U0001F98B morpho %s | \U0001F44B Type 'help' or '?' for help\n", morphoversionstring); - #endif } /* Set up program and compiler */ @@ -345,18 +287,17 @@ void cli(clioptions opt) { vm *v = morpho_newvm(); /* Line editor */ - lineditor edit; + inline_editor *edit = inline_new(CLI_PROMPT); lexer l; - linedit_init(&edit); - linedit_setprompt(&edit, CLI_PROMPT); - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); - linedit_multiline(&edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); - linedit_autocomplete(&edit, cli_complete, NULL); + inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); + inline_multiline(edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); + inline_autocomplete(edit, cli_complete, NULL); #ifdef CLI_USELIBUNISTRING - linedit_setgraphemesplitter(&edit, libunistring_graphemefn); + inline_setgraphemesplitter(&edit, libunistring_graphemefn); #endif #ifdef CLI_USELIBGRAPHEME - linedit_setgraphemesplitter(&edit, libgrapheme_graphemefn); + inline_setgraphemesplitter(edit, libgrapheme_graphemefn); #endif morpho_setinputfn(v, cli_inputcallbackfn, NULL); @@ -375,16 +316,16 @@ void cli(clioptions opt) { if (!tty && n>0) break; char *input=NULL; - while (!input) input=linedit(&edit); + while (!input) input=inline_readline(edit); /* Check for CLI commands. */ /* Let the user quit by typing 'quit'. */ if (strncmp(input, CLI_QUIT, strlen(CLI_QUIT))==0) { break; } else if (strncmp(input, CLI_HELP, strlen(CLI_HELP))==0) { - cli_help(&edit, input+strlen(CLI_HELP), &err, help); continue; + cli_help(edit, input+strlen(CLI_HELP), &err, help); continue; } else if (strncmp(input, CLI_SHORT_HELP, strlen(CLI_SHORT_HELP))==0) { - cli_help(&edit, input+strlen(CLI_SHORT_HELP), &err, help); continue; + cli_help(edit, input+strlen(CLI_SHORT_HELP), &err, help); continue; } /* Compile code */ @@ -414,7 +355,7 @@ void cli(clioptions opt) { } } - linedit_clear(&edit); + inline_free(edit); morpho_freevm(v); varray_charclear(&src); @@ -436,10 +377,9 @@ void cli_run(const char *in, clioptions opt) { vm *v = morpho_newvm(); /* Set up line editor for output */ - lineditor edit; + inline_editor *edit=inline_new(CLI_PROMPT); lexer l; - linedit_init(&edit); - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); morpho_setinputfn(v, cli_inputcallbackfn, &edit); morpho_setprintfn(v, cli_printcallbackfn, &edit); @@ -488,7 +428,7 @@ void cli_run(const char *in, clioptions opt) { printf("Could not open file '%s'.\n", in); } - linedit_clear(&edit); + inline_free(edit); MORPHO_FREE(src); morpho_freevm(v); @@ -540,56 +480,54 @@ char *cli_loadsource(const char *in) { * ********************************************************************** */ /** Displays a single line of source */ -static void cli_printline(lineditor *edit, int line, char *prompt, const char *src, int length) { +static void cli_printline(inline_editor *edit, int line, char *prompt, const char *src, int length) { printf("%s %4u : ", prompt, line); /* Display the src line */ char srcline[length]; strncpy(srcline, src, length-1); srcline[length-1]='\0'; - linedit_displaywithsyntaxcoloring(edit, srcline); + inline_displaywithsyntaxcoloring(edit, srcline); printf("\n"); } /** Disassembles the program showing syntax colored lines of source */ void cli_disassemblewithsrc(program *p, char *src) { - lineditor edit; - linedit_init(&edit); + inline_editor *edit = inline_new(""); + if (!edit) return; lexer l; - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); int line=1, length=0; for (unsigned int i=0; src[i]!='\0'; i++) { length++; if (src[i]=='\n' || src[i]=='\0') { - cli_printline(&edit, line, ">>>", src+i-length+1, length); + cli_printline(edit, line, ">>>", src+i-length+1, length); morpho_disassemble(NULL, p, NULL); line++; length=0; } } - linedit_clear(&edit); + inline_free(edit); } /** Displays a source listing from source lines start to end */ void cli_list(const char *src, int start, int end) { - lineditor edit; - if (src) { - linedit_init(&edit); + inline_editor *edit = inline_new(""); + if (!edit) return; lexer l; - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); int line=1, length=0; for (unsigned int i=0; src[i]!='\0'; i++) { length++; if (src[i]=='\n' || src[i]=='\0') { - if (line>=start && line <=end) cli_printline(&edit, line, "", src+i-length+1, length); + if (line>=start && line <=end) cli_printline(edit, line, "", src+i-length+1, length); line++; length=0; } } - - linedit_clear(&edit); + inline_free(edit); } } diff --git a/src/cli.h b/src/cli.h index e934143..3b16931 100644 --- a/src/cli.h +++ b/src/cli.h @@ -11,18 +11,22 @@ #include #include -#include "linedit.h" +#include "inline.h" #include "help.h" #include "debugger.h" -#define CLI_DEFAULTCOLOR LINEDIT_DEFAULTCOLOR -#define CLI_ERRORCOLOR LINEDIT_RED -#define CLI_WARNINGCOLOR LINEDIT_YELLOW -#define CLI_NOEMPHASIS LINEDIT_NONE +#define CLI_DEFAULTCOLOR -1 +#define CLI_ERRORCOLOR INLINE_RED +#define CLI_WARNINGCOLOR INLINE_YELLOW -#define CLI_PROMPT ">" -#define CLI_CONTINUATIONPROMPT "~" +#define CLI_NOEMPHASIS -1 +#define CLI_BOLD 0 +#define CLI_UNDERLINE 1 +#define CLI_ITALIC 2 + +#define CLI_PROMPT "> " +#define CLI_CONTINUATIONPROMPT "~ " #define CLI_QUIT "quit" #define CLI_HELP "help" #define CLI_SHORT_HELP "?" @@ -38,10 +42,9 @@ typedef unsigned int clioptions; extern char *cli_globalsrc; -void cli_displaywithstyle(lineditor *edit, linedit_color col, linedit_emphasis emph, int n, ...); +void cli_displaywithstyle(int col, int emph, int n, ...); void cli_reporterror(error *err, vm *v); - void cli_run(const char *in, clioptions opt); void cli(clioptions opt); diff --git a/src/debugger.c b/src/debugger.c index a82e1d8..203080e 100644 --- a/src/debugger.c +++ b/src/debugger.c @@ -28,13 +28,13 @@ __declspec(dllimport) extern objecttype objectstringtype; typedef struct { debugger *debug; /** Debugger */ - lineditor *edit; /** lineeditor for output */ + inline_editor *edit; /** lineeditor for output */ error *err; /** Error structure to fill out */ char *info; /** Report any info to the user after error messages */ bool stop; } clidebugger; -void clidebugger_init(clidebugger *debug, vm *v, lineditor *edit, error *err) { +void clidebugger_init(clidebugger *debug, vm *v, inline_editor *edit, error *err) { debug->debug=vm_getdebugger(v); debugger_seterror(debug->debug, err); debug->edit=edit; @@ -49,8 +49,8 @@ void clidebugger_init(clidebugger *debug, vm *v, lineditor *edit, error *err) { /** Display the morpho banner */ void clidebugger_banner(clidebugger *debug) { - cli_displaywithstyle(debug->edit, DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Morpho debugger---\n"); - cli_displaywithstyle(debug->edit, CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "Type '?' or 'h' for help.\n"); + cli_displaywithstyle(DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Morpho debugger---\n"); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "Type '?' or 'h' for help.\n"); morpho_printf(debugger_currentvm(debug->debug), "%s ", (debug->debug->singlestep ? "Single stepping" : "Breakpoint")); debugger_showlocation(debug->debug, debug->debug->iindx); @@ -60,13 +60,13 @@ void clidebugger_banner(clidebugger *debug) { /** Display the resume text */ void clidebugger_resumebanner(clidebugger *debug) { - cli_displaywithstyle(debug->edit, DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Resuming----------\n"); + cli_displaywithstyle(DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Resuming----------\n"); } /** Display an error message */ void clidebugger_reporterror(clidebugger *debug) { if (debug->err->cat!=ERROR_NONE) { - cli_displaywithstyle(debug->edit, DEBUGGER_ERROR_COLOR, CLI_NOEMPHASIS, 3, "Error: ", debug->err->msg, "\n"); + cli_displaywithstyle(DEBUGGER_ERROR_COLOR, CLI_NOEMPHASIS, 3, "Error: ", debug->err->msg, "\n"); } } @@ -116,7 +116,7 @@ void clidebugger_clearinfo(clidebugger *debug) { /** Show info */ void clidebugger_showinfo(clidebugger *debug) { - if (debug->info) cli_displaywithstyle(debug->edit, CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, debug->info); + if (debug->info) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, debug->info); } /* ********************************************************************** @@ -586,17 +586,14 @@ void clidebugger_enter(vm *v) { error err; error_init(&err); - lineditor edit; - linedit_init(&edit); - linedit_setprompt(&edit, DEBUGGER_PROMPT); - + inline_editor *edit = inline_new(DEBUGGER_PROMPT); clidebugger debug; - clidebugger_init(&debug, v, &edit, &err); + clidebugger_init(&debug, v, edit, &err); clidebugger_banner(&debug); while (!debug.stop) { clidebugger_clearinfo(&debug); - char *input = linedit(&edit); + char *input = inline_readline(edit); if (!input) break; if (!clidebugger_parse(&debug, input) || @@ -610,7 +607,7 @@ void clidebugger_enter(vm *v) { clidebugger_resumebanner(&debug); - linedit_clear(&edit); + inline_free(edit); error_clear(&err); } diff --git a/src/debugger.h b/src/debugger.h index 60b070a..31a95c7 100644 --- a/src/debugger.h +++ b/src/debugger.h @@ -9,10 +9,10 @@ #include "cli.h" -#define DEBUGGER_PROMPT "@>" +#define DEBUGGER_PROMPT "@> " -#define DEBUGGER_COLOR LINEDIT_GREEN -#define DEBUGGER_ERROR_COLOR LINEDIT_RED +#define DEBUGGER_COLOR INLINE_GREEN +#define DEBUGGER_ERROR_COLOR INLINE_RED #define DBG_PRS "DbgPrs" #define DBG_PRS_MSG "Couldn't parse command." diff --git a/src/help.c b/src/help.c index 90a14be..f49b1ea 100644 --- a/src/help.c +++ b/src/help.c @@ -17,6 +17,7 @@ __declspec(dllimport) extern objecttype objectstringtype; __declspec(dllimport) extern objecttype objectlisttype; #endif +#include "cli.h" #include "help.h" /** The interactive help system uses a collection of Markdown files, located in @@ -158,8 +159,8 @@ objecthelptopic *help_search(char *query) { * ********************************************************************** */ /** Display a topic list */ -void help_topiclist(dictionary *dict, lineditor *edit) { - int width = linedit_getwidth(edit), max = 0; +void help_topiclist(dictionary *dict, inline_editor *edit) { + int width = 80 /*linedit_getwidth(edit)*/, max = 0; objectlist list = MORPHO_STATICLIST; varray_valueinit(&list.val); @@ -202,7 +203,7 @@ void help_topiclist(dictionary *dict, lineditor *edit) { } if (k==ncols-1 || i==list.val.count-1) { varray_charadd(&str, "\n\0", 2); - linedit_displaywithsyntaxcoloring(edit, str.data); + inline_displaywithsyntaxcoloring(edit, str.data); str.count=0; k=0; } } @@ -212,15 +213,15 @@ void help_topiclist(dictionary *dict, lineditor *edit) { } /** Parse a 'show' command */ -static void help_show(objecthelptopic *topic, lineditor *edit, char *command) { +static void help_show(objecthelptopic *topic, inline_editor *edit, char *command) { char *c=command; for (; isspace(*c); c++); if (*c=='(') c++; if (strncmp(c, "topics", 6)==0) { - linedit_displaywithstyle(edit, HELP_TOPICS, LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, HELP_TOPICS); help_topiclist(&helpdict, edit); } else if (strncmp(c, "subtopics", 9)==0) { - linedit_displaywithstyle(edit, HELP_SUBTOPICS, LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, HELP_SUBTOPICS); help_topiclist(&topic->subtopics, edit); } } @@ -241,7 +242,7 @@ static size_t help_parsesegment(char *string, char delim) { } /** Parses a section of inline code */ -static char *help_parseinlinecode(lineditor *edit, char *string) { +static char *help_parseinlinecode(inline_editor *edit, char *string) { char *s = string; if (*s=='`') s++; size_t nchars = help_parsesegment(s, '`'); @@ -251,12 +252,12 @@ static char *help_parseinlinecode(lineditor *edit, char *string) { strncpy(str, string+1, nchars); str[nchars]='\0'; - linedit_displaywithsyntaxcoloring(edit, str); + inline_displaywithsyntaxcoloring(edit, str); return s + nchars; } /** Parses an emphasized section */ -static char *help_parseemph(lineditor *edit, char *string, char delim, linedit_color col, linedit_emphasis emph) { +static char *help_parseemph(char *string, char delim, int col, int emph) { char *s = string; if (*s==delim) s++; size_t nchars = help_parsesegment(s, delim); @@ -266,22 +267,22 @@ static char *help_parseemph(lineditor *edit, char *string, char delim, linedit_c strncpy(str, string+1, nchars); str[nchars]='\0'; - linedit_displaywithstyle(edit, str, col, emph); + cli_displaywithstyle(col, emph, 1, str); return s + nchars; } /** Displays a single line of help text */ -static bool help_displayline(objecthelptopic *topic, lineditor *edit, char *line, bool allowheader) { +static bool help_displayline(objecthelptopic *topic, inline_editor *edit, char *line, bool allowheader) { if (line[0]=='#') { if (allowheader) { char *s = line; while (*s=='#' || isspace(*s)) s++; // Skip leading space - linedit_displaywithstyle(edit, s, LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, s); } else { return true; } } else if (line[0]=='\t' || strncmp(line, " ", 4)==0) { - linedit_displaywithsyntaxcoloring(edit, line); + inline_displaywithsyntaxcoloring(edit, line); } else if (line[0]=='[') { /* Process commands */ if (strncmp(line+1, "show", 4)==0) { @@ -303,9 +304,9 @@ static bool help_displayline(objecthelptopic *topic, lineditor *edit, char *line case '`': next=help_parseinlinecode(edit, c); break; case '*': - next=help_parseemph(edit, c, '*', LINEDIT_DEFAULTCOLOR, LINEDIT_BOLD); break; + next=help_parseemph(c, '*', CLI_DEFAULTCOLOR, CLI_BOLD); break; case '_': - next=help_parseemph(edit, c, '_', LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); break; + next=help_parseemph(c, '_', CLI_DEFAULTCOLOR, CLI_UNDERLINE); break; default: printf("%c", *c); break; } @@ -324,7 +325,7 @@ static bool help_displayline(objecthelptopic *topic, lineditor *edit, char *line } /** Displays a help topic */ -void help_display(lineditor *edit, objecthelptopic *topic) { +void help_display(inline_editor *edit, objecthelptopic *topic) { FILE *f = (topic ? fopen(topic->file, "r") : NULL); char line[HELP_LINELENGTH]; diff --git a/src/help.h b/src/help.h index bdc8287..2f71a15 100644 --- a/src/help.h +++ b/src/help.h @@ -12,7 +12,7 @@ #include #include -#include "linedit.h" +#include "inline.h" extern objecttype objecthelptopictype; #define OBJECT_HELPTOPIC objecthelptopictype @@ -33,7 +33,7 @@ typedef struct sobjecthelptopic { size_t help_querylength(char *query, char **s); objecthelptopic *help_search(char *query); -void help_display(lineditor *edit, objecthelptopic *topic); +void help_display(inline_editor *edit, objecthelptopic *topic); bool help_initialize(void); void help_finalize(void); diff --git a/src/inline.c b/src/inline.c new file mode 100644 index 0000000..bf7a080 --- /dev/null +++ b/src/inline.c @@ -0,0 +1,2135 @@ +/** @file inline.c + * @author T J Atherton + * + * @brief A simple grapheme aware line editor with history, completion, multiline editing and syntax highlighting */ + +#include "inline.h" +#include +#include +#include +#include + +#ifdef _WIN32 + #include + #include + #include + #define read _read + #define write _write + #define isatty _isatty + #define STDIN_FILENO _fileno(stdin) + #define STDOUT_FILENO _fileno(stdout) +#else + #include + #include + #include + #include + #include + #include +#endif + +#define INLINE_DEFAULT_BUFFER_SIZE 128 +#define INLINE_DEFAULT_PROMPT ">" + +#define INLINE_ESCAPECODE_MAXLENGTH 32 + +#define INLINE_INVALID -1 + +#define INLINE_TAB_WIDTH 2 + +//#define INLINE_NO_SIGNALS // <- Uncomment to disable installation of signals + +#ifdef _WIN32 +typedef DWORD termstate_t; +#else +typedef struct termios termstate_t; +#endif + +/* ********************************************************************** + * Line editor data structures and configuration + * ********************************************************************** */ + +/** Simple list of strings type */ +typedef struct inline_stringlist { + char **items; // List of strings + int count; // Number of strings + int index; // Current index +} inline_stringlist_t; + +/** Viewport */ +typedef struct { + int first_visible_line; // Vertical scroll offset + int first_visible_col; // Horizontal scroll offset + int screen_rows; // Viewport height + int screen_cols; // Viewport width (excludes prompt, which is not part of the viewport) +} inline_viewport; + +/** The editor data structure */ +typedef struct inline_editor { + char *prompt; + char *continuation_prompt; + + int ncols; // Number of columns + + char *buffer; // Buffer holding UTF8 + size_t buffer_len; // Length of contents in bytes + size_t buffer_size; // Size of buffer allocated in bytes + + char *clipboard; // Clipboard buffer + size_t clipboard_len; // Length of contents in bytes + size_t clipboard_size; // Size of clipboard in bytes + + size_t *graphemes; // Offset to each grapheme + int grapheme_count; // Number of graphemes + size_t grapheme_size; // Size of grapheme buffer in bytes + + size_t *lines; // Offset to each line + int line_count; // Number of lines + size_t line_size; // Size of line buffer in bytes + + int cursor_posn; // Position of cursor in graphemes + int selection_posn; // Selection posn in graphemes + int term_cursor_row; // Record the cursor's physical row + int term_lines_drawn; // Record how many lines were previously drawn + + inline_syntaxcolorfn syntax_fn; // Syntax coloring callback + void *syntax_ref; // User reference + + int *palette; // Palette: list of colors + int palette_count; // Length of palette list + + inline_completefn complete_fn; // Autocomplete callback + void *complete_ref; // User reference + + inline_stringlist_t suggestions; // List of suggestions from autocompleter + bool suggestion_shown; // Set if renderer was able to show a suggestion + + inline_multilinefn multiline_fn; // Multiline callback + void *multiline_ref; // User reference + + inline_graphemefn grapheme_fn; // Custom grapheme splitter + inline_widthfn width_fn; // Custom grapheme width function + + inline_stringlist_t history; // List of history entries + int max_history_length; // Maximum length of the history + + inline_viewport viewport; // Terminal viewport + +#ifdef _WIN32 // Preserve terminal state + termstate_t termstate_in; + termstate_t termstate_out; +#else + termstate_t termstate; +#endif + bool rawmode_enabled; // Record if rawmode has already been enabled + + bool refresh; // Set to refresh on next redraw +} inline_editor; + +static inline_editor *inline_lasteditor = NULL; + +// Forward declarations +static char *inline_strdup(const char *s); +static void inline_disablerawmode(inline_editor *edit); +static void inline_stringlist_init(inline_stringlist_t *list); +static void inline_stringlist_clear(inline_stringlist_t *list); +static void inline_recomputelines(inline_editor *edit); +static void inline_recomputegraphemes(inline_editor *edit); +static bool inline_insert(inline_editor *edit, const char *bytes, size_t nbytes); +static void inline_clear(inline_editor *edit); +static void inline_clearselection(inline_editor *edit); +static void inline_clearsuggestions(inline_editor *edit); + +/* ----------------------- + * New/free API + * ----------------------- */ + +/** API function to create a new line editor */ +inline_editor *inline_new(const char *prompt) { + inline_editor *edit = calloc(1, sizeof(*edit)); // All contents are zero'd + if (!edit) return NULL; + + edit->prompt = inline_strdup(prompt ? prompt : INLINE_DEFAULT_PROMPT); + if (!edit->prompt) goto inline_new_cleanup; + + edit->buffer_size = INLINE_DEFAULT_BUFFER_SIZE; // Allocate initial buffer + edit->buffer = malloc(edit->buffer_size); + if (!edit->buffer) goto inline_new_cleanup; + + edit->buffer[0] = '\0'; // Ensure zero terminated + edit->buffer_len = 0; + + edit->selection_posn = INLINE_INVALID; // No selection + edit->max_history_length = INLINE_INVALID; // Unlimited history + + inline_stringlist_init(&edit->suggestions); + inline_stringlist_init(&edit->history); + + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + + return edit; + +inline_new_cleanup: + inline_free(edit); + return NULL; +} + +/** API function to free a line editor and associated resources */ +void inline_free(inline_editor *edit) { + if (!edit) return; + + free(edit->prompt); + free(edit->continuation_prompt); + + free(edit->buffer); + free(edit->graphemes); + free(edit->lines); + free(edit->clipboard); + + inline_clearsuggestions(edit); + inline_stringlist_clear(&edit->history); + + free(edit->palette); + + if (inline_lasteditor==edit) inline_lasteditor = NULL; + + free(edit); +} + +/* ----------------------- + * Configuration API + * ----------------------- */ + +/** API function to enable syntax coloring */ +void inline_syntaxcolor(inline_editor *edit, inline_syntaxcolorfn fn, void *ref) { + edit->syntax_fn = fn; + edit->syntax_ref = ref; +} + +/** API function to set the color palette */ +bool inline_setpalette(inline_editor *edit, int count, const int *palette) { + free(edit->palette); // Clear any old palette data + edit->palette = NULL; + edit->palette_count = 0; + + if (count <= 0 || palette == NULL) return false; + + edit->palette = malloc(sizeof(int) * count); + if (!edit->palette) return false; + + memcpy(edit->palette, palette, sizeof(int) * count); + edit->palette_count = count; + return true; +} + +/** API function to enable autocomplete */ +void inline_autocomplete(inline_editor *edit, inline_completefn fn, void *ref) { + edit->complete_fn = fn; + edit->complete_ref = ref; +} + +/** API function to enable multiline editing */ +bool inline_multiline(inline_editor *edit, inline_multilinefn fn, void *ref, const char *continuation_prompt) { + edit->multiline_fn = fn; + edit->multiline_ref = ref; + + char *p = inline_strdup(continuation_prompt ? continuation_prompt : edit->prompt); + if (p) { + free(edit->continuation_prompt); + edit->continuation_prompt = p; + } + return (p!=NULL); +} + +/** API function to use a custom grapheme splitter */ +void inline_setgraphemesplitter(inline_editor *edit, inline_graphemefn fn) { + edit->grapheme_fn = fn; +} + +/** API function to use a custom grapheme width function */ +void inline_setgraphemewidth(inline_editor *edit, inline_widthfn fn) { + edit->width_fn = fn; +} + +/* ********************************************************************** + * Platform-dependent code + * ********************************************************************** */ + +/* ---------------------------------------- + * Check terminal features + * ---------------------------------------- */ + +/** API function to check whether stdin and stdout are TTYs. */ +bool inline_checktty(void) { + return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO); +} + +/** Check whether the terminal type is supported. */ +static bool inline_checksupported(void) { +#ifndef _WIN32 + const char *term = getenv("TERM"); + if (!term || !*term) return false; + + static const char *deny[] = { "dumb", "cons25", "emacs", NULL }; + + for (const char **p = deny; *p; p++) { + if (strcasecmp(term, *p) == 0) return false; + } +#endif + return true; // Windows and other terminals are supported +} + +/** Update the terminal width */ +static void inline_updateterminalwidth(inline_editor *edit) { + int width = 80; // fallback + +#ifdef _WIN32 + HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO csbi; + + if (GetConsoleScreenBufferInfo(h, &csbi)) { + width = csbi.srWindow.Right - csbi.srWindow.Left + 1; + } +#else + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 && ws.ws_col > 0) { + width = ws.ws_col; + } +#endif + + edit->ncols = width; +} + +/* ---------------------------------------- + * Handle crashes + * ---------------------------------------- */ + +static void inline_atexitrestore(void) { + if (inline_lasteditor) inline_disablerawmode(inline_lasteditor); +} + +#ifdef _WIN32 +static bool termstate_set = false; +static termstate_t termstate_in; +static termstate_t termstate_out; +static int resize_pending = 0; +static bool consolehandler_installed = false; +static BOOL WINAPI inline_consolehandler(DWORD ctrl) { + (void) ctrl; + if (termstate_set) { + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleMode(hIn, termstate_in); + SetConsoleMode(hOut, termstate_out); + } + return FALSE; // Allow default behavior +} +#else +termstate_t termstate; +static volatile sig_atomic_t termstate_set = 0; +static volatile sig_atomic_t resize_pending = 0; +typedef struct { + int sig; + void (*handler)(int, siginfo_t *, void *); + int flags; + bool has_previous; + bool installed; + struct sigaction previous; +} signalhandlerstate_t; + +signalhandlerstate_t *inline_findsighandler(int sig); + +static bool inline_callprevious(int sig, siginfo_t *info, void *ucontext) { + signalhandlerstate_t *handler = inline_findsighandler(sig); + if (!handler || !handler->has_previous || !handler->installed || + handler->previous.sa_handler == SIG_IGN || handler->previous.sa_handler == SIG_DFL) return false; + +#ifdef SA_SIGINFO + if (handler->previous.sa_flags & SA_SIGINFO) { + if (handler->previous.sa_sigaction) handler->previous.sa_sigaction(sig, info, ucontext); + return true; + } +#endif + if (handler->previous.sa_handler) { + handler->previous.sa_handler(sig); + return true; + } + return false; +} + +static void inline_restoredisposition(int sig) { + signalhandlerstate_t *handler = inline_findsighandler(sig); + + if (handler && handler->has_previous && handler->previous.sa_handler != SIG_IGN) { + sigaction(sig, &handler->previous, NULL); + } else { + struct sigaction restore; + memset(&restore, 0, sizeof(restore)); + restore.sa_handler = SIG_DFL; + sigemptyset(&restore.sa_mask); + sigaction(sig, &restore, NULL); + } +} + +static void inline_emergencyrestore(void) { + if (termstate_set) tcsetattr(STDIN_FILENO, TCSAFLUSH, &termstate); +} +static void inline_signalwinchhandler(int sig, siginfo_t *info, void *ucontext) { + resize_pending=1; + inline_callprevious(sig, info, ucontext); +} +static void inline_signalgracefulhandler(int sig, siginfo_t *info, void *ucontext) { + inline_emergencyrestore(); + if (inline_callprevious(sig, info, ucontext)) return; // If the previous signal handler was called and returned, we do too + inline_restoredisposition(sig); + kill(getpid(), sig); + _Exit(128 + sig); +} +static void inline_signalcrashhandler(int sig, siginfo_t *info, void *ucontext) { + inline_emergencyrestore(); + inline_restoredisposition(sig); + kill(getpid(), sig); + _Exit(128 + sig); +} + +static signalhandlerstate_t siglist[] = { + { SIGWINCH, inline_signalwinchhandler, SA_SIGINFO | SA_RESTART, false, false, { {0} } }, + { SIGTERM, inline_signalgracefulhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGQUIT, inline_signalgracefulhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGHUP, inline_signalgracefulhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGSEGV, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGABRT, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGBUS, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGFPE, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, +}; + +signalhandlerstate_t *inline_findsighandler(int sig) { + for (size_t i = 0; i < sizeof(siglist)/sizeof(siglist[0]); i++) if (siglist[i].sig == sig) return &siglist[i]; + return NULL; +} +#endif + +static int install_count = 0; + +/** Register emergency exit and signal handlers */ +static void inline_registeremergencyhandlers(void) { + install_count++; + if (install_count>1) return; + + static bool atexit_registered=false; + if (!atexit_registered) { atexit(inline_atexitrestore); atexit_registered=true; } +#ifdef _WIN32 + if (SetConsoleCtrlHandler(inline_consolehandler, TRUE)) consolehandler_installed=true; +#else + #ifndef INLINE_NO_SIGNALS + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + + for (size_t i = 0; i < sizeof(siglist)/sizeof(siglist[0]); i++) { + siglist[i].has_previous = false; + siglist[i].installed = false; + + if (sigaction(siglist[i].sig, NULL, &siglist[i].previous) == 0) { // Get previous action + if (siglist[i].previous.sa_handler == SIG_IGN) continue; // Skip ignored signals + siglist[i].has_previous = true; + } else memset(&siglist[i].previous, 0, sizeof(siglist[i].previous)); // Wipe + + sa.sa_sigaction = siglist[i].handler; + sa.sa_flags = siglist[i].flags; + if (sigaction(siglist[i].sig, &sa, NULL) == 0) siglist[i].installed=true; + } + #endif +#endif +} + +/** Restore emergency handlers previously installed */ +static void inline_restoreemergencyhandlers(void) { + if (install_count>0) install_count--; + if (install_count>0) return; +#ifdef _WIN32 + if (consolehandler_installed) + if (SetConsoleCtrlHandler(inline_consolehandler, FALSE)) consolehandler_installed = false; +#else + #ifndef INLINE_NO_SIGNALS + for (size_t i = 0; i < sizeof(siglist)/sizeof(siglist[0]); i++) { + if (!siglist[i].has_previous || !siglist[i].installed) continue; + sigaction(siglist[i].sig, &siglist[i].previous, NULL); // Restore previous handler + + siglist[i].installed = false; // Wipe + siglist[i].has_previous = false; + memset(&siglist[i].previous, 0, sizeof(siglist[i].previous)); + } + #endif +#endif +} + +/* ---------------------------------------- + * Switch to/from raw mode + * ---------------------------------------- */ + +/** Enable utf8 */ +void inline_setutf8(void) { +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); +#endif +} + +/** Enter raw mode */ +static bool inline_enablerawmode(inline_editor *edit) { + if (edit->rawmode_enabled) return true; + +#ifdef _WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + if (!GetConsoleMode(hIn, &mode)) return false; + edit->termstate_in = mode; + mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); // Disable cooked mode + mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + if (!SetConsoleMode(hIn, mode)) return false; // Disable cooked mode + + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (!GetConsoleMode(hOut, &edit->termstate_out)) return false; + DWORD newOut = edit->termstate_out | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if (!SetConsoleMode(hOut, newOut)) return false; // Enable VT output + + if (!termstate_set) { + termstate_in = edit->termstate_in; + termstate_out = edit->termstate_out; + termstate_set = true; + } +#else + if (tcgetattr(STDIN_FILENO, &edit->termstate) == -1) return false; + + struct termios raw = edit->termstate; + /* Input: Turn off: IXON - software flow control (ctrl-s and ctrl-q) + ICRNL - translate CR into NL (ctrl-m) + BRKINT - parity checking + ISTRIP - strip bit 8 of each input byte */ + raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP); + /* Output: Turn off: OPOST - output processing */ + raw.c_oflag &= ~(OPOST); + /* Character: CS8 Set 8 bits per byte */ + raw.c_cflag |= (CS8); + /* Turn off: ECHO - causes keypresses to be printed immediately + ICANON - canonical mode, reads line by line + IEXTEN - literal (ctrl-v) + ISIG - turn off signals (ctrl-c and ctrl-z) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* Set return condition for control characters */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) return false; + if (!termstate_set) { + termstate = edit->termstate; + termstate_set = true; + } +#endif + inline_lasteditor = edit; // Record last editor + inline_registeremergencyhandlers(); + + edit->rawmode_enabled = true; + return true; +} + +/** Restore terminal state to normal */ +static void inline_disablerawmode(inline_editor *edit) { + if (!edit || !edit->rawmode_enabled) return; + +#ifdef _WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleMode(hIn, edit->termstate_in); + SetConsoleMode(hOut, edit->termstate_out); +#else + tcsetattr(STDIN_FILENO, TCSAFLUSH, &edit->termstate); +#endif + + fputs("\r", stdout); // Print a carriage return to ensure we're back on the left hand side + edit->rawmode_enabled = false; + inline_restoreemergencyhandlers(); +} + +/* ********************************************************************** + * Utility functions + * ********************************************************************** */ + +/** Min and max */ +#define imin(a,b) ( ab ? a : b) + +/** Duplicate a string */ +static char *inline_strdup(const char *s) { + if (!s) return NULL; + + size_t n = strlen(s) + 1; + char *p = malloc(n); + if (!p) return NULL; + + memcpy(p, s, n); + return p; +} + +/** Ensure the buffer can grow by at least `extra` bytes. */ +static bool inline_extendbufferby(inline_editor *edit, size_t extra) { + if (extra > SIZE_MAX - edit->buffer_len - 1) return false; // Prevent overflow + size_t required = edit->buffer_len + extra + 1; // +1 for null terminator + + if (required <= edit->buffer_size) return true; // Sufficient space already + + size_t newcap = edit->buffer_size ? edit->buffer_size : INLINE_DEFAULT_BUFFER_SIZE; + while (newcap < required) { + if (newcap > SIZE_MAX / 2) return false; + newcap *= 2; // Grow exponentially + } + + void *p = realloc(edit->buffer, newcap); + if (!p) return false; + edit->buffer = p; + edit->buffer_size = newcap; + + return true; +} + +/* ---------------------------------------- + * Grapheme splitting + * ---------------------------------------- */ + +/** Determine length of utf8 character from the first byte */ +static inline int inline_utf8length(unsigned char b) { + if ((b & 0x80) == 0x00) return 1; // 0xxxxxxx + if ((b & 0xE0) == 0xC0) return 2; // 110xxxxx + if ((b & 0xF0) == 0xE0) return 3; // 1110xxxx + if ((b & 0xF8) == 0xF0) return 4; // 11110xxx + return 0; // Invalid or continuation +} + +/** Codepoint definition */ +typedef struct { + const unsigned char *seq; + size_t len; +} codepoint_t; + +#define CODEPOINT(s) { (const unsigned char *) s, sizeof(s)-1 } + +/** Suffix codepoints modify the previous codepoint, but don't join */ +static const codepoint_t suffix_extenders[] = { + CODEPOINT("\xEF\xB8\x8E"), // VS15 (U+FE0E) text presentation + CODEPOINT("\xEF\xB8\x8F"), // VS16 (U+FE0F) emoji presentation + CODEPOINT("\xE2\x83\xA3"), // U+20E3 Keycap combining mark + + // Emoji skin tone modifiers U+1F3FB–U+1F3FF + CODEPOINT("\xF0\x9F\x8F\xBB"), // light skin tone + CODEPOINT("\xF0\x9F\x8F\xBC"), // medium-light skin tone + CODEPOINT("\xF0\x9F\x8F\xBD"), // medium skin tone + CODEPOINT("\xF0\x9F\x8F\xBE"), // medium-dark skin tone + CODEPOINT("\xF0\x9F\x8F\xBF"), // dark skin tone +}; +static const size_t suffix_count = sizeof(suffix_extenders) / sizeof(suffix_extenders[0]); + +/* Joiner codepoints connect the next codepoint into the same grapheme */ +static const codepoint_t joiners[] = { + CODEPOINT("\xE2\x80\x8D"), // ZWJ (U+200D) +}; +static const size_t joiners_count = sizeof(joiners) / sizeof(joiners[0]); +#undef CODEPOINT + +/** Matches a codepoint against a table of possible matches */ +static size_t inline_matchcodepoint(size_t table_count, const codepoint_t *table, const unsigned char *p, const unsigned char *end) { + size_t remaining = (size_t)(end - p); + for (size_t i = 0; i < table_count; i++) { + const codepoint_t *cp = &table[i]; + if (remaining >= cp->len && memcmp(p, cp->seq, cp->len) == 0) { + return cp->len; + } + } + return 0; // no match +} + +/** Minimal grapheme splitter */ +static size_t inline_graphemesplit(const char *in, const char *end) { + const unsigned char *p = (const unsigned char *) in, + *uend = (const unsigned char *) end; + if (p >= uend) return 0; // At end already + + // Read first codepoint + size_t len = inline_utf8length(*p); + if (len == 0) len = 1; // Recover from malformed utf8 codepoint + if ((size_t)(uend - p) < len) return (size_t)(uend - p); + p += len; + + // Combining diacritical marks U+0300–U+036F (accents, etc.) + while (p < uend && *p >= 0xCC && *p <= 0xCF) { + len = inline_utf8length(*p); + if (len == 0 || (size_t)(uend - p) < len) break; + p += len; + } + + do { // Skip past suffix extenders + len = inline_matchcodepoint(suffix_count, suffix_extenders, p, uend); + p += len; + } while (len!=0); + + for (;;) { // Joiners (ZWJ sequences) + len = inline_matchcodepoint(joiners_count, joiners, p, uend); + if (len == 0) break; + p += len; + + if (p >= uend) break; + + len = inline_utf8length((unsigned char)*p); // Process joined codepoint + if (len == 0 || (size_t)(uend - p) < len) break; + p += len; + } + + return (size_t)(p - (const unsigned char *)in); +} + +/* ---------------------------------------- + * Grapheme buffer + * ---------------------------------------- */ + +/** Compute grapheme locations */ +static void inline_recomputegraphemes(inline_editor *edit) { + size_t needed = edit->buffer_len + 1; // Assume 1 byte per character as a worst case + sentinel + + size_t required_bytes = needed * sizeof(size_t); // Ensure capacity + if (required_bytes > edit->grapheme_size) { + size_t newsize = (edit->grapheme_size ? edit->grapheme_size : INLINE_DEFAULT_BUFFER_SIZE); + while (newsize < required_bytes) { + if (newsize > SIZE_MAX / 2) return; + newsize *= 2; + } + + size_t *new = realloc(edit->graphemes, newsize); + if (!new) { + edit->grapheme_count = 0; + return; + } + + edit->graphemes = new; + edit->grapheme_size = newsize; + } + + // Select splitter + inline_graphemefn fn = (edit->grapheme_fn ? edit->grapheme_fn : inline_graphemesplit); + + size_t count = 0; + const char *p = edit->buffer, *end = edit->buffer + edit->buffer_len; + + while (p < end) { // Walk the buffer and record grapheme boundaries + edit->graphemes[count++] = (size_t)(p - edit->buffer); + size_t len = fn(p, end); + if (len == 0) len = 1; // Malformed grapheme + if (len > (size_t)(end - p)) len = (size_t)(end - p); // Size longer than buffer + p += len; + } + + edit->graphemes[count] = edit->buffer_len; // Ensure last entry points to end of buffer + edit->grapheme_count = (int) count; +} + +/** Finds the start and end of grapheme i in bytes */ +static inline void inline_graphemerange(inline_editor *edit, int i, size_t *start, size_t *end) { + if (i < 0 || i >= edit->grapheme_count) { // Handle out of bounds access (incl. i representing end of line) + if (start) *start = edit->buffer_len; + if (end) *end = edit->buffer_len; + return; + } + + if (start) *start = edit->graphemes[i]; + if (end) *end = edit->graphemes[i+1]; +} + +/** Finds the first grapheme index using binary search whose start byte is >= byte_off */ +static int inline_findgraphemeindex(inline_editor *edit, size_t byte_off) { + int lo = 0, hi = edit->grapheme_count; + + while (lo < hi) { + int mid = (lo + hi) / 2; + size_t mid_off = edit->graphemes[mid]; + + if (mid_off < byte_off) lo = mid + 1; + else hi = mid; + } + + return lo; +} + +/* ---------------------------------------- + * Line buffer + * ---------------------------------------- */ + +/** Compute line locations */ +static void inline_recomputelines(inline_editor *edit) { + int count = 0; + + for (int g = 0; g < edit->grapheme_count; g++) // Count newline graphemes + if (edit->buffer[edit->graphemes[g]] == '\n') count++; + + size_t needed = sizeof(size_t) * (count + 2); // Need count+2 entries: first line + each newline + sentinel + if (needed > edit->line_size) { + size_t *new = realloc(edit->lines, needed); + if (!new) return; + edit->lines = new; + edit->line_size = needed; + } + + int i = 0; + edit->lines[i++] = 0; // First line always starts at 0 + + for (int g = 0; g < edit->grapheme_count; g++) // Subsequent lines start after each newline + if (edit->buffer[edit->graphemes[g]] == '\n') + edit->lines[i++] = edit->graphemes[g] + 1; + + edit->lines[i] = edit->buffer_len; // Sentinel + edit->line_count = i; +} + +/* ---------------------------------------- + * Grapheme display width + * ---------------------------------------- */ + +/** Check for ZWJ, VS16, keycap */ +static bool inline_checkextenders(const unsigned char *g, size_t len) { + for (size_t i = 0; i + 2 < len; i++) { + unsigned char a = g[i], b = g[i+1], c = g[i+2]; + if (a == 0xE2 && b == 0x80 && c == 0x8D) return true; // ZWJ + if (a == 0xEF && b == 0xB8 && c == 0x8F) return true; // VS16 + if (a == 0xE2 && b == 0x83 && c == 0xA3) return true; // keycap + } + return false; +} + +/** Predict the display width of a grapheme */ +static int inline_graphemewidth(const char *p, size_t len) { + const unsigned char *g = (const unsigned char *) p; + if (!len) return 0; + if (g[0] == '\t') return INLINE_TAB_WIDTH; // Tab + if (g[0] < 0x80) return 1; // ASCII fast path + + if (len >= 2 && (g[0] == 0xCC || g[0] == 0xCD)) return 0; // Combining-only grapheme (rare) + if (inline_checkextenders(g, len)) return 2; // Check for ZWJ, VS16 and other extenders + if (len >= 2 && g[0] == 0xEF && (g[1] == 0xBC || g[1] == 0xBD)) return 2; // Fullwidth forms (U+FF00 block) + + if (len >= 4 && (g[0] & 0xF8) == 0xF0) { // Emoji block (U+1F300–U+1FAFF) + if ((g[1] & 0xC0) != 0x80 || (g[2] & 0xC0) != 0x80 || (g[3] & 0xC0) != 0x80) return 1; + unsigned cp = ((g[0] & 0x07) << 18) | ((g[1] & 0x3F) << 12) | + ((g[2] & 0x3F) << 6) | (g[3] & 0x3F); + if (cp >= 0x1F300 && cp <= 0x1FAFF) return 2; + } + + if (len >= 3 && g[0] >= 0xE4 && g[0] <= 0xE9) { // CJK Unified Ideographs (U+4E00–U+9FFF) + if ((g[1] & 0xC0) != 0x80 || (g[2] & 0xC0) != 0x80) return 1; + unsigned cp = ((g[0] & 0x0F) << 12) | ((g[1] & 0x3F) << 6) | (g[2] & 0x3F); + if (cp >= 0x4E00 && cp <= 0x9FFF) return 2; + } + + return 1; +} + +/** Calculate the display width of a utf8 string using current grapheme splitter/width estimator */ +static bool inline_stringwidth(inline_editor *edit, const char *str, int *width) { + inline_graphemefn split_fn = (edit->grapheme_fn ? edit->grapheme_fn : inline_graphemesplit); + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + + const char *p = str, *end = str + strlen(str); + *width = 0; + + while (p < end) { + size_t glen = split_fn(p, end); + if (glen == 0) return false; // Malformed utf8 codepoint + *width += width_fn(p, glen); + p += glen; + } + return true; +} + +/** Compute terminal width of grapheme range [g_start, g_end) */ +static inline int inline_graphemerangewidth(inline_editor *edit, int g_start, int g_end) { + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + int width = 0; + for (int g = g_start; g < g_end; g++) { + size_t s, e; + inline_graphemerange(edit, g, &s, &e); + width += width_fn(edit->buffer + s, e - s); + } + return width; +} + +/* ---------------------------------------- + * String lists + * ---------------------------------------- */ + +/** Initialize a stringlist structure */ +static void inline_stringlist_init(inline_stringlist_t *list) { + list->items=NULL; + list->count=0; + list->index=INLINE_INVALID; +} + +/** Add an entry to a stringlist */ +static bool inline_stringlist_add(inline_stringlist_t *list, const char *s) { + if (!s) return false; // Never add a null pointer + char *copy = inline_strdup(s); + if (!copy) return false; + char **newitems = realloc(list->items, sizeof(char*) * (list->count + 1)); + if (!newitems) { free(copy); return false; } // Don't update if realloc fails + + list->items = newitems; + list->items[list->count] = copy; + list->count++; + return true; +} + +/** Removes and frees the first element of the stringlist; */ +static void inline_stringlist_popfront(inline_stringlist_t *list) { + if (list->count == 0) return; + free(list->items[0]); + + // Shift pointers down safely (overlapping copy) + if (list->count > 1) memmove(list->items, list->items + 1, sizeof(char*) * (list->count - 1)); + list->count--; +} + +/** Clear a stringlist */ +static void inline_stringlist_clear(inline_stringlist_t *list) { + if (list->items) { + for (int i = 0; i < list->count; i++) free(list->items[i]); + free(list->items); + } + + inline_stringlist_init(list); +} + +/** Get the current string in a stringlist */ +static int inline_stringlist_count(inline_stringlist_t *list) { + return list->count; +} + +/** Get the current string in a stringlist */ +static const char *inline_stringlist_current(inline_stringlist_t *list) { + if (list->count == 0 || list->index<0 || list->index >= list->count) return NULL; + return list->items[list->index]; +} + +/** Advance the current index by delta with optional wrapping */ +static void inline_stringlist_advance(inline_stringlist_t *list, int delta, bool wrap) { + if (list->count == 0 || list->index<0) return; + if (list->index >= list->count) list->index = list->count - 1; // Clamp + if (wrap) { + list->index = (list->index + delta + list->count) % list->count; + } else { + list->index = list->index + delta; + if (list->index<0) list->index=0; + if (list->index>=list->count) list->index=list->count-1; + } +} + +/* ---------------------------------------- + * Selections + * ---------------------------------------- */ + +/** Find the start and end points of a selection, if one is present. */ +static bool inline_selectionrange(inline_editor *edit, int *sel_l, int *sel_r, size_t *start, size_t *end) { + if (edit->selection_posn == INLINE_INVALID) return false; + + int l = imin(edit->selection_posn, edit->cursor_posn); + int r = imax(edit->selection_posn, edit->cursor_posn); + + if (sel_l) *sel_l = l; + if (sel_r) *sel_r = r; + if (start) inline_graphemerange(edit, l, start, NULL); + if (end) inline_graphemerange(edit, r, end, NULL); + + return true; +} + +/* ---------------------------------------- + * Clipboard + * ---------------------------------------- */ + +/** Copies a string of given length onto the clipboard. */ +static bool inline_copytoclipboard(inline_editor *edit, const char *string, size_t length) { + if (!string || length==0) { // Empty clipboard + if (edit->clipboard) edit->clipboard[0] = '\0'; + edit->clipboard_len = 0; + return true; + } + + size_t needed = length + 1; // Check if we have sufficient capacity and realloc if necessary + if (needed > edit->clipboard_size) { + size_t newsize = edit->clipboard_size ? edit->clipboard_size : INLINE_DEFAULT_BUFFER_SIZE; + while (newsize < needed) { + if (newsize > SIZE_MAX / 2) return false; + newsize *= 2; + } + + char *newbuf = realloc(edit->clipboard, newsize); + if (!newbuf) return false; // Leave clipboard unchanged on allocation failure + + edit->clipboard = newbuf; + edit->clipboard_size = newsize; + } + + memmove(edit->clipboard, string, length); // Copy onto clipboard + edit->clipboard[length] = '\0'; // Ensure null termination + edit->clipboard_len = length; + return true; +} + +/* ---------------------------------------- + * Autocomplete / Suggestions + * ---------------------------------------- */ + +/** Check if the cursor is at the end of the buffer */ +static bool inline_atend(inline_editor *edit) { + return edit->cursor_posn == edit->grapheme_count; +} + +/** Adds a suggestion to the suggestion list */ +static void inline_addsuggestion(inline_editor *edit, const char *s) { + inline_stringlist_add(&edit->suggestions, s); +} + +/** Clears the suggestion list */ +static void inline_clearsuggestions(inline_editor *edit) { + inline_stringlist_clear(&edit->suggestions); +} + +/** Generates suggestions by repeatedly calling the completion callback */ +static void inline_generatesuggestions(inline_editor *edit) { + if (!edit->complete_fn) return; + inline_clearsuggestions(edit); + if (edit->selection_posn!=INLINE_INVALID) return; // Enforce that suggestions cannot be generated while a selection is active + + if (edit->buffer && inline_atend(edit)) { + size_t index = 0; + const char *s; + while ((s=edit->complete_fn(edit->buffer, edit->complete_ref, &index))!=0) { + inline_addsuggestion(edit, s); + } + if (edit->suggestions.count > 0) edit->suggestions.index = 0; + } +} + +/** Check if suggestions are available */ +static bool inline_havesuggestions(inline_editor *edit) { + return inline_stringlist_count(&edit->suggestions) > 0; +} + +/** Returns the current suggestion */ +static const char *inline_currentsuggestion(inline_editor *edit) { + if (!inline_havesuggestions(edit)) return NULL; + return inline_stringlist_current(&edit->suggestions); +} + +/** Advance through the suggestions by delta (can be negative; we wrap around) */ +static void inline_advancesuggestions(inline_editor *edit, int delta) { + inline_stringlist_advance(&edit->suggestions, delta, true); +} + +/* ---------------------------------------- + * History + * ---------------------------------------- */ + +/** Set the history length. */ +void inline_sethistorylength(inline_editor *edit, int maxlen) { + edit->max_history_length=maxlen; + + if (maxlen > 0) { // Remove excess entries if necessary + while (edit->history.count > maxlen) inline_stringlist_popfront(&edit->history); + } else if (maxlen == 0) { // Clear history entirely + inline_stringlist_clear(&edit->history); + } +} + +/** Adds an entry to the history list */ +bool inline_addhistory(inline_editor *edit, const char *entry) { + if (!entry || !*entry || !edit->max_history_length) return false; // Skip empty buffers + + if (edit->history.count > 0) { // Avoid duplicate consecutive entries + const char *last = edit->history.items[edit->history.count - 1]; + if (strcmp(last, entry) == 0) return false; + } + + inline_stringlist_add(&edit->history, entry); + if (edit->max_history_length > 0 && edit->history.count > edit->max_history_length) inline_stringlist_popfront(&edit->history); + return true; +} + +/** Advances the current history */ +static void inline_advancehistory(inline_editor *edit, int delta) { + int count = inline_stringlist_count(&edit->history); + if (count == 0) return; + + // Enter history mode if we're not in it + if (edit->history.index == INLINE_INVALID) edit->history.index = count - 1; + else inline_stringlist_advance(&edit->history, delta, false); // No wrap + + // Load entry or exit history mode + const char *s = inline_stringlist_current(&edit->history); + inline_clear(edit); + if (s) { + inline_insert(edit, s, strlen(s)); + } else edit->history.index = INLINE_INVALID; // Exit history mode +} + +/** End browsing */ +static void inline_endhistorybrowsing(inline_editor *edit) { + edit->history.index = INLINE_INVALID; +} + +/* ---------------------------------------- + * Reset + * ---------------------------------------- */ + +/** Resets the editor before a new session */ +static void inline_reset(inline_editor *edit) { + inline_clear(edit); + inline_clearselection(edit); + inline_endhistorybrowsing(edit); + inline_stringlist_clear(&edit->suggestions); + edit->rawmode_enabled = false; + edit->term_cursor_row = 0; + edit->term_lines_drawn = 0; +} + +/* ---------------------------------------- + * Viewport + * ---------------------------------------- */ + +/** Initialize the viewport */ +static void inline_initviewport(inline_editor *edit) { + edit->viewport.first_visible_line = 0; + edit->viewport.first_visible_col = 0; + edit->viewport.screen_rows = 1; // Will adjust for multiline editing later + int prompt_width; + if (!inline_stringwidth(edit, edit->prompt, &prompt_width)) prompt_width = 0; + edit->viewport.screen_cols = edit->ncols - prompt_width - 1; // Reserve last col to avoid pending wrap state +} + +/** Compute logical cursor position in rows and columns */ +static void inline_cursorposn(inline_editor *edit, int *out_row, int *out_col) { + size_t byte_pos = edit->graphemes[edit->cursor_posn]; // byte offset of cursor + + int row = 0; // Find the row containing the cursor + while (row + 1 < edit->line_count && edit->lines[row + 1] <= byte_pos) row++; + + if (out_row) *out_row = row; + // The column is found by subtracting the grapheme offset of the start of the row + if (out_col) *out_col = edit->cursor_posn - inline_findgraphemeindex(edit, edit->lines[row]); +} + +/** Check the cursor is visible */ +static void inline_ensurecursorvisible(inline_editor *edit) { + int cursor_row, cursor_col; + inline_cursorposn(edit, &cursor_row, &cursor_col); + + int line_start_g = inline_findgraphemeindex(edit, edit->lines[cursor_row]); + int cursor_g = line_start_g + cursor_col; + + int cursor_term_col = inline_graphemerangewidth(edit, line_start_g, cursor_g); + + int first = edit->viewport.first_visible_col; + int end = first + edit->viewport.screen_cols; // exclusive + + if (cursor_term_col < first) { + edit->viewport.first_visible_col = cursor_term_col; + + } else if (cursor_term_col >= end) { + edit->viewport.first_visible_col = + cursor_term_col - edit->viewport.screen_cols; + } +} + +/* ********************************************************************** + * Rendering + * ********************************************************************** */ + +#define TERM_RESETCOLOR "\x1b[0m" +#define TERM_CLEAR "\x1b[K" +#define TERM_RESETFOREGROUND "\x1b[39m" +#define TERM_HIDECURSOR "\x1b[?25l" +#define TERM_SHOWCURSOR "\x1b[?25h" +#define TERM_FAINT "\x1b[2m" +#define TERM_INVERSEVIDEO "\x1b[7m" + +/** Write an escape sequence to the terminal */ +void inline_emit(const char *seq) { + write(STDOUT_FILENO, seq, (unsigned int) strlen(seq)); +} + +/** Writes an escape sequence to produce a given color */ +void inline_emitcolor(int color) { + if (color < 0) return; // default + char seq[INLINE_ESCAPECODE_MAXLENGTH]; + int n = 0; + + if (color < 16) { // ANSI 8 or bright 8 + int base = (color < 8 ? 30 : 90); + n = snprintf(seq, sizeof(seq), "\x1b[%dm", base + (color & 7)); + } else if (color <= 255) { // 256-color palette 8–255 + n = snprintf(seq, sizeof(seq), "\x1b[38;5;%dm", color); + } else { // Assume RGB packed as 0x01RRGGBB + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color >> 0) & 0xFF; + n = snprintf(seq, sizeof(seq), "\x1b[38;2;%d;%d;%dm", r, g, b); + } + + if (n > 0) write(STDOUT_FILENO, seq, n); +} + +/** Clip grapheme range [*g_start, *g_end) horizontally based on viewport */ +static inline void inline_clipgraphemerange(inline_editor *edit, int line_start, int *g_start, int *g_end) { + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + + int start_col = edit->viewport.first_visible_col; + int end_col = start_col + edit->viewport.screen_cols; + + int col = inline_graphemerangewidth(edit, line_start, *g_start); + int start = -1; + int end = *g_start; + + for (int i = *g_start; i < *g_end; i++) { + size_t s, e; + inline_graphemerange(edit, i, &s, &e); + int w = width_fn(edit->buffer + s, e - s); + + if ((col >= start_col) && (col < end_col)) { + if (start < 0) start = i; // First visible grapheme + end = i + 1; // extend visible range + } + + if (col + w > end_col) break; + col += w; + } + + if (start < 0) start = *g_end; // Clamp if line is empty or viewport is beyond end + if (end < start) end = start; + else if (end > start && edit->buffer[edit->graphemes[end-1]] == '\n') end--; + + *g_start = start; + *g_end = end; +} + +/** Move terminal cursor to the editor's origin */ +static inline void inline_movetoorigin(inline_editor *edit) { + write(STDOUT_FILENO, "\r", 1); // Move to start of current line + + if (edit->term_cursor_row > 0) { // Move up cursor_row lines + char seq[INLINE_ESCAPECODE_MAXLENGTH]; + int n = snprintf(seq, sizeof(seq), "\x1b[%dA", edit->term_cursor_row); + write(STDOUT_FILENO, seq, n); + } +} + +/** Move the cursor by a specified delta; down is positive dy */ +static inline void inline_moveby(int dx, int dy) { + char seq[INLINE_ESCAPECODE_MAXLENGTH]; + + if (dy<0) { // Up + int n = snprintf(seq, sizeof(seq), "\x1b[%dA", abs(dy)); + write(STDOUT_FILENO, seq, n); + } else { + for (int i = 0; i < dy; i++) inline_emit("\n"); // Ensure scroll + } + + if (dx!=0) { // Horizontal + int n = snprintf(seq, sizeof(seq), "\x1b[%d%c", abs(dx), (dx < 0 ? 'D' : 'C')); + write(STDOUT_FILENO, seq, n); + } +} + +/** Render a single line of text + * @param[in] - edit - the editor + * @param[in] - prompt - prompt for this line + * @param[in] - byte_start - byte offset for the start of the line + * @param[in] - byte_end - byte offset for the end of the line + * @param[in] - logical_cursor_col - column the cursor should be displayed in logical coordinates, or -1 if not on this line + * @param[in] - is_last - whether this is the last line + * @param[out] - rendered_cursor_col - if logical_cursor_col indicates the cursor is on this line, + * set to logical column the cursor should be rendered on, incuding clipping + * and prompt widt, or -1 if outside clipping window; otherwise not changed. */ +static void inline_renderline(inline_editor *edit, const char *prompt, size_t byte_start, size_t byte_end, + int logical_cursor_col, bool is_last, int *rendered_cursor_col) { + write(STDOUT_FILENO, prompt, (unsigned int) strlen(prompt)); // Write prompt + int prompt_width = 0; // Calculate its display width + if (!inline_stringwidth(edit, prompt, &prompt_width)) prompt_width = 0; + + int rendered_width = prompt_width; // Track rendered width + int rendered_cursor_posn = -1; + + // Compute selection bounds, if active + int sel_l = INLINE_INVALID, sel_r = INLINE_INVALID; + if (edit->selection_posn != INLINE_INVALID) { + sel_l = imin(edit->selection_posn, edit->cursor_posn); + sel_r = imax(edit->selection_posn, edit->cursor_posn); + } + + // Compute grapheme range for this line; remember the true start of the line + int line_start = inline_findgraphemeindex(edit, byte_start); + int g_start = line_start, g_end = inline_findgraphemeindex(edit, byte_end); + + // Apply horizontal clipping + inline_clipgraphemerange(edit, line_start, &g_start, &g_end); + + int current_color = -1; + bool selection_on = false; // Track the terminal inverse video state + + // Render syntax-colored, clipped graphemes + int g = g_start; + size_t off = edit->graphemes[g_start]; + + inline_syntaxcolorfn syntax_fn = (edit->palette_count>0 ? edit->syntax_fn : NULL); + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + + // Render syntax-colored, clipped graphemes + while (g < g_end && off < byte_end) { + // Compute color span from current point + inline_colorspan_t span = { .byte_end = off + 1, .color = 0 }; + bool ok=false; + if (syntax_fn) ok=syntax_fn(edit->buffer, edit->syntax_ref, off, &span); + if (!ok || span.byte_end <= off) span.byte_end = byte_end; // treat rest of line as uncolored + + int span_color = (span.color>=0 && span.color < edit->palette_count ? edit->palette[span.color] : -1); + + // Change color only if needed + if (span_color != current_color) { + if (current_color != -1) { + inline_emit(TERM_RESETCOLOR); + selection_on = false; + } + if (span_color >= 0) inline_emitcolor(span_color); + current_color = span_color; + } + + // Print graphemes until we reach span.byte_end (clipped) + for (; g < g_end; g++) { + size_t gs, ge; + inline_graphemerange(edit, g, &gs, &ge); + + if (gs >= span.byte_end) break; + + bool in_selection = (g >= sel_l && g < sel_r); // Are we in a selection? + if (in_selection != selection_on) { // Does terminal state match? + if (in_selection) inline_emit(TERM_INVERSEVIDEO); // Start reverse video + else { + inline_emit(TERM_RESETCOLOR); + if (current_color >= 0) inline_emitcolor(current_color); // Reapply syntax color + } + selection_on = in_selection; + } + + if (edit->buffer[gs] == '\n') break; + + if (logical_cursor_col >= 0 && // Check if this grapheme was where the cursor is + line_start + logical_cursor_col == g) rendered_cursor_posn = rendered_width; + + if (edit->buffer[gs] == '\t') { + for (int i=0; ibuffer + gs, (unsigned int) (ge - gs)); + rendered_width += width_fn(edit->buffer + gs, ge - gs); + } + + off = span.byte_end; + } + + if (selection_on || current_color != -1) inline_emit(TERM_RESETCOLOR); + + // Ghosted suggestion suffix (only if at right edge on last line) + if (is_last && g_end == edit->grapheme_count && logical_cursor_col >= 0) { + const char *suffix = inline_currentsuggestion(edit); + edit->suggestion_shown=false; + if (suffix && *suffix) { + int remaining_cols = edit->viewport.screen_cols - rendered_width; + + // Width of suggestion + int ghost_width = 0; + if (!inline_stringwidth(edit, suffix, &ghost_width)) ghost_width = 0; + + if (ghost_width <= remaining_cols) { // Show suggestion as faint text + edit->suggestion_shown=true; + inline_emit(TERM_FAINT); + write(STDOUT_FILENO, suffix, (unsigned int) strlen(suffix)); + inline_emit(TERM_RESETCOLOR); + } + } + } + + if (logical_cursor_col >= 0) { // Update cursor position if on this line + if (rendered_cursor_posn >= 0) *rendered_cursor_col = rendered_cursor_posn; + else *rendered_cursor_col = rendered_width; // cursor at end + } + + if (rendered_width < edit->viewport.screen_cols) inline_emit(TERM_CLEAR); // Clear to end of line +} + +/** Redraw the entire buffer in multiline mode */ +static void inline_redraw(inline_editor *edit) { + inline_emit(TERM_HIDECURSOR); // Prevent flickering + inline_movetoorigin(edit); + + int cursor_row, cursor_col; // Compute logical cursor column and row (pre-clipping) + inline_cursorposn(edit, &cursor_row, &cursor_col); + + int rendered_cursor_col = -1; // To be filled out by inline_renderline + for (int i = 0; i < edit->line_count; i++) { + size_t byte_start = edit->lines[i]; // Render lines + size_t byte_end = edit->lines[i+1]; + bool is_last = (i == edit->line_count - 1); + + inline_emit("\r"); // Move cursor to start of line + + inline_renderline(edit, (i==0 ? edit->prompt : edit->continuation_prompt), // prompt + byte_start, byte_end, + (cursor_row == i ? cursor_col : -1), // cursor column if on this line + is_last, // whether we're on the last line or not + &rendered_cursor_col ); + + if (i + 1 < edit->line_count) inline_emit("\n"); // Move to next line if not at end + } + + int extra = (edit->term_lines_drawn > edit->line_count ? edit->term_lines_drawn - edit->line_count : 0); + for (int i = 0; i < extra; i++) { + inline_emit("\n\r"); + inline_emit(TERM_CLEAR); + } + + write(STDOUT_FILENO, "\r", 1); // Move to start of line + inline_moveby(rendered_cursor_col, cursor_row - edit->line_count - extra + 1); + edit->term_cursor_row = cursor_row; // Record cursor row + edit->term_lines_drawn = edit->line_count; // Record no. of lines drawn + inline_emit(TERM_SHOWCURSOR); +} + +/** API function to print a syntax colored string */ +void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string) { + if (!edit || !string) return; + fflush(stdout); + size_t len = strlen(string); + + if (!edit->syntax_fn || !edit->palette_count) { // Syntax highlighting not configured, fallback to plain + write(STDOUT_FILENO, string, (unsigned int) len); + return; + } + + size_t offset = 0; + while (offset < len) { // + inline_colorspan_t span = { .byte_end = offset, .color=-1}; + + bool ok = edit->syntax_fn(string, edit->syntax_ref, offset, &span); // Obtain next span + if (!ok || span.byte_end <= offset) { // No more spans or broken callback; print the rest uncolored + write(STDOUT_FILENO, string + offset, (unsigned int) (len - offset)); + return; + } + + if (span.color < edit->palette_count && span.color >= 0) inline_emitcolor(edit->palette[span.color]); + for (size_t i = offset; i < span.byte_end; i++) { + if (string[i] == '\t') { + for (int t = 0; t < INLINE_TAB_WIDTH; t++) inline_emit(" "); + } else write(STDOUT_FILENO, &string[i], 1); + } + + inline_emit(TERM_RESETFOREGROUND); + + offset = span.byte_end; + } + fflush(stdout); +} + +/* ********************************************************************** + * Keypress decoding + * ********************************************************************** */ + +/* ---------------------------------------- + * Raw input layer + * ---------------------------------------- */ + +/** Type that represents a single unit of input */ +typedef unsigned char rawinput_t; + +#ifdef _WIN32 +static bool inline_readkeyevent(KEY_EVENT_RECORD *k) { + INPUT_RECORD rec; + DWORD nread; + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + + for (;;) { + if (!ReadConsoleInputW(hIn, &rec, 1, &nread)) return false; + + if (rec.EventType == WINDOW_BUFFER_SIZE_EVENT) { + if (inline_lasteditor) inline_lasteditor->refresh = true; + continue; + } + + if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown) { + *k = rec.Event.KeyEvent; + return true; + } + } +} + +/** Helper to emit an escape sequence */ +static int _emitstr(const char *s, unsigned char out[8]) { + int n = 0; + while (s[n] && n < 8) out[n] = (unsigned char)s[n++]; + return n; +} + +/** Mapping from Windows VK codes to POSIX escape sequences */ +typedef struct { + WORD vk; + const char *seq; +} vkmap_t; + +static const vkmap_t vk_table[] = { + { VK_RETURN, "\n" }, + { VK_BACK, "\b" }, + { VK_DELETE, "\x7f" }, + { VK_UP, "\x1b[A" }, + { VK_DOWN, "\x1b[B" }, + { VK_RIGHT, "\x1b[C" }, + { VK_LEFT, "\x1b[D" }, + { VK_HOME, "\x1b[H" }, + { VK_END, "\x1b[F" }, + { VK_PRIOR, "\x1b[5~" }, // Page Up + { VK_NEXT, "\x1b[6~" }, // Page Down +}; + +/** Convert windows keypress event to POSIX */ +static int inline_translatekeypress(const KEY_EVENT_RECORD *k, unsigned char out[8]) { + WORD vk = k->wVirtualKeyCode; + WCHAR wc = k->uChar.UnicodeChar; + DWORD mods = k->dwControlKeyState; + + // Shift-arrows + if ((mods & SHIFT_PRESSED) && (vk == VK_LEFT || vk == VK_RIGHT)) { + return _emitstr( (vk == VK_LEFT ? "\x1b[1;2D" : "\x1b[1;2C"), out ); + } + + // Search table of mappings + for (size_t i = 0; i < sizeof(vk_table)/sizeof(vk_table[0]); i++) { + if (vk_table[i].vk == vk) return _emitstr(vk_table[i].seq, out); + } + + // Ctrl + char + if (mods & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) { + if (vk >= 'A' && vk <= 'Z') { + out[0] = (unsigned char) (vk - 'A') + 1; + return 1; + } + } + + if (wc != 0) { // Unicode + if (wc < 0x80) { + out[0] = (unsigned char)wc; + return 1; + } else if (wc < 0x800) { + out[0] = (unsigned char)(0xC0 | ((unsigned int)wc >> 6)); + out[1] = (unsigned char)(0x80 | ((unsigned int)wc & 0x3F)); + return 2; + } else if (wc < 0xD800 || wc > 0xDFFF) { + out[0] = 0xE0 | (wc >> 12); + out[1] = 0x80 | ((wc >> 6) & 0x3F); + out[2] = 0x80 | (wc & 0x3F); + return 3; + } else if (wc >= 0xD800 && wc <= 0xDBFF) { // high surrogate + // Need the next KEY_EVENT for the low surrogate + KEY_EVENT_RECORD next; + if (!inline_readkeyevent(&next)) return 0; + + WCHAR wc2 = next.uChar.UnicodeChar; + if (wc2 >= 0xDC00 && wc2 <= 0xDFFF) { + uint32_t cp = 0x10000 + (((wc - 0xD800) << 10) | (wc2 - 0xDC00)); + out[0] = (unsigned char) (0xF0 | (cp >> 18)); + out[1] = (unsigned char) (0x80 | ((cp >> 12) & 0x3F)); + out[2] = (unsigned char) (0x80 | ((cp >> 6) & 0x3F)); + out[3] = (unsigned char) (0x80 | (cp & 0x3F)); + return 4; + } + } + } + + return 0; // Unknown key → ignore +} + +#endif + +/** Await a single raw unit of input and store in a rawinput_t */ +static bool inline_readraw(rawinput_t *out) { +#ifdef _WIN32 + static unsigned char buf[16]; // local ring buffer + static int len = 0; + static int pos = 0; + + if (pos < len) { // Return remaining bytes + *out = buf[pos++]; + return true; + } + + KEY_EVENT_RECORD k; // Get new key event + do { + if (!inline_readkeyevent(&k)) return false; + len = inline_translatekeypress(&k, buf); // Translate it to POSIX + pos = 0; + } while (len == 0); + + *out = buf[pos++]; // Return first byte + return true; +#else + int n = (int) read(STDIN_FILENO, out, 1); + return n == 1; +#endif +} + +/* ---------------------------------------- + * Keypress decoding layer + * ---------------------------------------- */ + +/** Identifies the type of keypress */ +typedef enum { + KEY_UNKNOWN, KEY_CHARACTER, + KEY_RETURN, KEY_TAB, KEY_SHIFT_TAB, KEY_DELETE, + KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, // Arrow keys + KEY_HOME, KEY_END, // Home and End + KEY_PAGE_UP, KEY_PAGE_DOWN, // Page up and page down + KEY_SHIFT_LEFT, KEY_SHIFT_RIGHT, // Shift+arrow key + KEY_CTRL +} keytype_t; + +/** A single keypress event obtained and processed by the terminal */ +typedef struct { + keytype_t type; /** Type of keypress */ + unsigned char c[5]; /** Up to four bytes of utf8 encoded unicode plus null terminator */ + int nbytes; /** Number of bytes */ +} keypress_t; + +static void inline_keypressunknown(keypress_t *keypress) { + keypress->type=KEY_UNKNOWN; + keypress->c[0]='\0'; + keypress->nbytes=0; +} + +static void inline_keypresswithchar(keypress_t *keypress, keytype_t type, char c) { + keypress->type=type; + keypress->c[0]=c; keypress->c[1]='\0'; + keypress->nbytes=1; +} + +/** Map from terminal codes to keytype_t */ +typedef struct { + const char *seq; + keytype_t type; +} escmap_t; + +static const escmap_t esc_table[] = { + { "[A", KEY_UP }, + { "[B", KEY_DOWN }, + { "[C", KEY_RIGHT }, + { "[D", KEY_LEFT }, + { "[H", KEY_HOME }, + { "[F", KEY_END }, + { "[Z", KEY_SHIFT_TAB }, + { "[5~", KEY_PAGE_UP }, + { "[6~", KEY_PAGE_DOWN }, + { "[1;2C", KEY_SHIFT_RIGHT }, + { "[1;2D", KEY_SHIFT_LEFT }, +}; + +static void inline_decode_escape(keypress_t *out) { + unsigned char seq[INLINE_ESCAPECODE_MAXLENGTH+1]; + int i = 0; + out->type = KEY_UNKNOWN; + + // Expect '[' + if (!inline_readraw(&seq[i]) || seq[0] != '[') { return; } + + // Read until alpha terminator + for (i = 1; i < INLINE_ESCAPECODE_MAXLENGTH - 1; i++) { + if (!inline_readraw(&seq[i])) break; + if (isalpha(seq[i]) || seq[i] == '~') break; + } + seq[i + 1] = '\0'; // Ensure null terminated + + // Lookup escape code + for (size_t j = 0; j < sizeof(esc_table)/sizeof(esc_table[0]); j++) { + if (strcmp((const char *)seq, esc_table[j].seq) == 0) { + out->type = esc_table[j].type; + return; + } + } +} + +/** Decode sequence of characters into a utf8 character */ +static void inline_decode_utf8(unsigned char first, keypress_t *out) { + out->nbytes = inline_utf8length(first); + + if (!out->nbytes) return; // Invalid first byte or stray continuation + + out->c[0] = first; + for (int i=1; inbytes; i++) { + if (!inline_readraw(&out->c[i])) { out->c[i] = '\0'; return; } + } + + out->c[out->nbytes] = '\0'; + out->type = KEY_CHARACTER; +} + +/** Raw control codes produced by POSIX terminals */ +enum keycodes { + BACKSPACE_CODE = 8, // Backspace (Ctrl+H) + TAB_CODE = 9, // Tab + LF_CODE = 10, // Line feed + RETURN_CODE = 13, // Enter / Return (CR) + ESC_CODE = 27, // Escape + DELETE_CODE = 127 // Delete (DEL) +}; + +/** Decode raw input units into a keypress */ +static void inline_decode(const rawinput_t *raw, keypress_t *out) { + inline_keypressunknown(out); // Initially UNKNOWN + unsigned char b = *raw; + + if (b < 32 || b == DELETE_CODE) { // Control keys (ASCII control range or DEL) + switch (b) { + case TAB_CODE: out->type = KEY_TAB; return; + case LF_CODE: return; + case RETURN_CODE: out->type = KEY_RETURN; return; + case BACKSPACE_CODE: // v fallthrough + case DELETE_CODE: out->type = KEY_DELETE; return; + case ESC_CODE: + inline_decode_escape(out); + return; + + default: // Control codes are Ctrl+A → 1, Ctrl+Z → 26 + if (b >= 1 && b <= 26) inline_keypresswithchar(out, KEY_CTRL, 'A' + (b - 1)); + return; + } + } + + if (b < 128) { // ASCII regular character + inline_keypresswithchar(out, KEY_CHARACTER, b); + return; + } + + inline_decode_utf8(b, out); // UTF8 +} + +/** Obtain a keypress event */ +static bool inline_readkeypress(inline_editor *edit, keypress_t *out) { + (void) edit; + rawinput_t raw; + if (!inline_readraw(&raw)) return false; + inline_decode(&raw, out); + return true; +} + +/* ********************************************************************** + * Input loop + * ********************************************************************** */ + +/** Update the cursor position */ +static inline void inline_setcursorposn(inline_editor *edit, int new_posn) { + if (new_posn < 0) new_posn = 0; + if (new_posn > edit->grapheme_count) new_posn = edit->grapheme_count; + if (edit->cursor_posn == new_posn) return; + edit->refresh = true; + + int old_row; + inline_cursorposn(edit, &old_row, NULL); + + edit->cursor_posn = new_posn; + inline_ensurecursorvisible(edit); +} + +/** Insert text into the buffer */ +static bool inline_insert(inline_editor *edit, const char *bytes, size_t nbytes) { + if (!inline_extendbufferby(edit, nbytes)) return false; // Ensure capacity + + size_t offset = 0; // Obtain the byte offset of the current cursor position + if (edit->cursor_posn < edit->grapheme_count) offset = edit->graphemes[edit->cursor_posn]; + else offset = edit->buffer_len; + + // Move contents after the insertion point to make room for the inserted text + memmove(edit->buffer + offset + nbytes, edit->buffer + offset, edit->buffer_len - offset); + + memcpy(edit->buffer + offset, bytes, nbytes); // Copy new text into buffer + edit->buffer_len += nbytes; + edit->buffer[edit->buffer_len] = '\0'; // Ensure null-terminated + + int old_count = edit->grapheme_count; // Save grapheme count + + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + + // Move cursor forward by number of graphemes + int inserted_count = edit->grapheme_count - old_count; + inline_setcursorposn(edit, edit->cursor_posn + (inserted_count > 0? inserted_count : 0)); + + edit->refresh = true; // Redraw + return true; +} + +/** Helper to delete bytes [ start, end ) in the buffer */ +static void inline_deletebytes(inline_editor *edit, size_t start, size_t end) { + if (start >= end || end > edit->buffer_len) return; + + size_t bytes = end - start; + memmove(edit->buffer + start, edit->buffer + end, edit->buffer_len - end); // Move subsequent text + edit->buffer_len -= bytes; + + edit->buffer[edit->buffer_len] = '\0'; // Ensure null termination + + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + edit->refresh = true; +} + +/** Helper to delete a grapheme at a given index */ +static void inline_deletegrapheme(inline_editor *edit, int index) { + if (index < 0 || index >= edit->grapheme_count) return; + + size_t start, end; + inline_graphemerange(edit, index, &start, &end); + inline_deletebytes(edit, start, end); +} + +/** Deletes selected text */ +static void inline_deleteselection(inline_editor *edit) { + int sel_l; + size_t start, end; + if (!inline_selectionrange(edit, &sel_l, NULL, &start, &end)) return; + + inline_deletebytes(edit, start, end); // Delete the selection + + edit->selection_posn = INLINE_INVALID; // Clear selection + inline_setcursorposn(edit, sel_l); // Cursor moves to start of deleted region +} + +/** Delete character under cursor */ +static void inline_deletecurrent(inline_editor *edit) { + if (edit->cursor_posn < edit->grapheme_count) { // Delete grapheme under cursor if at start of line + inline_deletegrapheme(edit, edit->cursor_posn); + } +} + +/** Delete text from the buffer */ +static void inline_delete(inline_editor *edit) { + if (edit->selection_posn != INLINE_INVALID) { + inline_deleteselection(edit); + } else if (edit->cursor_posn > 0) { // Delete grapheme before cursor + inline_deletegrapheme(edit, edit->cursor_posn - 1); + inline_setcursorposn(edit, edit->cursor_posn - 1); + } else inline_deletecurrent(edit); +} + +/** Clear the buffer */ +static void inline_clear(inline_editor *edit) { + edit->buffer_len = 0; // Clear text buffer + edit->buffer[0] = '\0'; + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + inline_setcursorposn(edit, 0); // Reset cursor + edit->refresh = true; + edit->suggestion_shown = false; +} + +/** Navigation keys */ +static void inline_navigatetolineboundary(inline_editor *edit, bool end) { + int row; + inline_cursorposn(edit, &row, NULL); + inline_setcursorposn(edit, inline_findgraphemeindex(edit, edit->lines[row + end])); +} + +static void inline_home(inline_editor *edit) { + inline_navigatetolineboundary(edit, false); +} + +static void inline_end(inline_editor *edit) { + inline_navigatetolineboundary(edit, true); +} + +static void inline_pageup(inline_editor *edit) { + inline_setcursorposn(edit, 0); +} + +static void inline_pagedown(inline_editor *edit) { + inline_setcursorposn(edit, edit->grapheme_count); +} + +static void inline_left(inline_editor *edit) { + if (edit->cursor_posn > 0) + inline_setcursorposn(edit, edit->cursor_posn - 1); +} + +static void inline_right(inline_editor *edit) { + if (edit->cursor_posn < edit->grapheme_count) + inline_setcursorposn(edit, edit->cursor_posn + 1); +} + +/** Selection */ +static void inline_beginselection(inline_editor *edit) { + if (edit->selection_posn==INLINE_INVALID) edit->selection_posn = edit->cursor_posn; +} + +static void inline_clearselection(inline_editor *edit) { + edit->selection_posn=INLINE_INVALID; +} + +/** Copy selected text */ +static void inline_copyselection(inline_editor *edit) { + size_t start, end; + if (inline_selectionrange(edit, NULL, NULL, &start, &end)) + inline_copytoclipboard(edit, edit->buffer + start, end - start); +} + +/** Cut selected text */ +static void inline_cutselection(inline_editor *edit) { + inline_copyselection(edit); + inline_deleteselection(edit); +} + +/** Cut part of a line */ +static void inline_cutline(inline_editor *edit, bool before) { + int row; + inline_cursorposn(edit, &row, NULL); + size_t b_line = edit->lines[row + (before ? 0 : 1)]; // line break position before or after + size_t b_cursor = edit->graphemes[edit->cursor_posn]; // Cursor position + + size_t b_start = imin(b_line, b_cursor), b_end = imax(b_line, b_cursor); + if (!before && b_end>0 && edit->buffer[b_end-1]=='\n') b_end--; // Don't include newline + if (b_start==b_end) return; // Nothing to copy + + inline_copytoclipboard(edit, edit->buffer + b_start, b_end - b_start); + inline_deletebytes(edit, b_start, b_end); + inline_setcursorposn(edit, inline_findgraphemeindex(edit, b_start)); // Cursor moves to start of deleted region +} + +/** Paste from clipboard */ +static void inline_paste(inline_editor *edit) { + if (edit->clipboard && edit->clipboard_len>0) { + if (edit->selection_posn != INLINE_INVALID) inline_deleteselection(edit); // Replace selection + inline_insert(edit, edit->clipboard, edit->clipboard_len); + } +} + +/** Process a history keypress */ +static void inline_historykey(inline_editor *edit, int delta) { + inline_advancehistory(edit, delta); + inline_setcursorposn(edit, edit->grapheme_count); // Move to end + inline_clearselection(edit); + inline_clearsuggestions(edit); +} + +/** Transpose two graphemes */ +static void inline_transpose(inline_editor *edit) { + int n = edit->grapheme_count, cur = edit->cursor_posn; + if (n < 2 || cur == 0) return; + + int a = (cur >= n ? n-2 : cur-1), b = a + 1; // The two graphemes to swap + size_t a_start, a_end, b_start, b_end; // Their byte bounds + inline_graphemerange(edit, a, &a_start, &a_end); + inline_graphemerange(edit, b, &b_start, &b_end); + + size_t a_len = a_end - a_start, b_len = b_end - b_start; // Temporary buffer + char *tmp = malloc(a_len); + if (!tmp) return; + + memcpy(tmp, edit->buffer + a_start, a_len); // Copy a into temporary buffer + memmove(edit->buffer + a_start, edit->buffer + b_start, b_len); // Copy b overwriting a + memcpy(edit->buffer + a_start + b_len, tmp, a_len); // Copy a from the temporary buffer + + free(tmp); + + inline_recomputegraphemes(edit); + if (cur < n) inline_setcursorposn(edit, edit->cursor_posn+1); +} + +/** Apply current suggestion */ +static void inline_applysuggestion(inline_editor *edit) { + const char *suffix = inline_currentsuggestion(edit); + if (suffix && *suffix) inline_insert(edit, suffix, strlen(suffix)); + inline_clearsuggestions(edit); +} + +/** Handle Ctrl+_ shortcuts */ +static bool inline_processshortcut(inline_editor *edit, char c) { + switch (c) { + case 'A': inline_home(edit); break; + case 'B': inline_left(edit); break; + case 'C': inline_copyselection(edit); break; + case 'D': + inline_clearselection(edit); + inline_deletecurrent(edit); + break; + case 'E': inline_end(edit); break; + case 'F': inline_right(edit); break; + case 'G': return false; // exit on Ctrl-G + case 'K': inline_cutline(edit, false); break; // Cut to end of line + case 'L': inline_clear(edit); break; + case 'N': inline_historykey(edit, 1); break; // Next history + case 'P': inline_historykey(edit, -1); break; // Previous history + case 'T': inline_transpose(edit); break; + case 'U': inline_cutline(edit, true); break; // Cut to start of line + case 'X': inline_cutselection(edit); break; + case 'Y': // v fallthrough + case 'V': inline_paste(edit); break; + default: break; + } + edit->refresh = true; + return true; +} + +/** Process a keypress */ +static bool inline_processkeypress(inline_editor *edit, const keypress_t *key) { + bool generatesuggestions=true, clearselection=true, endbrowsing=true; + switch (key->type) { + case KEY_RETURN: + if (!edit->multiline_fn || + !edit->multiline_fn(edit->buffer, edit->multiline_ref)) return false; + if (!inline_insert(edit, "\n", 1)) return false; + generatesuggestions = false; // newline shouldn't trigger suggestion + break; + case KEY_LEFT: inline_left(edit); break; + case KEY_RIGHT: + if (edit->suggestion_shown) { + inline_applysuggestion(edit); + generatesuggestions = false; + break; + } + inline_right(edit); + break; + case KEY_SHIFT_LEFT: + inline_beginselection(edit); + inline_left(edit); + clearselection=false; + break; + case KEY_SHIFT_RIGHT: + inline_beginselection(edit); + inline_right(edit); + clearselection=false; + break; + case KEY_UP: + inline_historykey(edit, -1); + endbrowsing=false; + break; + case KEY_DOWN: + inline_historykey(edit, +1); + endbrowsing=false; + break; + case KEY_HOME: inline_home(edit); break; + case KEY_END: inline_end(edit); break; + case KEY_PAGE_UP: inline_pageup(edit); break; + case KEY_PAGE_DOWN: inline_pagedown(edit); break; + case KEY_DELETE: inline_delete(edit); break; + case KEY_TAB: + if (inline_havesuggestions(edit)) { + inline_advancesuggestions(edit, 1); + generatesuggestions=false; + } else if (!inline_insert(edit, "\t", 1)) return false; + break; + case KEY_SHIFT_TAB: + if (inline_havesuggestions(edit)) { + inline_advancesuggestions(edit, -1); + generatesuggestions=false; + } + break; + case KEY_CTRL: return inline_processshortcut(edit, key->c[0]); + case KEY_CHARACTER: + if (!inline_insert(edit, (char *) key->c, key->nbytes)) return false; + break; + case KEY_UNKNOWN: + break; + } + + if (clearselection) inline_clearselection(edit); + if (generatesuggestions) inline_generatesuggestions(edit); + if (endbrowsing) inline_endhistorybrowsing(edit); + edit->refresh = true; + return true; +} + +/* ********************************************************************** + * Interface + * ********************************************************************** */ + +/** If we're not attached to a terminal, e.g. a pipe, simply read the file in. */ +static void inline_noterminal(inline_editor *edit) { + int c; + + while ((c = fgetc(stdin)) != EOF && c != '\n') { + if (!inline_extendbufferby(edit, 1)) break; // Buffer could not be extended + edit->buffer[edit->buffer_len++] = (char)c; + } + + edit->buffer[edit->buffer_len] = '\0'; // Ensure null termination +} + +/** If the terminal is unsupported, display a prompt and read the line normally. */ +static void inline_unsupported(inline_editor *edit) { + fputs(edit->prompt, stdout); + fflush(stdout); // Ensure prompt appears + + inline_noterminal(edit); + + int length = (int)edit->buffer_len - 1; // Strip trailing control characters + while (length >= 0 && iscntrl((unsigned char)edit->buffer[length])) { + edit->buffer[length--] = '\0'; + } + + edit->buffer_len = length + 1; +} + +/** Normal interface if terminal recognized */ +static void inline_supported(inline_editor *edit) { + inline_reset(edit); + inline_setutf8(); + if (!inline_enablerawmode(edit)) return; // Could not enter raw mode + inline_updateterminalwidth(edit); + inline_initviewport(edit); + inline_redraw(edit); + + keypress_t key; + while (inline_readkeypress(edit, &key)) { + if (!inline_processkeypress(edit, &key)) break; + + if (edit->refresh || resize_pending) { + inline_redraw(edit); + edit->refresh = false; + resize_pending = 0; + } + } + + inline_clearselection(edit); + inline_clearsuggestions(edit); + inline_redraw(edit); + inline_disablerawmode(edit); + + if (edit->buffer_len > 0) inline_addhistory(edit, edit->buffer); // Add to history if non-empty + write(STDOUT_FILENO, "\r\n", 2); +} + +/** API function to read a line of text from the user. + * @param edit - an inline_editor that has been created with inline_new. + * @returns a heap-allocated copy of the string input by the user (caller must free), + * or NULL on error. */ +char *inline_readline(inline_editor *edit) { + if (!edit) return NULL; + + edit->buffer_len = 0; // Reset buffer + edit->buffer[0] = '\0'; + + if (!inline_checktty()) { + inline_noterminal(edit); + } else if (inline_checksupported()) { + inline_supported(edit); + } else { + inline_unsupported(edit); + } + + return (edit->buffer ? inline_strdup(edit->buffer) : NULL); +} diff --git a/src/inline.h b/src/inline.h new file mode 100644 index 0000000..81f1b5b --- /dev/null +++ b/src/inline.h @@ -0,0 +1,223 @@ +/** @file inline.h + * @author T J Atherton + * + * @brief A simple grapheme aware line editor with history, completion, multiline editing and syntax highlighting */ + +#ifndef INLINE_H +#define INLINE_H + +#include +#include +#include + +#define INLINE_VERSION_MAJOR 0 +#define INLINE_VERSION_MINOR 1 +#define INLINE_VERSION_PATCH 0 + +/* Forward declaration of the line editor structure */ +typedef struct inline_editor inline_editor; + +/* ********************************************************************** + * Callback functions + * ********************************************************************** */ + +/* ----------------------- + * Autocomplete + * ----------------------- */ + +/** @brief Autocomplete callback function. + * + * Called repeatedly by the line editor to obtain completion + * suggestions for the given buffer contents. + * + * The editor initializes *index to zero before the first call. + * Each time the callback returns a suggestion, it should update + * *index to an opaque value representing the next iteration + * position. The editor does not interpret this value; it is + * entirely callback-defined. + * + * @param[in] utf8 Current contents of the line buffer. + * @param[in] ref User-supplied reference pointer. + * @param[in,out] index Opaque iteration state. Set to zero + * by the editor before the first call. + * + * @returns A UTF-8 string containing the completion suffix, + * or NULL if no more suggestions exist. For example, + * if the buffer ends with "pr", a suggestion might be + * "int" to form "print". + * + * @note The callback owns the returned string; it may therefore + * return pointers to static strings or internal buffers. + * The editor copies the suggestion immediately. */ +typedef const char *(*inline_completefn) (const char *utf8, void *ref, size_t *index); + +/* ----------------------- + * Syntax coloring + * ----------------------- */ + +/** @brief A single colored span of text. */ +typedef struct { + size_t byte_end; /* exclusive end of color span */ + int color; /* Index into color palette */ +} inline_colorspan_t; + +/** @brief Syntax coloring callback function, called repeatedly by + * the editor to obtain the next colored span. + * @param[in] utf8 The full buffer encoded as UTF-8 to analyze. + * @param[in] ref User-supplied reference pointer. + * @param[in] offset Byte offset at which to begin scanning. + * @param[out] out Filled with the next colored span, if any. + * + * @returns true if a span was found, false if no more spans exist. */ +typedef bool (*inline_syntaxcolorfn) (const char *utf8, void *ref, size_t offset, inline_colorspan_t *out); + +/* ----------------------- + * Multiline editing + * ----------------------- */ + +/** @brief Multiline callback function + * Called when inline wants to know whether it should enter multiline mode. + * The callback should parse the input and return true if inline should go to + * multiline mode or false otherwise. + * @param[in] utf8 The full buffer encoded as UTF-8. + * @param[in] ref User-supplied reference pointer. + * + * @returns true if more lines are required, false otherwise. */ +typedef bool (*inline_multilinefn) (const char *utf8, void *ref); + +/* ----------------------- + * Grapheme support + * ----------------------- */ + +/** @brief Unicode grapheme splitter callback function + * @param[in] in - a string + * @param[in] end - end of string + * @returns number of bytes in the next grapheme or 0 if incomplete + * @details If provided, inline will use this function to split UTF8 code + * into graphemes. Shims are provided in the documentation for + * libgrapheme and libunistring. A fallback implementation is used + * if not provided. */ +typedef size_t (*inline_graphemefn) (const char *in, const char *end); + +/** @brief Unicode grapheme display width callback function + * @param[in] g - a string representing a grapheme + * @param[in] len - length of grapheme in bytes + * @returns display width of grapheme in terminal columns + * @details If provided, inline will use this function to calculate the display + * width. A fallback implementation is used if not provided. */ +typedef int (*inline_widthfn)(const char *g, size_t len); + +/* ********************************************************************** + * Public API + * ********************************************************************** */ + +/** @brief Create a new line editor. + * @param[in] prompt The prompt string to display. This is immediately copied and you may free/modify upon return. + * @returns A newly allocated line editor.*/ +inline_editor *inline_new(const char *prompt); + +/** @brief Free a line editor and all associated resources. + * @param[in] edit Line editor to free. */ +void inline_free(inline_editor *edit); + +/** @brief Read a line of input from the terminal. + * @param[in] edit Line editor to use. + * @returns A heap allocated UTF-8 string containing the user's input, or NULL on EOF or error. + * Caller owns the string and must call it later using free(). */ +char *inline_readline(inline_editor *edit); + +/** @brief Sets the maximum length of the history. + * @param[in] edit Line editor to configure. + * @param[in] maxlen Maximum number of entries in the history buffer; + * negative values mean unlimited; 0 disables history */ +void inline_sethistorylength(inline_editor *edit, int maxlen); + +/** @brief Adds an entry to the history. + * @param[in] edit Line editor to use. + * @param[in] entry Entry to add. This is copied immediately and the pointer is not stored. + * @returns true if the entry was successfully added to the history list; false otherwise */ +bool inline_addhistory(inline_editor *edit, const char *entry); + +/** @brief Enable syntax coloring. + * @param[in] edit Line editor to configure. + * @param[in] fn Syntax coloring callback. + * @param[in] ref User-supplied reference pointer. */ +void inline_syntaxcolor(inline_editor *edit, inline_syntaxcolorfn fn, void *ref); + +/** Any color < 0 means use terminal default */ +#define INLINE_DEFAULT -1 + +/** Macros for basic ANSI terminal colors */ +#define INLINE_BLACK 0 +#define INLINE_RED 1 +#define INLINE_GREEN 2 +#define INLINE_YELLOW 3 +#define INLINE_BLUE 4 +#define INLINE_MAGENTA 5 +#define INLINE_CYAN 6 +#define INLINE_WHITE 7 + +/** Macro for xterm-256 216-color RGB cube: r,g,b are in [0..5]. */ +#define INLINE_COLOR_ANSI216(r, g, b) (16 + 36 * (int)(r) + 6 * (int)(g) + (int)(b)) + +/** Macro for xterm-256 gray levels: n is in [0..23] */ +#define INLINE_GRAY_ANSI(n) (232 + (n)) + +/** Macro for RGB values */ +#define INLINE_COLOR_RGB 0x01000000u + +#define INLINE_RGB(r, g, b) \ + (INLINE_COLOR_RGB | (((uint32_t)(r) & 0xffu) << 16) | \ + (((uint32_t)(g) & 0xffu) << 8) | ((uint32_t)(b) & 0xffu)) + +/** @brief Set the color palette used for syntax highlighting. + * + * Color indices returned by a inline_syntaxcolorfn are mapped + * through this palette to a final color value. The palette is copied by + * the inline_editor. Color values are interpreted: + * + * -1 → default color + * 0–7 → ANSI basic colors (see macros above) + * 8–255 → 256-color palette + * >=0x01000000 → RGB packed as 0x01RRGGBB + * + * @param[in] edit Line editor to configure. + * @param[in] count Number of entries in the palette. + * @param[in] palette Array mapping semantic color indices to color ints. + * @returns: true on success; false otherwise */ +bool inline_setpalette(inline_editor *edit, int count, const int *palette); + +/** @brief Enable autocomplete. + * @param[in] edit Line editor to configure. + * @param[in] fn Completion callback. + * @param[in] ref User-supplied reference pointer. */ +void inline_autocomplete(inline_editor *edit, inline_completefn fn, void *ref); + +/** @brief Enable multiline editing. + * @param[in] edit Line editor to configure. + * @param[in] fn Multiline callback. + * @param[in] ref User-supplied reference pointer. + * @param[in] continuation_prompt Prompt to use for continuation lines; this is copied immediately and you may free/modify after. + * @returns true on success; false otherwise */ +bool inline_multiline(inline_editor *edit, inline_multilinefn fn, void *ref, const char *continuation_prompt); + +/** @brief Supply a custom grapheme splitter. + * @param[in] edit Line editor to configure. + * @param[in] fn Grapheme callback. */ +void inline_setgraphemesplitter(inline_editor *edit, inline_graphemefn fn); + +/** @brief Supply a custom grapheme display width calculator. + * @param[in] edit Line editor to configure. + * @param[in] fn Grapheme display width callback. */ +void inline_setgraphemewidth(inline_editor *edit, inline_widthfn fn); + +/** @brief Display a UTF-8 string using syntax coloring. + * @param[in] edit Line editor to use. + * @param[in] string UTF-8 string to display.*/ +void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string); + +/** @brief Check whether stdin and stdout are TTYs. + * @returns true if both stdin and stdout are terminals. */ +bool inline_checktty(void); + +#endif /* INLINE_H */ diff --git a/src/linedit.c b/src/linedit.c deleted file mode 100644 index c264b55..0000000 --- a/src/linedit.c +++ /dev/null @@ -1,1927 +0,0 @@ -/** @file linedit.c - * @author T J Atherton - * - * @brief A simple UTF8 aware line editor with history, completion, multiline editing and syntax highlighting - */ - -#include "linedit.h" - -#ifdef _WIN32 -#include -#include -#include -#else -#include -#include -#include -#endif - -/** Maximum escape code size */ -#define LINEDIT_CODESTRINGSIZE 24 - -/* ********************************************************************** - * Platform dependent code - * ********************************************************************** */ - -/* ---------------------------------------- - * Check terminal features - * ---------------------------------------- */ - -/** Check if stdin and stdout are a tty */ -bool linedit_isatty(void) { -#ifdef _WIN32 - return (_isatty(_fileno(stdin)) && _isatty(_fileno(stdout))); -#else - return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO); -#endif -} - -/** Get terminal name */ -bool linedit_terminalname(char *buffer, size_t size) { -#ifdef _WIN32 - strncpy(buffer, "win", size); - return true; -#else - char *term = getenv("TERM"); - if (term) strncpy(buffer, term, size); - return term; -#endif -} - -/** Enable UTF8 mode */ -void linedit_setutf8(void) { -#ifdef _WIN32 - SetConsoleOutputCP(CP_UTF8); -#endif -} - -/* ---------------------------------------- - * Switch to/from raw mode - * ---------------------------------------- */ - -/** Hold the original terminal state */ -#ifdef _WIN32 -DWORD terminit; -#else -struct termios terminit; -#endif - -bool termexitregistered=false; - -void linedit_disablerawmode(void); - -/** @brief Enables 'raw' mode in the terminal - * @details In raw mode key presses are passed directly to us rather than - * being buffered. */ -void linedit_enablerawmode(void) { -#ifdef _WIN32 - HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE); - GetConsoleMode(hConsole, &terminit); - SetConsoleMode(hConsole, (terminit & ~(ENABLE_LINE_INPUT | - ENABLE_ECHO_INPUT | - ENABLE_PROCESSED_INPUT) | - ENABLE_VIRTUAL_TERMINAL_INPUT )); - - linedit_setutf8(); -#else - struct termios termraw; /* Use to set the raw state */ - - tcgetattr(STDIN_FILENO, &terminit); /** Get the original state*/ - - termraw=terminit; - /* Input: Turn off: IXON - software flow control (ctrl-s and ctrl-q) - ICRNL - translate CR into NL (ctrl-m) - BRKINT - parity checking - ISTRIP - strip bit 8 of each input byte */ - termraw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | BRKINT | ISTRIP); - /* Output: Turn off: OPOST - output processing */ - termraw.c_oflag &= ~(OPOST); - /* Character: CS8 Set 8 bits per byte */ - termraw.c_cflag |= (CS8); - /* Turn off: ECHO - causes keypresses to be printed immediately - ICANON - canonical mode, reads line by line - IEXTEN - literal (ctrl-v) - ISIG - turn off signals (ctrl-c and ctrl-z) */ - termraw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); - /* Set return condition for control characters */ - termraw.c_cc[VMIN] = 1; termraw.c_cc[VTIME] = 0; /* 1 byte, no timer */ - - tcsetattr(STDIN_FILENO, TCSAFLUSH, &termraw); -#endif - if (!termexitregistered) { - atexit(linedit_disablerawmode); - termexitregistered=true; - } -} - -/** @brief Restore terminal state to normal */ -void linedit_disablerawmode(void) { -#ifdef _WIN32 - HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE); - SetConsoleMode(hConsole, terminit); -#else - tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminit); -#endif - printf("\r"); /** Print a carriage return to ensure we're back on the left hand side */ -} - -/** @brief Gets the terminal width */ -void linedit_getterminalwidth(lineditor* edit) { - edit->ncols = 80; -#ifdef _WIN32 - HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); - CONSOLE_SCREEN_BUFFER_INFO csbi; - - if (GetConsoleScreenBufferInfo(h, &csbi)) { - edit->ncols = csbi.srWindow.Right - csbi.srWindow.Left + 1; - } -#else - struct winsize ws; - - /* Try ioctl first */ - if (!(ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0)) { - edit->ncols = ws.ws_col; - } - else { - // Should get cursor position etc here. - } -#endif -} - -/* ---------------------------------------- - * Detect if keypresses are available - * ---------------------------------------- */ - -/** Detect if a keypress is available; non-blocking */ -bool linedit_keypressavailable(void) { -#ifdef _WIN32 - return _kbhit(); -#else - fd_set readfds; - FD_ZERO(&readfds); - FD_SET(STDIN_FILENO, &readfds); - - struct timeval timeout={ .tv_sec=0, .tv_usec=0 }; - - return (select(1, &readfds, NULL, NULL, &timeout)>0); -#endif -} - -/** Read a single character from the terminal */ -bool linedit_getchar(char* out) { -#ifdef _WIN32 - *out = _getch(); - return true; -#else - return fread(out, sizeof(char), 1, stdin) == 1; -#endif -} - -/* ********************************************************************** - * Terminal - * ********************************************************************** */ - -/** @brief Compares two c strings independently of case - * @param[in] str1 - } strings to compare - * @param[in] str2 - } - * @returns 0 if the strings are identical, otherwise a positive or negative number indicating their lexographic order */ -int linedit_cstrcasecmp(char *str1, char *str2) { - if (str1 == str2) return 0; - int result=0; - - for (char *p1=str1, *p2=str2; result==0; p1++, p2++) { - result=tolower(*p1)-tolower(*p2); - if (*p1=='\0') break; - } - - return result; -} - -#ifdef _WIN32 -char *strndup(const char *s, size_t n) { - char *out = malloc(n+1); - if (out) { - memcpy(out, s, n); - out[n]='\0'; - } - return out; -} -#endif - -/* ---------------------------------------- - * Retrieve terminal type - * ---------------------------------------- */ - -typedef enum { - LINEDIT_NOTTTY, - LINEDIT_UNSUPPORTED, - LINEDIT_SUPPORTED -} linedit_terminaltype; - - -#define LINEDIT_TERMINALNAME_BUFFERSIZE 1024 - -/** Checks whether the current terminal is supported */ -linedit_terminaltype linedit_checksupport(void) { - /* Make sure both stdin and stdout are a tty */ - if (!linedit_isatty()) return LINEDIT_NOTTTY; - - char *unsupported[]={"dumb","cons25","emacs",NULL}; - char term[LINEDIT_TERMINALNAME_BUFFERSIZE]; - if (!linedit_terminalname(term, LINEDIT_TERMINALNAME_BUFFERSIZE)) return LINEDIT_UNSUPPORTED; - - for (unsigned int i=0; unsupported[i]!=NULL; i++) { - if (!linedit_cstrcasecmp(term, unsupported[i])) return LINEDIT_UNSUPPORTED; - } - - return LINEDIT_SUPPORTED; -} - -/* ---------------------------------------- - * Get cursor position and width - * ---------------------------------------- */ - -#define LINEDIT_CURSORPOSN_BUFFERSIZE 128 -/** @brief Gets the cursor position */ -bool linedit_getcursorposition(int *x, int *y) { - char answer[LINEDIT_CURSORPOSN_BUFFERSIZE]; - int i=0, row=0, col=0; - - /* Report cursor location */ - if (fwrite("\x1b[6n", sizeof(char), 4, stdout) != 4) return false; - - /* Read the response: ESC [ rows ; cols R */ - while (i < sizeof(answer)-1) { - if (fread(answer+i, sizeof(char), 1, stdin)<1) break; - if (answer[i] == 'R') break; // Response is 'R' terminated - i++; - } - answer[i] = '\0'; // Terminal response is not null-terminated by default - - /* Parse response */ - if (answer[0] != 27 || answer[1] != '[') return false; - if (sscanf(answer+2,"%d;%d",&row,&col) != 2) return false; - - if (y) *y = row; // Return result - if (x) *x = col; - return true; -} - -/* ---------------------------------------- - * Output - * ---------------------------------------- */ - -/** @brief Writes a string to the terminal */ -bool linedit_write(char *string) { - size_t length=strlen(string); - if (fwrite(string, sizeof(char), length, stdout)0) { - snprintf(code, LINEDIT_CODESTRINGSIZE, "\r\033[%iC", posn); - return linedit_write(code); - } - return true; -} - -/** @brief Moves the cursor up by n lines */ -bool linedit_moveup(int n) { - char code[LINEDIT_CODESTRINGSIZE]; - if (n>0) { - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[%iA", n); - return linedit_write(code); - } - return true; -} - -/** @brief Moves the cursor down by n lines */ - bool linedit_movedown(int n) { - char code[LINEDIT_CODESTRINGSIZE]; - if (n>0) { - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[%iB", n); - return linedit_write(code); - } - return true; -} - -/** @brief Hides the cursor */ -bool linedit_hidecursor(void) { - return linedit_write("\033[?25l"); -} - -/** @brief Shows the cursor */ -bool linedit_showcursor(void) { - return linedit_write("\033[?25h"); -} - -/* ********************************************************************** - * Unicode support - * ********************************************************************** */ - -/* ---------------------------------------- - * Basic UTF8 support - * ---------------------------------------- */ - -/** @brief Returns the number of bytes in the next character of a given utf8 string - @returns number of bytes */ -int linedit_utf8numberofbytes(char *string) { - if (!string) return 0; - uint8_t byte = * ((uint8_t *) string); - - if ((byte & 0xc0) == 0x80) return 0; // In the middle of a utf8 string - - // Get the number of bytes from the first character - if ((byte & 0xf8) == 0xf0) return 4; - if ((byte & 0xf0) == 0xe0) return 3; - if ((byte & 0xe0) == 0xc0) return 2; - return 1; -} - -/** Decodes a utf8 encoded character pointed to by c into an int */ -int linedit_utf8toint(char *c) { - unsigned int ret = -1; - int nbytes=linedit_utf8numberofbytes(c); - switch (nbytes) { - case 1: ret=(c[0] & 0x7f); break; - case 2: ret=((c[0] & 0x1f)<<6) | (c[1] & 0x3f); break; - case 3: ret=((c[0] & 0x0f)<<12) | ((c[1] & 0x3f)<<6) | (c[2] & 0x3f); break; - case 4: ret=((c[0] & 0x0f)<<18) | ((c[1] & 0x3f)<<12) | ((c[2] & 0x3f)<<6) | (c[3] & 0x3f) ; break; - default: break; - } - - return ret; -} - -/** @brief Utf8 character loop advancer. - @details Determines the number of bytes for the code point at c, and advances the counter i by that number. - Returns true if the number of bytes is >0 */ -bool linedit_utf8next(char *c, int *i) { - int adv = linedit_utf8numberofbytes(c); - if (adv) *i+=adv; - return adv; -} - -/** @brief Count the number of UTF characters in a string - @param[in] start - start of string - @param[in] length - length of string - @param[out] count - number of UTF characters - @returns true on success */ -bool linedit_utf8count(char *start, size_t length, size_t *count) { - size_t len, n=0; - for (char *c=start; ccount=0; - dict->capacity=0; - dict->contents=NULL; -} - -void linedit_graphemeclear(linedit_graphemedictionary *dict) { - for (int i=0; icapacity; i++) { - if (dict->contents[i].grapheme) free(dict->contents[i].grapheme); - } - linedit_graphemeinit(dict); -} - -uint32_t linedit_hashstring(const char* key, size_t length) { - uint32_t hash = 2166136261u; // String hashing function FNV-1a - - for (unsigned int i=0; i < length; i++) { - hash ^= key[i]; - hash *= 16777619u; // FNV prime number for 32 bits - } - - return hash; -} - -bool linedit_graphemeinsert(linedit_graphemedictionary *dict, char *grapheme, size_t length, int width); - -bool linedit_graphemeresize(linedit_graphemedictionary *dict, int size) { - linedit_graphemeentry *new=malloc(size*sizeof(linedit_graphemeentry)), *old=dict->contents; - int osize=dict->capacity; - - if (new) { // Clear the newly allocated structure - for (unsigned int i=0; icapacity=size; - dict->contents=new; - dict->count=0; - - if (old) { // Copy old contents across - for (unsigned int i=0; icontents) return false; - uint32_t hash = linedit_hashstring(grapheme, length); - - int start = hash % dict->capacity; - int i=start; - - do { - if (!dict->contents[i].grapheme) { // Blank entry -> not found - if (posn) *posn = i; - return false; - } - - if (strncmp(grapheme, dict->contents[i].grapheme, length)==0) { // Found - if (posn) *posn = i; - return true; - } - - i = (i+1) % dict->capacity; - } while (i!=start); // Loop terminates once we return to the starting position - - return false; -} - -#define LINEDIT_MINDICTIONARYSIZE 8 -#define LINEDIT_SIZEINCREASETHRESHOLD(x) (((x)>>1) + ((x)>>2)) -#define LINEDIT_INCREASEDICTIONARYSIZE(x) (2*x) - -bool linedit_graphemeinsert(linedit_graphemedictionary *dict, char *grapheme, size_t length, int width) { - - if (!dict->contents) { - if (!linedit_graphemeresize(dict, LINEDIT_MINDICTIONARYSIZE)) return false; - } else if (dict->count+1 > LINEDIT_SIZEINCREASETHRESHOLD(dict->capacity)) { - if (!linedit_graphemeresize(dict, LINEDIT_INCREASEDICTIONARYSIZE(dict->capacity))) return false; - } - - int posn; - if (linedit_graphemefind(dict, grapheme, length, &posn)) return true; - - char *label = strndup(grapheme, length); - if (!label) return false; - dict->contents[posn].grapheme=label; - dict->contents[posn].width=width; - dict->count++; - return true; -} - -bool linedit_graphemelookup(linedit_graphemedictionary *dict, char *grapheme, size_t length, int *width) { - if (!dict->contents) return false; - int posn; - if (!linedit_graphemefind(dict, grapheme, length, &posn)) return false; - if (width) *width = dict->contents[posn].width; - return true; -} - -void linedit_graphemeshow(linedit_graphemedictionary *dict) { - for (int i=0; icapacity; i++) { - if (dict->contents[i].grapheme) { - printf("%s: %i\n", dict->contents[i].grapheme, dict->contents[i].width); - } - } -} - -/* ---------------------------------------- - * Grapheme display width - * ---------------------------------------- */ - -/** @brief Identifies the length of the next grapheme */ -size_t linedit_graphemelength(lineditor *edit, char *str, char *end) { - if (*str=='\0') return 0; // Ensure we return on null terminator - if (edit->graphemefn) return edit->graphemefn(str, end); - return (size_t) linedit_utf8numberofbytes(str); // Fallback on displaying unicode chars one by one -} - -/** @brief Returns the display with of a grapheme sequence if known */ -bool linedit_graphemedisplaywidth(lineditor *edit, char *grapheme, size_t length, int *width) { - if (length==1) { - if (iscntrl(*grapheme)) return 0; - return 1; - } - - return linedit_graphemelookup(&edit->graphemedict, grapheme, length, width); -} - -/** @brief Renders a grapheme sequence, measuring its display width */ -bool linedit_graphememeasurewidth(lineditor *edit, char *grapheme, size_t length, int *width) { - int x0=0, x1=0, w=0; - linedit_getcursorposition(&x0, NULL); - if (fwrite(grapheme, sizeof(char), length, stdout)x0 ? x1-x0 : 1); - *width = w; - return linedit_graphemeinsert(&edit->graphemedict, grapheme, length, w); -} - -/* ********************************************************************** - * Rendering - * ********************************************************************** */ - -/** @brief Renders a string, showing only characters in columns l...r */ -void linedit_renderstring(lineditor *edit, char *string, size_t length, int l, int r) { - int i=0; - size_t len=0; - - for (char *s=string; *s!='\0'; s+=len) { - len = linedit_graphemelength(edit, s, string+length); - if (!len) break; - - if (*s=='\r') { // Reset on a carriage return - if (fwrite("\r", sizeof(char), 1, stdout)<1) return; - i=0; - } else if (*s=='\n') { // Clear to end of line and return - if (!linedit_write("\x1b[K\n\r")) return; - if (fwrite(edit->cprompt.string, sizeof(char), edit->cprompt.length, stdout)cprompt.length) return; - i=0; - } else if (*s=='\t') { - if (!linedit_write(" ")) return; - i+=1; - } else if (iscntrl(*s)) { - if (*s=='\033') { // A terminal control character - char *ctl=s; // First identify its length - while (!isalpha(*ctl) && *ctl!='\0') ctl++; - size_t len = ctl-s+1; - if (fwrite(s, sizeof(char), len, stdout)=l && icapacity=0; - string->length=0; - string->next=NULL; - string->string=NULL; -} - -/** Clears a string, deallocating memory if necessary */ -void linedit_stringclear(linedit_string *string) { - if (string->string) free(string->string); - linedit_stringinit(string); -} - -/** @brief Finds the index of character i in a utf8 encoded string. - * @param[in] string - string to index - * @param[in] i - Index of character to find - * @param[in] offset - Offset into the string - set to 0 to count from the start of the string - * @param[out] out - offset in bytes from offset to character i */ -bool linedit_stringutf8index(linedit_string *string, size_t i, size_t offset, size_t *out) { - int advance=0; - size_t nchars=0; - - for (size_t j=0; j+offset<=string->length; j+=advance, nchars++) { - if (nchars==i) { *out = j; return true; } - advance=linedit_utf8numberofbytes(string->string+offset+j); - if (advance==0) break; // If advance is 0, the string is corrupted; return failure - } - - return false; -} - -/** Resizes a string - * @param string - the string to grow - * @param size - requested size - * @returns true on success, false on failure */ -bool linedit_stringresize(linedit_string *string, size_t size) { - size_t newsize=linedit_MINIMUMSTRINGSIZE; - char *old=string->string; - - /* If we're increasing the size, grow by factors of 1.5 to avoid excessive calls to allocator */ - while (newsize<=size) newsize=((newsize<<1)+newsize)>>1; // mul by x1.5 - - string->string=realloc(string->string,newsize); - if (string->string) { - if (!old) { - string->string[0]='\0'; /* Make sure a new string is zero-terminated */ - string->length=0; - } - string->capacity=newsize; - } - return (string->string!=NULL); -} - -/** Adds a string to a string */ -void linedit_stringappend(linedit_string *string, char *c, size_t nbytes) { - if (string->capacity<=string->length+nbytes) { - if (!linedit_stringresize(string, string->length+nbytes+1)) return; - } - - strncpy(string->string+string->length, c, nbytes); - string->length+=nbytes; - string->string[string->length]='\0'; /* Keep the string zero-terminated */ -} - -/** @brief Inserts characters at a given position - * @param[in] string - string to amend - * @param[in] posn - insertion position as a character index - * @param[in] c - string to insert - * @param[in] n - number of bytes to insert - * @details If the position is after the length of the string - * the new characters are instead appended. */ -void linedit_stringinsert(linedit_string *string, size_t posn, char *c, size_t n) { - size_t offset; - if (!linedit_stringutf8index(string, posn, 0, &offset)) return; - - if (offsetlength) { - if (string->capacity<=string->length+n) { - if (!linedit_stringresize(string, string->length+n+1)) return; - } - /* Move the remaining part of the string */ - memmove(string->string+offset+n, string->string+offset, string->length-offset+1); - /* Copy in the text to insert */ - memmove(string->string+offset, c, n); - string->length+=n; - } else { - linedit_stringappend(string, c, n); - } -} - -/** @brief Deletes characters at a given position. - * @param[in] string - string to amend - * @param[in] posn - Delete characters as a character index - * @param[in] n - number of characters to delete */ -void linedit_stringdelete(linedit_string *string, size_t posn, size_t n) { - size_t offset; - size_t nbytes; - - if (string->lengthlength) { - if (offset+nbyteslength) { - memmove(string->string+offset, string->string+offset+nbytes, string->length-offset-nbytes+1); - } else { - string->string[offset]='\0'; - } - string->length=strlen(string->string); - } -} - -/** Adds a c string to a string */ -void linedit_stringaddcstring(linedit_string *string, char *s) { - if (!s || !string) return; - size_t size=strlen(s); - if (string->capacity<=string->length+size+1) { - if (!linedit_stringresize(string, string->length+size+1)) return; - } - - strncpy(string->string+string->length, s, string->capacity-string->length); - string->length+=size; -} - -/** Finds the length of a string in unicode characters */ -int linedit_stringlength(linedit_string *string) { - size_t count=0; - linedit_utf8count(string->string, string->length, &count); - return (int) count; -} - -/** Finds the display width of a string */ -int linedit_stringdisplaywidth(lineditor *edit, linedit_string *string) { - int width=0; - size_t len; - for (int i=0; ilength; i+=len) { - int w=1; - len = linedit_graphemelength(edit, string->string+i, string->string+string->length); - linedit_graphemedisplaywidth(edit, string->string+i, len, &w); - width+=w; - } - return width; -} - -/** Finds the display coordinates for a given position in a string */ -void linedit_stringdisplaycoordinates(lineditor *edit, linedit_string *string, int posn, int *xout, int *yout) { - int x=0, y=0, n=0; - size_t count; - for (int i=0; ilength; n+=count) { - if (posn>=0 && n>=posn) break; - - char *c=string->string+i; - size_t len = linedit_graphemelength(edit, c, string->string+string->length); - if (!linedit_utf8count(c, len, &count)) break; - - if (*c=='\n') { - x=0; y++; - } else { - int w=1; - linedit_graphemedisplaywidth(edit, string->string+i, len, &w); - if (x+w>edit->ncols) { // Lines that are too long wrap over - x=w; y++; - } else x+=w; - } - - i+=len; - if (!len) break; - } - if (xout) *xout = x; - if (yout) *yout = y; -} - -/** Finds the line and unicode character number for a given position in the string. - * @param[in] string - the string - * @param[in] posn - character position in the string - * @param[out] xout - x coordinates corresponding to posn n - * @param[out] yout - y */ -void linedit_stringcoordinates(linedit_string *string, int posn, int *xout, int *yout) { - int x=0, y=0, n=0; - for (int i=0; ilength; n++) { - if (n==posn) break; - char *c=string->string+i; - - if (*c=='\n') { - x=0; y++; - } else x++; - - if (!linedit_utf8next(c, &i)) break; - } - if (xout) *xout = x; - if (yout) *yout = y; -} - -/** Finds the position in the string for specified coordinates - * @param[in] string - the string - * @param[in] x - character position or -1 to find the end of the line - * @param[in] y - line number - * @param[out] posn - position corresponding to */ -void linedit_stringfindposition(linedit_string *string, int x, int y, int *posn) { - int xx=0, yy=0, n=0; - for (int i=0; ilength; n++) { - if (xx==x && yy==y) break; - - char *c = string->string+i; - if (*c=='\n') { - xx=0; yy++; - if (yy>y) break; - } else xx++; - - if (!linedit_utf8next(c, &i)) break; - } - if (posn) *posn = n; -} -/** Locates a particular posn in a string, returning a pointer to the character */ -char *linedit_stringlocate(linedit_string *string, int posn) { - size_t out; - if (!linedit_stringutf8index(string, posn, 0, &out)) return NULL; - return string->string+out; -} - -/** Counts the number of lines a string occupies */ -int linedit_stringcountlines(linedit_string *string) { - int lines; - linedit_stringcoordinates(string, -1, NULL, &lines); - return lines; -} - -/** Returns a C string from a string */ -char *linedit_cstring(linedit_string *string) { - return string->string; -} - -/** Creates a new string from a C string */ -linedit_string *linedit_newstring(char *string) { - linedit_string *new = malloc(sizeof(linedit_string)); - - if (new) { - linedit_stringinit(new); - linedit_stringaddcstring(new, string); - } - - return new; -} - -/* ********************************************************************** - * Lists of strings - * ********************************************************************** */ - -/** Adds an entry to a string list */ -void linedit_stringlistadd(linedit_stringlist *list, char *string) { - linedit_string *new=linedit_newstring(string); - - if (new) { - new->next=list->first; - list->first=new; - } -} - -/** Initializes a string list */ -void linedit_stringlistinit(linedit_stringlist *list) { - list->first=NULL; - list->posn=0; -} - -/** Frees the contents of a string list */ -void linedit_stringlistclear(linedit_stringlist *list) { - while (list->first!=NULL) { - linedit_string *s = list->first; - list->first=s->next; - linedit_stringclear(s); - free(s); - } - linedit_stringlistinit(list); -} - -/** Removes a string from a list */ -void linedit_stringlistremove(linedit_stringlist *list, linedit_string *string) { - linedit_string *s=NULL, *prev=NULL; - - for (s=list->first; s!=NULL; s=s->next) { - if (s==string) { - if (prev) { - prev->next=s->next; - } else { - list->first=s->next; - } - linedit_stringclear(s); - free(s); - return; - } - - prev=s; - } -} - -/** Chooses an element of a stringlist - * @param[in] list the list to select from - * @param[in] n entry number to select - * @parma[out] *m entry number actually selected - * @returns the selected element */ -linedit_string *linedit_stringlistselect(linedit_stringlist *list, unsigned int n, unsigned int *m) { - unsigned int i=0; - linedit_string *s=NULL; - - for (s=list->first; s!=NULL && s->next!=NULL; s=s->next) { - if (i==n) break; - i++; - } - - if (m) *m=i; - - return s; -} - -/** Count the number of entries in a string list */ -int linedit_stringlistcount(linedit_stringlist *list) { - int n=0; - for (linedit_string *s=list->first; s!=NULL; s=s->next) n++; - return n; -} - -/* ********************************************************************** - * History list - * ********************************************************************** */ - -/** Adds an entry to the history list */ -void linedit_historyadd(lineditor *edit, char *string) { - linedit_stringlistadd(&edit->history, string); -} - -/** Frees the history list */ -void linedit_historyclear(lineditor *edit) { - linedit_stringlistclear(&edit->history); -} - -/** Makes a particular history entry current */ -unsigned int linedit_historyselect(lineditor *edit, unsigned int n) { - unsigned int m=n; - linedit_string *s=linedit_stringlistselect(&edit->history, n, &m); - - if (s) { - edit->current.length=0; - linedit_stringaddcstring(&edit->current, s->string); - } - - return m; -} - -/** Advances the history list */ -void linedit_historyadvance(lineditor *edit, unsigned int n) { - edit->history.posn+=n; - edit->history.posn=linedit_historyselect(edit, edit->history.posn); -} - -/** Returns the number of entries in the history list */ -int linedit_historycount(lineditor *edit) { - return linedit_stringlistcount(&edit->history); -} - -/* ********************************************************************** - * Autocompletion - * ********************************************************************** */ - -bool linedit_atend(lineditor *edit); - -/** Regenerates the list of autocomplete suggestions */ -void linedit_generatesuggestions(lineditor *edit) { - if (edit->completer) { - linedit_stringlistclear(&edit->suggestions); - - if (edit->current.string && - linedit_atend(edit)) { - (edit->completer) (edit->current.string, edit->cref, &edit->suggestions); - } - } -} - -/** Check whether any suggestions are available */ -bool linedit_aresuggestionsavailable(lineditor *edit) { - return (edit->suggestions.first!=NULL); -} - -/** Get the current suggestion */ -char *linedit_currentsuggestion(lineditor *edit) { - linedit_string *s=linedit_stringlistselect(&edit->suggestions, edit->suggestions.posn, NULL); - - if (s) return s->string; - - return NULL; -} - -/** Advance through the suggestions */ -void linedit_advancesuggestions(lineditor *edit, unsigned int n) { - unsigned int rposn=edit->suggestions.posn+n, nposn=rposn; - linedit_stringlistselect(&edit->suggestions, rposn, &nposn); - edit->suggestions.posn=nposn; - if (rposn!=edit->suggestions.posn) edit->suggestions.posn=0; /* Go back to first */ -} - -/* ********************************************************************** - * Multiline mode - * ********************************************************************** */ - -/** Test whether we should enter multiline editing mode */ -bool linedit_shouldmultiline(lineditor *edit) { - if (edit->multiline && edit->current.string) return (edit->multiline) (edit->current.string, edit->mlref); - return false; -} - -/* ********************************************************************** - * Syntax highlighting - * ********************************************************************** */ - -/** @brief Writes a control sequence to reset default text */ -void linedit_stringdefaulttext(linedit_string *out) { - char code[LINEDIT_CODESTRINGSIZE]; - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[0m"); - linedit_stringaddcstring(out, code); -} - -/** @brief Writes a control sequence to set a given color */ -void linedit_stringsetcolor(linedit_string *out, linedit_color col) { - char code[LINEDIT_CODESTRINGSIZE]; - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[%im", (col==LINEDIT_DEFAULTCOLOR ? 0: 30+col)); - linedit_stringaddcstring(out, code); -} - -/** @brief Writes a control sequence to set a given emphasis */ -void linedit_stringsetemphasis(linedit_string *out, linedit_emphasis emph) { - char code[LINEDIT_CODESTRINGSIZE] = ""; - switch (emph) { - case LINEDIT_BOLD: snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[1m"); break; - case LINEDIT_UNDERLINE: snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[4m"); break; - case LINEDIT_REVERSE: snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[7m"); break; - case LINEDIT_NONE: break; - } - - linedit_stringaddcstring(out, code); -} - -/** Adds a string with selection highlighting - * @param[in] edit - active editor - * @param[in] in - input string - * @param[in] offset - offset of string in characters - * @param[in] length - length of string in characters - * @param[in] col - color - * @param[out] out - display plus coloring information written to this string */ -void linedit_addcstringwithselection(lineditor *edit, char *in, size_t offset, size_t length, linedit_color *col, linedit_string *out) { - int lposn=-1, rposn=-1; - - /* If a selection is active, discover its bounds */ - if (edit->mode==LINEDIT_SELECTIONMODE && !(edit->sposn<0)) { - lposn=(edit->sposn < edit->posn ? edit->sposn : edit->posn) - (int) offset; - rposn = (edit->sposn < edit->posn ? edit->posn : edit->sposn) - (int) offset; - } - - /* Set the color if provided */ - if (col) linedit_stringsetcolor(out, *col); - - /* Is the text we're showing outside the selected region entirely? */ - if (rposn<0 || lposn>(int) length) { - linedit_stringappend(out, in, length); - } else { - /* If not, add the characters one by one and insert highlighting */ - if (lposn<0) { - linedit_stringsetemphasis(out, LINEDIT_REVERSE); - } - char *c=in; - for (int i=0; itype - b->type; -} - -/** Returns a color matching tokentype type */ -linedit_color linedit_colorfromtokentype(lineditor *edit, linedit_tokentype type) { - linedit_colormap key = { .type = type, .col = LINEDIT_DEFAULTCOLOR }; - - if (edit->color) { - linedit_colormap *val = bsearch(&key, edit->color->col, edit->color->ncols, sizeof(linedit_colormap), linedit_colormapcmp); - - if (val) return val->col; - } - - return LINEDIT_DEFAULTCOLOR; -} - -/** Print a string with syntax coloring */ -void linedit_syntaxcolorstring(lineditor *edit, linedit_string *in, linedit_string *out) { - linedit_tokenizefn tokenizer=edit->color->tokenizer; - linedit_color col=LINEDIT_DEFAULTCOLOR; - linedit_token tok; - unsigned int iter=0; - - for (char *c=in->string; c!=NULL && *c!='\0';) { - bool success = (tokenizer) (c, edit->color->tokref, &tok); - /* Get the next token */ - if (success && tok.length>0 && tok.start>=c) { - size_t padding=tok.start-c; - /* If there's leading unrecognized characters, print them. */ - if (tok.start>c) { - col=LINEDIT_DEFAULTCOLOR; - linedit_addcstringwithselection(edit, c, c-in->string, padding, &col, out); - } - - /* Set the color */ - col = linedit_colorfromtokentype(edit, tok.type); - - /* Copy the token across */ - if (tok.length>0) { - linedit_addcstringwithselection(edit, tok.start, tok.start-in->string, tok.length, &col, out); - } - - c=tok.start+tok.length; - } else { - col=LINEDIT_DEFAULTCOLOR; - linedit_addcstringwithselection(edit, c, c-in->string, in->length-(c-in->string), &col, out); - return; - }; - iter++; - if (iter>in->length) { - if (!edit->color->lexwarning) { - fprintf(stderr, "\n\rLinedit error: Syntax colorer appears to be stuck in an infinite loop; ensure the tokenizer returns false if it doesn't recognize a token.\n"); - edit->color->lexwarning=true; - } - return; - } - } -} - -/** Print a string without syntax coloring */ -void linedit_plainstring(lineditor *edit, linedit_string *in, linedit_string *out) { - linedit_addcstringwithselection(edit, in->string, 0, in->length, NULL, out); -} - -/* ********************************************************************** - * Keypresses - * ********************************************************************** */ - -/** Identifies the type of keypress */ -typedef enum { - KEY_UNKNOWN, KEY_CHARACTER, - KEY_RETURN, KEY_TAB, KEY_DELETE, - KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, // Arrow keys - KEY_HOME, KEY_END, // Home and End - KEY_SHIFT_LEFT, KEY_SHIFT_RIGHT, // Shift+arrow key - KEY_CTRL, -} keytype; - -/** A single keypress event obtained and processed by the terminal */ -typedef struct { - keytype type; /** Type of keypress */ - char c[5]; /** Up to four bytes of utf8 encoded unicode plus null terminator */ - int nbytes; /** Number of bytes */ -} keypress; - -#define LINEDIT_KEYPRESSGETCHAR(a) ((a)->c[0]) - -/** Raw codes produced by the terminal */ -enum keycodes { - BACKSPACE_CODE = 8, // Backspace - TAB_CODE = 9, // Tab - RETURN_CODE = 13, // Enter or return - ESC_CODE = 27, // Escape - DELETE_CODE = 127, // Delete - ARROW_CODE = 224 // Windows arrow codes -}; - -/** Enable this macro to get reports on unhandled keypresses */ -//#define LINEDIT_DEBUGKEYPRESS - -/** Initializes a keypress structure */ -void linedit_keypressinit(keypress *out) { - out->type=KEY_UNKNOWN; - for (int i=0; i<5; i++) out->c[i]='\0'; - out->nbytes=0; -} - -/** @brief Read and decode a single keypress from the terminal */ -bool linedit_readkey(lineditor *edit, keypress *out) { - out->type=KEY_UNKNOWN; - - if (linedit_getchar(out->c)) { -#ifdef _WIN32 - if (out->c[0] == -32) { - if (linedit_getchar(out->c)) { - bool shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; - - switch (LINEDIT_KEYPRESSGETCHAR(out)) { - case 'H': out->type = KEY_UP; return true; - case 'P': out->type = KEY_DOWN; return true; - case 'M': out->type = (shift ? KEY_SHIFT_RIGHT : KEY_RIGHT); return true; - case 'K': out->type = (shift ? KEY_SHIFT_LEFT : KEY_LEFT); return true; - default: return false; - } - } - } -#endif - if (iscntrl(LINEDIT_KEYPRESSGETCHAR(out))) { - switch (LINEDIT_KEYPRESSGETCHAR(out)) { - case ESC_CODE: - { /* Escape sequences */ - char seq[LINEDIT_CODESTRINGSIZE]; - - /* Read in the escape sequence */ - for (unsigned int i=0; itype=KEY_SHIFT_RIGHT; - } else if (strncmp(seq, "[1;2D", 5)==0) { - out->type=KEY_SHIFT_LEFT; - } else { -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Extended escape sequence: "); - for (unsigned int i=0; i<10; i++) { - printf("%c", seq[i]); - if (isalpha(seq[i])) break; - } - printf("\n"); -#endif - } - } else { - switch (seq[1]) { - case 'A': out->type=KEY_UP; break; - case 'B': out->type=KEY_DOWN; break; - case 'C': out->type=KEY_RIGHT; break; - case 'D': out->type=KEY_LEFT; break; - default: -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Unhandled escape sequence: %c%c%c\r\n", seq[0], seq[1], seq[2]); -#endif - break; - } - } - } - } - break; - case TAB_CODE: out->type=KEY_TAB; break; - case BACKSPACE_CODE: // v fallthrough - case DELETE_CODE: out->type=KEY_DELETE; break; - case RETURN_CODE: out->type=KEY_RETURN; break; - default: - if (LINEDIT_KEYPRESSGETCHAR(out)>0 && LINEDIT_KEYPRESSGETCHAR(out)<27) { /* Ctrl+KEY_KEY_CHARACTER */ - out->type=KEY_CTRL; - out->c[0]+='A'-1; /* Return the character code */ -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Ctrl+character: %c\r\n", out->c[0]); -#endif - } else { -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Unhandled keypress: %d\r\n", LINEDIT_KEYPRESSGETCHAR(out)); -#endif - } - } - - } else { - out->nbytes=linedit_utf8numberofbytes(out->c); - /* Read in the unicode sequence */ - for (int i=1; inbytes; i++) { - if (!linedit_getchar(&out->c[i])) break; - } - out->type=KEY_CHARACTER; -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Character: %s (%i bytes)\r\n", out->c, out->nbytes); -#endif - } - } - return true; -} - -/* ********************************************************************** - * The line editor - * ********************************************************************** */ - -/* ---------------------------------------- - * Get and set editor state - * ---------------------------------------- */ - -/** @brief Gets the current mode */ -lineditormode linedit_getmode(lineditor *edit) { - return edit->mode; -} - -/** @brief Sets the current mode, setting/clearing any state dependent data */ -void linedit_setmode(lineditor *edit, lineditormode mode) { - if (mode!=LINEDIT_HISTORYMODE) { - if (edit->mode==LINEDIT_HISTORYMODE) { - linedit_stringlistremove(&edit->history, edit->history.first); - } - edit->history.posn=0; - } - if (mode==LINEDIT_SELECTIONMODE) { - if (edit->sposn<0) edit->sposn=edit->posn; - } else { - edit->sposn=-1; - } - edit->mode=mode; -} - -/** Sets the current position - * @param edit - the editor - * @param posn - position to set, or negative to move to end */ -void linedit_setposition(lineditor *edit, int posn) { - edit->posn=(posn<0 ? linedit_stringlength(&edit->current) : posn); -} - -/** @brief Advances the position by delta - * @details We ensure that the current position also lies within the string. */ -void linedit_advanceposition(lineditor *edit, int delta) { - edit->posn+=delta; - if (edit->posn<0) edit->posn=0; - int linewidth = linedit_stringlength(&edit->current); - if (edit->posn>linewidth) edit->posn=linewidth; -} - -/** @brief Checks if we're at the end of the input */ -bool linedit_atend(lineditor *edit) { - return (edit->posn==linedit_stringlength(&edit->current)); -} - -/** @brief Checks if we're at a newline character */ -bool linedit_atnewline(lineditor *edit) { - char *c=linedit_stringlocate(&edit->current, edit->posn); - return (c && *c=='\n'); -} - -/* ---------------------------------------- - * Redraw - * ---------------------------------------- */ - -/** Refreshes the display */ -void linedit_redraw(lineditor *edit) { - int sugglength=0; - linedit_string output; /* Holds the output string */ - linedit_stringinit(&output); - - // Render the current buffer to the output string - linedit_stringdefaulttext(&output); - - if (edit->color) { - linedit_syntaxcolorstring(edit, &edit->current, &output); - } else { - linedit_plainstring(edit, &edit->current, &output); - } - - // Display any autocompletion suggestions at the end - if (linedit_aresuggestionsavailable(edit)) { - char *suggestion = linedit_currentsuggestion(edit); - linedit_stringsetemphasis(&output, LINEDIT_BOLD); - linedit_stringaddcstring(&output, suggestion); - sugglength=(int) strlen(suggestion); - } - - // Reset default text - linedit_stringdefaulttext(&output); - - // Retrieve the display coordinates of the current editing position - int xpos, ypos, nlines; - linedit_stringdisplaycoordinates(edit, &edit->current, edit->posn, &xpos, &ypos); - linedit_stringdisplaycoordinates(edit, &edit->current, -1, NULL, &nlines); - - int promptwidth=linedit_stringdisplaywidth(edit, &edit->prompt); - int stringwidth=linedit_stringlength(&edit->current); - - int start=0, end=promptwidth+stringwidth+sugglength; - - linedit_hidecursor(); - linedit_moveup(ypos); // Move to the starting line - linedit_home(); - linedit_defaulttext(); - linedit_write(edit->prompt.string); - - // Now render the output string - linedit_renderstring(edit, output.string, output.length, start, end); - - linedit_erasetoendofline(); - - linedit_moveup(nlines-ypos); // Move to the cursor position - linedit_movetocolumn(promptwidth+xpos-start); - - linedit_showcursor(); - - linedit_stringclear(&output); -} - -/** @brief Changes the height of the current line editing session, erasing garbage if necessary */ -void linedit_changeheight(lineditor *edit, int oldheight, int newheight, int oldvpos, int newvpos) { - if (oldheight==newheight) { - if (oldvposoldheight) { - for (int i=0; icurrent); - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &vpos); - for (int i=vpos; icurrent, edit->posn); - size_t length=linedit_graphemelength(edit, str, edit->current.string+edit->current.length), count; - - if (!linedit_utf8count(str, length, &count)) return; - edit->posn+=count; -} - -/** @brief Moves the current posn to the previous grapheme */ -void linedit_prevgrapheme(lineditor *edit) { - char *str=linedit_stringlocate(&edit->current, edit->posn); - char *prev=edit->current.string; - - size_t len=0, count; - for (char *c=prev; ccurrent.string+edit->current.length); - if (!len) return; - prev=c; - } - - if (!linedit_utf8count(prev, str-prev, &count)) return; - edit->posn-=count; -} - -/** @brief Process a left keypress */ -void linedit_processarrowkeypress(lineditor *edit, lineditormode mode, int delta) { - linedit_setmode(edit, mode); - if (delta>0) { - linedit_nextgrapheme(edit); - } else linedit_prevgrapheme(edit); - - linedit_atnewline(edit); -} - -/** @brief Change the line */ -void linedit_processchangeline(lineditor *edit, int delta) { - int x, yinit, y; - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringcoordinates(&edit->current, edit->posn, &x, &yinit); - y=yinit+delta; - if (y<0) y=0; - linedit_stringfindposition(&edit->current, x, y, &edit->posn); -} - -/** @brief Obtain and process a single keypress */ -bool linedit_processkeypress(lineditor *edit) { - keypress key; - bool regeneratesuggestions=true; - - linedit_keypressinit(&key); - - do { - if (linedit_readkey(edit, &key)) { - switch (key.type) { - case KEY_CHARACTER: - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringinsert(&edit->current, edit->posn, key.c, key.nbytes); - linedit_advanceposition(edit, 1); - break; - case KEY_DELETE: - if (linedit_getmode(edit)==LINEDIT_SELECTIONMODE) { - /* Delete the selection */ - int lposn=(edit->sposn < edit->posn ? edit->sposn : edit->posn); - int rposn = (edit->sposn < edit->posn ? edit->posn : edit->sposn); - linedit_stringdelete(&edit->current, lposn, rposn-lposn); - edit->posn=lposn; - } else { - /* Delete a character */ - if (edit->posn>0) { - linedit_stringdelete(&edit->current, edit->posn-1, 1); - linedit_advanceposition(edit, -1); - } - } - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - break; - case KEY_LEFT: - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, -1); - break; - case KEY_RIGHT: - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, +1); - break; - case KEY_SHIFT_LEFT: - linedit_processarrowkeypress(edit, LINEDIT_SELECTIONMODE, -1); - break; - case KEY_SHIFT_RIGHT: - linedit_processarrowkeypress(edit, LINEDIT_SELECTIONMODE, +1); - break; - case KEY_UP: - { - if (linedit_getmode(edit)!=LINEDIT_HISTORYMODE) { - linedit_setmode(edit, LINEDIT_HISTORYMODE); - linedit_historyadd(edit, (edit->current.string ? edit->current.string : "")); - } - - linedit_historyadvance(edit, 1); - linedit_setposition(edit, -1); - } - break; - case KEY_DOWN: - if (linedit_getmode(edit)==LINEDIT_HISTORYMODE) { - linedit_historyadvance(edit, -1); - linedit_setposition(edit, -1); - } else if (linedit_aresuggestionsavailable(edit)) { - linedit_advancesuggestions(edit, 1); - regeneratesuggestions=false; - } - break; - case KEY_RETURN: - if (linedit_shouldmultiline(edit)) { - linedit_stringaddcstring(&edit->current, "\n"); - linedit_advanceposition(edit, +1); - } else return false; - break; - case KEY_TAB: - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - /* If suggestions are available (i.e. we're at the end of the line)... */ - if (linedit_aresuggestionsavailable(edit)) { - char *sugg = linedit_currentsuggestion(edit); - if (sugg) { - linedit_stringaddcstring(&edit->current, sugg); - linedit_movetoend(edit); - } - } else { // Otherwise simply add a tab character - linedit_stringinsert(&edit->current, edit->posn, "\t", 1); - linedit_advanceposition(edit, +1); - } - break; - case KEY_CTRL: /* Handle ctrl+letter combos */ - switch(LINEDIT_KEYPRESSGETCHAR(&key)) { - case 'A': /* Move to start of line */ - { - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - - int line; - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &line); - linedit_stringfindposition(&edit->current, 0, line, &edit->posn); - } - break; - case 'B': /* Move backward */ - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, -1); - break; - case 'C': /* Copy */ - if (linedit_getmode(edit)==LINEDIT_SELECTIONMODE) { - int lposn=(edit->sposn < edit->posn ? edit->sposn : edit->posn); - int rposn = (edit->sposn < edit->posn ? edit->posn : edit->sposn); - size_t lindx, rindx; - if (!linedit_stringutf8index(&edit->current, lposn, 0, &lindx)) break; - if (!linedit_stringutf8index(&edit->current, rposn, 0, &rindx)) break; - linedit_stringclear(&edit->clipboard); - linedit_stringappend(&edit->clipboard, edit->current.string+lindx, (size_t) rindx-lindx); - } - break; - case 'D': /* Delete the character underneath the cursor */ - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringdelete(&edit->current, edit->posn, 1); - break; - case 'E': { /* Move to end of line */ - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - int line; - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &line); - linedit_stringfindposition(&edit->current, -1, line, &edit->posn); - } - break; - case 'F': /* Move forward */ - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, +1); - break; - case 'G': { /* Abort current editing session */ - linedit_stringclear(&edit->current); - edit->posn=0; - return false; - } - case 'L': /* Clear buffer */ - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringclear(&edit->current); - edit->posn=0; - break; - case 'N': /* Next line */ - linedit_processchangeline(edit, 1); - break; - case 'P': /* Previous line */ - linedit_processchangeline(edit, -1); - break; - case 'V': /* Paste */ // v fallthrough - case 'Y': - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - if (edit->clipboard.length>0) { - linedit_stringinsert(&edit->current, edit->posn, edit->clipboard.string, edit->clipboard.length); - linedit_advanceposition(edit, linedit_stringlength(&edit->clipboard)); - } - break; - default: break; - } - break; - default: - break; - } - } - } while (linedit_keypressavailable()); - - if (regeneratesuggestions) linedit_generatesuggestions(edit); - - return true; -} - -/* ---------------------------------------- - * Main loops for different terminal types - * ---------------------------------------- */ - -/** If we're not attached to a terminal, e.g. a pipe, simply read the - file in. */ -void linedit_noterminal(lineditor *edit) { - int c; - linedit_stringclear(&edit->current); - do { - c = fgetc(stdin); - if (c==EOF || c=='\n') return; - char a = (char) c; - linedit_stringappend(&edit->current, &a, 1); - } while (true); -} - -/** If the terminal is unsupported, default to fgets with a fixed buffer */ -#define LINEDIT_UNSUPPORTEDBUFFER 4096 -void linedit_unsupported(lineditor *edit) { - char buffer[LINEDIT_UNSUPPORTEDBUFFER]; - printf("%s",edit->prompt.string); - if (fgets(buffer, LINEDIT_UNSUPPORTEDBUFFER, stdin)==buffer) { - int length=(int) strlen(buffer); - if (length>0) for (length--; length>=0 && iscntrl(buffer[length]); length--) { - buffer[length]='\0'; /* Remove trailing ctrl chars */ - } - linedit_stringaddcstring(&edit->current, buffer); - } -} - -/** Normal interface used if terminal is present */ -void linedit_supported(lineditor *edit) { - linedit_enablerawmode(); - - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_getterminalwidth(edit); - linedit_setposition(edit, 0); - linedit_redraw(edit); - - int vpos=0, nlines=0; // Keep track of the current vertical position and line number - - while (linedit_processkeypress(edit)) { - int newvpos; - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &newvpos); - int newnlines=linedit_stringcountlines(&edit->current); - linedit_changeheight(edit, nlines, newnlines, vpos, newvpos); - - linedit_redraw(edit); - vpos=newvpos; nlines=newnlines; - } - - /* Ensure we're always on the last line of the input when redrawing before exit */ - linedit_movetoend(edit); - - /* Remove any dangling suggestions */ - linedit_stringlistclear(&edit->suggestions); - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_redraw(edit); - - linedit_disablerawmode(); - - if (edit->current.length>0) { - linedit_historyadd(edit, edit->current.string); - } - - linedit_linefeed(); // Move to next line -} - -/* ********************************************************************** - * Public interface - * ********************************************************************** */ - -/** Initialize a line editor */ -void linedit_init(lineditor *edit) { - if (!edit) return; - edit->color=NULL; - edit->ncols=0; - linedit_stringlistinit(&edit->history); - linedit_stringlistinit(&edit->suggestions); - edit->mode=LINEDIT_DEFAULTMODE; - linedit_stringinit(&edit->current); - linedit_stringinit(&edit->prompt); - linedit_stringinit(&edit->cprompt); - linedit_stringinit(&edit->clipboard); - linedit_setprompt(edit, LINEDIT_DEFAULTPROMPT); - edit->completer=NULL; - edit->cref=NULL; - edit->multiline=NULL; - edit->mlref=NULL; - edit->graphemefn=NULL; - linedit_graphemeinit(&edit->graphemedict); -} - -/** Finalize a line editor */ -void linedit_clear(lineditor *edit) { - if (!edit) return; - if (edit->color) { - free(edit->color); - edit->color=NULL; - } - linedit_historyclear(edit); - linedit_stringlistclear(&edit->suggestions); - linedit_stringclear(&edit->current); - linedit_stringclear(&edit->prompt); - linedit_stringclear(&edit->cprompt); - linedit_stringclear(&edit->clipboard); - linedit_graphemeclear(&edit->graphemedict); -} - -/** Public interface to the line editor. - * @param edit - a line editor that has been initialized with linedit_init. - * @returns the string input by the user, or NULL if nothing entered. */ -char *linedit(lineditor *edit) { - if (!edit) return NULL; /** Ensure we are not passed a NULL pointer */ - - linedit_stringclear(&edit->current); - - switch (linedit_checksupport()) { - case LINEDIT_NOTTTY: linedit_noterminal(edit); break; - case LINEDIT_UNSUPPORTED: linedit_unsupported(edit); break; - case LINEDIT_SUPPORTED: linedit_supported(edit); break; - } - - return linedit_cstring(&edit->current); -} - -/** @brief Configures syntax coloring - * @param[in] edit Line editor to configure - * @param[in] tokenizer Callback function that will identify the next token from a string - * @param[in] ref Reference that will be passed to the tokenizer callback function. - * @param[in] map Map from token types to colors */ -void linedit_syntaxcolor(lineditor *edit, linedit_tokenizefn tokenizer, void *ref, linedit_colormap *map) { - if (!edit) return; - if (!map) return; - if (edit->color) free(edit->color); - int ncols; - - for (ncols=0; map[ncols].type!=LINEDIT_ENDCOLORMAP; ncols++); - - edit->color = malloc(sizeof(linedit_syntaxcolordata)+ncols*sizeof(linedit_colormap)); - - if (!edit->color) return; - - edit->color->tokenizer=tokenizer; - edit->color->tokref=ref; - edit->color->ncols=ncols; - edit->color->lexwarning=false; - for (unsigned int i=0; icolor->col[i]=map[i]; - } - - qsort(edit->color->col, ncols, sizeof(linedit_colormap), linedit_colormapcmp); -} - -/** @brief Configures autocomplete - * @param[in] edit Line editor to configure - * @param[in] completer Callback function that will identify autocomplete suggestions - * @param[in] ref Reference that will be passed to the autocomplete callback function. */ -void linedit_autocomplete(lineditor *edit, linedit_completefn completer, void *ref) { - if (!edit) return; - edit->completer=completer; - edit->cref=ref; -} - -/** @brief Configures multiline editing - * @param[in] edit Line editor to configure - * @param[in] multiline Callback function to test whether to enter multiline mode - * @param[in] ref Reference that will be passed to the multiline callback function. - * @param[in] cprompt Continuation prompt, or NULL to just reuse the regular prompt */ -void linedit_multiline(lineditor *edit, linedit_multilinefn multiline, void *ref, char *cprompt) { - edit->multiline=multiline; - edit->mlref=ref; - linedit_stringclear(&edit->cprompt); - if (cprompt) { - linedit_stringaddcstring(&edit->cprompt, cprompt); - } else { - linedit_stringaddcstring(&edit->cprompt, edit->prompt.string); - } -} - -/** @brief Adds a completion suggestion - * @param completion completion data structure - * @param string string to add */ -void linedit_addsuggestion(linedit_stringlist *completion, char *string) { - linedit_stringlistadd(completion, string); -} - -/** @brief Sets the prompt - * @param edit Line editor to configure - * @param prompt prompt string to use */ -void linedit_setprompt(lineditor *edit, char *prompt) { - if (!edit) return; - linedit_stringclear(&edit->prompt); - linedit_stringaddcstring(&edit->prompt, prompt); -} - -/** @brief Sets the grapheme splitter to use - * @param[in] edit Line editor to configure - * @param[in] graphemefn Grapheme splitter to use */ -void linedit_setgraphemesplitter(lineditor *edit, linedit_graphemefn graphemefn) { - if (!edit) return; - edit->graphemefn=graphemefn; -} - -/** @brief Displays a string with a given color and emphasis - * @param edit Line editor in use - * @param string String to display */ -void linedit_displaywithstyle(lineditor *edit, char *string, linedit_color col, linedit_emphasis emph) { - if (linedit_checksupport()==LINEDIT_SUPPORTED) { - linedit_string out; - linedit_stringinit(&out); - linedit_stringsetcolor(&out, col); - linedit_stringsetemphasis(&out, emph); - linedit_stringaddcstring(&out, string); - linedit_stringdefaulttext(&out); - - printf("%s", out.string); - - linedit_stringclear(&out); - } else { - printf("%s", string); - } -} - -/** @brief Displays a string with syntax coloring - * @param edit Line editor in use - * @param string String to display - */ -void linedit_displaywithsyntaxcoloring(lineditor *edit, char *string) { - if (linedit_checksupport()==LINEDIT_SUPPORTED) { - linedit_string in, out; - linedit_stringinit(&in); - linedit_stringinit(&out); - linedit_stringaddcstring(&in, string); - - linedit_syntaxcolorstring(edit, &in, &out); - linedit_stringdefaulttext(&out); - printf("%s", out.string); - - linedit_stringclear(&in); - linedit_stringclear(&out); - } else { - printf("%s", string); - } -} - -/** @brief Gets the terminal width - * @param edit Line editor in use - * @returns The width in characters */ -int linedit_getwidth(lineditor *edit) { - linedit_getterminalwidth(edit); - return edit->ncols; -} - -/** @brief Checks whether the underlying terminal is a TTY - * @param[in] edit Line editor to use - * @returns true if stdin and stdout are ttys */ -bool linedit_checktty(void) { - return linedit_checksupport()!=LINEDIT_NOTTTY; -} diff --git a/src/linedit.h b/src/linedit.h deleted file mode 100644 index c732a17..0000000 --- a/src/linedit.h +++ /dev/null @@ -1,285 +0,0 @@ -/** @file linedit.h - * @author T J Atherton - * - * @brief A simple UTF8 aware line editor with history, completion, multiline editing and syntax highlighting -*/ - -#ifndef linedit_h -#define linedit_h - -#include -#include -#include -#include -#include -#include - -/* ********************************************************************** - * Types - * ********************************************************************** */ - -/* ----------------------- - * Linedit strings - * ----------------------- */ - -typedef struct linedit_string_s linedit_string; - -/** lineditor strings */ -struct linedit_string_s { - size_t capacity; /** Capacity of the string in bytes */ - size_t length; /** Length in bytes */ - char *string; /** String data */ - linedit_string *next; /** Enable strings to be chained together */ -} ; - -/** A list of strings */ -typedef struct { - int posn; /* We use this to keep track of where in the list the user is */ - linedit_string *first; -} linedit_stringlist; - -/* ----------------------- - * Tokenization - * ----------------------- */ - -/** Token types */ -typedef int linedit_tokentype; - -/** lineditor tokens */ -typedef struct { - linedit_tokentype type; - char *start; - size_t length; -} linedit_token; - -/** @brief Tokenizer callback function - * @param in - a string - * @param ref - pointer to a reference structure provided to linedit by the user - * @param tok - pointer to a token structure that the caller should fill out. - * @details This user function is called when linedit needs to tokenize a string. - * The function should identify the next token in the string and fill out - * the following fields: - * tok->type - should contain the token type. This is used e.g. - * an index to the color array. - * tok->start - should point to the first significant character - * in the token - * tok->length - should contain the length of the token, in bytes - * The function should return true if a token was successfully processed or - * false otherwise. */ -typedef bool (*linedit_tokenizefn) (char *in, void *ref, linedit_token *tok); - -/* ----------------------- - * Color - * ----------------------- */ - -/** Colors */ -typedef enum { - LINEDIT_BLACK, - LINEDIT_RED, - LINEDIT_GREEN, - LINEDIT_YELLOW, - LINEDIT_BLUE, - LINEDIT_MAGENTA, - LINEDIT_CYAN, - LINEDIT_WHITE, - LINEDIT_DEFAULTCOLOR, -} linedit_color; - -typedef enum { - LINEDIT_BOLD, - LINEDIT_UNDERLINE, - LINEDIT_REVERSE, - LINEDIT_NONE -} linedit_emphasis; - -#define LINEDIT_ENDCOLORMAP -1 - -typedef struct { - linedit_tokentype type; - linedit_color col; -} linedit_colormap; - -/** Structure to hold all information related to syntax coloring */ -typedef struct { - linedit_tokenizefn tokenizer; /** A tokenizer function */ - void *tokref; /** Reference passed to tokenizer callback function */ - bool lexwarning; - unsigned int ncols; /** Number of colors provided */ - linedit_colormap col[]; /** Flexible array member mapping token types to colors */ -} linedit_syntaxcolordata; - -/* ----------------------- - * Completion - * ----------------------- */ - -/** @brief Autocompletion callback function - * @param[in] in - a string - * @param[in] ref - pointer to a reference structure provided to linedit by the user - * @param[out] completion - autocompletion structure - * @details This user function is called when linedit requests autocompletion - * of a string. The function should identify any possible suggestions - * and call linedit_addcompletion to add them one by one. - * - * Only *remaining* characters from the suggestion should be added, - * e.g. for "hello" if the user has typed "he" the function should add - * "llo" as a suggestion. - * - * The function should return true if autocompletion was successfully - * processed or false otherwise. -*/ -typedef bool (*linedit_completefn) (char *in, void *ref, linedit_stringlist *completion); - -/* ----------------------- - * Multiline callback - * ----------------------- */ - -/** @brief Multiline callback function - * @param[in] in - a string - * @param[in] ref - pointer to a reference structure provided by the user - * @details This user function is called when linedit wants to know whether - * it should enter multiline mode. The function should parse the - * input and return true if linedit should go to multiline mode or - * false otherwise. Typically, return true if the input is incomplete -*/ -typedef bool (*linedit_multilinefn) (char *in, void *ref); - -/* ----------------------- - * Unicode grapheme support - * ----------------------- */ - -/** Grapheme dictionary entry */ -typedef struct { - char *grapheme; - int width; -} linedit_graphemeentry; - -/** Grapheme dictionary */ -typedef struct { - int count; - int capacity; - linedit_graphemeentry *contents; -} linedit_graphemedictionary; - -/** @brief Unicode grapheme splitter - * @param[in] in - a string - * @param[in] end - end of string - * @returns offset to next grapheme - * @details If provided, linedit will use this function to split UTF8 code into graphemes, which enables length estimation. -*/ -typedef size_t (*linedit_graphemefn) (const char *in, const char *end); - -/* ----------------------- - * lineditor structure - * ----------------------- */ - -#define LINEDIT_DEFAULTPROMPT ">" - -/** Keep track of what the line editor is doing */ -typedef enum { - LINEDIT_DEFAULTMODE, - LINEDIT_SELECTIONMODE, - LINEDIT_HISTORYMODE -} lineditormode; - -/** Holds all state information needed for a line editor */ -typedef struct { - lineditormode mode; /** Current editing mode */ - int posn; /** Position of the cursor in UTF8 characters */ - int sposn; /** Starting point of a selection */ - int ncols; /** Number of columns */ - linedit_string prompt; /** The prompt */ - linedit_string cprompt; /** Continuation prompt */ - - linedit_string current; /** Current string that's being edited */ - linedit_string clipboard;/** Copy/paste clipboard */ - - linedit_stringlist history; /** History list */ - linedit_stringlist suggestions; /** Autocompletion suggestions */ - - linedit_syntaxcolordata *color; /** Structure to handle syntax coloring */ - - linedit_completefn completer; /** Autocompletion callback function*/ - void *cref; /** Reference for autocompletion callback function */ - - linedit_multilinefn multiline; /** Multiline callback */ - void *mlref; /** Reference for multiline callback function */ - - linedit_graphemefn graphemefn; /** Grapheme splitting */ - linedit_graphemedictionary graphemedict; /** Grapheme dictionary */ -} lineditor; - -/* ********************************************************************** - * Public interface - * ********************************************************************** */ - -/** Public interface to the line editor. - * @param[in] edit - A line editor that has been initialized with linedit_init. - * @returns the string input by the user, or NULL if nothing entered. */ -char *linedit(lineditor *edit); - -/** @brief Configures syntax coloring - * @param[in] edit Line editor to configure - * @param[in] tokenizer Callback function that will identify the next token from a string - * @param[in] ref Reference that will be passed to the tokenizer callback function. - * @param[in] map Map from token types to colors */ -void linedit_syntaxcolor(lineditor *edit, linedit_tokenizefn tokenizer, void *ref, linedit_colormap *cols); - -/** @brief Configures autocomplete - * @param[in] edit Line editor to configure - * @param[in] completer Callback function that will identify autocomplete suggestions - * @param[in] ref Reference that will be passed to the autocomplete callback function. */ -void linedit_autocomplete(lineditor *edit, linedit_completefn completer, void *ref); - -/** @brief Configures multiline editing - * @param[in] edit Line editor to configure - * @param[in] multiline Callback function to test whether to enter multiline mode - * @param[in] ref Reference that will be passed to the multiline callback function. - * @param[in] cprompt Continuation prompt, or NULL to just reuse the regular prompt */ -void linedit_multiline(lineditor *edit, linedit_multilinefn multiline, void *ref, char *cprompt); - -/** @brief Adds a completion suggestion - * @param[in] completion Completion data structure - * @param[in] string String to add */ -void linedit_addsuggestion(linedit_stringlist *completion, char *string); - -/** @brief Sets the prompt - * @param[in] edit Line editor to configure - * @param[in] prompt Prompt string to use */ -void linedit_setprompt(lineditor *edit, char *prompt); - -/** @brief Sets the grapheme splitter to use - * @param[in] edit Line editor to configure - * @param[in] graphemefn Grapheme splitter to use */ -void linedit_setgraphemesplitter(lineditor *edit, linedit_graphemefn graphemefn); - -/** @brief Displays a string with a given color and emphasis - * @param[in] edit Line editor to use - * @param[in] string String to display - * @param[in] col Color - * @param[in] emph Emphasis */ -void linedit_displaywithstyle(lineditor *edit, char *string, linedit_color col, linedit_emphasis emph); - -/** @brief Displays a string with syntax coloring - * @param[in] edit Line editor to use - * @param[in] string String to display */ -void linedit_displaywithsyntaxcoloring(lineditor *edit, char *string); - -/** @brief Gets the terminal width - * @param[in] edit Line editor to use - * @returns The width in characters */ -int linedit_getwidth(lineditor *edit); - -/** @brief Checks whether the underlying terminal is a TTY - * @returns true if stdin and stdout are ttys */ -bool linedit_checktty(void); - -/** Enable UTF8 output */ -void linedit_setutf8(void); - -/** Initialize a line editor */ -void linedit_init(lineditor *edit); - -/** Finalize a line editor */ -void linedit_clear(lineditor *edit); - -#endif /* linedit_h */ diff --git a/src/main.c b/src/main.c index 18ef445..bf65be3 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,7 @@ #include #include +#include #include From bd8877d932b1342ce4fc8226ac23d71837426ad6 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:47:41 -0500 Subject: [PATCH 02/63] Correctly free input --- .gitignore | 1 + src/cli.c | 5 ++++- src/debugger.c | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b464cb7..17d4e20 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build-xcode/* .vscode/* .vscode/settings.json /.vscode +REVIEW.md diff --git a/src/cli.c b/src/cli.c index 1deb161..1a176c0 100644 --- a/src/cli.c +++ b/src/cli.c @@ -109,6 +109,7 @@ void cli_inputcallbackfn(vm *v, void *ref, morphoinputmode mode, varray_char *st inline_editor *line=inline_new(""); char *out=inline_readline(line); if (out) varray_charadd(str, out, (int) strlen(out)); + free(out); inline_free(line); } } @@ -352,7 +353,9 @@ void cli(clioptions opt) { } else { /** ... otherwise just raise an error. */ cli_reporterror(&err, v); - } + } + + if (input) free(input); } inline_free(edit); diff --git a/src/debugger.c b/src/debugger.c index 203080e..2fe3e3f 100644 --- a/src/debugger.c +++ b/src/debugger.c @@ -602,6 +602,8 @@ void clidebugger_enter(vm *v) { error_clear(&err); } + if (input) free(input); + clidebugger_showinfo(&debug); } From 6e68eca49163bafe5f142f3fb3e8d1cb51db5945 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:44:49 -0500 Subject: [PATCH 03/63] -version CLI option --- src/main.c | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main.c b/src/main.c index bf65be3..5e51363 100644 --- a/src/main.c +++ b/src/main.c @@ -17,9 +17,9 @@ int main(int argc, const char * argv[]) { clioptions opt = CLI_RUN; const char *file = NULL; int i=0; + bool shouldrun = true; morpho_initialize(); - clidebugger_initialize(); /* Process command line arguments */ for (i=1; i Date: Wed, 11 Feb 2026 21:41:20 -0500 Subject: [PATCH 04/63] Restructure option processing --- src/main.c | 171 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 66 deletions(-) diff --git a/src/main.c b/src/main.c index 5e51363..5ae9e7f 100644 --- a/src/main.c +++ b/src/main.c @@ -1,90 +1,129 @@ /** @file main.c * @author T J Atherton * - * @brief Main entry point + * @brief Main entry point and process options */ #include -#include +#include #include +#include #include #include "cli.h" #include "debugger.h" -int main(int argc, const char * argv[]) { +/* Option handler: return false to exit without running */ +typedef bool (*optionfn)(const char *opt, clioptions *flags); + +static bool opt_version(const char *opt, clioptions *flags) { + version v; + morpho_version(&v); + char buf[VERSION_MAXSTRINGLENGTH]; + version_tostring(&v, VERSION_MAXSTRINGLENGTH, buf); + printf("Morpho v%s\n", buf); + return false; +} + +static bool opt_disassembleonly(const char *opt, clioptions *flags) { + *flags ^= CLI_RUN; + *flags |= CLI_DISASSEMBLE; + return false; +} + +static bool opt_disassemblelist(const char *opt, clioptions *flags) { + *flags |= CLI_DISASSEMBLE | CLI_DISASSEMBLESHOWSRC; + return true; +} + +static bool opt_disassemble(const char *opt, clioptions *flags) { + *flags |= CLI_DISASSEMBLE; + return true; +} + +static bool opt_debug(const char *opt, clioptions *flags) { + *flags |= CLI_DEBUG; + return true; +} + +static bool opt_optimize(const char *opt, clioptions *flags) { + (void)opt; + *flags |= CLI_OPTIMIZE; + return true; +} + +static bool opt_profile(const char *opt, clioptions *flags) { + (void)opt; +#ifdef MORPHO_PROFILER + *flags |= CLI_PROFILE; +#endif + return true; +} + +static bool opt_workers(const char *opt, clioptions *flags) { + (void)flags; + const char *c = opt + 1; + while (*c && *c != '=' && !isdigit((unsigned char)*c)) c++; + if (*c == '=') c++; + int n = isdigit((unsigned char)*c) ? atoi(c) : 0; + if (n < 0) n = 0; + morpho_setthreadnumber(n); + return true; +} + +typedef struct { + const char *s, *l; + optionfn fn; +} option_t; + +static const option_t opt_table[] = { + { "-D", NULL, opt_disassembleonly }, + { "-dl", NULL, opt_disassemblelist }, + { "-d", "--disassemble", opt_disassemble }, + { "-debug", "--debug", opt_debug }, + { "-O", "--optimize", opt_optimize }, +#ifdef MORPHO_PROFILER + { "-profile", "--profile", opt_profile }, +#endif + { "-v", "--version", opt_version }, + { "-w", "--workers", opt_workers }, + { NULL, NULL, NULL }, +}; + +static bool parse_option(const char *arg, clioptions *flags) { + for (int j = 0; opt_table[j].s || opt_table[j].l; j++) { + const char *s = opt_table[j].s, *l = opt_table[j].l; + if ((s && strcmp(arg, s) == 0) || + (l && strcmp(arg, l) == 0)) + return opt_table[j].fn(arg, flags); + } + printf("Unknown option: '%s'\n", arg); + return false; +} + +int main(int argc, const char *argv[]) { clioptions opt = CLI_RUN; const char *file = NULL; - int i=0; - bool shouldrun = true; - + int i = 1; + bool run = true; + morpho_initialize(); - - /* Process command line arguments */ - for (i=1; i Date: Wed, 11 Feb 2026 21:44:56 -0500 Subject: [PATCH 05/63] Option handling --- src/main.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main.c b/src/main.c index 5ae9e7f..39e5fb2 100644 --- a/src/main.c +++ b/src/main.c @@ -14,10 +14,13 @@ #include "cli.h" #include "debugger.h" -/* Option handler: return false to exit without running */ +/** Option handler + * @param[in] opt - option to process + * @param[out] flags - flags to modify + * @returns: control program execution: true to execute program, false if not */ typedef bool (*optionfn)(const char *opt, clioptions *flags); -static bool opt_version(const char *opt, clioptions *flags) { +static bool opt_version(const char *opt, clioptions *flags) { // Display version version v; morpho_version(&v); char buf[VERSION_MAXSTRINGLENGTH]; @@ -26,34 +29,34 @@ static bool opt_version(const char *opt, clioptions *flags) { return false; } -static bool opt_disassembleonly(const char *opt, clioptions *flags) { +static bool opt_disassembleonly(const char *opt, clioptions *flags) { // Disassemble only *flags ^= CLI_RUN; *flags |= CLI_DISASSEMBLE; return false; } -static bool opt_disassemblelist(const char *opt, clioptions *flags) { +static bool opt_disassemblelist(const char *opt, clioptions *flags) { // Disassemble & list *flags |= CLI_DISASSEMBLE | CLI_DISASSEMBLESHOWSRC; return true; } -static bool opt_disassemble(const char *opt, clioptions *flags) { +static bool opt_disassemble(const char *opt, clioptions *flags) { // Disassemble before running *flags |= CLI_DISASSEMBLE; return true; } -static bool opt_debug(const char *opt, clioptions *flags) { +static bool opt_debug(const char *opt, clioptions *flags) { // Enable debugging *flags |= CLI_DEBUG; return true; } -static bool opt_optimize(const char *opt, clioptions *flags) { +static bool opt_optimize(const char *opt, clioptions *flags) { // Enable optimization (void)opt; *flags |= CLI_OPTIMIZE; return true; } -static bool opt_profile(const char *opt, clioptions *flags) { +static bool opt_profile(const char *opt, clioptions *flags) { // Enable profiling (void)opt; #ifdef MORPHO_PROFILER *flags |= CLI_PROFILE; @@ -61,7 +64,7 @@ static bool opt_profile(const char *opt, clioptions *flags) { return true; } -static bool opt_workers(const char *opt, clioptions *flags) { +static bool opt_workers(const char *opt, clioptions *flags) { // Set number of worker threads (void)flags; const char *c = opt + 1; while (*c && *c != '=' && !isdigit((unsigned char)*c)) c++; From 7e11202910e6182761fc2ce6152abbe0fddc303a Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:48:01 -0500 Subject: [PATCH 06/63] Use inexact match --- src/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.c b/src/main.c index 39e5fb2..e6f0541 100644 --- a/src/main.c +++ b/src/main.c @@ -97,8 +97,8 @@ static const option_t opt_table[] = { static bool parse_option(const char *arg, clioptions *flags) { for (int j = 0; opt_table[j].s || opt_table[j].l; j++) { const char *s = opt_table[j].s, *l = opt_table[j].l; - if ((s && strcmp(arg, s) == 0) || - (l && strcmp(arg, l) == 0)) + if ((s && strncmp(arg, s, strlen(s)) == 0) || + (l && strncmp(arg, l, strlen(l)) == 0)) return opt_table[j].fn(arg, flags); } printf("Unknown option: '%s'\n", arg); From e8dc6f36843909a2115a2fdeaf1daf10bf64d502 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:28:10 -0500 Subject: [PATCH 07/63] Command line eval option --- src/cli.c | 48 ++++++++++++++++++++++++ src/cli.h | 1 + src/main.c | 107 ++++++++++++++++++++++++++++++++++------------------- 3 files changed, 117 insertions(+), 39 deletions(-) diff --git a/src/cli.c b/src/cli.c index 1a176c0..a86e9dd 100644 --- a/src/cli.c +++ b/src/cli.c @@ -373,6 +373,54 @@ void cli(clioptions opt) { * Run a file * ********************************************************************** */ +/** Compile and run source string (no file). Used by -e / --eval. */ +void cli_runstring(const char *src, clioptions opt) { + program *p = morpho_newprogram(); + compiler *c = morpho_newcompiler(p); + vm *v = morpho_newvm(); + + inline_editor *edit = inline_new(CLI_PROMPT); + lexer l; + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); + + morpho_setinputfn(v, cli_inputcallbackfn, &edit); + morpho_setprintfn(v, cli_printcallbackfn, &edit); + morpho_setwarningfn(v, cli_warningcallbackfn, &edit); + morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); + + error err; + error_init(&err); + + bool success = morpho_compile((char *) src, c, (opt & CLI_OPTIMIZE), &err); + + if (success) { + if (opt & CLI_DISASSEMBLE) { + if (opt & CLI_DISASSEMBLESHOWSRC) { + cli_disassemblewithsrc(p, (char *)src); + } else { + morpho_disassemble(v, p, NULL); + } + } + if (opt & CLI_RUN) { + if (opt & CLI_DEBUG) { + success = morpho_debug(v, p); + } else if (opt & CLI_PROFILE) { + success = morpho_profile(v, p); + } else { + success = morpho_run(v, p); + } + if (!success) cli_reporterror(morpho_geterror(v), v); + } + } else { + cli_reporterror(&err, v); + } + + inline_free(edit); + morpho_freevm(v); + morpho_freeprogram(p); + morpho_freecompiler(c); +} + /** Loads and runs a file. */ void cli_run(const char *in, clioptions opt) { program *p = morpho_newprogram(); diff --git a/src/cli.h b/src/cli.h index 3b16931..f5f8040 100644 --- a/src/cli.h +++ b/src/cli.h @@ -46,6 +46,7 @@ void cli_displaywithstyle(int col, int emph, int n, ...); void cli_reporterror(error *err, vm *v); void cli_run(const char *in, clioptions opt); +void cli_runstring(const char *src, clioptions opt); void cli(clioptions opt); char *cli_loadsource(const char *in); diff --git a/src/main.c b/src/main.c index e6f0541..79b3a79 100644 --- a/src/main.c +++ b/src/main.c @@ -14,13 +14,19 @@ #include "cli.h" #include "debugger.h" -/** Option handler - * @param[in] opt - option to process - * @param[out] flags - flags to modify - * @returns: control program execution: true to execute program, false if not */ -typedef bool (*optionfn)(const char *opt, clioptions *flags); +/** Context passed to option handlers that need argc/argv (e.g. eval). */ +typedef struct { + int argc; + const char **argv; + int *idx; +} opt_ctx; + +/** Option handler. arg is NULL for options that take no argument. + * @returns true to continue (run file/repl), false to exit without running. */ +typedef bool (*optionfn)(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx); -static bool opt_version(const char *opt, clioptions *flags) { // Display version +static bool opt_version(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)flags; (void)ctx; version v; morpho_version(&v); char buf[VERSION_MAXSTRINGLENGTH]; @@ -29,79 +35,105 @@ static bool opt_version(const char *opt, clioptions *flags) { // Display version return false; } -static bool opt_disassembleonly(const char *opt, clioptions *flags) { // Disassemble only +static bool opt_disassembleonly(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; *flags ^= CLI_RUN; *flags |= CLI_DISASSEMBLE; return false; } -static bool opt_disassemblelist(const char *opt, clioptions *flags) { // Disassemble & list +static bool opt_disassemblelist(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; *flags |= CLI_DISASSEMBLE | CLI_DISASSEMBLESHOWSRC; return true; } -static bool opt_disassemble(const char *opt, clioptions *flags) { // Disassemble before running +static bool opt_disassemble(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; *flags |= CLI_DISASSEMBLE; return true; } -static bool opt_debug(const char *opt, clioptions *flags) { // Enable debugging +static bool opt_debug(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; *flags |= CLI_DEBUG; return true; } -static bool opt_optimize(const char *opt, clioptions *flags) { // Enable optimization - (void)opt; +static bool opt_optimize(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; *flags |= CLI_OPTIMIZE; return true; } -static bool opt_profile(const char *opt, clioptions *flags) { // Enable profiling - (void)opt; +static bool opt_profile(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; #ifdef MORPHO_PROFILER *flags |= CLI_PROFILE; #endif return true; } -static bool opt_workers(const char *opt, clioptions *flags) { // Set number of worker threads - (void)flags; - const char *c = opt + 1; +static bool opt_workers(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + const char *c = arg ? arg : (opt + 1); while (*c && *c != '=' && !isdigit((unsigned char)*c)) c++; if (*c == '=') c++; int n = isdigit((unsigned char)*c) ? atoi(c) : 0; if (n < 0) n = 0; morpho_setthreadnumber(n); + (void)flags; return true; } +static bool opt_eval(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; + clidebugger_initialize(); + if (ctx && ctx->idx && ctx->argc - *ctx->idx - 1 > 0) + morpho_setargs(ctx->argc - *ctx->idx - 1, ctx->argv + *ctx->idx + 1); + cli_runstring(arg, *flags); + return false; +} + typedef struct { const char *s, *l; + bool takes_arg; optionfn fn; } option_t; static const option_t opt_table[] = { - { "-D", NULL, opt_disassembleonly }, - { "-dl", NULL, opt_disassemblelist }, - { "-d", "--disassemble", opt_disassemble }, - { "-debug", "--debug", opt_debug }, - { "-O", "--optimize", opt_optimize }, + { "-D", NULL, false, opt_disassembleonly }, + { "-dl", NULL, false, opt_disassemblelist }, + { "-d", "--disassemble", false, opt_disassemble }, + { "-debug", "--debug", false, opt_debug }, + { "-e", "--eval", true, opt_eval }, + { "-O", "--optimize", false, opt_optimize }, #ifdef MORPHO_PROFILER - { "-profile", "--profile", opt_profile }, + { "-profile", "--profile", false, opt_profile }, #endif - { "-v", "--version", opt_version }, - { "-w", "--workers", opt_workers }, - { NULL, NULL, NULL }, + { "-v", "--version", false, opt_version }, + { "-w", "--workers", false, opt_workers }, + { NULL, NULL, false, NULL }, }; -static bool parse_option(const char *arg, clioptions *flags) { +/** Parse one option at argv[*idx]. If option takes an argument, consumes *idx+1 and advances *idx. */ +static bool parse_option(int argc, const char *argv[], int *idx, clioptions *flags) { + const char *arg = argv[*idx], *opt_arg = NULL; for (int j = 0; opt_table[j].s || opt_table[j].l; j++) { const char *s = opt_table[j].s, *l = opt_table[j].l; - if ((s && strncmp(arg, s, strlen(s)) == 0) || - (l && strncmp(arg, l, strlen(l)) == 0)) - return opt_table[j].fn(arg, flags); + if ((s && strncmp(arg, s, strlen(s)) == 0) || (l && strncmp(arg, l, strlen(l)) == 0)) { + if (opt_table[j].takes_arg) { + if (*idx + 1 >= argc) { + fprintf(stderr, "morpho: %s requires an argument.\n", arg); + return false; + } + opt_arg = argv[*idx + 1]; + (*idx)++; + } + opt_ctx ctx = { argc, argv, idx }; + return opt_table[j].fn(arg, opt_arg, flags, &ctx); + } } - printf("Unknown option: '%s'\n", arg); + printf("Unknown option %s.\n", arg); return false; } @@ -113,19 +145,16 @@ int main(int argc, const char *argv[]) { morpho_initialize(); - for (; i < argc; i++) { // Process args + for (; i < argc && !file; i++) { const char *arg = argv[i]; - if (arg[0] == '-') { - run &= parse_option(arg, &opt); - } else { - file = arg; - break; - } + if (arg && arg[0] == '-') { + run &= parse_option(argc, argv, &i, &opt); + } else if (arg) file = arg; } if (run) { clidebugger_initialize(); - if (i < argc) morpho_setargs(argc - i - 1, argv + i); + if (i < argc) morpho_setargs(argc - i - 1, argv + i); // Pass unused args to morpho (file ? cli_run(file, opt) : cli(opt)); } From 6fa618a0bf3fc22335d8843e5084d00080822cd8 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:14:54 -0500 Subject: [PATCH 08/63] Remove help files as they're in the main morpho repo --- help/Makefile | 20 --- help/array.md | 34 ----- help/builtin.md | 294 ------------------------------------------ help/classes.md | 85 ------------ help/color.md | 132 ------------------- help/complex.md | 43 ------ help/conf.py | 60 --------- help/constants.md | 14 -- help/controlflow.md | 192 --------------------------- help/delaunay.md | 48 ------- help/dictionary.md | 51 -------- help/errors.md | 256 ------------------------------------ help/field.md | 78 ----------- help/file.md | 96 -------------- help/functionals.md | 251 ------------------------------------ help/functions.md | 93 ------------- help/graphics.md | 196 ---------------------------- help/help.md | 34 ----- help/implicitmesh.md | 27 ---- help/index.rst | 69 ---------- help/kdtree.md | 81 ------------ help/list.md | 132 ------------------- help/make.bat | 35 ----- help/matrix.md | 175 ------------------------- help/mesh.md | 66 ---------- help/meshgen.md | 113 ---------------- help/meshslice.md | 43 ------ help/meshtools.md | 229 -------------------------------- help/modules.md | 39 ------ help/optimize.md | 83 ------------ help/plot.md | 105 --------------- help/povray.md | 80 ------------ help/range.md | 32 ----- help/requirements.txt | 7 - help/selection.md | 67 ---------- help/sparse.md | 27 ---- help/string.md | 58 --------- help/syntax.md | 147 --------------------- help/system.md | 71 ---------- help/values.md | 45 ------- help/variables.md | 70 ---------- help/vtk.md | 116 ----------------- 42 files changed, 3894 deletions(-) delete mode 100644 help/Makefile delete mode 100644 help/array.md delete mode 100644 help/builtin.md delete mode 100644 help/classes.md delete mode 100644 help/color.md delete mode 100644 help/complex.md delete mode 100644 help/conf.py delete mode 100644 help/constants.md delete mode 100644 help/controlflow.md delete mode 100644 help/delaunay.md delete mode 100644 help/dictionary.md delete mode 100644 help/errors.md delete mode 100644 help/field.md delete mode 100644 help/file.md delete mode 100644 help/functionals.md delete mode 100644 help/functions.md delete mode 100644 help/graphics.md delete mode 100644 help/help.md delete mode 100644 help/implicitmesh.md delete mode 100644 help/index.rst delete mode 100644 help/kdtree.md delete mode 100644 help/list.md delete mode 100644 help/make.bat delete mode 100644 help/matrix.md delete mode 100644 help/mesh.md delete mode 100644 help/meshgen.md delete mode 100644 help/meshslice.md delete mode 100644 help/meshtools.md delete mode 100644 help/modules.md delete mode 100644 help/optimize.md delete mode 100644 help/plot.md delete mode 100644 help/povray.md delete mode 100644 help/range.md delete mode 100644 help/requirements.txt delete mode 100644 help/selection.md delete mode 100644 help/sparse.md delete mode 100644 help/string.md delete mode 100644 help/syntax.md delete mode 100644 help/system.md delete mode 100644 help/values.md delete mode 100644 help/variables.md delete mode 100644 help/vtk.md diff --git a/help/Makefile b/help/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/help/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/help/array.md b/help/array.md deleted file mode 100644 index 4f0b197..0000000 --- a/help/array.md +++ /dev/null @@ -1,34 +0,0 @@ -[comment]: # (Array class help) -[version]: # (0.5) - -# Array -[tagarray]: # (Array) - -Arrays are collection objects that can have any number of indices. Their size is set when they are created: - - var a[5] - var b[2,2] - var c[nv,nv,nv] - -Values can be retrieved with appropriate indices: - - print a[0,0] - -Array can be indexed with slices: - - print a[[0,2,4],2] - print a[1,0..2] - -Any morpho value can be stored in an array element - - a[0,0] = [1,2,3] - -[showsubtopics]: # (subtopics) - -## Dimensions -[tagdimensions]: # (Dimensions) - -Get the dimensions of an Array object: - - var a[2,2] - print a.dimensions() // expect: [ 2, 2 ] diff --git a/help/builtin.md b/help/builtin.md deleted file mode 100644 index 0489dd4..0000000 --- a/help/builtin.md +++ /dev/null @@ -1,294 +0,0 @@ -[comment]: # (Builtin function help) -[version]: # (0.5) - -# Builtin functions -[tagbuiltin]: # (builtin) - -Morpho provides a number of built-in functions. - -[showsubtopics]: # (subtopics) - -## Random -[tagrandom]: # (random) -[tagrand]: # (rand) - -The `random` function generates a random number from a uniform distribution on the interval [0,1]. - - print random() - -See also `randomnormal` and `randomint`. - -## Randomnormal -[tagrandomnormal]: # (randomnormal) - -The `randomnormal` function generates a random number from a normal (gaussian) distribution with unit variance and zero offset. - - print randomnormal() - -See also `random` and `randomint`. - -## Randomint -[tagrandomnormal]: # (randomnormal) - -The `randomint` function generates a random integer with a specified maximum value. - - print randomint(10) // Generates a random integer [0,10) - -## isnil -[tagisnil]: # (isnil) - -Returns `true` if a value is `nil` or `false` otherwise. - -## isint -[tagisint]: # (isint) - -Returns `true` if a value is an integer or `false` otherwise. - -## isfloat -[tagisfloat]: # (isfloat) - -Returns `true` if a value is a floating point number or `false` otherwise. - -## isbool -[tagisbool]: # (isbool) - -Returns `true` if a value is a boolean or `false` otherwise. - -## isobject -[tagisobject]: # (isobject) - -Returns `true` if a value is an object or `false` otherwise. - -## isstring -[tagisstring]: # (isstring) - -Returns `true` if a value is a string or `false` otherwise. - -## isclass -[tagisclass]: # (isclass) - -Returns `true` if a value is a class or `false` otherwise. - -## isrange -[tagisrange]: # (isrange) - -Returns `true` if a value is a range or `false` otherwise. - -## isdictionary -[tagisdictionary]: # (isdictionary) - -Returns `true` if a value is a dictionary or `false` otherwise. - -## islist -[tagislist]: # (islist) - -Returns `true` if a value is a list or `false` otherwise. - -## isarray -[tagisarray]: # (isarray) - -Returns `true` if a value is an array or `false` otherwise. - -## ismatrix -[tagismatrix]: # (ismatrix) - -Returns `true` if a value is a matrix or `false` otherwise. - -## issparse -[tagissparse]: # (issparse) - -Returns `true` if a value is a sparse matrix or `false` otherwise. - -## isinf -[tagisinf]: # (isinf) - -Returns `true` if a value is infinite or `false` otherwise. - -## isnan -[tagisnan]: # (isnan) - -Returns `true` if a value is a Not a Number or `false` otherwise. - -## iscallable -[tagiscallable]: # (iscallable) - -Returns `true` if a value is callable or `false` otherwise. - -## isfinite -[tagisfinite]: # (isfinite) - -Returns `true` if a value is finite or `false` otherwise. - - print isfinite(1) // expect: true - print isfinite(1/0) // expect: false - -## isnumber -[tagisnumber]: # (isnumber) - -Returns `true` if a value is a real number, or `false` otherwise. - - print isnumber(1) // expect: true - print isnumber(Object()) // expect: false - -## ismesh -[tagismesh]: # (ismesh) - -Returns `true` if a value is a `Mesh`, or `false` otherwise. - -## isselection -[tagisselection]: # (isselection) - -Returns `true` if a value is a `Selection`, or `false` otherwise. - -## isfield -[tagisfield]: # (isfield) - -Returns `true` if a value is a `Field`, or `false` otherwise. - -## Apply -[tagapply]: # (apply) - -Apply calls a function with the arguments provided as a list: - - apply(f, [0.5, 0.5]) // calls f(0.5, 0.5) - -It's often useful where a function or method and/or the number of parameters isn't known ahead of time. The first parameter to apply can be any callable object, including a method invocation or a closure. - -You may also instead omit the list and use apply with multiple arguments: - - apply(f, 0.5, 0.5) // calls f(0.5, 0.5) - -There is one edge case that occurs when you want to call a function that accepts a single list as a parameter. In this case, enclose the list in another list: - - apply(f, [[1,2]]) // equivalent to f([1,2]) - -## Abs -[tagabs]: # (abs) - -Returns the absolute value of a number: - - print abs(-10) // prints 10 - -## Arctan -[tagarctan]: # (arctan) - -Returns the arctangent of an input value that lies from `-Inf` to `Inf`. You can use one argument: - - print arctan(0) // expect: 0 - -or use two arguments to return the angle in the correct quadrant: - - print arctan(x, y) - -Note the order `x`, `y` differs from some other languages. - -## Exp -[tagexp]: # (exp) - -Exponential function `e^x`. Inverse of `log`. - - print exp(0) // expect: 1 - print exp(Pi*im) // expect: -1 + 0im - -## Log -[taglog]: # (log) - -Natural logarithm function. Inverse of `exp`. - - print log(1) // expect: 0 - -## Log10 -[taglog10]: # (log10) - -Base 10 logarithm function. - - print log10(10) // expect: 1 - -## Sin -[tagsin]: # (sin) - -Sine trigonometric function. - - print sin(0) // expect: 0 - -## Sinh -[tagsinh]: # (sinh) - -Hyperbolic sine trigonometric function. - - print sinh(0) // expect: 0 - -## Cos -[tagcos]: # (cos) - -Cosine trigonometric function. - - print cos(0) // expect: 1 - -## Cosh -[tagcosh]: # (cosh) - -Hyperbolic cosine trigonometric function. - - print cosh(0) // expect: 1 - -## Tan -[tagtan]: # (tan) - -Tangent trigonometric function. - - print tan(0) // expect: 0 - -## Tanh -[tagtanh]: # (tanh) - -Hyperbolic tangent trigonometric function. - - print tanh(0) // expect: 0 - -## Asin -[tagasin]: # (asin) - -Inverse sine trigonometric function. Returns a value on the interval `[-Pi/2,Pi/2]`. - - print asin(0) // expect: 0 - -## Acos -[tagacos]: # (acos) - -Inverse cosine trigonometric function. Returns a value on the interval `[-Pi/2,Pi/2]`. - - print acos(1) // expect: 0 - -## Sqrt -[tagsqrt]: # (sqrt) - -Square root function. - - print sqrt(4) // expect: 2 - -## Min -[tagmin]: # (min) - -Finds the minimum value of its arguments. If any of the arguments are Objects and are enumerable, (e.g. a `List`), `min` will search inside them for a minimum value. Accepts any number of arguments. - - print min(3,2,1) // expect: 1 - print min([3,2,1]) // expect: 1 - print min([3,2,1],[0,-1,2]) // expect: -2 - -## Max -[tagmax]: # (max) - -Finds the maximum value of its arguments. If any of the arguments are Objects and are enumerable, (e.g. a `List`), `max` will search inside them for a maximum value. Accepts any number of arguments. - - print min(3,2,1) // expect: 3 - print min([3,2,1]) // expect: 3 - print min([3,2,1],[0,-1,2]) // expect: 3 - -## Bounds -[tagbounds]: # (bounds) - -Returns both the results of `min` and `max` as a list, Providing a set of bounds for its arguments and any enumerable objects within them. - - print bounds(1,2,3) // expect: [1,3] - print bounds([3,2,1],[0,-1,2]) // expect: [-1,3] diff --git a/help/classes.md b/help/classes.md deleted file mode 100644 index f57f6f0..0000000 --- a/help/classes.md +++ /dev/null @@ -1,85 +0,0 @@ -[comment]: # (Morpho classes help file) -[version]: # (0.5) - -[toplevel]: # - -# Classes -[tagclass]: # (class) - -Classes are defined using the `class` keyword followed by the name of the class. -The definition includes methods that the class responds to. The special `init` method -is called whenever an object is created. - - class Cake { - init(type) { - self.type = type - } - - eat() { - print "A delicious "+self.type+" cake" - } - } - -Objects are created by calling the class as if it was a function: - - var c = Cake("carrot") - -Methods are called using the . operator: - - c.eat() - -[showsubtopics]: # (subtopics) - -## Is -[tagis]: # (is) - -The `is` keyword is used to specify a class's superclass: - - class A is B { - - } - -All methods defined by the superclass `B` are copied into the new class `A`, *before* any methods specified in the class definition. Hence, you can replace methods from the superclass simply by defining a method with the same name. - -## With -[tagwith]: # (with) -[tagmixin]: # (mixin) - -The `with` keyword is used together with `is` to insert additional methods into a class definition *without* making them the superclass. These are often called `mixins`. These methods are inserted after the superclass's methods. Multiple classes can be specified after `with`; they are added in the order specified. - - class A is B with C, D { - - } - -Here `B` is the superclass of `A`, but methods defined by `C` and `D` are also available to `A`. If `B`, `C` and `D` define methods with the same name, those in `C` take precedence over any in `B` and those in `D` take precedence over `B` and `C`. - -## Self -[tagself]: # (self) - -The `self` keyword is used to access an object's properties and methods from within its definition. - - class Vehicle { - init (type) { self.type = type } - - drive () { print "Driving my ${self.type}." } - } - -## Super -[tagsuper]: # (super) - -The keyword `super` allows you to access methods provided by an object's superclass rather than its own. This is particularly useful when the programmer wants a class to extend the functionality of a parent class, but needs to make sure the old behavior is still maintained. - -For example, consider the following pair of classes: - - class Lunch { - init(type) { self.type=type } - } - - class Soup is Lunch { - init(type) { - print "Delicious soup!" - super.init(type) - } - } - -The subclass Soup uses `super` to call the original initializer. diff --git a/help/color.md b/help/color.md deleted file mode 100644 index 93a5c31..0000000 --- a/help/color.md +++ /dev/null @@ -1,132 +0,0 @@ -[comment]: # (Color module help) -[version]: # (0.5) - -# Color -[tagcolor]: # (color) - -The `color` module provides support for working with color. Colors are represented in morpho by `Color` objects. The module predefines some colors including `Red`, `Green`, `Blue`, `Black`, `White`. - -To use the module, use import as usual: - - import color - -Create a Color object from an RGB pair: - - var col = Color(0.5,0.5,0.5) // A 50% gray - -The `color` module also provides `ColorMap`s, which are give a sequence of colors as a function of a parameter; these are useful for plotting the values of a `Field` for example. - -[showsubtopics]: # (subtopics) - -## RGB -[tagrgb]: # (rgb) - -Gets the rgb components of a `Color` or `ColorMap` object as a list. Takes a single argument in the range 0 to 1, although the result will only depend on this argument if the object is a `ColorMap`. - - var col = Color(0.1,0.5,0.7) - print col.rgb(0) - -## Red -[tagred]: # (red) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Green -[taggreen]: # (green) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Blue -[tagblue]: # (blue) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## White -[tagwhite]: # (white) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Black -[tagblack]: # (black) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Cyan -[tagcyan]: # (cyan) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Magenta -[tagmagenta]: # (magenta) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Yellow -[tagyellow]: # (yellow) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Brown -[tagbrown]: # (brown) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Orange -[tagorange]: # (orange) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Pink -[tagpink]: # (pink) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Purple -[tagpurple]: # (purple) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Colormap -[tagcolormap]: # (colormap) -The `color` module provides `ColorMap`s which are subclasses of `Color` that map a single parameter in the range 0 to 1 onto a continuum of colors. `Color`s and `Colormap`s have the same interface. - -Get the red, green or blue components of a color or colormap: - - var col = HueMap() - print col.red(0.5) // argument can be in range 0 to 1 - -Get all three components as a list: - - col.rgb(0) - -Create a grayscale: - - var c = Gray(0.2) // 20% gray - -Available ColorMaps: `GradientMap`, `GrayMap`, `HueMap`, `ViridisMap`, `MagmaMap`, `InfernoMap` and `PlasmaMap`. - -## GradientMap -[taggradientmap]: # (gradientmap) - -`GradientMap` is a `Colormap` that displays a white-green-purple sequence. - -## GrayMap -[taggraymap]: # (graymap) - -`GrayMap` is a `Colormap` that displays grayscales. - -## HueMap -[taghuemap]: # (huemap) - -`HueMap` is a `Colormap` that displays vivid colors. It is periodic on the interval 0 to 1. - -## ViridisMap -[tagviridismap]: # (viridismap) - -`ViridisMap` is a `Colormap` that displays a purple-green-yellow sequence. -It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. - -## MagmaMap -[tagmagmamap]: # (magmamap) - -`MagmaMap` is a `Colormap` that displays a black-red-yellow sequence. -It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. - -## InfernoMap -[taginfernomap]: # (infernomap) - -`InfernoMap` is a `Colormap` that displays a black-red-yellow sequence. -It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. - -## PlasmaMap -[tagplasmamap]: # (plasmamap) - -`InfernoMap` is a `Colormap` that displays a blue-red-yellow sequence. It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. diff --git a/help/complex.md b/help/complex.md deleted file mode 100644 index edc5252..0000000 --- a/help/complex.md +++ /dev/null @@ -1,43 +0,0 @@ -[comment]: # (Complex help) -[version]: # (0.5) - -# Complex -[tagcomplex]: # (complex) -[tagim]: # (im) - -Morpho provides complex numbers. The keyword `im` is used to denote the imaginary part of a complex number: - - var a=1+5im - print a*a - -Print values on the unit circle in the complex plane: - - import constants - for (phi in 0..Pi:Pi/5) print exp(im*phi) - -Get the real and imaginary parts of a complex number: - - print real(a) - print imag(a) - -or alternatively: - - print a.real() - print a.imag() - -[showsuptopics]: # subtopics - -## Angle -[tagangle]: # (angle) - -Returns the angle `phi` associated with the polar representation of a complex number `r*exp(im*phi)`: - - print z.angle() - -## Conj -[tagconjugate]: # (conjugate) -[tagconj]: # (conj) - -Returns the complex conjugate of a number: - - print z.conj() diff --git a/help/conf.py b/help/conf.py deleted file mode 100644 index 6b7bcc2..0000000 --- a/help/conf.py +++ /dev/null @@ -1,60 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -# -- Project information ----------------------------------------------------- - -project = 'morpho' -copyright = '2021, T J Atherton' -author = 'T J Atherton' - -# The full version, including alpha/beta/rc tags -release = '0.5.0' - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['myst_parser'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# Use recommonmark to parse md files -from recommonmark.parser import CommonMarkParser -source_parsers = {'.md': CommonMarkParser} -source_suffix = ['.rst', '.md'] - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - - -latex_engine = 'xelatex' diff --git a/help/constants.md b/help/constants.md deleted file mode 100644 index 01d2fcc..0000000 --- a/help/constants.md +++ /dev/null @@ -1,14 +0,0 @@ -[comment]: # (Constants module help) -[version]: # (0.5) - -# Constants -[tagconstants]: # (constants) - -The constants module contains a number of useful mathematical and physical constants. Import it like any other module: - - import constants - -Available constants: - -* `E` the base of natural logarithms. -* `Pi` ratio of the perimeter of a circle to its diameter. diff --git a/help/controlflow.md b/help/controlflow.md deleted file mode 100644 index 973a1ac..0000000 --- a/help/controlflow.md +++ /dev/null @@ -1,192 +0,0 @@ -[comment]: # (Morpho control flow help file) -[version]: # (0.5) - -[toplevel]: # - -# Control Flow -[tagcontrol]: # (control) - -Control flow statements are used to determine whether and how many times a selected piece of code is executed. These include: - -* `if` - Selectively execute a piece of code if a condition is met. -* `else` - Execute a different block of code if the test in an `if` statement fails. -* `for` - Repeatedly execute a section of code with a counter -* `while` - Repeatedly execute a section of code while a condition is true. - -## If -[tagif]: # (if) -[tagelse]: # (else) - -`If` allows you to selectively execute a section of code depending on whether a condition is met. The simplest version looks like this: - - if (x<1) print x - -where the body of the loop, `print x`, is only executed if x is less than 1. The body can be a code block to accommodate longer sections of code: - - if (x<1) { - ... // do something - } - -If you want to choose between two alternatives, use `else`: - - if (a==b) { - // do something - } else { - // this code is executed only if the condition is false - } - -You can even chain multiple tests together like this: - - if (a==b) { - // option 1 - } else if (a==c) { - // option 2 - } else { - // something else - } - -## While -[tagwhile]: # (while) - -While loops repeat a section of code while a condition is true. For example, - - var k=1 - while (k <= 4) { print k; k+=1 } - ^cond ^body - -prints the numbers 1 to 4. The loop has two sections: `cond` is the condition to be executed and `body` is the section of code to be repeated. - -Simple loops like the above example, especially those that involve counting out a sequence of numbers, are more conveniently written using a `for` loop, - - for (k in 1..4) print k - -Where `while` loops can be very useful is where the state of an object is being changed in the loop, e.g. - - var a = List(1,2,3,4) - while (a.count()>0) print a.pop() - -which prints 4,3,2,1. - -## Do -[tagdo]: # (do) - -A `do`...`while` loop repeats code while a condition is true---similar to a `while` loop---but the test happens at the end: - - var k=1 - do { - print k; - k+=1 - } while (k<5) - -which prints 1,2,3,4 - -Hence this type of loop executes at least one interation - -## For -[tagfor]: # (for) -[tagin]: # (in) - -For loops allow you to repeatedly execute a section of code. They come in two versions: the simpler version looks like this, - - for (var i in 1..5) print i - -which prints the numbers 1 to 5 in turn. The variable `i` is the *loop variable*, which takes on a different value each iteration. `1..5` is a range, which denotes a sequence of numbers. The *body* of the loop, `print i`, is the code to be repeatedly executed. - -Morpho will implicitly insert a `var` before the loop variable if it's missing, so this works too: - - for (i in 1..5) print i - -If you want your loop variable to count in increments other than 1, you can specify a stepsize in the range: - - for (i in 1..5:2) print i - ^step - -Ranges need not be integer: - - for (i in 0.1..0.5:0.1) print i - -You can also replace the range with other kinds of collection object to loop over their contents: - - var a = Matrix([1,2,3,4]) - for (x in a) print x - -Morpho iterates over the collection object using an integer *counter variable* that's normally hidden. If you want to know the current value of the counter (e.g. to get the index of an element as well as its value), you can use the following: - - var a = [1, 2, 3] - for (x, i in a) print "${i}: ${x}" - -Morpho also provides a second form of `for` loop similar to that in C: - - for (var i=0; i<5; i+=1) { print i } - ^start ^test ^inc. ^body - -which is executed as follows: - start: the variable `i` is declared and initially set to zero. - test: before each iteration, the test is evaluated. If the test is `false`, the loop terminates. - body: the body of the loop is executed. - inc: the variable `i` is increased by 1. - -You can include any code that you like in each of the sections. - -## Break -[tagbreak]: # (break) - -`Break` is used inside loops to finish the loop early. For example - - for (i in 1..5) { - if (i>3) break // --. - print i // | (Once i>3) - } // | - ... // <-' - -would only print 1, 2 and 3. Once the condition `i>3` is true, the `break` statement causes execution to continue after the loop body. - -Both `for` and `while` loops support break. - -## Continue -[tagcontinue]: # (continue) - -`Continue` is used inside loops to skip over the rest of an iteration. For example - - for (i in 1..5) { // <-. - print "Hello" | - if (i>3) continue // --' - print i - } - -prints "Hello" five times but only prints 1, 2 and 3. Once the condition `i>3` is true, the `continue` statement causes execution to transfer to the start of the loop body. - -Traditional `for` loops also support `continue`: - - // v increment - for (var i=0; i<5; i+=1) { - if (i==2) continue - print i - } - -Since `continue` causes control to be transferred *to the increment section* in this kind of loop, here the program prints 0..4 but the number 2 is skipped. - -Use of `continue` with `while` loops is possible but isn't recommended as it can easily produce an infinite loop! - - var i=0 - while (i<5) { - if (i==2) continue - print i - i+=1 - } - -In this example, when the condition `i==2` is `true`, execution skips back to the start, but `i` *isn't* incremented. The loop gets stuck in the iteration `i==2`. - -## Try -[tagtry]: # (try) -[tagcatch]: # (catch) - -A `try` and `catch` statement allow you handle errors. For example - - try { - // Do something - } catch { - "Tag" : // Handle the error - } - -Code within the block after the `try` keyword is executed. If an error is generated then Morpho looks to see if the tag associated with the error matches any of the labels in the `catch` block. If it does, the code after the matching label is executed. If no error occurs, the catch block is skipped entirely. diff --git a/help/delaunay.md b/help/delaunay.md deleted file mode 100644 index d7de7be..0000000 --- a/help/delaunay.md +++ /dev/null @@ -1,48 +0,0 @@ -[comment]: # (Delaunay module help) -[version]: # (0.5) - -# Delaunay -[tagdelaunay]: # (delaunay) - -The `delaunay` module creates Delaunay triangulations from point clouds. It is dimensionally independent, so generates tetrahedra in 3D and higher order simplices beyond. - -To use the module, first import it: - - import delaunay - -To create a Delaunary triangulation from a list of points: - - var pts = [] - for (i in 0...100) pts.append(Matrix([random(), random()])) - var del=Delaunay(pts) - print del.triangulate() - -The module also provides `DelaunayMesh` to directly create meshes from Delaunay triangulations. - -[showsubtopics]: # (subtopics) - -## Triangulate -[tagtriangulate]: # (triangulate) - -The `triangulate` method performs the delaunay triangulation. To use it, first construct a `Delaunay` object with the point cloud of interest: - - var del=Delaunay(pts) - -Then call `triangulate`: - - var tri = del.triangulate() - -This returns a list of triangles `[ [i, j, k], ... ]`. - -## Circumsphere -[tagcircumsphere]: # (circumsphere) - -The `Circumsphere` class calculates the circumsphere of a set of points, i.e. a sphere such that all the points are on the surface of the sphere. It is used internally by the `delaunay` module. - -Create a `Circumsphere` from a list of points and a triangle specified by indices into that list: - - var sph = Circumsphere(pts, [i,j,k]) - -Test if an arbitrary point is inside the `Circumsphere` or not: - - print sph.pointinsphere(pt) diff --git a/help/dictionary.md b/help/dictionary.md deleted file mode 100644 index 989ffc5..0000000 --- a/help/dictionary.md +++ /dev/null @@ -1,51 +0,0 @@ -[comment]: # (Dictionary help) -[version]: # (0.5) - -# Dictionary -[tag]: # (Dictionary) - -Dictionaries are collection objects that associate a unique *key* with a particular *value*. Keys can be any kind of morpho value, including numbers, strings and objects. - -An example dictionary mapping states to capitals: - - var dict = { "Massachusetts" : "Boston", - "New York" : "Albany", - "Vermont" : "Montpelier" } - -Look up values by a given key with index notation: - - print dict["Vermont"] - -You can change the value associated with a key, or add new elements to the dictionary like this: - - dict["Maine"]="Augusta" - -Create an empty dictionary using the `Dictionary` constructor function: - - var d = Dictionary() - -Loop over keys in a dictionary: - - for (k in dict) print k - -The `keys` method returns a Morpho List of the keys. - - var keys = dict.keys() // will return ["Massachusetts", "New York", "Vermont"] - -The `contains` method returns a Bool value for whether the Dictionary -contains a given key. - - print dict.contains("Vermont") // true - print dict.contains("New Hampshire") // false - -The `remove` method removes a given key from the Dictionary. - - dict.remove("Vermont") - print dict // { New York : Albany, Massachusetts : Boston } - -The `clear` method removes all the (key, value) pairs fromt the -dictionary, resulting in an empty dictionary. - - dict.clear() - - print dict // { } diff --git a/help/errors.md b/help/errors.md deleted file mode 100644 index 4d404cc..0000000 --- a/help/errors.md +++ /dev/null @@ -1,256 +0,0 @@ -[comment]: # (Errors help file) -[version]: # (0.5) - -# Errors -[tagerrors]: # (errors) - -When an error occurs in running a morpho program, an error message is displayed together with an explanation of where in the program that the error happened. - -[showsubtopics]: # (subtopics) - -## Alloc -[tagalloc]: # (alloc) - -This error may occur when creating new objects or resizing them. It typically indicates that the computer is under memory pressure. - -## ArrayDim -[tagarraydim]: # arraydim - -This error occurs if you try to index an array with the wrong number of indices: - - var a[2,2] - print a[1] - -## ClssLcksMthd -[tagclsslcksmthd]: # (clsslcksmthd) - -This error occurs if you try to invoke a method on a class that doesn't exist: - - class Foo { } - print Foo.foo() - -## CnctFld -[tagcnctfld]: # (cnctfld) - -This error occurs when concatenation of strings or other objects fails, typically because of low memory. - -## DbgQuit -[tagdbgquit]: # (dbgquit) - -This notification is generated after selecting `Quit` within the debugger. Execution of the program is halted and control returns to the user. - -## DctSprtr -[tagdctsprtr]: # (dctsprtr) - -This error occurs when a Dictionary initializer is missing a ':' between a key/vale pair - - var a = { "A" "B" } // Missing colon - -Fix by putting in the missing separator - - var a = { "A" : "B" } - -## DctTrmntr -[tagdcttrmntr]: # (dcttrmntr) - -This error occurs when a Dictionary initializer is missing the '}' required at the end: - - var a = { "A" : "B" // Missing terminator - -Fix by inserting the missing curly brace at the end - - var a = { "A" : "B" } - -## GlblRtrn -[tagglblrtrn]: # (glblrtrn) - -This error occurs when morpho encounters a `return` keyword outside of a function or method definition. - -## InstFail -[taginstfail]: # (instfail) - -This error occurs when morpho tried to create a new object, but something went wrong. - -## Intrnl -[tagintrnl]: # (intrnl) - -This error indicates an internal problem with morpho. Please contact the developers for support. - -## InvldArgs -[taginvldargs]: # (invldargs) - -This error occurs if you call a function with the wrong number of arguments: - - fn f(x) { return x } - f(1,2) - -## InvldOp -[taginvldop]: # (invldop) - -This error occurs when an operator like `+` or `-` is given operands that it doesn't understand. For example, - - print "Hello" * "Goodbye" // Causes 'InvldOp' - -causes this error because the multiplication operator doesn't know how to multiply strings. - -If the operands are objects, this means that the objects don't provide a method for the requested operation, e.g. for - - print object1 / object2 - -`object1` would need to provide a `div()` method that can successfully handle `object2`. - -## InvldUncd -[taginvlduncd]: # (invlduncd) - -This error occurs when an invalid unicode escape sequence is found in a string. - - print "\u23" // Must have 4 hex characters - print "\U01F98B" // Must have 8 hex characters - -Fix by specifying the character in the correct form: - - print "\U0001F98B" - -## IndxBnds -[tagindxbnds]: # (indxbnds) - -This error can occur when selecting an entry from a collection object (such as a list) if the index supplied is bigger than the number of entries: - - var a = [1,2,3] - print a[10] - -## MssngComma -[tagmssngcomma]: # (mssngcomma) - -This error occurs when a comma is missing between expressions. For example, a comma is required between dictionary entries - - var a = { "A" : "B" "C" : "D" } - -Fix this by inserting the missing comma - - var a = { "A" : "B" , "C" : "D" } - -## MtrxIncmptbl -[tagmtrxincmptbl]: # (mtrxincmptbl) - -This error occurs when an arithmetic operation is performed on two 'incompatible' matrices. For example, two matrices must have the same dimensions, i.e. the same number of rows and columns, to be added or subtracted, - - var a = Matrix([[1,2],[3,4]]) - var b = Matrix([[1]]) - print a+b // generates a `MtrxIncmptbl` error. - -Or to be multiplied together, the number of columns of the left hand matrix must equal the number of rows of the right hand matrix. - - var a = Matrix([[1,2],[3,4]]) - var b = Matrix([1,2]) - print a*b // ok - print b*a // generates a `MtrxIncmptbl` error. - -## NoInit -[tagnoinit]: # (noinit) - -This error can occur if you try to create a new object from a class that doesn't have an `init` method: - - class Foo { } - var a = Foo(0.3) - -Here, the argument to `Foo` causes the `NoInit` error because no `init` method is available to process it. - -## NotAnObj -[tagnotanobj]: # (notanobj) - -This error occurs if you try to access a property of something that isn't an object: - - var a = 1 - a.size = 5 - -## NonNmIndx -[tagnonnmindx]: # (nonnmindx) - -This error occurs if you try to index an array with a non-numerical index: - - var a[2,2] - print a["foo","bar"] - -## NotAnInst -[tagnotaninst]: # (notaninst) - -This error occurs if you try to invoke a method on something that isn't an object: - - var a = 4 - print a.foo() - -## NotIndxbl -[tagnotindxbl]: # (notindxbl) - -This error occurs if you try to index something that isn't a collection: - - var a = 0.3 - print a[1] - -## ObjLcksPrp -[tagobjlcksprp]: # (objlcksprp) - -This error occurs if you try to access a property or method that hasn't been defined for an object: - - var a = Object() - print a.pifflepaffle - -or - - print a.foo() - -## RcrsnLmt -[tagrcrsnlmt]: # (rcrsnlmt) - -This error may occur when trying to parse very deeply nested structures. - -## StrEsc -[tagstresc]: # (stresc) - - - -## SymblUndf -[tagsymblundf]: # (symblundf) - -This error occurs if you refer to something that has not been previously declared, for example trying to use a variable of call a function that doesn't exist. It's possible that the symbol is spelt incorrectly, or that the capitalization doesn't match the definition (*morpho* symbols are case-sensitive). - -A common problem is to try to assign to a variable that hasn't yet been declared: - - a = 5 - -To fix this, prefix with `var`: - - var a = 5 - -## Uncallable -[taguncallable]: # (uncallable) - -This error occurs when you try to call something that isn't a method or a function. Here, we initialize a variable with a string and call it: - - var f = "Not a function" - f() // Causes 'Uncallable' - -## UnescpdCtrl -[tagunescpdctrl]: # (unescpdctrl) - -This error is generated when morpho detects a control character in a string literal. To fix, specify the control character using its character code - - print "\x09" // Tab character - -or use the relevant shorthand code - - print "\t" // Also tab - -## ValRng -[tagvalrng]: # (valrng) - -This error occurs when the morpho compiler encounters a value that can't be represented in morpho. For example, - - print 1.2e1000 - -lies outside of double precision floating point arithmetic. Similarly, - - print 12345678901234567 - -is too big to be a morpho integer. \ No newline at end of file diff --git a/help/field.md b/help/field.md deleted file mode 100644 index 0e9d4d6..0000000 --- a/help/field.md +++ /dev/null @@ -1,78 +0,0 @@ -[comment]: # (Field class help) -[version]: # (0.5) - -# Field -[tagfield]: # (Field) - -Fields are used to store information, including numbers or matrices, associated with the elements of a `Mesh` object. - -You can create a `Field` by applying a function to each of the vertices, - - var f = Field(mesh, fn (x, y, z) x+y+z) - -or by supplying a single constant value, - - var f = Field(mesh, Matrix([1,0,0])) - -Fields can then be added and subtracted using the `+` and `-` operators. - -To access elements of a `Field`, use index notation: - - print f[grade, element, index] - -where -* `grade` is the grade to select -* `element` is the element id -* `index` is the element index - -As a shorthand, it's possible to omit the grade and index; these are then both assumed to be `0`: - - print f[2] - -[showsubtopics]: # (subtopics) - -## Mesh -[tagmesh]: # (mesh) - -Returns the Mesh associated with a Field object: - - var f.mesh() - -## Grade -[taggrade]: # (grade) - -To create fields that include grades other than just vertices, use the `grade` option to `Field`. This can be just a grade index, - - var f = Field(mesh, 0, grade=2) - -which creates an empty field with `0` for each of the facets of the mesh `mesh`. - -You can store more than one item per element by supplying a list to the `grade` option indicating how many items you want to store on each grade. For example, - - var f = Field(mesh, 1.0, grade=[0,2,1]) - -stores two numbers on the line (grade 1) elements and one number on the facets (grade 2) elements. Each number in the field is initialized to the value `1.0`. - -## Shape -[tagshape]: # (shape) - -The `shape` method returns a list indicating the number of items stored on each element of a particular grade. This has the same format as the list you supply to the `grade` option of the `Field` constructor. For example, - - [1,0,2] - -would indicate one item stored on each vertex and two items stored on each facet. - -## Op -[tagop]: # (op) - -The `op` method applies a function to every item stored in a `Field`, returning the result as elements of a new `Field` object. For example, - - f.op(fn (x) x.norm()) - -calls the `norm` method on each element stored in `f`. - -Additional `Field` objects may be supplied as extra arguments to `op`. These must have the same shape (the same number of items stored on each grade). The function supplied to `op` will now be called with the corresponding element from each field as arguments. For example, - - f.op(fn (x,y) x.inner(y), g) - -calculates an elementwise inner product between the elements of Fields `f` and `g`. diff --git a/help/file.md b/help/file.md deleted file mode 100644 index fc8c8f8..0000000 --- a/help/file.md +++ /dev/null @@ -1,96 +0,0 @@ -[comment]: # (File class help) -[version]: # (0.5) - -# File -[tagfile]: # (File) - -The `File` class provides the capability to read from and write to files, or to obtain the contents of a file in convenient formats. - -To open a file, create a File object with the filename as the argument - - var f = File("myfile.txt") - -which opens `"myfile.txt"` for *reading*. To open a file for writing or appending, you need to provide a mode selector - - var g = File("myfile.txt", "write") - -or - - var g = File("myfile.txt", "append") - -Once the file is open, you can then read or write by calling appropriate methods: - - f.lines() // reads the contents of the file into an array of lines. - f.readline() // reads a single line - f.readchar() // reads a single character. - f.write(string) // writes the arguments to the file. - -After you're done with the file, close it with - - f.close() - -[show]: # (subtopics) - -## lines -[taglines]: # (lines) - -Returns the contents of a file as an array of strings; each element corresponds to a single line. - -Read in the contents of a file and print line by line: - - var f = File("input.txt") - var s = f.lines() - for (i in s) print i - f.close() - -## readline -[tagreadline]: # (readline) - -Reads a single line from a file; returns the result as a string. - -Read in the contents of a file and print each line: - - var f = File("input.txt") - while (!f.eof()) { - print f.readline() - } - f.close() - -## readchar -[tagreadchar]: # (readchar) - -Reads a single character from a file; returns the result as a string. - -## write -[tagwrite]: # (write) - -Writes to a file. - -Write the contents of a list to a file: - - var f = File("output.txt", "w") - for (k, i in list) f.write("${i}: ${k}") - f.close() - -## close -[tagclose]: # (close) - -Closes an open file. - -## eof -[tageof]: # (eof) - -Returns true if at the end of the file; false otherwise - -# Folder -[tagfolder]: # (Folder) - -The `Folder` class enables you to find whether a filepath refers to a folder, and find the contents of that folder. - -Find whether a path refers to a folder: - - print Folder.isfolder("path/folder") - -Get a list of a folder's contents: - - print Folder.contents("path/folder") diff --git a/help/functionals.md b/help/functionals.md deleted file mode 100644 index c883325..0000000 --- a/help/functionals.md +++ /dev/null @@ -1,251 +0,0 @@ -[comment]: # (Functionals help) -[version]: # (0.5) - -# Functionals -[tagfunctionals]: # (functionals) - -A number of `functionals` are available in Morpho. Each of these represents an integral over some `Mesh` and `Field` objects (on a particular `Selection`) and are used to define energies and constraints in an `OptimizationProblem` provided by the `optimize` module. - -Many functionals are built in. Additional functionals are available by importing the `functionals` module: - - import functionals - -Functionals provide a number of standard methods: - -* `total`(mesh) - returns the value of the integral with a provided mesh, selection and fields -* `integrand`(mesh) - returns the contribution to the integral from each element -* `gradient`(mesh) - returns the gradient of the functional with respect to vertex motions. -* `fieldgradient`(mesh, field) - returns the gradient of the functional with respect to components of the field - -Each of these may be called with a mesh, a field and a selection. - -[showsubtopics]: # (subtopics) - -## Length -[taglength]: # (length) - -A `Length` functional calculates the length of a line element in a mesh. - -Evaluate the length of a circular loop: - - import constants - import meshtools - var m = LineMesh(fn (t) [cos(t), sin(t), 0], 0...2*Pi:Pi/20, closed=true) - var le = Length() - print le.total(m) - -## AreaEnclosed -[tagareaenclosed]: # (areaenclosed) - -An `AreaEnclosed` functional calculates the area enclosed by a loop of line elements. - - var la = AreaEnclosed() - -## Area -[tagarea]: # (area) - -An `Area` functional calculates the area of the area elements in a mesh: - - var la = Area() - print la.total(mesh) - -## VolumeEnclosed -[tagvolumeenclosed]: # (volumeenclosed) - -A `VolumeEnclosed` functional is used to calculate the volume enclosed by a surface. Note that this estimate may become inaccurate for highly deformed surfaces. - - var lv = VolumeEnclosed() - -## Volume -[tagvolume]: # (volume) - -A `Volume` functional calculates the volume of volume elements. - - var lv = Volume() - -## ScalarPotential -[tagscalarpotential]: # (scalarpotential) - -The `ScalarPotential` functional is applied to point elements. - - var ls = ScalarPotential(potential) - -You must supply a function (which may be anonymous) that returns the potential. You may optionally provide a function that returns the gradient as well at initialization: - - var ls = ScalarPotential(potential, gradient) - -This functional is often used to constrain the mesh to the level set of a function. For example, to confine a set of points to a sphere: - - import optimize - fn sphere(x,y,z) { return x^2+y^2+z^2-1 } - fn grad(x,y,z) { return Matrix([2*x, 2*y, 2*z]) } - var lsph = ScalarPotential(sphere, grad) - problem.addlocalconstraint(lsph) - -See the thomson example for use of this technique. - -## LinearElasticity -[taglinearelasticity]: # (linearelasticity) - -The `LinearElasticity` functional measures the linear elastic energy away from a reference state. - -You must initialize with a reference mesh: - - var le = LinearElasticity(mref) - -Manually set the poisson's ratio and grade to operate on: - - le.poissonratio = 0.2 - le.grade = 2 - -## EquiElement -[tagequielement]: # (equielement) - -The `EquiElement` functional measures the discrepency between the size of elements adjacent to each vertex. It can be used to equalize elements for regularization purposes. - -## LineCurvatureSq -[taglinecurvaturesq]: # (linecurvaturesq) - -The `LineCurvatureSq` functional measures the integrated curvature squared of a sequence of line elements. - -## LineTorsionSq -[taglinetorsionsq]: # (linetorsionsq) - -The `LineTorsionSq` functional measures the integrated torsion squared of a sequence of line elements. - -## MeanCurvatureSq -[tagmeancurvsq]: # (meancurvaturesq) - -The `MeanCurvatureSq` functional computes the integrated mean curvature over a surface. - -## GaussCurvature -[taggausscurv]: # (gausscurvature) - -The `GaussCurvature` computes the integrated gaussian curvature over a surface. - -Note that for surfaces with a boundary, the integrand is correct only for the interior points. To compute the geodesic curvature of the boundary in that case, you can set the optional flag `geodesic` to `true` and compute the total on the boundary selection. -Here is an example for a 2D disk mesh. - - var mesh = Mesh("disk.mesh") - mesh.addgrade(1) - - var whole = Selection(mesh, fn(x,y,z) true) - var bnd = Selection(mesh, boundary=true) - var interior = whole.difference(bnd) - - var gauss = GaussCurvature() - print gauss.total(mesh, selection=interior) // expect: 0 - gauss.geodesic = true - print gauss.total(mesh, selection=bnd) // expect: 2*Pi - -## GradSq -[taggradsq]: # (gradsq) - -The `GradSq` functional measures the integral of the gradient squared of a field. The field can be a scalar, vector or matrix function. - -Initialize with the required field: - - var le=GradSq(phi) - -## Nematic -[tagnematic]: # (nematic) - -The `Nematic` functional measures the elastic energy of a nematic liquid crystal. - - var lf=Nematic(nn) - -There are a number of optional parameters that can be used to set the splay, twist and bend constants: - - var lf=Nematic(nn, ksplay=1, ktwist=0.5, kbend=1.5, pitch=0.1) - -These are stored as properties of the object and can be retrieved as follows: - - print lf.ksplay - -## NematicElectric -[tagnematic]: # (nematic) - -The `NematicElectric` functional measures the integral of a nematic and electric coupling term integral((n.E)^2) where the electric field E may be computed from a scalar potential or supplied as a vector. - -Initialize with a director field `nn` and a scalar potential `phi`: - var lne = NematicElectric(nn, phi) - -## NormSq -[tagnormsq]: # (normsq) - -The `NormSq` functional measures the elementwise L2 norm squared of a field. - -## LineIntegral -[taglineintegral]: # (lineintegral) - -The `LineIntegral` functional computes the line integral of a function. You supply an integrand function that takes a position matrix as an argument. - -To compute `integral(x^2+y^2)` over a line element: - - var la=LineIntegral(fn (x) x[0]^2+x[1]^2) - -The function `tangent()` returns a unit vector tangent to the current element: - - var la=LineIntegral(fn (x) x.inner(tangent())) - -You can also integrate functions that involve fields: - - var la=LineIntegral(fn (x, n) n.inner(tangent()), n) - -where `n` is a vector field. The local interpolated value of this field is passed to your integrand function. More than one field can be used; they are passed as arguments to the integrand function in the order you supply them to `LineIntegral`. - -## AreaIntegral -[tagareaintegral]: # (areaintegral) - -The `AreaIntegral` functional computes the area integral of a function. You supply an integrand function that takes a position matrix as an argument. - -To compute integral(x*y) over an area element: - - var la=AreaIntegral(fn (x) x[0]*x[1]) - -You can also integrate functions that involve fields: - - var la=AreaIntegral(fn (x, phi) phi^2, phi) - -More than one field can be used; they are passed as arguments to the integrand function in the order you supply them to `AreaIntegral`. - -## VolumeIntegral -[tagvolumeintegral]: # (volumeintegral) - -The `VolumeIntegral` functional computes the volume integral of a function. You supply an integrand function that takes a position matrix as an argument. - -To compute integral(x*y*z) over an volume element: - - var la=VolumeIntegral(fn (x) x[0]*x[1]*x[2]) - -You can also integrate functions that involve fields: - - var la=VolumeIntegral(fn (x, phi) phi^2, phi) - -More than one field can be used; they are passed as arguments to the integrand function in the order you supply them to `VolumeIntegral`. - -## Hydrogel -[taghydrogel]: # (hydrogel) - -The `Hydrogel` functional computes the Flory-Rehner energy over an element: - - (a*phi*log(phi) + b*(1-phi)+log(1-phi) + c*phi*(1-phi))*V + - d*(log(phiref/phi)/3 - (phiref/phi)^(2/3) + 1)*V0 - -The first three terms come from the Flory-Huggins mixing energy, whereas -the fourth term proportional to d comes from the Flory-Rehner elastic -energy. - -The value of phi is calculated from a reference mesh -that you provide on initializing the Functional: - - var lfh = Hydrogel(mref) - -Here, a, b, c, d and phiref are parameters you can supply (they are `nil` -by default), V is the current volume and V0 is the reference volume of a -given element. You also need to supply the initial value of phi, labeled -as phi0, which is assumed to be the same for all the elements. -Manually set the coefficients and grade to operate on: - - lfh.a = 1; lfh.b = 1; lfh.c = 1; lfh.d = 1; - lfh.grade = 2, lfh.phi0 = 0.5, lfh.phiref = 0.1 diff --git a/help/functions.md b/help/functions.md deleted file mode 100644 index 75469c3..0000000 --- a/help/functions.md +++ /dev/null @@ -1,93 +0,0 @@ -[comment]: # (Morpho functions help file) -[version]: # (0.5) - -[toplevel]: # - -# Functions -[tagfn]: # (fn) -[tagfun]: # (fun) -[tagfunction]: # (function) - -A function in morpho is defined with the `fn` keyword, followed by the function's name, a list of parameters enclosed in parentheses, and the body of the function in curly braces. This example computes the square of a number: - - fn sqr(x) { - return x*x - } - -Once a function has been defined you can evaluate it like any other morpho function. - - print sqr(2) - -[show]: # (subtopics) - -## Variadic -[tagvariadic]: # (variadic) - -As well as regular parameters, functions can also be defined with *variadic* parameters: - - fn func(x, ...v) { - for (a in v) print a - } - -This function can then be called with 1 or more arguments: - - func(1) - func(1, 2) - func(1, 2, 3) // All valid! - -The variadic parameter `v` captures all the extra arguments supplied. Functions cannot be defined with more than one variadic parameter. - -You can mix regular, variadic and optional parameters. Variadic parameters come before optional parameters: - - fn func(x, ...v, optional=true) { - // - } - -## Optional -[tagoptional]: # (optional) - -Functions can also be defined with *optional* parameters: - - fn func(a=1) { - print a - } - -Each optional parameter must be defined with a default value (here `1`). The function can then be called either with or without the optional parameter: - - func() // a == 1 due to default value - func(a=2) // a == 2 supplied by the user - -## Return -[tagreturn]: # (return) - -The `return` keyword is used to exit from a function, optionally passing a given value back to the caller. `return` can be used anywhere within a function. The below example calculates the `n` th Fibonacci number, - - fn fib(n) { - if (n<2) return n - return fib(n-1) + fib(n-2) - } - -by returning early if `n<2`, otherwise returning the result by recursively calling itself. - -# Closures -[tagclosures]: # (closures) -[tagclosure]: # (closure) - -Functions in morpho can form *closures*, i.e. they can enclose information from their local context. In this example, - - fn foo(a) { - fn g() { return a } - return g - } - -the function `foo` returns a function that captures the value of `a`. If we now try calling `foo` and then calling the returned functions, - - var p=foo(1), q=foo(2) - print p() // expect: 1 - print q() // expect: 2 - -we can see that `p` and `q` seem to contain different copies of `g` that encapsulate the value that `foo` was called with. - -Morpho hints that a returned function is actually a closure by displaying it with double brackets: - - print foo(1) // expect: <> diff --git a/help/graphics.md b/help/graphics.md deleted file mode 100644 index f5627d8..0000000 --- a/help/graphics.md +++ /dev/null @@ -1,196 +0,0 @@ -[comment]: # (Graphics module help) -[version]: # (0.5) - -# Graphics -[taggraphics]: # (graphics) - -The `graphics` module provides a number of classes to provide simple visualization capabilities. To use it, you first need to import the module: - - import graphics - -The `Graphics` class acts as an abstract container for graphical information; to actually launch the display see the `Show` class. You can create an empty scene like this, - - var g = Graphics() - -Additional elements can be added using the `display` method. - - g.display(element) - -Morpho provides the following fundamental Graphical element classes: - - TriangleComplex - -You can also use functions like `Arrow`, `Tube` and `Cylinder` to create these elements conveniently. - -To combine graphics objects, use the add operator: - - var g1 = Graphics(), g2 = Graphics() - // ... - Show(g1+g2) - -[show]: # (subtopics) - -## Show -[tagshow]: # (Show) - -`Show` is used to launch an interactive graphical display using the external `morphoview` application. `Show` takes a `Graphics` object as an argument: - - var g = Graphics() - Show(g) - -## TriangleComplex -[tagTriangleComplex]: # (TriangleComplex) - -A `TriangleComplex` is a graphical element that can be used as part of a graphical display. It consists of a list of vertices and a connectivity matrix that selects which vertices are used in each triangle. - -To create one, call the constructor with the following arguments: - - TriangleComplex(position, normals, colors, connectivity) - -* `position` is a `Matrix` containing vertex positions as *columns*. -* `normals` is a `Matrix` with a normal for each vertex. -* `colors` is the color of the object. -* `connectivity` is a `Sparse` matrix where each column represents a triangle and rows correspond to vertices. - -You can also provide optional arguments: - -* `transmit` sets the transparency of the object. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the object using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - - -Add to a `Graphics` object using the `display` method. - -## Arrow -[tagArrow]: # (Arrow) - -The `Arrow` function creates an arrow. It takes two arguments: - - arrow(start, end) - -* `start` and `end` are the two vertices. The arrow points `start` -> `end`. - -You can also provide optional arguments: - -* `aspectratio` controls the width of the arrow relative to its length -* `n` is an integer that controls the quality of the display. Higher `n` leads to a rounder arrow. -* `color` is the color of the arrow. This can be a list of RGB values or a `Color` object -* `transmit` sets the transparency of the arrow. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the arrow using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Display an arrow: - - var g = Graphics([]) - g.display(Arrow([-1/2,-1/2,-1/2], [1/2,1/2,1/2], aspectratio=0.05, n=10)) - Show(g) - -## Cylinder -[tagCylinder]: # (Cylinder) - -The `Cylinder` function creates a cylinder. It takes two required arguments: - - cylinder(start, end) - -* `start` and `end` are the two vertices. - -You can also provide optional arguments: - -* `aspectratio` controls the width of the cylinder relative to its length. -* `n` is an integer that controls the quality of the display. Higher `n` leads to a rounder cylinder. -* `color` is the color of the cylinder. This can be a list of RGB values or a `Color` object. -* `transmit` sets the transparency of the cylinder. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the cylinder using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Display an cylinder: - - var g = Graphics() - g.display(Cylinder([-1/2,-1/2,-1/2], [1/2,1/2,1/2], aspectratio=0.1, n=10)) - Show(g) - -## Tube -[tagTube]: # (Tube) - -The `Tube` function connects a sequence of points to form a tube. - - Tube(points, radius) - -* `points` is a list of points; this can be a list of lists or a `Matrix` with the positions as columns. -* `radius` is the radius of the tube. - -You can also provide optional arguments: - -* `n` is an integer that controls the quality of the display. Higher `n` leads to a rounder tube. -* `color` is the color of the tube. This can be a list of RGB values or a `Color` object. -* `closed` is a `bool` that indicates whether the tube should be closed to form a loop. -* `transmit` sets the transparency of the tube. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the tube using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Draw a square: - - var a = Tube([[-1/2,-1/2,0],[1/2,-1/2,0],[1/2,1/2,0],[-1/2,1/2,0]], 0.1, closed=true) - var g = Graphics() - g.display(a) - -## Sphere -[tagSphere]: # (Sphere) - -The `Sphere` function creates a sphere. - - Sphere(center, radius) - -* `center` is the position of the center of the sphere; this can be a list or column `Matrix`. -* `radius` is the radius of the sphere - -You can also provide optional arguments: - -* `color` is the color of the sphere. This can be a list of RGB values or a `Color` object. -* `transmit` sets the transparency of the sphere. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the sphere using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Draw some randomly sized spheres: - - var g = Graphics() - for (i in 0...10) { - g.display(Sphere([random()-1/2, random()-1/2, random()-1/2], 0.1*(1+random()), color=Gray(random()))) - } - Show(g) - -## Text -[tagText]: # (Text) - -A `Text` object is used to display text. - - Text(text, position) - -* `text` is the text to display as a string. -* `position` is the position at which to display the text. - -You can also provide optional arguments: - -* `color` is the color of the text. This should be a `Color` object. -* `dirn` is the direction along which the text is drawn. This should be a `List` or a `Matrix`. -* `size` is the font size to use -* `vertical` is the vertical direction for the text -* `font` is the `Font` object to use. - -Draw several pieces of text around the y axis: - - var g = Graphics() - for (phi in 0..Pi:Pi/8) { - g.display(Text("Hello World", [0,0,0], size=72, dirn=[0,1,0], vertical=[cos(phi),0,sin(phi)])) - } - Show(g) diff --git a/help/help.md b/help/help.md deleted file mode 100644 index 8b52966..0000000 --- a/help/help.md +++ /dev/null @@ -1,34 +0,0 @@ -[comment]: # (Morpho language help file) -[version]: # (0.5) - -# Help -[tag]: # (help) - -Morpho provides an online help system. To get help about a topic called `topicname`, type - - help topicname - -A list of available topics is provided below and includes language keywords like `class`, `fn` and `for`, built in classes like `Matrix` and `File` or information about functions like `exp` and `random`. - -Some topics have additional subtopics: to access these type - - help topic subtopic - -For example, to get help on a method for a particular class, you could type - - help Classname.methodname - -Note that `help` ignores all punctuation. - -You can also use `?` as a shorthand synonym for `help` - - ? topic - -A useful feature is that, if an error occurs, simply type `help` to get more information about the error. - -[showtopics]: # (topics) - -# Quit -[tagquit]: # (quit) - -The `quit` CLI command quits `morpho` run in interactive mode and returns to the shell. diff --git a/help/implicitmesh.md b/help/implicitmesh.md deleted file mode 100644 index b897440..0000000 --- a/help/implicitmesh.md +++ /dev/null @@ -1,27 +0,0 @@ -[comment]: # (Implicitmesh module help) -[version]: # (0.5) - -# ImplicitMesh -[tagimplicitmesh]: # (implicitmesh) - -The `implicitmesh` module allows you to build meshes from implicit functions. For example, the unit sphere could be specified using the function `x^2+y^2+z^2-1 == 0`. - -To use the module, first import it: - - import implicitmesh - -To create a sphere, first create an ImplicitMeshBuilder object with the implict function you'd like to use: - - var impl = ImplicitMeshBuilder(fn (x,y,z) x^2+y^2+z^2-1) - -You can use an existing function (or method) as well as an anonymous function as above. - -Then build the mesh, - - var mesh = impl.build(stepsize=0.25) - -The `build` method takes a number of optional arguments: - -* `start` - the starting point. If not provided, the value Matrix([1,1,1]) is used. -* `stepsize` - approximate lengthscale to use. -* `maxiterations` - maximum number of iterations to use. If this limit is exceeded, a partially built mesh will be returned. diff --git a/help/index.rst b/help/index.rst deleted file mode 100644 index 147b74c..0000000 --- a/help/index.rst +++ /dev/null @@ -1,69 +0,0 @@ -Morpho -====== - -.. toctree:: - :caption: Language - :maxdepth: 1 - - syntax - values - variables - controlflow - functions - classes - modules - help - builtin - -.. toctree:: - :caption: Data Types - :maxdepth: 1 - - array - complex - dictionary - list - matrix - range - sparse - string - -.. toctree:: - :caption: Computational Geometry - :maxdepth: 1 - - field - functionals - mesh - selection - -.. toctree:: - :caption: I/O - :maxdepth: 1 - - file - system - -.. toctree:: - :caption: Modules - :maxdepth: 1 - - color - constants - delaunay - graphics - implicitmesh - kdtree - meshgen - meshslice - meshtools - optimize - plot - povray - vtk - -.. toctree:: - :caption: Error messages - :maxdepth: 1 - - errors diff --git a/help/kdtree.md b/help/kdtree.md deleted file mode 100644 index b44fffc..0000000 --- a/help/kdtree.md +++ /dev/null @@ -1,81 +0,0 @@ -[comment]: # (KDTree module help) -[version]: # (0.5) - -# KDTree -[tagkdtree]: # (kdtree) - -The `kdtree` module implements a k-dimensional tree, a space partitioning data structure that can be used to accelerate computational geometry calculations. - -To use the module, first import it: - - import kdtree - -To create a tree from a list of points: - - var pts = [] - for (i in 0...100) pts.append(Matrix([random(), random(), random()])) - var tree=KDTree(pts) - -Add further points: - - tree.insert(Matrix([0,0,0])) - -Test whether a given point is present in the tree: - - tree.ismember(Matrix([1,0,0])) - -Find all points within a given bounding box: - - var pts = tree.search([[-1,1], [-1,1], [-1,1]]) - for (x in pts) print x.location - -Find the nearest point to a given point: - - var pt = tree.nearest(Matrix([0.1, 0.1, 0.5])) - print pt.location - -[showsubtopics]: # (subtopics) - -## Insert -[taginsert]: # (insert) - -Inserts a new point into a k-d tree. Returns a KDTreeNode object. - - var node = tree.insert(Matrix([0,0,0])) - -Note that, for performance reasons, if the set of points is known ahead of time, it is generally better to build the tree using the constructor function KDTree rather than one-by-one with insert. - -## Ismember -[tagismember]: # (ismember) - -Checks if a point is a member of a k-d tree. Returns `true` or `false`. - - print tree.ismember(Matrix([0,0,0])) - -## Nearest -[tagnearest]: # (nearest) - -Finds the point in a k-d tree nearest to a point of interest. Returns a KDTreeNode object. - - var pt = tree.nearest(Matrix([0.1, 0.1, 0.5])) - -To get the location of this nearest point, access the location property: - - print pt.location - -## Search -[tagsearch]: # (search) - -Finds all points in a k-d tree that lie within a cuboidal bounding box. Returns a list of KDTreeNode objects. - -Find and display all points that lie in a cuboid 0<=x<=1, 0<=y<=2, 1<=z<=2: - - var result = tree.search([[0,1], [0,2], [1,2]]) - for (x in result) print x.location - -## KDTreeNode -[tagkdtreenode]: # (kdtreenode) - -An object corresponding to a single node in a k-d tree. To get the location of the node, access the `location` property: - - print node.location diff --git a/help/list.md b/help/list.md deleted file mode 100644 index d996d27..0000000 --- a/help/list.md +++ /dev/null @@ -1,132 +0,0 @@ -[comment]: # (List class help) -[version]: # (0.5) - -# List -[taglist]: # (List) - -Lists are collection objects that contain a sequence of values each associated with an integer index. - -Create a list like this: - - var list = [1, 2, 3] - -Look up values using index notation: - - list[0] - -Indexing can also be done with slices: - list[0..2] - list[[0,1,3]] - -You can change list entries like this: - - list[0] = "Hello" - -Create an empty list: - - var list = [] - -Loop over elements of a list: - - for (i in list) print i - -[showsubtopics]: # (subtopics) - -## Append -[tagappend]: # (Append) - -Adds an element to the end of a list: - - var list = [] - list.append("Foo") - -## Insert -[taginsert]: # (Insert) - -Inserts an element into a list at a specified index: - - var list = [1,2,3] - list.insert(1, "Foo") - print list // prints [ 1, Foo, 2, 3 ] - -## Pop -[tagpop]: # (pop) - -Remove the last element from a list, returning the element removed: - - print list.pop() - -If an integer argument is supplied, returns and removes that element: - - var a = [1,2,3] - print a.pop(1) // prints '2' - print a // prints [ 1, 3 ] - -## Sort -[tagsort]: # (sort) - -Sorts the contents of a list into ascending order: - - list.sort() - -Note that this sorts the list "in place" (i.e. it modifies the order of the list on which it is invoked) and hence returns `nil`. - -You can provide your own function to use to compare values in the list - - list.sort(fn (a, b) a-b) - -This function should return a negative value if `ab` and `0` if `a` and `b` are equal. - -## Order -[tagorder]: # (order) - -Returns a list of indices that would, if used in order, would sort a list. For example - - var list = [2,3,1] - print list.order() // expect: [2,0,1] - -would produce `[2,0,1]` - -## Remove -[tagremove]: # (remove) - -Remove any occurrences of a value from a list: - - var list = [1,2,3] - list.remove(1) - -## ismember -[tagismember]: # (ismember) - -Tests if a value is a member of a list: - - var list = [1,2,3] - print list.ismember(1) // expect: true - -## Add -[tagadd]: # (add) - -Join two lists together: - - var l1 = [1,2,3], l2 = [4, 5, 6] - print l1+l2 // expect: [1,2,3,4,5,6] - -## Tuples -[tagtuples]: # (tuples) - -Generate all possible 2-tuples from a list: - - var t = [ 1, 2, 3].tuples(2) - -produces `[ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ] ... ]`. - -## Sets -[tagsets]: # (sets) - -Generate all possible sets of order 2 from a list. - - var t = [ 1, 2, 3 ].sets(2) - -produces `[ [ 1, 2 ], [ 1, 3 ], [ 2, 3 ] ]`. - -Note that sets include only distinct elements from the list (no element is repeated) and ordering is unimportant, hence only one of `[ 1, 2 ]` and `[ 2, 1 ]` is returned. diff --git a/help/make.bat b/help/make.bat deleted file mode 100644 index 922152e..0000000 --- a/help/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/help/matrix.md b/help/matrix.md deleted file mode 100644 index c830b45..0000000 --- a/help/matrix.md +++ /dev/null @@ -1,175 +0,0 @@ -[comment]: # (Matrix class help) -[version]: # (0.5) - -# Matrix -[tagmatrix]: # (Matrix) - -The Matrix class provides support for matrices. A matrix can be initialized with a given size, - - var a = Matrix(nrows,ncols) - -where all elements are initially set to zero. Alternatively, a matrix can be created from an array, - - var a = Matrix([[1,2], [3,4]]) - -or a Sparse matrix, - - var a = Sparse([[0,0,1],[1,1,1],[2,2,1]]) - var b = Matrix(a) - -You can create a column vector like this, - - var v = Matrix([1,2]) - -Finally, you can create a Matrix by assembling other matrices like this, - - var a = Matrix([[0,1],[1,0]]) - var b = Matrix([[a,0],[0,a]]) // produces a 4x4 matrix - -Once a matrix is created, you can use all the regular arithmetic operators with matrix operands, e.g. - - a+b - a*b - -The division operator is used to solve a linear system, e.g. - - var a = Matrix([[1,2],[3,4]]) - var b = Matrix([1,2]) - - print b/a - -yields the solution to the system a*x = b. - -[showsubtopics]: # (subtopics) - -## Assign -[tagassign]: # (Assign) - -Copies the contents of matrix B into matrix A: - - A.assign(B) - -The two matrices must have the same dimensions. - -## Dimensions -[tagdimensions]: # (Dimensions) - -Returns the dimensions of a matrix: - - var A = Matrix([1,2,3]) // Create a column matrix - print A.dimensions() // Expect: [ 3, 1 ] - -## Eigenvalues -[tageigenvalues]: # (Eigenvalues) - -Returns a list of eigenvalues of a Matrix: - - var A = Matrix([[0,1],[1,0]]) - print A.eigenvalues() // Expect: [1,-1] - -## Eigensystem -[tageigensystem]: # (Eigensystem) - -Returns the eigenvalues and eigenvectors of a Matrix: - - var A = Matrix([[0,1],[1,0]]) - print A.eigensystem() - -Eigensystem returns a two element list: The first element is a List of eigenvalues. The second element is a Matrix containing the corresponding eigenvectors as its columns: - - print A.eigensystem()[0] - // [ 1, -1 ] - print A.eigensystem()[1] - // [ 0.707107 -0.707107 ] - // [ 0.707107 0.707107 ] - -## Inner -[taginner]: # (Inner) - -Computes the Frobenius inner product between two matrices: - - var prod = A.inner(B) - -## Outer -[tagouter]: # (Outer) - -Computes the outer produce between two vectors: - - var prod = A.outer(B) - -Note that `outer` always treats both vectors as column vectors. - -## Inverse -[taginverse]: # (Inverse) - -Returns the inverse of a matrix if it is invertible. Raises a -`MtrxSnglr` error if the matrix is singular. E.g. - - var m = Matrix([[1,2],[3,4]]) - var mi = m.inverse() - -yields the inverse of the matrix `m`, such that mi*m is the identity -matrix. - -## Norm -[tagnorm]: # (Norm) - -Returns a matrix norm. By default the L2 norm is returned: - - var a = Matrix([1,2,3,4]) - print a.norm() // Expect: sqrt(30) = 5.47723... - -You can select a different norm by supplying an argument: - - import constants - print a.norm(1) // Expect: 10 (L1 norm is sum of absolute values) - print a.norm(3) // Expect: 4.64159 (An unusual choice of norm) - print a.norm(Inf) // Expect: 4 (Inf-norm corresponds to maximum absolute value) - -## Reshape -[tagreshape]: # (Reshape) - -Changes the dimensions of a matrix such that the total number of elements remains constant: - - var A = Matrix([[1,3],[2,4]]) - A.reshape(1,4) // 1 row, 4 columns - print A // Expect: [ 1, 2, 3, 4 ] - -Note that elements are stored in column major-order. - -## Sum -[tagsum]: # (Sum) - -Returns the sum of all entries in a matrix: - - var sum = A.sum() - -## Transpose -[tagtranspose]: # (Transpose) - -Returns the transpose of a matrix: - - var At = A.transpose() - -## Trace -[tagtrace]: # (Trace) - -Computes the trace (the sum of the diagonal elements) of a square matrix: - - var tr = A.trace() - -## Roll -[tagroll]: # (Roll) - -Rotates values in a Matrix about a given axis by a given shift: - - var r = A.roll(shift, axis) - -Elements that roll beyond the last position are re-introduced at the first. - -## IdentityMatrix -[tagidentitymatrix]: # (IdentityMatrix) - -Constructs an identity matrix of a specified size: - - var a = IdentityMatrix(size) diff --git a/help/mesh.md b/help/mesh.md deleted file mode 100644 index 49a2c7c..0000000 --- a/help/mesh.md +++ /dev/null @@ -1,66 +0,0 @@ -[comment]: # (Mesh class help) -[version]: # (0.5) - -# Mesh -[tagmesh]: # (Mesh) - -The `Mesh` class provides support for meshes. Meshes may consist of different kinds of element, including vertices, line elements, facets or area elements, tetrahedra or volume elements. - -To create a mesh, you can import it from a file: - - var m = Mesh("sphere.mesh") - -or use one of the functions available in `meshtools` or `implicitmesh` packages. - -Each type of element is referred to as belonging to a different `grade`. Point-like elements (vertices) are *grade 0*; line-like elements (edges) are *grade 1*; area-like elements (facets; triangles) are *grade 2* etc. - -The `plot` package includes functions to visualize meshes. - -[showsubtopics]: # (showsubtopics) - -## Save -[tagsave]: # (Save) - -Saves a mesh as a .mesh file. - - m.save("new.mesh") - -## Vertexposition -[tagvertexposition]: # (vertexposition) - -Retrieves the position of a vertex given an id: - - print m.vertexposition(id) - -## Setvertexposition -[tagsetvertexposition]: # (setvertexposition) - -Sets the position of a vertex given an id and a position vector: - - print m.setvertexposition(1, Matrix([0,0,0])) - -## Addgrade -[tagaddgrade]: # (addgrade) - -Adds a new grade to a mesh. This is commonly used when, for example, a mesh file includes facets but not edges. To add the missing edges: - - m.addgrade(1) - -## Addsymmetry -[tagaddsymmetry]: # (addsymmetry) - -Adds a symmetry to a mesh. Experimental in version 0.5. - -## Maxgrade -[tagmaxgrade]: # (maxgrade) - -Returns the highest grade element present: - - print m.maxgrade() - -## Count -[tagcount]: # (count) - -Counts the number of elements. If no argument is provided, returns the number of vertices. Otherwise, returns the number of elements present of a given grade: - - print m.count(2) // Returns the number of area-like elements. diff --git a/help/meshgen.md b/help/meshgen.md deleted file mode 100644 index c748c93..0000000 --- a/help/meshgen.md +++ /dev/null @@ -1,113 +0,0 @@ -[comment]: # (Meshgen module help) -[version]: # (0.5) - -# Meshgen -[tagmeshgen]: # (meshgen) - -The `meshgen` module is used to create `Mesh` objects corresponding to a specified domain. It provides the `MeshGen` class to perform the meshing, which are created with the following arguments: - - MeshGen(domain, boundingbox) - -Domains are specified by a scalar function that is positive in the region to be meshed and locally smooth. For example, to mesh the unit disk: - - var dom = fn (x) -(x[0]^2+x[1]^2-1) - -A `MeshGen` object is then created and then used to build the `Mesh` like this: - - var mg = MeshGen(dom, [-1..1:0.2, -1..1:0.2]) - var m = mg.build() - -A bounding box for the mesh must be specified as a `List` of `Range` objects, one for each dimension. The increment on each `Range` gives an approximate scale for the size of elements generated. - -To facilitate convenient creation of domains, a `Domain` class is provided that provides set operations `union`, `intersection` and `difference`. - -`MeshGen` accepts a number of optional arguments: - -* `weight` A scalar weight function that controls mesh density. -* `quiet` Set to `true` to suppress `MeshGen` output. -* `method` a list of options that controls the method used. - -Some method choices that are available include: - -* `"FixedStepSize"` Use a fixed step size in optimization. -* `"StartGrid"` Start from a regular grid of points (the default). -* `"StartRandom"` Start from a randomly generated collection of points. - -There are also a number of properties of a `MeshGen` object that can be set prior to calling `build` to control the operation of the mesh generation: - -* `stepsize`, `steplimit` Stepsize used internally by the `Optimizer` -* `fscale` an internal "pressure" -* `ttol` how far the vertices are allowed to move before retriangulation -* `etol` energy tolerance for optimization problem -* `maxiterations` Maximum number of iterations of minimization + - retriangulation (default is 100) - -`MeshGen` picks default values that cover a reasonable range of uses. - -[showsubtopics]: # (subtopics) - -## Domain -[tagdomain]: # (domain) - -The `Domain` class is used to conveniently build a domain by composing simpler elements. - -Create a `Domain` from a scalar function that is positive in the region of interest: - - var dom = Domain(fn (x) -(x[0]^2+x[1]^2-1)) - -You can pass it to `MeshGen` to specify the region to mesh: - - var mg = MeshGen(dom, [-1..1:0.2, -1..1:0.2]) - -You can combine `Domain` objects using set operations `union`, `intersection` and `difference`: - - var a = CircularDomain(Matrix([-0.5,0]), 1) - var b = CircularDomain(Matrix([0.5,0]), 1) - var c = CircularDomain(Matrix([0,0]), 0.3) - var dom = a.union(b).difference(c) - -## CircularDomain -[tagcirculardomain]: # (circulardomain) - -Conveniently constructs a `Domain` object correspondiong to a disk. Requires the position of the center and a radius as arguments. - -Create a domain corresponding to the unit disk: - - var c = CircularDomain([0,0], 1) - -## RectangularDomain -[tagrectangulardomain]: # (rectangulardomain) - -Conveniently constructs a `Domain` object corresponding to a rectangle. Requires a list of ranges as arguments. Works in arbitrary dimensions - -Create a square `Domain`: - - var c = RectangularDomain([-1..1, -1..1]) - -## HalfSpaceDomain -[halfspacedomain]: # (halfspacedomain) - -Conveniently constructs a `Domain` object correspondiong to a half space defined by a plane at `x0` and a normal `n`: - - var hs = HalfSpaceDomain(x0, n) - -Note `n` is an "outward" normal, so points into the *excluded* region. - -Half space corresponding to the allowed region `x<0`: - - var hs = HalfSpaceDomain(Matrix([0,0,0]), Matrix([1,0,0])) - -Note that `HalfSpaceDomain`s cannot be meshed directly as they correspond to an infinite region. They are useful, however, for combining with other domains. - -Create half a disk by cutting a `HalfSpaceDomain` from a `CircularDomain`: - - var c = CircularDomain([0,0], 1) - var hs = HalfSpaceDomain(Matrix([0,0]), Matrix([-1,0])) - var dom = c.difference(hs) - var mg = MeshGen(dom, [-1..1:0.2, -1..1:0.2], quiet=false) - var m = mg.build() - -## MshGnDim -[mshgndim]: # (mshgndim) - -The `MeshGen` module currently supports 2 and 3 dimensional meshes. Higher dimensional meshing will be available in a future release; please contact the developer if you are interested in this functionality. \ No newline at end of file diff --git a/help/meshslice.md b/help/meshslice.md deleted file mode 100644 index f698141..0000000 --- a/help/meshslice.md +++ /dev/null @@ -1,43 +0,0 @@ -[comment]: # (Meshslice module help) -[version]: # (0.5) - -# Meshslice -[tagmeshslice]: # (meshslice) - -The `meshslice` module is used to slice a `Mesh` object along a given plane, yielding a new `Mesh` object of lower dimensionality. You can also use `meshslice` to project `Field` objects onto the new mesh. - -To use the module, begin by importing it: - - import meshslice - -Then construct a `MeshSlicer` object, passing the mesh you want to slice in the constructor: - - var slice = MeshSlicer(mesh) - -You then perform a slice by calling the `slice` method, passing the plane you want to slice through. This method returns a new `Mesh` object comprising the slice. A plane is defined by a point that lies on the plane `pt` and a direction normal to the plan `dirn`: - - var slc = slice.slice(pt, dirn) - -Having performed a slice, you can then project any associated `Field` objects onto the sliced mesh by calling the `slicefield` method: - - var phi = Field(mesh, fn (x,y,z) x+y+z) - var sphi = slice.slicefield(phi) - -The new field returned by `slicefield` lives on the sliced mesh. You can slice any number of fields. - -You can perform multiple slices with the same `MeshSlicer` simply by calling `slice` again with a different plane. - -## SlcEmpty -[tagslcempty]: # (slcempty) - -This error occurs if you try to use `slicefield` on a `MeshSlicer` without having performed a slice. For example: - - var slice = MeshSlicer(mesh) - slice.slicefield(phi) // Throws SlcEmpty - slice.slice([0,0,0],[1,0,0]) - -To fix, call `slice` before `slicefield`: - - var slice = MeshSlicer(mesh) - slice.slice([0,0,0],[1,0,0]) - slice.slicefield(phi) // Now slices correctly diff --git a/help/meshtools.md b/help/meshtools.md deleted file mode 100644 index 1f55795..0000000 --- a/help/meshtools.md +++ /dev/null @@ -1,229 +0,0 @@ -[comment]: # (Morpho meshtools help file) -[version]: # (0.5) - -# Meshtools -[tagmeshtools]: # (meshtools) - -The Meshtools package contains a number of functions and classes to assist with creating and manipulating meshes. - -[showsubtopics]: # (subtopics) - -## AreaMesh -[tagareamesh]: # (areamesh) - -This function creates a mesh composed of triangles from a parametric function. To use it: - - var m = AreaMesh(function, range1, range2, closed=boolean) - -where - -* `function` is a parametric function that has one parameter. It should return a list of coordinates or a column matrix corresponding to this parameter. -* `range1` is the Range to use for the first parameter of the parametric function. -* `range2` is the Range to use for the second parameter of the parametric function. -* `closed` is an optional parameter indicating whether to create a closed loop or not. You can supply a list where each element indicates whether the relevant parameter is closed or not. - -To use `AreaMesh`, import the `meshtools` module: - - import meshtools - -Create a square: - - var m = AreaMesh(fn (u,v) [u, v, 0], 0..1:0.1, 0..1:0.1) - -Create a tube: - - var m = AreaMesh(fn (u, v) [v, cos(u), sin(u)], -Pi...Pi:Pi/4, - -1..1:0.1, closed=[true, false]) - -Create a torus: - - var c=0.5, a=0.2 - var m = AreaMesh(fn (u, v) [(c + a*cos(v))*cos(u), - (c + a*cos(v))*sin(u), - a*sin(v)], 0...2*Pi:Pi/16, 0...2*Pi:Pi/8, closed=true) - -## LineMesh -[taglinemesh]: # (linemesh) - -This function creates a mesh composed of line elements from a parametric function. To use it: - - var m = LineMesh(function, range, closed=boolean) - -where - -* `function` is a parametric function that has one parameter. It should return a list of coordinates or a column matrix corresponding to this parameter. -* `range` is the Range to use for the parametric function. -* `closed` is an optional parameter indicating whether to create a closed loop or not. - -To use `LineMesh`, import the `meshtools` module: - - import meshtools - -Create a circle: - - import constants - var m = LineMesh(fn (t) [sin(t), cos(t), 0], 0...2*Pi:2*Pi/50, closed=true) - -## PolyhedronMesh -[tagpolyhedronmesh]: # (polyhedron) - -This function creates a mesh corresponding to a polyhedron. - - var m = PolyhedronMesh(vertices, faces) - -where `vertices` is a list of vertices and `faces` is a list of faces specified as a list of vertex indices. - -To use `PolyhedronMesh`, import the `meshtools` module: - - import meshtools - -Create a cube: - - var m = PolyhedronMesh([ [-0.5, -0.5, -0.5], [ 0.5, -0.5, -0.5], - [-0.5, 0.5, -0.5], [ 0.5, 0.5, -0.5], - [-0.5, -0.5, 0.5], [ 0.5, -0.5, 0.5], - [-0.5, 0.5, 0.5], [ 0.5, 0.5, 0.5]], - [ [0,1,3,2], [4,5,7,6], [0,1,5,4], - [3,2,6,7], [0,2,6,4], [1,3,7,5] ]) - -*Note* that the vertices in each face list must be specified strictly in cyclic order. - -## DelaunayMesh -[tagdelaunaymesh]: # (delaunaymesh) - -The `DelaunayMesh` constructor function creates a `Mesh` object directly from a point cloud using the Delaunay triangulator. - - var pts = [] - for (i in 0...100) pts.append(Matrix([random(), random()])) - var m=DelaunayMesh(pts) - Show(plotmesh(m)) - -You can control the output dimension of the mesh (e.g. to create a 2D mesh embedded in 3D space) using the optional `outputdim` property. - - var m = DelaunayMesh(pts, outputdim=3) - -## Equiangulate -[tagequiangulate]: # (equiangulate) - -Attempts to equiangulate a mesh, exchanging elements to improve their regularity. - - equiangulate(mesh) - -*Note* this function modifies the mesh in place; it does not create a new mesh. - -## ChangeMeshDimension -[tagchangemeshdimension]: # (changemeshdimension) - -Changes the dimension in which a mesh is embedded. For example, you may have created a mesh in 2D that you now wish to use in 3D. - -To use: - - var new = ChangeMeshDimension(mesh, dim) - -where `mesh` is the mesh you wish to change, and `dim` is the new embedding dimension. - -## MeshBuilder -[tagmeshbuiler]: # (meshbuilder) - -The `MeshBuilder` class simplifies user creation of meshes. To use this class, begin by creating a `MeshBuilder` object: - - var build = MeshBuilder() - -You can then add vertices, edges, etc. one by one using `addvertex`, `addedge`, `addface` and `addelement`. Each of these returns an element id: - - var id1=build.addvertex(Matrix([0,0,0])) - var id2=build.addvertex(Matrix([1,1,1])) - build.addedge([id1, id2]) - -Once the mesh is ready, call the `build` method to construct the `Mesh`: - - var m = build.build() - -You can specify the dimension of the `Mesh` explicitly when initializing the `MeshBuilder`: - - var mb = MeshBuilder(dimension=2) - -or implicitly when adding the first vertex: - - var mb = MeshBuilder() - mb.addvertex([0,1]) // A 2D mesh - -## MshBldDimIncnstnt -[tagmshblddimincnstnt]: # (mshblddimincnstnt) - -This error is produced if you try to add a vertex that is inconsistent with the mesh dimension, e.g. - - var mb = MeshBuilder(dimension=2) - mb.addvertex([1,0,0]) // Throws an error! - -To fix this ensure all vertices have the correct dimension. - -## MshBldDimUnknwn -[tagmshblddimunknwn]: # (mshblddimunknwn) - -This error is produced if you try to add an element to a `MeshBuilder` object but haven't yet specified the dimension (at initialization) or by adding a vertex. - - var mb = MeshBuilder() - mb.addedge([0,1]) // No vertices have been added - -To fix this add the vertices first. - -## MeshRefiner -[tagmeshrefiner]: # (meshrefiner) - -The `MeshRefiner` class is used to refine meshes, and to correct associated data structures that depend on the mesh. - -To prepare for refining, first create a `MeshRefiner` object either with a `Mesh`, - - var mr = MeshRefiner(mesh) - -or with a list of objects that can include a `Mesh` as well as `Field`s and `Selection`s. - - var mr = MeshRefiner([mesh, field, selection ... ]) - -To perform the refinement, call the `refine` method. You can refine all elements, - - var dict = mr.refine() - -or refine selected elements using a `Selection`, - - var dict = mr.refine(selection=select) - -The `refine` method returns a `Dictionary` that maps old objects to new, refined objects. Use this to update your data structures. - - var newmesh = dict[oldmesh] - -## MeshPruner -[tagmeshpruner]: # (meshpruner) - -The `MeshPruner` class is used to prune excessive detail from meshes (a process that's sometimes referred to as coarsening), and to correct associated data structures that depend on the mesh. - -First create a `MeshPruner` object either with a `Mesh`, - - var mp = MeshPruner(mesh) - -or with a list of objects that can include a `Mesh` as well as `Field`s and `Selection`s. - - var mp = MeshPruner([mesh, field, selection ... ]) - -To perform the coarsening, call the `prune` method with a `Selection`, - - var dict = mp.prune(select) - -The `prune` method returns a `Dictionary` that maps old objects to new, refined objects. Use this to update your data structures. - - var newmesh = dict[oldmesh] - -## MeshMerge -[tagmeshmerge]: # (meshmerge) -[tagmerge]: # (meshmerge) - -The `MeshMerge` class is used to combine meshes into a single mesh, removing any duplicate elements. - -To use, create a `MeshMerge` object with a list of meshes to merge, - - var mrg = MeshMerge([m1, m2, m3, ... ]) - -and then call the `merge` method to return a combined mesh: - - var newmesh = mrg.merge() diff --git a/help/modules.md b/help/modules.md deleted file mode 100644 index 85aacc0..0000000 --- a/help/modules.md +++ /dev/null @@ -1,39 +0,0 @@ -[comment]: # (Morpho modules help file) -[version]: # (0.5) - -[toplevel]: # - -# Modules -[tagmodules]: # (modules) - -Morpho is extensible and provides a convenient module system that works like standard libraries in other languages. Modules may define useful variables, functions and classes, and can be made available using the `import` keyword. For example, - - import color - -loads the `color` module that provides functionality related to color. - -You can create your own modules; they're just regular morpho files that are stored in a standard place. On UNIX platforms, this is `/usr/local/share/morpho/modules`. - -## Import -[tagimport]: # (import) -[tagas]: # (as) - -Import provides access to the module system and including code from multiple source files. - -To import code from another file, use import with the filename: - - import "file.morpho" - -which immediately includes all the contents of `"file.morpho"`. Any classes, functions or variables defined in that file can now be used, which allows you to divide your program into multiple source files. - -Morpho provides a number of built in modules--and you can write your own--which can be loaded like this: - - import color - -which imports the `color` module. - -You can selectively import symbols from a modules by using the `for` keyword: - - import color for HueMap, Red - -which imports only the `HueMap` class and the `Red` variable. diff --git a/help/optimize.md b/help/optimize.md deleted file mode 100644 index a084fc5..0000000 --- a/help/optimize.md +++ /dev/null @@ -1,83 +0,0 @@ -[comment]: # (Morpho optimize help file) -[version]: # (0.5) - -# Optimize -[tagoptimize]: # (optimize) - -The `optimize` package contains a number of functions and classes to perform shape optimization. - -[showsubtopics]: # (subtopics) - -## OptimizationProblem -[tagoptimizationproblem]: # (optimizationproblem) - -An `OptimizationProblem` object defines an optimization problem, which may include functionals to optimize as well as global and local constraints. - -Create an `OptimizationProblem` with a mesh: - - var problem = OptimizationProblem(mesh) - -Add an energy: - - var la = Area() - problem.addenergy(la) - -Add an energy that operates on a selected region, and with an optional prefactor: - - problem.addenergy(la, selection=sel, prefactor=2) - -Add a constraint: - - problem.addconstraint(la) - -Add a local constraint (here a onesided level set constraint): - - var ls = ScalarPotential(fn (x,y,z) z, fn (x,y,z) Matrix([0,0,1])) - problem.addlocalconstraint(ls, onesided=true) - -## Optimizer -[tagoptimizer]: # (optimizer) - -`Optimizer` objects are used to optimize `Mesh`es and `Field`s. You should use the appropriate subclass: `ShapeOptimizer` or `FieldOptimizer` respectively. - -[showsubtopics]: # (subtopics) - -## ShapeOptimizer -[tagshapeoptimizer]: # (shapeoptimizer) - -A `ShapeOptimizer` object performs shape optimization: it moves the vertex positions to reduce an overall energy. - -Create a `ShapeOptimizer` object with an `OptimizationProblem` and a `Mesh`: - - var sopt = ShapeOptimizer(problem, m) - -Take a step down the gradient with fixed stepsize: - - sopt.relax(5) // Takes five steps - -Linesearch down the gradient: - - sopt.linesearch(5) // Performs five linesearches - -Perform conjugate gradient (usually gives faster convergence): - - sopt.conjugategradient(5) // Performs five conjugate gradient steps. - -Control a number of properties of the optimizer: - - sopt.stepsize=0.1 // The stepsize to take - sopt.steplimit=0.5 // Maximum stepsize for optimizing methods - sopt.etol = 1e-8 // Energy convergence tolerance - sopt.ctol = 1e-9 // Tolerance to which constraints are satisfied - sopt.maxconstraintsteps = 20 // Maximum number of constraint steps to use - -## FieldOptimizer -[tagfieldoptimizer]: # (fieldoptimizer) - -A `FieldOptimizer` object performs field optimization: it changes elements of a `Field` to reduce an overall energy. - -Create a `FieldOptimizer` object with an `OptimizationProblem` and a `Field`: - - var sopt = FieldOptimizer(problem, fld) - -Field optimizers provide the same options and methods as Shape optimizers: see the `ShapeOptimizer` documentation for details. diff --git a/help/plot.md b/help/plot.md deleted file mode 100644 index f3fd386..0000000 --- a/help/plot.md +++ /dev/null @@ -1,105 +0,0 @@ -[comment]: # (Plot module help) -[version]: # (0.5.4) - -# Plot -[tagplot]: # (plot) - -The `plot` module provides visualization capabilities for Meshes, Selections and Fields. These functions produce Graphics objects that can be displayed with `Show`. - -To use the module, first import it: - - import plot - -[showsubtopics]: # (subtopics) - -## Plotmesh -[tagplotmesh]: # (plotmesh) - -Visualizes a `Mesh` object: - - var g = plotmesh(mesh) - -Plotmesh accepts a number of optional arguments to control what is displayed: - -* `selection` - Only elements in a provided `Selection` are drawn. -* `grade` - Only draw the specified grade. This can also be a list of multiple grades to draw. -* `color` - Draw the mesh in a provided `Color`. -* `filter` and `transmit` - Used by the `povray` module to indicate transparency. - -## Plotmeshlabels -[tagplotmeshlabels]: # (plotmeshlabels) - -Draws the ids for elements in a `Mesh`: - - var g = plotmeshlabels(mesh) - -Plotmeshlabels accepts a number of optional arguments to control the output: - -* `grade` - Only draw the specified grade. This can also be a list of multiple grades to draw. -* `selection` - Only labels in a provided `Selection` are drawn. -* `offset` - Local offset vector for labels. Can be a `List`, a `Matrix` or a function. -* `dirn` - Text direction for labels. Can be a `List`, a `Matrix` or a function. -* `vertical` - Text vertical direction. Can be a `List`, a `Matrix` or a function. -* `color` - Label color. Can be a `Color` object or a `Dictionary` of colors for each grade. -* `fontsize` - Font size to use. - -## Plotselection -[tagplotselection]: # (plotselection) - -Visualizes a `Selection` object: - - var g = plotselection(mesh, sel) - -Plotselection accepts a number of optional arguments to control what is displayed: - -* `grade` - Only draw the specified grade. This can also be a list of multiple grades to draw. -* `filter` and `transmit` - Used by the `povray` module to indicate transparency. - -## Plotfield -[tagplotfield]: # (plotfield) - -Visualizes a scalar `Field` object: - - var g = plotfield(field) - -Plotfield accepts a number of optional arguments to control what is displayed: - -* `grade` - Draw the specified grade. -* `colormap` - A `Colormap` object to use. The field is automatically scaled. -* `scalebar` - A `Scalebar` object to use. -* `style` - Plot style. See below. -* `filter` and `transmit` - Used by the `povray` module to indicate transparency. -* `cmin` and `cmax` - Can be used to define the data range covered. - Values beyond these limits will be colored by the lower/upper bound of - the colormap accordingly. - -Supported plot styles: - -* `default` - Color `Mesh` elements by the corresponding value of the `Field`. -* `interpolate` - Interpolate `Field` quantities onto higher elements. - -## ScaleBar -[tagscalebar]: # (scalebar) - -Represents a scalebar for a plot: - - Show(plotfield(field, scalebar=ScaleBar(posn=[1.2,0,0]))) - -`ScaleBar`s can be created with many adjustable parameters: - -* `nticks` - Maximum number of ticks to show. -* `posn` - Position to draw the `ScaleBar`. -* `length` - Length of `ScaleBar` to draw. -* `dirn` - Direction to draw the `ScaleBar` in. -* `tickdirn` - Direction to draw the ticks in. -* `colormap` - `ColorMap` to use. -* `textdirn` - Direction to draw labels in. -* `textvertical` - Label vertical direction. -* `fontsize` - Fontsize for labels -* `textcolor` - Color for labels - -You can draw the `ScaleBar` directly by calling the `draw` method: - - sb.draw(min, max) - -where `min` and `max` are the minimum and maximum values to display on the scalebar. \ No newline at end of file diff --git a/help/povray.md b/help/povray.md deleted file mode 100644 index 90176dd..0000000 --- a/help/povray.md +++ /dev/null @@ -1,80 +0,0 @@ -[comment]: # (Povray module help) -[version]: # (0.5) - -# POVRay -[tagpovray]: # (povray) - -The `povray` module provides integration with POVRay, a popular open source ray-tracing package for high quality graphical rendering. To use the module, first import it: - - import povray - -To raytrace a graphic, begin by creating a `POVRaytracer` object: - - var pov = POVRaytracer(graphic) - -Create a .pov file that can be run with POVRay: - - pov.write("out.pov") - -Create, render and display a scene using POVRay: - - pov.render("out.pov") - -This also creates the .png file for the scene. - -The `POVRaytracer` constructor supports an optional `camera` argument: - -* `camera` - a `Camera` object (see below / help) containing the - settings for the povray camera. - -The `Camera` object can be initialized as follows: - - var camera = Camera() - -This object contains the default settings of the camera, which can be -changed using the following optional arguments, or by just setting the -attributes after instantiation: - -* `antialias` - whether to antialias the output or not (`true` by default) -* `width` - image width (`2048` by default) -* `height` - image height (`1536` by default) -* `viewangle` - camera angle (higher means wider view) (`24` by default) -* `viewpoint` - position of camera (`Matrix([0,0,-5])` by default) -* `look_at` - coordinate to look at (`Matrix([0,0,0])` by defualt) -* `sky` - orientation pointing to the sky (`Matrix([0,1,0])` by default) - -The default settings generate a reasonable centered view of the x-y -plane. - -These attributes can also be set directly for the `POVRaytracer` object: - - pov.look_at = Matrix([0,0,1]) - -The `render` method supports two optional boolean arguments: - -* `quiet` - whether to suppress the parser and render statistics from `povray` or not (`false` by default) -* `display` - whether to turn on the graphic display while rendering or not (`true` by default) - -# Camera -[tagcamera]: # (camera) - -The `Camera` object can be initialized as follows: - - var camera = Camera() - -This object contains the default settings of the camera, which can be -changed using the following optional arguments, or by just setting the -attributes after instantiation: - -* `antialias` - whether to antialias the output or not (`true` by default) -* `width` - image width (`2048` by default) -* `height` - image height (`1536` by default) -* `viewangle` - camera angle (higher means wider view) (`24` by default) -* `viewpoint` - position of camera (`Matrix([0,0,-5])` by default) -* `look_at` - coordinate to look at (`Matrix([0,0,0])` by defualt) -* `sky` - orientation pointing to the sky (`Matrix([0,1,0])` by default) - - camera.sky = Matrix([0,0,1]) - -The default settings generate a reasonable centered view of the x-y -plane. diff --git a/help/range.md b/help/range.md deleted file mode 100644 index ba76f8f..0000000 --- a/help/range.md +++ /dev/null @@ -1,32 +0,0 @@ -[comment]: # (Morpho range help file) -[version]: # (0.5) - -# Range -[tagrange]: # (range) - -Ranges represent a sequence of numerical values. There are two ways to create them depending on whether the upper value is included or not: - - var a = 1..5 // inclusive version, i.e. [1,2,3,4,5] - var b = 1...5 // exclusive version, i.e. [1,2,3,4] - -By default, the increment between values is 1, but you can use a different value like this: - - var a = 1..5:0.5 // 1 - 5 with an increment of 0.5. - -You can also create Range objects using the appropriate constructor function: - - var a = Range(1,5,0.5) - -Ranges are particularly useful in writing loops: - - for (i in 1..5) print i - -They can easily be converted to a list of values: - - var c = List(1..5) - -To find the number of elements in a Range, use the `count` method - - print (1..5).count() - -[showmethodsrange]: # diff --git a/help/requirements.txt b/help/requirements.txt deleted file mode 100644 index c6f5535..0000000 --- a/help/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# File: docs/requirements.txt - -# Defining the exact version will make sure things don't break -sphinx==3.5.3 -sphinx_rtd_theme==0.5.1 -myst-parser==0.13.5 -Jinja2<3.1 diff --git a/help/selection.md b/help/selection.md deleted file mode 100644 index 6e76f78..0000000 --- a/help/selection.md +++ /dev/null @@ -1,67 +0,0 @@ -[comment]: # (Morpho selection class help file) -[version]: # (0.5) - -# Selection -[tagselection]: # (selection) - -The Selection class enables you to select components of a mesh for later use. You can supply a function that is applied to the coordinates of every vertex in the mesh, or select components like boundaries. - -Create an empty selection: - - var s = Selection(mesh) - -Select vertices above the z=0 plane using an anonymous function: - - var s = Selection(mesh, fn (x,y,z) z>0) - -Select the boundary of a mesh: - - var s = Selection(mesh, boundary=true) - -Selection objects can be composed using set operations: - - var s = s1.union(s2) - -or - var s = s1.intersection(s2) - -To add additional grades, use the addgrade method. For example, to add areas: - s.addgrade(2) - -[showsubtopics]: # subtopics - -## addgrade -[tagaddgrade]: # (addgrade) -Adds elements of the specified grade to a Selection. For example, to add edges to an existing selection, use - - s.addgrade(1) - -By default, this only adds an element if *all* vertices in the element are currently selected. Sometimes, it's useful to be able to add elements for which only some vertices are selected. The optional argument `partials` allows you to do this: - - s.addgrade(1, partials=true) - -Note that this method modifies the existing selection, and does not generate a new Selection object. - -## removegrade -[tagremovegrade]: # (removegrade) -Removes elements of the specified grade from a Selection. For example, to remove edges from an existing selection, use - - s.removegrade(1) - -Note that this method modifies the existing selection, and does not generate a new Selection object. - -## idlistforgrade -[tagidlistforgrade]: # (idlistforgrade) -Returns a list of element ids included in the selection. - -To find out which edges are selected: - - var edges = s.idlistforgrade(1) - -## isselected -[tagisselected]: # (isselected) -Checks if an element id is selected, returning `true` or `false` accordingly. - -To check if edge number 5 is selected: - - var f = s.isselected(1, 5)) diff --git a/help/sparse.md b/help/sparse.md deleted file mode 100644 index b320f81..0000000 --- a/help/sparse.md +++ /dev/null @@ -1,27 +0,0 @@ -[comment]: # (Sparse class help) -[version]: # (0.5) - -# Sparse -[tagsparse]: # (Sparse) - -The Sparse class provides support for sparse matrices. An empty sparse matrix can be initialized with a given size, - - var a = Sparse(nrows,ncols) - -Alternatively, a matrix can be created from an array of triplets, - - var a = Sparse([[row, col, value] ...]) - -For example, - - var a = Sparse([[0,0,2], [1,1,-2]]) - -creates the matrix - - [ 2 0 ] - [ 0 -2 ] - -Once a sparse matrix is created, you can use all the regular arithmetic operators with matrix operands, e.g. - - a+b - a*b diff --git a/help/string.md b/help/string.md deleted file mode 100644 index 3cc3a67..0000000 --- a/help/string.md +++ /dev/null @@ -1,58 +0,0 @@ -[comment]: # (String class help) -[version]: # (0.5) - -# String -[tagstring]: # (String) - -Strings represent textual information. They are written in Morpho like this: - - var a = "Hello world" - -Strings may incorporate any valid UTF-8 sequence, and you can insert special characters using the "\" character including: - - "\b" // backspace - "\f" // form feed - "\n" // newline - "\r" // line feed - "\t" // tab - "\xhh" // specify an ASCII character with two hex digits - "\uhhhh" // specify a unicode character with four hex digits - "\Uhhhhhhhh" // specify a unicode character with eight hex digits - -Unicode characters are converted to UTF8 by the morpho compiler. - -You can also create strings using the constructor function `String`, which takes any number of parameters: - - var a = String("Hello", "World") - -A very useful feature, called *string interpolation*, enables the results of any morpho expression can be interpolated into a string. Here, the values of `i` and `func(i)` will be inserted into the string as it is created: - - print "${i}: ${func(i)}" - -To get an individual character, use index notatation - - print "morpho"[0] - -You can loop over each character like this: - - for (c in "morpho") print c - -Note that strings are immutable, and hence - - var a = "morpho" - a[0] = 4 - -raises an error. - -[showsubtopics]: # - -## split -[tagsplit]: # (split) - -The split method splits a String into a list of substrings. It takes one argument, which is a string of characters to use to split the string: - - print "1,2,3".split(",") - -gives - - [ 1, 2, 3 ] diff --git a/help/syntax.md b/help/syntax.md deleted file mode 100644 index b7c1859..0000000 --- a/help/syntax.md +++ /dev/null @@ -1,147 +0,0 @@ -[comment]: # (Morpho syntax help file) -[version]: # (0.5) - -[toplevel]: # - -# Syntax -[tagsyntax]: # (syntax) - -Morpho provides a flexible object oriented language similar to other languages in the C family (like C++, Java and Javascript) with a simplified syntax. - -Morpho programs are stored as plain text with the .morpho file extension. A program can be run from the command line by typing - - morpho5 program.morpho - -## Comments -[tagcomment]: # (comment) -[tagcomments]: # (comments) -[tagcommentvar]: # (//) -[tagcommentvarr]: # (/*) -[tagcommentvarrr]: # (*/) -Two types of comment are available. The first type is called a 'line comment' whereby text after `//` on the same line is ignored by the interpreter. - - a.dosomething() // A comment - -Longer 'block' comments can be created by placing text between `/*` and `*/`. Newlines are ignored - - /* This - is - a longer comment */ - -In contrast to C, these comments can be nested - - /* A nested /* comment */ */ - -enabling the programmer to quickly comment out a section of code. - -## Symbols -[tagsymbols]: # (symbols) -[tagnames]: # (names) - -Symbols are used to refer to named entities, including variables, classes, functions etc. Symbols must begin with a letter or underscore _ as the first character and may include letters or numbers as the remainder. Symbols are case sensitive. - - asymbol - _alsoasymbol - another_symbol - EvenThis123 - YET_ANOTHER_SYMBOL - -Classes are typically given names with an initial capital letter. Variable names are usually all lower case. - -## Newlines -[tagnewlines]: # (newlines) -[tagnewline]: # (newline) - -Strictly, morpho ends statements with semicolons like C, but in practice these are usually optional and you can just start a new line instead. For example, instead of - - var a = 1; // The ; is optional - -you can simply use - - var a = 1 - -If you want to put several statements on the same line, you can separate them with semicolons: - - var a = 1; print a - -There are a few edge cases to be aware of: The morpho parser works by accepting a newline anywhere it expects to find a semicolon. To split a statement over multiple lines, signal to morpho that you plan to continue by leaving the statement unfinished. Hence, do this: - - print a + - 1 - -rather than this: - - print a // < Morpho thinks this is a complete statement - + 1 // < and so this line will cause a syntax error - - -## Booleans -[tagtrue]: # (true) -[tagfalse]: # (false) -[tagbooleans]: # (true) - -Comparison operations like `==`, `<` and `>=` return `true` or `false` depending on the result of the comparison. For example, - - print 1==2 - -prints `false`. The constants `true` or `false` are provided for you to use in your own code: - - return true - -## Nil -[tagnil]: # (nil) - -The keyword `nil` is used to represent the absence of an object or value. - -Note that in `if` statements, a value of `nil` is treated like `false`. - - if (nil) { - // Never executed. - } - -## Blocks -[tagblocks]: # (blocks) -[tagblock]: # (block) - -Code is divided into *blocks*, which are delimited by curly brackets like this: - - { - var a = "Hello" - print a - } - -This syntax is used in function declarations, loops and conditional statements. - -Any variables declared within a block become *local* to that block, and cannot be seen outside of it. For example, - - var a = "Foo" - { - var a = "Bar" - print a - } - print a - -would print "Bar" then "Foo"; the version of `a` inside the code block is said to *shadow* the outer version. - -## Precedence -[tagprecedence]: # (precedence) - -Precedence refers to the order in which morpho evaluates operations. For example, - - print 1+2*3 - -prints `7` because `2*3` is evaluated before the addition; the operator `*` is said to have higher precedence than `+`. - -You can always modify the order of evaluation by using parentheses: - - print (1+2)*3 // prints 9 - -## Print -[tagprint]: # (print) - -The `print` keyword is used to print information to the console. It can be followed by any value, e.g. - - print 1 - print true - print a - print "Hello" diff --git a/help/system.md b/help/system.md deleted file mode 100644 index bb9e8ba..0000000 --- a/help/system.md +++ /dev/null @@ -1,71 +0,0 @@ -[comment]: # (System help) -[version]: # (0.5) - -# System -[tagsystem]: # (system) - -The `System` class provides information and access to some features of the runtime environment. - -[showsubtopics]: # (subtopics) - -## Platform -[tagplatform]: # (platform) - -Detect which platform morpho was compiled for: - - print System.platform() - -which returns `"macos"`, `"linux"`, `"unix"` or `"windows"`. - -## Version -[tagversion]: # (version) - -Find the current version of morpho: - - print System.version() - -## Clock -[tagclock]: # (clock) - -Returns the system time in seconds, with at least millisecond granularity. Primarily intended for timing: - - var start=System.clock() - // Do something - print System.clock()-start - -Note that `System.clock` measures the actual physical time elapsed, not the time spent in a process. - -## Sleep -[tagsleep]: # (sleep) - -Pauses the program for a specified number of seconds: - - System.sleep(0.5) // Sleep for half a second - -## Readline -[tagreadline]: # (readline) - -Reads a line of input from the console: - - var in = System.readline() - -## Arguments -[tagargunents]: # (arguments) - -Returns a `List` of arguments passed to the current morpho on the command line. - - var args = System.arguments() - for (e in args) print e - -Run a morpho program with arguments: - - morpho5 program.morpho hello world - -Note that, in line with UNIX conventions, command line arguments before the program file name are passed to the `morpho5` runtime; those after are passed to the morpho program via `System.arguments`. - -## Exit -[tagexit]: # (exit) - -Stop execution of a program: - - System.exit() diff --git a/help/values.md b/help/values.md deleted file mode 100644 index 6f8e415..0000000 --- a/help/values.md +++ /dev/null @@ -1,45 +0,0 @@ -[comment]: # (Values help) -[version]: # (0.5) - -# Values -[tagvalues]: # (values) - -Values are the basic unit of information in morpho: All functions in morpho accept values as arguments and return values. - -[showsubtopics]: # (subtopics) - -## Int -[tagint]: # (int) - -Morpho provides integers, which work as you would expect in other languages, although you rarely need to worry about the distinction between floats and integers. - -Convert a floating point number to an Integer: - - print Int(1.3) // expect: 1 - -Convert a string to an integer: - - print Int("10")+1 // expect: 11 - -## Float -[tagfloat]: # (float) - -Morpho provides double precision floating point numbers. - -Convert a string to a floating point number: - - print Float("1.2e2")+1 // expect: 121 - -## Ceil -[tagceil]: # (ceil) - -Returns the smallest integer larger than or equal to its argument: - - print ceil(1.3) // expect: 2 - -## Floor -[tagfloor]: # (floor) - -Returns the largest integer smaller than or equal to its argument: - - print floor(1.3) // expect: 1 diff --git a/help/variables.md b/help/variables.md deleted file mode 100644 index ad53df9..0000000 --- a/help/variables.md +++ /dev/null @@ -1,70 +0,0 @@ -[comment]: # (Morpho variables help file) -[version]: # (0.5) - -[toplevel]: # - -# Variables -[tagvariables]: # (variables) -[tagvar]: # (var) - -Variables are defined using the `var` keyword followed by the variable name: - - var a - -Optionally, an initial assignment may be given: - - var a = 1 - -Variables defined in a block of code are visible only within that block, so - - var greeting = "Hello" - { - var greeting = "Goodbye" - print greeting - } - print greeting - -will print - -*Goodbye* -*Hello* - -Multiple variables can be defined at once by separating them with commas - - var a, b=2, c[2]=[1,2] - -where each can have its own initializer (or not). - -## Indexing -[taglb]: # ([) -[tagrb]: # (]) -[tagindex]: # (index) -[tagsub]: # (subscript) - -Morpho provides a number of collection objects, such as `List`, `Range`, `Array`, `Dictionary`, `Matrix` and `Sparse`, that can contain more than one value. Index notation (sometimes called subscript notation) is used to access elements of these objects. - -To retrieve an item from a collection, you use the `[` and `]` brackets like this: - - var a = List("Apple", "Bag", "Cat") - print a[0] - -which prints *Apple*. Note that the first element is accessed with `0` not `1`. - -Similarly, to set an entry in a collection, use: - - a[0]="Adder" - -which would replaces the first element in `a` with `"Adder"`. - -Some collection objects need more than one index, - - var a = Matrix([[1,0],[0,1]]) - print a[0,0] - -and others such as `Dictionary` use non-numerical indices, - - var b = Dictionary() - b["Massachusetts"]="Boston" - b["California"]="Sacramento" - -as in this dictionary of state capitals. diff --git a/help/vtk.md b/help/vtk.md deleted file mode 100644 index ca32de4..0000000 --- a/help/vtk.md +++ /dev/null @@ -1,116 +0,0 @@ -[comment]: # (Morpho vtk module help file) -[version]: # (0.5) - -# VTK -[tagvtk]: # (vtk) - -The vtk module contains classes to allow I/O of meshes and fields using -the VTK Legacy Format. - -[showsubtopics]: # (subtopics) - -## VTKExporter -[tagvtkexporter]: # (VTKExporter) - -This class can be used to export the field(s) and/or at a given state -to a single .vtk file. To use it, import the `vtk` module: - - import vtk - -Initialize the `VTKExporter` - - var vtkE = VTKExporter(obj) - -where `obj` can either be - -* A `Mesh` object: This prepares the Mesh for exporting. -* A `Field` object: This prepares both the Field and the Mesh associated - with it for exporting. - -Use the `export` method to export to a VTK file. - - vtkE.export("output.vtk") - -Optionally, use the `addfield` method to add one or more fields before -exporting: - - vtkE.addfield(f, fieldname="f") - -where, - -* `f` is the field object to be exported -* `fieldname` is an optional argument that assigns a name to the field - in the VTK file. This name is required to be a character - string without embedded whitespace. If not provided, the name would be - either "scalars" or "vectors" depending on the field type**. - -** Note that this currently only supports scalar or vector (column -matrix) fields that live on the vertices ( shape `[1,0,0]`). Support for -tensorial fields and fields on cells coming soon. - -Minimal example: - - import vtk - import meshtools - - var m1 = LineMesh(fn (t) [t,0,0], -1..1:2) - - var vtkE = VTKExporter(m1) // Export just the mesh - - vtkE.export("mesh.vtk") - - var f1 = Field(m1, fn(x,y,z) x) - - var g1 = Field(m1, fn(x,y,z) Matrix([x,2*x,3*x])) - - vtkE = VTKExporter(f1, fieldname="f") // Export fields - - vtkE.addfield(g1, fieldname="g") - - vtkE.export("data.vtk") - -## VTKImporter -[tagvtkimporter]: # (VTKImporter) - -This class can be used to import the field(s) and/or the mesh at a -given state from a single .vtk file. To use it, import the `vtk` module: - - import vtk - -Initialize the `VTKImporter` with the filename - - var vtkI = VTKImporter("output.vtk") - -Use the `mesh` method to get the mesh: - - var mesh = vtkI.mesh() - -Use the `field` method to get the field: - - var f = vtkI.field(fieldname) - -Use the `fieldlist` method to get the list of the names of the fields contained in the file: - - print vtkI.fieldlist() - -Use the `containsfield` method to check whether the file contains a field by a given `fieldname`: - - if (tkI.containsfield(fieldname)) { - ... - } - -where `fieldname` is the name assigned to the field in the .vtk file - -Minimal example: - - import vtk - import meshtools - - var vtkI = VTKImporter("data.vtk") - - var m = vtkI.mesh() - - var f = vtkI.field("f") - - var g = vtkI.field("g") - From 7faf2236fde90451add1c15ec634e733f7e5ef6f Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:21:36 -0500 Subject: [PATCH 09/63] Check option --- src/main.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main.c b/src/main.c index 79b3a79..7091c55 100644 --- a/src/main.c +++ b/src/main.c @@ -85,6 +85,12 @@ static bool opt_workers(const char *opt, const char *arg, clioptions *flags, opt return true; } +static bool opt_check(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags &= ~CLI_RUN; /* Clear RUN flag - compile only, don't execute */ + return true; +} + static bool opt_eval(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; clidebugger_initialize(); @@ -105,6 +111,7 @@ static const option_t opt_table[] = { { "-dl", NULL, false, opt_disassemblelist }, { "-d", "--disassemble", false, opt_disassemble }, { "-debug", "--debug", false, opt_debug }, + { "-c", "--check", false, opt_check }, { "-e", "--eval", true, opt_eval }, { "-O", "--optimize", false, opt_optimize }, #ifdef MORPHO_PROFILER From 7a99b88f7af3357c7dc1f30b6dffdbe1ea31c6e1 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:23:24 -0500 Subject: [PATCH 10/63] Interactive flag --- src/cli.h | 1 + src/main.c | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/cli.h b/src/cli.h index f5f8040..701e963 100644 --- a/src/cli.h +++ b/src/cli.h @@ -37,6 +37,7 @@ #define CLI_DEBUG (1<<3) #define CLI_OPTIMIZE (1<<4) #define CLI_PROFILE (1<<5) +#define CLI_INTERACTIVE (1<<6) typedef unsigned int clioptions; diff --git a/src/main.c b/src/main.c index 7091c55..f18c0bd 100644 --- a/src/main.c +++ b/src/main.c @@ -91,6 +91,12 @@ static bool opt_check(const char *opt, const char *arg, clioptions *flags, opt_c return true; } +static bool opt_interactive(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_INTERACTIVE; /* Enter REPL after running file */ + return true; +} + static bool opt_eval(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; clidebugger_initialize(); @@ -113,6 +119,7 @@ static const option_t opt_table[] = { { "-debug", "--debug", false, opt_debug }, { "-c", "--check", false, opt_check }, { "-e", "--eval", true, opt_eval }, + { "-i", "--interactive", false, opt_interactive }, { "-O", "--optimize", false, opt_optimize }, #ifdef MORPHO_PROFILER { "-profile", "--profile", false, opt_profile }, @@ -162,7 +169,12 @@ int main(int argc, const char *argv[]) { if (run) { clidebugger_initialize(); if (i < argc) morpho_setargs(argc - i - 1, argv + i); // Pass unused args to morpho - (file ? cli_run(file, opt) : cli(opt)); + if (file) { + cli_run(file, opt); + if (opt & CLI_INTERACTIVE) cli(opt); /* Enter REPL after running file */ + } else { + cli(opt); + } } morpho_finalize(); From 5cde689830e9e23cce88f3e568b0ee085b4357fa Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:32:27 -0500 Subject: [PATCH 11/63] Code organization --- src/cli.c | 63 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/src/cli.c b/src/cli.c index a86e9dd..2ef588d 100644 --- a/src/cli.c +++ b/src/cli.c @@ -40,6 +40,10 @@ void inline_setutf8(void); void inline_emitcolor(int color); void inline_emit(const char *seq); +/* ********************************************************************** + * Utility functions + * ********************************************************************** */ + void cli_emitemphasis(int emph) { switch (emph) { case CLI_NOEMPHASIS: inline_emit(RESET); break; @@ -90,8 +94,30 @@ void cli_reporterror(error *err, vm *v) { } } + +/** Interactive help */ +void cli_help(inline_editor *edit, char *query, error *err, bool avail) { + char *q=query; + if (help_querylength(q, NULL)==0) { + if (err->cat!=ERROR_NONE) { + q=err->id; + error_clear(err); + } else { + q=HELP_INDEXPAGE; + } + } + + objecthelptopic *topic = help_search(q); + if (topic) { + help_display(edit, topic); + } else { + while (isspace(*q) && *q!='\0') q++; + printf("No help found for '%s'\n", q); + } +} + /* ********************************************************************** - * CLI callbacks + * Morpho callbacks * ********************************************************************** */ /** Print callback */ @@ -125,11 +151,10 @@ void cli_debuggercallbackfn(vm *v, void *ref) { } /* ********************************************************************** - * Interactive cli + * Inline callbacks * ********************************************************************** */ /** Define colors for different token types */ - int palette[] = { CLI_DEFAULTCOLOR, // 0 default INLINE_YELLOW, // 1 help @@ -225,27 +250,6 @@ bool cli_multiline(const char *in, void *ref) { return (nb>0); } -/** Interactive help */ -void cli_help(inline_editor *edit, char *query, error *err, bool avail) { - char *q=query; - if (help_querylength(q, NULL)==0) { - if (err->cat!=ERROR_NONE) { - q=err->id; - error_clear(err); - } else { - q=HELP_INDEXPAGE; - } - } - - objecthelptopic *topic = help_search(q); - if (topic) { - help_display(edit, topic); - } else { - while (isspace(*q) && *q!='\0') q++; - printf("No help found for '%s'\n", q); - } -} - #ifdef CLI_USELIBUNISTRING size_t libunistring_graphemefn(const char *in, const char *end) { char *next = (char *) u8_grapheme_next((uint8_t *) in, (uint8_t *) end); @@ -260,6 +264,10 @@ size_t libgrapheme_graphemefn(const char *in, const char *end) { } #endif +/* ********************************************************************** + * Interactive mode + * ********************************************************************** */ + /** @brief Provide a command line interface */ void cli(clioptions opt) { bool tty=inline_checktty(); @@ -370,7 +378,7 @@ void cli(clioptions opt) { } /* ********************************************************************** - * Run a file + * Non-interactive run * ********************************************************************** */ /** Compile and run source string (no file). Used by -e / --eval. */ @@ -421,7 +429,10 @@ void cli_runstring(const char *src, clioptions opt) { morpho_freecompiler(c); } -/** Loads and runs a file. */ +/* ********************************************************************** + * Load and run a file + * ********************************************************************** */ + void cli_run(const char *in, clioptions opt) { program *p = morpho_newprogram(); compiler *c = morpho_newcompiler(p); From efd7776c6515a457bd1449a0641942c19e08fe7b Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:47:02 -0500 Subject: [PATCH 12/63] Interactive mode --- src/cli.c | 323 ++++++++++++++++++++++++++--------------------------- src/main.c | 7 +- 2 files changed, 158 insertions(+), 172 deletions(-) diff --git a/src/cli.c b/src/cli.c index 2ef588d..0bce170 100644 --- a/src/cli.c +++ b/src/cli.c @@ -6,6 +6,7 @@ #include #include +#include #include #include "cli.h" @@ -268,9 +269,115 @@ size_t libgrapheme_graphemefn(const char *in, const char *end) { * Interactive mode * ********************************************************************** */ + +/** Runtime context holding VM, compiler, program, editor, and lexer */ +typedef struct { + vm *v; + compiler *c; + program *p; + inline_editor *edit; + lexer l; +} runtime_t; + +/** Forward declaration */ +static void cli_repl(runtime_t *rt, clioptions opt); + /** @brief Provide a command line interface */ void cli(clioptions opt) { - bool tty=inline_checktty(); + cli_repl(NULL, opt); /* NULL means create new runtime */ +} + +/* ********************************************************************** + * Helper structures and functions + * ********************************************************************** */ + +/** Set up runtime context (VM, compiler, program, editor, callbacks) */ +static runtime_t cli_newruntime(void) { + runtime_t rt = { NULL, NULL, NULL, NULL }; + rt.p = morpho_newprogram(); + rt.c = morpho_newcompiler(rt.p); + rt.v = morpho_newvm(); + rt.edit = inline_new(CLI_PROMPT); + + inline_syntaxcolor(rt.edit, cli_syntaxcolorfn, &rt.l); + + morpho_setinputfn(rt.v, cli_inputcallbackfn, NULL); + morpho_setprintfn(rt.v, cli_printcallbackfn, &rt.edit); + morpho_setwarningfn(rt.v, cli_warningcallbackfn, &rt.edit); + morpho_setdebuggerfn(rt.v, cli_debuggercallbackfn, NULL); + + return rt; +} + +/** Clean up runtime context */ +static void cli_freeruntime(runtime_t *rt) { + if (rt->edit) inline_free(rt->edit); + if (rt->v) morpho_freevm(rt->v); + if (rt->p) morpho_freeprogram(rt->p); + if (rt->c) morpho_freecompiler(rt->c); +} + + +/** Compile and execute source code */ +static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { + error err; + error_init(&err); + + bool success = morpho_compile((char *)src, rt->c, (opt & CLI_OPTIMIZE), &err); + + if (success) { + if (opt & CLI_DISASSEMBLE) { + if (opt & CLI_DISASSEMBLESHOWSRC) { + cli_disassemblewithsrc(rt->p, (char *)src); + } else { + morpho_disassemble(rt->v, rt->p, NULL); + } + } + if (opt & CLI_RUN) { + if (opt & CLI_DEBUG) { + success = morpho_debug(rt->v, rt->p); + } else if (opt & CLI_PROFILE) { + success = morpho_profile(rt->v, rt->p); + } else { + success = morpho_run(rt->v, rt->p); + } + if (!success) cli_reporterror(morpho_geterror(rt->v), rt->v); + } + } else { + cli_reporterror(&err, rt->v); + } + + return success; +} + +/* ********************************************************************** + * Non-interactive run + * ********************************************************************** */ + +/** Compile and run source string (no file). Used by -e / --eval. */ +void cli_runstring(const char *src, clioptions opt) { + runtime_t rt = cli_newruntime(); + cli_globalsrc = (char *)src; + + cli_compileandrun(&rt, src, opt); + + /* If interactive mode, enter REPL with same VM (cleans up on exit) */ + if (opt & CLI_INTERACTIVE) { + cli_repl(&rt, opt); + } + cli_freeruntime(&rt); +} + +/** Enter REPL mode, optionally reusing existing runtime */ +static void cli_repl(runtime_t *rt, clioptions opt) { + bool own_runtime = (rt == NULL); + runtime_t runtime_storage; + if (own_runtime) { + runtime_storage = cli_newruntime(); + rt = &runtime_storage; + } + + bool tty = inline_checktty(); version morphoversion; morpho_version(&morphoversion); char morphoversionstring[VERSION_MAXSTRINGLENGTH]; @@ -280,153 +387,78 @@ void cli(clioptions opt) { inline_setutf8(); printf("\U0001F98B morpho %s | \U0001F44B Type 'help' or '?' for help\n", morphoversionstring); } - - /* Set up program and compiler */ - program *p = morpho_newprogram(); - compiler *c = morpho_newcompiler(p); bool help = help_initialize(); - /* Keep the line by line src as a varray */ varray_char src; varray_charinit(&src); - varray_charwrite(&src, '\0'); // Begin with zero string + varray_charwrite(&src, '\0'); - /* Set up VM */ - vm *v = morpho_newvm(); - - /* Line editor */ - inline_editor *edit = inline_new(CLI_PROMPT); - lexer l; - inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); - inline_multiline(edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); - inline_autocomplete(edit, cli_complete, NULL); + /* Configure editor for REPL (if not already configured) */ + inline_setpalette(rt->edit, sizeof(palette)/sizeof(palette[0]), palette); + inline_syntaxcolor(rt->edit, cli_syntaxcolorfn, &rt->l); + inline_multiline(rt->edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); + inline_autocomplete(rt->edit, cli_complete, NULL); #ifdef CLI_USELIBUNISTRING - inline_setgraphemesplitter(&edit, libunistring_graphemefn); + inline_setgraphemesplitter(&rt->edit, libunistring_graphemefn); #endif #ifdef CLI_USELIBGRAPHEME - inline_setgraphemesplitter(edit, libgrapheme_graphemefn); + inline_setgraphemesplitter(rt->edit, libgrapheme_graphemefn); #endif - - morpho_setinputfn(v, cli_inputcallbackfn, NULL); - morpho_setprintfn(v, cli_printcallbackfn, &edit); - morpho_setwarningfn(v, cli_warningcallbackfn, &edit); - morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); - error err; /* Error structure that received messages from the compiler and VM */ - bool success=false; /* Keep track of whether compilation and execution was successful */ - - /* Initialize the error struct */ + error err; error_init(&err); - - /* Read-evaluate-print loop */ - for (int n=0;;n++) { - if (!tty && n>0) break; - char *input=NULL; - - while (!input) input=inline_readline(edit); + + for (int n = 0; ; n++) { + if (!tty && n > 0) break; + char *input = NULL; + while (!input) input = inline_readline(rt->edit); - /* Check for CLI commands. */ - /* Let the user quit by typing 'quit'. */ - if (strncmp(input, CLI_QUIT, strlen(CLI_QUIT))==0) { - break; - } else if (strncmp(input, CLI_HELP, strlen(CLI_HELP))==0) { - cli_help(edit, input+strlen(CLI_HELP), &err, help); continue; - } else if (strncmp(input, CLI_SHORT_HELP, strlen(CLI_SHORT_HELP))==0) { - cli_help(edit, input+strlen(CLI_SHORT_HELP), &err, help); continue; + if (strncmp(input, CLI_QUIT, strlen(CLI_QUIT)) == 0) { + free(input); + break; + } else if (strncmp(input, CLI_HELP, strlen(CLI_HELP)) == 0) { + cli_help(rt->edit, input + strlen(CLI_HELP), &err, help); + free(input); + continue; + } else if (strncmp(input, CLI_SHORT_HELP, strlen(CLI_SHORT_HELP)) == 0) { + cli_help(rt->edit, input + strlen(CLI_SHORT_HELP), &err, help); + free(input); + continue; } - /* Compile code */ - success=morpho_compile(input, c, false, &err); + bool success = morpho_compile(input, rt->c, false, &err); - if (success) { /** If compilation was successful, and we're in interactive mode, execute... */ - /** Retain input in interactive session */ - src.count--; // Remove zero terminator - varray_charadd(&src, input, (int) strlen(input)); + if (success) { + src.count--; + varray_charadd(&src, input, (int)strlen(input)); varray_charwrite(&src, '\n'); - varray_charwrite(&src, '\0'); // Ensure zero terminated - cli_globalsrc=src.data; + varray_charwrite(&src, '\0'); + cli_globalsrc = src.data; if (opt & CLI_DISASSEMBLE) { - morpho_disassemble(v, p, NULL); + morpho_disassemble(rt->v, rt->p, NULL); } if (opt & CLI_RUN) { - success=morpho_debug(v, p); + success = morpho_debug(rt->v, rt->p); if (!success) { - cli_reporterror(morpho_geterror(v), v); - err=*morpho_geterror(v); + cli_reporterror(morpho_geterror(rt->v), rt->v); + err = *morpho_geterror(rt->v); } } } else { - /** ... otherwise just raise an error. */ - cli_reporterror(&err, v); + cli_reporterror(&err, rt->v); } - if (input) free(input); + free(input); } - inline_free(edit); - morpho_freevm(v); - varray_charclear(&src); - help_finalize(); - morpho_freecompiler(c); - morpho_freeprogram(p); -} - -/* ********************************************************************** - * Non-interactive run - * ********************************************************************** */ - -/** Compile and run source string (no file). Used by -e / --eval. */ -void cli_runstring(const char *src, clioptions opt) { - program *p = morpho_newprogram(); - compiler *c = morpho_newcompiler(p); - vm *v = morpho_newvm(); - - inline_editor *edit = inline_new(CLI_PROMPT); - lexer l; - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); - - morpho_setinputfn(v, cli_inputcallbackfn, &edit); - morpho_setprintfn(v, cli_printcallbackfn, &edit); - morpho_setwarningfn(v, cli_warningcallbackfn, &edit); - morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); - - error err; - error_init(&err); - - bool success = morpho_compile((char *) src, c, (opt & CLI_OPTIMIZE), &err); - - if (success) { - if (opt & CLI_DISASSEMBLE) { - if (opt & CLI_DISASSEMBLESHOWSRC) { - cli_disassemblewithsrc(p, (char *)src); - } else { - morpho_disassemble(v, p, NULL); - } - } - if (opt & CLI_RUN) { - if (opt & CLI_DEBUG) { - success = morpho_debug(v, p); - } else if (opt & CLI_PROFILE) { - success = morpho_profile(v, p); - } else { - success = morpho_run(v, p); - } - if (!success) cli_reporterror(morpho_geterror(v), v); - } - } else { - cli_reporterror(&err, v); + if (own_runtime) { + cli_freeruntime(rt); } - - inline_free(edit); - morpho_freevm(v); - morpho_freeprogram(p); - morpho_freecompiler(c); } /* ********************************************************************** @@ -434,68 +466,27 @@ void cli_runstring(const char *src, clioptions opt) { * ********************************************************************** */ void cli_run(const char *in, clioptions opt) { - program *p = morpho_newprogram(); - compiler *c = morpho_newcompiler(p); - vm *v = morpho_newvm(); - - /* Set up line editor for output */ - inline_editor *edit=inline_new(CLI_PROMPT); - lexer l; - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); - - morpho_setinputfn(v, cli_inputcallbackfn, &edit); - morpho_setprintfn(v, cli_printcallbackfn, &edit); - morpho_setwarningfn(v, cli_warningcallbackfn, &edit); - morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); + runtime_t rt = cli_newruntime(); char *src = cli_loadsource(in); if (src) cli_globalsrc = src; - error err; /* Error structure that received messages from the compiler and VM */ - error_init(&err); - - bool success=false; /* Keep track of whether compilation and execution was successful */ - - /* Open the input file if provided */ file_setworkingdirectory(in); if (src) { - /* Compile code */ - success=morpho_compile(src, c, (opt & CLI_OPTIMIZE), &err); - - /* Run code if successful */ - if (success) { - if (opt & CLI_DISASSEMBLE) { - if (opt & CLI_DISASSEMBLESHOWSRC) { - cli_disassemblewithsrc(p, src); - } else { - morpho_disassemble(v, p, NULL); - } - } - if (opt & CLI_RUN) { - if (opt & CLI_DEBUG) { - morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); - success=morpho_debug(v, p); - } else if (opt & CLI_PROFILE) { - success=morpho_profile(v, p); - } else { - success=morpho_run(v, p); - } - if (!success) cli_reporterror(morpho_geterror(v), v); - } - } else { - cli_reporterror(&err, v); - } + cli_compileandrun(&rt, src, opt); + MORPHO_FREE(src); } else { printf("Could not open file '%s'.\n", in); + cli_freeruntime(&rt); + return; } - inline_free(edit); - - MORPHO_FREE(src); - morpho_freevm(v); - morpho_freeprogram(p); - morpho_freecompiler(c); + /* If interactive mode, enter REPL with same VM (cleans up on exit) */ + if (opt & CLI_INTERACTIVE) { + cli_repl(&rt, opt); + } + cli_freeruntime(&rt); } /* ********************************************************************** diff --git a/src/main.c b/src/main.c index f18c0bd..8359d43 100644 --- a/src/main.c +++ b/src/main.c @@ -169,12 +169,7 @@ int main(int argc, const char *argv[]) { if (run) { clidebugger_initialize(); if (i < argc) morpho_setargs(argc - i - 1, argv + i); // Pass unused args to morpho - if (file) { - cli_run(file, opt); - if (opt & CLI_INTERACTIVE) cli(opt); /* Enter REPL after running file */ - } else { - cli(opt); - } + (file ? cli_run(file, opt) : cli(opt)); } morpho_finalize(); From ddf13dbcb25497bb91e6d793b63596ae87285e2d Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:53:54 -0500 Subject: [PATCH 13/63] --list option --- src/cli.c | 1 + src/main.c | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/cli.c b/src/cli.c index 0bce170..4281350 100644 --- a/src/cli.c +++ b/src/cli.c @@ -570,6 +570,7 @@ void cli_list(const char *src, int start, int end) { if (!edit) return; lexer l; inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); + inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); int line=1, length=0; for (unsigned int i=0; src[i]!='\0'; i++) { diff --git a/src/main.c b/src/main.c index 8359d43..168236f 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -93,10 +94,22 @@ static bool opt_check(const char *opt, const char *arg, clioptions *flags, opt_c static bool opt_interactive(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; (void)arg; (void)ctx; - *flags |= CLI_INTERACTIVE; /* Enter REPL after running file */ + *flags |= CLI_INTERACTIVE; // Enter REPL after running file return true; } +static bool opt_list(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)flags; (void)ctx; + if (!arg) return false; + + char *src = cli_loadsource(arg); + if (src) { + cli_list(src, 1, INT_MAX); + MORPHO_FREE(src); + } else fprintf(stderr, "morpho: Could not open file '%s'\n", arg); + return false; // Don't run program after listing +} + static bool opt_eval(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; clidebugger_initialize(); @@ -120,6 +133,7 @@ static const option_t opt_table[] = { { "-c", "--check", false, opt_check }, { "-e", "--eval", true, opt_eval }, { "-i", "--interactive", false, opt_interactive }, + { "-l", "--list", true, opt_list }, { "-O", "--optimize", false, opt_optimize }, #ifdef MORPHO_PROFILER { "-profile", "--profile", false, opt_profile }, From 3319d2744168e247ae2223c32cebda1261cf6992 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 07:11:40 -0500 Subject: [PATCH 14/63] Syntax highlight comments --- src/cli.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/cli.c b/src/cli.c index 4281350..3f2d21f 100644 --- a/src/cli.c +++ b/src/cli.c @@ -161,7 +161,8 @@ int palette[] = { INLINE_YELLOW, // 1 help INLINE_BLUE, // 2 string/integer/number literals INLINE_CYAN, // 3 symbol - INLINE_MAGENTA // 4 keyword + INLINE_MAGENTA, // 4 keyword + INLINE_GRAY_ANSI(12) // 5 comment (mid-level gray) }; tokentype help[] = { TOKEN_QUESTION }; @@ -176,11 +177,57 @@ static bool matchtokentype(tokentype match, size_t n, tokentype *list) { return false; } +/** Detect and parse comments manually (lexer skips them). + * Returns the byte position after the comment, or offset if no comment found. */ +static size_t detect_comment(const char *in, size_t offset) { + const char *start = in + offset; + + // Skip whitespace first + const char *p = start; + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; + + // Check for // style comment + if (p[0] == '/' && p[1] == '/') { + p += 2; + while (*p != '\0' && *p != '\n' && *p != '\r') p++; // Scan to end of line + return p - in; + } + + // Check for /* style comment + if (p[0] == '/' && p[1] == '*') { + p += 2; + int nesting = 1; // Track nesting depth + + while (nesting > 0 && *p != '\0') { + if (p[0] == '/' && p[1] == '*') { + nesting++; + p += 2; + } else if (p[0] == '*' && p[1] == '/') { + nesting--; + p += 2; + } else p++; + } + return p - in; + } + + return offset; // No comment found +} + /** A tokenizer for syntax coloring that uses the morpho lexer */ bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspan_t *out) { bool success=false; lexer *l=(lexer *) ref; if (!l) return false; + + // Check for comments first (before lexer processing) + size_t comment_end = detect_comment(in, offset); + if (comment_end > offset) { + // Found a comment + out->color = 5; // Comment color (gray) + out->byte_end = comment_end; + return true; // Successfully colored a comment + } + lex_init(l, in+offset, 0); token tok; From bb05fc93d149ffed7ace989da269bdefbb2f0c94 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 07:15:55 -0500 Subject: [PATCH 15/63] Help --- src/main.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main.c b/src/main.c index 168236f..283530b 100644 --- a/src/main.c +++ b/src/main.c @@ -36,6 +36,30 @@ static bool opt_version(const char *opt, const char *arg, clioptions *flags, opt return false; } +static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)flags; (void)ctx; + printf("Usage: morpho6 [options] [file] [options passed to program]\n"); + printf("\nOptions:\n"); + printf(" -h, --help Show this help message\n"); + printf(" -v, --version Show version information\n"); + printf(" -c, --check Check syntax without executing\n"); + printf(" -e, --eval Execute code string\n"); + printf(" -i, --interactive Enter REPL after running file\n"); + printf(" -l, --list List file with syntax highlighting\n"); + printf(" -d, --disassemble Show disassembly\n"); + printf(" -D Disassemble only (no execution)\n"); + printf(" -dl Disassemble with source listing\n"); + printf(" -debug, --debug Enable debugger\n"); + printf(" -O, --optimize Enable optimizations\n"); +#ifdef MORPHO_PROFILER + printf(" -profile, --profile Enable profiling\n"); +#endif + printf(" -w, --workers Set number of worker threads\n"); + printf("\nIf no file is specified, morpho enters interactive REPL mode.\n"); + printf("Any options after the file name are passed to the morpho program.\n"); + return false; +} + static bool opt_disassembleonly(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; (void)arg; (void)ctx; *flags ^= CLI_RUN; @@ -132,6 +156,7 @@ static const option_t opt_table[] = { { "-debug", "--debug", false, opt_debug }, { "-c", "--check", false, opt_check }, { "-e", "--eval", true, opt_eval }, + { "-h", "--help", false, opt_help }, { "-i", "--interactive", false, opt_interactive }, { "-l", "--list", true, opt_list }, { "-O", "--optimize", false, opt_optimize }, From 1bfa4e1a0b69968be1da2d6bb50fbfebb76504ba Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:41:23 -0500 Subject: [PATCH 16/63] Correct behavior in pipes --- src/cli.c | 32 +++++++++++++++++++++++++++----- src/cli.h | 1 + src/main.c | 17 ++++++++++++++++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/cli.c b/src/cli.c index 3f2d21f..baaf722 100644 --- a/src/cli.c +++ b/src/cli.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "cli.h" @@ -26,7 +27,7 @@ char *cli_globalsrc=NULL; -#define CLI_BUFFERSIZE 1024 +#define CLI_BUFFERSIZE 4096 #define BLU "\x1B[34m" #define CYN "\x1B[36m" @@ -59,13 +60,16 @@ void cli_emitemphasis(int emph) { void cli_displaywithstyle(int col, int emph, int n, ...) { va_list args; va_start(args, n); + bool is_tty = inline_checktty(); // Only emit escape codes if stdout is a TTY + for (int i=0; i 0) { + varray_charadd(&buffer, chunk, (int)nread); + } + + /* Ensure null termination */ + varray_charwrite(&buffer, '\0'); + + return buffer.data; +} + /** Loads a source file, returning it as a C-string. Call MORPHO_FREE on it when finished. */ char *cli_loadsource(const char *in) { FILE *f = NULL; /* Input file */ diff --git a/src/cli.h b/src/cli.h index 701e963..d6ecaf9 100644 --- a/src/cli.h +++ b/src/cli.h @@ -51,6 +51,7 @@ void cli_runstring(const char *src, clioptions opt); void cli(clioptions opt); char *cli_loadsource(const char *in); +char *cli_loadstdin(void); void cli_disassemblewithsrc(program *p, char *src); void cli_list(const char *in, int start, int end); diff --git a/src/main.c b/src/main.c index 283530b..6d256d6 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,7 @@ #include "cli.h" #include "debugger.h" +#include "inline.h" /** Context passed to option handlers that need argc/argv (e.g. eval). */ typedef struct { @@ -56,6 +57,7 @@ static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ct #endif printf(" -w, --workers Set number of worker threads\n"); printf("\nIf no file is specified, morpho enters interactive REPL mode.\n"); + printf("If stdin is piped or redirected, morpho reads and executes from stdin.\n"); printf("Any options after the file name are passed to the morpho program.\n"); return false; } @@ -208,7 +210,20 @@ int main(int argc, const char *argv[]) { if (run) { clidebugger_initialize(); if (i < argc) morpho_setargs(argc - i - 1, argv + i); // Pass unused args to morpho - (file ? cli_run(file, opt) : cli(opt)); + + if (file) { + cli_run(file, opt); + } else if (!inline_checktty()) { + // stdin is piped/redirected - read and execute from stdin + char *src = cli_loadstdin(); + if (src) { + cli_runstring(src, opt); + MORPHO_FREE(src); + } + } else { + // stdin is a TTY - enter REPL + cli(opt); + } } morpho_finalize(); From d1b92f6f08c7e97eb68642113549b253caa8cfae Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:00:24 -0500 Subject: [PATCH 17/63] Fix disassemble only --- src/cli.c | 1 - src/main.c | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cli.c b/src/cli.c index baaf722..25ef3ab 100644 --- a/src/cli.c +++ b/src/cli.c @@ -367,7 +367,6 @@ static void cli_freeruntime(runtime_t *rt) { if (rt->c) morpho_freecompiler(rt->c); } - /** Compile and execute source code */ static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { error err; diff --git a/src/main.c b/src/main.c index 6d256d6..c3cb6a5 100644 --- a/src/main.c +++ b/src/main.c @@ -66,7 +66,7 @@ static bool opt_disassembleonly(const char *opt, const char *arg, clioptions *fl (void)opt; (void)arg; (void)ctx; *flags ^= CLI_RUN; *flags |= CLI_DISASSEMBLE; - return false; + return true; } static bool opt_disassemblelist(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { From db6ce34e009009dfb2a5e41567775526445d963a Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:33:41 -0500 Subject: [PATCH 18/63] Improve colors --- src/cli.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cli.c b/src/cli.c index 25ef3ab..aa37b9e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -159,14 +159,14 @@ void cli_debuggercallbackfn(vm *v, void *ref) { * Inline callbacks * ********************************************************************** */ -/** Define colors for different token types */ +/** Define colors for different token types (256-color palette) */ int palette[] = { - CLI_DEFAULTCOLOR, // 0 default - INLINE_YELLOW, // 1 help - INLINE_BLUE, // 2 string/integer/number literals - INLINE_CYAN, // 3 symbol - INLINE_MAGENTA, // 4 keyword - INLINE_GRAY_ANSI(12) // 5 comment (mid-level gray) + CLI_DEFAULTCOLOR, // 0 default + INLINE_YELLOW, // 1 help + INLINE_COLOR_ANSI216(0, 3, 5), // 2 string/integer/number literals (sky blue) + INLINE_CYAN, // 3 symbol + INLINE_MAGENTA, // 4 keyword + INLINE_GRAY_ANSI(12) // 5 comment (mid-level gray) }; tokentype help[] = { TOKEN_QUESTION }; From 319738876311444450727586d434a9aabc2ec16f Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:00:11 -0500 Subject: [PATCH 19/63] Check terminal width correctly --- src/cli.c | 1 + src/help.c | 5 ++++- src/inline.c | 18 ++++++++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/cli.c b/src/cli.c index aa37b9e..44f9bf9 100644 --- a/src/cli.c +++ b/src/cli.c @@ -41,6 +41,7 @@ char *cli_globalsrc=NULL; void inline_setutf8(void); void inline_emitcolor(int color); void inline_emit(const char *seq); +bool inline_getterminalwidth(int *width); /* ********************************************************************** * Utility functions diff --git a/src/help.c b/src/help.c index f49b1ea..c8705cd 100644 --- a/src/help.c +++ b/src/help.c @@ -158,9 +158,12 @@ objecthelptopic *help_search(char *query) { * Display help * ********************************************************************** */ +bool inline_getterminalwidth(int *width); + /** Display a topic list */ void help_topiclist(dictionary *dict, inline_editor *edit) { - int width = 80 /*linedit_getwidth(edit)*/, max = 0; + int width = 80, max = 0; + inline_getterminalwidth(&width); objectlist list = MORPHO_STATICLIST; varray_valueinit(&list.val); diff --git a/src/inline.c b/src/inline.c index bf7a080..43a1b33 100644 --- a/src/inline.c +++ b/src/inline.c @@ -279,25 +279,31 @@ static bool inline_checksupported(void) { return true; // Windows and other terminals are supported } -/** Update the terminal width */ -static void inline_updateterminalwidth(inline_editor *edit) { - int width = 80; // fallback - +/** Read the width from the terminal */ +bool inline_getterminalwidth(int *width) { #ifdef _WIN32 HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO csbi; if (GetConsoleScreenBufferInfo(h, &csbi)) { - width = csbi.srWindow.Right - csbi.srWindow.Left + 1; + *width=csbi.srWindow.Right - csbi.srWindow.Left + 1; + return true; } #else struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 && ws.ws_col > 0) { - width = ws.ws_col; + *width=ws.ws_col; + return true; } #endif + return false; +} +/** Update the terminal width */ +static void inline_updateterminalwidth(inline_editor *edit) { + int width = 80; // fallback + inline_getterminalwidth(&width); edit->ncols = width; } From e33194f0975375e982b5c17026b623dffec4f94c Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:26:34 -0500 Subject: [PATCH 20/63] Color operators --- src/cli.c | 22 +++++++++++++++++----- src/inline.c | 24 +++++++++++++++++++----- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/cli.c b/src/cli.c index 44f9bf9..95b3450 100644 --- a/src/cli.c +++ b/src/cli.c @@ -39,8 +39,8 @@ char *cli_globalsrc=NULL; #define UNDERLINE "\x1B[4m" void inline_setutf8(void); -void inline_emitcolor(int color); void inline_emit(const char *seq); +void inline_emitcolor(int color); bool inline_getterminalwidth(int *width); /* ********************************************************************** @@ -164,17 +164,28 @@ void cli_debuggercallbackfn(vm *v, void *ref) { int palette[] = { CLI_DEFAULTCOLOR, // 0 default INLINE_YELLOW, // 1 help - INLINE_COLOR_ANSI216(0, 3, 5), // 2 string/integer/number literals (sky blue) - INLINE_CYAN, // 3 symbol + INLINE_COLOR_ANSI216(0, 1, 5), // 2 string/integer/number literals (darker blue, visible on both backgrounds) + INLINE_COLOR_ANSI216(0, 5, 5), // 3 symbol (bright cyan/teal, distinct from blue) INLINE_MAGENTA, // 4 keyword - INLINE_GRAY_ANSI(12) // 5 comment (mid-level gray) + INLINE_GRAY_ANSI(12), // 5 comment (mid-level gray) + INLINE_COLOR_ANSI216(5, 3, 0) // 6 operator (amber) }; tokentype help[] = { TOKEN_QUESTION }; tokentype literal[] = { TOKEN_STRING, TOKEN_INTERPOLATION, TOKEN_INTEGER, TOKEN_NUMBER, TOKEN_IMAG }; tokentype symbols[] = { TOKEN_SYMBOL }; tokentype keywords[] = { TOKEN_TRUE, TOKEN_FALSE, TOKEN_NIL, TOKEN_SELF, TOKEN_SUPER, TOKEN_PRINT, TOKEN_VAR, TOKEN_IF, TOKEN_ELSE, TOKEN_IN, TOKEN_WHILE, TOKEN_FOR, TOKEN_DO, TOKEN_BREAK, TOKEN_CONTINUE, TOKEN_FUNCTION, - TOKEN_RETURN, TOKEN_CLASS, TOKEN_IMPORT, TOKEN_AS, TOKEN_IS, TOKEN_VAR, TOKEN_WITH, TOKEN_TRY, TOKEN_CATCH }; + TOKEN_RETURN, TOKEN_CLASS, TOKEN_IMPORT, TOKEN_AS, TOKEN_IS, TOKEN_WITH, TOKEN_TRY, TOKEN_CATCH }; +tokentype operators[] = { + TOKEN_PLUS, TOKEN_MINUS, TOKEN_STAR, TOKEN_SLASH, TOKEN_CIRCUMFLEX, + TOKEN_PLUSPLUS, TOKEN_MINUSMINUS, + TOKEN_PLUSEQ, TOKEN_MINUSEQ, TOKEN_STAREQ, TOKEN_SLASHEQ, + TOKEN_HASH, TOKEN_AT, + TOKEN_EXCLAMATION, TOKEN_AMP, TOKEN_VBAR, TOKEN_DBLAMP, TOKEN_DBLVBAR, + TOKEN_EQUAL, TOKEN_EQ, TOKEN_NEQ, + TOKEN_LT, TOKEN_GT, TOKEN_LTEQ, TOKEN_GTEQ, + TOKEN_DOTDOT, TOKEN_DOTDOTDOT +}; /** Checks if match matches any tokentype in a given list */ static bool matchtokentype(tokentype match, size_t n, tokentype *list) { @@ -249,6 +260,7 @@ bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspa else if (matchtokentype(tok.type, sizeof(literal)/sizeof(literal[0]), literal)) out->color=2; else if (matchtokentype(tok.type, sizeof(symbols)/sizeof(symbols[0]), symbols)) out->color=3; else if (matchtokentype(tok.type, sizeof(keywords)/sizeof(keywords[0]), keywords)) out->color=4; + else if (matchtokentype(tok.type, sizeof(operators)/sizeof(operators[0]), operators)) out->color=6; } success=(tok.type!=TOKEN_EOF); } diff --git a/src/inline.c b/src/inline.c index 43a1b33..f2ea73f 100644 --- a/src/inline.c +++ b/src/inline.c @@ -138,6 +138,7 @@ static bool inline_insert(inline_editor *edit, const char *bytes, size_t nbytes) static void inline_clear(inline_editor *edit); static void inline_clearselection(inline_editor *edit); static void inline_clearsuggestions(inline_editor *edit); +static bool inline_stringwidth(inline_editor *edit, const char *str, int *width); /* ----------------------- * New/free API @@ -307,6 +308,13 @@ static void inline_updateterminalwidth(inline_editor *edit) { edit->ncols = width; } +/** Update viewport width based on current terminal width (preserves viewport position) */ +static void inline_updateviewportwidth(inline_editor *edit) { + int prompt_width; + if (!inline_stringwidth(edit, edit->prompt, &prompt_width)) prompt_width = 0; + edit->viewport.screen_cols = edit->ncols - prompt_width - 1; // Reserve last col to avoid pending wrap state +} + /* ---------------------------------------- * Handle crashes * ---------------------------------------- */ @@ -1112,9 +1120,7 @@ static void inline_initviewport(inline_editor *edit) { edit->viewport.first_visible_line = 0; edit->viewport.first_visible_col = 0; edit->viewport.screen_rows = 1; // Will adjust for multiline editing later - int prompt_width; - if (!inline_stringwidth(edit, edit->prompt, &prompt_width)) prompt_width = 0; - edit->viewport.screen_cols = edit->ncols - prompt_width - 1; // Reserve last col to avoid pending wrap state + inline_updateviewportwidth(edit); } /** Compute logical cursor position in rows and columns */ @@ -1468,6 +1474,7 @@ static bool inline_readkeyevent(KEY_EVENT_RECORD *k) { if (!ReadConsoleInputW(hIn, &rec, 1, &nread)) return false; if (rec.EventType == WINDOW_BUFFER_SIZE_EVENT) { + resize_pending = 1; if (inline_lasteditor) inline_lasteditor->refresh = true; continue; } @@ -2103,10 +2110,17 @@ static void inline_supported(inline_editor *edit) { while (inline_readkeypress(edit, &key)) { if (!inline_processkeypress(edit, &key)) break; - if (edit->refresh || resize_pending) { + if (resize_pending) { + /* Update terminal width and viewport on resize */ + inline_updateterminalwidth(edit); + inline_updateviewportwidth(edit); + edit->refresh = true; // Ensure we redraw after resize + resize_pending = 0; + } + + if (edit->refresh) { inline_redraw(edit); edit->refresh = false; - resize_pending = 0; } } From 8501c88b7287e969e79ceeb92e31a625fbb2e0fb Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:38:41 -0500 Subject: [PATCH 21/63] --no-color option --- src/cli.c | 32 +++++++++++++++++++------------- src/cli.h | 5 +++-- src/debugger.c | 2 +- src/main.c | 12 ++++++++++-- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/cli.c b/src/cli.c index 95b3450..6bb14c0 100644 --- a/src/cli.c +++ b/src/cli.c @@ -165,7 +165,7 @@ int palette[] = { CLI_DEFAULTCOLOR, // 0 default INLINE_YELLOW, // 1 help INLINE_COLOR_ANSI216(0, 1, 5), // 2 string/integer/number literals (darker blue, visible on both backgrounds) - INLINE_COLOR_ANSI216(0, 5, 5), // 3 symbol (bright cyan/teal, distinct from blue) + INLINE_COLOR_ANSI216(0, 3, 4), // 3 symbol (darker cyan/teal, distinct from blue) INLINE_MAGENTA, // 4 keyword INLINE_GRAY_ANSI(12), // 5 comment (mid-level gray) INLINE_COLOR_ANSI216(5, 3, 0) // 6 operator (amber) @@ -355,14 +355,14 @@ void cli(clioptions opt) { * ********************************************************************** */ /** Set up runtime context (VM, compiler, program, editor, callbacks) */ -static runtime_t cli_newruntime(void) { +static runtime_t cli_newruntime(clioptions opt) { runtime_t rt = { NULL, NULL, NULL, NULL }; rt.p = morpho_newprogram(); rt.c = morpho_newcompiler(rt.p); rt.v = morpho_newvm(); rt.edit = inline_new(CLI_PROMPT); - inline_syntaxcolor(rt.edit, cli_syntaxcolorfn, &rt.l); + if (!(opt & CLI_NOCOLOR)) inline_syntaxcolor(rt.edit, cli_syntaxcolorfn, &rt.l); morpho_setinputfn(rt.v, cli_inputcallbackfn, NULL); morpho_setprintfn(rt.v, cli_printcallbackfn, &rt.edit); @@ -390,7 +390,7 @@ static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { if (success) { if (opt & CLI_DISASSEMBLE) { if (opt & CLI_DISASSEMBLESHOWSRC) { - cli_disassemblewithsrc(rt->p, (char *)src); + cli_disassemblewithsrc(rt->p, (char *)src, opt); } else { morpho_disassemble(rt->v, rt->p, NULL); } @@ -418,7 +418,7 @@ static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { /** Compile and run source string (no file). Used by -e / --eval. */ void cli_runstring(const char *src, clioptions opt) { - runtime_t rt = cli_newruntime(); + runtime_t rt = cli_newruntime(opt); cli_globalsrc = (char *)src; cli_compileandrun(&rt, src, opt); @@ -435,7 +435,7 @@ static void cli_repl(runtime_t *rt, clioptions opt) { bool own_runtime = (rt == NULL); runtime_t runtime_storage; if (own_runtime) { - runtime_storage = cli_newruntime(); + runtime_storage = cli_newruntime(opt); rt = &runtime_storage; } @@ -458,7 +458,9 @@ static void cli_repl(runtime_t *rt, clioptions opt) { /* Configure editor for REPL (if not already configured) */ inline_setpalette(rt->edit, sizeof(palette)/sizeof(palette[0]), palette); - inline_syntaxcolor(rt->edit, cli_syntaxcolorfn, &rt->l); + if (!(opt & CLI_NOCOLOR)) { + inline_syntaxcolor(rt->edit, cli_syntaxcolorfn, &rt->l); + } inline_multiline(rt->edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); inline_autocomplete(rt->edit, cli_complete, NULL); #ifdef CLI_USELIBUNISTRING @@ -528,7 +530,7 @@ static void cli_repl(runtime_t *rt, clioptions opt) { * ********************************************************************** */ void cli_run(const char *in, clioptions opt) { - runtime_t rt = cli_newruntime(); + runtime_t rt = cli_newruntime(opt); char *src = cli_loadsource(in); if (src) cli_globalsrc = src; @@ -625,11 +627,13 @@ static void cli_printline(inline_editor *edit, int line, char *prompt, const cha } /** Disassembles the program showing syntax colored lines of source */ -void cli_disassemblewithsrc(program *p, char *src) { +void cli_disassemblewithsrc(program *p, char *src, clioptions opt) { inline_editor *edit = inline_new(""); if (!edit) return; lexer l; - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); + if (!(opt & CLI_NOCOLOR)) { + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); + } int line=1, length=0; for (unsigned int i=0; src[i]!='\0'; i++) { @@ -645,13 +649,15 @@ void cli_disassemblewithsrc(program *p, char *src) { } /** Displays a source listing from source lines start to end */ -void cli_list(const char *src, int start, int end) { +void cli_list(const char *src, int start, int end, clioptions opt) { if (src) { inline_editor *edit = inline_new(""); if (!edit) return; lexer l; - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); - inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); + if (!(opt & CLI_NOCOLOR)) { + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); + inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); + } int line=1, length=0; for (unsigned int i=0; src[i]!='\0'; i++) { diff --git a/src/cli.h b/src/cli.h index d6ecaf9..344c3d1 100644 --- a/src/cli.h +++ b/src/cli.h @@ -38,6 +38,7 @@ #define CLI_OPTIMIZE (1<<4) #define CLI_PROFILE (1<<5) #define CLI_INTERACTIVE (1<<6) +#define CLI_NOCOLOR (1<<7) typedef unsigned int clioptions; @@ -52,7 +53,7 @@ void cli(clioptions opt); char *cli_loadsource(const char *in); char *cli_loadstdin(void); -void cli_disassemblewithsrc(program *p, char *src); -void cli_list(const char *in, int start, int end); +void cli_disassemblewithsrc(program *p, char *src, clioptions opt); +void cli_list(const char *in, int start, int end, clioptions opt); #endif /* cli_h */ diff --git a/src/debugger.c b/src/debugger.c index 2fe3e3f..26cb010 100644 --- a/src/debugger.c +++ b/src/debugger.c @@ -89,7 +89,7 @@ void clidebugger_list(clidebugger *debug, int *nlines) { int start = line - n, end = line + n; if (start<0) start = 0; - cli_list(src, start, end); + cli_list(src, start, end, 0); // Use default (color enabled) for debugger if (in) MORPHO_FREE(src); } } diff --git a/src/main.c b/src/main.c index c3cb6a5..8c81a5d 100644 --- a/src/main.c +++ b/src/main.c @@ -55,6 +55,7 @@ static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ct #ifdef MORPHO_PROFILER printf(" -profile, --profile Enable profiling\n"); #endif + printf(" --no-color Disable syntax highlighting\n"); printf(" -w, --workers Set number of worker threads\n"); printf("\nIf no file is specified, morpho enters interactive REPL mode.\n"); printf("If stdin is piped or redirected, morpho reads and executes from stdin.\n"); @@ -112,6 +113,12 @@ static bool opt_workers(const char *opt, const char *arg, clioptions *flags, opt return true; } +static bool opt_nocolor(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_NOCOLOR; + return true; +} + static bool opt_check(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; (void)arg; (void)ctx; *flags &= ~CLI_RUN; /* Clear RUN flag - compile only, don't execute */ @@ -125,12 +132,12 @@ static bool opt_interactive(const char *opt, const char *arg, clioptions *flags, } static bool opt_list(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { - (void)opt; (void)flags; (void)ctx; + (void)opt; (void)ctx; if (!arg) return false; char *src = cli_loadsource(arg); if (src) { - cli_list(src, 1, INT_MAX); + cli_list(src, 1, INT_MAX, *flags); MORPHO_FREE(src); } else fprintf(stderr, "morpho: Could not open file '%s'\n", arg); return false; // Don't run program after listing @@ -165,6 +172,7 @@ static const option_t opt_table[] = { #ifdef MORPHO_PROFILER { "-profile", "--profile", false, opt_profile }, #endif + { NULL, "--no-color", false, opt_nocolor }, { "-v", "--version", false, opt_version }, { "-w", "--workers", false, opt_workers }, { NULL, NULL, false, NULL }, From 6cf06b34ca9514f314664e60313ec2a092018697 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:56:11 -0500 Subject: [PATCH 22/63] CLI output ensure terminal support has been checked --- src/cli.c | 6 +++++- src/inline.c | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cli.c b/src/cli.c index 6bb14c0..e3c600e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -42,6 +42,7 @@ void inline_setutf8(void); void inline_emit(const char *seq); void inline_emitcolor(int color); bool inline_getterminalwidth(int *width); +bool inline_checksupported(void); /* ********************************************************************** * Utility functions @@ -57,11 +58,13 @@ void cli_emitemphasis(int emph) { } } +bool is_supported = false; + /** Displays several strings with a specified style using linedit */ void cli_displaywithstyle(int col, int emph, int n, ...) { va_list args; va_start(args, n); - bool is_tty = inline_checktty(); // Only emit escape codes if stdout is a TTY + bool is_tty = inline_checktty() && is_supported; // Only emit escape codes if stdout is a TTY for (int i=0; i Date: Fri, 13 Feb 2026 18:55:11 -0500 Subject: [PATCH 23/63] Correct passing of args to morpho runtime --- src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.c b/src/main.c index 8c81a5d..186b483 100644 --- a/src/main.c +++ b/src/main.c @@ -217,7 +217,7 @@ int main(int argc, const char *argv[]) { if (run) { clidebugger_initialize(); - if (i < argc) morpho_setargs(argc - i - 1, argv + i); // Pass unused args to morpho + if (i < argc) morpho_setargs(argc - i, argv + i); // Pass unused args to morpho if (file) { cli_run(file, opt); From 573762c3e05bb70d26717ac49e906d1a12a80470 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Fri, 13 Feb 2026 19:25:02 -0500 Subject: [PATCH 24/63] Remove profiler qualifiers --- src/main.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main.c b/src/main.c index 186b483..df76f79 100644 --- a/src/main.c +++ b/src/main.c @@ -52,9 +52,7 @@ static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ct printf(" -dl Disassemble with source listing\n"); printf(" -debug, --debug Enable debugger\n"); printf(" -O, --optimize Enable optimizations\n"); -#ifdef MORPHO_PROFILER printf(" -profile, --profile Enable profiling\n"); -#endif printf(" --no-color Disable syntax highlighting\n"); printf(" -w, --workers Set number of worker threads\n"); printf("\nIf no file is specified, morpho enters interactive REPL mode.\n"); @@ -96,9 +94,7 @@ static bool opt_optimize(const char *opt, const char *arg, clioptions *flags, op static bool opt_profile(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; (void)arg; (void)ctx; -#ifdef MORPHO_PROFILER *flags |= CLI_PROFILE; -#endif return true; } @@ -169,9 +165,7 @@ static const option_t opt_table[] = { { "-i", "--interactive", false, opt_interactive }, { "-l", "--list", true, opt_list }, { "-O", "--optimize", false, opt_optimize }, -#ifdef MORPHO_PROFILER { "-profile", "--profile", false, opt_profile }, -#endif { NULL, "--no-color", false, opt_nocolor }, { "-v", "--version", false, opt_version }, { "-w", "--workers", false, opt_workers }, From 5d80eb70e9184bf58dcee75cb942eb228e2aef8f Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Sat, 14 Feb 2026 08:05:54 -0500 Subject: [PATCH 25/63] Clean up terminal code --- src/cli.c | 16 +++---------- src/inline.h | 67 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/cli.c b/src/cli.c index e3c600e..07efed1 100644 --- a/src/cli.c +++ b/src/cli.c @@ -29,26 +29,16 @@ char *cli_globalsrc=NULL; #define CLI_BUFFERSIZE 4096 -#define BLU "\x1B[34m" -#define CYN "\x1B[36m" -#define GRY "\x1B[38;2;128;128;128m" -#define RESET "\x1B[0m" - +#define RESET "\x1B[0m" #define BOLD "\x1B[1m" #define ITALIC "\x1B[3m" #define UNDERLINE "\x1B[4m" -void inline_setutf8(void); -void inline_emit(const char *seq); -void inline_emitcolor(int color); -bool inline_getterminalwidth(int *width); -bool inline_checksupported(void); - /* ********************************************************************** * Utility functions * ********************************************************************** */ -void cli_emitemphasis(int emph) { +void inline_emitemphasis(int emph) { switch (emph) { case CLI_NOEMPHASIS: inline_emit(RESET); break; case CLI_BOLD: inline_emit(BOLD); break; @@ -68,7 +58,7 @@ void cli_displaywithstyle(int col, int emph, int n, ...) { for (int i=0; i=0x01000000 → RGB packed as 0x01RRGGBB @@ -197,7 +194,7 @@ void inline_autocomplete(inline_editor *edit, inline_completefn fn, void *ref); * @param[in] edit Line editor to configure. * @param[in] fn Multiline callback. * @param[in] ref User-supplied reference pointer. - * @param[in] continuation_prompt Prompt to use for continuation lines; this is copied immediately and you may free/modify after. + * @param[in] continuation_prompt Prompt to use for continuation lines; this is copied immediately and you may free/modify after. * @returns true on success; false otherwise */ bool inline_multiline(inline_editor *edit, inline_multilinefn fn, void *ref, const char *continuation_prompt); @@ -211,13 +208,37 @@ void inline_setgraphemesplitter(inline_editor *edit, inline_graphemefn fn); * @param[in] fn Grapheme display width callback. */ void inline_setgraphemewidth(inline_editor *edit, inline_widthfn fn); -/** @brief Display a UTF-8 string using syntax coloring. - * @param[in] edit Line editor to use. - * @param[in] string UTF-8 string to display.*/ -void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string); +/* ********************************************************************** + * Terminal helpers + * ********************************************************************** */ /** @brief Check whether stdin and stdout are TTYs. * @returns true if both stdin and stdout are terminals. */ bool inline_checktty(void); +/** @brief Check is the terminal is supported (i.e. likely capable of processed output) + * @returns true if supported. */ +bool inline_checksupported(void); + +/** @brief Gets the current width of the terminal. + * @param[out] width - the width of the terminal, only set if successfully retrieved. + * @returns true on success. */ +bool inline_getterminalwidth(int *width); + +/** @brief Set UTF8 mode. */ +void inline_setutf8(void); + +/** @brief Emits a string to stdout. + * @param[in] str - String to emit.*/ +void inline_emit(const char *str); + +/** @brief Emits a terminal control code to stdout corresponding to a given palette color. + * @param[in] color - Color to emit in the format used for inline_setpalette above.*/ +void inline_emitcolor(int color); + +/** @brief Display a UTF-8 string using syntax coloring. + * @param[in] edit Line editor to use. + * @param[in] string UTF-8 string to display.*/ +void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string); + #endif /* INLINE_H */ From 344c8c1b329dfb9d42f0bc53fe143779312e3ecf Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Sat, 14 Feb 2026 08:28:52 -0500 Subject: [PATCH 26/63] Remove unused variables --- src/cli.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cli.c b/src/cli.c index 07efed1..7def252 100644 --- a/src/cli.c +++ b/src/cli.c @@ -121,7 +121,6 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { /** Print callback */ void cli_printcallbackfn(vm *v, void *ref, char *string) { - inline_editor *l = (inline_editor *) ref; cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_BOLD, 1, string); } @@ -280,8 +279,6 @@ const char *cli_complete(const char *in, void *ref, size_t *index) { /* Now try to match the token against a library of words */ len=strlen(tok); - int success=false; - for (size_t i=*index; words[i]!=NULL; i++) { if ( (len Date: Sat, 14 Feb 2026 15:57:20 -0500 Subject: [PATCH 27/63] Updated inline.c --- src/inline.c | 454 +++++++++++++++++++++++++++------------------------ 1 file changed, 240 insertions(+), 214 deletions(-) diff --git a/src/inline.c b/src/inline.c index a7fcf95..32bdbf4 100644 --- a/src/inline.c +++ b/src/inline.c @@ -24,7 +24,7 @@ #include #include #include - #include + #include #endif #define INLINE_DEFAULT_BUFFER_SIZE 128 @@ -32,7 +32,7 @@ #define INLINE_ESCAPECODE_MAXLENGTH 32 -#define INLINE_INVALID -1 +#define INLINE_INVALID -1 #define INLINE_TAB_WIDTH 2 @@ -40,7 +40,7 @@ #ifdef _WIN32 typedef DWORD termstate_t; -#else +#else typedef struct termios termstate_t; #endif @@ -65,8 +65,8 @@ typedef struct { /** The editor data structure */ typedef struct inline_editor { - char *prompt; - char *continuation_prompt; + char *prompt; + char *continuation_prompt; int ncols; // Number of columns @@ -75,7 +75,7 @@ typedef struct inline_editor { size_t buffer_size; // Size of buffer allocated in bytes char *clipboard; // Clipboard buffer - size_t clipboard_len; // Length of contents in bytes + size_t clipboard_len; // Length of contents in bytes size_t clipboard_size; // Size of clipboard in bytes size_t *graphemes; // Offset to each grapheme @@ -87,7 +87,7 @@ typedef struct inline_editor { size_t line_size; // Size of line buffer in bytes int cursor_posn; // Position of cursor in graphemes - int selection_posn; // Selection posn in graphemes + int selection_posn; // Selection posn in graphemes int term_cursor_row; // Record the cursor's physical row int term_lines_drawn; // Record how many lines were previously drawn @@ -98,7 +98,7 @@ typedef struct inline_editor { int palette_count; // Length of palette list inline_completefn complete_fn; // Autocomplete callback - void *complete_ref; // User reference + void *complete_ref; // User reference inline_stringlist_t suggestions; // List of suggestions from autocompleter bool suggestion_shown; // Set if renderer was able to show a suggestion @@ -109,26 +109,26 @@ typedef struct inline_editor { inline_graphemefn grapheme_fn; // Custom grapheme splitter inline_widthfn width_fn; // Custom grapheme width function - inline_stringlist_t history; // List of history entries + inline_stringlist_t history; // List of history entries int max_history_length; // Maximum length of the history inline_viewport viewport; // Terminal viewport -#ifdef _WIN32 // Preserve terminal state - termstate_t termstate_in; - termstate_t termstate_out; -#else - termstate_t termstate; -#endif +#ifdef _WIN32 // Preserve terminal state + termstate_t termstate_in; + termstate_t termstate_out; +#else + termstate_t termstate; +#endif bool rawmode_enabled; // Record if rawmode has already been enabled bool refresh; // Set to refresh on next redraw -} inline_editor; +} inline_editor; static inline_editor *inline_lasteditor = NULL; // Forward declarations -static char *inline_strdup(const char *s); +static char *inline_strdup(const char *s); static void inline_disablerawmode(inline_editor *edit); static void inline_stringlist_init(inline_stringlist_t *list); static void inline_stringlist_clear(inline_stringlist_t *list); @@ -172,7 +172,7 @@ inline_editor *inline_new(const char *prompt) { inline_new_cleanup: inline_free(edit); - return NULL; + return NULL; } /** API function to free a line editor and associated resources */ @@ -192,7 +192,7 @@ void inline_free(inline_editor *edit) { free(edit->palette); - if (inline_lasteditor==edit) inline_lasteditor = NULL; + if (inline_lasteditor==edit) inline_lasteditor = NULL; free(edit); } @@ -220,7 +220,7 @@ bool inline_setpalette(inline_editor *edit, int count, const int *palette) { memcpy(edit->palette, palette, sizeof(int) * count); edit->palette_count = count; - return true; + return true; } /** API function to enable autocomplete */ @@ -244,16 +244,16 @@ bool inline_multiline(inline_editor *edit, inline_multilinefn fn, void *ref, con /** API function to use a custom grapheme splitter */ void inline_setgraphemesplitter(inline_editor *edit, inline_graphemefn fn) { - edit->grapheme_fn = fn; + edit->grapheme_fn = fn; } /** API function to use a custom grapheme width function */ void inline_setgraphemewidth(inline_editor *edit, inline_widthfn fn) { - edit->width_fn = fn; + edit->width_fn = fn; } /* ********************************************************************** - * Platform-dependent code + * Platform-dependent code * ********************************************************************** */ /* ---------------------------------------- @@ -287,14 +287,14 @@ bool inline_getterminalwidth(int *width) { CONSOLE_SCREEN_BUFFER_INFO csbi; if (GetConsoleScreenBufferInfo(h, &csbi)) { - *width=csbi.srWindow.Right - csbi.srWindow.Left + 1; + if (width) *width=csbi.srWindow.Right - csbi.srWindow.Left + 1; return true; } #else struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 && ws.ws_col > 0) { - *width=ws.ws_col; + if (width) *width=ws.ws_col; return true; } #endif @@ -303,15 +303,15 @@ bool inline_getterminalwidth(int *width) { /** Update the terminal width */ static void inline_updateterminalwidth(inline_editor *edit) { - int width = 80; // fallback + int width = 80; // fallback inline_getterminalwidth(&width); edit->ncols = width; } /** Update viewport width based on current terminal width (preserves viewport position) */ static void inline_updateviewportwidth(inline_editor *edit) { - int prompt_width; - if (!inline_stringwidth(edit, edit->prompt, &prompt_width)) prompt_width = 0; + int prompt_width; + if (!inline_stringwidth(edit, edit->prompt, &prompt_width)) prompt_width = 0; edit->viewport.screen_cols = edit->ncols - prompt_width - 1; // Reserve last col to avoid pending wrap state } @@ -325,10 +325,10 @@ static void inline_atexitrestore(void) { #ifdef _WIN32 static bool termstate_set = false; -static termstate_t termstate_in; +static termstate_t termstate_in; static termstate_t termstate_out; static int resize_pending = 0; -static bool consolehandler_installed = false; +static bool consolehandler_installed = false; static BOOL WINAPI inline_consolehandler(DWORD ctrl) { (void) ctrl; if (termstate_set) { @@ -339,7 +339,7 @@ static BOOL WINAPI inline_consolehandler(DWORD ctrl) { } return FALSE; // Allow default behavior } -#else +#else termstate_t termstate; static volatile sig_atomic_t termstate_set = 0; static volatile sig_atomic_t resize_pending = 0; @@ -365,11 +365,11 @@ static bool inline_callprevious(int sig, siginfo_t *info, void *ucontext) { return true; } #endif - if (handler->previous.sa_handler) { - handler->previous.sa_handler(sig); - return true; + if (handler->previous.sa_handler) { + handler->previous.sa_handler(sig); + return true; } - return false; + return false; } static void inline_restoredisposition(int sig) { @@ -378,7 +378,7 @@ static void inline_restoredisposition(int sig) { if (handler && handler->has_previous && handler->previous.sa_handler != SIG_IGN) { sigaction(sig, &handler->previous, NULL); } else { - struct sigaction restore; + struct sigaction restore; memset(&restore, 0, sizeof(restore)); restore.sa_handler = SIG_DFL; sigemptyset(&restore.sa_mask); @@ -398,13 +398,13 @@ static void inline_signalgracefulhandler(int sig, siginfo_t *info, void *ucontex if (inline_callprevious(sig, info, ucontext)) return; // If the previous signal handler was called and returned, we do too inline_restoredisposition(sig); kill(getpid(), sig); - _Exit(128 + sig); + _Exit(128 + sig); } static void inline_signalcrashhandler(int sig, siginfo_t *info, void *ucontext) { inline_emergencyrestore(); inline_restoredisposition(sig); kill(getpid(), sig); - _Exit(128 + sig); + _Exit(128 + sig); } static signalhandlerstate_t siglist[] = { @@ -424,18 +424,18 @@ signalhandlerstate_t *inline_findsighandler(int sig) { } #endif -static int install_count = 0; +static int install_count = 0; /** Register emergency exit and signal handlers */ static void inline_registeremergencyhandlers(void) { install_count++; - if (install_count>1) return; + if (install_count>1) return; static bool atexit_registered=false; if (!atexit_registered) { atexit(inline_atexitrestore); atexit_registered=true; } -#ifdef _WIN32 - if (SetConsoleCtrlHandler(inline_consolehandler, TRUE)) consolehandler_installed=true; -#else +#ifdef _WIN32 + if (SetConsoleCtrlHandler(inline_consolehandler, TRUE)) consolehandler_installed=true; +#else #ifndef INLINE_NO_SIGNALS struct sigaction sa; memset(&sa, 0, sizeof(sa)); @@ -454,28 +454,28 @@ static void inline_registeremergencyhandlers(void) { sa.sa_flags = siglist[i].flags; if (sigaction(siglist[i].sig, &sa, NULL) == 0) siglist[i].installed=true; } - #endif + #endif #endif } /** Restore emergency handlers previously installed */ static void inline_restoreemergencyhandlers(void) { if (install_count>0) install_count--; - if (install_count>0) return; + if (install_count>0) return; #ifdef _WIN32 - if (consolehandler_installed) - if (SetConsoleCtrlHandler(inline_consolehandler, FALSE)) consolehandler_installed = false; -#else + if (consolehandler_installed) + if (SetConsoleCtrlHandler(inline_consolehandler, FALSE)) consolehandler_installed = false; +#else #ifndef INLINE_NO_SIGNALS for (size_t i = 0; i < sizeof(siglist)/sizeof(siglist[0]); i++) { if (!siglist[i].has_previous || !siglist[i].installed) continue; sigaction(siglist[i].sig, &siglist[i].previous, NULL); // Restore previous handler - siglist[i].installed = false; // Wipe + siglist[i].installed = false; // Wipe siglist[i].has_previous = false; memset(&siglist[i].previous, 0, sizeof(siglist[i].previous)); } - #endif + #endif #endif } @@ -495,8 +495,8 @@ void inline_setutf8(void) { static bool inline_enablerawmode(inline_editor *edit) { if (edit->rawmode_enabled) return true; -#ifdef _WIN32 - HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); +#ifdef _WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); DWORD mode = 0; if (!GetConsoleMode(hIn, &mode)) return false; edit->termstate_in = mode; @@ -504,17 +504,17 @@ static bool inline_enablerawmode(inline_editor *edit) { mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; if (!SetConsoleMode(hIn, mode)) return false; // Disable cooked mode - HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); if (!GetConsoleMode(hOut, &edit->termstate_out)) return false; DWORD newOut = edit->termstate_out | ENABLE_VIRTUAL_TERMINAL_PROCESSING; if (!SetConsoleMode(hOut, newOut)) return false; // Enable VT output - if (!termstate_set) { - termstate_in = edit->termstate_in; + if (!termstate_set) { + termstate_in = edit->termstate_in; termstate_out = edit->termstate_out; termstate_set = true; } -#else +#else if (tcgetattr(STDIN_FILENO, &edit->termstate) == -1) return false; struct termios raw = edit->termstate; @@ -536,8 +536,8 @@ static bool inline_enablerawmode(inline_editor *edit) { raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) return false; - if (!termstate_set) { - termstate = edit->termstate; + if (!termstate_set) { + termstate = edit->termstate; termstate_set = true; } #endif @@ -557,7 +557,7 @@ static void inline_disablerawmode(inline_editor *edit) { HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleMode(hIn, edit->termstate_in); SetConsoleMode(hOut, edit->termstate_out); -#else +#else tcsetattr(STDIN_FILENO, TCSAFLUSH, &edit->termstate); #endif @@ -595,12 +595,12 @@ static bool inline_extendbufferby(inline_editor *edit, size_t extra) { size_t newcap = edit->buffer_size ? edit->buffer_size : INLINE_DEFAULT_BUFFER_SIZE; while (newcap < required) { - if (newcap > SIZE_MAX / 2) return false; + if (newcap > SIZE_MAX / 2) return false; newcap *= 2; // Grow exponentially } void *p = realloc(edit->buffer, newcap); - if (!p) return false; + if (!p) return false; edit->buffer = p; edit->buffer_size = newcap; @@ -662,40 +662,47 @@ static size_t inline_matchcodepoint(size_t table_count, const codepoint_t *table return 0; // no match } -/** Minimal grapheme splitter */ +/** Minimal heuristic grapheme splitter */ static size_t inline_graphemesplit(const char *in, const char *end) { - const unsigned char *p = (const unsigned char *) in, - *uend = (const unsigned char *) end; - if (p >= uend) return 0; // At end already + const unsigned char *p = (const unsigned char *)in; + const unsigned char *uend = (const unsigned char *)end; + if (p >= uend) return 0; - // Read first codepoint + // Decode first codepoint size_t len = inline_utf8length(*p); - if (len == 0) len = 1; // Recover from malformed utf8 codepoint - if ((size_t)(uend - p) < len) return (size_t)(uend - p); + if (len == 0) len = 1; + if ((size_t)(uend - p) < len) return (size_t)(uend - p); + + const unsigned char *prev = p; // remember start of previous codepoint p += len; - // Combining diacritical marks U+0300–U+036F (accents, etc.) - while (p < uend && *p >= 0xCC && *p <= 0xCF) { + while (p < uend && *p >= 0xCC && *p <= 0xCF) { // Combining marks len = inline_utf8length(*p); if (len == 0 || (size_t)(uend - p) < len) break; + prev = p; p += len; } - do { // Skip past suffix extenders + do { // Suffix extenders len = inline_matchcodepoint(suffix_count, suffix_extenders, p, uend); - p += len; - } while (len!=0); + if (len) { + prev = p; + p += len; + } + } while (len != 0); - for (;;) { // Joiners (ZWJ sequences) - len = inline_matchcodepoint(joiners_count, joiners, p, uend); - if (len == 0) break; + for (;;) { // ZWJ joiners — only join if prev and next are non-ASCII + len = inline_matchcodepoint(joiners_count, joiners, p, uend); // Check for ZWJ p += len; + if (p >= uend || len ==0) break; - if (p >= uend) break; + size_t next_len = inline_utf8length(*p); // Decode next codepoint + if (next_len == 0 || (size_t)(uend - p) < next_len) break; - len = inline_utf8length((unsigned char)*p); // Process joined codepoint - if (len == 0 || (size_t)(uend - p) < len) break; - p += len; + if (*prev < 0x80 || *p < 0x80) break; // Only join if both sides are non-ASCII + + prev = p; + p += next_len; } return (size_t)(p - (const unsigned char *)in); @@ -713,7 +720,7 @@ static void inline_recomputegraphemes(inline_editor *edit) { if (required_bytes > edit->grapheme_size) { size_t newsize = (edit->grapheme_size ? edit->grapheme_size : INLINE_DEFAULT_BUFFER_SIZE); while (newsize < required_bytes) { - if (newsize > SIZE_MAX / 2) return; + if (newsize > SIZE_MAX / 2) return; newsize *= 2; } @@ -736,7 +743,7 @@ static void inline_recomputegraphemes(inline_editor *edit) { while (p < end) { // Walk the buffer and record grapheme boundaries edit->graphemes[count++] = (size_t)(p - edit->buffer); size_t len = fn(p, end); - if (len == 0) len = 1; // Malformed grapheme + if (len == 0) len = 1; // Malformed grapheme if (len > (size_t)(end - p)) len = (size_t)(end - p); // Size longer than buffer p += len; } @@ -750,7 +757,7 @@ static inline void inline_graphemerange(inline_editor *edit, int i, size_t *star if (i < 0 || i >= edit->grapheme_count) { // Handle out of bounds access (incl. i representing end of line) if (start) *start = edit->buffer_len; if (end) *end = edit->buffer_len; - return; + return; } if (start) *start = edit->graphemes[i]; @@ -819,9 +826,9 @@ static bool inline_checkextenders(const unsigned char *g, size_t len) { /** Predict the display width of a grapheme */ static int inline_graphemewidth(const char *p, size_t len) { - const unsigned char *g = (const unsigned char *) p; + const unsigned char *g = (const unsigned char *) p; if (!len) return 0; - if (g[0] == '\t') return INLINE_TAB_WIDTH; // Tab + if (g[0] == '\t') return INLINE_TAB_WIDTH; // Tab if (g[0] < 0x80) return 1; // ASCII fast path if (len >= 2 && (g[0] == 0xCC || g[0] == 0xCD)) return 0; // Combining-only grapheme (rare) @@ -888,14 +895,14 @@ static void inline_stringlist_init(inline_stringlist_t *list) { static bool inline_stringlist_add(inline_stringlist_t *list, const char *s) { if (!s) return false; // Never add a null pointer char *copy = inline_strdup(s); - if (!copy) return false; + if (!copy) return false; char **newitems = realloc(list->items, sizeof(char*) * (list->count + 1)); if (!newitems) { free(copy); return false; } // Don't update if realloc fails list->items = newitems; - list->items[list->count] = copy; + list->items[list->count] = copy; list->count++; - return true; + return true; } /** Removes and frees the first element of the stringlist; */ @@ -953,8 +960,8 @@ static bool inline_selectionrange(inline_editor *edit, int *sel_l, int *sel_r, s int l = imin(edit->selection_posn, edit->cursor_posn); int r = imax(edit->selection_posn, edit->cursor_posn); - if (sel_l) *sel_l = l; - if (sel_r) *sel_r = r; + if (sel_l) *sel_l = l; + if (sel_r) *sel_r = r; if (start) inline_graphemerange(edit, l, start, NULL); if (end) inline_graphemerange(edit, r, end, NULL); @@ -968,16 +975,16 @@ static bool inline_selectionrange(inline_editor *edit, int *sel_l, int *sel_r, s /** Copies a string of given length onto the clipboard. */ static bool inline_copytoclipboard(inline_editor *edit, const char *string, size_t length) { if (!string || length==0) { // Empty clipboard - if (edit->clipboard) edit->clipboard[0] = '\0'; + if (edit->clipboard) edit->clipboard[0] = '\0'; edit->clipboard_len = 0; - return true; + return true; } size_t needed = length + 1; // Check if we have sufficient capacity and realloc if necessary - if (needed > edit->clipboard_size) { + if (needed > edit->clipboard_size) { size_t newsize = edit->clipboard_size ? edit->clipboard_size : INLINE_DEFAULT_BUFFER_SIZE; while (newsize < needed) { - if (newsize > SIZE_MAX / 2) return false; + if (newsize > SIZE_MAX / 2) return false; newsize *= 2; } @@ -991,7 +998,7 @@ static bool inline_copytoclipboard(inline_editor *edit, const char *string, size memmove(edit->clipboard, string, length); // Copy onto clipboard edit->clipboard[length] = '\0'; // Ensure null termination edit->clipboard_len = length; - return true; + return true; } /* ---------------------------------------- @@ -1051,7 +1058,7 @@ static void inline_advancesuggestions(inline_editor *edit, int delta) { /** Set the history length. */ void inline_sethistorylength(inline_editor *edit, int maxlen) { - edit->max_history_length=maxlen; + edit->max_history_length=maxlen; if (maxlen > 0) { // Remove excess entries if necessary while (edit->history.count > maxlen) inline_stringlist_popfront(&edit->history); @@ -1070,13 +1077,13 @@ bool inline_addhistory(inline_editor *edit, const char *entry) { } inline_stringlist_add(&edit->history, entry); - if (edit->max_history_length > 0 && edit->history.count > edit->max_history_length) inline_stringlist_popfront(&edit->history); - return true; + if (edit->max_history_length > 0 && edit->history.count > edit->max_history_length) inline_stringlist_popfront(&edit->history); + return true; } /** Advances the current history */ static void inline_advancehistory(inline_editor *edit, int delta) { - int count = inline_stringlist_count(&edit->history); + int count = inline_stringlist_count(&edit->history); if (count == 0) return; // Enter history mode if we're not in it @@ -1107,7 +1114,7 @@ static void inline_reset(inline_editor *edit) { inline_endhistorybrowsing(edit); inline_stringlist_clear(&edit->suggestions); edit->rawmode_enabled = false; - edit->term_cursor_row = 0; + edit->term_cursor_row = 0; edit->term_lines_drawn = 0; } @@ -1130,7 +1137,7 @@ static void inline_cursorposn(inline_editor *edit, int *out_row, int *out_col) { int row = 0; // Find the row containing the cursor while (row + 1 < edit->line_count && edit->lines[row + 1] <= byte_pos) row++; - if (out_row) *out_row = row; + if (out_row) *out_row = row; // The column is found by subtracting the grapheme offset of the start of the row if (out_col) *out_col = edit->cursor_posn - inline_findgraphemeindex(edit, edit->lines[row]); } @@ -1222,7 +1229,7 @@ static inline void inline_clipgraphemerange(inline_editor *edit, int line_start, if (start < 0) start = *g_end; // Clamp if line is empty or viewport is beyond end if (end < start) end = start; - else if (end > start && edit->buffer[edit->graphemes[end-1]] == '\n') end--; + else if (end > start && edit->buffer[edit->graphemes[end-1]] == '\n') end--; *g_start = start; *g_end = end; @@ -1256,24 +1263,24 @@ static inline void inline_moveby(int dx, int dy) { } } -/** Render a single line of text +/** Render a single line of text * @param[in] - edit - the editor * @param[in] - prompt - prompt for this line * @param[in] - byte_start - byte offset for the start of the line * @param[in] - byte_end - byte offset for the end of the line * @param[in] - logical_cursor_col - column the cursor should be displayed in logical coordinates, or -1 if not on this line - * @param[in] - is_last - whether this is the last line - * @param[out] - rendered_cursor_col - if logical_cursor_col indicates the cursor is on this line, - * set to logical column the cursor should be rendered on, incuding clipping + * @param[in] - is_last - whether this is the last line + * @param[out] - rendered_cursor_col - if logical_cursor_col indicates the cursor is on this line, + * set to logical column the cursor should be rendered on, incuding clipping * and prompt widt, or -1 if outside clipping window; otherwise not changed. */ -static void inline_renderline(inline_editor *edit, const char *prompt, size_t byte_start, size_t byte_end, +static void inline_renderline(inline_editor *edit, const char *prompt, size_t byte_start, size_t byte_end, int logical_cursor_col, bool is_last, int *rendered_cursor_col) { write(STDOUT_FILENO, prompt, (unsigned int) strlen(prompt)); // Write prompt int prompt_width = 0; // Calculate its display width - if (!inline_stringwidth(edit, prompt, &prompt_width)) prompt_width = 0; + if (!inline_stringwidth(edit, prompt, &prompt_width)) prompt_width = 0; int rendered_width = prompt_width; // Track rendered width - int rendered_cursor_posn = -1; + int rendered_cursor_posn = -1; // Compute selection bounds, if active int sel_l = INLINE_INVALID, sel_r = INLINE_INVALID; @@ -1336,7 +1343,7 @@ static void inline_renderline(inline_editor *edit, const char *prompt, size_t by selection_on = in_selection; } - if (edit->buffer[gs] == '\n') break; + if (edit->buffer[gs] == '\n') break; if (logical_cursor_col >= 0 && // Check if this grapheme was where the cursor is line_start + logical_cursor_col == g) rendered_cursor_posn = rendered_width; @@ -1355,13 +1362,13 @@ static void inline_renderline(inline_editor *edit, const char *prompt, size_t by // Ghosted suggestion suffix (only if at right edge on last line) if (is_last && g_end == edit->grapheme_count && logical_cursor_col >= 0) { const char *suffix = inline_currentsuggestion(edit); - edit->suggestion_shown=false; + edit->suggestion_shown=false; if (suffix && *suffix) { int remaining_cols = edit->viewport.screen_cols - rendered_width; // Width of suggestion int ghost_width = 0; - if (!inline_stringwidth(edit, suffix, &ghost_width)) ghost_width = 0; + if (!inline_stringwidth(edit, suffix, &ghost_width)) ghost_width = 0; if (ghost_width <= remaining_cols) { // Show suggestion as faint text edit->suggestion_shown=true; @@ -1397,7 +1404,7 @@ static void inline_redraw(inline_editor *edit) { inline_emit("\r"); // Move cursor to start of line inline_renderline(edit, (i==0 ? edit->prompt : edit->continuation_prompt), // prompt - byte_start, byte_end, + byte_start, byte_end, (cursor_row == i ? cursor_col : -1), // cursor column if on this line is_last, // whether we're on the last line or not &rendered_cursor_col ); @@ -1408,7 +1415,7 @@ static void inline_redraw(inline_editor *edit) { int extra = (edit->term_lines_drawn > edit->line_count ? edit->term_lines_drawn - edit->line_count : 0); for (int i = 0; i < extra; i++) { inline_emit("\n\r"); - inline_emit(TERM_CLEAR); + inline_emit(TERM_CLEAR); } write(STDOUT_FILENO, "\r", 1); // Move to start of line @@ -1430,7 +1437,7 @@ void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string) { } size_t offset = 0; - while (offset < len) { // + while (offset < len) { // inline_colorspan_t span = { .byte_end = offset, .color=-1}; bool ok = edit->syntax_fn(string, edit->syntax_ref, offset, &span); // Obtain next span @@ -1464,7 +1471,7 @@ void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string) { /** Type that represents a single unit of input */ typedef unsigned char rawinput_t; -#ifdef _WIN32 +#ifdef _WIN32 static bool inline_readkeyevent(KEY_EVENT_RECORD *k) { INPUT_RECORD rec; DWORD nread; @@ -1537,19 +1544,22 @@ static int inline_translatekeypress(const KEY_EVENT_RECORD *k, unsigned char out } } + // Alt characters + int i=0; + if (mods & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) { + out[i++] = '\x1b'; // Prefix character with esc + } + if (wc != 0) { // Unicode if (wc < 0x80) { - out[0] = (unsigned char)wc; - return 1; + out[i++] = (unsigned char)wc; } else if (wc < 0x800) { - out[0] = (unsigned char)(0xC0 | ((unsigned int)wc >> 6)); - out[1] = (unsigned char)(0x80 | ((unsigned int)wc & 0x3F)); - return 2; - } else if (wc < 0xD800 || wc > 0xDFFF) { - out[0] = 0xE0 | (wc >> 12); - out[1] = 0x80 | ((wc >> 6) & 0x3F); - out[2] = 0x80 | (wc & 0x3F); - return 3; + out[i++] = (unsigned char)(0xC0 | ((unsigned int)wc >> 6)); + out[i++] = (unsigned char)(0x80 | ((unsigned int)wc & 0x3F)); + } else if (wc < 0xD800 || wc > 0xDFFF) { + out[i++] = 0xE0 | (wc >> 12); + out[i++] = 0x80 | ((wc >> 6) & 0x3F); + out[i++] = 0x80 | (wc & 0x3F); } else if (wc >= 0xD800 && wc <= 0xDBFF) { // high surrogate // Need the next KEY_EVENT for the low surrogate KEY_EVENT_RECORD next; @@ -1558,16 +1568,15 @@ static int inline_translatekeypress(const KEY_EVENT_RECORD *k, unsigned char out WCHAR wc2 = next.uChar.UnicodeChar; if (wc2 >= 0xDC00 && wc2 <= 0xDFFF) { uint32_t cp = 0x10000 + (((wc - 0xD800) << 10) | (wc2 - 0xDC00)); - out[0] = (unsigned char) (0xF0 | (cp >> 18)); - out[1] = (unsigned char) (0x80 | ((cp >> 12) & 0x3F)); - out[2] = (unsigned char) (0x80 | ((cp >> 6) & 0x3F)); - out[3] = (unsigned char) (0x80 | (cp & 0x3F)); - return 4; + out[i++] = (unsigned char) (0xF0 | (cp >> 18)); + out[i++] = (unsigned char) (0x80 | ((cp >> 12) & 0x3F)); + out[i++] = (unsigned char) (0x80 | ((cp >> 6) & 0x3F)); + out[i++] = (unsigned char) (0x80 | (cp & 0x3F)); } } } - return 0; // Unknown key → ignore + return i; // Return } #endif @@ -1606,12 +1615,12 @@ static bool inline_readraw(rawinput_t *out) { /** Identifies the type of keypress */ typedef enum { KEY_UNKNOWN, KEY_CHARACTER, - KEY_RETURN, KEY_TAB, KEY_SHIFT_TAB, KEY_DELETE, + KEY_RETURN, KEY_CTRL_RETURN, KEY_TAB, KEY_SHIFT_TAB, KEY_DELETE, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, // Arrow keys KEY_HOME, KEY_END, // Home and End KEY_PAGE_UP, KEY_PAGE_DOWN, // Page up and page down KEY_SHIFT_LEFT, KEY_SHIFT_RIGHT, // Shift+arrow key - KEY_CTRL + KEY_CTRL, KEY_ALT // Ctrl, meta keys } keytype_t; /** A single keypress event obtained and processed by the terminal */ @@ -1622,15 +1631,30 @@ typedef struct { } keypress_t; static void inline_keypressunknown(keypress_t *keypress) { - keypress->type=KEY_UNKNOWN; - keypress->c[0]='\0'; - keypress->nbytes=0; + keypress->type=KEY_UNKNOWN; + keypress->c[0]='\0'; + keypress->nbytes=0; } static void inline_keypresswithchar(keypress_t *keypress, keytype_t type, char c) { - keypress->type=type; + keypress->type=type; keypress->c[0]=c; keypress->c[1]='\0'; - keypress->nbytes=1; + keypress->nbytes=1; +} + +/** Decode sequence of characters into a utf8 character */ +static void inline_decode_utf8(unsigned char first, keypress_t *out) { + out->nbytes = inline_utf8length(first); + + if (!out->nbytes) return; // Invalid first byte or stray continuation + + out->c[0] = first; + for (int i=1; inbytes; i++) { + if (!inline_readraw(&out->c[i])) { out->c[i] = '\0'; return; } + } + + out->c[out->nbytes] = '\0'; + out->type = KEY_CHARACTER; } /** Map from terminal codes to keytype_t */ @@ -1658,17 +1682,22 @@ static void inline_decode_escape(keypress_t *out) { int i = 0; out->type = KEY_UNKNOWN; - // Expect '[' - if (!inline_readraw(&seq[i]) || seq[0] != '[') { return; } + if (!inline_readraw(&seq[i])) return; // Read byte after esc - // Read until alpha terminator + if (seq[0] !='[') { // Is this an alt + char combo? + inline_decode_utf8(seq[0], out); + out->type=KEY_ALT; // Override type + return; + } + + // It's an escape code, so read until alpha terminator for (i = 1; i < INLINE_ESCAPECODE_MAXLENGTH - 1; i++) { if (!inline_readraw(&seq[i])) break; if (isalpha(seq[i]) || seq[i] == '~') break; } seq[i + 1] = '\0'; // Ensure null terminated - // Lookup escape code + // Lookup escape code for (size_t j = 0; j < sizeof(esc_table)/sizeof(esc_table[0]); j++) { if (strcmp((const char *)seq, esc_table[j].seq) == 0) { out->type = esc_table[j].type; @@ -1677,29 +1706,14 @@ static void inline_decode_escape(keypress_t *out) { } } -/** Decode sequence of characters into a utf8 character */ -static void inline_decode_utf8(unsigned char first, keypress_t *out) { - out->nbytes = inline_utf8length(first); - - if (!out->nbytes) return; // Invalid first byte or stray continuation - - out->c[0] = first; - for (int i=1; inbytes; i++) { - if (!inline_readraw(&out->c[i])) { out->c[i] = '\0'; return; } - } - - out->c[out->nbytes] = '\0'; - out->type = KEY_CHARACTER; -} - /** Raw control codes produced by POSIX terminals */ enum keycodes { - BACKSPACE_CODE = 8, // Backspace (Ctrl+H) - TAB_CODE = 9, // Tab + BACKSPACE_CODE = 8, // Backspace (Ctrl+H) + TAB_CODE = 9, // Tab LF_CODE = 10, // Line feed - RETURN_CODE = 13, // Enter / Return (CR) - ESC_CODE = 27, // Escape - DELETE_CODE = 127 // Delete (DEL) + RETURN_CODE = 13, // Enter / Return (CR) + ESC_CODE = 27, // Escape + DELETE_CODE = 127 // Delete (DEL) }; /** Decode raw input units into a keypress */ @@ -1710,7 +1724,7 @@ static void inline_decode(const rawinput_t *raw, keypress_t *out) { if (b < 32 || b == DELETE_CODE) { // Control keys (ASCII control range or DEL) switch (b) { case TAB_CODE: out->type = KEY_TAB; return; - case LF_CODE: return; + case LF_CODE: out->type = KEY_CTRL_RETURN; return; case RETURN_CODE: out->type = KEY_RETURN; return; case BACKSPACE_CODE: // v fallthrough case DELETE_CODE: out->type = KEY_DELETE; return; @@ -1718,7 +1732,7 @@ static void inline_decode(const rawinput_t *raw, keypress_t *out) { inline_decode_escape(out); return; - default: // Control codes are Ctrl+A → 1, Ctrl+Z → 26 + default: // Control codes are Ctrl+A → 1, Ctrl+Z → 26 if (b >= 1 && b <= 26) inline_keypresswithchar(out, KEY_CTRL, 'A' + (b - 1)); return; } @@ -1734,11 +1748,11 @@ static void inline_decode(const rawinput_t *raw, keypress_t *out) { /** Obtain a keypress event */ static bool inline_readkeypress(inline_editor *edit, keypress_t *out) { - (void) edit; + (void) edit; rawinput_t raw; if (!inline_readraw(&raw)) return false; inline_decode(&raw, out); - return true; + return true; } /* ********************************************************************** @@ -1749,7 +1763,7 @@ static bool inline_readkeypress(inline_editor *edit, keypress_t *out) { static inline void inline_setcursorposn(inline_editor *edit, int new_posn) { if (new_posn < 0) new_posn = 0; if (new_posn > edit->grapheme_count) new_posn = edit->grapheme_count; - if (edit->cursor_posn == new_posn) return; + if (edit->cursor_posn == new_posn) return; edit->refresh = true; int old_row; @@ -1761,11 +1775,11 @@ static inline void inline_setcursorposn(inline_editor *edit, int new_posn) { /** Insert text into the buffer */ static bool inline_insert(inline_editor *edit, const char *bytes, size_t nbytes) { - if (!inline_extendbufferby(edit, nbytes)) return false; // Ensure capacity + if (!inline_extendbufferby(edit, nbytes)) return false; // Ensure capacity size_t offset = 0; // Obtain the byte offset of the current cursor position if (edit->cursor_posn < edit->grapheme_count) offset = edit->graphemes[edit->cursor_posn]; - else offset = edit->buffer_len; + else offset = edit->buffer_len; // Move contents after the insertion point to make room for the inserted text memmove(edit->buffer + offset + nbytes, edit->buffer + offset, edit->buffer_len - offset); @@ -1773,15 +1787,13 @@ static bool inline_insert(inline_editor *edit, const char *bytes, size_t nbytes) memcpy(edit->buffer + offset, bytes, nbytes); // Copy new text into buffer edit->buffer_len += nbytes; edit->buffer[edit->buffer_len] = '\0'; // Ensure null-terminated - - int old_count = edit->grapheme_count; // Save grapheme count - inline_recomputegraphemes(edit); + inline_recomputegraphemes(edit); inline_recomputelines(edit); - // Move cursor forward by number of graphemes - int inserted_count = edit->grapheme_count - old_count; - inline_setcursorposn(edit, edit->cursor_posn + (inserted_count > 0? inserted_count : 0)); + // Move cursor to end of inserted text + int newpos = inline_findgraphemeindex(edit, offset + nbytes); + inline_setcursorposn(edit, newpos); edit->refresh = true; // Redraw return true; @@ -1813,7 +1825,7 @@ static void inline_deletegrapheme(inline_editor *edit, int index) { /** Deletes selected text */ static void inline_deleteselection(inline_editor *edit) { - int sel_l; + int sel_l; size_t start, end; if (!inline_selectionrange(edit, &sel_l, NULL, &start, &end)) return; @@ -1834,7 +1846,7 @@ static void inline_deletecurrent(inline_editor *edit) { static void inline_delete(inline_editor *edit) { if (edit->selection_posn != INLINE_INVALID) { inline_deleteselection(edit); - } else if (edit->cursor_posn > 0) { // Delete grapheme before cursor + } else if (edit->cursor_posn > 0) { // Delete grapheme before cursor inline_deletegrapheme(edit, edit->cursor_posn - 1); inline_setcursorposn(edit, edit->cursor_posn - 1); } else inline_deletecurrent(edit); @@ -1842,7 +1854,7 @@ static void inline_delete(inline_editor *edit) { /** Clear the buffer */ static void inline_clear(inline_editor *edit) { - edit->buffer_len = 0; // Clear text buffer + edit->buffer_len = 0; // Clear text buffer edit->buffer[0] = '\0'; inline_recomputegraphemes(edit); inline_recomputelines(edit); @@ -1875,12 +1887,12 @@ static void inline_pagedown(inline_editor *edit) { } static void inline_left(inline_editor *edit) { - if (edit->cursor_posn > 0) + if (edit->cursor_posn > 0) inline_setcursorposn(edit, edit->cursor_posn - 1); } static void inline_right(inline_editor *edit) { - if (edit->cursor_posn < edit->grapheme_count) + if (edit->cursor_posn < edit->grapheme_count) inline_setcursorposn(edit, edit->cursor_posn + 1); } @@ -1896,7 +1908,7 @@ static void inline_clearselection(inline_editor *edit) { /** Copy selected text */ static void inline_copyselection(inline_editor *edit) { size_t start, end; - if (inline_selectionrange(edit, NULL, NULL, &start, &end)) + if (inline_selectionrange(edit, NULL, NULL, &start, &end)) inline_copytoclipboard(edit, edit->buffer + start, end - start); } @@ -1909,7 +1921,7 @@ static void inline_cutselection(inline_editor *edit) { /** Cut part of a line */ static void inline_cutline(inline_editor *edit, bool before) { int row; - inline_cursorposn(edit, &row, NULL); + inline_cursorposn(edit, &row, NULL); size_t b_line = edit->lines[row + (before ? 0 : 1)]; // line break position before or after size_t b_cursor = edit->graphemes[edit->cursor_posn]; // Cursor position @@ -1953,7 +1965,7 @@ static void inline_transpose(inline_editor *edit) { if (!tmp) return; memcpy(tmp, edit->buffer + a_start, a_len); // Copy a into temporary buffer - memmove(edit->buffer + a_start, edit->buffer + b_start, b_len); // Copy b overwriting a + memmove(edit->buffer + a_start, edit->buffer + b_start, b_len); // Copy b overwriting a memcpy(edit->buffer + a_start + b_len, tmp, a_len); // Copy a from the temporary buffer free(tmp); @@ -1974,23 +1986,35 @@ static bool inline_processshortcut(inline_editor *edit, char c) { switch (c) { case 'A': inline_home(edit); break; case 'B': inline_left(edit); break; - case 'C': inline_copyselection(edit); break; - case 'D': + case 'C': inline_clear(edit); return false; // exit on Ctrl-C + case 'D': inline_clearselection(edit); - inline_deletecurrent(edit); - break; + inline_deletecurrent(edit); + break; case 'E': inline_end(edit); break; case 'F': inline_right(edit); break; case 'G': return false; // exit on Ctrl-G case 'K': inline_cutline(edit, false); break; // Cut to end of line - case 'L': inline_clear(edit); break; + case 'L': inline_clear(edit); break; case 'N': inline_historykey(edit, 1); break; // Next history + case 'O': inline_copyselection(edit); break; case 'P': inline_historykey(edit, -1); break; // Previous history - case 'T': inline_transpose(edit); break; + case 'T': inline_transpose(edit); break; case 'U': inline_cutline(edit, true); break; // Cut to start of line - case 'X': inline_cutselection(edit); break; + case 'X': inline_cutselection(edit); break; case 'Y': // v fallthrough - case 'V': inline_paste(edit); break; + case 'V': inline_paste(edit); break; + default: break; + } + edit->refresh = true; + return true; +} + +/** Handle Meta + _ shortcuts; upper case versions indicate Shift + Meta + _ */ +static bool inline_processmeta(inline_editor *edit, const unsigned char *c, int nbytes) { + (void) nbytes; + switch (*c) { + case 'w': case 'W': inline_copyselection(edit); break; default: break; } edit->refresh = true; @@ -2001,31 +2025,32 @@ static bool inline_processshortcut(inline_editor *edit, char c) { static bool inline_processkeypress(inline_editor *edit, const keypress_t *key) { bool generatesuggestions=true, clearselection=true, endbrowsing=true; switch (key->type) { - case KEY_RETURN: + case KEY_RETURN: if (!edit->multiline_fn || !edit->multiline_fn(edit->buffer, edit->multiline_ref)) return false; + case KEY_CTRL_RETURN: // v fallthrough if (!inline_insert(edit, "\n", 1)) return false; generatesuggestions = false; // newline shouldn't trigger suggestion break; case KEY_LEFT: inline_left(edit); break; - case KEY_RIGHT: + case KEY_RIGHT: if (edit->suggestion_shown) { inline_applysuggestion(edit); generatesuggestions = false; break; } - inline_right(edit); + inline_right(edit); break; - case KEY_SHIFT_LEFT: + case KEY_SHIFT_LEFT: inline_beginselection(edit); inline_left(edit); - clearselection=false; - break; - case KEY_SHIFT_RIGHT: + clearselection=false; + break; + case KEY_SHIFT_RIGHT: inline_beginselection(edit); inline_right(edit); - clearselection=false; - break; + clearselection=false; + break; case KEY_UP: inline_historykey(edit, -1); endbrowsing=false; @@ -2036,23 +2061,24 @@ static bool inline_processkeypress(inline_editor *edit, const keypress_t *key) { break; case KEY_HOME: inline_home(edit); break; case KEY_END: inline_end(edit); break; - case KEY_PAGE_UP: inline_pageup(edit); break; + case KEY_PAGE_UP: inline_pageup(edit); break; case KEY_PAGE_DOWN: inline_pagedown(edit); break; case KEY_DELETE: inline_delete(edit); break; case KEY_TAB: if (inline_havesuggestions(edit)) { inline_advancesuggestions(edit, 1); - generatesuggestions=false; + generatesuggestions=false; } else if (!inline_insert(edit, "\t", 1)) return false; - break; + break; case KEY_SHIFT_TAB: if (inline_havesuggestions(edit)) { inline_advancesuggestions(edit, -1); - generatesuggestions=false; + generatesuggestions=false; } - break; + break; case KEY_CTRL: return inline_processshortcut(edit, key->c[0]); - case KEY_CHARACTER: + case KEY_ALT: return inline_processmeta(edit, key->c, key->nbytes); + case KEY_CHARACTER: if (!inline_insert(edit, (char *) key->c, key->nbytes)) return false; break; case KEY_UNKNOWN: @@ -2101,12 +2127,12 @@ static void inline_unsupported(inline_editor *edit) { static void inline_supported(inline_editor *edit) { inline_reset(edit); inline_setutf8(); - if (!inline_enablerawmode(edit)) return; // Could not enter raw mode + if (!inline_enablerawmode(edit)) return; // Could not enter raw mode inline_updateterminalwidth(edit); inline_initviewport(edit); inline_redraw(edit); - keypress_t key; + keypress_t key; while (inline_readkeypress(edit, &key)) { if (!inline_processkeypress(edit, &key)) break; @@ -2118,9 +2144,9 @@ static void inline_supported(inline_editor *edit) { resize_pending = 0; } - if (edit->refresh) { - inline_redraw(edit); - edit->refresh = false; + if (edit->refresh) { + inline_redraw(edit); + edit->refresh = false; } } @@ -2129,7 +2155,7 @@ static void inline_supported(inline_editor *edit) { inline_redraw(edit); inline_disablerawmode(edit); - if (edit->buffer_len > 0) inline_addhistory(edit, edit->buffer); // Add to history if non-empty + if (edit->buffer_len > 0) inline_addhistory(edit, edit->buffer); // Add to history if non-empty write(STDOUT_FILENO, "\r\n", 2); } @@ -2140,7 +2166,7 @@ static void inline_supported(inline_editor *edit) { char *inline_readline(inline_editor *edit) { if (!edit) return NULL; - edit->buffer_len = 0; // Reset buffer + edit->buffer_len = 0; // Reset buffer edit->buffer[0] = '\0'; if (!inline_checktty()) { From f15cdcfdf2b9d2fac302fc7a9a4535716162697b Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:29:51 -0500 Subject: [PATCH 28/63] String interpolation fixed --- src/cli.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cli.c b/src/cli.c index 7def252..9533c95 100644 --- a/src/cli.c +++ b/src/cli.c @@ -225,6 +225,7 @@ static size_t detect_comment(const char *in, size_t offset) { bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspan_t *out) { bool success=false; lexer *l=(lexer *) ref; + if (!l) return false; // Check for comments first (before lexer processing) @@ -236,8 +237,9 @@ bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspa return true; // Successfully colored a comment } - lex_init(l, in+offset, 0); - + if (offset==0) lex_init(l, in, 0); + else l->current=in+offset; + token tok; error err; error_init(&err); @@ -257,7 +259,7 @@ bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspa success=(tok.type!=TOKEN_EOF); } - lex_clear(l); + if (tok.type==TOKEN_EOF) lex_clear(l); return success; } From 30a666ec82ad89d42b188e58fd709a1feba87af9 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:22:34 -0500 Subject: [PATCH 29/63] Update inline.h --- src/inline.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/inline.h b/src/inline.h index 4d37897..433ff69 100644 --- a/src/inline.h +++ b/src/inline.h @@ -62,7 +62,8 @@ typedef struct { } inline_colorspan_t; /** @brief Syntax coloring callback function, called repeatedly by - * the editor to obtain the next colored span. + * the editor to obtain the next colored span. The span is assumed + * to begin as offset. * @param[in] utf8 The full buffer encoded as UTF-8 to analyze. * @param[in] ref User-supplied reference pointer. * @param[in] offset Byte offset at which to begin scanning. @@ -242,3 +243,4 @@ void inline_emitcolor(int color); void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string); #endif /* INLINE_H */ + From f4d4e7e89bdb00b32455e65c0ce5e22f154c3290 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:10:04 -0500 Subject: [PATCH 30/63] Update inline to latest version --- src/inline.c | 231 +++++++++++++++++++++++++++------------------------ src/inline.h | 3 +- 2 files changed, 122 insertions(+), 112 deletions(-) diff --git a/src/inline.c b/src/inline.c index 32bdbf4..0d77bda 100644 --- a/src/inline.c +++ b/src/inline.c @@ -8,6 +8,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -620,6 +621,19 @@ static inline int inline_utf8length(unsigned char b) { return 0; // Invalid or continuation } +/** Decode utf8 into an integer */ +static inline uint32_t inline_utf8decode(const unsigned char *p) { + int len = inline_utf8length(*p); + switch (len) { + case 1: return p[0]; + case 2: return ((p[0] & 0x1F) << 6) | (p[1] & 0x3F); + case 3: return ((p[0] & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F); + case 4: return ((p[0] & 0x07) << 18) | ((p[1] & 0x3F) << 12) | ((p[2] & 0x3F) << 6) | (p[3] & 0x3F); + default: break; + } + return 0; +} + /** Codepoint definition */ typedef struct { const unsigned char *seq; @@ -642,12 +656,6 @@ static const codepoint_t suffix_extenders[] = { CODEPOINT("\xF0\x9F\x8F\xBF"), // dark skin tone }; static const size_t suffix_count = sizeof(suffix_extenders) / sizeof(suffix_extenders[0]); - -/* Joiner codepoints connect the next codepoint into the same grapheme */ -static const codepoint_t joiners[] = { - CODEPOINT("\xE2\x80\x8D"), // ZWJ (U+200D) -}; -static const size_t joiners_count = sizeof(joiners) / sizeof(joiners[0]); #undef CODEPOINT /** Matches a codepoint against a table of possible matches */ @@ -662,47 +670,80 @@ static size_t inline_matchcodepoint(size_t table_count, const codepoint_t *table return 0; // no match } -/** Minimal heuristic grapheme splitter */ +static bool inline_isextendedpictographic(uint32_t cp) { + if (cp >= 0x1F300 && cp <= 0x1FAFF) return true; // Emoji blocks + if (cp >= 0x2600 && cp <= 0x26FF) return true; // Misc symbols + if (cp >= 0x2700 && cp <= 0x27BF) return true; // Dingbats + if (cp == 0x1F3F3 || cp == 0x1F3F4) return true; // White/black flag + return false; +} + +static inline bool inline_isregionalindicator(uint32_t cp) { + return (cp >= 0x1F1E6 && cp <= 0x1F1FF); +} + +/** Heuristic grapheme splitter */ static size_t inline_graphemesplit(const char *in, const char *end) { const unsigned char *p = (const unsigned char *)in; const unsigned char *uend = (const unsigned char *)end; if (p >= uend) return 0; - // Decode first codepoint size_t len = inline_utf8length(*p); - if (len == 0) len = 1; - if ((size_t)(uend - p) < len) return (size_t)(uend - p); + if (len == 0 || (size_t)(uend - p) < len) len = 1; - const unsigned char *prev = p; // remember start of previous codepoint + uint32_t prev_cp = inline_utf8decode(p); // Set basepoint + bool prev_had_vs16 = false; p += len; while (p < uend && *p >= 0xCC && *p <= 0xCF) { // Combining marks len = inline_utf8length(*p); if (len == 0 || (size_t)(uend - p) < len) break; - prev = p; p += len; } - do { // Suffix extenders - len = inline_matchcodepoint(suffix_count, suffix_extenders, p, uend); - if (len) { - prev = p; - p += len; + while ((len = inline_matchcodepoint(suffix_count, suffix_extenders, p, uend)) != 0) { + if (len == 3 && p[0]==0xEF && p[1]==0xB8 && p[2]==0x8F) prev_had_vs16 = true; // VS16 + p += len; + } + + if (p < uend && inline_isregionalindicator(prev_cp)) { // Regional indicator pair + size_t next_len = inline_utf8length(*p); + if (next_len > 0 && (size_t)(uend - p) >= next_len) { + uint32_t next_cp = inline_utf8decode(p); + if (inline_isregionalindicator(next_cp)) { + p += next_len; + return (size_t)(p - (const unsigned char *)in); + } } - } while (len != 0); + } - for (;;) { // ZWJ joiners — only join if prev and next are non-ASCII - len = inline_matchcodepoint(joiners_count, joiners, p, uend); // Check for ZWJ - p += len; - if (p >= uend || len ==0) break; + for (;;) { // ZWJ chains + if ((size_t)(uend - p) < 3 || p[0]!=0xE2 || p[1]!=0x80 || p[2]!=0x8D) break; // ZWJ + p += 3; + if (p >= uend) break; - size_t next_len = inline_utf8length(*p); // Decode next codepoint + size_t next_len = inline_utf8length(*p); if (next_len == 0 || (size_t)(uend - p) < next_len) break; - if (*prev < 0x80 || *p < 0x80) break; // Only join if both sides are non-ASCII + uint32_t next_cp = inline_utf8decode(p); - prev = p; + if (!(inline_isextendedpictographic(prev_cp) || prev_had_vs16) || + !inline_isextendedpictographic(next_cp)) break; + + prev_cp = next_cp; // consume base + prev_had_vs16 = false; p += next_len; + + while (p < uend && *p >= 0xCC && *p <= 0xCF) { // Combining mark + next_len = inline_utf8length(*p); + if (next_len == 0 || (size_t)(uend - p) < next_len) break; + p += next_len; + } + + while ((next_len = inline_matchcodepoint(suffix_count, suffix_extenders, p, uend)) != 0) { + if (next_len == 3 && p[0]==0xEF && p[1]==0xB8 && p[2]==0x8F) prev_had_vs16 = true; + p += next_len; + } } return (size_t)(p - (const unsigned char *)in); @@ -815,42 +856,37 @@ static void inline_recomputelines(inline_editor *edit) { /** Check for ZWJ, VS16, keycap */ static bool inline_checkextenders(const unsigned char *g, size_t len) { - for (size_t i = 0; i + 2 < len; i++) { - unsigned char a = g[i], b = g[i+1], c = g[i+2]; - if (a == 0xE2 && b == 0x80 && c == 0x8D) return true; // ZWJ - if (a == 0xEF && b == 0xB8 && c == 0x8F) return true; // VS16 - if (a == 0xE2 && b == 0x83 && c == 0xA3) return true; // keycap + for (size_t i = 0; i < len; ) { + uint32_t cp = inline_utf8decode(g + i); + if (cp == 0x200D || cp == 0xFE0E || cp == 0xFE0F || cp == 0x20E3) return true; // ZWJ, VS15, VS16, Keycap + if (cp >= 0x1F3FB && cp <= 0x1F3FF) return true; // Skin tones + if (cp >= 0x1F9B0 && cp <= 0x1F9B3) return true; // Hair modifiers + i += inline_utf8length(g[i]); } return false; } /** Predict the display width of a grapheme */ static int inline_graphemewidth(const char *p, size_t len) { - const unsigned char *g = (const unsigned char *) p; if (!len) return 0; - if (g[0] == '\t') return INLINE_TAB_WIDTH; // Tab - if (g[0] < 0x80) return 1; // ASCII fast path - - if (len >= 2 && (g[0] == 0xCC || g[0] == 0xCD)) return 0; // Combining-only grapheme (rare) - if (inline_checkextenders(g, len)) return 2; // Check for ZWJ, VS16 and other extenders - if (len >= 2 && g[0] == 0xEF && (g[1] == 0xBC || g[1] == 0xBD)) return 2; // Fullwidth forms (U+FF00 block) + const unsigned char *g = (const unsigned char *)p; + uint32_t cp = inline_utf8decode(g); - if (len >= 4 && (g[0] & 0xF8) == 0xF0) { // Emoji block (U+1F300–U+1FAFF) - if ((g[1] & 0xC0) != 0x80 || (g[2] & 0xC0) != 0x80 || (g[3] & 0xC0) != 0x80) return 1; - unsigned cp = ((g[0] & 0x07) << 18) | ((g[1] & 0x3F) << 12) | - ((g[2] & 0x3F) << 6) | (g[3] & 0x3F); - if (cp >= 0x1F300 && cp <= 0x1FAFF) return 2; - } - - if (len >= 3 && g[0] >= 0xE4 && g[0] <= 0xE9) { // CJK Unified Ideographs (U+4E00–U+9FFF) - if ((g[1] & 0xC0) != 0x80 || (g[2] & 0xC0) != 0x80) return 1; - unsigned cp = ((g[0] & 0x0F) << 12) | ((g[1] & 0x3F) << 6) | (g[2] & 0x3F); - if (cp >= 0x4E00 && cp <= 0x9FFF) return 2; - } - - return 1; + if (cp == '\t') return INLINE_TAB_WIDTH; // Tab + if (cp < 0x80) return 1; // ASCII code + if (cp >= 0x0300 && cp <= 0x036F) return 0; // Combining marks + if (cp >= 0xAC00 && cp <= 0xD7A3) return 2; // Hangul syllables + if (cp >= 0xFF01 && cp <= 0xFF60) return 2; // Fullwidth forms + if (cp >= 0xFFE0 && cp <= 0xFFE6) return 2; + if (inline_isregionalindicator(cp) && // Regional indicator sequence + len > 4 && inline_isregionalindicator(inline_utf8decode(g + inline_utf8length(*g)))) return 2; + if (inline_checkextenders(g, len)) return 2; // Emoji extenders (ZWJ, VS16, skin tones, etc.) + if (cp >= 0x1F300 && cp <= 0x1FAFF) return 2; // Emoji + if (cp >= 0x4E00 && cp <= 0x9FFF) return 2; // CJK Unified Ideographs + return 1; // Anything else } + /** Calculate the display width of a utf8 string using current grapheme splitter/width estimator */ static bool inline_stringwidth(inline_editor *edit, const char *str, int *width) { inline_graphemefn split_fn = (edit->grapheme_fn ? edit->grapheme_fn : inline_graphemesplit); @@ -1379,7 +1415,7 @@ static void inline_renderline(inline_editor *edit, const char *prompt, size_t by } } - if (logical_cursor_col >= 0) { // Update cursor position if on this line + if (logical_cursor_col >= 0 && rendered_cursor_col) { // Update cursor position if on this line if (rendered_cursor_posn >= 0) *rendered_cursor_col = rendered_cursor_posn; else *rendered_cursor_col = rendered_width; // cursor at end } @@ -1436,27 +1472,20 @@ void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string) { return; } - size_t offset = 0; - while (offset < len) { // - inline_colorspan_t span = { .byte_end = offset, .color=-1}; - - bool ok = edit->syntax_fn(string, edit->syntax_ref, offset, &span); // Obtain next span - if (!ok || span.byte_end <= offset) { // No more spans or broken callback; print the rest uncolored - write(STDOUT_FILENO, string + offset, (unsigned int) (len - offset)); - return; - } - - if (span.color < edit->palette_count && span.color >= 0) inline_emitcolor(edit->palette[span.color]); - for (size_t i = offset; i < span.byte_end; i++) { - if (string[i] == '\t') { - for (int t = 0; t < INLINE_TAB_WIDTH; t++) inline_emit(" "); - } else write(STDOUT_FILENO, &string[i], 1); - } - - inline_emit(TERM_RESETFOREGROUND); + inline_reset(edit); + edit->viewport.screen_cols=INT_MAX; + inline_insert(edit, string, len); + inline_recomputegraphemes(edit); + inline_recomputelines(edit); - offset = span.byte_end; + for (int i = 0; i < edit->line_count; i++) { + size_t byte_start = edit->lines[i], byte_end = edit->lines[i+1]; + bool is_last = (i == edit->line_count - 1); + inline_renderline(edit, "", byte_start, byte_end, -1, is_last, NULL ); + if (i + 1 < edit->line_count) inline_emit("\n\r"); // Move to next line if not at end } + + inline_clear(edit); fflush(stdout); } @@ -1545,7 +1574,7 @@ static int inline_translatekeypress(const KEY_EVENT_RECORD *k, unsigned char out } // Alt characters - int i=0; + int i=0; if (mods & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) { out[i++] = '\x1b'; // Prefix character with esc } @@ -1576,7 +1605,7 @@ static int inline_translatekeypress(const KEY_EVENT_RECORD *k, unsigned char out } } - return i; // Return + return i; // Return } #endif @@ -1643,7 +1672,7 @@ static void inline_keypresswithchar(keypress_t *keypress, keytype_t type, char c } /** Decode sequence of characters into a utf8 character */ -static void inline_decode_utf8(unsigned char first, keypress_t *out) { +static void inline_decodeutf8input(unsigned char first, keypress_t *out) { out->nbytes = inline_utf8length(first); if (!out->nbytes) return; // Invalid first byte or stray continuation @@ -1685,7 +1714,7 @@ static void inline_decode_escape(keypress_t *out) { if (!inline_readraw(&seq[i])) return; // Read byte after esc if (seq[0] !='[') { // Is this an alt + char combo? - inline_decode_utf8(seq[0], out); + inline_decodeutf8input(seq[0], out); out->type=KEY_ALT; // Override type return; } @@ -1743,7 +1772,7 @@ static void inline_decode(const rawinput_t *raw, keypress_t *out) { return; } - inline_decode_utf8(b, out); // UTF8 + inline_decodeutf8input(b, out); // UTF8 } /** Obtain a keypress event */ @@ -1896,7 +1925,7 @@ static void inline_right(inline_editor *edit) { inline_setcursorposn(edit, edit->cursor_posn + 1); } -/** Selection */ +/** Selections */ static void inline_beginselection(inline_editor *edit) { if (edit->selection_posn==INLINE_INVALID) edit->selection_posn = edit->cursor_posn; } @@ -1989,7 +2018,7 @@ static bool inline_processshortcut(inline_editor *edit, char c) { case 'C': inline_clear(edit); return false; // exit on Ctrl-C case 'D': inline_clearselection(edit); - inline_deletecurrent(edit); + inline_deletecurrent(edit); break; case 'E': inline_end(edit); break; case 'F': inline_right(edit); break; @@ -2012,7 +2041,7 @@ static bool inline_processshortcut(inline_editor *edit, char c) { /** Handle Meta + _ shortcuts; upper case versions indicate Shift + Meta + _ */ static bool inline_processmeta(inline_editor *edit, const unsigned char *c, int nbytes) { - (void) nbytes; + (void) nbytes; switch (*c) { case 'w': case 'W': inline_copyselection(edit); break; default: break; @@ -2026,20 +2055,17 @@ static bool inline_processkeypress(inline_editor *edit, const keypress_t *key) { bool generatesuggestions=true, clearselection=true, endbrowsing=true; switch (key->type) { case KEY_RETURN: - if (!edit->multiline_fn || - !edit->multiline_fn(edit->buffer, edit->multiline_ref)) return false; + if (!edit->multiline_fn || !edit->multiline_fn(edit->buffer, edit->multiline_ref)) return false; case KEY_CTRL_RETURN: // v fallthrough if (!inline_insert(edit, "\n", 1)) return false; generatesuggestions = false; // newline shouldn't trigger suggestion break; - case KEY_LEFT: inline_left(edit); break; + case KEY_LEFT: inline_left(edit); break; case KEY_RIGHT: if (edit->suggestion_shown) { inline_applysuggestion(edit); generatesuggestions = false; - break; - } - inline_right(edit); + } else inline_right(edit); break; case KEY_SHIFT_LEFT: inline_beginselection(edit); @@ -2051,14 +2077,8 @@ static bool inline_processkeypress(inline_editor *edit, const keypress_t *key) { inline_right(edit); clearselection=false; break; - case KEY_UP: - inline_historykey(edit, -1); - endbrowsing=false; - break; - case KEY_DOWN: - inline_historykey(edit, +1); - endbrowsing=false; - break; + case KEY_UP: inline_historykey(edit, -1); endbrowsing=false; break; + case KEY_DOWN: inline_historykey(edit, +1); endbrowsing=false; break; case KEY_HOME: inline_home(edit); break; case KEY_END: inline_end(edit); break; case KEY_PAGE_UP: inline_pageup(edit); break; @@ -2076,13 +2096,12 @@ static bool inline_processkeypress(inline_editor *edit, const keypress_t *key) { generatesuggestions=false; } break; - case KEY_CTRL: return inline_processshortcut(edit, key->c[0]); - case KEY_ALT: return inline_processmeta(edit, key->c, key->nbytes); + case KEY_CTRL: return inline_processshortcut(edit, key->c[0]); + case KEY_ALT: return inline_processmeta(edit, key->c, key->nbytes); case KEY_CHARACTER: if (!inline_insert(edit, (char *) key->c, key->nbytes)) return false; break; - case KEY_UNKNOWN: - break; + case KEY_UNKNOWN: break; } if (clearselection) inline_clearselection(edit); @@ -2116,9 +2135,7 @@ static void inline_unsupported(inline_editor *edit) { inline_noterminal(edit); int length = (int)edit->buffer_len - 1; // Strip trailing control characters - while (length >= 0 && iscntrl((unsigned char)edit->buffer[length])) { - edit->buffer[length--] = '\0'; - } + while (length >= 0 && iscntrl((unsigned char)edit->buffer[length])) edit->buffer[length--] = '\0'; edit->buffer_len = length + 1; } @@ -2165,17 +2182,11 @@ static void inline_supported(inline_editor *edit) { * or NULL on error. */ char *inline_readline(inline_editor *edit) { if (!edit) return NULL; + inline_clear(edit); // Reset buffer - edit->buffer_len = 0; // Reset buffer - edit->buffer[0] = '\0'; - - if (!inline_checktty()) { - inline_noterminal(edit); - } else if (inline_checksupported()) { - inline_supported(edit); - } else { - inline_unsupported(edit); - } + if (!inline_checktty()) inline_noterminal(edit); + else if (inline_checksupported()) inline_supported(edit); + else inline_unsupported(edit); return (edit->buffer ? inline_strdup(edit->buffer) : NULL); } diff --git a/src/inline.h b/src/inline.h index 433ff69..64dbadb 100644 --- a/src/inline.h +++ b/src/inline.h @@ -62,7 +62,7 @@ typedef struct { } inline_colorspan_t; /** @brief Syntax coloring callback function, called repeatedly by - * the editor to obtain the next colored span. The span is assumed + * the editor to obtain the next colored span. The span is assumed * to begin as offset. * @param[in] utf8 The full buffer encoded as UTF-8 to analyze. * @param[in] ref User-supplied reference pointer. @@ -243,4 +243,3 @@ void inline_emitcolor(int color); void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string); #endif /* INLINE_H */ - From 44eb6e0dc842297c9498cd8e3d9a9fc72b207f64 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:14:52 -0500 Subject: [PATCH 31/63] Minimal use of new help system --- src/CMakeLists.txt | 2 +- src/cli.c | 19 +++++++++++++++++-- src/cli.h | 2 +- src/{help.c => hlp.c} | 22 ++++++++++++++++------ src/{help.h => hlp.h} | 16 ++++++++++------ 5 files changed, 45 insertions(+), 16 deletions(-) rename src/{help.c => hlp.c} (98%) rename src/{help.h => hlp.h} (84%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a5085d..c60133f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,7 +2,7 @@ target_sources(morpho6 PRIVATE cli.c cli.h debugger.c debugger.h - help.c help.h + hlp.c hlp.h inline.c inline.h main.c ) \ No newline at end of file diff --git a/src/cli.c b/src/cli.c index 9533c95..f40370e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -12,6 +12,8 @@ #include "cli.h" +#include +#include #include #include @@ -95,6 +97,7 @@ void cli_reporterror(error *err, vm *v) { /** Interactive help */ +#ifndef MORPHO_INCLUDE_HELP void cli_help(inline_editor *edit, char *query, error *err, bool avail) { char *q=query; if (help_querylength(q, NULL)==0) { @@ -114,6 +117,18 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { printf("No help found for '%s'\n", q); } } +#else +void cli_help(inline_editor *edit, char *query, error *err, bool avail) { + varray_char result; + varray_charinit(&result); + + morpho_helpastext(query, &result); + + if (result.count>0) printf("%s", result.data); + + varray_charclear(&result); +} +#endif /* ********************************************************************** * Morpho callbacks @@ -443,7 +458,7 @@ static void cli_repl(runtime_t *rt, clioptions opt) { printf("\U0001F98B morpho %s | \U0001F44B Type 'help' or '?' for help\n", morphoversionstring); } - bool help = help_initialize(); + bool help = hlp_initialize(); varray_char src; varray_charinit(&src); @@ -511,7 +526,7 @@ static void cli_repl(runtime_t *rt, clioptions opt) { } varray_charclear(&src); - help_finalize(); + hlp_finalize(); if (own_runtime) { cli_freeruntime(rt); diff --git a/src/cli.h b/src/cli.h index 344c3d1..37d57ca 100644 --- a/src/cli.h +++ b/src/cli.h @@ -12,7 +12,7 @@ #include #include "inline.h" -#include "help.h" +#include "hlp.h" #include "debugger.h" diff --git a/src/help.c b/src/hlp.c similarity index 98% rename from src/help.c rename to src/hlp.c index c8705cd..bc644f3 100644 --- a/src/help.c +++ b/src/hlp.c @@ -1,4 +1,4 @@ -/** @file help.c +/** @file hlp.c * @author T J Atherton * * @brief Interactive help system @@ -8,6 +8,12 @@ #include #include + +#include "cli.h" +#include "hlp.h" + +#ifndef MORPHO_INCLUDE_HELP + #include #include #include @@ -17,9 +23,6 @@ __declspec(dllimport) extern objecttype objectstringtype; __declspec(dllimport) extern objecttype objectlisttype; #endif -#include "cli.h" -#include "help.h" - /** The interactive help system uses a collection of Markdown files, located in * MORPHO_HELPFOLDER, that define available topics. Help files are all * valid Markdown, although only a subset is used, and the help system interprets @@ -489,7 +492,7 @@ bool help_findfiles(void) { /** Initializes the help system * @returns true if help is available */ -bool help_initialize(void) { +bool hlp_initialize(void) { objecthelptopictype=object_addtype(&objecthelptopicdefn); dictionary_init(&helpdict); @@ -498,7 +501,7 @@ bool help_initialize(void) { } /** Finalizes the help system */ -void help_finalize(void) { +void hlp_finalize(void) { while (topics) { objecthelptopic *c = topics; topics = c->next; @@ -507,3 +510,10 @@ void help_finalize(void) { dictionary_freecontents(&helpdict, true, false); dictionary_clear(&helpdict); } + +#else + +bool hlp_initialize(void) { return true; } +void hlp_finalize(void) { } + +#endif diff --git a/src/help.h b/src/hlp.h similarity index 84% rename from src/help.h rename to src/hlp.h index 2f71a15..cd4e4d2 100644 --- a/src/help.h +++ b/src/hlp.h @@ -1,11 +1,11 @@ -/** @file help.h +/** @file hlp.h * @author T J Atherton * * @brief Interactive help system */ -#ifndef help_h -#define help_h +#ifndef hlp_h +#define hlp_h #include @@ -14,6 +14,8 @@ #include "inline.h" +#ifndef MORPHO_INCLUDE_HELP + extern objecttype objecthelptopictype; #define OBJECT_HELPTOPIC objecthelptopictype @@ -35,7 +37,9 @@ size_t help_querylength(char *query, char **s); objecthelptopic *help_search(char *query); void help_display(inline_editor *edit, objecthelptopic *topic); -bool help_initialize(void); -void help_finalize(void); +#endif + +bool hlp_initialize(void); +void hlp_finalize(void); -#endif /* help_h */ +#endif /* hlp_h */ From 9591b61ee05e80579861fe5c9a9f9df55e8eb486 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:21:57 -0500 Subject: [PATCH 32/63] Now retrieve topic --- src/cli.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cli.c b/src/cli.c index f40370e..0373c15 100644 --- a/src/cli.c +++ b/src/cli.c @@ -122,9 +122,14 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { varray_char result; varray_charinit(&result); - morpho_helpastext(query, &result); + help_topic topic; - if (result.count>0) printf("%s", result.data); + if (morpho_helpastopic(query, &topic)) { + + } else { // Failed so retrieve a hint + help_queryhint(query, &result); + if (result.count>0) printf("%s", result.data); + } varray_charclear(&result); } From 52a38645ee369af2b6e19785ed57c60c92665d16 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:30:13 -0500 Subject: [PATCH 33/63] First go at new help display --- src/cli.c | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 6 deletions(-) diff --git a/src/cli.c b/src/cli.c index 0373c15..ce9828e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -118,19 +119,125 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { } } #else +/* --- Minimal help: render help_topic with inline highlighting --- */ + +/** Length of segment between delimiters (excluding closing delim). Returns 0 if no closer. */ +static size_t cli_help_segment(const char *s, char delim) { + const char *p = s; + while (*p != delim) { + if (*p == '\0') return 0; + p++; + } + return (size_t)(p - s); +} + +/** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. */ +static void cli_help_paraline(inline_editor *edit, const char *line) { + char buf[CLI_BUFFERSIZE]; + for (const char *c = line; *c != '\0'; ) { + if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { + putchar(c[1]); + c += 2; + continue; + } + if (*c == '`') { + c++; + size_t n = cli_help_segment(c, '`'); + if (n > 0 && n < sizeof(buf) - 1) { + memcpy(buf, c, n); + buf[n] = '\0'; + inline_displaywithsyntaxcoloring(edit, buf); + c += n + 1; + continue; + } + } else if (*c == '*' || *c == '_') { + char delim = *c; + c++; + size_t n = cli_help_segment(c, delim); + if (n > 0 && n < sizeof(buf) - 1) { + memcpy(buf, c, n); + buf[n] = '\0'; + cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_BOLD : CLI_UNDERLINE), 1, buf); + c += n + 1; + continue; + } + } + putchar(*c); + c++; + } +} + +/** Print a help_topic to the terminal with highlighting and emphasis. */ +static void cli_helptopic_print(inline_editor *edit, const help_topic *t) { + const md_file *file = t->file; + const char *src = file ? file->source : NULL; + size_t src_len = file ? file->sourcelen : 0; + if (!src || t->nblocks == 0) return; + + char buf[CLI_BUFFERSIZE]; + for (unsigned int i = 0; i < t->nblocks; i++) { + const md_block *b = &t->content_blocks[i]; + size_t start = b->span.start; + size_t len = b->span.length; + if (start >= src_len) continue; + if (start + len > src_len) len = src_len - start; + if (len >= sizeof(buf)) len = sizeof(buf) - 1; + memcpy(buf, src + start, len); + buf[len] = '\0'; + + switch (b->type) { + case MD_BLOCK_HEADER: { + const char *title = buf; + while (*title == '#' || (*title == ' ' && title < buf + len)) title++; + if (*title) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, title); + putchar('\n'); + break; + } + case MD_BLOCK_CODE: + inline_displaywithsyntaxcoloring(edit, buf); + if (len > 0 && buf[len - 1] != '\n') putchar('\n'); + break; + case MD_BLOCK_PARAGRAPH: + case MD_BLOCK_LIST: + for (char *line = buf; *line; ) { + char *eol = strchr(line, '\n'); + if (eol) { + *eol = '\0'; + cli_help_paraline(edit, line); + putchar('\n'); + line = eol + 1; + } else { + cli_help_paraline(edit, line); + putchar('\n'); + break; + } + } + break; + case MD_BLOCK_BLANK: + putchar('\n'); + break; + case MD_BLOCK_THEMATIC_BREAK: + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "---\n"); + break; + case MD_BLOCK_LINK_DEF: + break; + } + } +} + void cli_help(inline_editor *edit, char *query, error *err, bool avail) { varray_char result; varray_charinit(&result); - + (void)avail; + help_topic topic; - if (morpho_helpastopic(query, &topic)) { - - } else { // Failed so retrieve a hint + cli_helptopic_print(edit, &topic); + } else { help_queryhint(query, &result); - if (result.count>0) printf("%s", result.data); + if (result.count > 0) printf("%s", result.data); } - + (void)err; varray_charclear(&result); } #endif From 2c51bc7697b6dc041df9a3e24cacce8568e1b6ce Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:35:39 -0500 Subject: [PATCH 34/63] Strip leading spaces --- src/cli.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/cli.c b/src/cli.c index ce9828e..b015aa2 100644 --- a/src/cli.c +++ b/src/cli.c @@ -226,19 +226,22 @@ static void cli_helptopic_print(inline_editor *edit, const help_topic *t) { } void cli_help(inline_editor *edit, char *query, error *err, bool avail) { - varray_char result; - varray_charinit(&result); - (void)avail; - + char *q = query; + while (isspace(*q) && *q!='\0') q++; // Strip any leading space + help_topic topic; - if (morpho_helpastopic(query, &topic)) { + if (morpho_helpastopic(q, &topic)) { cli_helptopic_print(edit, &topic); } else { - help_queryhint(query, &result); - if (result.count > 0) printf("%s", result.data); + varray_char result; + varray_charinit(&result); + + help_queryhint(q, &result); + if (result.count > 0) printf("%s\n", result.data); + else printf("No help found for '%s'\n", q); + + varray_charclear(&result); } - (void)err; - varray_charclear(&result); } #endif From 6c27c6a1d37b9fec573e9179edb979e189c1c53b Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:43:40 -0500 Subject: [PATCH 35/63] Spacing between code lines --- src/cli.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/cli.c b/src/cli.c index b015aa2..12204f8 100644 --- a/src/cli.c +++ b/src/cli.c @@ -194,8 +194,20 @@ static void cli_helptopic_print(inline_editor *edit, const help_topic *t) { break; } case MD_BLOCK_CODE: + /* Blank line before code only when previous block is not blank and not code */ + if (i > 0) { + md_blocktype prev = t->content_blocks[i - 1].type; + if (prev != MD_BLOCK_BLANK && prev != MD_BLOCK_CODE) + putchar('\n'); + } inline_displaywithsyntaxcoloring(edit, buf); if (len > 0 && buf[len - 1] != '\n') putchar('\n'); + /* Blank line after code only when next block is not blank and not code */ + if (i + 1 < t->nblocks) { + md_blocktype next = t->content_blocks[i + 1].type; + if (next != MD_BLOCK_BLANK && next != MD_BLOCK_CODE) + putchar('\n'); + } break; case MD_BLOCK_PARAGRAPH: case MD_BLOCK_LIST: From 548d262d4b9b173ace0d26bc9750697e86198556 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:56:12 -0500 Subject: [PATCH 36/63] Clean up cli_matchdelimiter --- src/cli.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/cli.c b/src/cli.c index 12204f8..6d3519e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -122,12 +122,9 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { /* --- Minimal help: render help_topic with inline highlighting --- */ /** Length of segment between delimiters (excluding closing delim). Returns 0 if no closer. */ -static size_t cli_help_segment(const char *s, char delim) { - const char *p = s; - while (*p != delim) { - if (*p == '\0') return 0; - p++; - } +static size_t cli_matchdelimiter(const char *s, char delim) { + const char *p; + for (p = s; *p != delim; p++) if (*p == '\0') return 0; return (size_t)(p - s); } @@ -142,7 +139,7 @@ static void cli_help_paraline(inline_editor *edit, const char *line) { } if (*c == '`') { c++; - size_t n = cli_help_segment(c, '`'); + size_t n = cli_matchdelimiter(c, '`'); if (n > 0 && n < sizeof(buf) - 1) { memcpy(buf, c, n); buf[n] = '\0'; @@ -153,7 +150,7 @@ static void cli_help_paraline(inline_editor *edit, const char *line) { } else if (*c == '*' || *c == '_') { char delim = *c; c++; - size_t n = cli_help_segment(c, delim); + size_t n = cli_matchdelimiter(c, delim); if (n > 0 && n < sizeof(buf) - 1) { memcpy(buf, c, n); buf[n] = '\0'; From 17c94d2e03bb81cd22c9d517d638f633ac4f084a Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:05:58 -0500 Subject: [PATCH 37/63] Clean cli_displayline --- src/cli.c | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/cli.c b/src/cli.c index 6d3519e..3ce92eb 100644 --- a/src/cli.c +++ b/src/cli.c @@ -128,39 +128,34 @@ static size_t cli_matchdelimiter(const char *s, char delim) { return (size_t)(p - s); } +/** Copies span [src, src+n) into buf and null-terminates. Returns true if n < bufsize. */ +static bool cli_copyspan(const char *src, size_t n, char *buf, size_t bufsize) { + if (n >= bufsize) return false; + memcpy(buf, src, n); + buf[n] = '\0'; + return true; +} + /** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. */ -static void cli_help_paraline(inline_editor *edit, const char *line) { +static void cli_displayline(inline_editor *edit, const char *line) { char buf[CLI_BUFFERSIZE]; for (const char *c = line; *c != '\0'; ) { if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { putchar(c[1]); c += 2; - continue; - } - if (*c == '`') { + } else if (*c == '`') { c++; size_t n = cli_matchdelimiter(c, '`'); - if (n > 0 && n < sizeof(buf) - 1) { - memcpy(buf, c, n); - buf[n] = '\0'; + if (n > 0 && cli_copyspan(c, n, buf, sizeof(buf))) inline_displaywithsyntaxcoloring(edit, buf); - c += n + 1; - continue; - } + c += n + 1; } else if (*c == '*' || *c == '_') { - char delim = *c; - c++; + char delim = *c++; size_t n = cli_matchdelimiter(c, delim); - if (n > 0 && n < sizeof(buf) - 1) { - memcpy(buf, c, n); - buf[n] = '\0'; + if (n > 0 && cli_copyspan(c, n, buf, sizeof(buf))) cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_BOLD : CLI_UNDERLINE), 1, buf); - c += n + 1; - continue; - } - } - putchar(*c); - c++; + c += n + 1; + } else putchar(*c++); } } @@ -212,11 +207,11 @@ static void cli_helptopic_print(inline_editor *edit, const help_topic *t) { char *eol = strchr(line, '\n'); if (eol) { *eol = '\0'; - cli_help_paraline(edit, line); + cli_displayline(edit, line); putchar('\n'); line = eol + 1; } else { - cli_help_paraline(edit, line); + cli_displayline(edit, line); putchar('\n'); break; } From 2ba856eaadc08602f19abf84be92f84807b88a63 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:11:51 -0500 Subject: [PATCH 38/63] Clean cli_displaytopic --- src/cli.c | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/cli.c b/src/cli.c index 3ce92eb..f263ab6 100644 --- a/src/cli.c +++ b/src/cli.c @@ -159,23 +159,20 @@ static void cli_displayline(inline_editor *edit, const char *line) { } } -/** Print a help_topic to the terminal with highlighting and emphasis. */ -static void cli_helptopic_print(inline_editor *edit, const help_topic *t) { +/** Display a help_topic to the terminal with highlighting and emphasis. */ +static void cli_displaytopic(inline_editor *edit, const help_topic *t) { const md_file *file = t->file; - const char *src = file ? file->source : NULL; - size_t src_len = file ? file->sourcelen : 0; + const char *src = (file ? file->source : NULL); + size_t src_len = (file ? file->sourcelen : 0); if (!src || t->nblocks == 0) return; char buf[CLI_BUFFERSIZE]; for (unsigned int i = 0; i < t->nblocks; i++) { const md_block *b = &t->content_blocks[i]; - size_t start = b->span.start; - size_t len = b->span.length; + size_t start = b->span.start, len = b->span.length; if (start >= src_len) continue; if (start + len > src_len) len = src_len - start; - if (len >= sizeof(buf)) len = sizeof(buf) - 1; - memcpy(buf, src + start, len); - buf[len] = '\0'; + if (!cli_copyspan(src + start, len, buf, sizeof(buf))) continue; switch (b->type) { case MD_BLOCK_HEADER: { @@ -189,16 +186,14 @@ static void cli_helptopic_print(inline_editor *edit, const help_topic *t) { /* Blank line before code only when previous block is not blank and not code */ if (i > 0) { md_blocktype prev = t->content_blocks[i - 1].type; - if (prev != MD_BLOCK_BLANK && prev != MD_BLOCK_CODE) - putchar('\n'); + if (prev != MD_BLOCK_BLANK && prev != MD_BLOCK_CODE) putchar('\n'); } inline_displaywithsyntaxcoloring(edit, buf); if (len > 0 && buf[len - 1] != '\n') putchar('\n'); /* Blank line after code only when next block is not blank and not code */ if (i + 1 < t->nblocks) { md_blocktype next = t->content_blocks[i + 1].type; - if (next != MD_BLOCK_BLANK && next != MD_BLOCK_CODE) - putchar('\n'); + if (next != MD_BLOCK_BLANK && next != MD_BLOCK_CODE) putchar('\n'); } break; case MD_BLOCK_PARAGRAPH: @@ -217,14 +212,11 @@ static void cli_helptopic_print(inline_editor *edit, const help_topic *t) { } } break; - case MD_BLOCK_BLANK: - putchar('\n'); - break; + case MD_BLOCK_BLANK: putchar('\n'); break; case MD_BLOCK_THEMATIC_BREAK: cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "---\n"); break; - case MD_BLOCK_LINK_DEF: - break; + case MD_BLOCK_LINK_DEF: break; } } } @@ -235,7 +227,7 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { help_topic topic; if (morpho_helpastopic(q, &topic)) { - cli_helptopic_print(edit, &topic); + cli_displaytopic(edit, &topic); } else { varray_char result; varray_charinit(&result); From c00b3488fe0933629614ee0eeca1684dab828c70 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:26:54 -0500 Subject: [PATCH 39/63] Simplify code display --- src/cli.c | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/cli.c b/src/cli.c index f263ab6..cc76fc1 100644 --- a/src/cli.c +++ b/src/cli.c @@ -136,11 +136,14 @@ static bool cli_copyspan(const char *src, size_t n, char *buf, size_t bufsize) { return true; } -/** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. */ +/** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. Leading * is printed as a list bullet. */ static void cli_displayline(inline_editor *edit, const char *line) { char buf[CLI_BUFFERSIZE]; for (const char *c = line; *c != '\0'; ) { - if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { + if (c == line && *c == '*') { + putchar('*'); + c++; + } else if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { putchar(c[1]); c += 2; } else if (*c == '`') { @@ -183,33 +186,18 @@ static void cli_displaytopic(inline_editor *edit, const help_topic *t) { break; } case MD_BLOCK_CODE: - /* Blank line before code only when previous block is not blank and not code */ - if (i > 0) { - md_blocktype prev = t->content_blocks[i - 1].type; - if (prev != MD_BLOCK_BLANK && prev != MD_BLOCK_CODE) putchar('\n'); - } inline_displaywithsyntaxcoloring(edit, buf); if (len > 0 && buf[len - 1] != '\n') putchar('\n'); - /* Blank line after code only when next block is not blank and not code */ - if (i + 1 < t->nblocks) { - md_blocktype next = t->content_blocks[i + 1].type; - if (next != MD_BLOCK_BLANK && next != MD_BLOCK_CODE) putchar('\n'); - } break; case MD_BLOCK_PARAGRAPH: case MD_BLOCK_LIST: for (char *line = buf; *line; ) { char *eol = strchr(line, '\n'); - if (eol) { - *eol = '\0'; - cli_displayline(edit, line); - putchar('\n'); - line = eol + 1; - } else { - cli_displayline(edit, line); - putchar('\n'); - break; - } + if (eol) *eol = '\0'; + cli_displayline(edit, line); + putchar('\n'); + if (!eol) break; + line = eol + 1; } break; case MD_BLOCK_BLANK: putchar('\n'); break; From b35b21edc1650c99d2863f96d2881c887e120dd4 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:30:43 -0500 Subject: [PATCH 40/63] Remove stray newline after header --- src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.c b/src/cli.c index cc76fc1..f6d62b7 100644 --- a/src/cli.c +++ b/src/cli.c @@ -182,7 +182,7 @@ static void cli_displaytopic(inline_editor *edit, const help_topic *t) { const char *title = buf; while (*title == '#' || (*title == ' ' && title < buf + len)) title++; if (*title) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, title); - putchar('\n'); + if (len > 0 && buf[len - 1] != '\n') putchar('\n'); break; } case MD_BLOCK_CODE: From f5e87ea7e7cdd4354dcffc3b1f234bf40f82fd71 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:58:37 -0500 Subject: [PATCH 41/63] Separating out cli_displayblocklines --- src/cli.c | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/cli.c b/src/cli.c index f6d62b7..7f25f2f 100644 --- a/src/cli.c +++ b/src/cli.c @@ -136,14 +136,11 @@ static bool cli_copyspan(const char *src, size_t n, char *buf, size_t bufsize) { return true; } -/** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. Leading * is printed as a list bullet. */ +/** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. */ static void cli_displayline(inline_editor *edit, const char *line) { char buf[CLI_BUFFERSIZE]; for (const char *c = line; *c != '\0'; ) { - if (c == line && *c == '*') { - putchar('*'); - c++; - } else if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { + if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { putchar(c[1]); c += 2; } else if (*c == '`') { @@ -153,12 +150,28 @@ static void cli_displayline(inline_editor *edit, const char *line) { inline_displaywithsyntaxcoloring(edit, buf); c += n + 1; } else if (*c == '*' || *c == '_') { - char delim = *c++; + char delim = *c; c++; size_t n = cli_matchdelimiter(c, delim); if (n > 0 && cli_copyspan(c, n, buf, sizeof(buf))) cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_BOLD : CLI_UNDERLINE), 1, buf); c += n + 1; - } else putchar(*c++); + } else { + putchar(*c); + c++; + } + } +} + +/** Display block content as lines; if prefix is not NULL, print it before each line. */ +static void cli_displayblocklines(inline_editor *edit, char *buf, const char *prefix) { + for (char *line = buf; *line; ) { + char *eol = strchr(line, '\n'); + if (eol) *eol = '\0'; + if (prefix) fputs(prefix, stdout); + cli_displayline(edit, line); + putchar('\n'); + if (!eol) break; + line = eol + 1; } } @@ -190,15 +203,10 @@ static void cli_displaytopic(inline_editor *edit, const help_topic *t) { if (len > 0 && buf[len - 1] != '\n') putchar('\n'); break; case MD_BLOCK_PARAGRAPH: + cli_displayblocklines(edit, buf, NULL); + break; case MD_BLOCK_LIST: - for (char *line = buf; *line; ) { - char *eol = strchr(line, '\n'); - if (eol) *eol = '\0'; - cli_displayline(edit, line); - putchar('\n'); - if (!eol) break; - line = eol + 1; - } + cli_displayblocklines(edit, buf, "* "); break; case MD_BLOCK_BLANK: putchar('\n'); break; case MD_BLOCK_THEMATIC_BREAK: From 2e4b45a9ee34cae903722abca7ece80368b23be1 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:15:21 -0500 Subject: [PATCH 42/63] Move help functions into hlp.c and hlp.h --- src/cli.c | 100 +----------------------------------------------------- src/hlp.c | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/hlp.h | 5 +++ 3 files changed, 104 insertions(+), 99 deletions(-) diff --git a/src/cli.c b/src/cli.c index 7f25f2f..cd6ea4e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -119,111 +119,13 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { } } #else -/* --- Minimal help: render help_topic with inline highlighting --- */ - -/** Length of segment between delimiters (excluding closing delim). Returns 0 if no closer. */ -static size_t cli_matchdelimiter(const char *s, char delim) { - const char *p; - for (p = s; *p != delim; p++) if (*p == '\0') return 0; - return (size_t)(p - s); -} - -/** Copies span [src, src+n) into buf and null-terminates. Returns true if n < bufsize. */ -static bool cli_copyspan(const char *src, size_t n, char *buf, size_t bufsize) { - if (n >= bufsize) return false; - memcpy(buf, src, n); - buf[n] = '\0'; - return true; -} - -/** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. */ -static void cli_displayline(inline_editor *edit, const char *line) { - char buf[CLI_BUFFERSIZE]; - for (const char *c = line; *c != '\0'; ) { - if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { - putchar(c[1]); - c += 2; - } else if (*c == '`') { - c++; - size_t n = cli_matchdelimiter(c, '`'); - if (n > 0 && cli_copyspan(c, n, buf, sizeof(buf))) - inline_displaywithsyntaxcoloring(edit, buf); - c += n + 1; - } else if (*c == '*' || *c == '_') { - char delim = *c; c++; - size_t n = cli_matchdelimiter(c, delim); - if (n > 0 && cli_copyspan(c, n, buf, sizeof(buf))) - cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_BOLD : CLI_UNDERLINE), 1, buf); - c += n + 1; - } else { - putchar(*c); - c++; - } - } -} - -/** Display block content as lines; if prefix is not NULL, print it before each line. */ -static void cli_displayblocklines(inline_editor *edit, char *buf, const char *prefix) { - for (char *line = buf; *line; ) { - char *eol = strchr(line, '\n'); - if (eol) *eol = '\0'; - if (prefix) fputs(prefix, stdout); - cli_displayline(edit, line); - putchar('\n'); - if (!eol) break; - line = eol + 1; - } -} - -/** Display a help_topic to the terminal with highlighting and emphasis. */ -static void cli_displaytopic(inline_editor *edit, const help_topic *t) { - const md_file *file = t->file; - const char *src = (file ? file->source : NULL); - size_t src_len = (file ? file->sourcelen : 0); - if (!src || t->nblocks == 0) return; - - char buf[CLI_BUFFERSIZE]; - for (unsigned int i = 0; i < t->nblocks; i++) { - const md_block *b = &t->content_blocks[i]; - size_t start = b->span.start, len = b->span.length; - if (start >= src_len) continue; - if (start + len > src_len) len = src_len - start; - if (!cli_copyspan(src + start, len, buf, sizeof(buf))) continue; - - switch (b->type) { - case MD_BLOCK_HEADER: { - const char *title = buf; - while (*title == '#' || (*title == ' ' && title < buf + len)) title++; - if (*title) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, title); - if (len > 0 && buf[len - 1] != '\n') putchar('\n'); - break; - } - case MD_BLOCK_CODE: - inline_displaywithsyntaxcoloring(edit, buf); - if (len > 0 && buf[len - 1] != '\n') putchar('\n'); - break; - case MD_BLOCK_PARAGRAPH: - cli_displayblocklines(edit, buf, NULL); - break; - case MD_BLOCK_LIST: - cli_displayblocklines(edit, buf, "* "); - break; - case MD_BLOCK_BLANK: putchar('\n'); break; - case MD_BLOCK_THEMATIC_BREAK: - cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "---\n"); - break; - case MD_BLOCK_LINK_DEF: break; - } - } -} - void cli_help(inline_editor *edit, char *query, error *err, bool avail) { char *q = query; while (isspace(*q) && *q!='\0') q++; // Strip any leading space help_topic topic; if (morpho_helpastopic(q, &topic)) { - cli_displaytopic(edit, &topic); + hlp_displaytopic(edit, &topic); } else { varray_char result; varray_charinit(&result); diff --git a/src/hlp.c b/src/hlp.c index bc644f3..6f7a0be 100644 --- a/src/hlp.c +++ b/src/hlp.c @@ -512,6 +512,104 @@ void hlp_finalize(void) { } #else +/** New help display functions */ +#define HLP_BUFFERSIZE 4096 + +/** Length of segment between delimiters (excluding closing delim). Returns 0 if no closer. */ +static size_t hlp_matchdelimiter(const char *s, char delim) { + const char *p; + for (p = s; *p != delim; p++) if (*p == '\0') return 0; + return (size_t)(p - s); +} + +/** Copies span [src, src+n) into buf and null-terminates. Returns true if n < bufsize. */ +static bool hlp_copyspan(const char *src, size_t n, char *buf, size_t bufsize) { + if (n >= bufsize) return false; + memcpy(buf, src, n); + buf[n] = '\0'; + return true; +} + +/** Display one line of paragraph/list text with inline `code`, *bold*, _underline_. Escapes \* \_ \` \\ output the character literally. */ +static void hlp_displayline(inline_editor *edit, const char *line) { + char buf[HLP_BUFFERSIZE]; + for (const char *c = line; *c != '\0'; ) { + if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { + putchar(c[1]); + c += 2; + } else if (*c == '`') { + c++; + size_t n = hlp_matchdelimiter(c, '`'); + if (n > 0 && hlp_copyspan(c, n, buf, sizeof(buf))) + inline_displaywithsyntaxcoloring(edit, buf); + c += n + 1; + } else if (*c == '*' || *c == '_') { + char delim = *c; c++; + size_t n = hlp_matchdelimiter(c, delim); + if (n > 0 && hlp_copyspan(c, n, buf, sizeof(buf))) + cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_BOLD : CLI_UNDERLINE), 1, buf); + c += n + 1; + } else { + putchar(*c); + c++; + } + } +} + +/** Display block content as lines; if prefix is not NULL, print it before each line. */ +static void hlp_displayblocklines(inline_editor *edit, char *buf, const char *prefix) { + for (char *line = buf; *line; ) { + char *eol = strchr(line, '\n'); + if (eol) *eol = '\0'; + if (prefix) fputs(prefix, stdout); + hlp_displayline(edit, line); + putchar('\n'); + if (!eol) break; + line = eol + 1; + } +} + +/** Display a help_topic to the terminal with highlighting and emphasis. */ +void hlp_displaytopic(inline_editor *edit, const help_topic *t) { + const md_file *file = t->file; + const char *src = (file ? file->source : NULL); + size_t src_len = (file ? file->sourcelen : 0); + if (!src || t->nblocks == 0) return; + + char buf[HLP_BUFFERSIZE]; + for (unsigned int i = 0; i < t->nblocks; i++) { + const md_block *b = &t->content_blocks[i]; + size_t start = b->span.start, len = b->span.length; + if (start >= src_len) continue; + if (start + len > src_len) len = src_len - start; + if (!hlp_copyspan(src + start, len, buf, sizeof(buf))) continue; + + switch (b->type) { + case MD_BLOCK_HEADER: { + const char *title = buf; + while (*title == '#' || (*title == ' ' && title < buf + len)) title++; + if (*title) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, title); + if (len > 0 && buf[len - 1] != '\n') putchar('\n'); + break; + } + case MD_BLOCK_CODE: + inline_displaywithsyntaxcoloring(edit, buf); + if (len > 0 && buf[len - 1] != '\n') putchar('\n'); + break; + case MD_BLOCK_PARAGRAPH: + hlp_displayblocklines(edit, buf, NULL); + break; + case MD_BLOCK_LIST: + hlp_displayblocklines(edit, buf, "* "); + break; + case MD_BLOCK_BLANK: putchar('\n'); break; + case MD_BLOCK_THEMATIC_BREAK: + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "---\n"); + break; + case MD_BLOCK_LINK_DEF: break; + } + } +} bool hlp_initialize(void) { return true; } void hlp_finalize(void) { } diff --git a/src/hlp.h b/src/hlp.h index cd4e4d2..a1c9209 100644 --- a/src/hlp.h +++ b/src/hlp.h @@ -37,6 +37,11 @@ size_t help_querylength(char *query, char **s); objecthelptopic *help_search(char *query); void help_display(inline_editor *edit, objecthelptopic *topic); +#else +#include + +void hlp_displaytopic(inline_editor *edit, const help_topic *t); + #endif bool hlp_initialize(void); From 293d77b8917842e486b4af1d284cab27b772e91d Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:41:41 -0500 Subject: [PATCH 43/63] Blank query and topic list --- src/cli.c | 14 ++++++++++++-- src/hlp.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/hlp.h | 2 ++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/cli.c b/src/cli.c index cd6ea4e..35cc3d9 100644 --- a/src/cli.c +++ b/src/cli.c @@ -122,20 +122,30 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { void cli_help(inline_editor *edit, char *query, error *err, bool avail) { char *q = query; while (isspace(*q) && *q!='\0') q++; // Strip any leading space - + bool blank = (*q == '\0'); + if (blank) q = "help"; + help_topic topic; if (morpho_helpastopic(q, &topic)) { hlp_displaytopic(edit, &topic); } else { varray_char result; varray_charinit(&result); - + help_queryhint(q, &result); if (result.count > 0) printf("%s\n", result.data); else printf("No help found for '%s'\n", q); varray_charclear(&result); } + + if (blank) { + varray_value list; + varray_valueinit(&list); + morpho_helptopics(&list); + hlp_displaytopiclist(edit, &list); + varray_valueclear(&list); + } } #endif diff --git a/src/hlp.c b/src/hlp.c index 6f7a0be..db7a8ec 100644 --- a/src/hlp.c +++ b/src/hlp.c @@ -5,6 +5,7 @@ */ #include +#include #include #include @@ -611,6 +612,59 @@ void hlp_displaytopic(inline_editor *edit, const help_topic *t) { } } +static int hlp_topicname_cmp(const void *a, const void *b) { + const value *va = (const value *) a; + const value *vb = (const value *) b; + if (!MORPHO_ISSTRING(*va) || !MORPHO_ISSTRING(*vb)) return 0; + return strcmp(MORPHO_GETCSTRING(*va), MORPHO_GETCSTRING(*vb)); +} + +/** Display a list of topic names (e.g. from morpho_helptopics) in columns. */ +void hlp_displaytopiclist(inline_editor *edit, varray_value *topics) { + if (!topics || topics->count == 0) return; + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, "Topics:\n"); + int width = 80, max = 0; + inline_getterminalwidth(&width); + + value *data = topics->data; + unsigned int n = topics->count; + qsort(data, n, sizeof(value), hlp_topicname_cmp); + + for (unsigned int i = 0; i < n; i++) { + if (MORPHO_ISSTRING(data[i])) { + int len = (int) MORPHO_GETSTRINGLENGTH(data[i]); + if (len > max) max = len; + } + } + if (max == 0) max = 1; + int ncols = width / (max + 1); + if (ncols < 1) ncols = 1; + bool single = (unsigned int) ncols > n; + + varray_char str; + varray_charinit(&str); + int k = 0; + for (unsigned int i = 0; i < n; i++) { + if (MORPHO_ISSTRING(data[i])) { + varray_charadd(&str, MORPHO_GETCSTRING(data[i]), MORPHO_GETSTRINGLENGTH(data[i])); + if (single) { + varray_charadd(&str, " ", 2); + } else { + for (int j = len; j < max + 1; j++) varray_charwrite(&str, ' '); + } + k++; + } + if (k == ncols || i == n - 1) { + varray_charwrite(&str, '\n'); + varray_charwrite(&str, '\0'); + if (str.count > 0) inline_displaywithsyntaxcoloring(edit, str.data); + str.count = 0; + k = 0; + } + } + varray_charclear(&str); +} + bool hlp_initialize(void) { return true; } void hlp_finalize(void) { } diff --git a/src/hlp.h b/src/hlp.h index a1c9209..5385918 100644 --- a/src/hlp.h +++ b/src/hlp.h @@ -42,6 +42,8 @@ void help_display(inline_editor *edit, objecthelptopic *topic); void hlp_displaytopic(inline_editor *edit, const help_topic *t); +void hlp_displaytopiclist(inline_editor *edit, varray_value *topics); + #endif bool hlp_initialize(void); From ca3159fa689026bcdf326cb0b331795c23bd79a8 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:48:04 -0500 Subject: [PATCH 44/63] Debugger commands --- src/debugger.c | 36 ++++++++++++++++++++++++++++++++++++ src/hlp.c | 4 +++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/debugger.c b/src/debugger.c index 26cb010..15bef3b 100644 --- a/src/debugger.c +++ b/src/debugger.c @@ -4,6 +4,9 @@ * @brief Command line debugger */ +#include +#include + #include #include #include @@ -578,6 +581,38 @@ bool clidebugger_parse(clidebugger *debug, char *in) { return success; } +/* ********************************************************************** + * Debugger autocomplete + * ********************************************************************** */ + +static const char *debugger_commands[] = { + "address", "backtrace", "b", "break", "bt", "c", "clear", "continue", + "d", "disassem", "disassemble", "g", "gc", "global", "globals", "garbage", + "h", "help", "i", "info", "l", "list", "p", "print", "q", "quit", + "reg", "register", "registers", "s", "set", "stack", "step", "t", "trace", "x", + NULL +}; + +/** Autocomplete callback for debugger vocabulary (commands and sub-commands). */ +static const char *clidebugger_complete(const char *in, void *ref, size_t *index) { + (void) ref; + size_t len = strlen(in); + const char *tok = in + len; + while (tok > in && !isspace((unsigned char) *(tok - 1))) tok--; + if (tok >= in + len || iscntrl((unsigned char) *tok)) return NULL; + size_t toklen = strlen(tok); + + for (size_t i = *index; debugger_commands[i] != NULL; i++) { + const char *cmd = debugger_commands[i]; + size_t cmdlen = strlen(cmd); + if (toklen < cmdlen && strncmp(tok, cmd, toklen) == 0) { + *index = i + 1; + return cmd + toklen; + } + } + return NULL; +} + /* ********************************************************************** * Debugger REPL * ********************************************************************** */ @@ -587,6 +622,7 @@ void clidebugger_enter(vm *v) { error_init(&err); inline_editor *edit = inline_new(DEBUGGER_PROMPT); + inline_autocomplete(edit, clidebugger_complete, NULL); clidebugger debug; clidebugger_init(&debug, v, edit, &err); clidebugger_banner(&debug); diff --git a/src/hlp.c b/src/hlp.c index db7a8ec..c8d7e92 100644 --- a/src/hlp.c +++ b/src/hlp.c @@ -646,7 +646,9 @@ void hlp_displaytopiclist(inline_editor *edit, varray_value *topics) { int k = 0; for (unsigned int i = 0; i < n; i++) { if (MORPHO_ISSTRING(data[i])) { - varray_charadd(&str, MORPHO_GETCSTRING(data[i]), MORPHO_GETSTRINGLENGTH(data[i])); + char *s = MORPHO_GETCSTRING(data[i]); + int len = (int) MORPHO_GETSTRINGLENGTH(data[i]); + varray_charadd(&str, s, len); if (single) { varray_charadd(&str, " ", 2); } else { From 90ccd7b4e2ef7f370bec54906c1f5aa10adbb729 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:51:46 -0500 Subject: [PATCH 45/63] Highlight at in red --- src/cli.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/cli.c b/src/cli.c index 35cc3d9..2e178e6 100644 --- a/src/cli.c +++ b/src/cli.c @@ -122,7 +122,7 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { void cli_help(inline_editor *edit, char *query, error *err, bool avail) { char *q = query; while (isspace(*q) && *q!='\0') q++; // Strip any leading space - bool blank = (*q == '\0'); + bool blank = (*q == '\0'); // Check for blank query if (blank) q = "help"; help_topic topic; @@ -139,7 +139,7 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { varray_charclear(&result); } - if (blank) { + if (blank) { // Display list of topics varray_value list; varray_valueinit(&list); morpho_helptopics(&list); @@ -194,7 +194,8 @@ int palette[] = { INLINE_COLOR_ANSI216(0, 3, 4), // 3 symbol (darker cyan/teal, distinct from blue) INLINE_MAGENTA, // 4 keyword INLINE_GRAY_ANSI(12), // 5 comment (mid-level gray) - INLINE_COLOR_ANSI216(5, 3, 0) // 6 operator (amber) + INLINE_COLOR_ANSI216(5, 3, 0), // 6 operator (amber) + CLI_ERRORCOLOR // 7 debug breakpoint marker @ }; tokentype help[] = { TOKEN_QUESTION }; @@ -206,7 +207,7 @@ tokentype operators[] = { TOKEN_PLUS, TOKEN_MINUS, TOKEN_STAR, TOKEN_SLASH, TOKEN_CIRCUMFLEX, TOKEN_PLUSPLUS, TOKEN_MINUSMINUS, TOKEN_PLUSEQ, TOKEN_MINUSEQ, TOKEN_STAREQ, TOKEN_SLASHEQ, - TOKEN_HASH, TOKEN_AT, + TOKEN_HASH, TOKEN_EXCLAMATION, TOKEN_AMP, TOKEN_VBAR, TOKEN_DBLAMP, TOKEN_DBLVBAR, TOKEN_EQUAL, TOKEN_EQ, TOKEN_NEQ, TOKEN_LT, TOKEN_GT, TOKEN_LTEQ, TOKEN_GTEQ, @@ -284,7 +285,8 @@ bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspa out->byte_end=tok.start-in; } else { // A real token out->byte_end=offset+tok.length; - if (matchtokentype(tok.type, sizeof(help)/sizeof(help[0]), help)) out->color=1; + if (tok.type==TOKEN_AT) out->color=7; // Debug breakpoint marker in red + else if (matchtokentype(tok.type, sizeof(help)/sizeof(help[0]), help)) out->color=1; else if (matchtokentype(tok.type, sizeof(literal)/sizeof(literal[0]), literal)) out->color=2; else if (matchtokentype(tok.type, sizeof(symbols)/sizeof(symbols[0]), symbols)) out->color=3; else if (matchtokentype(tok.type, sizeof(keywords)/sizeof(keywords[0]), keywords)) out->color=4; From 7dce523ce3c04eb505a97c35f9edbf0f9f6c77ef Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:33:26 -0500 Subject: [PATCH 46/63] Fix styled output --- src/cli.c | 10 ++++++---- src/cli.h | 1 + src/hlp.c | 17 ++++++++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/cli.c b/src/cli.c index 2e178e6..0616a70 100644 --- a/src/cli.c +++ b/src/cli.c @@ -41,7 +41,7 @@ char *cli_globalsrc=NULL; * Utility functions * ********************************************************************** */ -void inline_emitemphasis(int emph) { +void cli_emitemphasis(int emph) { switch (emph) { case CLI_NOEMPHASIS: inline_emit(RESET); break; case CLI_BOLD: inline_emit(BOLD); break; @@ -59,13 +59,15 @@ void cli_displaywithstyle(int col, int emph, int n, ...) { va_start(args, n); bool is_tty = inline_checktty() && is_supported; // Only emit escape codes if stdout is a TTY + fflush(stdout); for (int i=0; i 0 && hlp_copyspan(c, n, buf, sizeof(buf))) inline_displaywithsyntaxcoloring(edit, buf); c += n + 1; - } else if (*c == '*' || *c == '_') { + } else if (*c == '*' && c[1] == '*') { /* **bold** */ + c += 2; + size_t n = hlp_matchdelimiter(c, '*'); + if (n > 0 && c[n] == '*' && c[n + 1] == '*' && hlp_copyspan(c, n, buf, sizeof(buf))) { + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_BOLD, 1, buf); + c += n + 2; + } else { + putchar(*(c - 2)); + putchar(*(c - 1)); + } + } else if (*c == '*' || *c == '_') { /* *emphasis* or _underline_ */ char delim = *c; c++; size_t n = hlp_matchdelimiter(c, delim); if (n > 0 && hlp_copyspan(c, n, buf, sizeof(buf))) - cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_BOLD : CLI_UNDERLINE), 1, buf); + cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_ITALIC : CLI_UNDERLINE), 1, buf); c += n + 1; } else { putchar(*c); From 44b0d441fd129f683c3c2e6524598acdba52b799 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:34:44 -0500 Subject: [PATCH 47/63] Update hlp.c --- src/hlp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hlp.c b/src/hlp.c index 0129072..319f060 100644 --- a/src/hlp.c +++ b/src/hlp.c @@ -545,7 +545,7 @@ static void hlp_displayline(inline_editor *edit, const char *line) { if (n > 0 && hlp_copyspan(c, n, buf, sizeof(buf))) inline_displaywithsyntaxcoloring(edit, buf); c += n + 1; - } else if (*c == '*' && c[1] == '*') { /* **bold** */ + } else if (*c == '*' && c[1] == '*') { // **bold** c += 2; size_t n = hlp_matchdelimiter(c, '*'); if (n > 0 && c[n] == '*' && c[n + 1] == '*' && hlp_copyspan(c, n, buf, sizeof(buf))) { @@ -555,7 +555,7 @@ static void hlp_displayline(inline_editor *edit, const char *line) { putchar(*(c - 2)); putchar(*(c - 1)); } - } else if (*c == '*' || *c == '_') { /* *emphasis* or _underline_ */ + } else if (*c == '*' || *c == '_') { // *italic* or _underline_ char delim = *c; c++; size_t n = hlp_matchdelimiter(c, delim); if (n > 0 && hlp_copyspan(c, n, buf, sizeof(buf))) From 9e8801711b64ac1de63c20c51a12681d3d354648 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:54:38 -0500 Subject: [PATCH 48/63] Show subtopics --- src/cli.c | 2 +- src/hlp.c | 20 +++++++++++++++++--- src/hlp.h | 5 ++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/cli.c b/src/cli.c index 0616a70..3e164ca 100644 --- a/src/cli.c +++ b/src/cli.c @@ -145,7 +145,7 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { varray_value list; varray_valueinit(&list); morpho_helptopics(&list); - hlp_displaytopiclist(edit, &list); + hlp_displaytopiclist(edit, &list, HLP_TOPICS_HDR); varray_valueclear(&list); } } diff --git a/src/hlp.c b/src/hlp.c index 319f060..7b20309 100644 --- a/src/hlp.c +++ b/src/hlp.c @@ -581,6 +581,15 @@ static void hlp_displayblocklines(inline_editor *edit, char *buf, const char *pr } } +/** Display subtopics for a given topic. */ +static void hlp_displaysubtopics(inline_editor *edit, const help_topic *t) { + varray_value subtopics; + varray_valueinit(&subtopics); + morpho_helpsubtopics(t, &subtopics); + if (subtopics.count > 0) hlp_displaytopiclist(edit, &subtopics, HLP_SUBTOPICS_HDR); + varray_valueclear(&subtopics); +} + /** Display a help_topic to the terminal with highlighting and emphasis. */ void hlp_displaytopic(inline_editor *edit, const help_topic *t) { const md_file *file = t->file; @@ -618,7 +627,12 @@ void hlp_displaytopic(inline_editor *edit, const help_topic *t) { case MD_BLOCK_THEMATIC_BREAK: cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "---\n"); break; - case MD_BLOCK_LINK_DEF: break; + case MD_SHOW_SUBTOPICS: + hlp_displaysubtopics(edit, t); + break; + case MD_BLOCK_LINK_DEF: + default: + break; } } } @@ -631,9 +645,9 @@ static int hlp_topicname_cmp(const void *a, const void *b) { } /** Display a list of topic names (e.g. from morpho_helptopics) in columns. */ -void hlp_displaytopiclist(inline_editor *edit, varray_value *topics) { +void hlp_displaytopiclist(inline_editor *edit, varray_value *topics, const char *heading) { if (!topics || topics->count == 0) return; - cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, "Topics:\n"); + if (heading) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, heading); int width = 80, max = 0; inline_getterminalwidth(&width); diff --git a/src/hlp.h b/src/hlp.h index 5385918..c556ef5 100644 --- a/src/hlp.h +++ b/src/hlp.h @@ -40,9 +40,12 @@ void help_display(inline_editor *edit, objecthelptopic *topic); #else #include +#define HLP_TOPICS_HDR "Topics:\n" +#define HLP_SUBTOPICS_HDR "Subtopics:\n" + void hlp_displaytopic(inline_editor *edit, const help_topic *t); -void hlp_displaytopiclist(inline_editor *edit, varray_value *topics); +void hlp_displaytopiclist(inline_editor *edit, varray_value *topics, const char *heading); #endif From 60fd85676f1f984679bf5cb5e4bdef6cfd827fb0 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:31:01 -0500 Subject: [PATCH 49/63] Fix error in unistring codepath --- src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.c b/src/cli.c index 3e164ca..54f6fcf 100644 --- a/src/cli.c +++ b/src/cli.c @@ -495,7 +495,7 @@ static void cli_repl(runtime_t *rt, clioptions opt) { inline_multiline(rt->edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); inline_autocomplete(rt->edit, cli_complete, NULL); #ifdef CLI_USELIBUNISTRING - inline_setgraphemesplitter(&rt->edit, libunistring_graphemefn); + inline_setgraphemesplitter(rt->edit, libunistring_graphemefn); #endif #ifdef CLI_USELIBGRAPHEME inline_setgraphemesplitter(rt->edit, libgrapheme_graphemefn); From 1e83d29b73f41a8e8dd2da4358d59354e4307ecb Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:47:22 -0500 Subject: [PATCH 50/63] Fix help query --- src/cli.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli.c b/src/cli.c index 54f6fcf..a5d5a21 100644 --- a/src/cli.c +++ b/src/cli.c @@ -333,8 +333,12 @@ const char *cli_complete(const char *in, void *ref, size_t *index) { /** Multiline function */ bool cli_multiline(const char *in, void *ref) { int nb=0; + const char *c; - for (const char *c=in; *c!='\0'; c++) { + for (c=in; isspace(*c); c++); // Skip leading whitespace + if (*c=='?' || strncmp(c, "help", 4)==0) return false; + + for (; *c!='\0'; c++) { switch (*c) { case '(': case '{': case '[': nb+=1; break; case ')': case '}': case ']': nb-=1; break; From 28e2ff22948389e94ef3925689aebe367b692782 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:27:10 -0500 Subject: [PATCH 51/63] Detect MORPHO_INCLUDE_HELP for includes --- src/cli.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cli.c b/src/cli.c index a5d5a21..86abf94 100644 --- a/src/cli.c +++ b/src/cli.c @@ -11,15 +11,17 @@ #include #include -#include "cli.h" - #include -#include #include #include +#include "cli.h" #include "debugger.h" +#ifdef MORPHO_INCLUDE_HELP + #include +#endif + #ifdef CLI_USELIBUNISTRING #include #endif From 6370fc3937e775f477aad1d274b9b7a7eda5334c Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Wed, 20 May 2026 15:52:57 +0200 Subject: [PATCH 52/63] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 17d4e20..4489f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build-xcode/* .vscode/settings.json /.vscode REVIEW.md +/dist From d58da089a2882dcc2fe7584691f22ba606652567 Mon Sep 17 00:00:00 2001 From: Tim Atherton <54725421+softmattertheory@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:58:36 -0400 Subject: [PATCH 53/63] CLI exit codes --- src/cli.c | 19 ++++++++++++++++++- src/cli.h | 4 ++++ src/main.c | 8 ++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/cli.c b/src/cli.c index 86abf94..723a531 100644 --- a/src/cli.c +++ b/src/cli.c @@ -31,6 +31,15 @@ #endif char *cli_globalsrc=NULL; +static int s_cli_lastexitcode = EXIT_SUCCESS; + +void cli_setexitcode(int code) { + s_cli_lastexitcode = code; +} + +int cli_exitcode(void) { + return s_cli_lastexitcode; +} #define CLI_BUFFERSIZE 4096 @@ -77,6 +86,7 @@ void cli_displaywithstyle(int col, int emph, int n, ...) { /** Report an error if one has occurred. */ void cli_reporterror(error *err, vm *v) { if (err->cat!=ERROR_NONE) { + cli_setexitcode(EXIT_FAILURE); cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, "Error '", err->id, "'"); if (ERROR_ISRUNTIMEERROR(*err)) { @@ -440,10 +450,14 @@ static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { } else { success = morpho_run(rt->v, rt->p); } - if (!success) cli_reporterror(morpho_geterror(rt->v), rt->v); + if (!success) { + cli_reporterror(morpho_geterror(rt->v), rt->v); + cli_setexitcode(EXIT_FAILURE); + } } } else { cli_reporterror(&err, rt->v); + cli_setexitcode(EXIT_FAILURE); } return success; @@ -544,11 +558,13 @@ static void cli_repl(runtime_t *rt, clioptions opt) { success = morpho_debug(rt->v, rt->p); if (!success) { cli_reporterror(morpho_geterror(rt->v), rt->v); + cli_setexitcode(EXIT_FAILURE); err = *morpho_geterror(rt->v); } } } else { cli_reporterror(&err, rt->v); + cli_setexitcode(EXIT_FAILURE); } free(input); @@ -579,6 +595,7 @@ void cli_run(const char *in, clioptions opt) { MORPHO_FREE(src); } else { printf("Could not open file '%s'.\n", in); + cli_setexitcode(EXIT_FAILURE); cli_freeruntime(&rt); return; } diff --git a/src/cli.h b/src/cli.h index e5d9686..f7b71b9 100644 --- a/src/cli.h +++ b/src/cli.h @@ -43,6 +43,8 @@ typedef unsigned int clioptions; extern char *cli_globalsrc; +void cli_setexitcode(int code); +int cli_exitcode(void); void cli_displaywithstyle(int col, int emph, int n, ...); void cli_emitemphasis(int emph); @@ -58,3 +60,5 @@ void cli_disassemblewithsrc(program *p, char *src, clioptions opt); void cli_list(const char *in, int start, int end, clioptions opt); #endif /* cli_h */ + + diff --git a/src/main.c b/src/main.c index df76f79..e75a470 100644 --- a/src/main.c +++ b/src/main.c @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -181,6 +182,7 @@ static bool parse_option(int argc, const char *argv[], int *idx, clioptions *fla if (opt_table[j].takes_arg) { if (*idx + 1 >= argc) { fprintf(stderr, "morpho: %s requires an argument.\n", arg); + cli_setexitcode(EXIT_FAILURE); return false; } opt_arg = argv[*idx + 1]; @@ -190,7 +192,8 @@ static bool parse_option(int argc, const char *argv[], int *idx, clioptions *fla return opt_table[j].fn(arg, opt_arg, flags, &ctx); } } - printf("Unknown option %s.\n", arg); + fprintf(stderr, "morpho: Unknown option %s.\n", arg); + cli_setexitcode(EXIT_FAILURE); return false; } @@ -201,6 +204,7 @@ int main(int argc, const char *argv[]) { bool run = true; morpho_initialize(); + cli_setexitcode(EXIT_SUCCESS); for (; i < argc && !file; i++) { const char *arg = argv[i]; @@ -229,5 +233,5 @@ int main(int argc, const char *argv[]) { } morpho_finalize(); - return 0; + return cli_exitcode(); } From aa23a2e49aee61eb4a5430512e35b4dae9bf4bea Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 12:27:14 -0400 Subject: [PATCH 54/63] Update README.md --- README.md | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4441e1b..27f83da 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,7 @@ Like morpho itself, Morpho-cli is released under an MIT license (see the LICENSE ## Installation -For this release, morpho can be installed on all supported platforms using the homebrew package manager. Alternatively, the program can be installed from source as described below. - -### Install with homebrew - -The simplest way to install morpho-cli is through the [homebrew package manager](https://brew.sh). To do so: +The simplest way to install the morpho terminal app is to use the [homebrew package manager](https://brew.sh). To do so: 1. If not already installed, install homebrew on your machine as described on the [homebrew website](https://brew.sh) @@ -26,7 +22,35 @@ brew install morpho morpho-cli morpho-morphoview morpho-morphopm If you need to uninstall morpho, simply open a terminal and type `brew uninstall morpho-cli morpho-morphoview morpho`. It's very important to uninstall the homebrew morpho in this way before attempting to install from source as below. -### Install from source +## Running + +To run type, + + morpho6 + +into a terminal. The terminal app takes a number of optional arguments: + +| Option | Description | +|--------|-------------| +| `-h`, `--help` | Show help message | +| `-v`, `--version` | Show version information | +| `-c`, `--check` | Check syntax without executing | +| `-e`, `--eval ` | Execute a code string | +| `-i`, `--interactive` | Enter REPL after running a file | +| `-l`, `--list ` | List a file with syntax highlighting | +| `-d`, `--disassemble` | Show disassembly | +| `-D` | Disassemble only (no execution) | +| `-dl` | Disassemble with source listing | +| `-debug`, `--debug` | Enable debugger | +| `-O`, `--optimize` | Enable optimizations | +| `-profile`, `--profile` | Enable profiling | +| `--no-color` | Disable syntax highlighting | +| `-w`, `--workers ` | Set number of worker threads | + +If no file is specified, morpho enters interactive REPL mode. If stdin is piped or redirected, morpho reads and executes from stdin. Any options after the file name are passed to the morpho program. + + +## Manual install from source To install, clone this repository: @@ -35,13 +59,8 @@ To install, clone this repository: and then, cd morpho-cli - mkdir build - cd build - cmake -DCMAKE_BUILD_TYPE=Release .. - make install + cmake -S . -B build + cmake --build build --config Release + sudo cmake --install build --config Release -You may need to use sudo make install. - -To run, - - morpho6 +This manual build installs into '/usr/local/bin' by default. From 6d3cdf86ac1f1140595c91dda059baff3182bae5 Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 12:42:12 -0400 Subject: [PATCH 55/63] Perform help queries from the command line --- README.md | 2 +- src/cli.c | 44 ++++++++++++++++++++++++++++++++++++-------- src/cli.h | 1 + src/main.c | 13 +++++++++++-- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 27f83da..1b4d877 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ into a terminal. The terminal app takes a number of optional arguments: | Option | Description | |--------|-------------| -| `-h`, `--help` | Show help message | +| `-h`, `--help [query]` | Show CLI help, or look up a language help topic | | `-v`, `--version` | Show version information | | `-c`, `--check` | Check syntax without executing | | `-e`, `--eval ` | Execute a code string | diff --git a/src/cli.c b/src/cli.c index 723a531..15e639d 100644 --- a/src/cli.c +++ b/src/cli.c @@ -111,9 +111,10 @@ void cli_reporterror(error *err, vm *v) { } -/** Interactive help */ +/** Interactive help. Returns true if help was found or a useful hint was shown. */ #ifndef MORPHO_INCLUDE_HELP -void cli_help(inline_editor *edit, char *query, error *err, bool avail) { +static bool cli_help(inline_editor *edit, char *query, error *err, bool avail) { + (void)avail; char *q=query; if (help_querylength(q, NULL)==0) { if (err->cat!=ERROR_NONE) { @@ -127,29 +128,31 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { objecthelptopic *topic = help_search(q); if (topic) { help_display(edit, topic); - } else { - while (isspace(*q) && *q!='\0') q++; - printf("No help found for '%s'\n", q); + return true; } + while (isspace(*q) && *q!='\0') q++; + printf("No help found for '%s'\n", q); + return false; } #else -void cli_help(inline_editor *edit, char *query, error *err, bool avail) { +static bool cli_help(inline_editor *edit, char *query, error *err, bool avail) { + (void)err; (void)avail; char *q = query; while (isspace(*q) && *q!='\0') q++; // Strip any leading space bool blank = (*q == '\0'); // Check for blank query if (blank) q = "help"; + bool found = false; help_topic topic; if (morpho_helpastopic(q, &topic)) { hlp_displaytopic(edit, &topic); + found = true; } else { varray_char result; varray_charinit(&result); - help_queryhint(q, &result); if (result.count > 0) printf("%s\n", result.data); else printf("No help found for '%s'\n", q); - varray_charclear(&result); } @@ -159,7 +162,9 @@ void cli_help(inline_editor *edit, char *query, error *err, bool avail) { morpho_helptopics(&list); hlp_displaytopiclist(edit, &list, HLP_TOPICS_HDR); varray_valueclear(&list); + found = true; } + return found; } #endif @@ -726,3 +731,26 @@ void cli_list(const char *src, int start, int end, clioptions opt) { } } +/** Look up and display a help topic from the command line. */ +void cli_helpquery(const char *query, clioptions opt) { + inline_editor *edit = inline_new(""); + if (!edit) { + cli_setexitcode(EXIT_FAILURE); + return; + } + + lexer l; + if (!(opt & CLI_NOCOLOR)) { + inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); + inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); + } + + error err; + error_init(&err); + bool help = hlp_initialize(); + char *q = query ? (char *) query : (char *) ""; + if (!cli_help(edit, q, &err, help)) cli_setexitcode(EXIT_FAILURE); + hlp_finalize(); + inline_free(edit); +} + diff --git a/src/cli.h b/src/cli.h index f7b71b9..67ee776 100644 --- a/src/cli.h +++ b/src/cli.h @@ -58,6 +58,7 @@ char *cli_loadsource(const char *in); char *cli_loadstdin(void); void cli_disassemblewithsrc(program *p, char *src, clioptions opt); void cli_list(const char *in, int start, int end, clioptions opt); +void cli_helpquery(const char *query, clioptions opt); #endif /* cli_h */ diff --git a/src/main.c b/src/main.c index e75a470..0dbaf68 100644 --- a/src/main.c +++ b/src/main.c @@ -39,10 +39,19 @@ static bool opt_version(const char *opt, const char *arg, clioptions *flags, opt } static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { - (void)opt; (void)arg; (void)flags; (void)ctx; + (void)opt; (void)arg; + /* Optional query: morpho6 --help [topic] looks up built-in language help. */ + if (ctx && ctx->idx && *ctx->idx + 1 < ctx->argc) { + const char *next = ctx->argv[*ctx->idx + 1]; + if (next && next[0] != '-') { + (*ctx->idx)++; + cli_helpquery(next, *flags); + return false; + } + } printf("Usage: morpho6 [options] [file] [options passed to program]\n"); printf("\nOptions:\n"); - printf(" -h, --help Show this help message\n"); + printf(" -h, --help [query] Show this help, or look up a language help topic\n"); printf(" -v, --version Show version information\n"); printf(" -c, --check Check syntax without executing\n"); printf(" -e, --eval Execute code string\n"); From c681264ab56cb569d18942a9bbea4a238882b806 Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 12:47:14 -0400 Subject: [PATCH 56/63] Fix option parsing bugs --- src/cli.c | 11 ++++++++-- src/main.c | 64 +++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/cli.c b/src/cli.c index 15e639d..531b3af 100644 --- a/src/cli.c +++ b/src/cli.c @@ -697,12 +697,16 @@ void cli_disassemblewithsrc(program *p, char *src, clioptions opt) { int line=1, length=0; for (unsigned int i=0; src[i]!='\0'; i++) { length++; - if (src[i]=='\n' || src[i]=='\0') { + if (src[i]=='\n') { cli_printline(edit, line, ">>>", src+i-length+1, length); morpho_disassemble(NULL, p, NULL); line++; length=0; } } + if (length > 0) { + cli_printline(edit, line, ">>>", src+strlen(src)-length, length+1); + morpho_disassemble(NULL, p, NULL); + } inline_free(edit); } @@ -721,12 +725,15 @@ void cli_list(const char *src, int start, int end, clioptions opt) { int line=1, length=0; for (unsigned int i=0; src[i]!='\0'; i++) { length++; - if (src[i]=='\n' || src[i]=='\0') { + if (src[i]=='\n') { if (line>=start && line <=end) cli_printline(edit, line, "", src+i-length+1, length); line++; length=0; } } + if (length > 0 && line>=start && line <=end) { + cli_printline(edit, line, "", src+strlen(src)-length, length+1); + } inline_free(edit); } } diff --git a/src/main.c b/src/main.c index 0dbaf68..c6745fe 100644 --- a/src/main.c +++ b/src/main.c @@ -109,13 +109,15 @@ static bool opt_profile(const char *opt, const char *arg, clioptions *flags, opt } static bool opt_workers(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { - const char *c = arg ? arg : (opt + 1); - while (*c && *c != '=' && !isdigit((unsigned char)*c)) c++; - if (*c == '=') c++; - int n = isdigit((unsigned char)*c) ? atoi(c) : 0; + (void)opt; (void)flags; (void)ctx; + if (!arg || !isdigit((unsigned char)*arg)) { + fprintf(stderr, "morpho: -w/--workers requires a number.\n"); + cli_setexitcode(EXIT_FAILURE); + return false; + } + int n = atoi(arg); if (n < 0) n = 0; morpho_setthreadnumber(n); - (void)flags; return true; } @@ -167,8 +169,8 @@ typedef struct { static const option_t opt_table[] = { { "-D", NULL, false, opt_disassembleonly }, { "-dl", NULL, false, opt_disassemblelist }, - { "-d", "--disassemble", false, opt_disassemble }, { "-debug", "--debug", false, opt_debug }, + { "-d", "--disassemble", false, opt_disassemble }, { "-c", "--check", false, opt_check }, { "-e", "--eval", true, opt_eval }, { "-h", "--help", false, opt_help }, @@ -178,28 +180,56 @@ static const option_t opt_table[] = { { "-profile", "--profile", false, opt_profile }, { NULL, "--no-color", false, opt_nocolor }, { "-v", "--version", false, opt_version }, - { "-w", "--workers", false, opt_workers }, + { "-w", "--workers", true, opt_workers }, { NULL, NULL, false, NULL }, }; +/** Match argv token to an option name. Exact match, or for options that take an + * argument: name=value, or (short options only) -w4 with the value attached. */ +static bool option_matches(const char *arg, const char *name, bool takes_arg, const char **attached) { + if (!name) return false; + size_t n = strlen(name); + if (strncmp(arg, name, n) != 0) return false; + if (arg[n] == '\0') { + *attached = NULL; + return true; + } + if (!takes_arg) return false; + if (arg[n] == '=') { + *attached = arg + n + 1; + return true; + } + /* Short option with attached value, e.g. -w4 */ + if (name[0] == '-' && name[1] != '-' && isdigit((unsigned char)arg[n])) { + *attached = arg + n; + return true; + } + return false; +} + /** Parse one option at argv[*idx]. If option takes an argument, consumes *idx+1 and advances *idx. */ static bool parse_option(int argc, const char *argv[], int *idx, clioptions *flags) { const char *arg = argv[*idx], *opt_arg = NULL; for (int j = 0; opt_table[j].s || opt_table[j].l; j++) { - const char *s = opt_table[j].s, *l = opt_table[j].l; - if ((s && strncmp(arg, s, strlen(s)) == 0) || (l && strncmp(arg, l, strlen(l)) == 0)) { - if (opt_table[j].takes_arg) { - if (*idx + 1 >= argc) { - fprintf(stderr, "morpho: %s requires an argument.\n", arg); - cli_setexitcode(EXIT_FAILURE); - return false; - } + const char *attached = NULL; + bool match = option_matches(arg, opt_table[j].s, opt_table[j].takes_arg, &attached) || + option_matches(arg, opt_table[j].l, opt_table[j].takes_arg, &attached); + if (!match) continue; + + if (opt_table[j].takes_arg) { + if (attached) { + opt_arg = attached; + } else if (*idx + 1 >= argc) { + fprintf(stderr, "morpho: %s requires an argument.\n", arg); + cli_setexitcode(EXIT_FAILURE); + return false; + } else { opt_arg = argv[*idx + 1]; (*idx)++; } - opt_ctx ctx = { argc, argv, idx }; - return opt_table[j].fn(arg, opt_arg, flags, &ctx); } + opt_ctx ctx = { argc, argv, idx }; + return opt_table[j].fn(arg, opt_arg, flags, &ctx); } fprintf(stderr, "morpho: Unknown option %s.\n", arg); cli_setexitcode(EXIT_FAILURE); From 8a202d2f075cbf8aa2d059084740d2103df463a0 Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 14:46:47 -0400 Subject: [PATCH 57/63] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b4d877..5a89411 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,9 @@ into a terminal. The terminal app takes a number of optional arguments: | `--no-color` | Disable syntax highlighting | | `-w`, `--workers ` | Set number of worker threads | -If no file is specified, morpho enters interactive REPL mode. If stdin is piped or redirected, morpho reads and executes from stdin. Any options after the file name are passed to the morpho program. +If no file is specified, morpho enters interactive mode. If stdin is piped or redirected, morpho reads and executes from stdin. Any options after the file name are passed to the morpho program and are available from the `System` class: + var args = System.arguments() ## Manual install from source From 763a533e4af57780a79ea35e58281ae822a5a7f6 Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 15:28:34 -0400 Subject: [PATCH 58/63] Enables --eval code to run as a preamble. --- README.md | 2 +- src/cli.c | 47 +++++++++++++++++++++++++++++------------------ src/cli.h | 6 +++--- src/debugger.c | 4 ++++ src/main.c | 38 +++++++++++++++++++++++++------------- 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 5a89411..7653224 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ into a terminal. The terminal app takes a number of optional arguments: | `-h`, `--help [query]` | Show CLI help, or look up a language help topic | | `-v`, `--version` | Show version information | | `-c`, `--check` | Check syntax without executing | -| `-e`, `--eval ` | Execute a code string | +| `-e`, `--eval ` | Execute a code string (before file/REPL if combined) | | `-i`, `--interactive` | Enter REPL after running a file | | `-l`, `--list ` | List a file with syntax highlighting | | `-d`, `--disassemble` | Show disassembly | diff --git a/src/cli.c b/src/cli.c index 531b3af..26cf355 100644 --- a/src/cli.c +++ b/src/cli.c @@ -395,10 +395,31 @@ typedef struct { /** Forward declaration */ static void cli_repl(runtime_t *rt, clioptions opt); +static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt); +static runtime_t cli_newruntime(clioptions opt); +static void cli_freeruntime(runtime_t *rt); + +/** Compile preamble (if any) then src (if any) in an existing runtime. + * @returns false if a compile/run step failed. */ +static bool cli_runpreambleandsource(runtime_t *rt, const char *preamble, const char *src, clioptions opt) { + if (preamble && *preamble) { + cli_globalsrc = (char *) preamble; + if (!cli_compileandrun(rt, preamble, opt)) return false; + } + if (src) { + cli_globalsrc = (char *) src; + if (!cli_compileandrun(rt, src, opt)) return false; + } + return true; +} /** @brief Provide a command line interface */ -void cli(clioptions opt) { - cli_repl(NULL, opt); /* NULL means create new runtime */ +void cli(clioptions opt, const char *preamble) { + runtime_t rt = cli_newruntime(opt); + if (cli_runpreambleandsource(&rt, preamble, NULL, opt)) { + cli_repl(&rt, opt); + } + cli_freeruntime(&rt); } /* ********************************************************************** @@ -473,14 +494,10 @@ static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { * ********************************************************************** */ /** Compile and run source string (no file). Used by -e / --eval. */ -void cli_runstring(const char *src, clioptions opt) { +void cli_runstring(const char *src, clioptions opt, const char *preamble) { runtime_t rt = cli_newruntime(opt); - cli_globalsrc = (char *)src; - - cli_compileandrun(&rt, src, opt); - /* If interactive mode, enter REPL with same VM (cleans up on exit) */ - if (opt & CLI_INTERACTIVE) { + if (cli_runpreambleandsource(&rt, preamble, src, opt) && (opt & CLI_INTERACTIVE)) { cli_repl(&rt, opt); } cli_freeruntime(&rt); @@ -587,27 +604,21 @@ static void cli_repl(runtime_t *rt, clioptions opt) { * Load and run a file * ********************************************************************** */ -void cli_run(const char *in, clioptions opt) { +void cli_run(const char *in, clioptions opt, const char *preamble) { runtime_t rt = cli_newruntime(opt); char *src = cli_loadsource(in); - if (src) cli_globalsrc = src; file_setworkingdirectory(in); if (src) { - cli_compileandrun(&rt, src, opt); + if (cli_runpreambleandsource(&rt, preamble, src, opt) && (opt & CLI_INTERACTIVE)) { + cli_repl(&rt, opt); + } MORPHO_FREE(src); } else { printf("Could not open file '%s'.\n", in); cli_setexitcode(EXIT_FAILURE); - cli_freeruntime(&rt); - return; - } - - /* If interactive mode, enter REPL with same VM (cleans up on exit) */ - if (opt & CLI_INTERACTIVE) { - cli_repl(&rt, opt); } cli_freeruntime(&rt); } diff --git a/src/cli.h b/src/cli.h index 67ee776..35d3fd6 100644 --- a/src/cli.h +++ b/src/cli.h @@ -50,9 +50,9 @@ void cli_displaywithstyle(int col, int emph, int n, ...); void cli_emitemphasis(int emph); void cli_reporterror(error *err, vm *v); -void cli_run(const char *in, clioptions opt); -void cli_runstring(const char *src, clioptions opt); -void cli(clioptions opt); +void cli_run(const char *in, clioptions opt, const char *preamble); +void cli_runstring(const char *src, clioptions opt, const char *preamble); +void cli(clioptions opt, const char *preamble); char *cli_loadsource(const char *in); char *cli_loadstdin(void); diff --git a/src/debugger.c b/src/debugger.c index 15bef3b..c7bdc97 100644 --- a/src/debugger.c +++ b/src/debugger.c @@ -650,6 +650,10 @@ void clidebugger_enter(vm *v) { } void clidebugger_initialize(void) { + static bool initialized = false; + if (initialized) return; + initialized = true; + morpho_defineerror(DBG_PRS, ERROR_PARSE, DBG_PRS_MSG); morpho_defineerror(DBG_INFO, ERROR_PARSE, DBG_INFO_MSG); morpho_defineerror(DBG_INVLD, ERROR_PARSE, DBG_INVLD_MSG); diff --git a/src/main.c b/src/main.c index c6745fe..f4f1058 100644 --- a/src/main.c +++ b/src/main.c @@ -54,7 +54,7 @@ static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ct printf(" -h, --help [query] Show this help, or look up a language help topic\n"); printf(" -v, --version Show version information\n"); printf(" -c, --check Check syntax without executing\n"); - printf(" -e, --eval Execute code string\n"); + printf(" -e, --eval Execute code string (before file/REPL if combined)\n"); printf(" -i, --interactive Enter REPL after running file\n"); printf(" -l, --list List file with syntax highlighting\n"); printf(" -d, --disassemble Show disassembly\n"); @@ -151,13 +151,19 @@ static bool opt_list(const char *opt, const char *arg, clioptions *flags, opt_ct return false; // Don't run program after listing } +static varray_char evalcode; + static bool opt_eval(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { - (void)opt; - clidebugger_initialize(); - if (ctx && ctx->idx && ctx->argc - *ctx->idx - 1 > 0) - morpho_setargs(ctx->argc - *ctx->idx - 1, ctx->argv + *ctx->idx + 1); - cli_runstring(arg, *flags); - return false; + (void)opt; (void)flags; (void)ctx; + if (!arg) return false; + /* Accumulate -e snippets; they are compiled before the file/REPL. */ + if (evalcode.count > 0) { + evalcode.count--; /* drop trailing NUL */ + varray_charwrite(&evalcode, '\n'); + } + varray_charadd(&evalcode, (char *) arg, (int) strlen(arg)); + varray_charwrite(&evalcode, '\0'); + return true; } typedef struct { @@ -244,6 +250,7 @@ int main(int argc, const char *argv[]) { morpho_initialize(); cli_setexitcode(EXIT_SUCCESS); + varray_charinit(&evalcode); for (; i < argc && !file; i++) { const char *arg = argv[i]; @@ -255,22 +262,27 @@ int main(int argc, const char *argv[]) { if (run) { clidebugger_initialize(); if (i < argc) morpho_setargs(argc - i, argv + i); // Pass unused args to morpho + const char *preamble = (evalcode.count > 0) ? evalcode.data : NULL; if (file) { - cli_run(file, opt); + cli_run(file, opt, preamble); } else if (!inline_checktty()) { // stdin is piped/redirected - read and execute from stdin char *src = cli_loadstdin(); - if (src) { - cli_runstring(src, opt); - MORPHO_FREE(src); + if (src || preamble) { + cli_runstring(src, opt, preamble); + if (src) MORPHO_FREE(src); } + } else if (preamble && !(opt & CLI_INTERACTIVE)) { + // -e alone on a TTY: run the snippet and exit + cli_runstring(preamble, opt, NULL); } else { - // stdin is a TTY - enter REPL - cli(opt); + // stdin is a TTY - enter REPL (optionally after -e preamble) + cli(opt, preamble); } } + varray_charclear(&evalcode); morpho_finalize(); return cli_exitcode(); } From 2e38e46e87513d93761d502b3af2d3c0e5de6930 Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 15:59:47 -0400 Subject: [PATCH 59/63] Consolidate some redundant code --- src/cli.c | 238 ++++++++++++++++++++++++++----------------------- src/cli.h | 7 +- src/debugger.c | 3 +- src/debugger.h | 2 +- src/hlp.c | 1 - src/inline.c | 5 ++ src/inline.h | 3 + src/main.c | 14 ++- 8 files changed, 155 insertions(+), 118 deletions(-) diff --git a/src/cli.c b/src/cli.c index 26cf355..2834f47 100644 --- a/src/cli.c +++ b/src/cli.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,7 @@ char *cli_globalsrc=NULL; static int s_cli_lastexitcode = EXIT_SUCCESS; +static clioptions s_cli_activeopt = 0; void cli_setexitcode(int code) { s_cli_lastexitcode = code; @@ -41,6 +43,19 @@ int cli_exitcode(void) { return s_cli_lastexitcode; } +void cli_applyoptions(clioptions opt) { + s_cli_activeopt = opt; +} + +/** True when ANSI styling should be emitted. */ +bool cli_usecolor(void) { + if (s_cli_activeopt & CLI_NOCOLOR) return false; + const char *nocolor = getenv("NO_COLOR"); + if (nocolor && *nocolor) return false; + if (!inline_checkstdouttty()) return false; + return inline_checksupported(); +} + #define CLI_BUFFERSIZE 4096 #define RESET "\x1B[0m" @@ -62,22 +77,20 @@ void cli_emitemphasis(int emph) { } } -bool is_supported = false; - -/** Displays several strings with a specified style using linedit */ +/** Displays several strings with a specified style */ void cli_displaywithstyle(int col, int emph, int n, ...) { va_list args; va_start(args, n); - bool is_tty = inline_checktty() && is_supported; // Only emit escape codes if stdout is a TTY + bool color = cli_usecolor(); fflush(stdout); for (int i=0; iid, "': ", err->msg, "\n"); } -/** Warning callback */ +/** Debugger callback */ void cli_debuggercallbackfn(vm *v, void *ref) { + (void)ref; clidebugger_enter(v); } @@ -319,10 +334,11 @@ bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspa return success; } -static char *words[] = {"as", "and", "break", "class", "continue", "do", "else", "for", "false", "fn", "help", "if", "in", "import", "nil", "or", "print", "return", "true", "var", "while", "quit", "self", "super", "this", "try", "catch", NULL}; +static char *words[] = {"as", "and", "break", "catch", "class", "continue", "do", "else", "false", "fn", "for", "help", "if", "import", "in", "is", "nil", "or", "print", "quit", "return", "self", "super", "true", "try", "var", "while", "with", NULL}; /** Autocomplete function */ const char *cli_complete(const char *in, void *ref, size_t *index) { + (void)ref; size_t len=strlen(in); /* First find the last token in the input */ @@ -331,7 +347,7 @@ const char *cli_complete(const char *in, void *ref, size_t *index) { while (tok>in && !isspace(*(tok-1))) tok--; /* Ensure we have at least one character */ - if (iscntrl(*tok)) return false; + if (iscntrl(*tok)) return NULL; /* Now try to match the token against a library of words */ len=strlen(tok); @@ -434,8 +450,11 @@ static runtime_t cli_newruntime(clioptions opt) { rt.v = morpho_newvm(); rt.edit = inline_new(CLI_PROMPT); - is_supported = inline_checksupported(); // Ensure we are using a supported terminal - if (!(opt & CLI_NOCOLOR)) inline_syntaxcolor(rt.edit, cli_syntaxcolorfn, &rt.l); + cli_applyoptions(opt); + if (cli_usecolor()) { + inline_syntaxcolor(rt.edit, cli_syntaxcolorfn, &rt.l); + inline_setpalette(rt.edit, sizeof(palette)/sizeof(palette[0]), palette); + } morpho_setinputfn(rt.v, cli_inputcallbackfn, NULL); morpho_setprintfn(rt.v, cli_printcallbackfn, &rt.edit); @@ -453,12 +472,32 @@ static void cli_freeruntime(runtime_t *rt) { if (rt->c) morpho_freecompiler(rt->c); } +/** Run a compiled program according to CLI flags. */ +static bool cli_execute(runtime_t *rt, clioptions opt, error *err_out) { + if (!(opt & CLI_RUN)) return true; + + bool success; + if (opt & CLI_DEBUG) { + success = morpho_debug(rt->v, rt->p); + } else if (opt & CLI_PROFILE) { + success = morpho_profile(rt->v, rt->p); + } else { + success = morpho_run(rt->v, rt->p); + } + + if (!success) { + cli_reporterror(morpho_geterror(rt->v), rt->v); + if (err_out) *err_out = *morpho_geterror(rt->v); + } + return success; +} + /** Compile and execute source code */ static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { error err; error_init(&err); - bool success = morpho_compile((char *)src, rt->c, (opt & CLI_OPTIMIZE), &err); + bool success = morpho_compile((char *)src, rt->c, (opt & CLI_OPTIMIZE) != 0, &err); if (success) { if (opt & CLI_DISASSEMBLE) { @@ -468,22 +507,9 @@ static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { morpho_disassemble(rt->v, rt->p, NULL); } } - if (opt & CLI_RUN) { - if (opt & CLI_DEBUG) { - success = morpho_debug(rt->v, rt->p); - } else if (opt & CLI_PROFILE) { - success = morpho_profile(rt->v, rt->p); - } else { - success = morpho_run(rt->v, rt->p); - } - if (!success) { - cli_reporterror(morpho_geterror(rt->v), rt->v); - cli_setexitcode(EXIT_FAILURE); - } - } + success = cli_execute(rt, opt, NULL); } else { cli_reporterror(&err, rt->v); - cli_setexitcode(EXIT_FAILURE); } return success; @@ -503,15 +529,8 @@ void cli_runstring(const char *src, clioptions opt, const char *preamble) { cli_freeruntime(&rt); } -/** Enter REPL mode, optionally reusing existing runtime */ +/** Enter REPL mode, reusing an existing runtime */ static void cli_repl(runtime_t *rt, clioptions opt) { - bool own_runtime = (rt == NULL); - runtime_t runtime_storage; - if (own_runtime) { - runtime_storage = cli_newruntime(opt); - rt = &runtime_storage; - } - bool tty = inline_checktty(); version morphoversion; morpho_version(&morphoversion); @@ -529,9 +548,9 @@ static void cli_repl(runtime_t *rt, clioptions opt) { varray_charinit(&src); varray_charwrite(&src, '\0'); - /* Configure editor for REPL (if not already configured) */ - inline_setpalette(rt->edit, sizeof(palette)/sizeof(palette[0]), palette); - if (!(opt & CLI_NOCOLOR)) { + /* Configure editor for REPL */ + if (cli_usecolor()) { + inline_setpalette(rt->edit, sizeof(palette)/sizeof(palette[0]), palette); inline_syntaxcolor(rt->edit, cli_syntaxcolorfn, &rt->l); } inline_multiline(rt->edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); @@ -564,7 +583,7 @@ static void cli_repl(runtime_t *rt, clioptions opt) { continue; } - bool success = morpho_compile(input, rt->c, false, &err); + bool success = morpho_compile(input, rt->c, (opt & CLI_OPTIMIZE) != 0, &err); if (success) { src.count--; @@ -576,17 +595,9 @@ static void cli_repl(runtime_t *rt, clioptions opt) { if (opt & CLI_DISASSEMBLE) { morpho_disassemble(rt->v, rt->p, NULL); } - if (opt & CLI_RUN) { - success = morpho_debug(rt->v, rt->p); - if (!success) { - cli_reporterror(morpho_geterror(rt->v), rt->v); - cli_setexitcode(EXIT_FAILURE); - err = *morpho_geterror(rt->v); - } - } + success = cli_execute(rt, opt, &err); } else { cli_reporterror(&err, rt->v); - cli_setexitcode(EXIT_FAILURE); } free(input); @@ -594,10 +605,6 @@ static void cli_repl(runtime_t *rt, clioptions opt) { varray_charclear(&src); hlp_finalize(); - - if (own_runtime) { - cli_freeruntime(rt); - } } /* ********************************************************************** @@ -667,6 +674,7 @@ char *cli_loadsource(const char *in) { if (size) { /* Size the buffer to match */ if (!varray_charresize(&buffer, size+1)) { + fclose(f); return NULL; } @@ -674,10 +682,9 @@ char *cli_loadsource(const char *in) { for (char *c=buffer.data; !feof(f); c=c+strlen(c)) { if (!fgets(c, (int) (buffer.data+buffer.capacity-c), f)) { c[0]='\0'; break; } } - - fclose(f); } + fclose(f); return buffer.data; } @@ -685,84 +692,95 @@ char *cli_loadsource(const char *in) { * Source listing and disassembly * ********************************************************************** */ -/** Displays a single line of source */ -static void cli_printline(inline_editor *edit, int line, char *prompt, const char *src, int length) { +/** Create a temporary editor configured for colored display. */ +static inline_editor *cli_tempeditor(lexer *l) { + inline_editor *edit = inline_new(""); + if (!edit) return NULL; + if (cli_usecolor()) { + inline_syntaxcolor(edit, cli_syntaxcolorfn, l); + inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); + } + return edit; +} + +/** Displays a single line of source. nbytes is the span length; a trailing newline is stripped. */ +static void cli_printline(inline_editor *edit, int line, const char *prompt, const char *src, int nbytes) { printf("%s %4u : ", prompt, line); - /* Display the src line */ - char srcline[length]; - strncpy(srcline, src, length-1); - srcline[length-1]='\0'; - inline_displaywithsyntaxcoloring(edit, srcline); + char stackbuf[512]; + char *buf = (nbytes + 1 <= (int) sizeof(stackbuf)) ? stackbuf : malloc((size_t) nbytes + 1); + if (!buf) return; + memcpy(buf, src, (size_t) nbytes); + buf[nbytes] = '\0'; + if (nbytes > 0 && buf[nbytes - 1] == '\n') buf[nbytes - 1] = '\0'; + inline_displaywithsyntaxcoloring(edit, buf); printf("\n"); + if (buf != stackbuf) free(buf); } -/** Disassembles the program showing syntax colored lines of source */ -void cli_disassemblewithsrc(program *p, char *src, clioptions opt) { - inline_editor *edit = inline_new(""); - if (!edit) return; +typedef void (*cli_sourcelinefn)(inline_editor *edit, int line, const char *start, int nbytes, void *ref); + +/** Walk source lines in [startline, endline] and invoke fn for each. */ +static void cli_foreachsourceline(const char *src, int startline, int endline, + cli_sourcelinefn fn, void *ref) { + if (!src || !fn) return; lexer l; - if (!(opt & CLI_NOCOLOR)) { - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); - } - - int line=1, length=0; - for (unsigned int i=0; src[i]!='\0'; i++) { + inline_editor *edit = cli_tempeditor(&l); + if (!edit) return; + + int line = 1, length = 0; + for (unsigned int i = 0; src[i] != '\0'; i++) { length++; - if (src[i]=='\n') { - cli_printline(edit, line, ">>>", src+i-length+1, length); - morpho_disassemble(NULL, p, NULL); - line++; length=0; + if (src[i] == '\n') { + if (line >= startline && line <= endline) + fn(edit, line, src + i - length + 1, length, ref); + line++; + length = 0; } } - if (length > 0) { - cli_printline(edit, line, ">>>", src+strlen(src)-length, length+1); - morpho_disassemble(NULL, p, NULL); - } - + if (length > 0 && line >= startline && line <= endline) + fn(edit, line, src + strlen(src) - length, length, ref); + inline_free(edit); } +static void cli_listline(inline_editor *edit, int line, const char *start, int nbytes, void *ref) { + (void)ref; + cli_printline(edit, line, "", start, nbytes); +} + +static void cli_disassembleline(inline_editor *edit, int line, const char *start, int nbytes, void *ref) { + program *p = (program *) ref; + int matchline = line; + cli_printline(edit, line, ">>>", start, nbytes); + morpho_disassemble(NULL, p, &matchline); +} + +/** Disassembles the program showing syntax colored lines of source */ +void cli_disassemblewithsrc(program *p, char *src, clioptions opt) { + clioptions prev = s_cli_activeopt; + s_cli_activeopt |= opt; + cli_foreachsourceline(src, 1, INT_MAX, cli_disassembleline, p); + s_cli_activeopt = prev; +} + /** Displays a source listing from source lines start to end */ void cli_list(const char *src, int start, int end, clioptions opt) { - if (src) { - inline_editor *edit = inline_new(""); - if (!edit) return; - lexer l; - if (!(opt & CLI_NOCOLOR)) { - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); - inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); - } - - int line=1, length=0; - for (unsigned int i=0; src[i]!='\0'; i++) { - length++; - if (src[i]=='\n') { - if (line>=start && line <=end) cli_printline(edit, line, "", src+i-length+1, length); - line++; - length=0; - } - } - if (length > 0 && line>=start && line <=end) { - cli_printline(edit, line, "", src+strlen(src)-length, length+1); - } - inline_free(edit); - } + clioptions prev = s_cli_activeopt; + s_cli_activeopt |= opt; + cli_foreachsourceline(src, start, end, cli_listline, NULL); + s_cli_activeopt = prev; } /** Look up and display a help topic from the command line. */ void cli_helpquery(const char *query, clioptions opt) { - inline_editor *edit = inline_new(""); + cli_applyoptions(opt); + lexer l; + inline_editor *edit = cli_tempeditor(&l); if (!edit) { cli_setexitcode(EXIT_FAILURE); return; } - lexer l; - if (!(opt & CLI_NOCOLOR)) { - inline_syntaxcolor(edit, cli_syntaxcolorfn, &l); - inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); - } - error err; error_init(&err); bool help = hlp_initialize(); diff --git a/src/cli.h b/src/cli.h index 35d3fd6..c0ea5a1 100644 --- a/src/cli.h +++ b/src/cli.h @@ -14,8 +14,6 @@ #include "inline.h" #include "hlp.h" -#include "debugger.h" - #define CLI_DEFAULTCOLOR -1 #define CLI_ERRORCOLOR INLINE_RED #define CLI_WARNINGCOLOR INLINE_YELLOW @@ -45,6 +43,8 @@ typedef unsigned int clioptions; extern char *cli_globalsrc; void cli_setexitcode(int code); int cli_exitcode(void); +void cli_applyoptions(clioptions opt); +bool cli_usecolor(void); void cli_displaywithstyle(int col, int emph, int n, ...); void cli_emitemphasis(int emph); @@ -60,6 +60,9 @@ void cli_disassemblewithsrc(program *p, char *src, clioptions opt); void cli_list(const char *in, int start, int end, clioptions opt); void cli_helpquery(const char *query, clioptions opt); +void clidebugger_enter(vm *v); +void clidebugger_initialize(void); + #endif /* cli_h */ diff --git a/src/debugger.c b/src/debugger.c index c7bdc97..063f8a3 100644 --- a/src/debugger.c +++ b/src/debugger.c @@ -13,6 +13,7 @@ #include #include +#include "cli.h" #include "debugger.h" #ifdef _WIN32 @@ -31,7 +32,7 @@ __declspec(dllimport) extern objecttype objectstringtype; typedef struct { debugger *debug; /** Debugger */ - inline_editor *edit; /** lineeditor for output */ + inline_editor *edit; /** Inline editor for output */ error *err; /** Error structure to fill out */ char *info; /** Report any info to the user after error messages */ bool stop; diff --git a/src/debugger.h b/src/debugger.h index 31a95c7..58079ea 100644 --- a/src/debugger.h +++ b/src/debugger.h @@ -7,7 +7,7 @@ #ifndef debugger_h #define debugger_h -#include "cli.h" +#include "inline.h" #define DEBUGGER_PROMPT "@> " diff --git a/src/hlp.c b/src/hlp.c index 7b20309..7ad06e2 100644 --- a/src/hlp.c +++ b/src/hlp.c @@ -531,7 +531,6 @@ static bool hlp_copyspan(const char *src, size_t n, char *buf, size_t bufsize) { return true; } -#define RESET "\x1B[0m" /** Display one line of paragraph/list text with inline `code`, *emphasis*, **bold**, _underline_. Escapes \* \_ \` \\ output the character literally. */ static void hlp_displayline(inline_editor *edit, const char *line) { char buf[HLP_BUFFERSIZE]; diff --git a/src/inline.c b/src/inline.c index 0d77bda..bd16d02 100644 --- a/src/inline.c +++ b/src/inline.c @@ -266,6 +266,11 @@ bool inline_checktty(void) { return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO); } +/** API function to check whether stdout is a TTY. */ +bool inline_checkstdouttty(void) { + return isatty(STDOUT_FILENO); +} + /** Check whether the terminal type is supported. */ bool inline_checksupported(void) { #ifndef _WIN32 diff --git a/src/inline.h b/src/inline.h index 64dbadb..aa8f407 100644 --- a/src/inline.h +++ b/src/inline.h @@ -217,6 +217,9 @@ void inline_setgraphemewidth(inline_editor *edit, inline_widthfn fn); * @returns true if both stdin and stdout are terminals. */ bool inline_checktty(void); +/** @brief Check whether stdout is a TTY (for color/gated output when stdin may be piped). */ +bool inline_checkstdouttty(void); + /** @brief Check is the terminal is supported (i.e. likely capable of processed output) * @returns true if supported. */ bool inline_checksupported(void); diff --git a/src/main.c b/src/main.c index f4f1058..e29103b 100644 --- a/src/main.c +++ b/src/main.c @@ -73,7 +73,7 @@ static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ct static bool opt_disassembleonly(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; (void)arg; (void)ctx; - *flags ^= CLI_RUN; + *flags &= ~CLI_RUN; *flags |= CLI_DISASSEMBLE; return true; } @@ -141,13 +141,19 @@ static bool opt_interactive(const char *opt, const char *arg, clioptions *flags, static bool opt_list(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { (void)opt; (void)ctx; - if (!arg) return false; + if (!arg) { + cli_setexitcode(EXIT_FAILURE); + return false; + } char *src = cli_loadsource(arg); if (src) { cli_list(src, 1, INT_MAX, *flags); MORPHO_FREE(src); - } else fprintf(stderr, "morpho: Could not open file '%s'\n", arg); + } else { + fprintf(stderr, "morpho: Could not open file '%s'\n", arg); + cli_setexitcode(EXIT_FAILURE); + } return false; // Don't run program after listing } @@ -259,6 +265,8 @@ int main(int argc, const char *argv[]) { } else if (arg) file = arg; } + cli_applyoptions(opt); + if (run) { clidebugger_initialize(); if (i < argc) morpho_setargs(argc - i, argv + i); // Pass unused args to morpho From 91096adefcacfd2eaef125b4735f03317b943ecb Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 16:21:53 -0400 Subject: [PATCH 60/63] Add CI --- .github/scripts/smoke-test.sh | 84 +++++++++++++++++++++++++++++ .github/workflows/basic-install.yml | 38 +++++++++++++ 2 files changed, 122 insertions(+) create mode 100755 .github/scripts/smoke-test.sh create mode 100644 .github/workflows/basic-install.yml diff --git a/.github/scripts/smoke-test.sh b/.github/scripts/smoke-test.sh new file mode 100755 index 0000000..d62f682 --- /dev/null +++ b/.github/scripts/smoke-test.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Smoke tests for morpho6 CLI (install / PR gate). +set -euo pipefail + +MORPHO6="${MORPHO6:-morpho6}" + +expect_ok() { + local desc="$1" + shift + echo "==> $desc" + "$@" +} + +expect_fail() { + local desc="$1" + shift + echo "==> $desc (expect failure)" + if "$@"; then + echo "expected failure, but succeeded: $*" >&2 + exit 1 + fi +} + +expect_contains() { + local desc="$1" + local needle="$2" + shift 2 + echo "==> $desc" + local out + out="$("$@" 2>&1)" || true + if ! grep -Fq -- "$needle" <<<"$out"; then + echo "expected output to contain: $needle" >&2 + echo "got:" >&2 + echo "$out" >&2 + exit 1 + fi +} + +# Basic eval / version / help +expect_ok "version" "$MORPHO6" --version +expect_ok "eval" "$MORPHO6" -e 'print(1)' +expect_ok "multiple -e preamble" "$MORPHO6" -e 'var x = 1' -e 'print(x)' + +# Help queries +expect_contains "CLI usage" "Usage: morpho6" "$MORPHO6" --help +expect_contains "help index topic" "Topics:" "$MORPHO6" --no-color --help help +expect_contains "help Matrix" "Matrix" "$MORPHO6" --no-color --help Matrix +expect_fail "unknown help topic" "$MORPHO6" --no-color --help NotARealTopicXYZ123 + +# Option parsing +expect_ok "workers" "$MORPHO6" --workers 2 -e 'print(1)' +expect_ok "disassemble only" "$MORPHO6" -D -e 'print(1)' +expect_contains "disassemble with source" ">>> 1 :" "$MORPHO6" --no-color -dl -e 'print(1)' + +# -D must not execute (disassembly may still mention constants) +out="$("$MORPHO6" -D -e 'print("EXECUTED")' 2>&1)" || true +if grep -Eq '^EXECUTED$' <<<"$out"; then + echo "-D should not execute the program" >&2 + echo "$out" >&2 + exit 1 +fi + +# Failure exit codes +expect_fail "missing list file" "$MORPHO6" -l /no/such/file.morpho +expect_fail "bad eval" "$MORPHO6" -e 'var x =' +expect_fail "unknown option" "$MORPHO6" --not-a-real-option + +# File + preamble +tmp="$(mktemp /tmp/morpho-smoke.XXXXXX.morpho)" +trap 'rm -f "$tmp"' EXIT +printf 'print(x)\n' >"$tmp" +expect_ok "eval preamble + file" "$MORPHO6" -e 'var x = 7' "$tmp" + +# Piped stdin + preamble +expect_ok "eval preamble + stdin" bash -c "printf 'print(x)\n' | $MORPHO6 -e 'var x = 3'" + +# NO_COLOR / --no-color should not emit CSI sequences for help +out="$(NO_COLOR=1 "$MORPHO6" --help Matrix 2>&1)" || true +if grep -Eq $'\x1B\[' <<<"$out"; then + echo "NO_COLOR=1 still produced ANSI escapes" >&2 + exit 1 +fi + +echo "All smoke tests passed." diff --git a/.github/workflows/basic-install.yml b/.github/workflows/basic-install.yml new file mode 100644 index 0000000..b786d54 --- /dev/null +++ b/.github/workflows/basic-install.yml @@ -0,0 +1,38 @@ +name: Basic install + +on: + push: + branches: [ "main", "inline" ] + pull_request: + branches: [ "main", "inline" ] + +jobs: + basic-install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install Morpho dependencies + run: | + sudo apt update + sudo apt install -y libsuitesparse-dev liblapacke-dev libunistring-dev + + - name: Build and install Morpho + run: | + git clone --depth 1 --branch dev https://github.com/Morpho-lang/morpho.git + cd morpho + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release + sudo cmake --install build --config Release + sudo ldconfig + + - name: Build and install morpho-cli + run: | + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release + sudo cmake --install build --config Release + + - name: Smoke test morpho6 + run: | + chmod +x .github/scripts/smoke-test.sh + .github/scripts/smoke-test.sh From b6305db1ac68e97aab472d4e31974dde6a4bbaa2 Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 16:29:12 -0400 Subject: [PATCH 61/63] Allow a raw query "help" query to generate a topic list --- src/cli.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli.c b/src/cli.c index 2834f47..c129dcf 100644 --- a/src/cli.c +++ b/src/cli.c @@ -169,7 +169,8 @@ static bool cli_help(inline_editor *edit, char *query, error *err, bool avail) { varray_charclear(&result); } - if (blank) { // Display list of topics + /* Index page promises a topic list below; show it for blank or "help". */ + if (blank || strcmp(q, "help") == 0) { varray_value list; varray_valueinit(&list); morpho_helptopics(&list); From 736dd17d781ad84276ca95bc41cc72fc924689cd Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 16:53:10 -0400 Subject: [PATCH 62/63] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7653224..23f9f0b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Morpho-cli -A terminal app for the [morpho language](https://github.com/Morpho-lang/morpho). Improved unicode support requires an external library e.g. [libgrapheme](https://libs.suckless.org/libgrapheme/) or [libunistring](https://www.gnu.org/software/libunistring/). +A terminal app for the [morpho language](https://github.com/Morpho-lang/morpho) using the [inline line editor](https://github.com/Morpho-lang/inline). Improved unicode support requires an external library e.g. [libgrapheme](https://libs.suckless.org/libgrapheme/) or [libunistring](https://www.gnu.org/software/libunistring/). Like morpho itself, Morpho-cli is released under an MIT license (see the LICENSE file for details). See the [main morpho repository](https://github.com/Morpho-lang/morpho) for information about contributing etc. Please report any issues or feature requests about the morpho-cli terminal app specifically here: suggestions for improvement are very welcome. From cc67fdc77a9adb56344bb6f031146c4fd5144381 Mon Sep 17 00:00:00 2001 From: softmattertheory Date: Tue, 28 Jul 2026 21:49:32 -0400 Subject: [PATCH 63/63] Route write through inline_write to avoid no check warnings --- src/inline.c | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/inline.c b/src/inline.c index bd16d02..9c084dc 100644 --- a/src/inline.c +++ b/src/inline.c @@ -1217,9 +1217,18 @@ static void inline_ensurecursorvisible(inline_editor *edit) { #define TERM_FAINT "\x1b[2m" #define TERM_INVERSEVIDEO "\x1b[7m" +/** Write bytes to stdout (return value checked to silence warn_unused_result). */ +static void inline_write(const void *buf, size_t n) { +#ifdef _WIN32 + (void) write(STDOUT_FILENO, buf, (unsigned int) n); +#else + if (write(STDOUT_FILENO, buf, n) < 0) { /* ignore */ } +#endif +} + /** Write an escape sequence to the terminal */ void inline_emit(const char *seq) { - write(STDOUT_FILENO, seq, (unsigned int) strlen(seq)); + inline_write(seq, strlen(seq)); } /** Writes an escape sequence to produce a given color */ @@ -1240,7 +1249,7 @@ void inline_emitcolor(int color) { n = snprintf(seq, sizeof(seq), "\x1b[38;2;%d;%d;%dm", r, g, b); } - if (n > 0) write(STDOUT_FILENO, seq, n); + if (n > 0) inline_write(seq, (size_t) n); } /** Clip grapheme range [*g_start, *g_end) horizontally based on viewport */ @@ -1278,12 +1287,12 @@ static inline void inline_clipgraphemerange(inline_editor *edit, int line_start, /** Move terminal cursor to the editor's origin */ static inline void inline_movetoorigin(inline_editor *edit) { - write(STDOUT_FILENO, "\r", 1); // Move to start of current line + inline_write("\r", 1); // Move to start of current line if (edit->term_cursor_row > 0) { // Move up cursor_row lines char seq[INLINE_ESCAPECODE_MAXLENGTH]; int n = snprintf(seq, sizeof(seq), "\x1b[%dA", edit->term_cursor_row); - write(STDOUT_FILENO, seq, n); + if (n > 0) inline_write(seq, (size_t) n); } } @@ -1293,14 +1302,14 @@ static inline void inline_moveby(int dx, int dy) { if (dy<0) { // Up int n = snprintf(seq, sizeof(seq), "\x1b[%dA", abs(dy)); - write(STDOUT_FILENO, seq, n); + if (n > 0) inline_write(seq, (size_t) n); } else { for (int i = 0; i < dy; i++) inline_emit("\n"); // Ensure scroll } if (dx!=0) { // Horizontal int n = snprintf(seq, sizeof(seq), "\x1b[%d%c", abs(dx), (dx < 0 ? 'D' : 'C')); - write(STDOUT_FILENO, seq, n); + if (n > 0) inline_write(seq, (size_t) n); } } @@ -1316,7 +1325,7 @@ static inline void inline_moveby(int dx, int dy) { * and prompt widt, or -1 if outside clipping window; otherwise not changed. */ static void inline_renderline(inline_editor *edit, const char *prompt, size_t byte_start, size_t byte_end, int logical_cursor_col, bool is_last, int *rendered_cursor_col) { - write(STDOUT_FILENO, prompt, (unsigned int) strlen(prompt)); // Write prompt + inline_write(prompt, strlen(prompt)); // Write prompt int prompt_width = 0; // Calculate its display width if (!inline_stringwidth(edit, prompt, &prompt_width)) prompt_width = 0; @@ -1391,7 +1400,7 @@ static void inline_renderline(inline_editor *edit, const char *prompt, size_t by if (edit->buffer[gs] == '\t') { for (int i=0; ibuffer + gs, (unsigned int) (ge - gs)); + } else inline_write(edit->buffer + gs, ge - gs); rendered_width += width_fn(edit->buffer + gs, ge - gs); } @@ -1414,7 +1423,7 @@ static void inline_renderline(inline_editor *edit, const char *prompt, size_t by if (ghost_width <= remaining_cols) { // Show suggestion as faint text edit->suggestion_shown=true; inline_emit(TERM_FAINT); - write(STDOUT_FILENO, suffix, (unsigned int) strlen(suffix)); + inline_write(suffix, strlen(suffix)); inline_emit(TERM_RESETCOLOR); } } @@ -1459,7 +1468,7 @@ static void inline_redraw(inline_editor *edit) { inline_emit(TERM_CLEAR); } - write(STDOUT_FILENO, "\r", 1); // Move to start of line + inline_write("\r", 1); // Move to start of line inline_moveby(rendered_cursor_col, cursor_row - edit->line_count - extra + 1); edit->term_cursor_row = cursor_row; // Record cursor row edit->term_lines_drawn = edit->line_count; // Record no. of lines drawn @@ -1473,7 +1482,7 @@ void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string) { size_t len = strlen(string); if (!edit->syntax_fn || !edit->palette_count) { // Syntax highlighting not configured, fallback to plain - write(STDOUT_FILENO, string, (unsigned int) len); + inline_write(string, len); return; } @@ -2178,7 +2187,7 @@ static void inline_supported(inline_editor *edit) { inline_disablerawmode(edit); if (edit->buffer_len > 0) inline_addhistory(edit, edit->buffer); // Add to history if non-empty - write(STDOUT_FILENO, "\r\n", 2); + inline_write("\r\n", 2); } /** API function to read a line of text from the user.