From 9bb083a6f8dacd6d59d12739766869116aeea1b5 Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Thu, 25 Jun 2026 18:57:53 +1000 Subject: [PATCH] some cool things --- include/ground.h | 9 + meson.build | 12 +- src/Bytecode/Instruction/execute.c | 4 + src/Stringify/Arg.c | 45 + src/Stringify/Heap.c | 29 + src/Stringify/Instruction.c | 298 +++++ src/Stringify/Program.c | 25 + src/Stringify/String.c | 12 + src/Stringify/Value.c | 67 ++ src/include/estr.h | 52 + src/libmain.c | 17 + src/linenoise/.gitignore | 4 + src/linenoise/LICENSE | 25 + src/linenoise/Makefile | 13 + src/linenoise/README.markdown | 380 ++++++ src/linenoise/example.c | 124 ++ src/linenoise/linenoise-test.c | 1297 ++++++++++++++++++++ src/linenoise/linenoise.c | 1762 ++++++++++++++++++++++++++++ src/linenoise/linenoise.h | 114 ++ 19 files changed, 4288 insertions(+), 1 deletion(-) create mode 100644 src/Stringify/Arg.c create mode 100644 src/Stringify/Heap.c create mode 100644 src/Stringify/Instruction.c create mode 100644 src/Stringify/Program.c create mode 100644 src/Stringify/String.c create mode 100644 src/Stringify/Value.c create mode 100644 src/include/estr.h create mode 100644 src/linenoise/.gitignore create mode 100644 src/linenoise/LICENSE create mode 100644 src/linenoise/Makefile create mode 100644 src/linenoise/README.markdown create mode 100644 src/linenoise/example.c create mode 100644 src/linenoise/linenoise-test.c create mode 100644 src/linenoise/linenoise.c create mode 100644 src/linenoise/linenoise.h diff --git a/include/ground.h b/include/ground.h index 840bd81..75f22f8 100644 --- a/include/ground.h +++ b/include/ground.h @@ -439,6 +439,15 @@ struct _Ground { GroundValue* (*get) (GroundBytecodeHeap* heap, GroundSize idx); } Heap; } Bytecode; + + struct { + char* (*Arg)(GroundArg* arg); + char* (*Heap)(GroundBytecodeHeap* heap); + char* (*Instruction)(GroundInstruction* instruction); + char* (*Program)(GroundProgram* program); + char* (*String)(GroundString* string); + char* (*Value)(GroundValue* value); + } Stringify; }; extern struct _Ground Ground; diff --git a/meson.build b/meson.build index cf0c7e3..5910251 100644 --- a/meson.build +++ b/meson.build @@ -98,7 +98,17 @@ sources = files( 'src/State/findLabel.c', 'src/State/findVariable.c', - 'src/Struct/addField.c' + 'src/Stringify/Arg.c', + 'src/Stringify/Heap.c', + 'src/Stringify/Instruction.c', + 'src/Stringify/Program.c', + 'src/Stringify/String.c', + 'src/Stringify/Value.c', + + 'src/Struct/addField.c', + + + 'src/linenoise/linenoise.c' ) cli_sources = files( diff --git a/src/Bytecode/Instruction/execute.c b/src/Bytecode/Instruction/execute.c index c4c1af1..1c770ad 100644 --- a/src/Bytecode/Instruction/execute.c +++ b/src/Bytecode/Instruction/execute.c @@ -1,4 +1,5 @@ #include "../../../include/ground.h" +#include "../../linenoise/linenoise.h" #include #include #include @@ -65,6 +66,9 @@ int64_t _GroundBytecodeInstructionExecute(GroundBytecodeInstruction* instruction return -2; } INPUT: { + char* input = linenoise(""); + HEAP_SET(heap, instruction->args.at[0], Ground.New.Value.String(Ground.New.String(input))); + free(input); return -1; } PRINT: { diff --git a/src/Stringify/Arg.c b/src/Stringify/Arg.c new file mode 100644 index 0000000..754cf8d --- /dev/null +++ b/src/Stringify/Arg.c @@ -0,0 +1,45 @@ +#include "../../include/ground.h" + +char* _GroundStringifyArg(GroundArg* arg) { + if (arg->type == GroundArg_Value) { + return Ground.Stringify.Value(&arg->as.value); + } else { + char* buf = malloc(strlen(arg->as.ref->string) + 2); // add 2 for sigil + null byte + if (buf == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("malloc failed in Ground.Stringify.Arg"); + return NULL; + } + switch (arg->type) { + case GroundArg_ValueRef: { + buf[0] = '$'; + break; + } + case GroundArg_DirectRef: { + buf[0] = '&'; + break; + } + case GroundArg_LineRef: { + buf[0] = '%'; + break; + } + case GroundArg_Label: { + buf[0] = '@'; + break; + } + case GroundArg_FunctionRef: { + buf[0] = '!'; + break; + } + case GroundArg_TypeRef: { + buf[0] = '-'; + break; + } + case GroundArg_Value: { + break; + } + } + memcpy(buf + 1, arg->as.ref->string, strlen(arg->as.ref->string) + 1); + return buf; + } +} diff --git a/src/Stringify/Heap.c b/src/Stringify/Heap.c new file mode 100644 index 0000000..b4f384c --- /dev/null +++ b/src/Stringify/Heap.c @@ -0,0 +1,29 @@ +#include "../../include/ground.h" +#include "../include/estr.h" +#include + +char* _GroundStringifyHeap(GroundBytecodeHeap* heap) { + + Estr heapString = CREATE_ESTR(""); + + for (GroundSize i = 0; i < heap->len; i++) { + char* string = Ground.Stringify.Value(&heap->heap[i]); + if (Ground.Flags.error) { + return NULL; + } + if (string == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("Ground.Stringify.Value unexpectedly returned NULL in Ground.Stringify.Heap"); + return NULL; + } + + char* buf = malloc(snprintf(NULL, 0, "#%" PRIu64 ": %s\n", i, string) + 1); + sprintf(buf, "#%" PRIu64 ": %s\n", i, string); + + APPEND_ESTR(heapString, buf); + + free(string); + } + + return heapString.str; +} diff --git a/src/Stringify/Instruction.c b/src/Stringify/Instruction.c new file mode 100644 index 0000000..d521b65 --- /dev/null +++ b/src/Stringify/Instruction.c @@ -0,0 +1,298 @@ +#include "../../include/ground.h" +#include "../include/estr.h" + +char* _GroundStringifyInstruction(GroundInstruction* instruction) { + Estr estr; + switch (instruction->type) { + + case GroundInstruction_IF: { + estr = CREATE_ESTR("IF "); + break; + } + + case GroundInstruction_JUMP: { + estr = CREATE_ESTR("JUMP "); + break; + } + + case GroundInstruction_END: { + estr = CREATE_ESTR("END "); + break; + } + + case GroundInstruction_INPUT: { + estr = CREATE_ESTR("INPUT "); + break; + } + + case GroundInstruction_PRINT: { + estr = CREATE_ESTR("PRINT "); + break; + } + + case GroundInstruction_PRINTLN: { + estr = CREATE_ESTR("PRINTLN "); + break; + } + + case GroundInstruction_SET: { + estr = CREATE_ESTR("SET "); + break; + } + + case GroundInstruction_GETTYPE: { + estr = CREATE_ESTR("GETTYPE "); + break; + } + + case GroundInstruction_EXISTS: { + estr = CREATE_ESTR("EXISTS "); + break; + } + + case GroundInstruction_SETLIST: { + estr = CREATE_ESTR("SETLIST "); + break; + } + + case GroundInstruction_SETLISTAT: { + estr = CREATE_ESTR("SETLISTAT "); + break; + } + + case GroundInstruction_GETLISTAT: { + estr = CREATE_ESTR("GETLISTAT "); + break; + } + + case GroundInstruction_GETLISTSIZE: { + estr = CREATE_ESTR("GETLISTSIZE "); + break; + } + + case GroundInstruction_LISTAPPEND: { + estr = CREATE_ESTR("LISTAPPEND "); + break; + } + + case GroundInstruction_GETSTRSIZE: { + estr = CREATE_ESTR("GETSTRSIZE "); + break; + } + + case GroundInstruction_GETSTRCHARAT: { + estr = CREATE_ESTR("GETSTRCHARAT "); + break; + } + + case GroundInstruction_ADD: { + estr = CREATE_ESTR("ADD "); + break; + } + + case GroundInstruction_SUBTRACT: { + estr = CREATE_ESTR("SUBTRACT "); + break; + } + + case GroundInstruction_MULTIPLY: { + estr = CREATE_ESTR("MULTIPLY "); + break; + } + + case GroundInstruction_DIVIDE: { + estr = CREATE_ESTR("DIVIDE "); + break; + } + + case GroundInstruction_EQUAL: { + estr = CREATE_ESTR("EQUAL "); + break; + } + + case GroundInstruction_INEQUAL: { + estr = CREATE_ESTR("INEQUAL "); + break; + } + + case GroundInstruction_NOT: { + estr = CREATE_ESTR("NOT "); + break; + } + + case GroundInstruction_GREATER: { + estr = CREATE_ESTR("GREATER "); + break; + } + + case GroundInstruction_LESSER: { + estr = CREATE_ESTR("LESSER "); + break; + } + + case GroundInstruction_AND: { + estr = CREATE_ESTR("AND "); + break; + } + + case GroundInstruction_OR: { + estr = CREATE_ESTR("OR "); + break; + } + + case GroundInstruction_XOR: { + estr = CREATE_ESTR("XOR "); + break; + } + + case GroundInstruction_NEG: { + estr = CREATE_ESTR("NEG "); + break; + } + + case GroundInstruction_SHIFT: { + estr = CREATE_ESTR("SHIFT "); + break; + } + + case GroundInstruction_STOI: { + estr = CREATE_ESTR("STOI "); + break; + } + + case GroundInstruction_STOD: { + estr = CREATE_ESTR("STOD "); + break; + } + + case GroundInstruction_ITOC: { + estr = CREATE_ESTR("ITOC "); + break; + } + + case GroundInstruction_CTOI: { + estr = CREATE_ESTR("CTOI "); + break; + } + + case GroundInstruction_TOSTRING: { + estr = CREATE_ESTR("TOSTRING "); + break; + } + + case GroundInstruction_FUN: { + estr = CREATE_ESTR("FUN "); + break; + } + + case GroundInstruction_RETURN: { + estr = CREATE_ESTR("RETURN "); + break; + } + + case GroundInstruction_ENDFUN: { + estr = CREATE_ESTR("ENDFUN "); + break; + } + + case GroundInstruction_CALL: { + estr = CREATE_ESTR("CALL "); + break; + } + + case GroundInstruction_CALLMETHOD: { + estr = CREATE_ESTR("CALLMETHOD "); + break; + } + + case GroundInstruction_STRUCT: { + estr = CREATE_ESTR("STRUCT "); + break; + } + + case GroundInstruction_ENDSTRUCT: { + estr = CREATE_ESTR("ENDSTRUCT "); + break; + } + + case GroundInstruction_INIT: { + estr = CREATE_ESTR("INIT "); + break; + } + + case GroundInstruction_GETFIELD: { + estr = CREATE_ESTR("GETFIELD "); + break; + } + + case GroundInstruction_SETFIELD: { + estr = CREATE_ESTR("SETFIELD "); + break; + } + + case GroundInstruction_USE: { + estr = CREATE_ESTR("USE "); + break; + } + + case GroundInstruction_EXTERN: { + estr = CREATE_ESTR("EXTERN "); + break; + } + + case GroundInstruction_CREATELABEL: { + estr = CREATE_ESTR("CREATELABEL "); + break; + } + + case GroundInstruction_PAUSE: { + estr = CREATE_ESTR("PAUSE "); + break; + } + + case GroundInstruction_DROP: { + estr = CREATE_ESTR("DROP "); + break; + } + + case GroundInstruction_LICENSE: { + estr = CREATE_ESTR("LICENSE "); + break; + } + + case GroundInstruction_ERROR: { + estr = CREATE_ESTR("ERROR "); + break; + } + + case GroundInstruction_THROW: { + estr = CREATE_ESTR("THROW "); + break; + } + + case GroundInstruction_CATCH: { + estr = CREATE_ESTR("CATCH "); + break; + } + + } + + for (GroundSize i = 0; i < instruction->args.len; i++) { + char* arg = Ground.Stringify.Arg(&instruction->args.at[i]); + if (Ground.Flags.error) { + DESTROY_ESTR(estr); + return NULL; + } + if (arg == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("Ground.Stringify.Arg unexpectedly returned NULL in Ground.Stringify.Instruction"); + DESTROY_ESTR(estr); + return NULL; + } + + APPEND_ESTR(estr, arg); + free(arg); + } + + return estr.str; +} diff --git a/src/Stringify/Program.c b/src/Stringify/Program.c new file mode 100644 index 0000000..d9894b5 --- /dev/null +++ b/src/Stringify/Program.c @@ -0,0 +1,25 @@ +#include "../../include/ground.h" +#include "../include/estr.h" + +char* _GroundStringifyProgram(GroundProgram* program) { + Estr estr = CREATE_ESTR(""); + for (size_t i = 0; i < program->len; i++) { + char* arg = Ground.Stringify.Instruction(&program->at[i]); + if (Ground.Flags.error) { + DESTROY_ESTR(estr); + return NULL; + } + if (arg == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("Ground.Stringify.Instruction unexpectedly returned null in Ground.Stringify.Program"); + return NULL; + } + + APPEND_ESTR(estr, arg); + APPEND_ESTR(estr, " "); + free(arg); + } + + return estr.str; + +} diff --git a/src/Stringify/String.c b/src/Stringify/String.c new file mode 100644 index 0000000..9d52267 --- /dev/null +++ b/src/Stringify/String.c @@ -0,0 +1,12 @@ +#include "../../include/ground.h" + +char* _GroundStringifyString(GroundString* string) { + char* out = malloc(string->len + 1); + if (out == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("malloc failed in Ground.Stringify.String"); + return NULL; + } + memcpy(out, string->cstr, string->len + 1); + return out; +} diff --git a/src/Stringify/Value.c b/src/Stringify/Value.c new file mode 100644 index 0000000..e0da102 --- /dev/null +++ b/src/Stringify/Value.c @@ -0,0 +1,67 @@ +#include "../../include/ground.h" +#include + +char* _GroundStringifyValue(GroundValue* value) { + switch (value->type.type) { + case GroundType_Int: { + char* buf = malloc(snprintf(NULL, 0, "%" PRId64, value->as.Int) + 1); + if (buf == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("malloc failed in Ground.Stringify.Value"); + return NULL; + } + sprintf(buf, "%" PRId64, value->as.Int); + return buf; + } + case GroundType_Double: { + char* buf = malloc(snprintf(NULL, 0, "%f", value->as.Double) + 1); + if (buf == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("malloc failed in Ground.Stringify.Value"); + return NULL; + } + sprintf(buf, "%f", value->as.Double); + return buf; + } + case GroundType_Char: { + char* buf = malloc(snprintf(NULL, 0, "%c", value->as.Char) + 1); + if (buf == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("malloc failed in Ground.Stringify.Value"); + return NULL; + } + sprintf(buf, "%c", value->as.Char); + return buf; + } + case GroundType_Bool: { + char* buf = malloc(6); // max(len(true), len(false)) + 1 + if (buf == NULL) { + Ground.Flags.error = true; + Ground.Log.Error("malloc failed in Ground.Stringify.Value"); + return NULL; + } + sprintf(buf, value->as.Bool ? "true" : "false"); + return buf; + } + case GroundType_String: { + return Ground.Stringify.String(&value->as.String); + } + case GroundType_List: { + // TODO implement list stringification + } + case GroundType_Function: { + // TODO implement function stringification + } + case GroundType_Struct: { + // TODO implement struct stringification + } + case GroundType_Object: { + // TODO implement object stringification + } + + } + + Ground.Flags.error = true; + Ground.Log.Error("FIXME implement all cases in Ground.Stringify.Value"); + return NULL; +} diff --git a/src/include/estr.h b/src/include/estr.h new file mode 100644 index 0000000..df5d172 --- /dev/null +++ b/src/include/estr.h @@ -0,0 +1,52 @@ +#include +#include +#include +#include + +#ifndef ESTR_H +#define ESTR_H + +/* + + estr.h - Easy string manipulation + This library has macros to allow easier manipulation of strings. No longer shall + you have to malloc and realloc away to keep adding to your strings. + + Usage: + + Estr myString = CREATE_ESTR("my awesome string"); + APPEND_ESTR(myString, " is so cool"); + printf("%s\n", myString.str); + +*/ + +#define CREATE_ESTR(instr) \ + (Estr) { \ + .str = instr,\ + .size = strlen(instr),\ + .shouldBeFreed = 0, \ + .destroyed = 0 \ + } + +#define APPEND_ESTR(estr, instr) { \ + estr.size = estr.size + strlen(instr); \ + char* tmp_ptr = malloc(estr.size + 1); \ + if (tmp_ptr == NULL) printf("WARNING: Could not realloc estr " #estr "\n"); \ + else { \ + snprintf(tmp_ptr, estr.size + 1, "%s%s", estr.str, instr); \ + if (estr.shouldBeFreed > 0) free(estr.str); \ + estr.shouldBeFreed = 1; \ + estr.str = tmp_ptr; \ + } \ +} + +#define DESTROY_ESTR(estr) if (estr.shouldBeFreed > 0 && estr.destroyed < 1) free(estr.str); + +typedef struct Estr { + char* str; + size_t size; + int8_t shouldBeFreed; + int8_t destroyed; +} Estr; + +#endif // ESTR_H diff --git a/src/libmain.c b/src/libmain.c index 4cf8434..0d4fd60 100644 --- a/src/libmain.c +++ b/src/libmain.c @@ -107,6 +107,14 @@ void _GroundBytecodeHeapSet(GroundBytecodeHeap* heap, GroundSize idx, GroundValu GroundValue* _GroundBytecodeHeapGet(GroundBytecodeHeap* heap, GroundSize idx); +char* _GroundStringifyArg(GroundArg* arg); +char* _GroundStringifyHeap(GroundBytecodeHeap* heap); +char* _GroundStringifyInstruction(GroundInstruction* instruction); +char* _GroundStringifyProgram(GroundProgram* program); +char* _GroundStringifyString(GroundString* string); +char* _GroundStringifyValue(GroundValue* value); + + struct _Ground Ground = { .Flags = { @@ -240,4 +248,13 @@ struct _Ground Ground = { .get = _GroundBytecodeHeapGet, }, }, + + .Stringify = { + .Arg = _GroundStringifyArg, + .Heap = _GroundStringifyHeap, + .Instruction = _GroundStringifyInstruction, + .Program = _GroundStringifyProgram, + .String = _GroundStringifyString, + .Value = _GroundStringifyValue, + }, }; diff --git a/src/linenoise/.gitignore b/src/linenoise/.gitignore new file mode 100644 index 0000000..c44db86 --- /dev/null +++ b/src/linenoise/.gitignore @@ -0,0 +1,4 @@ +linenoise-example +linenoise-test +*.dSYM +history.txt diff --git a/src/linenoise/LICENSE b/src/linenoise/LICENSE new file mode 100644 index 0000000..18e8148 --- /dev/null +++ b/src/linenoise/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2010-2014, Salvatore Sanfilippo +Copyright (c) 2010-2013, Pieter Noordhuis + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/linenoise/Makefile b/src/linenoise/Makefile new file mode 100644 index 0000000..9688de3 --- /dev/null +++ b/src/linenoise/Makefile @@ -0,0 +1,13 @@ +all: linenoise-example linenoise-test + +linenoise-example: linenoise.h linenoise.c example.c + $(CC) -Wall -W -Os -g -o linenoise-example linenoise.c example.c + +linenoise-test: linenoise-test.c linenoise-example + $(CC) -Wall -W -Os -g -o linenoise-test linenoise-test.c + +test: linenoise-test linenoise-example + ./linenoise-test + +clean: + rm -f linenoise-example linenoise-test diff --git a/src/linenoise/README.markdown b/src/linenoise/README.markdown new file mode 100644 index 0000000..4964c84 --- /dev/null +++ b/src/linenoise/README.markdown @@ -0,0 +1,380 @@ +# Linenoise + +A minimal, zero-config, BSD licensed, readline replacement used in Redis, +MongoDB, Android and many other projects. + +* Single and multi line editing mode with the usual key bindings implemented. +* History handling. +* Completion. +* Hints (suggestions at the right of the prompt as you type). +* Multiplexing mode, with prompt hiding/restoring for asynchronous output. +* UTF-8 support for multi-byte characters and emoji. +* About ~1100 lines (comments and spaces excluded) of BSD license source code. +* Only uses a subset of VT100 escapes (ANSI.SYS compatible). + +## Can a line editing library be 20k lines of code? + +Line editing with some support for history is a really important feature for command line utilities. Instead of retyping almost the same stuff again and again it's just much better to hit the up arrow and edit on syntax errors, or in order to try a slightly different command. But apparently code dealing with terminals is some sort of Black Magic: readline is 30k lines of code, libedit 20k. Is it reasonable to link small utilities to huge libraries just to get a minimal support for line editing? + +So what usually happens is either: + + * Large programs with configure scripts disabling line editing if readline is not present in the system, or not supporting it at all since readline is GPL licensed and libedit (the BSD clone) is not as known and available as readline is (real world example of this problem: Tclsh). + * Smaller programs not using a configure script not supporting line editing at all (A problem we had with `redis-cli`, for instance). + +The result is a pollution of binaries without line editing support. + +So I spent more or less two hours doing a reality check resulting in this little library: is it *really* needed for a line editing library to be 20k lines of code? Apparently not, it is possibe to get a very small, zero configuration, trivial to embed library, that solves the problem. Smaller programs will just include this, supporting line editing out of the box. Larger programs may use this little library or just checking with configure if readline/libedit is available and resorting to Linenoise if not. + +## Terminals, in 2010. + +Apparently almost every terminal you can happen to use today has some kind of support for basic VT100 escape sequences. So I tried to write a lib using just very basic VT100 features. The resulting library appears to work everywhere I tried to use it, and now can work even on ANSI.SYS compatible terminals, since no +VT220 specific sequences are used anymore. + +The library is currently about 850 lines of code. In order to use it in your project just look at the *example.c* file in the source distribution, it is pretty straightforward. The library supports both a blocking mode and a multiplexing mode, see the API documentation later in this file for more information. + +Linenoise is BSD-licensed code, so you can use both in free software and commercial software. + +## Tested with... + + * Linux text only console ($TERM = linux) + * Linux KDE terminal application ($TERM = xterm) + * Linux xterm ($TERM = xterm) + * Linux Buildroot ($TERM = vt100) + * Mac OS X iTerm ($TERM = xterm) + * Mac OS X default Terminal.app ($TERM = xterm) + * OpenBSD 4.5 through an OSX Terminal.app ($TERM = screen) + * IBM AIX 6.1 + * FreeBSD xterm ($TERM = xterm) + * ANSI.SYS + * Emacs comint mode ($TERM = dumb) + +Please test it everywhere you can and report back! + +## Let's push this forward! + +Patches should be provided in the respect of Linenoise sensibility for small +easy to understand code. + +Send feedbacks to antirez at gmail + +# The API + +Linenoise is very easy to use, and reading the example shipped with the +library should get you up to speed ASAP. Here is a list of API calls +and how to use them. Let's start with the simple blocking mode: + + char *linenoise(const char *prompt); + +This is the main Linenoise call: it shows the user a prompt with line editing +and history capabilities. The prompt you specify is used as a prompt, that is, +it will be printed to the left of the cursor. The library returns a buffer +with the line composed by the user, or NULL on end of file or when there +is an out of memory condition. + +When a tty is detected (the user is actually typing into a terminal session) +the maximum editable line length is `LINENOISE_MAX_LINE`. When instead the +standard input is not a tty, which happens every time you redirect a file +to a program, or use it in an Unix pipeline, there are no limits to the +length of the line that can be returned. + +The returned line should be freed with the `free()` standard system call. +However sometimes it could happen that your program uses a different dynamic +allocation library, so you may also used `linenoiseFree` to make sure the +line is freed with the same allocator it was created. + +The canonical loop used by a program using Linenoise will be something like +this: + + while((line = linenoise("hello> ")) != NULL) { + printf("You wrote: %s\n", line); + linenoiseFree(line); /* Or just free(line) if you use libc malloc. */ + } + +## Single line VS multi line editing + +By default, Linenoise uses single line editing, that is, a single row on the +screen will be used, and as the user types more, the text will scroll towards +left to make room. This works if your program is one where the user is +unlikely to write a lot of text, otherwise multi line editing, where multiple +screens rows are used, can be a lot more comfortable. + +In order to enable multi line editing use the following API call: + + linenoiseSetMultiLine(1); + +You can disable it using `0` as argument. + +## History + +Linenoise supporst history, so that the user does not have to retype +again and again the same things, but can use the down and up arrows in order +to search and re-edit already inserted lines of text. + +The followings are the history API calls: + + int linenoiseHistoryAdd(const char *line); + int linenoiseHistorySetMaxLen(int len); + int linenoiseHistorySave(const char *filename); + int linenoiseHistoryLoad(const char *filename); + +Use `linenoiseHistoryAdd` every time you want to add a new element +to the top of the history (it will be the first the user will see when +using the up arrow). + +Note that for history to work, you have to set a length for the history +(which is zero by default, so history will be disabled if you don't set +a proper one). This is accomplished using the `linenoiseHistorySetMaxLen` +function. + +Linenoise has direct support for persisting the history into an history +file. The functions `linenoiseHistorySave` and `linenoiseHistoryLoad` do +just that. Both functions return -1 on error and 0 on success. + +## Mask mode + +Sometimes it is useful to allow the user to type passwords or other +secrets that should not be displayed. For such situations linenoise supports +a "mask mode" that will just replace the characters the user is typing +with `*` characters, like in the following example: + + $ ./linenoise_example + hello> get mykey + echo: 'get mykey' + hello> /mask + hello> ********* + +You can enable and disable mask mode using the following two functions: + + void linenoiseMaskModeEnable(void); + void linenoiseMaskModeDisable(void); + +## Completion + +Linenoise supports completion, which is the ability to complete the user +input when she or he presses the `` key. + +In order to use completion, you need to register a completion callback, which +is called every time the user presses ``. Your callback will return a +list of items that are completions for the current string. + +The following is an example of registering a completion callback: + + linenoiseSetCompletionCallback(completion); + +The completion must be a function returning `void` and getting as input +a `const char` pointer, which is the line the user has typed so far, and +a `linenoiseCompletions` object pointer, which is used as argument of +`linenoiseAddCompletion` in order to add completions inside the callback. +An example will make it more clear: + + void completion(const char *buf, linenoiseCompletions *lc) { + if (buf[0] == 'h') { + linenoiseAddCompletion(lc,"hello"); + linenoiseAddCompletion(lc,"hello there"); + } + } + +Basically in your completion callback, you inspect the input, and return +a list of items that are good completions by using `linenoiseAddCompletion`. + +If you want to test the completion feature, compile the example program +with `make`, run it, type `h` and press ``. + +## Hints + +Linenoise has a feature called *hints* which is very useful when you +use Linenoise in order to implement a REPL (Read Eval Print Loop) for +a program that accepts commands and arguments, but may also be useful in +other conditions. + +The feature shows, on the right of the cursor, as the user types, hints that +may be useful. The hints can be displayed using a different color compared +to the color the user is typing, and can also be bold. + +For example as the user starts to type `"git remote add"`, with hints it's +possible to show on the right of the prompt a string ` `. + +The feature works similarly to the history feature, using a callback. +To register the callback we use: + + linenoiseSetHintsCallback(hints); + +The callback itself is implemented like this: + + char *hints(const char *buf, int *color, int *bold) { + if (!strcasecmp(buf,"git remote add")) { + *color = 35; + *bold = 0; + return " "; + } + return NULL; + } + +The callback function returns the string that should be displayed or NULL +if no hint is available for the text the user currently typed. The returned +string will be trimmed as needed depending on the number of columns available +on the screen. + +It is possible to return a string allocated in dynamic way, by also registering +a function to deallocate the hint string once used: + + void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); + +The free hint callback will just receive the pointer and free the string +as needed (depending on how the hits callback allocated it). + +As you can see in the example above, a `color` (in xterm color terminal codes) +can be provided together with a `bold` attribute. If no color is set, the +current terminal foreground color is used. If no bold attribute is set, +non-bold text is printed. + +Color codes are: + + red = 31 + green = 32 + yellow = 33 + blue = 34 + magenta = 35 + cyan = 36 + white = 37; + +## Screen handling + +Sometimes you may want to clear the screen as a result of something the +user typed. You can do this by calling the following function: + + void linenoiseClearScreen(void); + +## Asyncrhronous API + +Sometimes you want to read from the keyboard but also from sockets or other +external events, and at the same time there could be input to display to the +user *while* the user is typing something. Let's call this the "IRC problem", +since if you want to write an IRC client with linenoise, without using +some fully featured libcurses approach, you will surely end having such an +issue. + +Fortunately now a multiplexing friendly API exists, and it is just what the +blocking calls internally use. To start, we need to initialize a linenoise +context like this: + + struct linenoiseState ls; + char buf[1024]; + linenoiseEditStart(&ls,-1,-1,buf,sizeof(buf),"some prompt> "); + +The two -1 and -1 arguments are the stdin/out descriptors. If they are +set to -1, linenoise will just use the default stdin/out file descriptors. +Now as soon as we have data from stdin (and we know it via select(2) or +some other way), we can ask linenoise to read the next character with: + + linenoiseEditFeed(&ls); + +The function returns a `char` pointer: if the user didn't yet press enter +to provide a line to the program, it will return `linenoiseEditMore`, that +means we need to call `linenoiseEditFeed()` again when more data is +available. If the function returns non NULL, then this is a heap allocated +data (to be freed with `linenoiseFree()`) representing the user input. +When the function returns NULL, than the user pressed CTRL-C or CTRL-D +with an empty line, to quit the program, or there was some I/O error. + +After each line is received (or if you want to quit the program, and exit raw mode), the following function needs to be called: + + linenoiseEditStop(&ls); + +To start reading the next line, a new linenoiseEditStart() must +be called, in order to reset the state, and so forth, so a typical event +handler called when the standard input is readable, will work similarly +to the example below: + +``` c +void stdinHasSomeData(void) { + char *line = linenoiseEditFeed(&LineNoiseState); + if (line == linenoiseEditMore) return; + linenoiseEditStop(&LineNoiseState); + if (line == NULL) exit(0); + + printf("line: %s\n", line); + linenoiseFree(line); + linenoiseEditStart(&LineNoiseState,-1,-1,LineNoiseBuffer,sizeof(LineNoiseBuffer),"serial> "); +} +``` + +Now that we have a way to avoid blocking in the user input, we can use +two calls to hide/show the edited line, so that it is possible to also +show some input that we received (from socekts, bluetooth, whatever) on +screen: + + linenoiseHide(&ls); + printf("some data...\n"); + linenoiseShow(&ls); + +To the API calls, the linenoise example C file implements a multiplexing +example using select(2) and the asynchronous API: + +```c + struct linenoiseState ls; + char buf[1024]; + linenoiseEditStart(&ls,-1,-1,buf,sizeof(buf),"hello> "); + + while(1) { + // Select(2) setup code removed... + retval = select(ls.ifd+1, &readfds, NULL, NULL, &tv); + if (retval == -1) { + perror("select()"); + exit(1); + } else if (retval) { + line = linenoiseEditFeed(&ls); + /* A NULL return means: line editing is continuing. + * Otherwise the user hit enter or stopped editing + * (CTRL+C/D). */ + if (line != linenoiseEditMore) break; + } else { + // Timeout occurred + static int counter = 0; + linenoiseHide(&ls); + printf("Async output %d.\n", counter++); + linenoiseShow(&ls); + } + } + linenoiseEditStop(&ls); + if (line == NULL) exit(0); /* Ctrl+D/C. */ +``` + +You can test the example by running the example program with the `--async` option. + +## Running the tests + +To run the test suite: + + make test + +The tests will display a virtual terminal showing linenoise output in real-time, making it easy to see what's being tested and debug any failures. + +### What the tests cover + +The test suite verifies: + +* Basic typing and cursor movement (left, right, home, end) +* Backspace and delete operations +* UTF-8 multi-byte characters (accented letters, CJK) +* Emoji and grapheme clusters (skin tones, ZWJ sequences like flags) +* Horizontal scrolling for long lines +* Multiline mode editing and navigation +* History navigation in multiline mode +* Word and line deletion (Ctrl-W, Ctrl-U) + +### How the test harness works + +The test program (`linenoise-test.c`) implements a VT100 terminal emulator that captures and verifies linenoise output: + +1. **Fork and pipes**: The test harness forks `linenoise-example`, connecting to it via pipes. The child process sees `LINENOISE_ASSUME_TTY=1` to enable terminal mode despite not having a real TTY. +2. **VT100 emulator**: A minimal VT100 emulator parses escape sequences (cursor movement, screen clearing, etc.) and maintains a virtual screen buffer. Each cell stores a complete UTF-8 grapheme cluster and its display width. +3. **Visual rendering**: After each operation, the virtual screen is rendered to your real terminal with a border, so you can watch the test execute and see exactly what linenoise is displaying. +4. **Assertions**: Tests verify screen contents and cursor position against expected values. + +This approach tests linenoise as users actually experience it, catching rendering bugs that unit tests would miss. + +## Related projects + +* [Linenoise NG](https://github.com/arangodb/linenoise-ng) is a fork of Linenoise that aims to add more advanced features like Windows support and other features. Uses C++ instead of C as development language. +* [Linenoise-swift](https://github.com/andybest/linenoise-swift) is a reimplementation of Linenoise written in Swift. diff --git a/src/linenoise/example.c b/src/linenoise/example.c new file mode 100644 index 0000000..3a7f8b3 --- /dev/null +++ b/src/linenoise/example.c @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include "linenoise.h" + +void completion(const char *buf, linenoiseCompletions *lc) { + if (buf[0] == 'h') { + linenoiseAddCompletion(lc,"hello"); + linenoiseAddCompletion(lc,"hello there"); + } +} + +char *hints(const char *buf, int *color, int *bold) { + if (!strcasecmp(buf,"hello")) { + *color = 35; + *bold = 0; + return " World"; + } + return NULL; +} + +int main(int argc, char **argv) { + char *line; + char *prgname = argv[0]; + int async = 0; + + /* Parse options, with --multiline we enable multi line editing. */ + while(argc > 1) { + argc--; + argv++; + if (!strcmp(*argv,"--multiline")) { + linenoiseSetMultiLine(1); + printf("Multi-line mode enabled.\n"); + } else if (!strcmp(*argv,"--keycodes")) { + linenoisePrintKeyCodes(); + exit(0); + } else if (!strcmp(*argv,"--async")) { + async = 1; + } else { + fprintf(stderr, "Usage: %s [--multiline] [--keycodes] [--async]\n", prgname); + exit(1); + } + } + + /* Set the completion callback. This will be called every time the + * user uses the key. */ + linenoiseSetCompletionCallback(completion); + linenoiseSetHintsCallback(hints); + + /* Load history from file. The history file is just a plain text file + * where entries are separated by newlines. */ + linenoiseHistoryLoad("history.txt"); /* Load the history at startup */ + + /* Now this is the main loop of the typical linenoise-based application. + * The call to linenoise() will block as long as the user types something + * and presses enter. + * + * The typed string is returned as a malloc() allocated string by + * linenoise, so the user needs to free() it. */ + + while(1) { + if (!async) { + line = linenoise("hello> "); + if (line == NULL) break; + } else { + /* Asynchronous mode using the multiplexing API: wait for + * data on stdin, and simulate async data coming from some source + * using the select(2) timeout. */ + struct linenoiseState ls; + char buf[1024]; + linenoiseEditStart(&ls,-1,-1,buf,sizeof(buf),"hello> "); + while(1) { + fd_set readfds; + struct timeval tv; + int retval; + + FD_ZERO(&readfds); + FD_SET(ls.ifd, &readfds); + tv.tv_sec = 1; // 1 sec timeout + tv.tv_usec = 0; + + retval = select(ls.ifd+1, &readfds, NULL, NULL, &tv); + if (retval == -1) { + perror("select()"); + exit(1); + } else if (retval) { + line = linenoiseEditFeed(&ls); + /* A NULL return means: line editing is continuing. + * Otherwise the user hit enter or stopped editing + * (CTRL+C/D). */ + if (line != linenoiseEditMore) break; + } else { + // Timeout occurred + static int counter = 0; + linenoiseHide(&ls); + printf("Async output %d.\n", counter++); + linenoiseShow(&ls); + } + } + linenoiseEditStop(&ls); + if (line == NULL) exit(0); /* Ctrl+D/C. */ + } + + /* Do something with the string. */ + if (line[0] != '\0' && line[0] != '/') { + printf("echo: '%s'\n", line); + linenoiseHistoryAdd(line); /* Add to the history. */ + linenoiseHistorySave("history.txt"); /* Save the history on disk. */ + } else if (!strncmp(line,"/historylen",11)) { + /* The "/historylen" command will change the history len. */ + int len = atoi(line+11); + linenoiseHistorySetMaxLen(len); + } else if (!strncmp(line, "/mask", 5)) { + linenoiseMaskModeEnable(); + } else if (!strncmp(line, "/unmask", 7)) { + linenoiseMaskModeDisable(); + } else if (line[0] == '/') { + printf("Unreconized command: %s\n", line); + } + free(line); + } + return 0; +} diff --git a/src/linenoise/linenoise-test.c b/src/linenoise/linenoise-test.c new file mode 100644 index 0000000..bd53212 --- /dev/null +++ b/src/linenoise/linenoise-test.c @@ -0,0 +1,1297 @@ +/* linenoise-test.c -- Test framework for linenoise with VT100 emulator. + * + * This file implements: + * 1. A minimal VT100 terminal emulator that parses escape sequences + * 2. A test harness that runs linenoise via pipes + * 3. Visual rendering so the user can watch tests run + * 4. Test functions and assertions + * + * The emulator maintains a logical screen buffer and also renders to the + * real terminal, allowing visual verification if tests fail. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ========================= VT100 Emulator ========================= */ + +#define EMU_ROWS 15 +#define EMU_COLS 60 + +/* Each screen cell stores a complete grapheme cluster and its display width. + * Wide characters (emoji, CJK) have width=2 and occupy two cells: the main + * cell holds the character, the next cell has width=0 (continuation). + * Complex emoji (ZWJ sequences) can be up to ~30 bytes. */ +typedef struct { + char ch[32]; /* UTF-8 bytes for grapheme cluster + null terminator */ + int len; /* Current length of content in ch[] */ + int width; /* Display width: 0=continuation, 1=normal, 2=wide char */ +} emu_cell_t; + +static emu_cell_t emu_screen[EMU_ROWS][EMU_COLS]; +static int emu_cursor_row = 0; +static int emu_cursor_col = 0; +static int emu_rows = EMU_ROWS; +static int emu_cols = EMU_COLS; +static int emu_after_zwj = 0; /* Track if last char was ZWJ for grapheme clusters */ + +/* UTF-8 accumulator for multi-byte sequences. */ +static char utf8_buf[5]; +static int utf8_len = 0; +static int utf8_expected = 0; + +/* Parser state for escape sequences. */ +enum { + STATE_NORMAL, + STATE_ESC, /* Saw ESC */ + STATE_CSI /* Saw ESC [ */ +}; + +static int parser_state = STATE_NORMAL; +static char csi_buf[32]; +static int csi_len = 0; + +/* Determine expected UTF-8 byte length from first byte. */ +static int utf8_byte_len(unsigned char c) { + if ((c & 0x80) == 0) return 1; + if ((c & 0xE0) == 0xC0) return 2; + if ((c & 0xF0) == 0xE0) return 3; + if ((c & 0xF8) == 0xF0) return 4; + return 1; +} + +/* Decode UTF-8 bytes into a codepoint. */ +static uint32_t utf8_decode(const char *s, int len) { + unsigned char c = s[0]; + uint32_t cp; + if (len == 1) { + cp = c; + } else if (len == 2) { + cp = (c & 0x1F) << 6; + cp |= (s[1] & 0x3F); + } else if (len == 3) { + cp = (c & 0x0F) << 12; + cp |= (s[1] & 0x3F) << 6; + cp |= (s[2] & 0x3F); + } else if (len == 4) { + cp = (c & 0x07) << 18; + cp |= (s[1] & 0x3F) << 12; + cp |= (s[2] & 0x3F) << 6; + cp |= (s[3] & 0x3F); + } else { + cp = c; + } + return cp; +} + +/* Determine display width of a codepoint. Returns 0, 1 or 2. */ +static int codepoint_width(uint32_t cp) { + /* Zero-width characters. */ + if (cp == 0) return 0; + if (cp >= 0x0300 && cp <= 0x036F) return 0; /* Combining diacriticals */ + if (cp >= 0x1AB0 && cp <= 0x1AFF) return 0; /* Combining diacriticals ext */ + if (cp >= 0x1DC0 && cp <= 0x1DFF) return 0; /* Combining diacriticals sup */ + if (cp >= 0x20D0 && cp <= 0x20FF) return 0; /* Combining for symbols */ + if (cp >= 0xFE20 && cp <= 0xFE2F) return 0; /* Combining half marks */ + + /* Grapheme-extending characters: zero width. */ + if (cp == 0xFE0E || cp == 0xFE0F) return 0; /* Variation selectors */ + if (cp >= 0x1F3FB && cp <= 0x1F3FF) return 0; /* Skin tone modifiers */ + if (cp == 0x200D) return 0; /* Zero Width Joiner */ + + /* Wide characters: CJK, Emoji, etc. */ + if (cp >= 0x1100 && cp <= 0x115F) return 2; /* Hangul Jamo */ + if (cp >= 0x231A && cp <= 0x231B) return 2; /* Watch, Hourglass */ + if (cp >= 0x23E9 && cp <= 0x23F3) return 2; /* Various symbols */ + if (cp >= 0x23F8 && cp <= 0x23FA) return 2; /* Various symbols */ + if (cp >= 0x25AA && cp <= 0x25AB) return 2; /* Small squares */ + if (cp >= 0x25B6 && cp <= 0x25C0) return 2; /* Play/reverse buttons */ + if (cp >= 0x25FB && cp <= 0x25FE) return 2; /* Squares */ + if (cp >= 0x2600 && cp <= 0x26FF) return 2; /* Misc symbols */ + if (cp >= 0x2700 && cp <= 0x27BF) return 2; /* Dingbats */ + if (cp >= 0x2934 && cp <= 0x2935) return 2; /* Arrows */ + if (cp >= 0x2B05 && cp <= 0x2B07) return 2; /* Arrows */ + if (cp >= 0x2B1B && cp <= 0x2B1C) return 2; /* Squares */ + if (cp == 0x2B50 || cp == 0x2B55) return 2; /* Star, circle */ + if (cp >= 0x2E80 && cp <= 0x9FFF) return 2; /* CJK */ + if (cp >= 0xAC00 && cp <= 0xD7AF) return 2; /* Hangul Syllables */ + if (cp >= 0xF900 && cp <= 0xFAFF) return 2; /* CJK Compatibility */ + if (cp >= 0xFE10 && cp <= 0xFE1F) return 2; /* Vertical forms */ + if (cp >= 0xFE30 && cp <= 0xFE6F) return 2; /* CJK Compatibility Forms */ + if (cp >= 0xFF00 && cp <= 0xFF60) return 2; /* Fullwidth forms */ + if (cp >= 0xFFE0 && cp <= 0xFFE6) return 2; /* Fullwidth symbols */ + if (cp >= 0x1F1E6 && cp <= 0x1F1FF) return 2; /* Regional indicators */ + if (cp >= 0x1F300 && cp <= 0x1F9FF) return 2; /* Emoji symbols */ + if (cp >= 0x1FA00 && cp <= 0x1FAFF) return 2; /* Emoji extended */ + if (cp >= 0x20000 && cp <= 0x2FFFF) return 2; /* CJK Extension B+ */ + if (cp >= 0x30000 && cp <= 0x3FFFF) return 2; /* CJK Extension G+ */ + + return 1; +} + +/* Set a cell to a space (empty). */ +static void emu_clear_cell(int row, int col) { + emu_screen[row][col].ch[0] = ' '; + emu_screen[row][col].ch[1] = '\0'; + emu_screen[row][col].len = 1; + emu_screen[row][col].width = 1; +} + +/* Initialize the emulator. */ +static void emu_init(int rows, int cols) { + emu_rows = rows < EMU_ROWS ? rows : EMU_ROWS; + emu_cols = cols < EMU_COLS ? cols : EMU_COLS; + emu_cursor_row = 0; + emu_cursor_col = 0; + emu_after_zwj = 0; + parser_state = STATE_NORMAL; + csi_len = 0; + utf8_len = 0; + utf8_expected = 0; + for (int r = 0; r < emu_rows; r++) { + for (int c = 0; c < emu_cols; c++) { + emu_clear_cell(r, c); + } + } +} + +/* Clear from cursor to end of line. */ +static void emu_clear_to_eol(void) { + for (int c = emu_cursor_col; c < emu_cols; c++) { + emu_clear_cell(emu_cursor_row, c); + } +} + +/* Clear entire screen. */ +static void emu_clear_screen(void) { + for (int r = 0; r < emu_rows; r++) { + for (int c = 0; c < emu_cols; c++) { + emu_clear_cell(r, c); + } + } + emu_cursor_row = 0; + emu_cursor_col = 0; +} + +/* Parse CSI parameters (e.g., "5" from ESC[5C). */ +static int csi_get_param(int def) { + if (csi_len == 0) return def; + csi_buf[csi_len] = '\0'; + int val = atoi(csi_buf); + return val > 0 ? val : def; +} + +/* Handle a complete CSI sequence. */ +static void emu_handle_csi(char cmd) { + int n = csi_get_param(1); + + switch (cmd) { + case 'A': /* Cursor Up */ + emu_cursor_row -= n; + if (emu_cursor_row < 0) emu_cursor_row = 0; + break; + case 'B': /* Cursor Down */ + emu_cursor_row += n; + if (emu_cursor_row >= emu_rows) emu_cursor_row = emu_rows - 1; + break; + case 'C': /* Cursor Forward */ + emu_cursor_col += n; + if (emu_cursor_col >= emu_cols) emu_cursor_col = emu_cols - 1; + break; + case 'D': /* Cursor Backward */ + emu_cursor_col -= n; + if (emu_cursor_col < 0) emu_cursor_col = 0; + break; + case 'H': /* Cursor Home (or position if params given) */ + emu_cursor_row = 0; + emu_cursor_col = 0; + break; + case 'J': /* Erase Display */ + if (n == 2) emu_clear_screen(); + break; + case 'K': /* Erase Line */ + if (n == 0 || csi_len == 0) emu_clear_to_eol(); + break; + case 'm': /* SGR (colors/attributes) - ignore */ + break; + default: + /* Unknown CSI sequence, ignore */ + break; + } +} + +/* Find the previous non-continuation cell (for appending extending chars). */ +static int emu_find_prev_cell(int row, int col) { + /* Move back to find the cell that owns this position. */ + while (col > 0) { + col--; + if (emu_screen[row][col].width != 0) { + return col; + } + } + return -1; /* No previous cell found. */ +} + +/* Check if codepoint is Zero Width Joiner. */ +static int emu_is_zwj(uint32_t cp) { + return cp == 0x200D; +} + +/* Place a complete character at the current cursor position. */ +static void emu_put_char(const char *ch, int chlen) { + uint32_t cp = utf8_decode(ch, chlen); + int width = codepoint_width(cp); + + /* If we're after a ZWJ, append this char to the previous cell + * regardless of its width (it's being joined). */ + if (emu_after_zwj) { + emu_after_zwj = 0; + int prev_col = emu_find_prev_cell(emu_cursor_row, emu_cursor_col); + if (prev_col >= 0) { + emu_cell_t *cell = &emu_screen[emu_cursor_row][prev_col]; + if (cell->len + chlen < (int)sizeof(cell->ch) - 1) { + memcpy(cell->ch + cell->len, ch, chlen); + cell->len += chlen; + cell->ch[cell->len] = '\0'; + } + } + /* Check if this char is also a ZWJ (unlikely but possible). */ + if (emu_is_zwj(cp)) { + emu_after_zwj = 1; + } + return; + } + + if (width == 0) { + /* Zero-width character - append to previous cell if possible. + * This handles variation selectors, skin tones, ZWJ sequences. */ + int prev_col = emu_find_prev_cell(emu_cursor_row, emu_cursor_col); + if (prev_col >= 0) { + emu_cell_t *cell = &emu_screen[emu_cursor_row][prev_col]; + /* Append if there's room in the buffer. */ + if (cell->len + chlen < (int)sizeof(cell->ch) - 1) { + memcpy(cell->ch + cell->len, ch, chlen); + cell->len += chlen; + cell->ch[cell->len] = '\0'; + } + } + /* If this was a ZWJ, next char should also be appended. */ + if (emu_is_zwj(cp)) { + emu_after_zwj = 1; + } + return; + } + + /* Check if there's room for this character. */ + if (emu_cursor_col + width > emu_cols) { + /* No room, don't display (clip at edge). */ + return; + } + + /* Before overwriting, handle orphaned continuation cells: + * 1. If current cell is a continuation (width=0), clear it first + * 2. If current cell was a wide char (width=2), clear its continuation */ + emu_cell_t *cur = &emu_screen[emu_cursor_row][emu_cursor_col]; + if (cur->width == 0) { + /* This was a continuation cell - convert to space. */ + emu_clear_cell(emu_cursor_row, emu_cursor_col); + } else if (cur->width == 2 && emu_cursor_col + 1 < emu_cols) { + /* This was a wide char - clear its orphaned continuation. */ + emu_clear_cell(emu_cursor_row, emu_cursor_col + 1); + } + + /* Store the character in the current cell. */ + memcpy(emu_screen[emu_cursor_row][emu_cursor_col].ch, ch, chlen); + emu_screen[emu_cursor_row][emu_cursor_col].ch[chlen] = '\0'; + emu_screen[emu_cursor_row][emu_cursor_col].len = chlen; + emu_screen[emu_cursor_row][emu_cursor_col].width = width; + emu_cursor_col++; + + /* For wide characters, mark the next cell as continuation. */ + if (width == 2 && emu_cursor_col < emu_cols) { + emu_screen[emu_cursor_row][emu_cursor_col].ch[0] = '\0'; + emu_screen[emu_cursor_row][emu_cursor_col].len = 0; + emu_screen[emu_cursor_row][emu_cursor_col].width = 0; + emu_cursor_col++; + } +} + +/* Feed a single byte to the emulator. */ +static void emu_feed_byte(unsigned char c) { + switch (parser_state) { + case STATE_NORMAL: + if (c == 0x1b) { + parser_state = STATE_ESC; + utf8_len = 0; /* Cancel any pending UTF-8 sequence. */ + } else if (c == '\r') { + emu_cursor_col = 0; + utf8_len = 0; + } else if (c == '\n') { + emu_cursor_row++; + if (emu_cursor_row >= emu_rows) { + /* Scroll up: move all rows up, clear bottom row. */ + for (int r = 0; r < emu_rows - 1; r++) { + memcpy(emu_screen[r], emu_screen[r + 1], + sizeof(emu_cell_t) * emu_cols); + } + for (int c2 = 0; c2 < emu_cols; c2++) { + emu_clear_cell(emu_rows - 1, c2); + } + emu_cursor_row = emu_rows - 1; + } + utf8_len = 0; + } else if (c == '\b') { + if (emu_cursor_col > 0) { + emu_cursor_col--; + /* If we're on a continuation cell, back up one more. */ + if (emu_screen[emu_cursor_row][emu_cursor_col].width == 0 && + emu_cursor_col > 0) { + emu_cursor_col--; + } + } + utf8_len = 0; + } else if (c >= 32 || (c & 0x80)) { + /* Printable character or UTF-8 byte. */ + if ((c & 0x80) == 0) { + /* ASCII character - display immediately. */ + char ch[2] = {c, '\0'}; + emu_put_char(ch, 1); + utf8_len = 0; + } else if ((c & 0xC0) == 0xC0) { + /* Start of UTF-8 multi-byte sequence. */ + utf8_buf[0] = c; + utf8_len = 1; + utf8_expected = utf8_byte_len(c); + } else if ((c & 0xC0) == 0x80 && utf8_len > 0) { + /* Continuation byte. */ + utf8_buf[utf8_len++] = c; + if (utf8_len >= utf8_expected) { + /* Complete UTF-8 character. */ + utf8_buf[utf8_len] = '\0'; + emu_put_char(utf8_buf, utf8_len); + utf8_len = 0; + } + } else { + /* Invalid UTF-8 - reset. */ + utf8_len = 0; + } + } + break; + + case STATE_ESC: + if (c == '[') { + parser_state = STATE_CSI; + csi_len = 0; + } else { + /* Unknown escape, back to normal. */ + parser_state = STATE_NORMAL; + } + break; + + case STATE_CSI: + if (c >= '0' && c <= '9') { + if (csi_len < (int)sizeof(csi_buf) - 1) { + csi_buf[csi_len++] = c; + } + } else if (c == ';') { + /* Multiple params - for simplicity, just reset. */ + csi_len = 0; + } else { + /* End of CSI sequence. */ + emu_handle_csi(c); + parser_state = STATE_NORMAL; + } + break; + } +} + +/* Debug flag for verbose output. */ +static int emu_debug = 0; + +/* Feed a buffer to the emulator. */ +static void emu_feed(const char *buf, int len) { + if (emu_debug) { + printf("EMU_FEED (%d bytes): ", len); + for (int i = 0; i < len && i < 200; i++) { + unsigned char c = buf[i]; + if (c >= 32 && c < 127) printf("%c", c); + else printf("<%02X>", c); + } + if (len > 200) printf("..."); + printf("\n"); + } + for (int i = 0; i < len; i++) { + emu_feed_byte((unsigned char)buf[i]); + } +} + +/* Get a row from the screen as a UTF-8 string (trimmed of trailing spaces). */ +static const char *emu_get_row(int row) { + static char buf[EMU_COLS * 4 + 1]; /* Each cell can be up to 4 UTF-8 bytes. */ + if (row < 0 || row >= emu_rows) { + buf[0] = '\0'; + return buf; + } + /* Build the row string, skipping continuation cells. */ + int pos = 0; + int last_non_space = -1; + for (int c = 0; c < emu_cols; c++) { + emu_cell_t *cell = &emu_screen[row][c]; + if (cell->width == 0) continue; /* Skip continuation cells. */ + + int chlen = strlen(cell->ch); + if (pos + chlen < (int)sizeof(buf) - 1) { + memcpy(buf + pos, cell->ch, chlen); + if (!(chlen == 1 && cell->ch[0] == ' ')) { + last_non_space = pos + chlen; + } + pos += chlen; + } + } + /* Trim trailing spaces. */ + if (last_non_space >= 0) { + buf[last_non_space] = '\0'; + } else { + buf[0] = '\0'; + } + return buf; +} + +/* ========================= Visual Rendering ========================= */ + +/* Render the emulator state to the real terminal for visual inspection. + * This shows the screen contents and cursor position. */ +static void render_to_terminal(const char *test_name) { + /* Clear real screen and move home. */ + printf("\x1b[2J\x1b[H"); + + /* Header. */ + printf("\x1b[1;36m=== LINENOISE TEST: %s ===\x1b[0m\n\n", test_name); + + /* Draw screen with border. */ + printf("\x1b[33m+"); + for (int c = 0; c < emu_cols; c++) printf("-"); + printf("+\x1b[0m\n"); + + for (int r = 0; r < emu_rows; r++) { + printf("\x1b[33m|\x1b[0m"); + for (int c = 0; c < emu_cols; c++) { + emu_cell_t *cell = &emu_screen[r][c]; + + if (cell->width == 0) { + /* Continuation cell - skip (already printed with wide char). */ + continue; + } + + if (r == emu_cursor_row && c == emu_cursor_col) { + /* Highlight cursor position. */ + printf("\x1b[7m%s\x1b[0m", cell->ch); + } else { + printf("%s", cell->ch); + } + } + printf("\x1b[33m|\x1b[0m\n"); + } + + printf("\x1b[33m+"); + for (int c = 0; c < emu_cols; c++) printf("-"); + printf("+\x1b[0m\n"); + + /* Cursor info. */ + printf("\nCursor: row=%d, col=%d\n", emu_cursor_row, emu_cursor_col); + fflush(stdout); +} + +/* ========================= Test Harness ========================= */ + +static int child_pid = -1; +static int pipe_to_child[2]; /* We write, child reads (child's stdin) */ +static int pipe_from_child[2]; /* Child writes, we read (child's stdout) */ +static const char *current_test = "unknown"; + +/* Start the linenoise example program. */ +static int test_start(const char *test_name, const char *program) { + current_test = test_name; + emu_init(EMU_ROWS, EMU_COLS); + + if (pipe(pipe_to_child) == -1) { + perror("pipe"); + return -1; + } + if (pipe(pipe_from_child) == -1) { + perror("pipe"); + return -1; + } + + child_pid = fork(); + if (child_pid == -1) { + perror("fork"); + return -1; + } + + if (child_pid == 0) { + /* Child process. */ + close(pipe_to_child[1]); /* Close write end. */ + close(pipe_from_child[0]); /* Close read end. */ + + dup2(pipe_to_child[0], STDIN_FILENO); + dup2(pipe_from_child[1], STDOUT_FILENO); + dup2(pipe_from_child[1], STDERR_FILENO); + + close(pipe_to_child[0]); + close(pipe_from_child[1]); + + /* Set test environment variables. */ + setenv("LINENOISE_ASSUME_TTY", "1", 1); + setenv("LINENOISE_COLS", "60", 1); + + /* Use shell to parse the command line arguments. */ + execl("/bin/sh", "sh", "-c", program, NULL); + perror("exec"); + exit(1); + } + + /* Parent process. */ + close(pipe_to_child[0]); /* Close read end. */ + close(pipe_from_child[1]); /* Close write end. */ + + /* Give child time to start and print prompt. */ + usleep(50000); /* 50ms */ + + /* Read initial output (prompt) with timeout. */ + char buf[4096]; + fd_set fds; + struct timeval tv = {1, 0}; /* 1 second timeout */ + + FD_ZERO(&fds); + FD_SET(pipe_from_child[0], &fds); + + if (select(pipe_from_child[0] + 1, &fds, NULL, NULL, &tv) > 0) { + int n = read(pipe_from_child[0], buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + emu_feed(buf, n); + } + } + + render_to_terminal(test_name); + return 0; +} + +/* End the test, clean up. */ +static void test_end(void) { + if (child_pid > 0) { + /* Send Ctrl-D (EOF) to terminate cleanly. */ + write(pipe_to_child[1], "\x04", 1); + usleep(50000); + + /* Close our end of the pipe to signal EOF. */ + close(pipe_to_child[1]); + + /* Wait briefly for child to exit. */ + int status; + int wait_result = waitpid(child_pid, &status, WNOHANG); + if (wait_result == 0) { + /* Child didn't exit, send SIGTERM. */ + kill(child_pid, SIGTERM); + usleep(10000); + waitpid(child_pid, &status, WNOHANG); + } + child_pid = -1; + } else { + close(pipe_to_child[1]); + } + close(pipe_from_child[0]); +} + +/* Send keys to linenoise and read response. */ +static void send_keys(const char *keys) { + write(pipe_to_child[1], keys, strlen(keys)); + usleep(30000); /* 30ms - give linenoise time to process. */ + + /* Read response with timeout. */ + char buf[4096]; + fd_set fds; + struct timeval tv; + int max_reads = 10; /* Prevent infinite loop. */ + + while (max_reads-- > 0) { + FD_ZERO(&fds); + FD_SET(pipe_from_child[0], &fds); + tv.tv_sec = 0; + tv.tv_usec = 50000; /* 50ms timeout */ + + if (select(pipe_from_child[0] + 1, &fds, NULL, NULL, &tv) <= 0) { + break; /* Timeout or error. */ + } + int n = read(pipe_from_child[0], buf, sizeof(buf) - 1); + if (n <= 0) break; + buf[n] = '\0'; + emu_feed(buf, n); + } + + render_to_terminal(current_test); +} + +/* Send special keys. */ +#define KEY_UP "\x1b[A" +#define KEY_DOWN "\x1b[B" +#define KEY_RIGHT "\x1b[C" +#define KEY_LEFT "\x1b[D" +#define KEY_HOME "\x1b[H" +#define KEY_END "\x1b[F" +#define KEY_DELETE "\x1b[3~" +#define KEY_BACKSPACE "\x7f" +#define KEY_ENTER "\r" +#define KEY_CTRL_A "\x01" +#define KEY_CTRL_E "\x05" +#define KEY_CTRL_U "\x15" +#define KEY_CTRL_K "\x0b" +#define KEY_CTRL_W "\x17" +#define KEY_CTRL_T "\x14" +#define KEY_CTRL_C "\x03" + +/* ========================= Test Assertions ========================= */ + +static int tests_run = 0; +static int tests_passed = 0; +static int tests_failed = 0; + +static void assert_screen_row(int row, const char *expected) { + tests_run++; + const char *actual = emu_get_row(row); + if (strcmp(actual, expected) == 0) { + tests_passed++; + printf("\x1b[32m[PASS]\x1b[0m Row %d == \"%s\"\n", row, expected); + } else { + tests_failed++; + printf("\x1b[31m[FAIL]\x1b[0m Row %d:\n", row); + printf(" Expected: \"%s\"\n", expected); + printf(" Actual: \"%s\"\n", actual); + } + fflush(stdout); +} + +static void assert_cursor(int row, int col) { + tests_run++; + if (emu_cursor_row == row && emu_cursor_col == col) { + tests_passed++; + printf("\x1b[32m[PASS]\x1b[0m Cursor at (%d, %d)\n", row, col); + } else { + tests_failed++; + printf("\x1b[31m[FAIL]\x1b[0m Cursor position:\n"); + printf(" Expected: (%d, %d)\n", row, col); + printf(" Actual: (%d, %d)\n", emu_cursor_row, emu_cursor_col); + } + fflush(stdout); +} + +static void assert_row_contains(int row, const char *substr) { + tests_run++; + const char *actual = emu_get_row(row); + if (strstr(actual, substr) != NULL) { + tests_passed++; + printf("\x1b[32m[PASS]\x1b[0m Row %d contains \"%s\"\n", row, substr); + } else { + tests_failed++; + printf("\x1b[31m[FAIL]\x1b[0m Row %d doesn't contain \"%s\"\n", row, substr); + printf(" Actual: \"%s\"\n", actual); + } + fflush(stdout); +} + +/* Assert that a cell contains specific bytes (for verifying grapheme clusters). */ +static void assert_cell_content(int row, int col, const char *expected, int expected_len) { + tests_run++; + emu_cell_t *cell = &emu_screen[row][col]; + if (cell->len == expected_len && memcmp(cell->ch, expected, expected_len) == 0) { + tests_passed++; + printf("\x1b[32m[PASS]\x1b[0m Cell (%d,%d) contains %d bytes\n", row, col, expected_len); + } else { + tests_failed++; + printf("\x1b[31m[FAIL]\x1b[0m Cell (%d,%d) content mismatch:\n", row, col); + printf(" Expected: %d bytes [", expected_len); + for (int i = 0; i < expected_len; i++) printf("%02X ", (unsigned char)expected[i]); + printf("]\n"); + printf(" Actual: %d bytes [", cell->len); + for (int i = 0; i < cell->len; i++) printf("%02X ", (unsigned char)cell->ch[i]); + printf("]\n"); + } + fflush(stdout); +} + +/* Assert that a cell has the expected display width. */ +static void assert_cell_width(int row, int col, int expected_width) { + tests_run++; + emu_cell_t *cell = &emu_screen[row][col]; + if (cell->width == expected_width) { + tests_passed++; + printf("\x1b[32m[PASS]\x1b[0m Cell (%d,%d) width == %d\n", row, col, expected_width); + } else { + tests_failed++; + printf("\x1b[31m[FAIL]\x1b[0m Cell (%d,%d) width:\n", row, col); + printf(" Expected: %d\n", expected_width); + printf(" Actual: %d\n", cell->width); + } + fflush(stdout); +} + +/* ========================= Tests ========================= */ + +static void test_simple_typing(void) { + if (test_start("Simple Typing", "./linenoise-example") == -1) return; + + send_keys("hello"); + assert_row_contains(0, "hello"); + assert_cursor(0, strlen("hello> ") + 5); + + send_keys(" world"); + assert_screen_row(0, "hello> hello world"); + + test_end(); +} + +static void test_cursor_movement(void) { + if (test_start("Cursor Movement", "./linenoise-example") == -1) return; + + send_keys("abcdef"); + int prompt_len = strlen("hello> "); + + /* Move left 3 times. */ + send_keys(KEY_LEFT KEY_LEFT KEY_LEFT); + assert_cursor(0, prompt_len + 3); /* After "abc" */ + + /* Move right 1 time. */ + send_keys(KEY_RIGHT); + assert_cursor(0, prompt_len + 4); /* After "abcd" */ + + /* Home. */ + send_keys(KEY_CTRL_A); + assert_cursor(0, prompt_len); + + /* End. */ + send_keys(KEY_CTRL_E); + assert_cursor(0, prompt_len + 6); + + test_end(); +} + +static void test_backspace_delete(void) { + if (test_start("Backspace and Delete", "./linenoise-example") == -1) return; + + send_keys("hello"); + int prompt_len = strlen("hello> "); + + /* Backspace. */ + send_keys(KEY_BACKSPACE); + assert_row_contains(0, "hell"); + assert_cursor(0, prompt_len + 4); + + /* Move left and delete forward. */ + send_keys(KEY_LEFT KEY_LEFT); + send_keys(KEY_DELETE); + assert_row_contains(0, "hel"); + + test_end(); +} + +static void test_utf8_typing(void) { + if (test_start("UTF-8 Typing", "./linenoise-example") == -1) return; + + /* Type some UTF-8 characters. */ + send_keys("caf\xc3\xa9"); /* "café" - é is 2 bytes */ + assert_row_contains(0, "café"); + + test_end(); +} + +static void test_utf8_emoji(void) { + if (test_start("UTF-8 Emoji", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); + + /* Type text with emoji (🎉 is 4 bytes, displays as 2 columns). */ + send_keys("hi \xf0\x9f\x8e\x89 there"); /* "hi 🎉 there" */ + assert_row_contains(0, "hi"); + + /* The emoji takes 2 columns, so cursor should be at: + * prompt(7) + "hi "(3) + emoji(2) + " there"(6) = 18 */ + assert_cursor(0, prompt_len + 3 + 2 + 6); + + test_end(); +} + +static void test_utf8_cursor_over_emoji(void) { + if (test_start("UTF-8 Cursor Over Emoji", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); + + /* Type: "a🎉b" */ + send_keys("a\xf0\x9f\x8e\x89" "b"); + /* Cursor after 'b': prompt + 'a'(1) + emoji(2) + 'b'(1) = prompt + 4 */ + assert_cursor(0, prompt_len + 4); + + /* Move left over 'b'. */ + send_keys(KEY_LEFT); + assert_cursor(0, prompt_len + 3); /* After emoji */ + + /* Move left over emoji (should move 2 columns in one keystroke). */ + send_keys(KEY_LEFT); + assert_cursor(0, prompt_len + 1); /* After 'a' */ + + /* Move left over 'a'. */ + send_keys(KEY_LEFT); + assert_cursor(0, prompt_len); /* At start */ + + test_end(); +} + +static void test_utf8_backspace_emoji(void) { + if (test_start("UTF-8 Backspace Emoji", "./linenoise-example") == -1) return; + + /* Type: "x🎉y" then backspace should delete 'y', then emoji, then 'x'. */ + send_keys("x\xf0\x9f\x8e\x89" "y"); + assert_row_contains(0, "x"); /* Contains at least 'x' */ + + send_keys(KEY_BACKSPACE); /* Delete 'y' */ + /* Now should be "x🎉" */ + + send_keys(KEY_BACKSPACE); /* Delete emoji (4 bytes, one backspace) */ + assert_row_contains(0, "hello> x"); + + send_keys(KEY_BACKSPACE); /* Delete 'x' */ + /* Now should be empty after prompt */ + + /* Type new text to verify buffer is truly empty (no orphaned bytes). */ + send_keys("ok"); + assert_row_contains(0, "hello> ok"); + + test_end(); +} + +static void test_utf8_backspace_4byte_only(void) { + if (test_start("UTF-8 Backspace 4-byte Only", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); + + /* Type a single 4-byte emoji (robot 🤖 = F0 9F A4 96). */ + send_keys("\xf0\x9f\xa4\x96"); + assert_cursor(0, prompt_len + 2); /* Emoji is 2 columns wide */ + + /* Backspace should delete the entire 4-byte emoji in one keystroke. */ + send_keys(KEY_BACKSPACE); + assert_cursor(0, prompt_len); /* Cursor should be at prompt end */ + + /* Type new text to verify no orphaned bytes remain in buffer. */ + send_keys("test"); + assert_row_contains(0, "hello> test"); + + /* The row should NOT contain any garbage characters. */ + /* If there were orphaned bytes, "test" would appear after them. */ + + test_end(); +} + +static void test_utf8_grapheme_clusters(void) { + if (test_start("UTF-8 Grapheme Clusters", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); + + /* Test 1: Heart with variation selector ❤️ (U+2764 + U+FE0F = 6 bytes). + * Bytes: E2 9D A4 EF B8 8F */ + send_keys("\xe2\x9d\xa4\xef\xb8\x8f"); + assert_cursor(0, prompt_len + 2); /* Emoji is 2 columns wide */ + + /* Backspace should delete the entire grapheme cluster (6 bytes). */ + send_keys(KEY_BACKSPACE); + assert_cursor(0, prompt_len); + + /* Verify buffer is clean by typing new text. */ + send_keys("a"); + assert_row_contains(0, "hello> a"); + send_keys(KEY_BACKSPACE); + + /* Test 2: Thumbs up with skin tone 👍🏻 (U+1F44D + U+1F3FB = 8 bytes). + * Bytes: F0 9F 91 8D F0 9F 8F BB */ + send_keys("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbb"); + assert_cursor(0, prompt_len + 2); /* Still 2 columns (skin tone is zero-width) */ + + /* Backspace should delete the entire grapheme cluster (8 bytes). */ + send_keys(KEY_BACKSPACE); + assert_cursor(0, prompt_len); + + /* Verify buffer is clean. */ + send_keys("b"); + assert_row_contains(0, "hello> b"); + send_keys(KEY_BACKSPACE); + + /* Test 3: Rainbow flag 🏳️‍🌈 (U+1F3F3 + U+FE0F + U+200D + U+1F308 = 14 bytes). + * Bytes: F0 9F 8F B3 EF B8 8F E2 80 8D F0 9F 8C 88 */ + send_keys("\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x8c\x88"); + /* This should render as 2 columns (single emoji). + * The ZWJ-joined rainbow should not add extra width. */ + assert_cursor(0, prompt_len + 2); + + /* Backspace should delete the entire ZWJ sequence. */ + send_keys(KEY_BACKSPACE); + assert_cursor(0, prompt_len); + + /* Verify buffer is clean. */ + send_keys("c"); + assert_row_contains(0, "hello> c"); + send_keys(KEY_BACKSPACE); + + /* Test 4: Family emoji 👨‍👩‍👧 (man + ZWJ + woman + ZWJ + girl = 18 bytes). + * Bytes: F0 9F 91 A8 E2 80 8D F0 9F 91 A9 E2 80 8D F0 9F 91 A7 + * Should render as 2 columns despite having 3 emoji joined by ZWJ. */ + send_keys("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"); + assert_cursor(0, prompt_len + 2); + + /* Backspace should delete the entire ZWJ sequence. */ + send_keys(KEY_BACKSPACE); + assert_cursor(0, prompt_len); + + /* Verify buffer is clean. */ + send_keys("ok"); + assert_row_contains(0, "hello> ok"); + + test_end(); +} + +static void test_utf8_grapheme_cursor_movement(void) { + if (test_start("UTF-8 Grapheme Cursor Movement", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); + + /* Type: a + thumbs up with skin tone + b + * 👍🏻 = F0 9F 91 8D F0 9F 8F BB (8 bytes, 2 columns) + * Layout: prompt(7) + a(1) + 👍🏻(2) + b(1) = 11 total columns */ + send_keys("a\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbb" "b"); + assert_cursor(0, prompt_len + 4); /* 7 + 1 + 2 + 1 = 11 */ + + /* Move left over 'b'. */ + send_keys(KEY_LEFT); + assert_cursor(0, prompt_len + 3); /* 7 + 1 + 2 = 10 */ + + /* Move left over thumbs up (should move 2 columns in one keystroke). */ + send_keys(KEY_LEFT); + assert_cursor(0, prompt_len + 1); /* 7 + 1 = 8 */ + + /* Move left over 'a'. */ + send_keys(KEY_LEFT); + assert_cursor(0, prompt_len); /* 7 */ + + /* Move right over 'a'. */ + send_keys(KEY_RIGHT); + assert_cursor(0, prompt_len + 1); /* 7 + 1 = 8 */ + + /* Move right over thumbs up (should move 2 columns in one keystroke). */ + send_keys(KEY_RIGHT); + assert_cursor(0, prompt_len + 3); /* 7 + 1 + 2 = 10 */ + + /* Move right over 'b'. */ + send_keys(KEY_RIGHT); + assert_cursor(0, prompt_len + 4); /* 7 + 1 + 2 + 1 = 11 */ + + test_end(); +} + +static void test_emulator_grapheme_storage(void) { + if (test_start("Emulator Grapheme Storage", "./linenoise-example") == -1) return; + emu_debug = 1; /* Enable debug output */ + + int prompt_len = strlen("hello> "); + + /* Test 1: Thumbs up with skin tone 👍🏻 should be stored as single cell. + * U+1F44D + U+1F3FB = 8 bytes. + * Bytes: F0 9F 91 8D F0 9F 8F BB */ + const char thumbs_up[] = "\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbb"; + send_keys(thumbs_up); + + /* Cell at column 7 should contain all 8 bytes. */ + assert_cell_content(0, prompt_len, thumbs_up, 8); + assert_cell_width(0, prompt_len, 2); + + /* Cell at column 8 should be continuation (width=0). */ + assert_cell_width(0, prompt_len + 1, 0); + + send_keys(KEY_BACKSPACE); + + /* Test 2: Heart with variation selector ❤️ should be stored as single cell. + * U+2764 + U+FE0F = 6 bytes. + * Bytes: E2 9D A4 EF B8 8F */ + const char heart[] = "\xe2\x9d\xa4\xef\xb8\x8f"; + send_keys(heart); + + /* Cell at column 7 should contain all 6 bytes. */ + assert_cell_content(0, prompt_len, heart, 6); + assert_cell_width(0, prompt_len, 2); + + /* Cell at column 8 should be continuation. */ + assert_cell_width(0, prompt_len + 1, 0); + + test_end(); +} + +static void test_ctrl_w_delete_word(void) { + if (test_start("Ctrl-W Delete Word", "./linenoise-example") == -1) return; + + send_keys("hello world"); + send_keys(KEY_CTRL_W); /* Delete "world" */ + assert_row_contains(0, "hello "); + + send_keys(KEY_CTRL_W); /* Delete "hello " */ + /* Should be empty now. */ + + test_end(); +} + +static void test_ctrl_u_delete_line(void) { + if (test_start("Ctrl-U Delete Line", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); + + send_keys("hello world"); + send_keys(KEY_CTRL_U); /* Delete entire line */ + assert_cursor(0, prompt_len); /* Cursor should be at start of input */ + + /* Type new text to verify buffer was cleared. */ + send_keys("new"); + assert_row_contains(0, "hello> new"); + + test_end(); +} + +static void test_horizontal_scroll(void) { + if (test_start("Horizontal Scroll", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); /* 7 chars */ + + /* Type text longer than the line (70 chars). The display should scroll + * horizontally to keep the cursor visible. + * prompt(7) + 70 = 77 > 60, causes scrolling. */ + send_keys("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee" + "ffffffffffffffffffff"); /* 70 chars: 50 + 20 */ + + /* The right side of the text should be visible (scrolled left). + * Cursor should be at the right edge. */ + assert_cursor(0, 59); /* At last column (60-col terminal) */ + assert_row_contains(0, "ffffffffffffffffffff"); /* The end should be visible */ + + /* Move cursor to beginning - text should scroll to show start. */ + send_keys(KEY_CTRL_A); + assert_cursor(0, prompt_len); /* After prompt */ + assert_row_contains(0, "hello> aaaaaaaaaa"); /* Start should now be visible */ + + /* Move cursor to end - text should scroll back. */ + send_keys(KEY_CTRL_E); + assert_cursor(0, 59); + assert_row_contains(0, "ffffffffffffffffffff"); /* End visible again */ + + /* Delete some chars from the end and verify left portion reappears. */ + for (int i = 0; i < 20; i++) send_keys(KEY_BACKSPACE); /* Delete 20 chars */ + + /* Now 50 chars remain, which fits: prompt(7) + 50 = 57 < 60 */ + assert_row_contains(0, "hello> aaaaaaaaaa"); /* Start should be visible */ + assert_row_contains(0, "eeeeeeeeee"); /* And most of the text */ + + test_end(); +} + +static void test_horizontal_scroll_utf8(void) { + if (test_start("Horizontal Scroll UTF-8", "./linenoise-example") == -1) return; + + int prompt_len = strlen("hello> "); /* 7 cols */ + + /* Type text with emojis that fills most of the line. + * Each emoji is 4 bytes but 2 columns. + * Type: "START" (5 cols) + 20 emojis (40 cols) + "END" (3 cols) = 48 cols. + * With prompt (7 cols), total = 55 cols, fits in 60-col terminal. */ + send_keys("START"); + /* Send 20 emojis in one batch. */ + send_keys("\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89" + "\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89" + "\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89" + "\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89\xf0\x9f\x8e\x89"); + send_keys("END"); + + /* Verify both START and END are visible (line fits). */ + assert_row_contains(0, "START"); + assert_row_contains(0, "END"); + + /* Move to start and verify cursor position. */ + send_keys(KEY_CTRL_A); + assert_cursor(0, prompt_len); + + /* Insert at beginning and verify. */ + send_keys("X"); + assert_row_contains(0, "hello> XSTART"); + + test_end(); +} + +/* ========================= Multi-line Mode Tests ========================= */ + +static void test_multiline_wrap(void) { + if (test_start("Multiline Wrap", "./linenoise-example --multiline") == -1) return; + + /* Type a line longer than 60 cols to force wrapping. + * Prompt is 7 chars ("hello> "), so we need 54+ chars to wrap. */ + send_keys("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee" + "ffffffffff"); /* 60 chars */ + + /* In multiline mode, full content should be displayed across rows. + * Just verify the content is there (not clipped like single-line mode). */ + assert_row_contains(0, "hello> aaaaaaaaaa"); + + test_end(); +} + +static void test_multiline_cursor_movement(void) { + if (test_start("Multiline Cursor Movement", "./linenoise-example --multiline") == -1) return; + + /* Type text that wraps (60 chars wraps on 60-col terminal). */ + send_keys("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee" + "ffffffffff"); /* 60 chars */ + + /* Move to beginning (Ctrl-A). */ + send_keys(KEY_CTRL_A); + /* Type something at the beginning to verify cursor position. */ + send_keys("X"); + assert_row_contains(0, "hello> Xaaaaaaaaaa"); /* X inserted at start */ + + /* Move to end (Ctrl-E) and type. */ + send_keys(KEY_CTRL_E); + send_keys("Z"); + /* The 'Z' should be at the end. We can't easily verify row position, + * but content should be updated. */ + + test_end(); +} + +static void test_multiline_utf8(void) { + if (test_start("Multiline UTF-8", "./linenoise-example --multiline") == -1) return; + + /* Type text with emoji. Each emoji is 4 bytes, 2 cols. */ + send_keys("Test "); + for (int i = 0; i < 10; i++) { + send_keys("\xf0\x9f\x8e\x89"); /* 🎉 - 4 bytes, 2 cols */ + } + /* 7 (prompt) + 5 ("Test ") + 20 (10 emojis * 2 cols) = 32 cols, fits on one line */ + + assert_row_contains(0, "Test"); + + /* Backspace should delete one emoji (4 bytes) at a time. */ + send_keys(KEY_BACKSPACE); + /* Now 9 emojis remain. */ + + /* Move to start and insert more. */ + send_keys(KEY_CTRL_A); + send_keys("Hi "); + assert_row_contains(0, "hello> Hi Test"); + + test_end(); +} + +static void test_multiline_history(void) { + if (test_start("Multiline History Navigation", "./linenoise-example --multiline") == -1) return; + + /* Type a long line that wraps to 2 rows. + * Prompt is 7 chars ("hello> "), so we need 54+ chars to wrap on 60-col terminal. */ + send_keys("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + /* This is 64 chars, with 7 char prompt = 71 cols, wraps to 2 rows on 60-col terminal. */ + + /* Press Enter to commit to history. */ + send_keys(KEY_ENTER); + + /* Now we have a new prompt. Type a short line. */ + send_keys("short"); + assert_row_contains(0, "hello> short"); + + /* Press Enter to commit the short line to history. */ + send_keys(KEY_ENTER); + + /* Navigate UP to get the short line from history. */ + send_keys(KEY_UP); + assert_row_contains(0, "hello> short"); + + /* Navigate UP again to get the long line. */ + send_keys(KEY_UP); + /* The long line wraps, check first row. */ + assert_row_contains(0, "hello> aaaaaa"); + + /* Navigate DOWN to go back to short line. + * This is the critical test: the long line should be fully cleared + * and only the short line should remain visible. */ + send_keys(KEY_DOWN); + assert_row_contains(0, "hello> short"); + + /* Verify row 1 is empty (no leftover from the long line). */ + assert_screen_row(1, ""); + + test_end(); +} + +/* ========================= Main ========================= */ + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + + printf("\x1b[2J\x1b[H"); /* Clear screen */ + printf("\x1b[1;35m"); + printf("╔════════════════════════════════════════╗\n"); + printf("║ LINENOISE TEST SUITE ║\n"); + printf("║ With VT100 Emulator ║\n"); + printf("╚════════════════════════════════════════╝\n"); + printf("\x1b[0m\n"); + + /* Run single-line mode tests. */ + test_simple_typing(); + test_cursor_movement(); + test_backspace_delete(); + test_utf8_typing(); + test_utf8_emoji(); + test_utf8_cursor_over_emoji(); + test_utf8_backspace_emoji(); + test_utf8_backspace_4byte_only(); + test_utf8_grapheme_clusters(); + test_utf8_grapheme_cursor_movement(); + test_emulator_grapheme_storage(); + test_ctrl_w_delete_word(); + test_ctrl_u_delete_line(); + + /* Horizontal scrolling tests (single-line mode). */ + test_horizontal_scroll(); + test_horizontal_scroll_utf8(); + + /* Run multi-line mode tests. */ + test_multiline_wrap(); + test_multiline_cursor_movement(); + test_multiline_utf8(); + test_multiline_history(); + + /* Summary. */ + printf("\n\x1b[1;35m"); + printf("╔════════════════════════════════════════╗\n"); + printf("║ TEST RESULTS ║\n"); + printf("╚════════════════════════════════════════╝\n"); + printf("\x1b[0m\n"); + + printf("Tests run: %d\n", tests_run); + printf("\x1b[32mTests passed: %d\x1b[0m\n", tests_passed); + if (tests_failed > 0) { + printf("\x1b[31mTests failed: %d\x1b[0m\n", tests_failed); + } else { + printf("Tests failed: %d\n", tests_failed); + } + + return tests_failed > 0 ? 1 : 0; +} diff --git a/src/linenoise/linenoise.c b/src/linenoise/linenoise.c new file mode 100644 index 0000000..055b34c --- /dev/null +++ b/src/linenoise/linenoise.c @@ -0,0 +1,1762 @@ +/* linenoise.c -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * You can find the latest source code at: + * + * http://github.com/antirez/linenoise + * + * Does a number of crazy assumptions that happen to be true in 99.9999% of + * the 2010 UNIX computers around. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2023, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------------------------ + * + * References: + * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html + * + * Todo list: + * - Filter bogus Ctrl+ combinations. + * - Win32 support + * + * Bloat: + * - History search like Ctrl+r in readline? + * + * List of escape sequences used by this program, we do everything just + * with three sequences. In order to be so cheap we may have some + * flickering effect with some slow terminal, but the lesser sequences + * the more compatible. + * + * EL (Erase Line) + * Sequence: ESC [ n K + * Effect: if n is 0 or missing, clear from cursor to end of line + * Effect: if n is 1, clear from beginning of line to cursor + * Effect: if n is 2, clear entire line + * + * CUF (CUrsor Forward) + * Sequence: ESC [ n C + * Effect: moves cursor forward n chars + * + * CUB (CUrsor Backward) + * Sequence: ESC [ n D + * Effect: moves cursor backward n chars + * + * The following is used to get the terminal width if getting + * the width with the TIOCGWINSZ ioctl fails + * + * DSR (Device Status Report) + * Sequence: ESC [ 6 n + * Effect: reports the current cusor position as ESC [ n ; m R + * where n is the row and m is the column + * + * When multi line mode is enabled, we also use an additional escape + * sequence. However multi line editing is disabled by default. + * + * CUU (Cursor Up) + * Sequence: ESC [ n A + * Effect: moves cursor up of n chars. + * + * CUD (Cursor Down) + * Sequence: ESC [ n B + * Effect: moves cursor down of n chars. + * + * When linenoiseClearScreen() is called, two additional escape sequences + * are used in order to clear the screen and position the cursor at home + * position. + * + * CUP (Cursor position) + * Sequence: ESC [ H + * Effect: moves the cursor to upper left corner + * + * ED (Erase display) + * Sequence: ESC [ 2 J + * Effect: clear the whole screen + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "linenoise.h" + +#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 +#define LINENOISE_MAX_LINE 4096 +static char *unsupported_term[] = {"dumb","cons25","emacs",NULL}; +static linenoiseCompletionCallback *completionCallback = NULL; +static linenoiseHintsCallback *hintsCallback = NULL; +static linenoiseFreeHintsCallback *freeHintsCallback = NULL; +static char *linenoiseNoTTY(void); +static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags); +static void refreshLineWithFlags(struct linenoiseState *l, int flags); + +static struct termios orig_termios; /* In order to restore at exit.*/ +static int maskmode = 0; /* Show "***" instead of input. For passwords. */ +static int rawmode = 0; /* For atexit() function to check if restore is needed*/ +static int mlmode = 0; /* Multi line mode. Default is single line. */ +static int atexit_registered = 0; /* Register atexit just 1 time. */ +static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; +static int history_len = 0; +static char **history = NULL; + +/* =========================== UTF-8 support ================================ */ + +/* Return the number of bytes that compose the UTF-8 character starting at + * 'c'. This function assumes a valid UTF-8 encoding and handles the four + * standard byte patterns: + * 0xxxxxxx -> 1 byte (ASCII) + * 110xxxxx -> 2 bytes + * 1110xxxx -> 3 bytes + * 11110xxx -> 4 bytes */ +static int utf8ByteLen(char c) { + unsigned char uc = (unsigned char)c; + if ((uc & 0x80) == 0) return 1; /* 0xxxxxxx: ASCII */ + if ((uc & 0xE0) == 0xC0) return 2; /* 110xxxxx: 2-byte seq */ + if ((uc & 0xF0) == 0xE0) return 3; /* 1110xxxx: 3-byte seq */ + if ((uc & 0xF8) == 0xF0) return 4; /* 11110xxx: 4-byte seq */ + return 1; /* Fallback for invalid encoding, treat as single byte. */ +} + +/* Decode a UTF-8 sequence starting at 's' into a Unicode codepoint. + * Returns the codepoint value. Assumes valid UTF-8 encoding. */ +static uint32_t utf8DecodeChar(const char *s, size_t *len) { + unsigned char *p = (unsigned char *)s; + uint32_t cp; + + if ((*p & 0x80) == 0) { + *len = 1; + return *p; + } else if ((*p & 0xE0) == 0xC0) { + *len = 2; + cp = (*p & 0x1F) << 6; + cp |= (p[1] & 0x3F); + return cp; + } else if ((*p & 0xF0) == 0xE0) { + *len = 3; + cp = (*p & 0x0F) << 12; + cp |= (p[1] & 0x3F) << 6; + cp |= (p[2] & 0x3F); + return cp; + } else if ((*p & 0xF8) == 0xF0) { + *len = 4; + cp = (*p & 0x07) << 18; + cp |= (p[1] & 0x3F) << 12; + cp |= (p[2] & 0x3F) << 6; + cp |= (p[3] & 0x3F); + return cp; + } + *len = 1; + return *p; /* Fallback for invalid sequences. */ +} + +/* Check if codepoint is a variation selector (emoji style modifiers). */ +static int isVariationSelector(uint32_t cp) { + return cp == 0xFE0E || cp == 0xFE0F; /* Text/emoji style */ +} + +/* Check if codepoint is a skin tone modifier. */ +static int isSkinToneModifier(uint32_t cp) { + return cp >= 0x1F3FB && cp <= 0x1F3FF; +} + +/* Check if codepoint is Zero Width Joiner. */ +static int isZWJ(uint32_t cp) { + return cp == 0x200D; +} + +/* Check if codepoint is a Regional Indicator (for flag emoji). */ +static int isRegionalIndicator(uint32_t cp) { + return cp >= 0x1F1E6 && cp <= 0x1F1FF; +} + +/* Check if codepoint is a combining mark or other zero-width character. */ +static int isCombiningMark(uint32_t cp) { + return (cp >= 0x0300 && cp <= 0x036F) || /* Combining Diacriticals */ + (cp >= 0x1AB0 && cp <= 0x1AFF) || /* Combining Diacriticals Extended */ + (cp >= 0x1DC0 && cp <= 0x1DFF) || /* Combining Diacriticals Supplement */ + (cp >= 0x20D0 && cp <= 0x20FF) || /* Combining Diacriticals for Symbols */ + (cp >= 0xFE20 && cp <= 0xFE2F); /* Combining Half Marks */ +} + +/* Check if codepoint extends the previous character (doesn't start a new grapheme). */ +static int isGraphemeExtend(uint32_t cp) { + return isVariationSelector(cp) || isSkinToneModifier(cp) || + isZWJ(cp) || isCombiningMark(cp); +} + +/* Decode the UTF-8 codepoint ending at position 'pos' (exclusive) and + * return its value. Also sets *cplen to the byte length of the codepoint. */ +static uint32_t utf8DecodePrev(const char *buf, size_t pos, size_t *cplen) { + if (pos == 0) { + *cplen = 0; + return 0; + } + /* Scan backwards to find the start byte. */ + size_t i = pos; + do { + i--; + } while (i > 0 && (pos - i) < 4 && ((unsigned char)buf[i] & 0xC0) == 0x80); + *cplen = pos - i; + size_t dummy; + return utf8DecodeChar(buf + i, &dummy); +} + +/* Given a buffer and a position, return the byte length of the grapheme + * cluster before that position. A grapheme cluster includes: + * - The base character + * - Any following variation selectors, skin tone modifiers + * - ZWJ sequences (emoji joined by Zero Width Joiner) + * - Regional indicator pairs (flag emoji) */ +static size_t utf8PrevCharLen(const char *buf, size_t pos) { + if (pos == 0) return 0; + + size_t total = 0; + size_t curpos = pos; + + /* First, get the last codepoint. */ + size_t cplen; + uint32_t cp = utf8DecodePrev(buf, curpos, &cplen); + if (cplen == 0) return 0; + total += cplen; + curpos -= cplen; + + /* If we're at an extending character, we need to find what it extends. + * Keep going back through the grapheme cluster. */ + while (curpos > 0) { + size_t prevlen; + uint32_t prevcp = utf8DecodePrev(buf, curpos, &prevlen); + if (prevlen == 0) break; + + if (isZWJ(prevcp)) { + /* ZWJ joins two emoji. Include the ZWJ and continue to get + * the preceding character. */ + total += prevlen; + curpos -= prevlen; + /* Now get the character before ZWJ. */ + prevcp = utf8DecodePrev(buf, curpos, &prevlen); + if (prevlen == 0) break; + total += prevlen; + curpos -= prevlen; + cp = prevcp; + continue; /* Check if there's more extending before this. */ + } else if (isGraphemeExtend(cp)) { + /* Current cp is an extending character; include previous. */ + total += prevlen; + curpos -= prevlen; + cp = prevcp; + continue; + } else if (isRegionalIndicator(cp) && isRegionalIndicator(prevcp)) { + /* Two regional indicators form a flag. But we need to be careful: + * flags are always pairs, so only join if we're at an even boundary. + * For simplicity, just join one pair. */ + total += prevlen; + curpos -= prevlen; + break; + } else { + /* No more extending; we've found the start of the cluster. */ + break; + } + } + + return total; +} + +/* Given a buffer, position and total length, return the byte length of the + * grapheme cluster at the current position. */ +static size_t utf8NextCharLen(const char *buf, size_t pos, size_t len) { + if (pos >= len) return 0; + + size_t total = 0; + size_t curpos = pos; + + /* Get the first codepoint. */ + size_t cplen; + uint32_t cp = utf8DecodeChar(buf + curpos, &cplen); + total += cplen; + curpos += cplen; + + int isRI = isRegionalIndicator(cp); + + /* Consume any extending characters that follow. */ + while (curpos < len) { + size_t nextlen; + uint32_t nextcp = utf8DecodeChar(buf + curpos, &nextlen); + + if (isZWJ(nextcp) && curpos + nextlen < len) { + /* ZWJ: include it and the following character. */ + total += nextlen; + curpos += nextlen; + /* Get the character after ZWJ. */ + nextcp = utf8DecodeChar(buf + curpos, &nextlen); + total += nextlen; + curpos += nextlen; + continue; /* Check for more extending after the joined char. */ + } else if (isGraphemeExtend(nextcp)) { + /* Variation selector, skin tone, combining mark, etc. */ + total += nextlen; + curpos += nextlen; + continue; + } else if (isRI && isRegionalIndicator(nextcp)) { + /* Second regional indicator for a flag pair. */ + total += nextlen; + curpos += nextlen; + isRI = 0; /* Only pair once. */ + continue; + } else { + break; + } + } + + return total; +} + +/* Return the display width of a Unicode codepoint. This is a heuristic + * that works for most common cases: + * - Control chars and zero-width: 0 columns + * - Grapheme-extending chars (VS, skin tone, ZWJ): 0 columns + * - ASCII printable: 1 column + * - Wide chars (CJK, emoji, fullwidth): 2 columns + * - Everything else: 1 column + * + * This is not a full wcwidth() implementation, but a minimal heuristic + * that handles emoji and CJK characters reasonably well. */ +static int utf8CharWidth(uint32_t cp) { + /* Control characters and combining marks: zero width. */ + if (cp < 32 || (cp >= 0x7F && cp < 0xA0)) return 0; + if (isCombiningMark(cp)) return 0; + + /* Grapheme-extending characters: zero width. + * These modify the preceding character rather than taking space. */ + if (isVariationSelector(cp)) return 0; + if (isSkinToneModifier(cp)) return 0; + if (isZWJ(cp)) return 0; + + /* Wide character ranges - these display as 2 columns: + * - CJK Unified Ideographs and Extensions + * - Fullwidth forms + * - Various emoji ranges */ + if (cp >= 0x1100 && + (cp <= 0x115F || /* Hangul Jamo */ + cp == 0x2329 || cp == 0x232A || /* Angle brackets */ + (cp >= 0x231A && cp <= 0x231B) || /* Watch, Hourglass */ + (cp >= 0x23E9 && cp <= 0x23F3) || /* Various symbols */ + (cp >= 0x23F8 && cp <= 0x23FA) || /* Various symbols */ + (cp >= 0x25AA && cp <= 0x25AB) || /* Small squares */ + (cp >= 0x25B6 && cp <= 0x25C0) || /* Play/reverse buttons */ + (cp >= 0x25FB && cp <= 0x25FE) || /* Squares */ + (cp >= 0x2600 && cp <= 0x26FF) || /* Misc Symbols (sun, cloud, etc) */ + (cp >= 0x2700 && cp <= 0x27BF) || /* Dingbats (❤, ✂, etc) */ + (cp >= 0x2934 && cp <= 0x2935) || /* Arrows */ + (cp >= 0x2B05 && cp <= 0x2B07) || /* Arrows */ + (cp >= 0x2B1B && cp <= 0x2B1C) || /* Squares */ + cp == 0x2B50 || cp == 0x2B55 || /* Star, circle */ + (cp >= 0x2E80 && cp <= 0xA4CF && + cp != 0x303F) || /* CJK ... Yi */ + (cp >= 0xAC00 && cp <= 0xD7A3) || /* Hangul Syllables */ + (cp >= 0xF900 && cp <= 0xFAFF) || /* CJK Compatibility Ideographs */ + (cp >= 0xFE10 && cp <= 0xFE1F) || /* Vertical forms */ + (cp >= 0xFE30 && cp <= 0xFE6F) || /* CJK Compatibility Forms */ + (cp >= 0xFF00 && cp <= 0xFF60) || /* Fullwidth Forms */ + (cp >= 0xFFE0 && cp <= 0xFFE6) || /* Fullwidth Signs */ + (cp >= 0x1F1E6 && cp <= 0x1F1FF) || /* Regional Indicators (flags) */ + (cp >= 0x1F300 && cp <= 0x1F64F) || /* Misc Symbols and Emoticons */ + (cp >= 0x1F680 && cp <= 0x1F6FF) || /* Transport and Map Symbols */ + (cp >= 0x1F900 && cp <= 0x1F9FF) || /* Supplemental Symbols */ + (cp >= 0x1FA00 && cp <= 0x1FAFF) || /* Chess, Extended-A */ + (cp >= 0x20000 && cp <= 0x2FFFF))) /* CJK Extension B and beyond */ + return 2; + + return 1; /* Default: single width */ +} + +/* Calculate the display width of a UTF-8 string of 'len' bytes. + * This is used for cursor positioning in the terminal. + * Handles grapheme clusters: characters joined by ZWJ contribute 0 width + * after the first character in the sequence. */ +static size_t utf8StrWidth(const char *s, size_t len) { + size_t width = 0; + size_t i = 0; + int after_zwj = 0; /* Track if previous char was ZWJ */ + + while (i < len) { + size_t clen; + uint32_t cp = utf8DecodeChar(s + i, &clen); + + if (after_zwj) { + /* Character after ZWJ: don't add width, it's joined. + * But do check for extending chars after it. */ + after_zwj = 0; + } else { + width += utf8CharWidth(cp); + } + + /* Check if this is a ZWJ - next char will be joined. */ + if (isZWJ(cp)) { + after_zwj = 1; + } + + i += clen; + } + return width; +} + +/* Return the display width of a single UTF-8 character at position 's'. */ +static int utf8SingleCharWidth(const char *s, size_t len) { + if (len == 0) return 0; + size_t clen; + uint32_t cp = utf8DecodeChar(s, &clen); + return utf8CharWidth(cp); +} + +enum KEY_ACTION{ + KEY_NULL = 0, /* NULL */ + CTRL_A = 1, /* Ctrl+a */ + CTRL_B = 2, /* Ctrl-b */ + CTRL_C = 3, /* Ctrl-c */ + CTRL_D = 4, /* Ctrl-d */ + CTRL_E = 5, /* Ctrl-e */ + CTRL_F = 6, /* Ctrl-f */ + CTRL_H = 8, /* Ctrl-h */ + TAB = 9, /* Tab */ + CTRL_K = 11, /* Ctrl+k */ + CTRL_L = 12, /* Ctrl+l */ + ENTER = 13, /* Enter */ + CTRL_N = 14, /* Ctrl-n */ + CTRL_P = 16, /* Ctrl-p */ + CTRL_T = 20, /* Ctrl-t */ + CTRL_U = 21, /* Ctrl+u */ + CTRL_W = 23, /* Ctrl+w */ + ESC = 27, /* Escape */ + BACKSPACE = 127 /* Backspace */ +}; + +static void linenoiseAtExit(void); +int linenoiseHistoryAdd(const char *line); +#define REFRESH_CLEAN (1<<0) // Clean the old prompt from the screen +#define REFRESH_WRITE (1<<1) // Rewrite the prompt on the screen. +#define REFRESH_ALL (REFRESH_CLEAN|REFRESH_WRITE) // Do both. +static void refreshLine(struct linenoiseState *l); + +/* Debugging macro. */ +#if 0 +FILE *lndebug_fp = NULL; +#define lndebug(...) \ + do { \ + if (lndebug_fp == NULL) { \ + lndebug_fp = fopen("/tmp/lndebug.txt","a"); \ + fprintf(lndebug_fp, \ + "[%d %d %d] p: %d, rows: %d, rpos: %d, max: %d, oldmax: %d\n", \ + (int)l->len,(int)l->pos,(int)l->oldpos,plen,rows,rpos, \ + (int)l->oldrows,old_rows); \ + } \ + fprintf(lndebug_fp, ", " __VA_ARGS__); \ + fflush(lndebug_fp); \ + } while (0) +#else +#define lndebug(fmt, ...) +#endif + +/* ======================= Low level terminal handling ====================== */ + +/* Enable "mask mode". When it is enabled, instead of the input that + * the user is typing, the terminal will just display a corresponding + * number of asterisks, like "****". This is useful for passwords and other + * secrets that should not be displayed. */ +void linenoiseMaskModeEnable(void) { + maskmode = 1; +} + +/* Disable mask mode. */ +void linenoiseMaskModeDisable(void) { + maskmode = 0; +} + +/* Set if to use or not the multi line mode. */ +void linenoiseSetMultiLine(int ml) { + mlmode = ml; +} + +/* Return true if the terminal name is in the list of terminals we know are + * not able to understand basic escape sequences. */ +static int isUnsupportedTerm(void) { + char *term = getenv("TERM"); + int j; + + if (term == NULL) return 0; + for (j = 0; unsupported_term[j]; j++) + if (!strcasecmp(term,unsupported_term[j])) return 1; + return 0; +} + +/* Raw mode: 1960 magic shit. */ +static int enableRawMode(int fd) { + struct termios raw; + + /* Test mode: when LINENOISE_ASSUME_TTY is set, skip terminal setup. + * This allows testing via pipes without a real terminal. */ + if (getenv("LINENOISE_ASSUME_TTY")) { + rawmode = 1; + return 0; + } + + if (!isatty(STDIN_FILENO)) goto fatal; + if (!atexit_registered) { + atexit(linenoiseAtExit); + atexit_registered = 1; + } + if (tcgetattr(fd,&orig_termios) == -1) goto fatal; + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + rawmode = 1; + return 0; + +fatal: + errno = ENOTTY; + return -1; +} + +static void disableRawMode(int fd) { + /* Test mode: nothing to restore. */ + if (getenv("LINENOISE_ASSUME_TTY")) { + rawmode = 0; + return; + } + /* Don't even check the return value as it's too late. */ + if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) + rawmode = 0; +} + +/* Use the ESC [6n escape sequence to query the horizontal cursor position + * and return it. On error -1 is returned, on success the position of the + * cursor. */ +static int getCursorPosition(int ifd, int ofd) { + char buf[32]; + int cols, rows; + unsigned int i = 0; + + /* Report cursor location */ + if (write(ofd, "\x1b[6n", 4) != 4) return -1; + + /* Read the response: ESC [ rows ; cols R */ + while (i < sizeof(buf)-1) { + if (read(ifd,buf+i,1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if (buf[0] != ESC || buf[1] != '[') return -1; + if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1; + return cols; +} + +/* Try to get the number of columns in the current terminal, or assume 80 + * if it fails. */ +static int getColumns(int ifd, int ofd) { + struct winsize ws; + + /* Test mode: use LINENOISE_COLS env var for fixed width. */ + char *cols_env = getenv("LINENOISE_COLS"); + if (cols_env) return atoi(cols_env); + + if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + /* ioctl() failed. Try to query the terminal itself. */ + int start, cols; + + /* Get the initial position so we can restore it later. */ + start = getCursorPosition(ifd,ofd); + if (start == -1) goto failed; + + /* Go to right margin and get position. */ + if (write(ofd,"\x1b[999C",6) != 6) goto failed; + cols = getCursorPosition(ifd,ofd); + if (cols == -1) goto failed; + + /* Restore position. */ + if (cols > start) { + char seq[32]; + snprintf(seq,32,"\x1b[%dD",cols-start); + if (write(ofd,seq,strlen(seq)) == -1) { + /* Can't recover... */ + } + } + return cols; + } else { + return ws.ws_col; + } + +failed: + return 80; +} + +/* Clear the screen. Used to handle ctrl+l */ +void linenoiseClearScreen(void) { + if (write(STDOUT_FILENO,"\x1b[H\x1b[2J",7) <= 0) { + /* nothing to do, just to avoid warning. */ + } +} + +/* Beep, used for completion when there is nothing to complete or when all + * the choices were already shown. */ +static void linenoiseBeep(void) { + fprintf(stderr, "\x7"); + fflush(stderr); +} + +/* ============================== Completion ================================ */ + +/* Free a list of completion option populated by linenoiseAddCompletion(). */ +static void freeCompletions(linenoiseCompletions *lc) { + size_t i; + for (i = 0; i < lc->len; i++) + free(lc->cvec[i]); + if (lc->cvec != NULL) + free(lc->cvec); +} + +/* Called by completeLine() and linenoiseShow() to render the current + * edited line with the proposed completion. If the current completion table + * is already available, it is passed as second argument, otherwise the + * function will use the callback to obtain it. + * + * Flags are the same as refreshLine*(), that is REFRESH_* macros. */ +static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags) { + /* Obtain the table of completions if the caller didn't provide one. */ + linenoiseCompletions ctable = { 0, NULL }; + if (lc == NULL) { + completionCallback(ls->buf,&ctable); + lc = &ctable; + } + + /* Show the edited line with completion if possible, or just refresh. */ + if (ls->completion_idx < lc->len) { + struct linenoiseState saved = *ls; + ls->len = ls->pos = strlen(lc->cvec[ls->completion_idx]); + ls->buf = lc->cvec[ls->completion_idx]; + refreshLineWithFlags(ls,flags); + ls->len = saved.len; + ls->pos = saved.pos; + ls->buf = saved.buf; + } else { + refreshLineWithFlags(ls,flags); + } + + /* Free the completions table if needed. */ + if (lc != &ctable) freeCompletions(&ctable); +} + +/* This is an helper function for linenoiseEdit*() and is called when the + * user types the key in order to complete the string currently in the + * input. + * + * The state of the editing is encapsulated into the pointed linenoiseState + * structure as described in the structure definition. + * + * If the function returns non-zero, the caller should handle the + * returned value as a byte read from the standard input, and process + * it as usually: this basically means that the function may return a byte + * read from the termianl but not processed. Otherwise, if zero is returned, + * the input was consumed by the completeLine() function to navigate the + * possible completions, and the caller should read for the next characters + * from stdin. */ +static int completeLine(struct linenoiseState *ls, int keypressed) { + linenoiseCompletions lc = { 0, NULL }; + int nwritten; + char c = keypressed; + + completionCallback(ls->buf,&lc); + if (lc.len == 0) { + linenoiseBeep(); + ls->in_completion = 0; + } else { + switch(c) { + case 9: /* tab */ + if (ls->in_completion == 0) { + ls->in_completion = 1; + ls->completion_idx = 0; + } else { + ls->completion_idx = (ls->completion_idx+1) % (lc.len+1); + if (ls->completion_idx == lc.len) linenoiseBeep(); + } + c = 0; + break; + case 27: /* escape */ + /* Re-show original buffer */ + if (ls->completion_idx < lc.len) refreshLine(ls); + ls->in_completion = 0; + c = 0; + break; + default: + /* Update buffer and return */ + if (ls->completion_idx < lc.len) { + nwritten = snprintf(ls->buf,ls->buflen,"%s", + lc.cvec[ls->completion_idx]); + ls->len = ls->pos = nwritten; + } + ls->in_completion = 0; + break; + } + + /* Show completion or original buffer */ + if (ls->in_completion && ls->completion_idx < lc.len) { + refreshLineWithCompletion(ls,&lc,REFRESH_ALL); + } else { + refreshLine(ls); + } + } + + freeCompletions(&lc); + return c; /* Return last read character */ +} + +/* Register a callback function to be called for tab-completion. */ +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { + completionCallback = fn; +} + +/* Register a hits function to be called to show hits to the user at the + * right of the prompt. */ +void linenoiseSetHintsCallback(linenoiseHintsCallback *fn) { + hintsCallback = fn; +} + +/* Register a function to free the hints returned by the hints callback + * registered with linenoiseSetHintsCallback(). */ +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *fn) { + freeHintsCallback = fn; +} + +/* This function is used by the callback function registered by the user + * in order to add completion options given the input string when the + * user typed . See the example.c source code for a very easy to + * understand example. */ +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { + size_t len = strlen(str); + char *copy, **cvec; + + copy = malloc(len+1); + if (copy == NULL) return; + memcpy(copy,str,len+1); + cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1)); + if (cvec == NULL) { + free(copy); + return; + } + lc->cvec = cvec; + lc->cvec[lc->len++] = copy; +} + +/* =========================== Line editing ================================= */ + +/* We define a very simple "append buffer" structure, that is an heap + * allocated string where we can append to. This is useful in order to + * write all the escape sequences in a buffer and flush them to the standard + * output in a single call, to avoid flickering effects. */ +struct abuf { + char *b; + int len; +}; + +static void abInit(struct abuf *ab) { + ab->b = NULL; + ab->len = 0; +} + +static void abAppend(struct abuf *ab, const char *s, int len) { + char *new = realloc(ab->b,ab->len+len); + + if (new == NULL) return; + memcpy(new+ab->len,s,len); + ab->b = new; + ab->len += len; +} + +static void abFree(struct abuf *ab) { + free(ab->b); +} + +/* Helper of refreshSingleLine() and refreshMultiLine() to show hints + * to the right of the prompt. Now uses display widths for proper UTF-8. */ +void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int pwidth) { + char seq[64]; + size_t bufwidth = utf8StrWidth(l->buf, l->len); + if (hintsCallback && pwidth + bufwidth < l->cols) { + int color = -1, bold = 0; + char *hint = hintsCallback(l->buf,&color,&bold); + if (hint) { + size_t hintlen = strlen(hint); + size_t hintwidth = utf8StrWidth(hint, hintlen); + size_t hintmaxwidth = l->cols - (pwidth + bufwidth); + /* Truncate hint to fit, respecting UTF-8 boundaries. */ + if (hintwidth > hintmaxwidth) { + size_t i = 0, w = 0; + while (i < hintlen) { + size_t clen = utf8NextCharLen(hint, i, hintlen); + int cwidth = utf8SingleCharWidth(hint + i, clen); + if (w + cwidth > hintmaxwidth) break; + w += cwidth; + i += clen; + } + hintlen = i; + } + if (bold == 1 && color == -1) color = 37; + if (color != -1 || bold != 0) + snprintf(seq,64,"\033[%d;%d;49m",bold,color); + else + seq[0] = '\0'; + abAppend(ab,seq,strlen(seq)); + abAppend(ab,hint,hintlen); + if (color != -1 || bold != 0) + abAppend(ab,"\033[0m",4); + /* Call the function to free the hint returned. */ + if (freeHintsCallback) freeHintsCallback(hint); + } + } +} + +/* Single line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. + * + * Flags is REFRESH_* macros. The function can just remove the old + * prompt, just write it, or both. + * + * This function is UTF-8 aware and uses display widths (not byte counts) + * for cursor positioning and horizontal scrolling. */ +static void refreshSingleLine(struct linenoiseState *l, int flags) { + char seq[64]; + size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */ + int fd = l->ofd; + char *buf = l->buf; + size_t len = l->len; /* Byte length of buffer to display */ + size_t pos = l->pos; /* Byte position of cursor */ + size_t poscol; /* Display column of cursor */ + size_t lencol; /* Display width of buffer */ + struct abuf ab; + + /* Calculate the display width up to cursor and total display width. */ + poscol = utf8StrWidth(buf, pos); + lencol = utf8StrWidth(buf, len); + + /* Scroll the buffer horizontally if cursor is past the right edge. + * We need to trim full UTF-8 characters from the left until the + * cursor position fits within the terminal width. */ + while (pwidth + poscol >= l->cols) { + size_t clen = utf8NextCharLen(buf, 0, len); + int cwidth = utf8SingleCharWidth(buf, clen); + buf += clen; + len -= clen; + pos -= clen; + poscol -= cwidth; + lencol -= cwidth; + } + + /* Trim from the right if the line still doesn't fit. */ + while (pwidth + lencol > l->cols) { + size_t clen = utf8PrevCharLen(buf, len); + int cwidth = utf8SingleCharWidth(buf + len - clen, clen); + len -= clen; + lencol -= cwidth; + } + + abInit(&ab); + /* Cursor to left edge */ + snprintf(seq,sizeof(seq),"\r"); + abAppend(&ab,seq,strlen(seq)); + + if (flags & REFRESH_WRITE) { + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,l->plen); + if (maskmode == 1) { + /* In mask mode, we output one '*' per UTF-8 character, not byte */ + size_t i = 0; + while (i < len) { + abAppend(&ab,"*",1); + i += utf8NextCharLen(buf, i, len); + } + } else { + abAppend(&ab,buf,len); + } + /* Show hints if any. */ + refreshShowHints(&ab,l,pwidth); + } + + /* Erase to right */ + snprintf(seq,sizeof(seq),"\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + + if (flags & REFRESH_WRITE) { + /* Move cursor to original position (using display column, not byte). */ + snprintf(seq,sizeof(seq),"\r\x1b[%dC", (int)(poscol+pwidth)); + abAppend(&ab,seq,strlen(seq)); + } + + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Multi line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. + * + * Flags is REFRESH_* macros. The function can just remove the old + * prompt, just write it, or both. + * + * This function is UTF-8 aware and uses display widths for positioning. */ +static void refreshMultiLine(struct linenoiseState *l, int flags) { + char seq[64]; + size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */ + size_t bufwidth = utf8StrWidth(l->buf, l->len); /* Buffer display width */ + size_t poswidth = utf8StrWidth(l->buf, l->pos); /* Cursor display width */ + int rows = (pwidth+bufwidth+l->cols-1)/l->cols; /* rows used by current buf. */ + int rpos = l->oldrpos; /* cursor relative row from previous refresh. */ + int rpos2; /* rpos after refresh. */ + int col; /* column position, zero-based. */ + int old_rows = l->oldrows; + int fd = l->ofd, j; + struct abuf ab; + + l->oldrows = rows; + + /* First step: clear all the lines used before. To do so start by + * going to the last row. */ + abInit(&ab); + + if (flags & REFRESH_CLEAN) { + if (old_rows-rpos > 0) { + lndebug("go down %d", old_rows-rpos); + snprintf(seq,64,"\x1b[%dB", old_rows-rpos); + abAppend(&ab,seq,strlen(seq)); + } + + /* Now for every row clear it, go up. */ + for (j = 0; j < old_rows-1; j++) { + lndebug("clear+up"); + snprintf(seq,64,"\r\x1b[0K\x1b[1A"); + abAppend(&ab,seq,strlen(seq)); + } + } + + if (flags & REFRESH_ALL) { + /* Clean the top line. */ + lndebug("clear"); + snprintf(seq,64,"\r\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + } + + if (flags & REFRESH_WRITE) { + /* Write the prompt and the current buffer content */ + abAppend(&ab,l->prompt,l->plen); + if (maskmode == 1) { + /* In mask mode, output one '*' per UTF-8 character, not byte */ + size_t i = 0; + while (i < l->len) { + abAppend(&ab,"*",1); + i += utf8NextCharLen(l->buf, i, l->len); + } + } else { + abAppend(&ab,l->buf,l->len); + } + + /* Show hints if any. */ + refreshShowHints(&ab,l,pwidth); + + /* If we are at the very end of the screen with our prompt, we need to + * emit a newline and move the prompt to the first column. */ + if (l->pos && + l->pos == l->len && + (poswidth+pwidth) % l->cols == 0) + { + lndebug(""); + abAppend(&ab,"\n",1); + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + rows++; + if (rows > (int)l->oldrows) l->oldrows = rows; + } + + /* Move cursor to right position. */ + rpos2 = (pwidth+poswidth+l->cols)/l->cols; /* Current cursor relative row */ + lndebug("rpos2 %d", rpos2); + + /* Go up till we reach the expected position. */ + if (rows-rpos2 > 0) { + lndebug("go-up %d", rows-rpos2); + snprintf(seq,64,"\x1b[%dA", rows-rpos2); + abAppend(&ab,seq,strlen(seq)); + } + + /* Set column. */ + col = (pwidth+poswidth) % l->cols; + lndebug("set col %d", 1+col); + if (col) + snprintf(seq,64,"\r\x1b[%dC", col); + else + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + } + + lndebug("\n"); + l->oldpos = l->pos; + if (flags & REFRESH_WRITE) l->oldrpos = rpos2; + + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Calls the two low level functions refreshSingleLine() or + * refreshMultiLine() according to the selected mode. */ +static void refreshLineWithFlags(struct linenoiseState *l, int flags) { + if (mlmode) + refreshMultiLine(l,flags); + else + refreshSingleLine(l,flags); +} + +/* Utility function to avoid specifying REFRESH_ALL all the times. */ +static void refreshLine(struct linenoiseState *l) { + refreshLineWithFlags(l,REFRESH_ALL); +} + +/* Hide the current line, when using the multiplexing API. */ +void linenoiseHide(struct linenoiseState *l) { + if (mlmode) + refreshMultiLine(l,REFRESH_CLEAN); + else + refreshSingleLine(l,REFRESH_CLEAN); +} + +/* Show the current line, when using the multiplexing API. */ +void linenoiseShow(struct linenoiseState *l) { + if (l->in_completion) { + refreshLineWithCompletion(l,NULL,REFRESH_WRITE); + } else { + refreshLineWithFlags(l,REFRESH_WRITE); + } +} + +/* Insert the character(s) 'c' of length 'clen' at cursor current position. + * This handles both single-byte ASCII and multi-byte UTF-8 sequences. + * + * On error writing to the terminal -1 is returned, otherwise 0. */ +int linenoiseEditInsert(struct linenoiseState *l, const char *c, size_t clen) { + if (l->len + clen <= l->buflen) { + if (l->len == l->pos) { + /* Append at end of line. */ + memcpy(l->buf+l->pos, c, clen); + l->pos += clen; + l->len += clen; + l->buf[l->len] = '\0'; + if ((!mlmode && + utf8StrWidth(l->prompt,l->plen)+utf8StrWidth(l->buf,l->len) < l->cols && + !hintsCallback)) { + /* Avoid a full update of the line in the trivial case: + * single-width char, no hints, fits in one line. */ + if (maskmode == 1) { + if (write(l->ofd,"*",1) == -1) return -1; + } else { + if (write(l->ofd,c,clen) == -1) return -1; + } + } else { + refreshLine(l); + } + } else { + /* Insert in the middle of the line. */ + memmove(l->buf+l->pos+clen, l->buf+l->pos, l->len-l->pos); + memcpy(l->buf+l->pos, c, clen); + l->len += clen; + l->pos += clen; + l->buf[l->len] = '\0'; + refreshLine(l); + } + } + return 0; +} + +/* Move cursor on the left. Moves by one UTF-8 character, not byte. */ +void linenoiseEditMoveLeft(struct linenoiseState *l) { + if (l->pos > 0) { + l->pos -= utf8PrevCharLen(l->buf, l->pos); + refreshLine(l); + } +} + +/* Move cursor on the right. Moves by one UTF-8 character, not byte. */ +void linenoiseEditMoveRight(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos += utf8NextCharLen(l->buf, l->pos, l->len); + refreshLine(l); + } +} + +/* Move cursor to the start of the line. */ +void linenoiseEditMoveHome(struct linenoiseState *l) { + if (l->pos != 0) { + l->pos = 0; + refreshLine(l); + } +} + +/* Move cursor to the end of the line. */ +void linenoiseEditMoveEnd(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos = l->len; + refreshLine(l); + } +} + +/* Substitute the currently edited line with the next or previous history + * entry as specified by 'dir'. */ +#define LINENOISE_HISTORY_NEXT 0 +#define LINENOISE_HISTORY_PREV 1 +void linenoiseEditHistoryNext(struct linenoiseState *l, int dir) { + if (history_len > 1) { + /* Update the current history entry before to + * overwrite it with the next one. */ + free(history[history_len - 1 - l->history_index]); + history[history_len - 1 - l->history_index] = strdup(l->buf); + /* Show the new entry */ + l->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1; + if (l->history_index < 0) { + l->history_index = 0; + return; + } else if (l->history_index >= history_len) { + l->history_index = history_len-1; + return; + } + strncpy(l->buf,history[history_len - 1 - l->history_index],l->buflen); + l->buf[l->buflen-1] = '\0'; + l->len = l->pos = strlen(l->buf); + refreshLine(l); + } +} + +/* Delete the character at the right of the cursor without altering the cursor + * position. Basically this is what happens with the "Delete" keyboard key. + * Now handles multi-byte UTF-8 characters. */ +void linenoiseEditDelete(struct linenoiseState *l) { + if (l->len > 0 && l->pos < l->len) { + size_t clen = utf8NextCharLen(l->buf, l->pos, l->len); + memmove(l->buf+l->pos, l->buf+l->pos+clen, l->len-l->pos-clen); + l->len -= clen; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +/* Backspace implementation. Deletes the UTF-8 character before the cursor. */ +void linenoiseEditBackspace(struct linenoiseState *l) { + if (l->pos > 0 && l->len > 0) { + size_t clen = utf8PrevCharLen(l->buf, l->pos); + memmove(l->buf+l->pos-clen, l->buf+l->pos, l->len-l->pos); + l->pos -= clen; + l->len -= clen; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +/* Delete the previous word, maintaining the cursor at the start of the + * current word. Handles UTF-8 by moving character-by-character. */ +void linenoiseEditDeletePrevWord(struct linenoiseState *l) { + size_t old_pos = l->pos; + size_t diff; + + /* Skip spaces before the word (move backwards by UTF-8 chars). */ + while (l->pos > 0 && l->buf[l->pos-1] == ' ') + l->pos -= utf8PrevCharLen(l->buf, l->pos); + /* Skip non-space characters (move backwards by UTF-8 chars). */ + while (l->pos > 0 && l->buf[l->pos-1] != ' ') + l->pos -= utf8PrevCharLen(l->buf, l->pos); + diff = old_pos - l->pos; + memmove(l->buf+l->pos, l->buf+old_pos, l->len-old_pos+1); + l->len -= diff; + refreshLine(l); +} + +/* This function is part of the multiplexed API of Linenoise, that is used + * in order to implement the blocking variant of the API but can also be + * called by the user directly in an event driven program. It will: + * + * 1. Initialize the linenoise state passed by the user. + * 2. Put the terminal in RAW mode. + * 3. Show the prompt. + * 4. Return control to the user, that will have to call linenoiseEditFeed() + * each time there is some data arriving in the standard input. + * + * The user can also call linenoiseEditHide() and linenoiseEditShow() if it + * is required to show some input arriving asyncronously, without mixing + * it with the currently edited line. + * + * When linenoiseEditFeed() returns non-NULL, the user finished with the + * line editing session (pressed enter CTRL-D/C): in this case the caller + * needs to call linenoiseEditStop() to put back the terminal in normal + * mode. This will not destroy the buffer, as long as the linenoiseState + * is still valid in the context of the caller. + * + * The function returns 0 on success, or -1 if writing to standard output + * fails. If stdin_fd or stdout_fd are set to -1, the default is to use + * STDIN_FILENO and STDOUT_FILENO. + */ +int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) { + /* Populate the linenoise state that we pass to functions implementing + * specific editing functionalities. */ + l->in_completion = 0; + l->ifd = stdin_fd != -1 ? stdin_fd : STDIN_FILENO; + l->ofd = stdout_fd != -1 ? stdout_fd : STDOUT_FILENO; + l->buf = buf; + l->buflen = buflen; + l->prompt = prompt; + l->plen = strlen(prompt); + l->oldpos = l->pos = 0; + l->len = 0; + + /* Enter raw mode. */ + if (enableRawMode(l->ifd) == -1) return -1; + + l->cols = getColumns(stdin_fd, stdout_fd); + l->oldrows = 0; + l->oldrpos = 1; /* Cursor starts on row 1. */ + l->history_index = 0; + + /* Buffer starts empty. */ + l->buf[0] = '\0'; + l->buflen--; /* Make sure there is always space for the nulterm */ + + /* If stdin is not a tty, stop here with the initialization. We + * will actually just read a line from standard input in blocking + * mode later, in linenoiseEditFeed(). */ + if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return 0; + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + linenoiseHistoryAdd(""); + + if (write(l->ofd,prompt,l->plen) == -1) return -1; + return 0; +} + +char *linenoiseEditMore = "If you see this, you are misusing the API: when linenoiseEditFeed() is called, if it returns linenoiseEditMore the user is yet editing the line. See the README file for more information."; + +/* This function is part of the multiplexed API of linenoise, see the top + * comment on linenoiseEditStart() for more information. Call this function + * each time there is some data to read from the standard input file + * descriptor. In the case of blocking operations, this function can just be + * called in a loop, and block. + * + * The function returns linenoiseEditMore to signal that line editing is still + * in progress, that is, the user didn't yet pressed enter / CTRL-D. Otherwise + * the function returns the pointer to the heap-allocated buffer with the + * edited line, that the user should free with linenoiseFree(). + * + * On special conditions, NULL is returned and errno is populated: + * + * EAGAIN if the user pressed Ctrl-C + * ENOENT if the user pressed Ctrl-D + * + * Some other errno: I/O error. + */ +char *linenoiseEditFeed(struct linenoiseState *l) { + /* Not a TTY, pass control to line reading without character + * count limits. */ + if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return linenoiseNoTTY(); + + char c; + int nread; + char seq[3]; + + nread = read(l->ifd,&c,1); + if (nread < 0) { + return (errno == EAGAIN || errno == EWOULDBLOCK) ? linenoiseEditMore : NULL; + } else if (nread == 0) { + return NULL; + } + + /* Only autocomplete when the callback is set. It returns < 0 when + * there was an error reading from fd. Otherwise it will return the + * character that should be handled next. */ + if ((l->in_completion || c == 9) && completionCallback != NULL) { + c = completeLine(l,c); + /* Return on errors */ + if (c < 0) return NULL; + /* Read next character when 0 */ + if (c == 0) return linenoiseEditMore; + } + + switch(c) { + case ENTER: /* enter */ + history_len--; + free(history[history_len]); + if (mlmode) linenoiseEditMoveEnd(l); + if (hintsCallback) { + /* Force a refresh without hints to leave the previous + * line as the user typed it after a newline. */ + linenoiseHintsCallback *hc = hintsCallback; + hintsCallback = NULL; + refreshLine(l); + hintsCallback = hc; + } + return strdup(l->buf); + case CTRL_C: /* ctrl-c */ + errno = EAGAIN; + return NULL; + case BACKSPACE: /* backspace */ + case 8: /* ctrl-h */ + linenoiseEditBackspace(l); + break; + case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the + line is empty, act as end-of-file. */ + if (l->len > 0) { + linenoiseEditDelete(l); + } else { + history_len--; + free(history[history_len]); + errno = ENOENT; + return NULL; + } + break; + case CTRL_T: /* ctrl-t, swaps current character with previous. */ + /* Handle UTF-8: swap the two UTF-8 characters around cursor. */ + if (l->pos > 0 && l->pos < l->len) { + char tmp[32]; + size_t prevlen = utf8PrevCharLen(l->buf, l->pos); + size_t currlen = utf8NextCharLen(l->buf, l->pos, l->len); + size_t prevstart = l->pos - prevlen; + /* Copy current char to tmp, move previous char right, paste tmp. */ + memcpy(tmp, l->buf + l->pos, currlen); + memmove(l->buf + prevstart + currlen, l->buf + prevstart, prevlen); + memcpy(l->buf + prevstart, tmp, currlen); + if (l->pos + currlen <= l->len) l->pos += currlen; + refreshLine(l); + } + break; + case CTRL_B: /* ctrl-b */ + linenoiseEditMoveLeft(l); + break; + case CTRL_F: /* ctrl-f */ + linenoiseEditMoveRight(l); + break; + case CTRL_P: /* ctrl-p */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); + break; + case CTRL_N: /* ctrl-n */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); + break; + case ESC: /* escape sequence */ + /* Read the next two bytes representing the escape sequence. + * Use two calls to handle slow terminals returning the two + * chars at different times. */ + if (read(l->ifd,seq,1) == -1) break; + if (read(l->ifd,seq+1,1) == -1) break; + + /* ESC [ sequences. */ + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + /* Extended escape, read additional byte. */ + if (read(l->ifd,seq+2,1) == -1) break; + if (seq[2] == '~') { + switch(seq[1]) { + case '3': /* Delete key. */ + linenoiseEditDelete(l); + break; + } + } + } else { + switch(seq[1]) { + case 'A': /* Up */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); + break; + case 'B': /* Down */ + linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); + break; + case 'C': /* Right */ + linenoiseEditMoveRight(l); + break; + case 'D': /* Left */ + linenoiseEditMoveLeft(l); + break; + case 'H': /* Home */ + linenoiseEditMoveHome(l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(l); + break; + } + } + } + + /* ESC O sequences. */ + else if (seq[0] == 'O') { + switch(seq[1]) { + case 'H': /* Home */ + linenoiseEditMoveHome(l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(l); + break; + } + } + break; + default: + /* Handle UTF-8 multi-byte sequences. When we receive the first byte + * of a multi-byte UTF-8 character, read the remaining bytes to + * complete the sequence before inserting. */ + { + char utf8[4]; + int utf8len = utf8ByteLen(c); + utf8[0] = c; + if (utf8len > 1) { + /* Read remaining bytes of the UTF-8 sequence. */ + int i; + for (i = 1; i < utf8len; i++) { + if (read(l->ifd, utf8+i, 1) != 1) break; + } + } + if (linenoiseEditInsert(l, utf8, utf8len)) return NULL; + } + break; + case CTRL_U: /* Ctrl+u, delete the whole line. */ + l->buf[0] = '\0'; + l->pos = l->len = 0; + refreshLine(l); + break; + case CTRL_K: /* Ctrl+k, delete from current to end of line. */ + l->buf[l->pos] = '\0'; + l->len = l->pos; + refreshLine(l); + break; + case CTRL_A: /* Ctrl+a, go to the start of the line */ + linenoiseEditMoveHome(l); + break; + case CTRL_E: /* ctrl+e, go to the end of the line */ + linenoiseEditMoveEnd(l); + break; + case CTRL_L: /* ctrl+l, clear screen */ + linenoiseClearScreen(); + refreshLine(l); + break; + case CTRL_W: /* ctrl+w, delete previous word */ + linenoiseEditDeletePrevWord(l); + break; + } + return linenoiseEditMore; +} + +/* This is part of the multiplexed linenoise API. See linenoiseEditStart() + * for more information. This function is called when linenoiseEditFeed() + * returns something different than NULL. At this point the user input + * is in the buffer, and we can restore the terminal in normal mode. */ +void linenoiseEditStop(struct linenoiseState *l) { + if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return; + disableRawMode(l->ifd); + printf("\n"); +} + +/* This just implements a blocking loop for the multiplexed API. + * In many applications that are not event-drivern, we can just call + * the blocking linenoise API, wait for the user to complete the editing + * and return the buffer. */ +static char *linenoiseBlockingEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) +{ + struct linenoiseState l; + + /* Editing without a buffer is invalid. */ + if (buflen == 0) { + errno = EINVAL; + return NULL; + } + + linenoiseEditStart(&l,stdin_fd,stdout_fd,buf,buflen,prompt); + char *res; + while((res = linenoiseEditFeed(&l)) == linenoiseEditMore); + linenoiseEditStop(&l); + return res; +} + +/* This special mode is used by linenoise in order to print scan codes + * on screen for debugging / development purposes. It is implemented + * by the linenoise_example program using the --keycodes option. */ +void linenoisePrintKeyCodes(void) { + char quit[4]; + + printf("Linenoise key codes debugging mode.\n" + "Press keys to see scan codes. Type 'quit' at any time to exit.\n"); + if (enableRawMode(STDIN_FILENO) == -1) return; + memset(quit,' ',4); + while(1) { + char c; + int nread; + + nread = read(STDIN_FILENO,&c,1); + if (nread <= 0) continue; + memmove(quit,quit+1,sizeof(quit)-1); /* shift string to left. */ + quit[sizeof(quit)-1] = c; /* Insert current char on the right. */ + if (memcmp(quit,"quit",sizeof(quit)) == 0) break; + + printf("'%c' %02x (%d) (type quit to exit)\n", + isprint(c) ? c : '?', (int)c, (int)c); + printf("\r"); /* Go left edge manually, we are in raw mode. */ + fflush(stdout); + } + disableRawMode(STDIN_FILENO); +} + +/* This function is called when linenoise() is called with the standard + * input file descriptor not attached to a TTY. So for example when the + * program using linenoise is called in pipe or with a file redirected + * to its standard input. In this case, we want to be able to return the + * line regardless of its length (by default we are limited to 4k). */ +static char *linenoiseNoTTY(void) { + char *line = NULL; + size_t len = 0, maxlen = 0; + + while(1) { + if (len == maxlen) { + if (maxlen == 0) maxlen = 16; + maxlen *= 2; + char *oldval = line; + line = realloc(line,maxlen); + if (line == NULL) { + if (oldval) free(oldval); + return NULL; + } + } + int c = fgetc(stdin); + if (c == EOF || c == '\n') { + if (c == EOF && len == 0) { + free(line); + return NULL; + } else { + line[len] = '\0'; + return line; + } + } else { + line[len] = c; + len++; + } + } +} + +/* The high level function that is the main API of the linenoise library. + * This function checks if the terminal has basic capabilities, just checking + * for a blacklist of stupid terminals, and later either calls the line + * editing function or uses dummy fgets() so that you will be able to type + * something even in the most desperate of the conditions. */ +char *linenoise(const char *prompt) { + char buf[LINENOISE_MAX_LINE]; + + if (!isatty(STDIN_FILENO) && !getenv("LINENOISE_ASSUME_TTY")) { + /* Not a tty: read from file / pipe. In this mode we don't want any + * limit to the line size, so we call a function to handle that. */ + return linenoiseNoTTY(); + } else if (isUnsupportedTerm()) { + size_t len; + + printf("%s",prompt); + fflush(stdout); + if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL; + len = strlen(buf); + while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) { + len--; + buf[len] = '\0'; + } + return strdup(buf); + } else { + char *retval = linenoiseBlockingEdit(STDIN_FILENO,STDOUT_FILENO,buf,LINENOISE_MAX_LINE,prompt); + return retval; + } +} + +/* This is just a wrapper the user may want to call in order to make sure + * the linenoise returned buffer is freed with the same allocator it was + * created with. Useful when the main program is using an alternative + * allocator. */ +void linenoiseFree(void *ptr) { + if (ptr == linenoiseEditMore) return; // Protect from API misuse. + free(ptr); +} + +/* ================================ History ================================= */ + +/* Free the history, but does not reset it. Only used when we have to + * exit() to avoid memory leaks are reported by valgrind & co. */ +static void freeHistory(void) { + if (history) { + int j; + + for (j = 0; j < history_len; j++) + free(history[j]); + free(history); + } +} + +/* At exit we'll try to fix the terminal to the initial conditions. */ +static void linenoiseAtExit(void) { + disableRawMode(STDIN_FILENO); + freeHistory(); +} + +/* This is the API call to add a new entry in the linenoise history. + * It uses a fixed array of char pointers that are shifted (memmoved) + * when the history max length is reached in order to remove the older + * entry and make room for the new one, so it is not exactly suitable for huge + * histories, but will work well for a few hundred of entries. + * + * Using a circular buffer is smarter, but a bit more complex to handle. */ +int linenoiseHistoryAdd(const char *line) { + char *linecopy; + + if (history_max_len == 0) return 0; + + /* Initialization on first call. */ + if (history == NULL) { + history = malloc(sizeof(char*)*history_max_len); + if (history == NULL) return 0; + memset(history,0,(sizeof(char*)*history_max_len)); + } + + /* Don't add duplicated lines. */ + if (history_len && !strcmp(history[history_len-1], line)) return 0; + + /* Add an heap allocated copy of the line in the history. + * If we reached the max length, remove the older line. */ + linecopy = strdup(line); + if (!linecopy) return 0; + if (history_len == history_max_len) { + free(history[0]); + memmove(history,history+1,sizeof(char*)*(history_max_len-1)); + history_len--; + } + history[history_len] = linecopy; + history_len++; + return 1; +} + +/* Set the maximum length for the history. This function can be called even + * if there is already some history, the function will make sure to retain + * just the latest 'len' elements if the new history length value is smaller + * than the amount of items already inside the history. */ +int linenoiseHistorySetMaxLen(int len) { + char **new; + + if (len < 1) return 0; + if (history) { + int tocopy = history_len; + + new = malloc(sizeof(char*)*len); + if (new == NULL) return 0; + + /* If we can't copy everything, free the elements we'll not use. */ + if (len < tocopy) { + int j; + + for (j = 0; j < tocopy-len; j++) free(history[j]); + tocopy = len; + } + memset(new,0,sizeof(char*)*len); + memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy); + free(history); + history = new; + } + history_max_len = len; + if (history_len > history_max_len) + history_len = history_max_len; + return 1; +} + +/* Save the history in the specified file. On success 0 is returned + * otherwise -1 is returned. */ +int linenoiseHistorySave(const char *filename) { + mode_t old_umask = umask(S_IXUSR|S_IRWXG|S_IRWXO); + FILE *fp; + int j; + + fp = fopen(filename,"w"); + umask(old_umask); + if (fp == NULL) return -1; + fchmod(fileno(fp),S_IRUSR|S_IWUSR); + for (j = 0; j < history_len; j++) + fprintf(fp,"%s\n",history[j]); + fclose(fp); + return 0; +} + +/* Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded 0 is returned, otherwise + * on error -1 is returned. */ +int linenoiseHistoryLoad(const char *filename) { + FILE *fp = fopen(filename,"r"); + char buf[LINENOISE_MAX_LINE]; + + if (fp == NULL) return -1; + + while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) { + char *p; + + p = strchr(buf,'\r'); + if (!p) p = strchr(buf,'\n'); + if (p) *p = '\0'; + linenoiseHistoryAdd(buf); + } + fclose(fp); + return 0; +} diff --git a/src/linenoise/linenoise.h b/src/linenoise/linenoise.h new file mode 100644 index 0000000..e56b627 --- /dev/null +++ b/src/linenoise/linenoise.h @@ -0,0 +1,114 @@ +/* linenoise.h -- VERSION 1.0 + * + * Guerrilla line editing library against the idea that a line editing lib + * needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2023, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __LINENOISE_H +#define __LINENOISE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include /* For size_t. */ + +extern char *linenoiseEditMore; + +/* The linenoiseState structure represents the state during line editing. + * We pass this state to functions implementing specific editing + * functionalities. */ +struct linenoiseState { + int in_completion; /* The user pressed TAB and we are now in completion + * mode, so input is handled by completeLine(). */ + size_t completion_idx; /* Index of next completion to propose. */ + int ifd; /* Terminal stdin file descriptor. */ + int ofd; /* Terminal stdout file descriptor. */ + char *buf; /* Edited line buffer. */ + size_t buflen; /* Edited line buffer size. */ + const char *prompt; /* Prompt to display. */ + size_t plen; /* Prompt length. */ + size_t pos; /* Current cursor position. */ + size_t oldpos; /* Previous refresh cursor position. */ + size_t len; /* Current edited line length. */ + size_t cols; /* Number of columns in terminal. */ + size_t oldrows; /* Rows used by last refrehsed line (multiline mode) */ + int oldrpos; /* Cursor row from last refresh (for multiline clearing). */ + int history_index; /* The history index we are currently editing. */ +}; + +typedef struct linenoiseCompletions { + size_t len; + char **cvec; +} linenoiseCompletions; + +/* Non blocking API. */ +int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt); +char *linenoiseEditFeed(struct linenoiseState *l); +void linenoiseEditStop(struct linenoiseState *l); +void linenoiseHide(struct linenoiseState *l); +void linenoiseShow(struct linenoiseState *l); + +/* Blocking API. */ +char *linenoise(const char *prompt); +void linenoiseFree(void *ptr); + +/* Completion API. */ +typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); +typedef char*(linenoiseHintsCallback)(const char *, int *color, int *bold); +typedef void(linenoiseFreeHintsCallback)(void *); +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); +void linenoiseSetHintsCallback(linenoiseHintsCallback *); +void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); +void linenoiseAddCompletion(linenoiseCompletions *, const char *); + +/* History API. */ +int linenoiseHistoryAdd(const char *line); +int linenoiseHistorySetMaxLen(int len); +int linenoiseHistorySave(const char *filename); +int linenoiseHistoryLoad(const char *filename); + +/* Other utilities. */ +void linenoiseClearScreen(void); +void linenoiseSetMultiLine(int ml); +void linenoisePrintKeyCodes(void); +void linenoiseMaskModeEnable(void); +void linenoiseMaskModeDisable(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __LINENOISE_H */