diff --git a/.github/scripts/smoke-test.sh b/.github/scripts/smoke-test.sh new file mode 100755 index 0000000..d62f682 --- /dev/null +++ b/.github/scripts/smoke-test.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Smoke tests for morpho6 CLI (install / PR gate). +set -euo pipefail + +MORPHO6="${MORPHO6:-morpho6}" + +expect_ok() { + local desc="$1" + shift + echo "==> $desc" + "$@" +} + +expect_fail() { + local desc="$1" + shift + echo "==> $desc (expect failure)" + if "$@"; then + echo "expected failure, but succeeded: $*" >&2 + exit 1 + fi +} + +expect_contains() { + local desc="$1" + local needle="$2" + shift 2 + echo "==> $desc" + local out + out="$("$@" 2>&1)" || true + if ! grep -Fq -- "$needle" <<<"$out"; then + echo "expected output to contain: $needle" >&2 + echo "got:" >&2 + echo "$out" >&2 + exit 1 + fi +} + +# Basic eval / version / help +expect_ok "version" "$MORPHO6" --version +expect_ok "eval" "$MORPHO6" -e 'print(1)' +expect_ok "multiple -e preamble" "$MORPHO6" -e 'var x = 1' -e 'print(x)' + +# Help queries +expect_contains "CLI usage" "Usage: morpho6" "$MORPHO6" --help +expect_contains "help index topic" "Topics:" "$MORPHO6" --no-color --help help +expect_contains "help Matrix" "Matrix" "$MORPHO6" --no-color --help Matrix +expect_fail "unknown help topic" "$MORPHO6" --no-color --help NotARealTopicXYZ123 + +# Option parsing +expect_ok "workers" "$MORPHO6" --workers 2 -e 'print(1)' +expect_ok "disassemble only" "$MORPHO6" -D -e 'print(1)' +expect_contains "disassemble with source" ">>> 1 :" "$MORPHO6" --no-color -dl -e 'print(1)' + +# -D must not execute (disassembly may still mention constants) +out="$("$MORPHO6" -D -e 'print("EXECUTED")' 2>&1)" || true +if grep -Eq '^EXECUTED$' <<<"$out"; then + echo "-D should not execute the program" >&2 + echo "$out" >&2 + exit 1 +fi + +# Failure exit codes +expect_fail "missing list file" "$MORPHO6" -l /no/such/file.morpho +expect_fail "bad eval" "$MORPHO6" -e 'var x =' +expect_fail "unknown option" "$MORPHO6" --not-a-real-option + +# File + preamble +tmp="$(mktemp /tmp/morpho-smoke.XXXXXX.morpho)" +trap 'rm -f "$tmp"' EXIT +printf 'print(x)\n' >"$tmp" +expect_ok "eval preamble + file" "$MORPHO6" -e 'var x = 7' "$tmp" + +# Piped stdin + preamble +expect_ok "eval preamble + stdin" bash -c "printf 'print(x)\n' | $MORPHO6 -e 'var x = 3'" + +# NO_COLOR / --no-color should not emit CSI sequences for help +out="$(NO_COLOR=1 "$MORPHO6" --help Matrix 2>&1)" || true +if grep -Eq $'\x1B\[' <<<"$out"; then + echo "NO_COLOR=1 still produced ANSI escapes" >&2 + exit 1 +fi + +echo "All smoke tests passed." diff --git a/.github/workflows/basic-install.yml b/.github/workflows/basic-install.yml new file mode 100644 index 0000000..b786d54 --- /dev/null +++ b/.github/workflows/basic-install.yml @@ -0,0 +1,38 @@ +name: Basic install + +on: + push: + branches: [ "main", "inline" ] + pull_request: + branches: [ "main", "inline" ] + +jobs: + basic-install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install Morpho dependencies + run: | + sudo apt update + sudo apt install -y libsuitesparse-dev liblapacke-dev libunistring-dev + + - name: Build and install Morpho + run: | + git clone --depth 1 --branch dev https://github.com/Morpho-lang/morpho.git + cd morpho + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release + sudo cmake --install build --config Release + sudo ldconfig + + - name: Build and install morpho-cli + run: | + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release + sudo cmake --install build --config Release + + - name: Smoke test morpho6 + run: | + chmod +x .github/scripts/smoke-test.sh + .github/scripts/smoke-test.sh diff --git a/.gitignore b/.gitignore index b464cb7..4489f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ build-xcode/* .vscode/* .vscode/settings.json /.vscode +REVIEW.md +/dist diff --git a/README.md b/README.md index 4441e1b..23f9f0b 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,13 @@ # Morpho-cli -A terminal app for the [morpho language](https://github.com/Morpho-lang/morpho). Improved unicode support requires an external library e.g. [libgrapheme](https://libs.suckless.org/libgrapheme/) or [libunistring](https://www.gnu.org/software/libunistring/). +A terminal app for the [morpho language](https://github.com/Morpho-lang/morpho) using the [inline line editor](https://github.com/Morpho-lang/inline). Improved unicode support requires an external library e.g. [libgrapheme](https://libs.suckless.org/libgrapheme/) or [libunistring](https://www.gnu.org/software/libunistring/). Like morpho itself, Morpho-cli is released under an MIT license (see the LICENSE file for details). See the [main morpho repository](https://github.com/Morpho-lang/morpho) for information about contributing etc. Please report any issues or feature requests about the morpho-cli terminal app specifically here: suggestions for improvement are very welcome. ## Installation -For this release, morpho can be installed on all supported platforms using the homebrew package manager. Alternatively, the program can be installed from source as described below. - -### Install with homebrew - -The simplest way to install morpho-cli is through the [homebrew package manager](https://brew.sh). To do so: +The simplest way to install the morpho terminal app is to use the [homebrew package manager](https://brew.sh). To do so: 1. If not already installed, install homebrew on your machine as described on the [homebrew website](https://brew.sh) @@ -26,7 +22,36 @@ brew install morpho morpho-cli morpho-morphoview morpho-morphopm If you need to uninstall morpho, simply open a terminal and type `brew uninstall morpho-cli morpho-morphoview morpho`. It's very important to uninstall the homebrew morpho in this way before attempting to install from source as below. -### Install from source +## Running + +To run type, + + morpho6 + +into a terminal. The terminal app takes a number of optional arguments: + +| Option | Description | +|--------|-------------| +| `-h`, `--help [query]` | Show CLI help, or look up a language help topic | +| `-v`, `--version` | Show version information | +| `-c`, `--check` | Check syntax without executing | +| `-e`, `--eval ` | Execute a code string (before file/REPL if combined) | +| `-i`, `--interactive` | Enter REPL after running a file | +| `-l`, `--list ` | List a file with syntax highlighting | +| `-d`, `--disassemble` | Show disassembly | +| `-D` | Disassemble only (no execution) | +| `-dl` | Disassemble with source listing | +| `-debug`, `--debug` | Enable debugger | +| `-O`, `--optimize` | Enable optimizations | +| `-profile`, `--profile` | Enable profiling | +| `--no-color` | Disable syntax highlighting | +| `-w`, `--workers ` | Set number of worker threads | + +If no file is specified, morpho enters interactive mode. If stdin is piped or redirected, morpho reads and executes from stdin. Any options after the file name are passed to the morpho program and are available from the `System` class: + + var args = System.arguments() + +## Manual install from source To install, clone this repository: @@ -35,13 +60,8 @@ To install, clone this repository: and then, cd morpho-cli - mkdir build - cd build - cmake -DCMAKE_BUILD_TYPE=Release .. - make install + cmake -S . -B build + cmake --build build --config Release + sudo cmake --install build --config Release -You may need to use sudo make install. - -To run, - - morpho6 +This manual build installs into '/usr/local/bin' by default. diff --git a/help/Makefile b/help/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/help/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/help/array.md b/help/array.md deleted file mode 100644 index 4f0b197..0000000 --- a/help/array.md +++ /dev/null @@ -1,34 +0,0 @@ -[comment]: # (Array class help) -[version]: # (0.5) - -# Array -[tagarray]: # (Array) - -Arrays are collection objects that can have any number of indices. Their size is set when they are created: - - var a[5] - var b[2,2] - var c[nv,nv,nv] - -Values can be retrieved with appropriate indices: - - print a[0,0] - -Array can be indexed with slices: - - print a[[0,2,4],2] - print a[1,0..2] - -Any morpho value can be stored in an array element - - a[0,0] = [1,2,3] - -[showsubtopics]: # (subtopics) - -## Dimensions -[tagdimensions]: # (Dimensions) - -Get the dimensions of an Array object: - - var a[2,2] - print a.dimensions() // expect: [ 2, 2 ] diff --git a/help/builtin.md b/help/builtin.md deleted file mode 100644 index 0489dd4..0000000 --- a/help/builtin.md +++ /dev/null @@ -1,294 +0,0 @@ -[comment]: # (Builtin function help) -[version]: # (0.5) - -# Builtin functions -[tagbuiltin]: # (builtin) - -Morpho provides a number of built-in functions. - -[showsubtopics]: # (subtopics) - -## Random -[tagrandom]: # (random) -[tagrand]: # (rand) - -The `random` function generates a random number from a uniform distribution on the interval [0,1]. - - print random() - -See also `randomnormal` and `randomint`. - -## Randomnormal -[tagrandomnormal]: # (randomnormal) - -The `randomnormal` function generates a random number from a normal (gaussian) distribution with unit variance and zero offset. - - print randomnormal() - -See also `random` and `randomint`. - -## Randomint -[tagrandomnormal]: # (randomnormal) - -The `randomint` function generates a random integer with a specified maximum value. - - print randomint(10) // Generates a random integer [0,10) - -## isnil -[tagisnil]: # (isnil) - -Returns `true` if a value is `nil` or `false` otherwise. - -## isint -[tagisint]: # (isint) - -Returns `true` if a value is an integer or `false` otherwise. - -## isfloat -[tagisfloat]: # (isfloat) - -Returns `true` if a value is a floating point number or `false` otherwise. - -## isbool -[tagisbool]: # (isbool) - -Returns `true` if a value is a boolean or `false` otherwise. - -## isobject -[tagisobject]: # (isobject) - -Returns `true` if a value is an object or `false` otherwise. - -## isstring -[tagisstring]: # (isstring) - -Returns `true` if a value is a string or `false` otherwise. - -## isclass -[tagisclass]: # (isclass) - -Returns `true` if a value is a class or `false` otherwise. - -## isrange -[tagisrange]: # (isrange) - -Returns `true` if a value is a range or `false` otherwise. - -## isdictionary -[tagisdictionary]: # (isdictionary) - -Returns `true` if a value is a dictionary or `false` otherwise. - -## islist -[tagislist]: # (islist) - -Returns `true` if a value is a list or `false` otherwise. - -## isarray -[tagisarray]: # (isarray) - -Returns `true` if a value is an array or `false` otherwise. - -## ismatrix -[tagismatrix]: # (ismatrix) - -Returns `true` if a value is a matrix or `false` otherwise. - -## issparse -[tagissparse]: # (issparse) - -Returns `true` if a value is a sparse matrix or `false` otherwise. - -## isinf -[tagisinf]: # (isinf) - -Returns `true` if a value is infinite or `false` otherwise. - -## isnan -[tagisnan]: # (isnan) - -Returns `true` if a value is a Not a Number or `false` otherwise. - -## iscallable -[tagiscallable]: # (iscallable) - -Returns `true` if a value is callable or `false` otherwise. - -## isfinite -[tagisfinite]: # (isfinite) - -Returns `true` if a value is finite or `false` otherwise. - - print isfinite(1) // expect: true - print isfinite(1/0) // expect: false - -## isnumber -[tagisnumber]: # (isnumber) - -Returns `true` if a value is a real number, or `false` otherwise. - - print isnumber(1) // expect: true - print isnumber(Object()) // expect: false - -## ismesh -[tagismesh]: # (ismesh) - -Returns `true` if a value is a `Mesh`, or `false` otherwise. - -## isselection -[tagisselection]: # (isselection) - -Returns `true` if a value is a `Selection`, or `false` otherwise. - -## isfield -[tagisfield]: # (isfield) - -Returns `true` if a value is a `Field`, or `false` otherwise. - -## Apply -[tagapply]: # (apply) - -Apply calls a function with the arguments provided as a list: - - apply(f, [0.5, 0.5]) // calls f(0.5, 0.5) - -It's often useful where a function or method and/or the number of parameters isn't known ahead of time. The first parameter to apply can be any callable object, including a method invocation or a closure. - -You may also instead omit the list and use apply with multiple arguments: - - apply(f, 0.5, 0.5) // calls f(0.5, 0.5) - -There is one edge case that occurs when you want to call a function that accepts a single list as a parameter. In this case, enclose the list in another list: - - apply(f, [[1,2]]) // equivalent to f([1,2]) - -## Abs -[tagabs]: # (abs) - -Returns the absolute value of a number: - - print abs(-10) // prints 10 - -## Arctan -[tagarctan]: # (arctan) - -Returns the arctangent of an input value that lies from `-Inf` to `Inf`. You can use one argument: - - print arctan(0) // expect: 0 - -or use two arguments to return the angle in the correct quadrant: - - print arctan(x, y) - -Note the order `x`, `y` differs from some other languages. - -## Exp -[tagexp]: # (exp) - -Exponential function `e^x`. Inverse of `log`. - - print exp(0) // expect: 1 - print exp(Pi*im) // expect: -1 + 0im - -## Log -[taglog]: # (log) - -Natural logarithm function. Inverse of `exp`. - - print log(1) // expect: 0 - -## Log10 -[taglog10]: # (log10) - -Base 10 logarithm function. - - print log10(10) // expect: 1 - -## Sin -[tagsin]: # (sin) - -Sine trigonometric function. - - print sin(0) // expect: 0 - -## Sinh -[tagsinh]: # (sinh) - -Hyperbolic sine trigonometric function. - - print sinh(0) // expect: 0 - -## Cos -[tagcos]: # (cos) - -Cosine trigonometric function. - - print cos(0) // expect: 1 - -## Cosh -[tagcosh]: # (cosh) - -Hyperbolic cosine trigonometric function. - - print cosh(0) // expect: 1 - -## Tan -[tagtan]: # (tan) - -Tangent trigonometric function. - - print tan(0) // expect: 0 - -## Tanh -[tagtanh]: # (tanh) - -Hyperbolic tangent trigonometric function. - - print tanh(0) // expect: 0 - -## Asin -[tagasin]: # (asin) - -Inverse sine trigonometric function. Returns a value on the interval `[-Pi/2,Pi/2]`. - - print asin(0) // expect: 0 - -## Acos -[tagacos]: # (acos) - -Inverse cosine trigonometric function. Returns a value on the interval `[-Pi/2,Pi/2]`. - - print acos(1) // expect: 0 - -## Sqrt -[tagsqrt]: # (sqrt) - -Square root function. - - print sqrt(4) // expect: 2 - -## Min -[tagmin]: # (min) - -Finds the minimum value of its arguments. If any of the arguments are Objects and are enumerable, (e.g. a `List`), `min` will search inside them for a minimum value. Accepts any number of arguments. - - print min(3,2,1) // expect: 1 - print min([3,2,1]) // expect: 1 - print min([3,2,1],[0,-1,2]) // expect: -2 - -## Max -[tagmax]: # (max) - -Finds the maximum value of its arguments. If any of the arguments are Objects and are enumerable, (e.g. a `List`), `max` will search inside them for a maximum value. Accepts any number of arguments. - - print min(3,2,1) // expect: 3 - print min([3,2,1]) // expect: 3 - print min([3,2,1],[0,-1,2]) // expect: 3 - -## Bounds -[tagbounds]: # (bounds) - -Returns both the results of `min` and `max` as a list, Providing a set of bounds for its arguments and any enumerable objects within them. - - print bounds(1,2,3) // expect: [1,3] - print bounds([3,2,1],[0,-1,2]) // expect: [-1,3] diff --git a/help/classes.md b/help/classes.md deleted file mode 100644 index f57f6f0..0000000 --- a/help/classes.md +++ /dev/null @@ -1,85 +0,0 @@ -[comment]: # (Morpho classes help file) -[version]: # (0.5) - -[toplevel]: # - -# Classes -[tagclass]: # (class) - -Classes are defined using the `class` keyword followed by the name of the class. -The definition includes methods that the class responds to. The special `init` method -is called whenever an object is created. - - class Cake { - init(type) { - self.type = type - } - - eat() { - print "A delicious "+self.type+" cake" - } - } - -Objects are created by calling the class as if it was a function: - - var c = Cake("carrot") - -Methods are called using the . operator: - - c.eat() - -[showsubtopics]: # (subtopics) - -## Is -[tagis]: # (is) - -The `is` keyword is used to specify a class's superclass: - - class A is B { - - } - -All methods defined by the superclass `B` are copied into the new class `A`, *before* any methods specified in the class definition. Hence, you can replace methods from the superclass simply by defining a method with the same name. - -## With -[tagwith]: # (with) -[tagmixin]: # (mixin) - -The `with` keyword is used together with `is` to insert additional methods into a class definition *without* making them the superclass. These are often called `mixins`. These methods are inserted after the superclass's methods. Multiple classes can be specified after `with`; they are added in the order specified. - - class A is B with C, D { - - } - -Here `B` is the superclass of `A`, but methods defined by `C` and `D` are also available to `A`. If `B`, `C` and `D` define methods with the same name, those in `C` take precedence over any in `B` and those in `D` take precedence over `B` and `C`. - -## Self -[tagself]: # (self) - -The `self` keyword is used to access an object's properties and methods from within its definition. - - class Vehicle { - init (type) { self.type = type } - - drive () { print "Driving my ${self.type}." } - } - -## Super -[tagsuper]: # (super) - -The keyword `super` allows you to access methods provided by an object's superclass rather than its own. This is particularly useful when the programmer wants a class to extend the functionality of a parent class, but needs to make sure the old behavior is still maintained. - -For example, consider the following pair of classes: - - class Lunch { - init(type) { self.type=type } - } - - class Soup is Lunch { - init(type) { - print "Delicious soup!" - super.init(type) - } - } - -The subclass Soup uses `super` to call the original initializer. diff --git a/help/color.md b/help/color.md deleted file mode 100644 index 93a5c31..0000000 --- a/help/color.md +++ /dev/null @@ -1,132 +0,0 @@ -[comment]: # (Color module help) -[version]: # (0.5) - -# Color -[tagcolor]: # (color) - -The `color` module provides support for working with color. Colors are represented in morpho by `Color` objects. The module predefines some colors including `Red`, `Green`, `Blue`, `Black`, `White`. - -To use the module, use import as usual: - - import color - -Create a Color object from an RGB pair: - - var col = Color(0.5,0.5,0.5) // A 50% gray - -The `color` module also provides `ColorMap`s, which are give a sequence of colors as a function of a parameter; these are useful for plotting the values of a `Field` for example. - -[showsubtopics]: # (subtopics) - -## RGB -[tagrgb]: # (rgb) - -Gets the rgb components of a `Color` or `ColorMap` object as a list. Takes a single argument in the range 0 to 1, although the result will only depend on this argument if the object is a `ColorMap`. - - var col = Color(0.1,0.5,0.7) - print col.rgb(0) - -## Red -[tagred]: # (red) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Green -[taggreen]: # (green) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Blue -[tagblue]: # (blue) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## White -[tagwhite]: # (white) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Black -[tagblack]: # (black) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Cyan -[tagcyan]: # (cyan) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Magenta -[tagmagenta]: # (magenta) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Yellow -[tagyellow]: # (yellow) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Brown -[tagbrown]: # (brown) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Orange -[tagorange]: # (orange) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Pink -[tagpink]: # (pink) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Purple -[tagpurple]: # (purple) -Built in `Color` object for use with the `graphics` and `plot` modules. - -## Colormap -[tagcolormap]: # (colormap) -The `color` module provides `ColorMap`s which are subclasses of `Color` that map a single parameter in the range 0 to 1 onto a continuum of colors. `Color`s and `Colormap`s have the same interface. - -Get the red, green or blue components of a color or colormap: - - var col = HueMap() - print col.red(0.5) // argument can be in range 0 to 1 - -Get all three components as a list: - - col.rgb(0) - -Create a grayscale: - - var c = Gray(0.2) // 20% gray - -Available ColorMaps: `GradientMap`, `GrayMap`, `HueMap`, `ViridisMap`, `MagmaMap`, `InfernoMap` and `PlasmaMap`. - -## GradientMap -[taggradientmap]: # (gradientmap) - -`GradientMap` is a `Colormap` that displays a white-green-purple sequence. - -## GrayMap -[taggraymap]: # (graymap) - -`GrayMap` is a `Colormap` that displays grayscales. - -## HueMap -[taghuemap]: # (huemap) - -`HueMap` is a `Colormap` that displays vivid colors. It is periodic on the interval 0 to 1. - -## ViridisMap -[tagviridismap]: # (viridismap) - -`ViridisMap` is a `Colormap` that displays a purple-green-yellow sequence. -It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. - -## MagmaMap -[tagmagmamap]: # (magmamap) - -`MagmaMap` is a `Colormap` that displays a black-red-yellow sequence. -It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. - -## InfernoMap -[taginfernomap]: # (infernomap) - -`InfernoMap` is a `Colormap` that displays a black-red-yellow sequence. -It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. - -## PlasmaMap -[tagplasmamap]: # (plasmamap) - -`InfernoMap` is a `Colormap` that displays a blue-red-yellow sequence. It is perceptually uniform and intended to be improve the accessibility of visualizations for viewers with color vision deficiency. diff --git a/help/complex.md b/help/complex.md deleted file mode 100644 index edc5252..0000000 --- a/help/complex.md +++ /dev/null @@ -1,43 +0,0 @@ -[comment]: # (Complex help) -[version]: # (0.5) - -# Complex -[tagcomplex]: # (complex) -[tagim]: # (im) - -Morpho provides complex numbers. The keyword `im` is used to denote the imaginary part of a complex number: - - var a=1+5im - print a*a - -Print values on the unit circle in the complex plane: - - import constants - for (phi in 0..Pi:Pi/5) print exp(im*phi) - -Get the real and imaginary parts of a complex number: - - print real(a) - print imag(a) - -or alternatively: - - print a.real() - print a.imag() - -[showsuptopics]: # subtopics - -## Angle -[tagangle]: # (angle) - -Returns the angle `phi` associated with the polar representation of a complex number `r*exp(im*phi)`: - - print z.angle() - -## Conj -[tagconjugate]: # (conjugate) -[tagconj]: # (conj) - -Returns the complex conjugate of a number: - - print z.conj() diff --git a/help/conf.py b/help/conf.py deleted file mode 100644 index 6b7bcc2..0000000 --- a/help/conf.py +++ /dev/null @@ -1,60 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -# -- Project information ----------------------------------------------------- - -project = 'morpho' -copyright = '2021, T J Atherton' -author = 'T J Atherton' - -# The full version, including alpha/beta/rc tags -release = '0.5.0' - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['myst_parser'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# Use recommonmark to parse md files -from recommonmark.parser import CommonMarkParser -source_parsers = {'.md': CommonMarkParser} -source_suffix = ['.rst', '.md'] - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - - -latex_engine = 'xelatex' diff --git a/help/constants.md b/help/constants.md deleted file mode 100644 index 01d2fcc..0000000 --- a/help/constants.md +++ /dev/null @@ -1,14 +0,0 @@ -[comment]: # (Constants module help) -[version]: # (0.5) - -# Constants -[tagconstants]: # (constants) - -The constants module contains a number of useful mathematical and physical constants. Import it like any other module: - - import constants - -Available constants: - -* `E` the base of natural logarithms. -* `Pi` ratio of the perimeter of a circle to its diameter. diff --git a/help/controlflow.md b/help/controlflow.md deleted file mode 100644 index 973a1ac..0000000 --- a/help/controlflow.md +++ /dev/null @@ -1,192 +0,0 @@ -[comment]: # (Morpho control flow help file) -[version]: # (0.5) - -[toplevel]: # - -# Control Flow -[tagcontrol]: # (control) - -Control flow statements are used to determine whether and how many times a selected piece of code is executed. These include: - -* `if` - Selectively execute a piece of code if a condition is met. -* `else` - Execute a different block of code if the test in an `if` statement fails. -* `for` - Repeatedly execute a section of code with a counter -* `while` - Repeatedly execute a section of code while a condition is true. - -## If -[tagif]: # (if) -[tagelse]: # (else) - -`If` allows you to selectively execute a section of code depending on whether a condition is met. The simplest version looks like this: - - if (x<1) print x - -where the body of the loop, `print x`, is only executed if x is less than 1. The body can be a code block to accommodate longer sections of code: - - if (x<1) { - ... // do something - } - -If you want to choose between two alternatives, use `else`: - - if (a==b) { - // do something - } else { - // this code is executed only if the condition is false - } - -You can even chain multiple tests together like this: - - if (a==b) { - // option 1 - } else if (a==c) { - // option 2 - } else { - // something else - } - -## While -[tagwhile]: # (while) - -While loops repeat a section of code while a condition is true. For example, - - var k=1 - while (k <= 4) { print k; k+=1 } - ^cond ^body - -prints the numbers 1 to 4. The loop has two sections: `cond` is the condition to be executed and `body` is the section of code to be repeated. - -Simple loops like the above example, especially those that involve counting out a sequence of numbers, are more conveniently written using a `for` loop, - - for (k in 1..4) print k - -Where `while` loops can be very useful is where the state of an object is being changed in the loop, e.g. - - var a = List(1,2,3,4) - while (a.count()>0) print a.pop() - -which prints 4,3,2,1. - -## Do -[tagdo]: # (do) - -A `do`...`while` loop repeats code while a condition is true---similar to a `while` loop---but the test happens at the end: - - var k=1 - do { - print k; - k+=1 - } while (k<5) - -which prints 1,2,3,4 - -Hence this type of loop executes at least one interation - -## For -[tagfor]: # (for) -[tagin]: # (in) - -For loops allow you to repeatedly execute a section of code. They come in two versions: the simpler version looks like this, - - for (var i in 1..5) print i - -which prints the numbers 1 to 5 in turn. The variable `i` is the *loop variable*, which takes on a different value each iteration. `1..5` is a range, which denotes a sequence of numbers. The *body* of the loop, `print i`, is the code to be repeatedly executed. - -Morpho will implicitly insert a `var` before the loop variable if it's missing, so this works too: - - for (i in 1..5) print i - -If you want your loop variable to count in increments other than 1, you can specify a stepsize in the range: - - for (i in 1..5:2) print i - ^step - -Ranges need not be integer: - - for (i in 0.1..0.5:0.1) print i - -You can also replace the range with other kinds of collection object to loop over their contents: - - var a = Matrix([1,2,3,4]) - for (x in a) print x - -Morpho iterates over the collection object using an integer *counter variable* that's normally hidden. If you want to know the current value of the counter (e.g. to get the index of an element as well as its value), you can use the following: - - var a = [1, 2, 3] - for (x, i in a) print "${i}: ${x}" - -Morpho also provides a second form of `for` loop similar to that in C: - - for (var i=0; i<5; i+=1) { print i } - ^start ^test ^inc. ^body - -which is executed as follows: - start: the variable `i` is declared and initially set to zero. - test: before each iteration, the test is evaluated. If the test is `false`, the loop terminates. - body: the body of the loop is executed. - inc: the variable `i` is increased by 1. - -You can include any code that you like in each of the sections. - -## Break -[tagbreak]: # (break) - -`Break` is used inside loops to finish the loop early. For example - - for (i in 1..5) { - if (i>3) break // --. - print i // | (Once i>3) - } // | - ... // <-' - -would only print 1, 2 and 3. Once the condition `i>3` is true, the `break` statement causes execution to continue after the loop body. - -Both `for` and `while` loops support break. - -## Continue -[tagcontinue]: # (continue) - -`Continue` is used inside loops to skip over the rest of an iteration. For example - - for (i in 1..5) { // <-. - print "Hello" | - if (i>3) continue // --' - print i - } - -prints "Hello" five times but only prints 1, 2 and 3. Once the condition `i>3` is true, the `continue` statement causes execution to transfer to the start of the loop body. - -Traditional `for` loops also support `continue`: - - // v increment - for (var i=0; i<5; i+=1) { - if (i==2) continue - print i - } - -Since `continue` causes control to be transferred *to the increment section* in this kind of loop, here the program prints 0..4 but the number 2 is skipped. - -Use of `continue` with `while` loops is possible but isn't recommended as it can easily produce an infinite loop! - - var i=0 - while (i<5) { - if (i==2) continue - print i - i+=1 - } - -In this example, when the condition `i==2` is `true`, execution skips back to the start, but `i` *isn't* incremented. The loop gets stuck in the iteration `i==2`. - -## Try -[tagtry]: # (try) -[tagcatch]: # (catch) - -A `try` and `catch` statement allow you handle errors. For example - - try { - // Do something - } catch { - "Tag" : // Handle the error - } - -Code within the block after the `try` keyword is executed. If an error is generated then Morpho looks to see if the tag associated with the error matches any of the labels in the `catch` block. If it does, the code after the matching label is executed. If no error occurs, the catch block is skipped entirely. diff --git a/help/delaunay.md b/help/delaunay.md deleted file mode 100644 index d7de7be..0000000 --- a/help/delaunay.md +++ /dev/null @@ -1,48 +0,0 @@ -[comment]: # (Delaunay module help) -[version]: # (0.5) - -# Delaunay -[tagdelaunay]: # (delaunay) - -The `delaunay` module creates Delaunay triangulations from point clouds. It is dimensionally independent, so generates tetrahedra in 3D and higher order simplices beyond. - -To use the module, first import it: - - import delaunay - -To create a Delaunary triangulation from a list of points: - - var pts = [] - for (i in 0...100) pts.append(Matrix([random(), random()])) - var del=Delaunay(pts) - print del.triangulate() - -The module also provides `DelaunayMesh` to directly create meshes from Delaunay triangulations. - -[showsubtopics]: # (subtopics) - -## Triangulate -[tagtriangulate]: # (triangulate) - -The `triangulate` method performs the delaunay triangulation. To use it, first construct a `Delaunay` object with the point cloud of interest: - - var del=Delaunay(pts) - -Then call `triangulate`: - - var tri = del.triangulate() - -This returns a list of triangles `[ [i, j, k], ... ]`. - -## Circumsphere -[tagcircumsphere]: # (circumsphere) - -The `Circumsphere` class calculates the circumsphere of a set of points, i.e. a sphere such that all the points are on the surface of the sphere. It is used internally by the `delaunay` module. - -Create a `Circumsphere` from a list of points and a triangle specified by indices into that list: - - var sph = Circumsphere(pts, [i,j,k]) - -Test if an arbitrary point is inside the `Circumsphere` or not: - - print sph.pointinsphere(pt) diff --git a/help/dictionary.md b/help/dictionary.md deleted file mode 100644 index 989ffc5..0000000 --- a/help/dictionary.md +++ /dev/null @@ -1,51 +0,0 @@ -[comment]: # (Dictionary help) -[version]: # (0.5) - -# Dictionary -[tag]: # (Dictionary) - -Dictionaries are collection objects that associate a unique *key* with a particular *value*. Keys can be any kind of morpho value, including numbers, strings and objects. - -An example dictionary mapping states to capitals: - - var dict = { "Massachusetts" : "Boston", - "New York" : "Albany", - "Vermont" : "Montpelier" } - -Look up values by a given key with index notation: - - print dict["Vermont"] - -You can change the value associated with a key, or add new elements to the dictionary like this: - - dict["Maine"]="Augusta" - -Create an empty dictionary using the `Dictionary` constructor function: - - var d = Dictionary() - -Loop over keys in a dictionary: - - for (k in dict) print k - -The `keys` method returns a Morpho List of the keys. - - var keys = dict.keys() // will return ["Massachusetts", "New York", "Vermont"] - -The `contains` method returns a Bool value for whether the Dictionary -contains a given key. - - print dict.contains("Vermont") // true - print dict.contains("New Hampshire") // false - -The `remove` method removes a given key from the Dictionary. - - dict.remove("Vermont") - print dict // { New York : Albany, Massachusetts : Boston } - -The `clear` method removes all the (key, value) pairs fromt the -dictionary, resulting in an empty dictionary. - - dict.clear() - - print dict // { } diff --git a/help/errors.md b/help/errors.md deleted file mode 100644 index 4d404cc..0000000 --- a/help/errors.md +++ /dev/null @@ -1,256 +0,0 @@ -[comment]: # (Errors help file) -[version]: # (0.5) - -# Errors -[tagerrors]: # (errors) - -When an error occurs in running a morpho program, an error message is displayed together with an explanation of where in the program that the error happened. - -[showsubtopics]: # (subtopics) - -## Alloc -[tagalloc]: # (alloc) - -This error may occur when creating new objects or resizing them. It typically indicates that the computer is under memory pressure. - -## ArrayDim -[tagarraydim]: # arraydim - -This error occurs if you try to index an array with the wrong number of indices: - - var a[2,2] - print a[1] - -## ClssLcksMthd -[tagclsslcksmthd]: # (clsslcksmthd) - -This error occurs if you try to invoke a method on a class that doesn't exist: - - class Foo { } - print Foo.foo() - -## CnctFld -[tagcnctfld]: # (cnctfld) - -This error occurs when concatenation of strings or other objects fails, typically because of low memory. - -## DbgQuit -[tagdbgquit]: # (dbgquit) - -This notification is generated after selecting `Quit` within the debugger. Execution of the program is halted and control returns to the user. - -## DctSprtr -[tagdctsprtr]: # (dctsprtr) - -This error occurs when a Dictionary initializer is missing a ':' between a key/vale pair - - var a = { "A" "B" } // Missing colon - -Fix by putting in the missing separator - - var a = { "A" : "B" } - -## DctTrmntr -[tagdcttrmntr]: # (dcttrmntr) - -This error occurs when a Dictionary initializer is missing the '}' required at the end: - - var a = { "A" : "B" // Missing terminator - -Fix by inserting the missing curly brace at the end - - var a = { "A" : "B" } - -## GlblRtrn -[tagglblrtrn]: # (glblrtrn) - -This error occurs when morpho encounters a `return` keyword outside of a function or method definition. - -## InstFail -[taginstfail]: # (instfail) - -This error occurs when morpho tried to create a new object, but something went wrong. - -## Intrnl -[tagintrnl]: # (intrnl) - -This error indicates an internal problem with morpho. Please contact the developers for support. - -## InvldArgs -[taginvldargs]: # (invldargs) - -This error occurs if you call a function with the wrong number of arguments: - - fn f(x) { return x } - f(1,2) - -## InvldOp -[taginvldop]: # (invldop) - -This error occurs when an operator like `+` or `-` is given operands that it doesn't understand. For example, - - print "Hello" * "Goodbye" // Causes 'InvldOp' - -causes this error because the multiplication operator doesn't know how to multiply strings. - -If the operands are objects, this means that the objects don't provide a method for the requested operation, e.g. for - - print object1 / object2 - -`object1` would need to provide a `div()` method that can successfully handle `object2`. - -## InvldUncd -[taginvlduncd]: # (invlduncd) - -This error occurs when an invalid unicode escape sequence is found in a string. - - print "\u23" // Must have 4 hex characters - print "\U01F98B" // Must have 8 hex characters - -Fix by specifying the character in the correct form: - - print "\U0001F98B" - -## IndxBnds -[tagindxbnds]: # (indxbnds) - -This error can occur when selecting an entry from a collection object (such as a list) if the index supplied is bigger than the number of entries: - - var a = [1,2,3] - print a[10] - -## MssngComma -[tagmssngcomma]: # (mssngcomma) - -This error occurs when a comma is missing between expressions. For example, a comma is required between dictionary entries - - var a = { "A" : "B" "C" : "D" } - -Fix this by inserting the missing comma - - var a = { "A" : "B" , "C" : "D" } - -## MtrxIncmptbl -[tagmtrxincmptbl]: # (mtrxincmptbl) - -This error occurs when an arithmetic operation is performed on two 'incompatible' matrices. For example, two matrices must have the same dimensions, i.e. the same number of rows and columns, to be added or subtracted, - - var a = Matrix([[1,2],[3,4]]) - var b = Matrix([[1]]) - print a+b // generates a `MtrxIncmptbl` error. - -Or to be multiplied together, the number of columns of the left hand matrix must equal the number of rows of the right hand matrix. - - var a = Matrix([[1,2],[3,4]]) - var b = Matrix([1,2]) - print a*b // ok - print b*a // generates a `MtrxIncmptbl` error. - -## NoInit -[tagnoinit]: # (noinit) - -This error can occur if you try to create a new object from a class that doesn't have an `init` method: - - class Foo { } - var a = Foo(0.3) - -Here, the argument to `Foo` causes the `NoInit` error because no `init` method is available to process it. - -## NotAnObj -[tagnotanobj]: # (notanobj) - -This error occurs if you try to access a property of something that isn't an object: - - var a = 1 - a.size = 5 - -## NonNmIndx -[tagnonnmindx]: # (nonnmindx) - -This error occurs if you try to index an array with a non-numerical index: - - var a[2,2] - print a["foo","bar"] - -## NotAnInst -[tagnotaninst]: # (notaninst) - -This error occurs if you try to invoke a method on something that isn't an object: - - var a = 4 - print a.foo() - -## NotIndxbl -[tagnotindxbl]: # (notindxbl) - -This error occurs if you try to index something that isn't a collection: - - var a = 0.3 - print a[1] - -## ObjLcksPrp -[tagobjlcksprp]: # (objlcksprp) - -This error occurs if you try to access a property or method that hasn't been defined for an object: - - var a = Object() - print a.pifflepaffle - -or - - print a.foo() - -## RcrsnLmt -[tagrcrsnlmt]: # (rcrsnlmt) - -This error may occur when trying to parse very deeply nested structures. - -## StrEsc -[tagstresc]: # (stresc) - - - -## SymblUndf -[tagsymblundf]: # (symblundf) - -This error occurs if you refer to something that has not been previously declared, for example trying to use a variable of call a function that doesn't exist. It's possible that the symbol is spelt incorrectly, or that the capitalization doesn't match the definition (*morpho* symbols are case-sensitive). - -A common problem is to try to assign to a variable that hasn't yet been declared: - - a = 5 - -To fix this, prefix with `var`: - - var a = 5 - -## Uncallable -[taguncallable]: # (uncallable) - -This error occurs when you try to call something that isn't a method or a function. Here, we initialize a variable with a string and call it: - - var f = "Not a function" - f() // Causes 'Uncallable' - -## UnescpdCtrl -[tagunescpdctrl]: # (unescpdctrl) - -This error is generated when morpho detects a control character in a string literal. To fix, specify the control character using its character code - - print "\x09" // Tab character - -or use the relevant shorthand code - - print "\t" // Also tab - -## ValRng -[tagvalrng]: # (valrng) - -This error occurs when the morpho compiler encounters a value that can't be represented in morpho. For example, - - print 1.2e1000 - -lies outside of double precision floating point arithmetic. Similarly, - - print 12345678901234567 - -is too big to be a morpho integer. \ No newline at end of file diff --git a/help/field.md b/help/field.md deleted file mode 100644 index 0e9d4d6..0000000 --- a/help/field.md +++ /dev/null @@ -1,78 +0,0 @@ -[comment]: # (Field class help) -[version]: # (0.5) - -# Field -[tagfield]: # (Field) - -Fields are used to store information, including numbers or matrices, associated with the elements of a `Mesh` object. - -You can create a `Field` by applying a function to each of the vertices, - - var f = Field(mesh, fn (x, y, z) x+y+z) - -or by supplying a single constant value, - - var f = Field(mesh, Matrix([1,0,0])) - -Fields can then be added and subtracted using the `+` and `-` operators. - -To access elements of a `Field`, use index notation: - - print f[grade, element, index] - -where -* `grade` is the grade to select -* `element` is the element id -* `index` is the element index - -As a shorthand, it's possible to omit the grade and index; these are then both assumed to be `0`: - - print f[2] - -[showsubtopics]: # (subtopics) - -## Mesh -[tagmesh]: # (mesh) - -Returns the Mesh associated with a Field object: - - var f.mesh() - -## Grade -[taggrade]: # (grade) - -To create fields that include grades other than just vertices, use the `grade` option to `Field`. This can be just a grade index, - - var f = Field(mesh, 0, grade=2) - -which creates an empty field with `0` for each of the facets of the mesh `mesh`. - -You can store more than one item per element by supplying a list to the `grade` option indicating how many items you want to store on each grade. For example, - - var f = Field(mesh, 1.0, grade=[0,2,1]) - -stores two numbers on the line (grade 1) elements and one number on the facets (grade 2) elements. Each number in the field is initialized to the value `1.0`. - -## Shape -[tagshape]: # (shape) - -The `shape` method returns a list indicating the number of items stored on each element of a particular grade. This has the same format as the list you supply to the `grade` option of the `Field` constructor. For example, - - [1,0,2] - -would indicate one item stored on each vertex and two items stored on each facet. - -## Op -[tagop]: # (op) - -The `op` method applies a function to every item stored in a `Field`, returning the result as elements of a new `Field` object. For example, - - f.op(fn (x) x.norm()) - -calls the `norm` method on each element stored in `f`. - -Additional `Field` objects may be supplied as extra arguments to `op`. These must have the same shape (the same number of items stored on each grade). The function supplied to `op` will now be called with the corresponding element from each field as arguments. For example, - - f.op(fn (x,y) x.inner(y), g) - -calculates an elementwise inner product between the elements of Fields `f` and `g`. diff --git a/help/file.md b/help/file.md deleted file mode 100644 index fc8c8f8..0000000 --- a/help/file.md +++ /dev/null @@ -1,96 +0,0 @@ -[comment]: # (File class help) -[version]: # (0.5) - -# File -[tagfile]: # (File) - -The `File` class provides the capability to read from and write to files, or to obtain the contents of a file in convenient formats. - -To open a file, create a File object with the filename as the argument - - var f = File("myfile.txt") - -which opens `"myfile.txt"` for *reading*. To open a file for writing or appending, you need to provide a mode selector - - var g = File("myfile.txt", "write") - -or - - var g = File("myfile.txt", "append") - -Once the file is open, you can then read or write by calling appropriate methods: - - f.lines() // reads the contents of the file into an array of lines. - f.readline() // reads a single line - f.readchar() // reads a single character. - f.write(string) // writes the arguments to the file. - -After you're done with the file, close it with - - f.close() - -[show]: # (subtopics) - -## lines -[taglines]: # (lines) - -Returns the contents of a file as an array of strings; each element corresponds to a single line. - -Read in the contents of a file and print line by line: - - var f = File("input.txt") - var s = f.lines() - for (i in s) print i - f.close() - -## readline -[tagreadline]: # (readline) - -Reads a single line from a file; returns the result as a string. - -Read in the contents of a file and print each line: - - var f = File("input.txt") - while (!f.eof()) { - print f.readline() - } - f.close() - -## readchar -[tagreadchar]: # (readchar) - -Reads a single character from a file; returns the result as a string. - -## write -[tagwrite]: # (write) - -Writes to a file. - -Write the contents of a list to a file: - - var f = File("output.txt", "w") - for (k, i in list) f.write("${i}: ${k}") - f.close() - -## close -[tagclose]: # (close) - -Closes an open file. - -## eof -[tageof]: # (eof) - -Returns true if at the end of the file; false otherwise - -# Folder -[tagfolder]: # (Folder) - -The `Folder` class enables you to find whether a filepath refers to a folder, and find the contents of that folder. - -Find whether a path refers to a folder: - - print Folder.isfolder("path/folder") - -Get a list of a folder's contents: - - print Folder.contents("path/folder") diff --git a/help/functionals.md b/help/functionals.md deleted file mode 100644 index c883325..0000000 --- a/help/functionals.md +++ /dev/null @@ -1,251 +0,0 @@ -[comment]: # (Functionals help) -[version]: # (0.5) - -# Functionals -[tagfunctionals]: # (functionals) - -A number of `functionals` are available in Morpho. Each of these represents an integral over some `Mesh` and `Field` objects (on a particular `Selection`) and are used to define energies and constraints in an `OptimizationProblem` provided by the `optimize` module. - -Many functionals are built in. Additional functionals are available by importing the `functionals` module: - - import functionals - -Functionals provide a number of standard methods: - -* `total`(mesh) - returns the value of the integral with a provided mesh, selection and fields -* `integrand`(mesh) - returns the contribution to the integral from each element -* `gradient`(mesh) - returns the gradient of the functional with respect to vertex motions. -* `fieldgradient`(mesh, field) - returns the gradient of the functional with respect to components of the field - -Each of these may be called with a mesh, a field and a selection. - -[showsubtopics]: # (subtopics) - -## Length -[taglength]: # (length) - -A `Length` functional calculates the length of a line element in a mesh. - -Evaluate the length of a circular loop: - - import constants - import meshtools - var m = LineMesh(fn (t) [cos(t), sin(t), 0], 0...2*Pi:Pi/20, closed=true) - var le = Length() - print le.total(m) - -## AreaEnclosed -[tagareaenclosed]: # (areaenclosed) - -An `AreaEnclosed` functional calculates the area enclosed by a loop of line elements. - - var la = AreaEnclosed() - -## Area -[tagarea]: # (area) - -An `Area` functional calculates the area of the area elements in a mesh: - - var la = Area() - print la.total(mesh) - -## VolumeEnclosed -[tagvolumeenclosed]: # (volumeenclosed) - -A `VolumeEnclosed` functional is used to calculate the volume enclosed by a surface. Note that this estimate may become inaccurate for highly deformed surfaces. - - var lv = VolumeEnclosed() - -## Volume -[tagvolume]: # (volume) - -A `Volume` functional calculates the volume of volume elements. - - var lv = Volume() - -## ScalarPotential -[tagscalarpotential]: # (scalarpotential) - -The `ScalarPotential` functional is applied to point elements. - - var ls = ScalarPotential(potential) - -You must supply a function (which may be anonymous) that returns the potential. You may optionally provide a function that returns the gradient as well at initialization: - - var ls = ScalarPotential(potential, gradient) - -This functional is often used to constrain the mesh to the level set of a function. For example, to confine a set of points to a sphere: - - import optimize - fn sphere(x,y,z) { return x^2+y^2+z^2-1 } - fn grad(x,y,z) { return Matrix([2*x, 2*y, 2*z]) } - var lsph = ScalarPotential(sphere, grad) - problem.addlocalconstraint(lsph) - -See the thomson example for use of this technique. - -## LinearElasticity -[taglinearelasticity]: # (linearelasticity) - -The `LinearElasticity` functional measures the linear elastic energy away from a reference state. - -You must initialize with a reference mesh: - - var le = LinearElasticity(mref) - -Manually set the poisson's ratio and grade to operate on: - - le.poissonratio = 0.2 - le.grade = 2 - -## EquiElement -[tagequielement]: # (equielement) - -The `EquiElement` functional measures the discrepency between the size of elements adjacent to each vertex. It can be used to equalize elements for regularization purposes. - -## LineCurvatureSq -[taglinecurvaturesq]: # (linecurvaturesq) - -The `LineCurvatureSq` functional measures the integrated curvature squared of a sequence of line elements. - -## LineTorsionSq -[taglinetorsionsq]: # (linetorsionsq) - -The `LineTorsionSq` functional measures the integrated torsion squared of a sequence of line elements. - -## MeanCurvatureSq -[tagmeancurvsq]: # (meancurvaturesq) - -The `MeanCurvatureSq` functional computes the integrated mean curvature over a surface. - -## GaussCurvature -[taggausscurv]: # (gausscurvature) - -The `GaussCurvature` computes the integrated gaussian curvature over a surface. - -Note that for surfaces with a boundary, the integrand is correct only for the interior points. To compute the geodesic curvature of the boundary in that case, you can set the optional flag `geodesic` to `true` and compute the total on the boundary selection. -Here is an example for a 2D disk mesh. - - var mesh = Mesh("disk.mesh") - mesh.addgrade(1) - - var whole = Selection(mesh, fn(x,y,z) true) - var bnd = Selection(mesh, boundary=true) - var interior = whole.difference(bnd) - - var gauss = GaussCurvature() - print gauss.total(mesh, selection=interior) // expect: 0 - gauss.geodesic = true - print gauss.total(mesh, selection=bnd) // expect: 2*Pi - -## GradSq -[taggradsq]: # (gradsq) - -The `GradSq` functional measures the integral of the gradient squared of a field. The field can be a scalar, vector or matrix function. - -Initialize with the required field: - - var le=GradSq(phi) - -## Nematic -[tagnematic]: # (nematic) - -The `Nematic` functional measures the elastic energy of a nematic liquid crystal. - - var lf=Nematic(nn) - -There are a number of optional parameters that can be used to set the splay, twist and bend constants: - - var lf=Nematic(nn, ksplay=1, ktwist=0.5, kbend=1.5, pitch=0.1) - -These are stored as properties of the object and can be retrieved as follows: - - print lf.ksplay - -## NematicElectric -[tagnematic]: # (nematic) - -The `NematicElectric` functional measures the integral of a nematic and electric coupling term integral((n.E)^2) where the electric field E may be computed from a scalar potential or supplied as a vector. - -Initialize with a director field `nn` and a scalar potential `phi`: - var lne = NematicElectric(nn, phi) - -## NormSq -[tagnormsq]: # (normsq) - -The `NormSq` functional measures the elementwise L2 norm squared of a field. - -## LineIntegral -[taglineintegral]: # (lineintegral) - -The `LineIntegral` functional computes the line integral of a function. You supply an integrand function that takes a position matrix as an argument. - -To compute `integral(x^2+y^2)` over a line element: - - var la=LineIntegral(fn (x) x[0]^2+x[1]^2) - -The function `tangent()` returns a unit vector tangent to the current element: - - var la=LineIntegral(fn (x) x.inner(tangent())) - -You can also integrate functions that involve fields: - - var la=LineIntegral(fn (x, n) n.inner(tangent()), n) - -where `n` is a vector field. The local interpolated value of this field is passed to your integrand function. More than one field can be used; they are passed as arguments to the integrand function in the order you supply them to `LineIntegral`. - -## AreaIntegral -[tagareaintegral]: # (areaintegral) - -The `AreaIntegral` functional computes the area integral of a function. You supply an integrand function that takes a position matrix as an argument. - -To compute integral(x*y) over an area element: - - var la=AreaIntegral(fn (x) x[0]*x[1]) - -You can also integrate functions that involve fields: - - var la=AreaIntegral(fn (x, phi) phi^2, phi) - -More than one field can be used; they are passed as arguments to the integrand function in the order you supply them to `AreaIntegral`. - -## VolumeIntegral -[tagvolumeintegral]: # (volumeintegral) - -The `VolumeIntegral` functional computes the volume integral of a function. You supply an integrand function that takes a position matrix as an argument. - -To compute integral(x*y*z) over an volume element: - - var la=VolumeIntegral(fn (x) x[0]*x[1]*x[2]) - -You can also integrate functions that involve fields: - - var la=VolumeIntegral(fn (x, phi) phi^2, phi) - -More than one field can be used; they are passed as arguments to the integrand function in the order you supply them to `VolumeIntegral`. - -## Hydrogel -[taghydrogel]: # (hydrogel) - -The `Hydrogel` functional computes the Flory-Rehner energy over an element: - - (a*phi*log(phi) + b*(1-phi)+log(1-phi) + c*phi*(1-phi))*V + - d*(log(phiref/phi)/3 - (phiref/phi)^(2/3) + 1)*V0 - -The first three terms come from the Flory-Huggins mixing energy, whereas -the fourth term proportional to d comes from the Flory-Rehner elastic -energy. - -The value of phi is calculated from a reference mesh -that you provide on initializing the Functional: - - var lfh = Hydrogel(mref) - -Here, a, b, c, d and phiref are parameters you can supply (they are `nil` -by default), V is the current volume and V0 is the reference volume of a -given element. You also need to supply the initial value of phi, labeled -as phi0, which is assumed to be the same for all the elements. -Manually set the coefficients and grade to operate on: - - lfh.a = 1; lfh.b = 1; lfh.c = 1; lfh.d = 1; - lfh.grade = 2, lfh.phi0 = 0.5, lfh.phiref = 0.1 diff --git a/help/functions.md b/help/functions.md deleted file mode 100644 index 75469c3..0000000 --- a/help/functions.md +++ /dev/null @@ -1,93 +0,0 @@ -[comment]: # (Morpho functions help file) -[version]: # (0.5) - -[toplevel]: # - -# Functions -[tagfn]: # (fn) -[tagfun]: # (fun) -[tagfunction]: # (function) - -A function in morpho is defined with the `fn` keyword, followed by the function's name, a list of parameters enclosed in parentheses, and the body of the function in curly braces. This example computes the square of a number: - - fn sqr(x) { - return x*x - } - -Once a function has been defined you can evaluate it like any other morpho function. - - print sqr(2) - -[show]: # (subtopics) - -## Variadic -[tagvariadic]: # (variadic) - -As well as regular parameters, functions can also be defined with *variadic* parameters: - - fn func(x, ...v) { - for (a in v) print a - } - -This function can then be called with 1 or more arguments: - - func(1) - func(1, 2) - func(1, 2, 3) // All valid! - -The variadic parameter `v` captures all the extra arguments supplied. Functions cannot be defined with more than one variadic parameter. - -You can mix regular, variadic and optional parameters. Variadic parameters come before optional parameters: - - fn func(x, ...v, optional=true) { - // - } - -## Optional -[tagoptional]: # (optional) - -Functions can also be defined with *optional* parameters: - - fn func(a=1) { - print a - } - -Each optional parameter must be defined with a default value (here `1`). The function can then be called either with or without the optional parameter: - - func() // a == 1 due to default value - func(a=2) // a == 2 supplied by the user - -## Return -[tagreturn]: # (return) - -The `return` keyword is used to exit from a function, optionally passing a given value back to the caller. `return` can be used anywhere within a function. The below example calculates the `n` th Fibonacci number, - - fn fib(n) { - if (n<2) return n - return fib(n-1) + fib(n-2) - } - -by returning early if `n<2`, otherwise returning the result by recursively calling itself. - -# Closures -[tagclosures]: # (closures) -[tagclosure]: # (closure) - -Functions in morpho can form *closures*, i.e. they can enclose information from their local context. In this example, - - fn foo(a) { - fn g() { return a } - return g - } - -the function `foo` returns a function that captures the value of `a`. If we now try calling `foo` and then calling the returned functions, - - var p=foo(1), q=foo(2) - print p() // expect: 1 - print q() // expect: 2 - -we can see that `p` and `q` seem to contain different copies of `g` that encapsulate the value that `foo` was called with. - -Morpho hints that a returned function is actually a closure by displaying it with double brackets: - - print foo(1) // expect: <> diff --git a/help/graphics.md b/help/graphics.md deleted file mode 100644 index f5627d8..0000000 --- a/help/graphics.md +++ /dev/null @@ -1,196 +0,0 @@ -[comment]: # (Graphics module help) -[version]: # (0.5) - -# Graphics -[taggraphics]: # (graphics) - -The `graphics` module provides a number of classes to provide simple visualization capabilities. To use it, you first need to import the module: - - import graphics - -The `Graphics` class acts as an abstract container for graphical information; to actually launch the display see the `Show` class. You can create an empty scene like this, - - var g = Graphics() - -Additional elements can be added using the `display` method. - - g.display(element) - -Morpho provides the following fundamental Graphical element classes: - - TriangleComplex - -You can also use functions like `Arrow`, `Tube` and `Cylinder` to create these elements conveniently. - -To combine graphics objects, use the add operator: - - var g1 = Graphics(), g2 = Graphics() - // ... - Show(g1+g2) - -[show]: # (subtopics) - -## Show -[tagshow]: # (Show) - -`Show` is used to launch an interactive graphical display using the external `morphoview` application. `Show` takes a `Graphics` object as an argument: - - var g = Graphics() - Show(g) - -## TriangleComplex -[tagTriangleComplex]: # (TriangleComplex) - -A `TriangleComplex` is a graphical element that can be used as part of a graphical display. It consists of a list of vertices and a connectivity matrix that selects which vertices are used in each triangle. - -To create one, call the constructor with the following arguments: - - TriangleComplex(position, normals, colors, connectivity) - -* `position` is a `Matrix` containing vertex positions as *columns*. -* `normals` is a `Matrix` with a normal for each vertex. -* `colors` is the color of the object. -* `connectivity` is a `Sparse` matrix where each column represents a triangle and rows correspond to vertices. - -You can also provide optional arguments: - -* `transmit` sets the transparency of the object. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the object using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - - -Add to a `Graphics` object using the `display` method. - -## Arrow -[tagArrow]: # (Arrow) - -The `Arrow` function creates an arrow. It takes two arguments: - - arrow(start, end) - -* `start` and `end` are the two vertices. The arrow points `start` -> `end`. - -You can also provide optional arguments: - -* `aspectratio` controls the width of the arrow relative to its length -* `n` is an integer that controls the quality of the display. Higher `n` leads to a rounder arrow. -* `color` is the color of the arrow. This can be a list of RGB values or a `Color` object -* `transmit` sets the transparency of the arrow. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the arrow using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Display an arrow: - - var g = Graphics([]) - g.display(Arrow([-1/2,-1/2,-1/2], [1/2,1/2,1/2], aspectratio=0.05, n=10)) - Show(g) - -## Cylinder -[tagCylinder]: # (Cylinder) - -The `Cylinder` function creates a cylinder. It takes two required arguments: - - cylinder(start, end) - -* `start` and `end` are the two vertices. - -You can also provide optional arguments: - -* `aspectratio` controls the width of the cylinder relative to its length. -* `n` is an integer that controls the quality of the display. Higher `n` leads to a rounder cylinder. -* `color` is the color of the cylinder. This can be a list of RGB values or a `Color` object. -* `transmit` sets the transparency of the cylinder. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the cylinder using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Display an cylinder: - - var g = Graphics() - g.display(Cylinder([-1/2,-1/2,-1/2], [1/2,1/2,1/2], aspectratio=0.1, n=10)) - Show(g) - -## Tube -[tagTube]: # (Tube) - -The `Tube` function connects a sequence of points to form a tube. - - Tube(points, radius) - -* `points` is a list of points; this can be a list of lists or a `Matrix` with the positions as columns. -* `radius` is the radius of the tube. - -You can also provide optional arguments: - -* `n` is an integer that controls the quality of the display. Higher `n` leads to a rounder tube. -* `color` is the color of the tube. This can be a list of RGB values or a `Color` object. -* `closed` is a `bool` that indicates whether the tube should be closed to form a loop. -* `transmit` sets the transparency of the tube. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the tube using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Draw a square: - - var a = Tube([[-1/2,-1/2,0],[1/2,-1/2,0],[1/2,1/2,0],[-1/2,1/2,0]], 0.1, closed=true) - var g = Graphics() - g.display(a) - -## Sphere -[tagSphere]: # (Sphere) - -The `Sphere` function creates a sphere. - - Sphere(center, radius) - -* `center` is the position of the center of the sphere; this can be a list or column `Matrix`. -* `radius` is the radius of the sphere - -You can also provide optional arguments: - -* `color` is the color of the sphere. This can be a list of RGB values or a `Color` object. -* `transmit` sets the transparency of the sphere. This parameter is only -used by the povray module as of now. Default is 0. -* `filter` sets the transparency of the sphere using a filter effect. -This parameter is only used by the povray module as of now. Default is 0. For the difference between `transmit` and `filter`, checkout the -[POVRay documentation](http://xahlee.info/3d/povray-glassy.html). - -Draw some randomly sized spheres: - - var g = Graphics() - for (i in 0...10) { - g.display(Sphere([random()-1/2, random()-1/2, random()-1/2], 0.1*(1+random()), color=Gray(random()))) - } - Show(g) - -## Text -[tagText]: # (Text) - -A `Text` object is used to display text. - - Text(text, position) - -* `text` is the text to display as a string. -* `position` is the position at which to display the text. - -You can also provide optional arguments: - -* `color` is the color of the text. This should be a `Color` object. -* `dirn` is the direction along which the text is drawn. This should be a `List` or a `Matrix`. -* `size` is the font size to use -* `vertical` is the vertical direction for the text -* `font` is the `Font` object to use. - -Draw several pieces of text around the y axis: - - var g = Graphics() - for (phi in 0..Pi:Pi/8) { - g.display(Text("Hello World", [0,0,0], size=72, dirn=[0,1,0], vertical=[cos(phi),0,sin(phi)])) - } - Show(g) diff --git a/help/help.md b/help/help.md deleted file mode 100644 index 8b52966..0000000 --- a/help/help.md +++ /dev/null @@ -1,34 +0,0 @@ -[comment]: # (Morpho language help file) -[version]: # (0.5) - -# Help -[tag]: # (help) - -Morpho provides an online help system. To get help about a topic called `topicname`, type - - help topicname - -A list of available topics is provided below and includes language keywords like `class`, `fn` and `for`, built in classes like `Matrix` and `File` or information about functions like `exp` and `random`. - -Some topics have additional subtopics: to access these type - - help topic subtopic - -For example, to get help on a method for a particular class, you could type - - help Classname.methodname - -Note that `help` ignores all punctuation. - -You can also use `?` as a shorthand synonym for `help` - - ? topic - -A useful feature is that, if an error occurs, simply type `help` to get more information about the error. - -[showtopics]: # (topics) - -# Quit -[tagquit]: # (quit) - -The `quit` CLI command quits `morpho` run in interactive mode and returns to the shell. diff --git a/help/implicitmesh.md b/help/implicitmesh.md deleted file mode 100644 index b897440..0000000 --- a/help/implicitmesh.md +++ /dev/null @@ -1,27 +0,0 @@ -[comment]: # (Implicitmesh module help) -[version]: # (0.5) - -# ImplicitMesh -[tagimplicitmesh]: # (implicitmesh) - -The `implicitmesh` module allows you to build meshes from implicit functions. For example, the unit sphere could be specified using the function `x^2+y^2+z^2-1 == 0`. - -To use the module, first import it: - - import implicitmesh - -To create a sphere, first create an ImplicitMeshBuilder object with the implict function you'd like to use: - - var impl = ImplicitMeshBuilder(fn (x,y,z) x^2+y^2+z^2-1) - -You can use an existing function (or method) as well as an anonymous function as above. - -Then build the mesh, - - var mesh = impl.build(stepsize=0.25) - -The `build` method takes a number of optional arguments: - -* `start` - the starting point. If not provided, the value Matrix([1,1,1]) is used. -* `stepsize` - approximate lengthscale to use. -* `maxiterations` - maximum number of iterations to use. If this limit is exceeded, a partially built mesh will be returned. diff --git a/help/index.rst b/help/index.rst deleted file mode 100644 index 147b74c..0000000 --- a/help/index.rst +++ /dev/null @@ -1,69 +0,0 @@ -Morpho -====== - -.. toctree:: - :caption: Language - :maxdepth: 1 - - syntax - values - variables - controlflow - functions - classes - modules - help - builtin - -.. toctree:: - :caption: Data Types - :maxdepth: 1 - - array - complex - dictionary - list - matrix - range - sparse - string - -.. toctree:: - :caption: Computational Geometry - :maxdepth: 1 - - field - functionals - mesh - selection - -.. toctree:: - :caption: I/O - :maxdepth: 1 - - file - system - -.. toctree:: - :caption: Modules - :maxdepth: 1 - - color - constants - delaunay - graphics - implicitmesh - kdtree - meshgen - meshslice - meshtools - optimize - plot - povray - vtk - -.. toctree:: - :caption: Error messages - :maxdepth: 1 - - errors diff --git a/help/kdtree.md b/help/kdtree.md deleted file mode 100644 index b44fffc..0000000 --- a/help/kdtree.md +++ /dev/null @@ -1,81 +0,0 @@ -[comment]: # (KDTree module help) -[version]: # (0.5) - -# KDTree -[tagkdtree]: # (kdtree) - -The `kdtree` module implements a k-dimensional tree, a space partitioning data structure that can be used to accelerate computational geometry calculations. - -To use the module, first import it: - - import kdtree - -To create a tree from a list of points: - - var pts = [] - for (i in 0...100) pts.append(Matrix([random(), random(), random()])) - var tree=KDTree(pts) - -Add further points: - - tree.insert(Matrix([0,0,0])) - -Test whether a given point is present in the tree: - - tree.ismember(Matrix([1,0,0])) - -Find all points within a given bounding box: - - var pts = tree.search([[-1,1], [-1,1], [-1,1]]) - for (x in pts) print x.location - -Find the nearest point to a given point: - - var pt = tree.nearest(Matrix([0.1, 0.1, 0.5])) - print pt.location - -[showsubtopics]: # (subtopics) - -## Insert -[taginsert]: # (insert) - -Inserts a new point into a k-d tree. Returns a KDTreeNode object. - - var node = tree.insert(Matrix([0,0,0])) - -Note that, for performance reasons, if the set of points is known ahead of time, it is generally better to build the tree using the constructor function KDTree rather than one-by-one with insert. - -## Ismember -[tagismember]: # (ismember) - -Checks if a point is a member of a k-d tree. Returns `true` or `false`. - - print tree.ismember(Matrix([0,0,0])) - -## Nearest -[tagnearest]: # (nearest) - -Finds the point in a k-d tree nearest to a point of interest. Returns a KDTreeNode object. - - var pt = tree.nearest(Matrix([0.1, 0.1, 0.5])) - -To get the location of this nearest point, access the location property: - - print pt.location - -## Search -[tagsearch]: # (search) - -Finds all points in a k-d tree that lie within a cuboidal bounding box. Returns a list of KDTreeNode objects. - -Find and display all points that lie in a cuboid 0<=x<=1, 0<=y<=2, 1<=z<=2: - - var result = tree.search([[0,1], [0,2], [1,2]]) - for (x in result) print x.location - -## KDTreeNode -[tagkdtreenode]: # (kdtreenode) - -An object corresponding to a single node in a k-d tree. To get the location of the node, access the `location` property: - - print node.location diff --git a/help/list.md b/help/list.md deleted file mode 100644 index d996d27..0000000 --- a/help/list.md +++ /dev/null @@ -1,132 +0,0 @@ -[comment]: # (List class help) -[version]: # (0.5) - -# List -[taglist]: # (List) - -Lists are collection objects that contain a sequence of values each associated with an integer index. - -Create a list like this: - - var list = [1, 2, 3] - -Look up values using index notation: - - list[0] - -Indexing can also be done with slices: - list[0..2] - list[[0,1,3]] - -You can change list entries like this: - - list[0] = "Hello" - -Create an empty list: - - var list = [] - -Loop over elements of a list: - - for (i in list) print i - -[showsubtopics]: # (subtopics) - -## Append -[tagappend]: # (Append) - -Adds an element to the end of a list: - - var list = [] - list.append("Foo") - -## Insert -[taginsert]: # (Insert) - -Inserts an element into a list at a specified index: - - var list = [1,2,3] - list.insert(1, "Foo") - print list // prints [ 1, Foo, 2, 3 ] - -## Pop -[tagpop]: # (pop) - -Remove the last element from a list, returning the element removed: - - print list.pop() - -If an integer argument is supplied, returns and removes that element: - - var a = [1,2,3] - print a.pop(1) // prints '2' - print a // prints [ 1, 3 ] - -## Sort -[tagsort]: # (sort) - -Sorts the contents of a list into ascending order: - - list.sort() - -Note that this sorts the list "in place" (i.e. it modifies the order of the list on which it is invoked) and hence returns `nil`. - -You can provide your own function to use to compare values in the list - - list.sort(fn (a, b) a-b) - -This function should return a negative value if `ab` and `0` if `a` and `b` are equal. - -## Order -[tagorder]: # (order) - -Returns a list of indices that would, if used in order, would sort a list. For example - - var list = [2,3,1] - print list.order() // expect: [2,0,1] - -would produce `[2,0,1]` - -## Remove -[tagremove]: # (remove) - -Remove any occurrences of a value from a list: - - var list = [1,2,3] - list.remove(1) - -## ismember -[tagismember]: # (ismember) - -Tests if a value is a member of a list: - - var list = [1,2,3] - print list.ismember(1) // expect: true - -## Add -[tagadd]: # (add) - -Join two lists together: - - var l1 = [1,2,3], l2 = [4, 5, 6] - print l1+l2 // expect: [1,2,3,4,5,6] - -## Tuples -[tagtuples]: # (tuples) - -Generate all possible 2-tuples from a list: - - var t = [ 1, 2, 3].tuples(2) - -produces `[ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ] ... ]`. - -## Sets -[tagsets]: # (sets) - -Generate all possible sets of order 2 from a list. - - var t = [ 1, 2, 3 ].sets(2) - -produces `[ [ 1, 2 ], [ 1, 3 ], [ 2, 3 ] ]`. - -Note that sets include only distinct elements from the list (no element is repeated) and ordering is unimportant, hence only one of `[ 1, 2 ]` and `[ 2, 1 ]` is returned. diff --git a/help/make.bat b/help/make.bat deleted file mode 100644 index 922152e..0000000 --- a/help/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/help/matrix.md b/help/matrix.md deleted file mode 100644 index c830b45..0000000 --- a/help/matrix.md +++ /dev/null @@ -1,175 +0,0 @@ -[comment]: # (Matrix class help) -[version]: # (0.5) - -# Matrix -[tagmatrix]: # (Matrix) - -The Matrix class provides support for matrices. A matrix can be initialized with a given size, - - var a = Matrix(nrows,ncols) - -where all elements are initially set to zero. Alternatively, a matrix can be created from an array, - - var a = Matrix([[1,2], [3,4]]) - -or a Sparse matrix, - - var a = Sparse([[0,0,1],[1,1,1],[2,2,1]]) - var b = Matrix(a) - -You can create a column vector like this, - - var v = Matrix([1,2]) - -Finally, you can create a Matrix by assembling other matrices like this, - - var a = Matrix([[0,1],[1,0]]) - var b = Matrix([[a,0],[0,a]]) // produces a 4x4 matrix - -Once a matrix is created, you can use all the regular arithmetic operators with matrix operands, e.g. - - a+b - a*b - -The division operator is used to solve a linear system, e.g. - - var a = Matrix([[1,2],[3,4]]) - var b = Matrix([1,2]) - - print b/a - -yields the solution to the system a*x = b. - -[showsubtopics]: # (subtopics) - -## Assign -[tagassign]: # (Assign) - -Copies the contents of matrix B into matrix A: - - A.assign(B) - -The two matrices must have the same dimensions. - -## Dimensions -[tagdimensions]: # (Dimensions) - -Returns the dimensions of a matrix: - - var A = Matrix([1,2,3]) // Create a column matrix - print A.dimensions() // Expect: [ 3, 1 ] - -## Eigenvalues -[tageigenvalues]: # (Eigenvalues) - -Returns a list of eigenvalues of a Matrix: - - var A = Matrix([[0,1],[1,0]]) - print A.eigenvalues() // Expect: [1,-1] - -## Eigensystem -[tageigensystem]: # (Eigensystem) - -Returns the eigenvalues and eigenvectors of a Matrix: - - var A = Matrix([[0,1],[1,0]]) - print A.eigensystem() - -Eigensystem returns a two element list: The first element is a List of eigenvalues. The second element is a Matrix containing the corresponding eigenvectors as its columns: - - print A.eigensystem()[0] - // [ 1, -1 ] - print A.eigensystem()[1] - // [ 0.707107 -0.707107 ] - // [ 0.707107 0.707107 ] - -## Inner -[taginner]: # (Inner) - -Computes the Frobenius inner product between two matrices: - - var prod = A.inner(B) - -## Outer -[tagouter]: # (Outer) - -Computes the outer produce between two vectors: - - var prod = A.outer(B) - -Note that `outer` always treats both vectors as column vectors. - -## Inverse -[taginverse]: # (Inverse) - -Returns the inverse of a matrix if it is invertible. Raises a -`MtrxSnglr` error if the matrix is singular. E.g. - - var m = Matrix([[1,2],[3,4]]) - var mi = m.inverse() - -yields the inverse of the matrix `m`, such that mi*m is the identity -matrix. - -## Norm -[tagnorm]: # (Norm) - -Returns a matrix norm. By default the L2 norm is returned: - - var a = Matrix([1,2,3,4]) - print a.norm() // Expect: sqrt(30) = 5.47723... - -You can select a different norm by supplying an argument: - - import constants - print a.norm(1) // Expect: 10 (L1 norm is sum of absolute values) - print a.norm(3) // Expect: 4.64159 (An unusual choice of norm) - print a.norm(Inf) // Expect: 4 (Inf-norm corresponds to maximum absolute value) - -## Reshape -[tagreshape]: # (Reshape) - -Changes the dimensions of a matrix such that the total number of elements remains constant: - - var A = Matrix([[1,3],[2,4]]) - A.reshape(1,4) // 1 row, 4 columns - print A // Expect: [ 1, 2, 3, 4 ] - -Note that elements are stored in column major-order. - -## Sum -[tagsum]: # (Sum) - -Returns the sum of all entries in a matrix: - - var sum = A.sum() - -## Transpose -[tagtranspose]: # (Transpose) - -Returns the transpose of a matrix: - - var At = A.transpose() - -## Trace -[tagtrace]: # (Trace) - -Computes the trace (the sum of the diagonal elements) of a square matrix: - - var tr = A.trace() - -## Roll -[tagroll]: # (Roll) - -Rotates values in a Matrix about a given axis by a given shift: - - var r = A.roll(shift, axis) - -Elements that roll beyond the last position are re-introduced at the first. - -## IdentityMatrix -[tagidentitymatrix]: # (IdentityMatrix) - -Constructs an identity matrix of a specified size: - - var a = IdentityMatrix(size) diff --git a/help/mesh.md b/help/mesh.md deleted file mode 100644 index 49a2c7c..0000000 --- a/help/mesh.md +++ /dev/null @@ -1,66 +0,0 @@ -[comment]: # (Mesh class help) -[version]: # (0.5) - -# Mesh -[tagmesh]: # (Mesh) - -The `Mesh` class provides support for meshes. Meshes may consist of different kinds of element, including vertices, line elements, facets or area elements, tetrahedra or volume elements. - -To create a mesh, you can import it from a file: - - var m = Mesh("sphere.mesh") - -or use one of the functions available in `meshtools` or `implicitmesh` packages. - -Each type of element is referred to as belonging to a different `grade`. Point-like elements (vertices) are *grade 0*; line-like elements (edges) are *grade 1*; area-like elements (facets; triangles) are *grade 2* etc. - -The `plot` package includes functions to visualize meshes. - -[showsubtopics]: # (showsubtopics) - -## Save -[tagsave]: # (Save) - -Saves a mesh as a .mesh file. - - m.save("new.mesh") - -## Vertexposition -[tagvertexposition]: # (vertexposition) - -Retrieves the position of a vertex given an id: - - print m.vertexposition(id) - -## Setvertexposition -[tagsetvertexposition]: # (setvertexposition) - -Sets the position of a vertex given an id and a position vector: - - print m.setvertexposition(1, Matrix([0,0,0])) - -## Addgrade -[tagaddgrade]: # (addgrade) - -Adds a new grade to a mesh. This is commonly used when, for example, a mesh file includes facets but not edges. To add the missing edges: - - m.addgrade(1) - -## Addsymmetry -[tagaddsymmetry]: # (addsymmetry) - -Adds a symmetry to a mesh. Experimental in version 0.5. - -## Maxgrade -[tagmaxgrade]: # (maxgrade) - -Returns the highest grade element present: - - print m.maxgrade() - -## Count -[tagcount]: # (count) - -Counts the number of elements. If no argument is provided, returns the number of vertices. Otherwise, returns the number of elements present of a given grade: - - print m.count(2) // Returns the number of area-like elements. diff --git a/help/meshgen.md b/help/meshgen.md deleted file mode 100644 index c748c93..0000000 --- a/help/meshgen.md +++ /dev/null @@ -1,113 +0,0 @@ -[comment]: # (Meshgen module help) -[version]: # (0.5) - -# Meshgen -[tagmeshgen]: # (meshgen) - -The `meshgen` module is used to create `Mesh` objects corresponding to a specified domain. It provides the `MeshGen` class to perform the meshing, which are created with the following arguments: - - MeshGen(domain, boundingbox) - -Domains are specified by a scalar function that is positive in the region to be meshed and locally smooth. For example, to mesh the unit disk: - - var dom = fn (x) -(x[0]^2+x[1]^2-1) - -A `MeshGen` object is then created and then used to build the `Mesh` like this: - - var mg = MeshGen(dom, [-1..1:0.2, -1..1:0.2]) - var m = mg.build() - -A bounding box for the mesh must be specified as a `List` of `Range` objects, one for each dimension. The increment on each `Range` gives an approximate scale for the size of elements generated. - -To facilitate convenient creation of domains, a `Domain` class is provided that provides set operations `union`, `intersection` and `difference`. - -`MeshGen` accepts a number of optional arguments: - -* `weight` A scalar weight function that controls mesh density. -* `quiet` Set to `true` to suppress `MeshGen` output. -* `method` a list of options that controls the method used. - -Some method choices that are available include: - -* `"FixedStepSize"` Use a fixed step size in optimization. -* `"StartGrid"` Start from a regular grid of points (the default). -* `"StartRandom"` Start from a randomly generated collection of points. - -There are also a number of properties of a `MeshGen` object that can be set prior to calling `build` to control the operation of the mesh generation: - -* `stepsize`, `steplimit` Stepsize used internally by the `Optimizer` -* `fscale` an internal "pressure" -* `ttol` how far the vertices are allowed to move before retriangulation -* `etol` energy tolerance for optimization problem -* `maxiterations` Maximum number of iterations of minimization + - retriangulation (default is 100) - -`MeshGen` picks default values that cover a reasonable range of uses. - -[showsubtopics]: # (subtopics) - -## Domain -[tagdomain]: # (domain) - -The `Domain` class is used to conveniently build a domain by composing simpler elements. - -Create a `Domain` from a scalar function that is positive in the region of interest: - - var dom = Domain(fn (x) -(x[0]^2+x[1]^2-1)) - -You can pass it to `MeshGen` to specify the region to mesh: - - var mg = MeshGen(dom, [-1..1:0.2, -1..1:0.2]) - -You can combine `Domain` objects using set operations `union`, `intersection` and `difference`: - - var a = CircularDomain(Matrix([-0.5,0]), 1) - var b = CircularDomain(Matrix([0.5,0]), 1) - var c = CircularDomain(Matrix([0,0]), 0.3) - var dom = a.union(b).difference(c) - -## CircularDomain -[tagcirculardomain]: # (circulardomain) - -Conveniently constructs a `Domain` object correspondiong to a disk. Requires the position of the center and a radius as arguments. - -Create a domain corresponding to the unit disk: - - var c = CircularDomain([0,0], 1) - -## RectangularDomain -[tagrectangulardomain]: # (rectangulardomain) - -Conveniently constructs a `Domain` object corresponding to a rectangle. Requires a list of ranges as arguments. Works in arbitrary dimensions - -Create a square `Domain`: - - var c = RectangularDomain([-1..1, -1..1]) - -## HalfSpaceDomain -[halfspacedomain]: # (halfspacedomain) - -Conveniently constructs a `Domain` object correspondiong to a half space defined by a plane at `x0` and a normal `n`: - - var hs = HalfSpaceDomain(x0, n) - -Note `n` is an "outward" normal, so points into the *excluded* region. - -Half space corresponding to the allowed region `x<0`: - - var hs = HalfSpaceDomain(Matrix([0,0,0]), Matrix([1,0,0])) - -Note that `HalfSpaceDomain`s cannot be meshed directly as they correspond to an infinite region. They are useful, however, for combining with other domains. - -Create half a disk by cutting a `HalfSpaceDomain` from a `CircularDomain`: - - var c = CircularDomain([0,0], 1) - var hs = HalfSpaceDomain(Matrix([0,0]), Matrix([-1,0])) - var dom = c.difference(hs) - var mg = MeshGen(dom, [-1..1:0.2, -1..1:0.2], quiet=false) - var m = mg.build() - -## MshGnDim -[mshgndim]: # (mshgndim) - -The `MeshGen` module currently supports 2 and 3 dimensional meshes. Higher dimensional meshing will be available in a future release; please contact the developer if you are interested in this functionality. \ No newline at end of file diff --git a/help/meshslice.md b/help/meshslice.md deleted file mode 100644 index f698141..0000000 --- a/help/meshslice.md +++ /dev/null @@ -1,43 +0,0 @@ -[comment]: # (Meshslice module help) -[version]: # (0.5) - -# Meshslice -[tagmeshslice]: # (meshslice) - -The `meshslice` module is used to slice a `Mesh` object along a given plane, yielding a new `Mesh` object of lower dimensionality. You can also use `meshslice` to project `Field` objects onto the new mesh. - -To use the module, begin by importing it: - - import meshslice - -Then construct a `MeshSlicer` object, passing the mesh you want to slice in the constructor: - - var slice = MeshSlicer(mesh) - -You then perform a slice by calling the `slice` method, passing the plane you want to slice through. This method returns a new `Mesh` object comprising the slice. A plane is defined by a point that lies on the plane `pt` and a direction normal to the plan `dirn`: - - var slc = slice.slice(pt, dirn) - -Having performed a slice, you can then project any associated `Field` objects onto the sliced mesh by calling the `slicefield` method: - - var phi = Field(mesh, fn (x,y,z) x+y+z) - var sphi = slice.slicefield(phi) - -The new field returned by `slicefield` lives on the sliced mesh. You can slice any number of fields. - -You can perform multiple slices with the same `MeshSlicer` simply by calling `slice` again with a different plane. - -## SlcEmpty -[tagslcempty]: # (slcempty) - -This error occurs if you try to use `slicefield` on a `MeshSlicer` without having performed a slice. For example: - - var slice = MeshSlicer(mesh) - slice.slicefield(phi) // Throws SlcEmpty - slice.slice([0,0,0],[1,0,0]) - -To fix, call `slice` before `slicefield`: - - var slice = MeshSlicer(mesh) - slice.slice([0,0,0],[1,0,0]) - slice.slicefield(phi) // Now slices correctly diff --git a/help/meshtools.md b/help/meshtools.md deleted file mode 100644 index 1f55795..0000000 --- a/help/meshtools.md +++ /dev/null @@ -1,229 +0,0 @@ -[comment]: # (Morpho meshtools help file) -[version]: # (0.5) - -# Meshtools -[tagmeshtools]: # (meshtools) - -The Meshtools package contains a number of functions and classes to assist with creating and manipulating meshes. - -[showsubtopics]: # (subtopics) - -## AreaMesh -[tagareamesh]: # (areamesh) - -This function creates a mesh composed of triangles from a parametric function. To use it: - - var m = AreaMesh(function, range1, range2, closed=boolean) - -where - -* `function` is a parametric function that has one parameter. It should return a list of coordinates or a column matrix corresponding to this parameter. -* `range1` is the Range to use for the first parameter of the parametric function. -* `range2` is the Range to use for the second parameter of the parametric function. -* `closed` is an optional parameter indicating whether to create a closed loop or not. You can supply a list where each element indicates whether the relevant parameter is closed or not. - -To use `AreaMesh`, import the `meshtools` module: - - import meshtools - -Create a square: - - var m = AreaMesh(fn (u,v) [u, v, 0], 0..1:0.1, 0..1:0.1) - -Create a tube: - - var m = AreaMesh(fn (u, v) [v, cos(u), sin(u)], -Pi...Pi:Pi/4, - -1..1:0.1, closed=[true, false]) - -Create a torus: - - var c=0.5, a=0.2 - var m = AreaMesh(fn (u, v) [(c + a*cos(v))*cos(u), - (c + a*cos(v))*sin(u), - a*sin(v)], 0...2*Pi:Pi/16, 0...2*Pi:Pi/8, closed=true) - -## LineMesh -[taglinemesh]: # (linemesh) - -This function creates a mesh composed of line elements from a parametric function. To use it: - - var m = LineMesh(function, range, closed=boolean) - -where - -* `function` is a parametric function that has one parameter. It should return a list of coordinates or a column matrix corresponding to this parameter. -* `range` is the Range to use for the parametric function. -* `closed` is an optional parameter indicating whether to create a closed loop or not. - -To use `LineMesh`, import the `meshtools` module: - - import meshtools - -Create a circle: - - import constants - var m = LineMesh(fn (t) [sin(t), cos(t), 0], 0...2*Pi:2*Pi/50, closed=true) - -## PolyhedronMesh -[tagpolyhedronmesh]: # (polyhedron) - -This function creates a mesh corresponding to a polyhedron. - - var m = PolyhedronMesh(vertices, faces) - -where `vertices` is a list of vertices and `faces` is a list of faces specified as a list of vertex indices. - -To use `PolyhedronMesh`, import the `meshtools` module: - - import meshtools - -Create a cube: - - var m = PolyhedronMesh([ [-0.5, -0.5, -0.5], [ 0.5, -0.5, -0.5], - [-0.5, 0.5, -0.5], [ 0.5, 0.5, -0.5], - [-0.5, -0.5, 0.5], [ 0.5, -0.5, 0.5], - [-0.5, 0.5, 0.5], [ 0.5, 0.5, 0.5]], - [ [0,1,3,2], [4,5,7,6], [0,1,5,4], - [3,2,6,7], [0,2,6,4], [1,3,7,5] ]) - -*Note* that the vertices in each face list must be specified strictly in cyclic order. - -## DelaunayMesh -[tagdelaunaymesh]: # (delaunaymesh) - -The `DelaunayMesh` constructor function creates a `Mesh` object directly from a point cloud using the Delaunay triangulator. - - var pts = [] - for (i in 0...100) pts.append(Matrix([random(), random()])) - var m=DelaunayMesh(pts) - Show(plotmesh(m)) - -You can control the output dimension of the mesh (e.g. to create a 2D mesh embedded in 3D space) using the optional `outputdim` property. - - var m = DelaunayMesh(pts, outputdim=3) - -## Equiangulate -[tagequiangulate]: # (equiangulate) - -Attempts to equiangulate a mesh, exchanging elements to improve their regularity. - - equiangulate(mesh) - -*Note* this function modifies the mesh in place; it does not create a new mesh. - -## ChangeMeshDimension -[tagchangemeshdimension]: # (changemeshdimension) - -Changes the dimension in which a mesh is embedded. For example, you may have created a mesh in 2D that you now wish to use in 3D. - -To use: - - var new = ChangeMeshDimension(mesh, dim) - -where `mesh` is the mesh you wish to change, and `dim` is the new embedding dimension. - -## MeshBuilder -[tagmeshbuiler]: # (meshbuilder) - -The `MeshBuilder` class simplifies user creation of meshes. To use this class, begin by creating a `MeshBuilder` object: - - var build = MeshBuilder() - -You can then add vertices, edges, etc. one by one using `addvertex`, `addedge`, `addface` and `addelement`. Each of these returns an element id: - - var id1=build.addvertex(Matrix([0,0,0])) - var id2=build.addvertex(Matrix([1,1,1])) - build.addedge([id1, id2]) - -Once the mesh is ready, call the `build` method to construct the `Mesh`: - - var m = build.build() - -You can specify the dimension of the `Mesh` explicitly when initializing the `MeshBuilder`: - - var mb = MeshBuilder(dimension=2) - -or implicitly when adding the first vertex: - - var mb = MeshBuilder() - mb.addvertex([0,1]) // A 2D mesh - -## MshBldDimIncnstnt -[tagmshblddimincnstnt]: # (mshblddimincnstnt) - -This error is produced if you try to add a vertex that is inconsistent with the mesh dimension, e.g. - - var mb = MeshBuilder(dimension=2) - mb.addvertex([1,0,0]) // Throws an error! - -To fix this ensure all vertices have the correct dimension. - -## MshBldDimUnknwn -[tagmshblddimunknwn]: # (mshblddimunknwn) - -This error is produced if you try to add an element to a `MeshBuilder` object but haven't yet specified the dimension (at initialization) or by adding a vertex. - - var mb = MeshBuilder() - mb.addedge([0,1]) // No vertices have been added - -To fix this add the vertices first. - -## MeshRefiner -[tagmeshrefiner]: # (meshrefiner) - -The `MeshRefiner` class is used to refine meshes, and to correct associated data structures that depend on the mesh. - -To prepare for refining, first create a `MeshRefiner` object either with a `Mesh`, - - var mr = MeshRefiner(mesh) - -or with a list of objects that can include a `Mesh` as well as `Field`s and `Selection`s. - - var mr = MeshRefiner([mesh, field, selection ... ]) - -To perform the refinement, call the `refine` method. You can refine all elements, - - var dict = mr.refine() - -or refine selected elements using a `Selection`, - - var dict = mr.refine(selection=select) - -The `refine` method returns a `Dictionary` that maps old objects to new, refined objects. Use this to update your data structures. - - var newmesh = dict[oldmesh] - -## MeshPruner -[tagmeshpruner]: # (meshpruner) - -The `MeshPruner` class is used to prune excessive detail from meshes (a process that's sometimes referred to as coarsening), and to correct associated data structures that depend on the mesh. - -First create a `MeshPruner` object either with a `Mesh`, - - var mp = MeshPruner(mesh) - -or with a list of objects that can include a `Mesh` as well as `Field`s and `Selection`s. - - var mp = MeshPruner([mesh, field, selection ... ]) - -To perform the coarsening, call the `prune` method with a `Selection`, - - var dict = mp.prune(select) - -The `prune` method returns a `Dictionary` that maps old objects to new, refined objects. Use this to update your data structures. - - var newmesh = dict[oldmesh] - -## MeshMerge -[tagmeshmerge]: # (meshmerge) -[tagmerge]: # (meshmerge) - -The `MeshMerge` class is used to combine meshes into a single mesh, removing any duplicate elements. - -To use, create a `MeshMerge` object with a list of meshes to merge, - - var mrg = MeshMerge([m1, m2, m3, ... ]) - -and then call the `merge` method to return a combined mesh: - - var newmesh = mrg.merge() diff --git a/help/modules.md b/help/modules.md deleted file mode 100644 index 85aacc0..0000000 --- a/help/modules.md +++ /dev/null @@ -1,39 +0,0 @@ -[comment]: # (Morpho modules help file) -[version]: # (0.5) - -[toplevel]: # - -# Modules -[tagmodules]: # (modules) - -Morpho is extensible and provides a convenient module system that works like standard libraries in other languages. Modules may define useful variables, functions and classes, and can be made available using the `import` keyword. For example, - - import color - -loads the `color` module that provides functionality related to color. - -You can create your own modules; they're just regular morpho files that are stored in a standard place. On UNIX platforms, this is `/usr/local/share/morpho/modules`. - -## Import -[tagimport]: # (import) -[tagas]: # (as) - -Import provides access to the module system and including code from multiple source files. - -To import code from another file, use import with the filename: - - import "file.morpho" - -which immediately includes all the contents of `"file.morpho"`. Any classes, functions or variables defined in that file can now be used, which allows you to divide your program into multiple source files. - -Morpho provides a number of built in modules--and you can write your own--which can be loaded like this: - - import color - -which imports the `color` module. - -You can selectively import symbols from a modules by using the `for` keyword: - - import color for HueMap, Red - -which imports only the `HueMap` class and the `Red` variable. diff --git a/help/optimize.md b/help/optimize.md deleted file mode 100644 index a084fc5..0000000 --- a/help/optimize.md +++ /dev/null @@ -1,83 +0,0 @@ -[comment]: # (Morpho optimize help file) -[version]: # (0.5) - -# Optimize -[tagoptimize]: # (optimize) - -The `optimize` package contains a number of functions and classes to perform shape optimization. - -[showsubtopics]: # (subtopics) - -## OptimizationProblem -[tagoptimizationproblem]: # (optimizationproblem) - -An `OptimizationProblem` object defines an optimization problem, which may include functionals to optimize as well as global and local constraints. - -Create an `OptimizationProblem` with a mesh: - - var problem = OptimizationProblem(mesh) - -Add an energy: - - var la = Area() - problem.addenergy(la) - -Add an energy that operates on a selected region, and with an optional prefactor: - - problem.addenergy(la, selection=sel, prefactor=2) - -Add a constraint: - - problem.addconstraint(la) - -Add a local constraint (here a onesided level set constraint): - - var ls = ScalarPotential(fn (x,y,z) z, fn (x,y,z) Matrix([0,0,1])) - problem.addlocalconstraint(ls, onesided=true) - -## Optimizer -[tagoptimizer]: # (optimizer) - -`Optimizer` objects are used to optimize `Mesh`es and `Field`s. You should use the appropriate subclass: `ShapeOptimizer` or `FieldOptimizer` respectively. - -[showsubtopics]: # (subtopics) - -## ShapeOptimizer -[tagshapeoptimizer]: # (shapeoptimizer) - -A `ShapeOptimizer` object performs shape optimization: it moves the vertex positions to reduce an overall energy. - -Create a `ShapeOptimizer` object with an `OptimizationProblem` and a `Mesh`: - - var sopt = ShapeOptimizer(problem, m) - -Take a step down the gradient with fixed stepsize: - - sopt.relax(5) // Takes five steps - -Linesearch down the gradient: - - sopt.linesearch(5) // Performs five linesearches - -Perform conjugate gradient (usually gives faster convergence): - - sopt.conjugategradient(5) // Performs five conjugate gradient steps. - -Control a number of properties of the optimizer: - - sopt.stepsize=0.1 // The stepsize to take - sopt.steplimit=0.5 // Maximum stepsize for optimizing methods - sopt.etol = 1e-8 // Energy convergence tolerance - sopt.ctol = 1e-9 // Tolerance to which constraints are satisfied - sopt.maxconstraintsteps = 20 // Maximum number of constraint steps to use - -## FieldOptimizer -[tagfieldoptimizer]: # (fieldoptimizer) - -A `FieldOptimizer` object performs field optimization: it changes elements of a `Field` to reduce an overall energy. - -Create a `FieldOptimizer` object with an `OptimizationProblem` and a `Field`: - - var sopt = FieldOptimizer(problem, fld) - -Field optimizers provide the same options and methods as Shape optimizers: see the `ShapeOptimizer` documentation for details. diff --git a/help/plot.md b/help/plot.md deleted file mode 100644 index f3fd386..0000000 --- a/help/plot.md +++ /dev/null @@ -1,105 +0,0 @@ -[comment]: # (Plot module help) -[version]: # (0.5.4) - -# Plot -[tagplot]: # (plot) - -The `plot` module provides visualization capabilities for Meshes, Selections and Fields. These functions produce Graphics objects that can be displayed with `Show`. - -To use the module, first import it: - - import plot - -[showsubtopics]: # (subtopics) - -## Plotmesh -[tagplotmesh]: # (plotmesh) - -Visualizes a `Mesh` object: - - var g = plotmesh(mesh) - -Plotmesh accepts a number of optional arguments to control what is displayed: - -* `selection` - Only elements in a provided `Selection` are drawn. -* `grade` - Only draw the specified grade. This can also be a list of multiple grades to draw. -* `color` - Draw the mesh in a provided `Color`. -* `filter` and `transmit` - Used by the `povray` module to indicate transparency. - -## Plotmeshlabels -[tagplotmeshlabels]: # (plotmeshlabels) - -Draws the ids for elements in a `Mesh`: - - var g = plotmeshlabels(mesh) - -Plotmeshlabels accepts a number of optional arguments to control the output: - -* `grade` - Only draw the specified grade. This can also be a list of multiple grades to draw. -* `selection` - Only labels in a provided `Selection` are drawn. -* `offset` - Local offset vector for labels. Can be a `List`, a `Matrix` or a function. -* `dirn` - Text direction for labels. Can be a `List`, a `Matrix` or a function. -* `vertical` - Text vertical direction. Can be a `List`, a `Matrix` or a function. -* `color` - Label color. Can be a `Color` object or a `Dictionary` of colors for each grade. -* `fontsize` - Font size to use. - -## Plotselection -[tagplotselection]: # (plotselection) - -Visualizes a `Selection` object: - - var g = plotselection(mesh, sel) - -Plotselection accepts a number of optional arguments to control what is displayed: - -* `grade` - Only draw the specified grade. This can also be a list of multiple grades to draw. -* `filter` and `transmit` - Used by the `povray` module to indicate transparency. - -## Plotfield -[tagplotfield]: # (plotfield) - -Visualizes a scalar `Field` object: - - var g = plotfield(field) - -Plotfield accepts a number of optional arguments to control what is displayed: - -* `grade` - Draw the specified grade. -* `colormap` - A `Colormap` object to use. The field is automatically scaled. -* `scalebar` - A `Scalebar` object to use. -* `style` - Plot style. See below. -* `filter` and `transmit` - Used by the `povray` module to indicate transparency. -* `cmin` and `cmax` - Can be used to define the data range covered. - Values beyond these limits will be colored by the lower/upper bound of - the colormap accordingly. - -Supported plot styles: - -* `default` - Color `Mesh` elements by the corresponding value of the `Field`. -* `interpolate` - Interpolate `Field` quantities onto higher elements. - -## ScaleBar -[tagscalebar]: # (scalebar) - -Represents a scalebar for a plot: - - Show(plotfield(field, scalebar=ScaleBar(posn=[1.2,0,0]))) - -`ScaleBar`s can be created with many adjustable parameters: - -* `nticks` - Maximum number of ticks to show. -* `posn` - Position to draw the `ScaleBar`. -* `length` - Length of `ScaleBar` to draw. -* `dirn` - Direction to draw the `ScaleBar` in. -* `tickdirn` - Direction to draw the ticks in. -* `colormap` - `ColorMap` to use. -* `textdirn` - Direction to draw labels in. -* `textvertical` - Label vertical direction. -* `fontsize` - Fontsize for labels -* `textcolor` - Color for labels - -You can draw the `ScaleBar` directly by calling the `draw` method: - - sb.draw(min, max) - -where `min` and `max` are the minimum and maximum values to display on the scalebar. \ No newline at end of file diff --git a/help/povray.md b/help/povray.md deleted file mode 100644 index 90176dd..0000000 --- a/help/povray.md +++ /dev/null @@ -1,80 +0,0 @@ -[comment]: # (Povray module help) -[version]: # (0.5) - -# POVRay -[tagpovray]: # (povray) - -The `povray` module provides integration with POVRay, a popular open source ray-tracing package for high quality graphical rendering. To use the module, first import it: - - import povray - -To raytrace a graphic, begin by creating a `POVRaytracer` object: - - var pov = POVRaytracer(graphic) - -Create a .pov file that can be run with POVRay: - - pov.write("out.pov") - -Create, render and display a scene using POVRay: - - pov.render("out.pov") - -This also creates the .png file for the scene. - -The `POVRaytracer` constructor supports an optional `camera` argument: - -* `camera` - a `Camera` object (see below / help) containing the - settings for the povray camera. - -The `Camera` object can be initialized as follows: - - var camera = Camera() - -This object contains the default settings of the camera, which can be -changed using the following optional arguments, or by just setting the -attributes after instantiation: - -* `antialias` - whether to antialias the output or not (`true` by default) -* `width` - image width (`2048` by default) -* `height` - image height (`1536` by default) -* `viewangle` - camera angle (higher means wider view) (`24` by default) -* `viewpoint` - position of camera (`Matrix([0,0,-5])` by default) -* `look_at` - coordinate to look at (`Matrix([0,0,0])` by defualt) -* `sky` - orientation pointing to the sky (`Matrix([0,1,0])` by default) - -The default settings generate a reasonable centered view of the x-y -plane. - -These attributes can also be set directly for the `POVRaytracer` object: - - pov.look_at = Matrix([0,0,1]) - -The `render` method supports two optional boolean arguments: - -* `quiet` - whether to suppress the parser and render statistics from `povray` or not (`false` by default) -* `display` - whether to turn on the graphic display while rendering or not (`true` by default) - -# Camera -[tagcamera]: # (camera) - -The `Camera` object can be initialized as follows: - - var camera = Camera() - -This object contains the default settings of the camera, which can be -changed using the following optional arguments, or by just setting the -attributes after instantiation: - -* `antialias` - whether to antialias the output or not (`true` by default) -* `width` - image width (`2048` by default) -* `height` - image height (`1536` by default) -* `viewangle` - camera angle (higher means wider view) (`24` by default) -* `viewpoint` - position of camera (`Matrix([0,0,-5])` by default) -* `look_at` - coordinate to look at (`Matrix([0,0,0])` by defualt) -* `sky` - orientation pointing to the sky (`Matrix([0,1,0])` by default) - - camera.sky = Matrix([0,0,1]) - -The default settings generate a reasonable centered view of the x-y -plane. diff --git a/help/range.md b/help/range.md deleted file mode 100644 index ba76f8f..0000000 --- a/help/range.md +++ /dev/null @@ -1,32 +0,0 @@ -[comment]: # (Morpho range help file) -[version]: # (0.5) - -# Range -[tagrange]: # (range) - -Ranges represent a sequence of numerical values. There are two ways to create them depending on whether the upper value is included or not: - - var a = 1..5 // inclusive version, i.e. [1,2,3,4,5] - var b = 1...5 // exclusive version, i.e. [1,2,3,4] - -By default, the increment between values is 1, but you can use a different value like this: - - var a = 1..5:0.5 // 1 - 5 with an increment of 0.5. - -You can also create Range objects using the appropriate constructor function: - - var a = Range(1,5,0.5) - -Ranges are particularly useful in writing loops: - - for (i in 1..5) print i - -They can easily be converted to a list of values: - - var c = List(1..5) - -To find the number of elements in a Range, use the `count` method - - print (1..5).count() - -[showmethodsrange]: # diff --git a/help/requirements.txt b/help/requirements.txt deleted file mode 100644 index c6f5535..0000000 --- a/help/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# File: docs/requirements.txt - -# Defining the exact version will make sure things don't break -sphinx==3.5.3 -sphinx_rtd_theme==0.5.1 -myst-parser==0.13.5 -Jinja2<3.1 diff --git a/help/selection.md b/help/selection.md deleted file mode 100644 index 6e76f78..0000000 --- a/help/selection.md +++ /dev/null @@ -1,67 +0,0 @@ -[comment]: # (Morpho selection class help file) -[version]: # (0.5) - -# Selection -[tagselection]: # (selection) - -The Selection class enables you to select components of a mesh for later use. You can supply a function that is applied to the coordinates of every vertex in the mesh, or select components like boundaries. - -Create an empty selection: - - var s = Selection(mesh) - -Select vertices above the z=0 plane using an anonymous function: - - var s = Selection(mesh, fn (x,y,z) z>0) - -Select the boundary of a mesh: - - var s = Selection(mesh, boundary=true) - -Selection objects can be composed using set operations: - - var s = s1.union(s2) - -or - var s = s1.intersection(s2) - -To add additional grades, use the addgrade method. For example, to add areas: - s.addgrade(2) - -[showsubtopics]: # subtopics - -## addgrade -[tagaddgrade]: # (addgrade) -Adds elements of the specified grade to a Selection. For example, to add edges to an existing selection, use - - s.addgrade(1) - -By default, this only adds an element if *all* vertices in the element are currently selected. Sometimes, it's useful to be able to add elements for which only some vertices are selected. The optional argument `partials` allows you to do this: - - s.addgrade(1, partials=true) - -Note that this method modifies the existing selection, and does not generate a new Selection object. - -## removegrade -[tagremovegrade]: # (removegrade) -Removes elements of the specified grade from a Selection. For example, to remove edges from an existing selection, use - - s.removegrade(1) - -Note that this method modifies the existing selection, and does not generate a new Selection object. - -## idlistforgrade -[tagidlistforgrade]: # (idlistforgrade) -Returns a list of element ids included in the selection. - -To find out which edges are selected: - - var edges = s.idlistforgrade(1) - -## isselected -[tagisselected]: # (isselected) -Checks if an element id is selected, returning `true` or `false` accordingly. - -To check if edge number 5 is selected: - - var f = s.isselected(1, 5)) diff --git a/help/sparse.md b/help/sparse.md deleted file mode 100644 index b320f81..0000000 --- a/help/sparse.md +++ /dev/null @@ -1,27 +0,0 @@ -[comment]: # (Sparse class help) -[version]: # (0.5) - -# Sparse -[tagsparse]: # (Sparse) - -The Sparse class provides support for sparse matrices. An empty sparse matrix can be initialized with a given size, - - var a = Sparse(nrows,ncols) - -Alternatively, a matrix can be created from an array of triplets, - - var a = Sparse([[row, col, value] ...]) - -For example, - - var a = Sparse([[0,0,2], [1,1,-2]]) - -creates the matrix - - [ 2 0 ] - [ 0 -2 ] - -Once a sparse matrix is created, you can use all the regular arithmetic operators with matrix operands, e.g. - - a+b - a*b diff --git a/help/string.md b/help/string.md deleted file mode 100644 index 3cc3a67..0000000 --- a/help/string.md +++ /dev/null @@ -1,58 +0,0 @@ -[comment]: # (String class help) -[version]: # (0.5) - -# String -[tagstring]: # (String) - -Strings represent textual information. They are written in Morpho like this: - - var a = "Hello world" - -Strings may incorporate any valid UTF-8 sequence, and you can insert special characters using the "\" character including: - - "\b" // backspace - "\f" // form feed - "\n" // newline - "\r" // line feed - "\t" // tab - "\xhh" // specify an ASCII character with two hex digits - "\uhhhh" // specify a unicode character with four hex digits - "\Uhhhhhhhh" // specify a unicode character with eight hex digits - -Unicode characters are converted to UTF8 by the morpho compiler. - -You can also create strings using the constructor function `String`, which takes any number of parameters: - - var a = String("Hello", "World") - -A very useful feature, called *string interpolation*, enables the results of any morpho expression can be interpolated into a string. Here, the values of `i` and `func(i)` will be inserted into the string as it is created: - - print "${i}: ${func(i)}" - -To get an individual character, use index notatation - - print "morpho"[0] - -You can loop over each character like this: - - for (c in "morpho") print c - -Note that strings are immutable, and hence - - var a = "morpho" - a[0] = 4 - -raises an error. - -[showsubtopics]: # - -## split -[tagsplit]: # (split) - -The split method splits a String into a list of substrings. It takes one argument, which is a string of characters to use to split the string: - - print "1,2,3".split(",") - -gives - - [ 1, 2, 3 ] diff --git a/help/syntax.md b/help/syntax.md deleted file mode 100644 index b7c1859..0000000 --- a/help/syntax.md +++ /dev/null @@ -1,147 +0,0 @@ -[comment]: # (Morpho syntax help file) -[version]: # (0.5) - -[toplevel]: # - -# Syntax -[tagsyntax]: # (syntax) - -Morpho provides a flexible object oriented language similar to other languages in the C family (like C++, Java and Javascript) with a simplified syntax. - -Morpho programs are stored as plain text with the .morpho file extension. A program can be run from the command line by typing - - morpho5 program.morpho - -## Comments -[tagcomment]: # (comment) -[tagcomments]: # (comments) -[tagcommentvar]: # (//) -[tagcommentvarr]: # (/*) -[tagcommentvarrr]: # (*/) -Two types of comment are available. The first type is called a 'line comment' whereby text after `//` on the same line is ignored by the interpreter. - - a.dosomething() // A comment - -Longer 'block' comments can be created by placing text between `/*` and `*/`. Newlines are ignored - - /* This - is - a longer comment */ - -In contrast to C, these comments can be nested - - /* A nested /* comment */ */ - -enabling the programmer to quickly comment out a section of code. - -## Symbols -[tagsymbols]: # (symbols) -[tagnames]: # (names) - -Symbols are used to refer to named entities, including variables, classes, functions etc. Symbols must begin with a letter or underscore _ as the first character and may include letters or numbers as the remainder. Symbols are case sensitive. - - asymbol - _alsoasymbol - another_symbol - EvenThis123 - YET_ANOTHER_SYMBOL - -Classes are typically given names with an initial capital letter. Variable names are usually all lower case. - -## Newlines -[tagnewlines]: # (newlines) -[tagnewline]: # (newline) - -Strictly, morpho ends statements with semicolons like C, but in practice these are usually optional and you can just start a new line instead. For example, instead of - - var a = 1; // The ; is optional - -you can simply use - - var a = 1 - -If you want to put several statements on the same line, you can separate them with semicolons: - - var a = 1; print a - -There are a few edge cases to be aware of: The morpho parser works by accepting a newline anywhere it expects to find a semicolon. To split a statement over multiple lines, signal to morpho that you plan to continue by leaving the statement unfinished. Hence, do this: - - print a + - 1 - -rather than this: - - print a // < Morpho thinks this is a complete statement - + 1 // < and so this line will cause a syntax error - - -## Booleans -[tagtrue]: # (true) -[tagfalse]: # (false) -[tagbooleans]: # (true) - -Comparison operations like `==`, `<` and `>=` return `true` or `false` depending on the result of the comparison. For example, - - print 1==2 - -prints `false`. The constants `true` or `false` are provided for you to use in your own code: - - return true - -## Nil -[tagnil]: # (nil) - -The keyword `nil` is used to represent the absence of an object or value. - -Note that in `if` statements, a value of `nil` is treated like `false`. - - if (nil) { - // Never executed. - } - -## Blocks -[tagblocks]: # (blocks) -[tagblock]: # (block) - -Code is divided into *blocks*, which are delimited by curly brackets like this: - - { - var a = "Hello" - print a - } - -This syntax is used in function declarations, loops and conditional statements. - -Any variables declared within a block become *local* to that block, and cannot be seen outside of it. For example, - - var a = "Foo" - { - var a = "Bar" - print a - } - print a - -would print "Bar" then "Foo"; the version of `a` inside the code block is said to *shadow* the outer version. - -## Precedence -[tagprecedence]: # (precedence) - -Precedence refers to the order in which morpho evaluates operations. For example, - - print 1+2*3 - -prints `7` because `2*3` is evaluated before the addition; the operator `*` is said to have higher precedence than `+`. - -You can always modify the order of evaluation by using parentheses: - - print (1+2)*3 // prints 9 - -## Print -[tagprint]: # (print) - -The `print` keyword is used to print information to the console. It can be followed by any value, e.g. - - print 1 - print true - print a - print "Hello" diff --git a/help/system.md b/help/system.md deleted file mode 100644 index bb9e8ba..0000000 --- a/help/system.md +++ /dev/null @@ -1,71 +0,0 @@ -[comment]: # (System help) -[version]: # (0.5) - -# System -[tagsystem]: # (system) - -The `System` class provides information and access to some features of the runtime environment. - -[showsubtopics]: # (subtopics) - -## Platform -[tagplatform]: # (platform) - -Detect which platform morpho was compiled for: - - print System.platform() - -which returns `"macos"`, `"linux"`, `"unix"` or `"windows"`. - -## Version -[tagversion]: # (version) - -Find the current version of morpho: - - print System.version() - -## Clock -[tagclock]: # (clock) - -Returns the system time in seconds, with at least millisecond granularity. Primarily intended for timing: - - var start=System.clock() - // Do something - print System.clock()-start - -Note that `System.clock` measures the actual physical time elapsed, not the time spent in a process. - -## Sleep -[tagsleep]: # (sleep) - -Pauses the program for a specified number of seconds: - - System.sleep(0.5) // Sleep for half a second - -## Readline -[tagreadline]: # (readline) - -Reads a line of input from the console: - - var in = System.readline() - -## Arguments -[tagargunents]: # (arguments) - -Returns a `List` of arguments passed to the current morpho on the command line. - - var args = System.arguments() - for (e in args) print e - -Run a morpho program with arguments: - - morpho5 program.morpho hello world - -Note that, in line with UNIX conventions, command line arguments before the program file name are passed to the `morpho5` runtime; those after are passed to the morpho program via `System.arguments`. - -## Exit -[tagexit]: # (exit) - -Stop execution of a program: - - System.exit() diff --git a/help/values.md b/help/values.md deleted file mode 100644 index 6f8e415..0000000 --- a/help/values.md +++ /dev/null @@ -1,45 +0,0 @@ -[comment]: # (Values help) -[version]: # (0.5) - -# Values -[tagvalues]: # (values) - -Values are the basic unit of information in morpho: All functions in morpho accept values as arguments and return values. - -[showsubtopics]: # (subtopics) - -## Int -[tagint]: # (int) - -Morpho provides integers, which work as you would expect in other languages, although you rarely need to worry about the distinction between floats and integers. - -Convert a floating point number to an Integer: - - print Int(1.3) // expect: 1 - -Convert a string to an integer: - - print Int("10")+1 // expect: 11 - -## Float -[tagfloat]: # (float) - -Morpho provides double precision floating point numbers. - -Convert a string to a floating point number: - - print Float("1.2e2")+1 // expect: 121 - -## Ceil -[tagceil]: # (ceil) - -Returns the smallest integer larger than or equal to its argument: - - print ceil(1.3) // expect: 2 - -## Floor -[tagfloor]: # (floor) - -Returns the largest integer smaller than or equal to its argument: - - print floor(1.3) // expect: 1 diff --git a/help/variables.md b/help/variables.md deleted file mode 100644 index ad53df9..0000000 --- a/help/variables.md +++ /dev/null @@ -1,70 +0,0 @@ -[comment]: # (Morpho variables help file) -[version]: # (0.5) - -[toplevel]: # - -# Variables -[tagvariables]: # (variables) -[tagvar]: # (var) - -Variables are defined using the `var` keyword followed by the variable name: - - var a - -Optionally, an initial assignment may be given: - - var a = 1 - -Variables defined in a block of code are visible only within that block, so - - var greeting = "Hello" - { - var greeting = "Goodbye" - print greeting - } - print greeting - -will print - -*Goodbye* -*Hello* - -Multiple variables can be defined at once by separating them with commas - - var a, b=2, c[2]=[1,2] - -where each can have its own initializer (or not). - -## Indexing -[taglb]: # ([) -[tagrb]: # (]) -[tagindex]: # (index) -[tagsub]: # (subscript) - -Morpho provides a number of collection objects, such as `List`, `Range`, `Array`, `Dictionary`, `Matrix` and `Sparse`, that can contain more than one value. Index notation (sometimes called subscript notation) is used to access elements of these objects. - -To retrieve an item from a collection, you use the `[` and `]` brackets like this: - - var a = List("Apple", "Bag", "Cat") - print a[0] - -which prints *Apple*. Note that the first element is accessed with `0` not `1`. - -Similarly, to set an entry in a collection, use: - - a[0]="Adder" - -which would replaces the first element in `a` with `"Adder"`. - -Some collection objects need more than one index, - - var a = Matrix([[1,0],[0,1]]) - print a[0,0] - -and others such as `Dictionary` use non-numerical indices, - - var b = Dictionary() - b["Massachusetts"]="Boston" - b["California"]="Sacramento" - -as in this dictionary of state capitals. diff --git a/help/vtk.md b/help/vtk.md deleted file mode 100644 index ca32de4..0000000 --- a/help/vtk.md +++ /dev/null @@ -1,116 +0,0 @@ -[comment]: # (Morpho vtk module help file) -[version]: # (0.5) - -# VTK -[tagvtk]: # (vtk) - -The vtk module contains classes to allow I/O of meshes and fields using -the VTK Legacy Format. - -[showsubtopics]: # (subtopics) - -## VTKExporter -[tagvtkexporter]: # (VTKExporter) - -This class can be used to export the field(s) and/or at a given state -to a single .vtk file. To use it, import the `vtk` module: - - import vtk - -Initialize the `VTKExporter` - - var vtkE = VTKExporter(obj) - -where `obj` can either be - -* A `Mesh` object: This prepares the Mesh for exporting. -* A `Field` object: This prepares both the Field and the Mesh associated - with it for exporting. - -Use the `export` method to export to a VTK file. - - vtkE.export("output.vtk") - -Optionally, use the `addfield` method to add one or more fields before -exporting: - - vtkE.addfield(f, fieldname="f") - -where, - -* `f` is the field object to be exported -* `fieldname` is an optional argument that assigns a name to the field - in the VTK file. This name is required to be a character - string without embedded whitespace. If not provided, the name would be - either "scalars" or "vectors" depending on the field type**. - -** Note that this currently only supports scalar or vector (column -matrix) fields that live on the vertices ( shape `[1,0,0]`). Support for -tensorial fields and fields on cells coming soon. - -Minimal example: - - import vtk - import meshtools - - var m1 = LineMesh(fn (t) [t,0,0], -1..1:2) - - var vtkE = VTKExporter(m1) // Export just the mesh - - vtkE.export("mesh.vtk") - - var f1 = Field(m1, fn(x,y,z) x) - - var g1 = Field(m1, fn(x,y,z) Matrix([x,2*x,3*x])) - - vtkE = VTKExporter(f1, fieldname="f") // Export fields - - vtkE.addfield(g1, fieldname="g") - - vtkE.export("data.vtk") - -## VTKImporter -[tagvtkimporter]: # (VTKImporter) - -This class can be used to import the field(s) and/or the mesh at a -given state from a single .vtk file. To use it, import the `vtk` module: - - import vtk - -Initialize the `VTKImporter` with the filename - - var vtkI = VTKImporter("output.vtk") - -Use the `mesh` method to get the mesh: - - var mesh = vtkI.mesh() - -Use the `field` method to get the field: - - var f = vtkI.field(fieldname) - -Use the `fieldlist` method to get the list of the names of the fields contained in the file: - - print vtkI.fieldlist() - -Use the `containsfield` method to check whether the file contains a field by a given `fieldname`: - - if (tkI.containsfield(fieldname)) { - ... - } - -where `fieldname` is the name assigned to the field in the .vtk file - -Minimal example: - - import vtk - import meshtools - - var vtkI = VTKImporter("data.vtk") - - var m = vtkI.mesh() - - var f = vtkI.field("f") - - var g = vtkI.field("g") - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 720827f..c60133f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,7 +2,7 @@ target_sources(morpho6 PRIVATE cli.c cli.h debugger.c debugger.h - help.c help.h - linedit.c linedit.h + hlp.c hlp.h + inline.c inline.h main.c ) \ No newline at end of file diff --git a/src/cli.c b/src/cli.c index fdcf0ea..c129dcf 100644 --- a/src/cli.c +++ b/src/cli.c @@ -6,14 +6,23 @@ #include #include +#include +#include +#include +#include +#include -#include "cli.h" - +#include #include #include +#include "cli.h" #include "debugger.h" +#ifdef MORPHO_INCLUDE_HELP + #include +#endif + #ifdef CLI_USELIBUNISTRING #include #endif @@ -23,65 +32,163 @@ #endif char *cli_globalsrc=NULL; +static int s_cli_lastexitcode = EXIT_SUCCESS; +static clioptions s_cli_activeopt = 0; + +void cli_setexitcode(int code) { + s_cli_lastexitcode = code; +} -#define CLI_BUFFERSIZE 1024 +int cli_exitcode(void) { + return s_cli_lastexitcode; +} + +void cli_applyoptions(clioptions opt) { + s_cli_activeopt = opt; +} -#define BLU "\x1B[34m" -#define CYN "\x1B[36m" -#define GRY "\x1B[38;2;128;128;128m" -#define RESET "\x1B[0m" +/** True when ANSI styling should be emitted. */ +bool cli_usecolor(void) { + if (s_cli_activeopt & CLI_NOCOLOR) return false; + const char *nocolor = getenv("NO_COLOR"); + if (nocolor && *nocolor) return false; + if (!inline_checkstdouttty()) return false; + return inline_checksupported(); +} -/** Displays several strings with a specified style using linedit */ -void cli_displaywithstyle(lineditor *edit, linedit_color col, linedit_emphasis emph, int n, ...) { +#define CLI_BUFFERSIZE 4096 + +#define RESET "\x1B[0m" +#define BOLD "\x1B[1m" +#define ITALIC "\x1B[3m" +#define UNDERLINE "\x1B[4m" + +/* ********************************************************************** + * Utility functions + * ********************************************************************** */ + +void cli_emitemphasis(int emph) { + switch (emph) { + case CLI_NOEMPHASIS: inline_emit(RESET); break; + case CLI_BOLD: inline_emit(BOLD); break; + case CLI_UNDERLINE: inline_emit(UNDERLINE); break; + case CLI_ITALIC: inline_emit(ITALIC); break; + default: break; + } +} + +/** Displays several strings with a specified style */ +void cli_displaywithstyle(int col, int emph, int n, ...) { va_list args; va_start(args, n); + bool color = cli_usecolor(); + + fflush(stdout); for (int i=0; icat!=ERROR_NONE) { - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, "Error '", err->id, "'"); + cli_setexitcode(EXIT_FAILURE); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, "Error '", err->id, "'"); if (ERROR_ISRUNTIMEERROR(*err)) { - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); morpho_stacktrace(v); } else { if (err->line!=ERROR_POSNUNIDENTIFIABLE && err->posn!=ERROR_POSNUNIDENTIFIABLE) { char posnbuffer[CLI_BUFFERSIZE]; snprintf(posnbuffer, CLI_BUFFERSIZE, " [line %u char %u", err->line, err->posn+1); - linedit_displaywithstyle(&linedit, posnbuffer, CLI_ERRORCOLOR, CLI_NOEMPHASIS); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 1, posnbuffer); if (err->file) { - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, " in module '", err->file, "'"); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, " in module '", err->file, "'"); } - linedit_displaywithstyle(&linedit, "] ", CLI_ERRORCOLOR, CLI_NOEMPHASIS); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 1, "] "); } - cli_displaywithstyle(&linedit, CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); + cli_displaywithstyle(CLI_ERRORCOLOR, CLI_NOEMPHASIS, 3, ": ", err->msg, "\n"); + } + } +} + + +/** Interactive help. Returns true if help was found or a useful hint was shown. */ +#ifndef MORPHO_INCLUDE_HELP +static bool cli_help(inline_editor *edit, char *query, error *err, bool avail) { + (void)avail; + char *q=query; + if (help_querylength(q, NULL)==0) { + if (err->cat!=ERROR_NONE) { + q=err->id; + error_clear(err); + } else { + q=HELP_INDEXPAGE; } } - linedit_clear(&linedit); + objecthelptopic *topic = help_search(q); + if (topic) { + help_display(edit, topic); + return true; + } + while (isspace(*q) && *q!='\0') q++; + printf("No help found for '%s'\n", q); + return false; +} +#else +static bool cli_help(inline_editor *edit, char *query, error *err, bool avail) { + (void)err; (void)avail; + char *q = query; + while (isspace(*q) && *q!='\0') q++; // Strip any leading space + bool blank = (*q == '\0'); // Check for blank query + if (blank) q = "help"; + + bool found = false; + help_topic topic; + if (morpho_helpastopic(q, &topic)) { + hlp_displaytopic(edit, &topic); + found = true; + } else { + varray_char result; + varray_charinit(&result); + help_queryhint(q, &result); + if (result.count > 0) printf("%s\n", result.data); + else printf("No help found for '%s'\n", q); + varray_charclear(&result); + } + + /* Index page promises a topic list below; show it for blank or "help". */ + if (blank || strcmp(q, "help") == 0) { + varray_value list; + varray_valueinit(&list); + morpho_helptopics(&list); + hlp_displaytopiclist(edit, &list, HLP_TOPICS_HDR); + varray_valueclear(&list); + found = true; + } + return found; } +#endif /* ********************************************************************** - * CLI callbacks + * Morpho callbacks * ********************************************************************** */ /** Print callback */ void cli_printcallbackfn(vm *v, void *ref, char *string) { - lineditor *l = (lineditor *) ref; - - cli_displaywithstyle(l, CLI_DEFAULTCOLOR, LINEDIT_BOLD, 1, string); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_BOLD, 1, string); } /** Input callback */ @@ -90,211 +197,192 @@ void cli_inputcallbackfn(vm *v, void *ref, morphoinputmode mode, varray_char *st int key = getchar(); if (key!=EOF) varray_charwrite(str, (char) key); } else { - lineditor line; - linedit_init(&line); - linedit_setprompt(&line, ""); - char *out=linedit(&line); + inline_editor *line=inline_new(""); + char *out=inline_readline(line); if (out) varray_charadd(str, out, (int) strlen(out)); - linedit_clear(&line); + free(out); + inline_free(line); } } /** Warning callback */ void cli_warningcallbackfn(vm *v, void *ref, error *err) { - lineditor *l = (lineditor *) ref; - - cli_displaywithstyle(l, CLI_WARNINGCOLOR, CLI_NOEMPHASIS, 5, "Warning '", err->id, "': ", err->msg, "\n"); + (void)v; (void)ref; + cli_displaywithstyle(CLI_WARNINGCOLOR, CLI_NOEMPHASIS, 5, "Warning '", err->id, "': ", err->msg, "\n"); } -/** Warning callback */ +/** Debugger callback */ void cli_debuggercallbackfn(vm *v, void *ref) { + (void)ref; clidebugger_enter(v); } /* ********************************************************************** - * Interactive cli + * Inline callbacks * ********************************************************************** */ -/** Define colors for different token types */ -linedit_colormap cli_tokencolors[] = { - { TOKEN_NEWLINE, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_QUESTION, LINEDIT_YELLOW }, - - { TOKEN_STRING, LINEDIT_BLUE }, - { TOKEN_INTERPOLATION, LINEDIT_BLUE }, - { TOKEN_INTEGER, LINEDIT_BLUE }, - { TOKEN_NUMBER, LINEDIT_BLUE }, - { TOKEN_SYMBOL, LINEDIT_CYAN }, - - { TOKEN_LEFTPAREN, LINEDIT_DEFAULTCOLOR }, - { TOKEN_RIGHTPAREN, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LEFTSQBRACKET, LINEDIT_DEFAULTCOLOR }, - { TOKEN_RIGHTSQBRACKET, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LEFTCURLYBRACKET, LINEDIT_DEFAULTCOLOR }, - { TOKEN_RIGHTCURLYBRACKET, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_COLON, LINEDIT_DEFAULTCOLOR }, - { TOKEN_SEMICOLON, LINEDIT_DEFAULTCOLOR }, - { TOKEN_COMMA, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_PLUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_MINUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_STAR, LINEDIT_DEFAULTCOLOR }, - { TOKEN_SLASH, LINEDIT_DEFAULTCOLOR }, - { TOKEN_CIRCUMFLEX, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_PLUSPLUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_MINUSMINUS, LINEDIT_DEFAULTCOLOR }, - { TOKEN_PLUSEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_MINUSEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_STAREQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_SLASHEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_HASH, LINEDIT_DEFAULTCOLOR }, - { TOKEN_AT, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_QUOTE, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DOT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DOTDOT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DOTDOTDOT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EXCLAMATION, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_AMP, LINEDIT_DEFAULTCOLOR }, - { TOKEN_VBAR, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DBLAMP, LINEDIT_DEFAULTCOLOR }, - { TOKEN_DBLVBAR, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EQUAL, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_NEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_GT, LINEDIT_DEFAULTCOLOR }, - { TOKEN_LTEQ, LINEDIT_DEFAULTCOLOR }, - { TOKEN_GTEQ, LINEDIT_DEFAULTCOLOR }, - - { TOKEN_TRUE, LINEDIT_MAGENTA }, - { TOKEN_FALSE, LINEDIT_MAGENTA }, - { TOKEN_NIL, LINEDIT_MAGENTA }, - { TOKEN_SELF, LINEDIT_MAGENTA }, - { TOKEN_SUPER, LINEDIT_MAGENTA }, - { TOKEN_IMAG, LINEDIT_BLUE }, - - { TOKEN_PRINT, LINEDIT_MAGENTA }, - { TOKEN_VAR, LINEDIT_MAGENTA }, - { TOKEN_IF, LINEDIT_MAGENTA }, - { TOKEN_ELSE, LINEDIT_MAGENTA }, - { TOKEN_IN, LINEDIT_MAGENTA }, - { TOKEN_WHILE, LINEDIT_MAGENTA }, - { TOKEN_FOR, LINEDIT_MAGENTA }, - { TOKEN_DO, LINEDIT_MAGENTA }, - { TOKEN_BREAK, LINEDIT_MAGENTA }, - { TOKEN_CONTINUE, LINEDIT_MAGENTA }, - { TOKEN_FUNCTION, LINEDIT_MAGENTA }, - { TOKEN_RETURN, LINEDIT_MAGENTA }, - { TOKEN_CLASS, LINEDIT_MAGENTA }, - { TOKEN_IMPORT, LINEDIT_MAGENTA }, - { TOKEN_AS, LINEDIT_MAGENTA }, - { TOKEN_IS, LINEDIT_MAGENTA }, - { TOKEN_WITH, LINEDIT_MAGENTA }, - { TOKEN_TRY, LINEDIT_MAGENTA }, - { TOKEN_CATCH, LINEDIT_MAGENTA }, - - { TOKEN_SHEBANG, LINEDIT_DEFAULTCOLOR }, - { TOKEN_INCOMPLETE, LINEDIT_DEFAULTCOLOR }, - { TOKEN_EOF, LINEDIT_DEFAULTCOLOR }, - { LINEDIT_ENDCOLORMAP, LINEDIT_DEFAULTCOLOR } +/** Define colors for different token types (256-color palette) */ +int palette[] = { + CLI_DEFAULTCOLOR, // 0 default + INLINE_YELLOW, // 1 help + INLINE_COLOR_ANSI216(0, 1, 5), // 2 string/integer/number literals (darker blue, visible on both backgrounds) + INLINE_COLOR_ANSI216(0, 3, 4), // 3 symbol (darker cyan/teal, distinct from blue) + INLINE_MAGENTA, // 4 keyword + INLINE_GRAY_ANSI(12), // 5 comment (mid-level gray) + INLINE_COLOR_ANSI216(5, 3, 0), // 6 operator (amber) + CLI_ERRORCOLOR // 7 debug breakpoint marker @ }; +tokentype help[] = { TOKEN_QUESTION }; +tokentype literal[] = { TOKEN_STRING, TOKEN_INTERPOLATION, TOKEN_INTEGER, TOKEN_NUMBER, TOKEN_IMAG }; +tokentype symbols[] = { TOKEN_SYMBOL }; +tokentype keywords[] = { TOKEN_TRUE, TOKEN_FALSE, TOKEN_NIL, TOKEN_SELF, TOKEN_SUPER, TOKEN_PRINT, TOKEN_VAR, TOKEN_IF, TOKEN_ELSE, TOKEN_IN, TOKEN_WHILE, TOKEN_FOR, TOKEN_DO, TOKEN_BREAK, TOKEN_CONTINUE, TOKEN_FUNCTION, + TOKEN_RETURN, TOKEN_CLASS, TOKEN_IMPORT, TOKEN_AS, TOKEN_IS, TOKEN_WITH, TOKEN_TRY, TOKEN_CATCH }; +tokentype operators[] = { + TOKEN_PLUS, TOKEN_MINUS, TOKEN_STAR, TOKEN_SLASH, TOKEN_CIRCUMFLEX, + TOKEN_PLUSPLUS, TOKEN_MINUSMINUS, + TOKEN_PLUSEQ, TOKEN_MINUSEQ, TOKEN_STAREQ, TOKEN_SLASHEQ, + TOKEN_HASH, + TOKEN_EXCLAMATION, TOKEN_AMP, TOKEN_VBAR, TOKEN_DBLAMP, TOKEN_DBLVBAR, + TOKEN_EQUAL, TOKEN_EQ, TOKEN_NEQ, + TOKEN_LT, TOKEN_GT, TOKEN_LTEQ, TOKEN_GTEQ, + TOKEN_DOTDOT, TOKEN_DOTDOTDOT +}; + +/** Checks if match matches any tokentype in a given list */ +static bool matchtokentype(tokentype match, size_t n, tokentype *list) { + for (size_t i=0; i 0 && *p != '\0') { + if (p[0] == '/' && p[1] == '*') { + nesting++; + p += 2; + } else if (p[0] == '*' && p[1] == '/') { + nesting--; + p += 2; + } else p++; + } + return p - in; + } + + return offset; // No comment found +} + /** A tokenizer for syntax coloring that uses the morpho lexer */ -bool cli_lex(char *in, void *ref, linedit_token *out) { +bool cli_syntaxcolorfn(const char *in, void *ref, size_t offset, inline_colorspan_t *out) { bool success=false; lexer *l=(lexer *) ref; + if (!l) return false; - lex_init(l, in, 0); + // Check for comments first (before lexer processing) + size_t comment_end = detect_comment(in, offset); + if (comment_end > offset) { + // Found a comment + out->color = 5; // Comment color (gray) + out->byte_end = comment_end; + return true; // Successfully colored a comment + } + + if (offset==0) lex_init(l, in, 0); + else l->current=in+offset; + token tok; error err; error_init(&err); if (lex(l, &tok, &err)) { - out->start=(char *) tok.start; - out->length=tok.length; - out->type=(linedit_tokentype) tok.type; + out->color=0; + if (tok.start>in+offset) { // Token began after offset + out->byte_end=tok.start-in; + } else { // A real token + out->byte_end=offset+tok.length; + if (tok.type==TOKEN_AT) out->color=7; // Debug breakpoint marker in red + else if (matchtokentype(tok.type, sizeof(help)/sizeof(help[0]), help)) out->color=1; + else if (matchtokentype(tok.type, sizeof(literal)/sizeof(literal[0]), literal)) out->color=2; + else if (matchtokentype(tok.type, sizeof(symbols)/sizeof(symbols[0]), symbols)) out->color=3; + else if (matchtokentype(tok.type, sizeof(keywords)/sizeof(keywords[0]), keywords)) out->color=4; + else if (matchtokentype(tok.type, sizeof(operators)/sizeof(operators[0]), operators)) out->color=6; + } success=(tok.type!=TOKEN_EOF); } - lex_clear(l); + if (tok.type==TOKEN_EOF) lex_clear(l); return success; } +static char *words[] = {"as", "and", "break", "catch", "class", "continue", "do", "else", "false", "fn", "for", "help", "if", "import", "in", "is", "nil", "or", "print", "quit", "return", "self", "super", "true", "try", "var", "while", "with", NULL}; + /** Autocomplete function */ -bool cli_complete(char *in, void *ref, linedit_stringlist *c) { +const char *cli_complete(const char *in, void *ref, size_t *index) { + (void)ref; size_t len=strlen(in); /* First find the last token in the input */ - char *tok = in+len; + const char *tok = in+len; /* Scan backwards from end of string over alphanumeric tokens */ while (tok>in && !isspace(*(tok-1))) tok--; /* Ensure we have at least one character */ - if (iscntrl(*tok)) return false; + if (iscntrl(*tok)) return NULL; /* Now try to match the token against a library of words */ len=strlen(tok); - char *words[] = {"as", "and", "break", "class", "continue", "do", "else", "for", "false", "fn", "help", "if", "in", "import", "nil", "or", "print", "return", "true", "var", "while", "quit", "self", "super", "this", "try", "catch", NULL}; - int success=false; - for (unsigned int i=0; words[i]!=NULL; i++) { + for (size_t i=*index; words[i]!=NULL; i++) { if ( (len0); } -/** Interactive help */ -void cli_help(lineditor *edit, char *query, error *err, bool avail) { - char *q=query; - if (help_querylength(q, NULL)==0) { - if (err->cat!=ERROR_NONE) { - q=err->id; - error_clear(err); - } else { - q=HELP_INDEXPAGE; - } - } - - objecthelptopic *topic = help_search(q); - if (topic) { - help_display(edit, topic); - } else { - while (isspace(*q) && *q!='\0') q++; - printf("No help found for '%s'\n", q); - } -} - #ifdef CLI_USELIBUNISTRING size_t libunistring_graphemefn(const char *in, const char *end) { char *next = (char *) u8_grapheme_next((uint8_t *) in, (uint8_t *) end); @@ -309,197 +397,263 @@ size_t libgrapheme_graphemefn(const char *in, const char *end) { } #endif +/* ********************************************************************** + * Interactive mode + * ********************************************************************** */ + +/** Runtime context holding VM, compiler, program, editor, and lexer */ +typedef struct { + vm *v; + compiler *c; + program *p; + inline_editor *edit; + lexer l; +} runtime_t; + +/** Forward declaration */ +static void cli_repl(runtime_t *rt, clioptions opt); +static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt); +static runtime_t cli_newruntime(clioptions opt); +static void cli_freeruntime(runtime_t *rt); + +/** Compile preamble (if any) then src (if any) in an existing runtime. + * @returns false if a compile/run step failed. */ +static bool cli_runpreambleandsource(runtime_t *rt, const char *preamble, const char *src, clioptions opt) { + if (preamble && *preamble) { + cli_globalsrc = (char *) preamble; + if (!cli_compileandrun(rt, preamble, opt)) return false; + } + if (src) { + cli_globalsrc = (char *) src; + if (!cli_compileandrun(rt, src, opt)) return false; + } + return true; +} + /** @brief Provide a command line interface */ -void cli(clioptions opt) { - bool tty=linedit_checktty(); +void cli(clioptions opt, const char *preamble) { + runtime_t rt = cli_newruntime(opt); + if (cli_runpreambleandsource(&rt, preamble, NULL, opt)) { + cli_repl(&rt, opt); + } + cli_freeruntime(&rt); +} + +/* ********************************************************************** + * Helper structures and functions + * ********************************************************************** */ + +/** Set up runtime context (VM, compiler, program, editor, callbacks) */ +static runtime_t cli_newruntime(clioptions opt) { + runtime_t rt = { NULL, NULL, NULL, NULL }; + rt.p = morpho_newprogram(); + rt.c = morpho_newcompiler(rt.p); + rt.v = morpho_newvm(); + rt.edit = inline_new(CLI_PROMPT); + + cli_applyoptions(opt); + if (cli_usecolor()) { + inline_syntaxcolor(rt.edit, cli_syntaxcolorfn, &rt.l); + inline_setpalette(rt.edit, sizeof(palette)/sizeof(palette[0]), palette); + } + + morpho_setinputfn(rt.v, cli_inputcallbackfn, NULL); + morpho_setprintfn(rt.v, cli_printcallbackfn, &rt.edit); + morpho_setwarningfn(rt.v, cli_warningcallbackfn, &rt.edit); + morpho_setdebuggerfn(rt.v, cli_debuggercallbackfn, NULL); + + return rt; +} + +/** Clean up runtime context */ +static void cli_freeruntime(runtime_t *rt) { + if (rt->edit) inline_free(rt->edit); + if (rt->v) morpho_freevm(rt->v); + if (rt->p) morpho_freeprogram(rt->p); + if (rt->c) morpho_freecompiler(rt->c); +} + +/** Run a compiled program according to CLI flags. */ +static bool cli_execute(runtime_t *rt, clioptions opt, error *err_out) { + if (!(opt & CLI_RUN)) return true; + + bool success; + if (opt & CLI_DEBUG) { + success = morpho_debug(rt->v, rt->p); + } else if (opt & CLI_PROFILE) { + success = morpho_profile(rt->v, rt->p); + } else { + success = morpho_run(rt->v, rt->p); + } + + if (!success) { + cli_reporterror(morpho_geterror(rt->v), rt->v); + if (err_out) *err_out = *morpho_geterror(rt->v); + } + return success; +} + +/** Compile and execute source code */ +static bool cli_compileandrun(runtime_t *rt, const char *src, clioptions opt) { + error err; + error_init(&err); + + bool success = morpho_compile((char *)src, rt->c, (opt & CLI_OPTIMIZE) != 0, &err); + + if (success) { + if (opt & CLI_DISASSEMBLE) { + if (opt & CLI_DISASSEMBLESHOWSRC) { + cli_disassemblewithsrc(rt->p, (char *)src, opt); + } else { + morpho_disassemble(rt->v, rt->p, NULL); + } + } + success = cli_execute(rt, opt, NULL); + } else { + cli_reporterror(&err, rt->v); + } + + return success; +} + +/* ********************************************************************** + * Non-interactive run + * ********************************************************************** */ + +/** Compile and run source string (no file). Used by -e / --eval. */ +void cli_runstring(const char *src, clioptions opt, const char *preamble) { + runtime_t rt = cli_newruntime(opt); + + if (cli_runpreambleandsource(&rt, preamble, src, opt) && (opt & CLI_INTERACTIVE)) { + cli_repl(&rt, opt); + } + cli_freeruntime(&rt); +} + +/** Enter REPL mode, reusing an existing runtime */ +static void cli_repl(runtime_t *rt, clioptions opt) { + bool tty = inline_checktty(); version morphoversion; morpho_version(&morphoversion); char morphoversionstring[VERSION_MAXSTRINGLENGTH]; version_tostring(&morphoversion, VERSION_MAXSTRINGLENGTH, morphoversionstring); if (tty) { - linedit_setutf8(); - #ifdef MORPHO_LONG_BANNER - // Original ASCII art source - https://www.asciiart.eu/animals/insects/butterflies - printf(BLU " ___ ___ \n" RESET); - printf(BLU "(" CYN " @ " GRY"\\Y/" CYN " @ " BLU ") " RESET " | morpho %s | \U0001F44B Type 'help' or '?' for help\n", morphoversionstring); - printf(BLU " \\" CYN"__" GRY"+|+" CYN"__" BLU"/ " RESET " | Documentation: https://morpho-lang.readthedocs.io/en/latest/ \n"); - printf(BLU" {" CYN"_" BLU "/ \\" CYN "_" BLU"} " RESET " | Code: https://github.com/Morpho-lang/morpho \n\n"); - #else + inline_setutf8(); printf("\U0001F98B morpho %s | \U0001F44B Type 'help' or '?' for help\n", morphoversionstring); - #endif } - - /* Set up program and compiler */ - program *p = morpho_newprogram(); - compiler *c = morpho_newcompiler(p); - bool help = help_initialize(); + bool help = hlp_initialize(); - /* Keep the line by line src as a varray */ varray_char src; varray_charinit(&src); - varray_charwrite(&src, '\0'); // Begin with zero string - - /* Set up VM */ - vm *v = morpho_newvm(); + varray_charwrite(&src, '\0'); - /* Line editor */ - lineditor edit; - lexer l; - linedit_init(&edit); - linedit_setprompt(&edit, CLI_PROMPT); - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); - linedit_multiline(&edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); - linedit_autocomplete(&edit, cli_complete, NULL); + /* Configure editor for REPL */ + if (cli_usecolor()) { + inline_setpalette(rt->edit, sizeof(palette)/sizeof(palette[0]), palette); + inline_syntaxcolor(rt->edit, cli_syntaxcolorfn, &rt->l); + } + inline_multiline(rt->edit, cli_multiline, NULL, CLI_CONTINUATIONPROMPT); + inline_autocomplete(rt->edit, cli_complete, NULL); #ifdef CLI_USELIBUNISTRING - linedit_setgraphemesplitter(&edit, libunistring_graphemefn); + inline_setgraphemesplitter(rt->edit, libunistring_graphemefn); #endif #ifdef CLI_USELIBGRAPHEME - linedit_setgraphemesplitter(&edit, libgrapheme_graphemefn); + inline_setgraphemesplitter(rt->edit, libgrapheme_graphemefn); #endif - - morpho_setinputfn(v, cli_inputcallbackfn, NULL); - morpho_setprintfn(v, cli_printcallbackfn, &edit); - morpho_setwarningfn(v, cli_warningcallbackfn, &edit); - morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); - - error err; /* Error structure that received messages from the compiler and VM */ - bool success=false; /* Keep track of whether compilation and execution was successful */ - /* Initialize the error struct */ + error err; error_init(&err); - - /* Read-evaluate-print loop */ - for (int n=0;;n++) { - if (!tty && n>0) break; - char *input=NULL; - - while (!input) input=linedit(&edit); + + for (int n = 0; ; n++) { + if (!tty && n > 0) break; + char *input = NULL; + while (!input) input = inline_readline(rt->edit); - /* Check for CLI commands. */ - /* Let the user quit by typing 'quit'. */ - if (strncmp(input, CLI_QUIT, strlen(CLI_QUIT))==0) { - break; - } else if (strncmp(input, CLI_HELP, strlen(CLI_HELP))==0) { - cli_help(&edit, input+strlen(CLI_HELP), &err, help); continue; - } else if (strncmp(input, CLI_SHORT_HELP, strlen(CLI_SHORT_HELP))==0) { - cli_help(&edit, input+strlen(CLI_SHORT_HELP), &err, help); continue; + if (strncmp(input, CLI_QUIT, strlen(CLI_QUIT)) == 0) { + free(input); + break; + } else if (strncmp(input, CLI_HELP, strlen(CLI_HELP)) == 0) { + cli_help(rt->edit, input + strlen(CLI_HELP), &err, help); + free(input); + continue; + } else if (strncmp(input, CLI_SHORT_HELP, strlen(CLI_SHORT_HELP)) == 0) { + cli_help(rt->edit, input + strlen(CLI_SHORT_HELP), &err, help); + free(input); + continue; } - /* Compile code */ - success=morpho_compile(input, c, false, &err); + bool success = morpho_compile(input, rt->c, (opt & CLI_OPTIMIZE) != 0, &err); - if (success) { /** If compilation was successful, and we're in interactive mode, execute... */ - /** Retain input in interactive session */ - src.count--; // Remove zero terminator - varray_charadd(&src, input, (int) strlen(input)); + if (success) { + src.count--; + varray_charadd(&src, input, (int)strlen(input)); varray_charwrite(&src, '\n'); - varray_charwrite(&src, '\0'); // Ensure zero terminated - cli_globalsrc=src.data; + varray_charwrite(&src, '\0'); + cli_globalsrc = src.data; if (opt & CLI_DISASSEMBLE) { - morpho_disassemble(v, p, NULL); - } - if (opt & CLI_RUN) { - success=morpho_debug(v, p); - if (!success) { - cli_reporterror(morpho_geterror(v), v); - err=*morpho_geterror(v); - } + morpho_disassemble(rt->v, rt->p, NULL); } + success = cli_execute(rt, opt, &err); } else { - /** ... otherwise just raise an error. */ - cli_reporterror(&err, v); - } + cli_reporterror(&err, rt->v); + } + + free(input); } - linedit_clear(&edit); - morpho_freevm(v); - varray_charclear(&src); - - help_finalize(); - - morpho_freecompiler(c); - morpho_freeprogram(p); + hlp_finalize(); } /* ********************************************************************** - * Run a file + * Load and run a file * ********************************************************************** */ -/** Loads and runs a file. */ -void cli_run(const char *in, clioptions opt) { - program *p = morpho_newprogram(); - compiler *c = morpho_newcompiler(p); - vm *v = morpho_newvm(); - - /* Set up line editor for output */ - lineditor edit; - lexer l; - linedit_init(&edit); - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); - - morpho_setinputfn(v, cli_inputcallbackfn, &edit); - morpho_setprintfn(v, cli_printcallbackfn, &edit); - morpho_setwarningfn(v, cli_warningcallbackfn, &edit); - morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); +void cli_run(const char *in, clioptions opt, const char *preamble) { + runtime_t rt = cli_newruntime(opt); char *src = cli_loadsource(in); - if (src) cli_globalsrc = src; - error err; /* Error structure that received messages from the compiler and VM */ - error_init(&err); - - bool success=false; /* Keep track of whether compilation and execution was successful */ - - /* Open the input file if provided */ file_setworkingdirectory(in); if (src) { - /* Compile code */ - success=morpho_compile(src, c, (opt & CLI_OPTIMIZE), &err); - - /* Run code if successful */ - if (success) { - if (opt & CLI_DISASSEMBLE) { - if (opt & CLI_DISASSEMBLESHOWSRC) { - cli_disassemblewithsrc(p, src); - } else { - morpho_disassemble(v, p, NULL); - } - } - if (opt & CLI_RUN) { - if (opt & CLI_DEBUG) { - morpho_setdebuggerfn(v, cli_debuggercallbackfn, NULL); - success=morpho_debug(v, p); - } else if (opt & CLI_PROFILE) { - success=morpho_profile(v, p); - } else { - success=morpho_run(v, p); - } - if (!success) cli_reporterror(morpho_geterror(v), v); - } - } else { - cli_reporterror(&err, v); + if (cli_runpreambleandsource(&rt, preamble, src, opt) && (opt & CLI_INTERACTIVE)) { + cli_repl(&rt, opt); } + MORPHO_FREE(src); } else { printf("Could not open file '%s'.\n", in); + cli_setexitcode(EXIT_FAILURE); } - - linedit_clear(&edit); - - MORPHO_FREE(src); - morpho_freevm(v); - morpho_freeprogram(p); - morpho_freecompiler(c); + cli_freeruntime(&rt); } /* ********************************************************************** * Load source code * ********************************************************************** */ +/** Loads source from stdin, returning it as a C-string. Call MORPHO_FREE on it when finished. */ +char *cli_loadstdin(void) { + varray_char buffer; + varray_charinit(&buffer); + + /* Read from stdin in chunks */ + char chunk[CLI_BUFFERSIZE]; + size_t nread; + + while ((nread = fread(chunk, 1, sizeof(chunk), stdin)) > 0) { + varray_charadd(&buffer, chunk, (int)nread); + } + + /* Ensure null termination */ + varray_charwrite(&buffer, '\0'); + + return buffer.data; +} + /** Loads a source file, returning it as a C-string. Call MORPHO_FREE on it when finished. */ char *cli_loadsource(const char *in) { FILE *f = NULL; /* Input file */ @@ -521,6 +675,7 @@ char *cli_loadsource(const char *in) { if (size) { /* Size the buffer to match */ if (!varray_charresize(&buffer, size+1)) { + fclose(f); return NULL; } @@ -528,10 +683,9 @@ char *cli_loadsource(const char *in) { for (char *c=buffer.data; !feof(f); c=c+strlen(c)) { if (!fgets(c, (int) (buffer.data+buffer.capacity-c), f)) { c[0]='\0'; break; } } - - fclose(f); } + fclose(f); return buffer.data; } @@ -539,57 +693,101 @@ char *cli_loadsource(const char *in) { * Source listing and disassembly * ********************************************************************** */ -/** Displays a single line of source */ -static void cli_printline(lineditor *edit, int line, char *prompt, const char *src, int length) { +/** Create a temporary editor configured for colored display. */ +static inline_editor *cli_tempeditor(lexer *l) { + inline_editor *edit = inline_new(""); + if (!edit) return NULL; + if (cli_usecolor()) { + inline_syntaxcolor(edit, cli_syntaxcolorfn, l); + inline_setpalette(edit, sizeof(palette)/sizeof(palette[0]), palette); + } + return edit; +} + +/** Displays a single line of source. nbytes is the span length; a trailing newline is stripped. */ +static void cli_printline(inline_editor *edit, int line, const char *prompt, const char *src, int nbytes) { printf("%s %4u : ", prompt, line); - /* Display the src line */ - char srcline[length]; - strncpy(srcline, src, length-1); - srcline[length-1]='\0'; - linedit_displaywithsyntaxcoloring(edit, srcline); + char stackbuf[512]; + char *buf = (nbytes + 1 <= (int) sizeof(stackbuf)) ? stackbuf : malloc((size_t) nbytes + 1); + if (!buf) return; + memcpy(buf, src, (size_t) nbytes); + buf[nbytes] = '\0'; + if (nbytes > 0 && buf[nbytes - 1] == '\n') buf[nbytes - 1] = '\0'; + inline_displaywithsyntaxcoloring(edit, buf); printf("\n"); + if (buf != stackbuf) free(buf); } -/** Disassembles the program showing syntax colored lines of source */ -void cli_disassemblewithsrc(program *p, char *src) { - lineditor edit; - linedit_init(&edit); +typedef void (*cli_sourcelinefn)(inline_editor *edit, int line, const char *start, int nbytes, void *ref); + +/** Walk source lines in [startline, endline] and invoke fn for each. */ +static void cli_foreachsourceline(const char *src, int startline, int endline, + cli_sourcelinefn fn, void *ref) { + if (!src || !fn) return; lexer l; - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); - - int line=1, length=0; - for (unsigned int i=0; src[i]!='\0'; i++) { + inline_editor *edit = cli_tempeditor(&l); + if (!edit) return; + + int line = 1, length = 0; + for (unsigned int i = 0; src[i] != '\0'; i++) { length++; - if (src[i]=='\n' || src[i]=='\0') { - cli_printline(&edit, line, ">>>", src+i-length+1, length); - morpho_disassemble(NULL, p, NULL); - line++; length=0; + if (src[i] == '\n') { + if (line >= startline && line <= endline) + fn(edit, line, src + i - length + 1, length, ref); + line++; + length = 0; } } - - linedit_clear(&edit); + if (length > 0 && line >= startline && line <= endline) + fn(edit, line, src + strlen(src) - length, length, ref); + + inline_free(edit); +} + +static void cli_listline(inline_editor *edit, int line, const char *start, int nbytes, void *ref) { + (void)ref; + cli_printline(edit, line, "", start, nbytes); +} + +static void cli_disassembleline(inline_editor *edit, int line, const char *start, int nbytes, void *ref) { + program *p = (program *) ref; + int matchline = line; + cli_printline(edit, line, ">>>", start, nbytes); + morpho_disassemble(NULL, p, &matchline); +} + +/** Disassembles the program showing syntax colored lines of source */ +void cli_disassemblewithsrc(program *p, char *src, clioptions opt) { + clioptions prev = s_cli_activeopt; + s_cli_activeopt |= opt; + cli_foreachsourceline(src, 1, INT_MAX, cli_disassembleline, p); + s_cli_activeopt = prev; } /** Displays a source listing from source lines start to end */ -void cli_list(const char *src, int start, int end) { - lineditor edit; - - if (src) { - linedit_init(&edit); - lexer l; - linedit_syntaxcolor(&edit, cli_lex, &l, cli_tokencolors); - - int line=1, length=0; - for (unsigned int i=0; src[i]!='\0'; i++) { - length++; - if (src[i]=='\n' || src[i]=='\0') { - if (line>=start && line <=end) cli_printline(&edit, line, "", src+i-length+1, length); - line++; - length=0; - } - } - - linedit_clear(&edit); +void cli_list(const char *src, int start, int end, clioptions opt) { + clioptions prev = s_cli_activeopt; + s_cli_activeopt |= opt; + cli_foreachsourceline(src, start, end, cli_listline, NULL); + s_cli_activeopt = prev; +} + +/** Look up and display a help topic from the command line. */ +void cli_helpquery(const char *query, clioptions opt) { + cli_applyoptions(opt); + lexer l; + inline_editor *edit = cli_tempeditor(&l); + if (!edit) { + cli_setexitcode(EXIT_FAILURE); + return; } + + error err; + error_init(&err); + bool help = hlp_initialize(); + char *q = query ? (char *) query : (char *) ""; + if (!cli_help(edit, q, &err, help)) cli_setexitcode(EXIT_FAILURE); + hlp_finalize(); + inline_free(edit); } diff --git a/src/cli.h b/src/cli.h index e934143..c0ea5a1 100644 --- a/src/cli.h +++ b/src/cli.h @@ -11,18 +11,20 @@ #include #include -#include "linedit.h" -#include "help.h" +#include "inline.h" +#include "hlp.h" -#include "debugger.h" +#define CLI_DEFAULTCOLOR -1 +#define CLI_ERRORCOLOR INLINE_RED +#define CLI_WARNINGCOLOR INLINE_YELLOW -#define CLI_DEFAULTCOLOR LINEDIT_DEFAULTCOLOR -#define CLI_ERRORCOLOR LINEDIT_RED -#define CLI_WARNINGCOLOR LINEDIT_YELLOW -#define CLI_NOEMPHASIS LINEDIT_NONE +#define CLI_NOEMPHASIS -1 +#define CLI_BOLD 0 +#define CLI_UNDERLINE 1 +#define CLI_ITALIC 2 -#define CLI_PROMPT ">" -#define CLI_CONTINUATIONPROMPT "~" +#define CLI_PROMPT "> " +#define CLI_CONTINUATIONPROMPT "~ " #define CLI_QUIT "quit" #define CLI_HELP "help" #define CLI_SHORT_HELP "?" @@ -33,20 +35,34 @@ #define CLI_DEBUG (1<<3) #define CLI_OPTIMIZE (1<<4) #define CLI_PROFILE (1<<5) +#define CLI_INTERACTIVE (1<<6) +#define CLI_NOCOLOR (1<<7) typedef unsigned int clioptions; extern char *cli_globalsrc; +void cli_setexitcode(int code); +int cli_exitcode(void); +void cli_applyoptions(clioptions opt); +bool cli_usecolor(void); -void cli_displaywithstyle(lineditor *edit, linedit_color col, linedit_emphasis emph, int n, ...); +void cli_displaywithstyle(int col, int emph, int n, ...); +void cli_emitemphasis(int emph); void cli_reporterror(error *err, vm *v); - -void cli_run(const char *in, clioptions opt); -void cli(clioptions opt); +void cli_run(const char *in, clioptions opt, const char *preamble); +void cli_runstring(const char *src, clioptions opt, const char *preamble); +void cli(clioptions opt, const char *preamble); char *cli_loadsource(const char *in); -void cli_disassemblewithsrc(program *p, char *src); -void cli_list(const char *in, int start, int end); +char *cli_loadstdin(void); +void cli_disassemblewithsrc(program *p, char *src, clioptions opt); +void cli_list(const char *in, int start, int end, clioptions opt); +void cli_helpquery(const char *query, clioptions opt); + +void clidebugger_enter(vm *v); +void clidebugger_initialize(void); #endif /* cli_h */ + + diff --git a/src/debugger.c b/src/debugger.c index a82e1d8..063f8a3 100644 --- a/src/debugger.c +++ b/src/debugger.c @@ -4,12 +4,16 @@ * @brief Command line debugger */ +#include +#include + #include #include #include #include #include +#include "cli.h" #include "debugger.h" #ifdef _WIN32 @@ -28,13 +32,13 @@ __declspec(dllimport) extern objecttype objectstringtype; typedef struct { debugger *debug; /** Debugger */ - lineditor *edit; /** lineeditor for output */ + inline_editor *edit; /** Inline editor for output */ error *err; /** Error structure to fill out */ char *info; /** Report any info to the user after error messages */ bool stop; } clidebugger; -void clidebugger_init(clidebugger *debug, vm *v, lineditor *edit, error *err) { +void clidebugger_init(clidebugger *debug, vm *v, inline_editor *edit, error *err) { debug->debug=vm_getdebugger(v); debugger_seterror(debug->debug, err); debug->edit=edit; @@ -49,8 +53,8 @@ void clidebugger_init(clidebugger *debug, vm *v, lineditor *edit, error *err) { /** Display the morpho banner */ void clidebugger_banner(clidebugger *debug) { - cli_displaywithstyle(debug->edit, DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Morpho debugger---\n"); - cli_displaywithstyle(debug->edit, CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "Type '?' or 'h' for help.\n"); + cli_displaywithstyle(DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Morpho debugger---\n"); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "Type '?' or 'h' for help.\n"); morpho_printf(debugger_currentvm(debug->debug), "%s ", (debug->debug->singlestep ? "Single stepping" : "Breakpoint")); debugger_showlocation(debug->debug, debug->debug->iindx); @@ -60,13 +64,13 @@ void clidebugger_banner(clidebugger *debug) { /** Display the resume text */ void clidebugger_resumebanner(clidebugger *debug) { - cli_displaywithstyle(debug->edit, DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Resuming----------\n"); + cli_displaywithstyle(DEBUGGER_COLOR, CLI_NOEMPHASIS, 1, "---Resuming----------\n"); } /** Display an error message */ void clidebugger_reporterror(clidebugger *debug) { if (debug->err->cat!=ERROR_NONE) { - cli_displaywithstyle(debug->edit, DEBUGGER_ERROR_COLOR, CLI_NOEMPHASIS, 3, "Error: ", debug->err->msg, "\n"); + cli_displaywithstyle(DEBUGGER_ERROR_COLOR, CLI_NOEMPHASIS, 3, "Error: ", debug->err->msg, "\n"); } } @@ -89,7 +93,7 @@ void clidebugger_list(clidebugger *debug, int *nlines) { int start = line - n, end = line + n; if (start<0) start = 0; - cli_list(src, start, end); + cli_list(src, start, end, 0); // Use default (color enabled) for debugger if (in) MORPHO_FREE(src); } } @@ -116,7 +120,7 @@ void clidebugger_clearinfo(clidebugger *debug) { /** Show info */ void clidebugger_showinfo(clidebugger *debug) { - if (debug->info) cli_displaywithstyle(debug->edit, CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, debug->info); + if (debug->info) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, debug->info); } /* ********************************************************************** @@ -578,6 +582,38 @@ bool clidebugger_parse(clidebugger *debug, char *in) { return success; } +/* ********************************************************************** + * Debugger autocomplete + * ********************************************************************** */ + +static const char *debugger_commands[] = { + "address", "backtrace", "b", "break", "bt", "c", "clear", "continue", + "d", "disassem", "disassemble", "g", "gc", "global", "globals", "garbage", + "h", "help", "i", "info", "l", "list", "p", "print", "q", "quit", + "reg", "register", "registers", "s", "set", "stack", "step", "t", "trace", "x", + NULL +}; + +/** Autocomplete callback for debugger vocabulary (commands and sub-commands). */ +static const char *clidebugger_complete(const char *in, void *ref, size_t *index) { + (void) ref; + size_t len = strlen(in); + const char *tok = in + len; + while (tok > in && !isspace((unsigned char) *(tok - 1))) tok--; + if (tok >= in + len || iscntrl((unsigned char) *tok)) return NULL; + size_t toklen = strlen(tok); + + for (size_t i = *index; debugger_commands[i] != NULL; i++) { + const char *cmd = debugger_commands[i]; + size_t cmdlen = strlen(cmd); + if (toklen < cmdlen && strncmp(tok, cmd, toklen) == 0) { + *index = i + 1; + return cmd + toklen; + } + } + return NULL; +} + /* ********************************************************************** * Debugger REPL * ********************************************************************** */ @@ -586,17 +622,15 @@ void clidebugger_enter(vm *v) { error err; error_init(&err); - lineditor edit; - linedit_init(&edit); - linedit_setprompt(&edit, DEBUGGER_PROMPT); - + inline_editor *edit = inline_new(DEBUGGER_PROMPT); + inline_autocomplete(edit, clidebugger_complete, NULL); clidebugger debug; - clidebugger_init(&debug, v, &edit, &err); + clidebugger_init(&debug, v, edit, &err); clidebugger_banner(&debug); while (!debug.stop) { clidebugger_clearinfo(&debug); - char *input = linedit(&edit); + char *input = inline_readline(edit); if (!input) break; if (!clidebugger_parse(&debug, input) || @@ -605,16 +639,22 @@ void clidebugger_enter(vm *v) { error_clear(&err); } + if (input) free(input); + clidebugger_showinfo(&debug); } clidebugger_resumebanner(&debug); - linedit_clear(&edit); + inline_free(edit); error_clear(&err); } void clidebugger_initialize(void) { + static bool initialized = false; + if (initialized) return; + initialized = true; + morpho_defineerror(DBG_PRS, ERROR_PARSE, DBG_PRS_MSG); morpho_defineerror(DBG_INFO, ERROR_PARSE, DBG_INFO_MSG); morpho_defineerror(DBG_INVLD, ERROR_PARSE, DBG_INVLD_MSG); diff --git a/src/debugger.h b/src/debugger.h index 60b070a..58079ea 100644 --- a/src/debugger.h +++ b/src/debugger.h @@ -7,12 +7,12 @@ #ifndef debugger_h #define debugger_h -#include "cli.h" +#include "inline.h" -#define DEBUGGER_PROMPT "@>" +#define DEBUGGER_PROMPT "@> " -#define DEBUGGER_COLOR LINEDIT_GREEN -#define DEBUGGER_ERROR_COLOR LINEDIT_RED +#define DEBUGGER_COLOR INLINE_GREEN +#define DEBUGGER_ERROR_COLOR INLINE_RED #define DBG_PRS "DbgPrs" #define DBG_PRS_MSG "Couldn't parse command." diff --git a/src/help.c b/src/hlp.c similarity index 65% rename from src/help.c rename to src/hlp.c index 90a14be..7ad06e2 100644 --- a/src/help.c +++ b/src/hlp.c @@ -1,13 +1,20 @@ -/** @file help.c +/** @file hlp.c * @author T J Atherton * * @brief Interactive help system */ #include +#include #include #include + +#include "cli.h" +#include "hlp.h" + +#ifndef MORPHO_INCLUDE_HELP + #include #include #include @@ -17,8 +24,6 @@ __declspec(dllimport) extern objecttype objectstringtype; __declspec(dllimport) extern objecttype objectlisttype; #endif -#include "help.h" - /** The interactive help system uses a collection of Markdown files, located in * MORPHO_HELPFOLDER, that define available topics. Help files are all * valid Markdown, although only a subset is used, and the help system interprets @@ -157,9 +162,12 @@ objecthelptopic *help_search(char *query) { * Display help * ********************************************************************** */ +bool inline_getterminalwidth(int *width); + /** Display a topic list */ -void help_topiclist(dictionary *dict, lineditor *edit) { - int width = linedit_getwidth(edit), max = 0; +void help_topiclist(dictionary *dict, inline_editor *edit) { + int width = 80, max = 0; + inline_getterminalwidth(&width); objectlist list = MORPHO_STATICLIST; varray_valueinit(&list.val); @@ -202,7 +210,7 @@ void help_topiclist(dictionary *dict, lineditor *edit) { } if (k==ncols-1 || i==list.val.count-1) { varray_charadd(&str, "\n\0", 2); - linedit_displaywithsyntaxcoloring(edit, str.data); + inline_displaywithsyntaxcoloring(edit, str.data); str.count=0; k=0; } } @@ -212,15 +220,15 @@ void help_topiclist(dictionary *dict, lineditor *edit) { } /** Parse a 'show' command */ -static void help_show(objecthelptopic *topic, lineditor *edit, char *command) { +static void help_show(objecthelptopic *topic, inline_editor *edit, char *command) { char *c=command; for (; isspace(*c); c++); if (*c=='(') c++; if (strncmp(c, "topics", 6)==0) { - linedit_displaywithstyle(edit, HELP_TOPICS, LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, HELP_TOPICS); help_topiclist(&helpdict, edit); } else if (strncmp(c, "subtopics", 9)==0) { - linedit_displaywithstyle(edit, HELP_SUBTOPICS, LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, HELP_SUBTOPICS); help_topiclist(&topic->subtopics, edit); } } @@ -241,7 +249,7 @@ static size_t help_parsesegment(char *string, char delim) { } /** Parses a section of inline code */ -static char *help_parseinlinecode(lineditor *edit, char *string) { +static char *help_parseinlinecode(inline_editor *edit, char *string) { char *s = string; if (*s=='`') s++; size_t nchars = help_parsesegment(s, '`'); @@ -251,12 +259,12 @@ static char *help_parseinlinecode(lineditor *edit, char *string) { strncpy(str, string+1, nchars); str[nchars]='\0'; - linedit_displaywithsyntaxcoloring(edit, str); + inline_displaywithsyntaxcoloring(edit, str); return s + nchars; } /** Parses an emphasized section */ -static char *help_parseemph(lineditor *edit, char *string, char delim, linedit_color col, linedit_emphasis emph) { +static char *help_parseemph(char *string, char delim, int col, int emph) { char *s = string; if (*s==delim) s++; size_t nchars = help_parsesegment(s, delim); @@ -266,22 +274,22 @@ static char *help_parseemph(lineditor *edit, char *string, char delim, linedit_c strncpy(str, string+1, nchars); str[nchars]='\0'; - linedit_displaywithstyle(edit, str, col, emph); + cli_displaywithstyle(col, emph, 1, str); return s + nchars; } /** Displays a single line of help text */ -static bool help_displayline(objecthelptopic *topic, lineditor *edit, char *line, bool allowheader) { +static bool help_displayline(objecthelptopic *topic, inline_editor *edit, char *line, bool allowheader) { if (line[0]=='#') { if (allowheader) { char *s = line; while (*s=='#' || isspace(*s)) s++; // Skip leading space - linedit_displaywithstyle(edit, s, LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, s); } else { return true; } } else if (line[0]=='\t' || strncmp(line, " ", 4)==0) { - linedit_displaywithsyntaxcoloring(edit, line); + inline_displaywithsyntaxcoloring(edit, line); } else if (line[0]=='[') { /* Process commands */ if (strncmp(line+1, "show", 4)==0) { @@ -303,9 +311,9 @@ static bool help_displayline(objecthelptopic *topic, lineditor *edit, char *line case '`': next=help_parseinlinecode(edit, c); break; case '*': - next=help_parseemph(edit, c, '*', LINEDIT_DEFAULTCOLOR, LINEDIT_BOLD); break; + next=help_parseemph(c, '*', CLI_DEFAULTCOLOR, CLI_BOLD); break; case '_': - next=help_parseemph(edit, c, '_', LINEDIT_DEFAULTCOLOR, LINEDIT_UNDERLINE); break; + next=help_parseemph(c, '_', CLI_DEFAULTCOLOR, CLI_UNDERLINE); break; default: printf("%c", *c); break; } @@ -324,7 +332,7 @@ static bool help_displayline(objecthelptopic *topic, lineditor *edit, char *line } /** Displays a help topic */ -void help_display(lineditor *edit, objecthelptopic *topic) { +void help_display(inline_editor *edit, objecthelptopic *topic) { FILE *f = (topic ? fopen(topic->file, "r") : NULL); char line[HELP_LINELENGTH]; @@ -485,7 +493,7 @@ bool help_findfiles(void) { /** Initializes the help system * @returns true if help is available */ -bool help_initialize(void) { +bool hlp_initialize(void) { objecthelptopictype=object_addtype(&objecthelptopicdefn); dictionary_init(&helpdict); @@ -494,7 +502,7 @@ bool help_initialize(void) { } /** Finalizes the help system */ -void help_finalize(void) { +void hlp_finalize(void) { while (topics) { objecthelptopic *c = topics; topics = c->next; @@ -503,3 +511,187 @@ void help_finalize(void) { dictionary_freecontents(&helpdict, true, false); dictionary_clear(&helpdict); } + +#else +/** New help display functions */ +#define HLP_BUFFERSIZE 4096 + +/** Length of segment between delimiters (excluding closing delim). Returns 0 if no closer. */ +static size_t hlp_matchdelimiter(const char *s, char delim) { + const char *p; + for (p = s; *p != delim; p++) if (*p == '\0') return 0; + return (size_t)(p - s); +} + +/** Copies span [src, src+n) into buf and null-terminates. Returns true if n < bufsize. */ +static bool hlp_copyspan(const char *src, size_t n, char *buf, size_t bufsize) { + if (n >= bufsize) return false; + memcpy(buf, src, n); + buf[n] = '\0'; + return true; +} + +/** Display one line of paragraph/list text with inline `code`, *emphasis*, **bold**, _underline_. Escapes \* \_ \` \\ output the character literally. */ +static void hlp_displayline(inline_editor *edit, const char *line) { + char buf[HLP_BUFFERSIZE]; + for (const char *c = line; *c != '\0'; ) { + if (*c == '\\' && (c[1] == '*' || c[1] == '_' || c[1] == '`' || c[1] == '\\')) { + putchar(c[1]); + c += 2; + } else if (*c == '`') { + c++; + size_t n = hlp_matchdelimiter(c, '`'); + if (n > 0 && hlp_copyspan(c, n, buf, sizeof(buf))) + inline_displaywithsyntaxcoloring(edit, buf); + c += n + 1; + } else if (*c == '*' && c[1] == '*') { // **bold** + c += 2; + size_t n = hlp_matchdelimiter(c, '*'); + if (n > 0 && c[n] == '*' && c[n + 1] == '*' && hlp_copyspan(c, n, buf, sizeof(buf))) { + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_BOLD, 1, buf); + c += n + 2; + } else { + putchar(*(c - 2)); + putchar(*(c - 1)); + } + } else if (*c == '*' || *c == '_') { // *italic* or _underline_ + char delim = *c; c++; + size_t n = hlp_matchdelimiter(c, delim); + if (n > 0 && hlp_copyspan(c, n, buf, sizeof(buf))) + cli_displaywithstyle(CLI_DEFAULTCOLOR, (delim == '*' ? CLI_ITALIC : CLI_UNDERLINE), 1, buf); + c += n + 1; + } else { + putchar(*c); + c++; + } + } +} + +/** Display block content as lines; if prefix is not NULL, print it before each line. */ +static void hlp_displayblocklines(inline_editor *edit, char *buf, const char *prefix) { + for (char *line = buf; *line; ) { + char *eol = strchr(line, '\n'); + if (eol) *eol = '\0'; + if (prefix) fputs(prefix, stdout); + hlp_displayline(edit, line); + putchar('\n'); + if (!eol) break; + line = eol + 1; + } +} + +/** Display subtopics for a given topic. */ +static void hlp_displaysubtopics(inline_editor *edit, const help_topic *t) { + varray_value subtopics; + varray_valueinit(&subtopics); + morpho_helpsubtopics(t, &subtopics); + if (subtopics.count > 0) hlp_displaytopiclist(edit, &subtopics, HLP_SUBTOPICS_HDR); + varray_valueclear(&subtopics); +} + +/** Display a help_topic to the terminal with highlighting and emphasis. */ +void hlp_displaytopic(inline_editor *edit, const help_topic *t) { + const md_file *file = t->file; + const char *src = (file ? file->source : NULL); + size_t src_len = (file ? file->sourcelen : 0); + if (!src || t->nblocks == 0) return; + + char buf[HLP_BUFFERSIZE]; + for (unsigned int i = 0; i < t->nblocks; i++) { + const md_block *b = &t->content_blocks[i]; + size_t start = b->span.start, len = b->span.length; + if (start >= src_len) continue; + if (start + len > src_len) len = src_len - start; + if (!hlp_copyspan(src + start, len, buf, sizeof(buf))) continue; + + switch (b->type) { + case MD_BLOCK_HEADER: { + const char *title = buf; + while (*title == '#' || (*title == ' ' && title < buf + len)) title++; + if (*title) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, title); + if (len > 0 && buf[len - 1] != '\n') putchar('\n'); + break; + } + case MD_BLOCK_CODE: + inline_displaywithsyntaxcoloring(edit, buf); + if (len > 0 && buf[len - 1] != '\n') putchar('\n'); + break; + case MD_BLOCK_PARAGRAPH: + hlp_displayblocklines(edit, buf, NULL); + break; + case MD_BLOCK_LIST: + hlp_displayblocklines(edit, buf, "* "); + break; + case MD_BLOCK_BLANK: putchar('\n'); break; + case MD_BLOCK_THEMATIC_BREAK: + cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_NOEMPHASIS, 1, "---\n"); + break; + case MD_SHOW_SUBTOPICS: + hlp_displaysubtopics(edit, t); + break; + case MD_BLOCK_LINK_DEF: + default: + break; + } + } +} + +static int hlp_topicname_cmp(const void *a, const void *b) { + const value *va = (const value *) a; + const value *vb = (const value *) b; + if (!MORPHO_ISSTRING(*va) || !MORPHO_ISSTRING(*vb)) return 0; + return strcmp(MORPHO_GETCSTRING(*va), MORPHO_GETCSTRING(*vb)); +} + +/** Display a list of topic names (e.g. from morpho_helptopics) in columns. */ +void hlp_displaytopiclist(inline_editor *edit, varray_value *topics, const char *heading) { + if (!topics || topics->count == 0) return; + if (heading) cli_displaywithstyle(CLI_DEFAULTCOLOR, CLI_UNDERLINE, 1, heading); + int width = 80, max = 0; + inline_getterminalwidth(&width); + + value *data = topics->data; + unsigned int n = topics->count; + qsort(data, n, sizeof(value), hlp_topicname_cmp); + + for (unsigned int i = 0; i < n; i++) { + if (MORPHO_ISSTRING(data[i])) { + int len = (int) MORPHO_GETSTRINGLENGTH(data[i]); + if (len > max) max = len; + } + } + if (max == 0) max = 1; + int ncols = width / (max + 1); + if (ncols < 1) ncols = 1; + bool single = (unsigned int) ncols > n; + + varray_char str; + varray_charinit(&str); + int k = 0; + for (unsigned int i = 0; i < n; i++) { + if (MORPHO_ISSTRING(data[i])) { + char *s = MORPHO_GETCSTRING(data[i]); + int len = (int) MORPHO_GETSTRINGLENGTH(data[i]); + varray_charadd(&str, s, len); + if (single) { + varray_charadd(&str, " ", 2); + } else { + for (int j = len; j < max + 1; j++) varray_charwrite(&str, ' '); + } + k++; + } + if (k == ncols || i == n - 1) { + varray_charwrite(&str, '\n'); + varray_charwrite(&str, '\0'); + if (str.count > 0) inline_displaywithsyntaxcoloring(edit, str.data); + str.count = 0; + k = 0; + } + } + varray_charclear(&str); +} + +bool hlp_initialize(void) { return true; } +void hlp_finalize(void) { } + +#endif diff --git a/src/help.h b/src/hlp.h similarity index 59% rename from src/help.h rename to src/hlp.h index bdc8287..c556ef5 100644 --- a/src/help.h +++ b/src/hlp.h @@ -1,18 +1,20 @@ -/** @file help.h +/** @file hlp.h * @author T J Atherton * * @brief Interactive help system */ -#ifndef help_h -#define help_h +#ifndef hlp_h +#define hlp_h #include #include #include -#include "linedit.h" +#include "inline.h" + +#ifndef MORPHO_INCLUDE_HELP extern objecttype objecthelptopictype; #define OBJECT_HELPTOPIC objecthelptopictype @@ -33,9 +35,21 @@ typedef struct sobjecthelptopic { size_t help_querylength(char *query, char **s); objecthelptopic *help_search(char *query); -void help_display(lineditor *edit, objecthelptopic *topic); +void help_display(inline_editor *edit, objecthelptopic *topic); + +#else +#include + +#define HLP_TOPICS_HDR "Topics:\n" +#define HLP_SUBTOPICS_HDR "Subtopics:\n" + +void hlp_displaytopic(inline_editor *edit, const help_topic *t); + +void hlp_displaytopiclist(inline_editor *edit, varray_value *topics, const char *heading); + +#endif -bool help_initialize(void); -void help_finalize(void); +bool hlp_initialize(void); +void hlp_finalize(void); -#endif /* help_h */ +#endif /* hlp_h */ diff --git a/src/inline.c b/src/inline.c new file mode 100644 index 0000000..9c084dc --- /dev/null +++ b/src/inline.c @@ -0,0 +1,2206 @@ +/** @file inline.c + * @author T J Atherton + * + * @brief A simple grapheme aware line editor with history, completion, multiline editing and syntax highlighting */ + +#include "inline.h" +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include + #include + #include + #define read _read + #define write _write + #define isatty _isatty + #define STDIN_FILENO _fileno(stdin) + #define STDOUT_FILENO _fileno(stdout) +#else + #include + #include + #include + #include + #include + #include +#endif + +#define INLINE_DEFAULT_BUFFER_SIZE 128 +#define INLINE_DEFAULT_PROMPT ">" + +#define INLINE_ESCAPECODE_MAXLENGTH 32 + +#define INLINE_INVALID -1 + +#define INLINE_TAB_WIDTH 2 + +//#define INLINE_NO_SIGNALS // <- Uncomment to disable installation of signals + +#ifdef _WIN32 +typedef DWORD termstate_t; +#else +typedef struct termios termstate_t; +#endif + +/* ********************************************************************** + * Line editor data structures and configuration + * ********************************************************************** */ + +/** Simple list of strings type */ +typedef struct inline_stringlist { + char **items; // List of strings + int count; // Number of strings + int index; // Current index +} inline_stringlist_t; + +/** Viewport */ +typedef struct { + int first_visible_line; // Vertical scroll offset + int first_visible_col; // Horizontal scroll offset + int screen_rows; // Viewport height + int screen_cols; // Viewport width (excludes prompt, which is not part of the viewport) +} inline_viewport; + +/** The editor data structure */ +typedef struct inline_editor { + char *prompt; + char *continuation_prompt; + + int ncols; // Number of columns + + char *buffer; // Buffer holding UTF8 + size_t buffer_len; // Length of contents in bytes + size_t buffer_size; // Size of buffer allocated in bytes + + char *clipboard; // Clipboard buffer + size_t clipboard_len; // Length of contents in bytes + size_t clipboard_size; // Size of clipboard in bytes + + size_t *graphemes; // Offset to each grapheme + int grapheme_count; // Number of graphemes + size_t grapheme_size; // Size of grapheme buffer in bytes + + size_t *lines; // Offset to each line + int line_count; // Number of lines + size_t line_size; // Size of line buffer in bytes + + int cursor_posn; // Position of cursor in graphemes + int selection_posn; // Selection posn in graphemes + int term_cursor_row; // Record the cursor's physical row + int term_lines_drawn; // Record how many lines were previously drawn + + inline_syntaxcolorfn syntax_fn; // Syntax coloring callback + void *syntax_ref; // User reference + + int *palette; // Palette: list of colors + int palette_count; // Length of palette list + + inline_completefn complete_fn; // Autocomplete callback + void *complete_ref; // User reference + + inline_stringlist_t suggestions; // List of suggestions from autocompleter + bool suggestion_shown; // Set if renderer was able to show a suggestion + + inline_multilinefn multiline_fn; // Multiline callback + void *multiline_ref; // User reference + + inline_graphemefn grapheme_fn; // Custom grapheme splitter + inline_widthfn width_fn; // Custom grapheme width function + + inline_stringlist_t history; // List of history entries + int max_history_length; // Maximum length of the history + + inline_viewport viewport; // Terminal viewport + +#ifdef _WIN32 // Preserve terminal state + termstate_t termstate_in; + termstate_t termstate_out; +#else + termstate_t termstate; +#endif + bool rawmode_enabled; // Record if rawmode has already been enabled + + bool refresh; // Set to refresh on next redraw +} inline_editor; + +static inline_editor *inline_lasteditor = NULL; + +// Forward declarations +static char *inline_strdup(const char *s); +static void inline_disablerawmode(inline_editor *edit); +static void inline_stringlist_init(inline_stringlist_t *list); +static void inline_stringlist_clear(inline_stringlist_t *list); +static void inline_recomputelines(inline_editor *edit); +static void inline_recomputegraphemes(inline_editor *edit); +static bool inline_insert(inline_editor *edit, const char *bytes, size_t nbytes); +static void inline_clear(inline_editor *edit); +static void inline_clearselection(inline_editor *edit); +static void inline_clearsuggestions(inline_editor *edit); +static bool inline_stringwidth(inline_editor *edit, const char *str, int *width); + +/* ----------------------- + * New/free API + * ----------------------- */ + +/** API function to create a new line editor */ +inline_editor *inline_new(const char *prompt) { + inline_editor *edit = calloc(1, sizeof(*edit)); // All contents are zero'd + if (!edit) return NULL; + + edit->prompt = inline_strdup(prompt ? prompt : INLINE_DEFAULT_PROMPT); + if (!edit->prompt) goto inline_new_cleanup; + + edit->buffer_size = INLINE_DEFAULT_BUFFER_SIZE; // Allocate initial buffer + edit->buffer = malloc(edit->buffer_size); + if (!edit->buffer) goto inline_new_cleanup; + + edit->buffer[0] = '\0'; // Ensure zero terminated + edit->buffer_len = 0; + + edit->selection_posn = INLINE_INVALID; // No selection + edit->max_history_length = INLINE_INVALID; // Unlimited history + + inline_stringlist_init(&edit->suggestions); + inline_stringlist_init(&edit->history); + + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + + return edit; + +inline_new_cleanup: + inline_free(edit); + return NULL; +} + +/** API function to free a line editor and associated resources */ +void inline_free(inline_editor *edit) { + if (!edit) return; + + free(edit->prompt); + free(edit->continuation_prompt); + + free(edit->buffer); + free(edit->graphemes); + free(edit->lines); + free(edit->clipboard); + + inline_clearsuggestions(edit); + inline_stringlist_clear(&edit->history); + + free(edit->palette); + + if (inline_lasteditor==edit) inline_lasteditor = NULL; + + free(edit); +} + +/* ----------------------- + * Configuration API + * ----------------------- */ + +/** API function to enable syntax coloring */ +void inline_syntaxcolor(inline_editor *edit, inline_syntaxcolorfn fn, void *ref) { + edit->syntax_fn = fn; + edit->syntax_ref = ref; +} + +/** API function to set the color palette */ +bool inline_setpalette(inline_editor *edit, int count, const int *palette) { + free(edit->palette); // Clear any old palette data + edit->palette = NULL; + edit->palette_count = 0; + + if (count <= 0 || palette == NULL) return false; + + edit->palette = malloc(sizeof(int) * count); + if (!edit->palette) return false; + + memcpy(edit->palette, palette, sizeof(int) * count); + edit->palette_count = count; + return true; +} + +/** API function to enable autocomplete */ +void inline_autocomplete(inline_editor *edit, inline_completefn fn, void *ref) { + edit->complete_fn = fn; + edit->complete_ref = ref; +} + +/** API function to enable multiline editing */ +bool inline_multiline(inline_editor *edit, inline_multilinefn fn, void *ref, const char *continuation_prompt) { + edit->multiline_fn = fn; + edit->multiline_ref = ref; + + char *p = inline_strdup(continuation_prompt ? continuation_prompt : edit->prompt); + if (p) { + free(edit->continuation_prompt); + edit->continuation_prompt = p; + } + return (p!=NULL); +} + +/** API function to use a custom grapheme splitter */ +void inline_setgraphemesplitter(inline_editor *edit, inline_graphemefn fn) { + edit->grapheme_fn = fn; +} + +/** API function to use a custom grapheme width function */ +void inline_setgraphemewidth(inline_editor *edit, inline_widthfn fn) { + edit->width_fn = fn; +} + +/* ********************************************************************** + * Platform-dependent code + * ********************************************************************** */ + +/* ---------------------------------------- + * Check terminal features + * ---------------------------------------- */ + +/** API function to check whether stdin and stdout are TTYs. */ +bool inline_checktty(void) { + return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO); +} + +/** API function to check whether stdout is a TTY. */ +bool inline_checkstdouttty(void) { + return isatty(STDOUT_FILENO); +} + +/** Check whether the terminal type is supported. */ +bool inline_checksupported(void) { +#ifndef _WIN32 + const char *term = getenv("TERM"); + if (!term || !*term) return false; + + static const char *deny[] = { "dumb", "cons25", "emacs", NULL }; + + for (const char **p = deny; *p; p++) { + if (strcasecmp(term, *p) == 0) return false; + } +#endif + return true; // Windows and other terminals are supported +} + +/** Read the width from the terminal */ +bool inline_getterminalwidth(int *width) { +#ifdef _WIN32 + HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO csbi; + + if (GetConsoleScreenBufferInfo(h, &csbi)) { + if (width) *width=csbi.srWindow.Right - csbi.srWindow.Left + 1; + return true; + } +#else + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 && ws.ws_col > 0) { + if (width) *width=ws.ws_col; + return true; + } +#endif + return false; +} + +/** Update the terminal width */ +static void inline_updateterminalwidth(inline_editor *edit) { + int width = 80; // fallback + inline_getterminalwidth(&width); + edit->ncols = width; +} + +/** Update viewport width based on current terminal width (preserves viewport position) */ +static void inline_updateviewportwidth(inline_editor *edit) { + int prompt_width; + if (!inline_stringwidth(edit, edit->prompt, &prompt_width)) prompt_width = 0; + edit->viewport.screen_cols = edit->ncols - prompt_width - 1; // Reserve last col to avoid pending wrap state +} + +/* ---------------------------------------- + * Handle crashes + * ---------------------------------------- */ + +static void inline_atexitrestore(void) { + if (inline_lasteditor) inline_disablerawmode(inline_lasteditor); +} + +#ifdef _WIN32 +static bool termstate_set = false; +static termstate_t termstate_in; +static termstate_t termstate_out; +static int resize_pending = 0; +static bool consolehandler_installed = false; +static BOOL WINAPI inline_consolehandler(DWORD ctrl) { + (void) ctrl; + if (termstate_set) { + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleMode(hIn, termstate_in); + SetConsoleMode(hOut, termstate_out); + } + return FALSE; // Allow default behavior +} +#else +termstate_t termstate; +static volatile sig_atomic_t termstate_set = 0; +static volatile sig_atomic_t resize_pending = 0; +typedef struct { + int sig; + void (*handler)(int, siginfo_t *, void *); + int flags; + bool has_previous; + bool installed; + struct sigaction previous; +} signalhandlerstate_t; + +signalhandlerstate_t *inline_findsighandler(int sig); + +static bool inline_callprevious(int sig, siginfo_t *info, void *ucontext) { + signalhandlerstate_t *handler = inline_findsighandler(sig); + if (!handler || !handler->has_previous || !handler->installed || + handler->previous.sa_handler == SIG_IGN || handler->previous.sa_handler == SIG_DFL) return false; + +#ifdef SA_SIGINFO + if (handler->previous.sa_flags & SA_SIGINFO) { + if (handler->previous.sa_sigaction) handler->previous.sa_sigaction(sig, info, ucontext); + return true; + } +#endif + if (handler->previous.sa_handler) { + handler->previous.sa_handler(sig); + return true; + } + return false; +} + +static void inline_restoredisposition(int sig) { + signalhandlerstate_t *handler = inline_findsighandler(sig); + + if (handler && handler->has_previous && handler->previous.sa_handler != SIG_IGN) { + sigaction(sig, &handler->previous, NULL); + } else { + struct sigaction restore; + memset(&restore, 0, sizeof(restore)); + restore.sa_handler = SIG_DFL; + sigemptyset(&restore.sa_mask); + sigaction(sig, &restore, NULL); + } +} + +static void inline_emergencyrestore(void) { + if (termstate_set) tcsetattr(STDIN_FILENO, TCSAFLUSH, &termstate); +} +static void inline_signalwinchhandler(int sig, siginfo_t *info, void *ucontext) { + resize_pending=1; + inline_callprevious(sig, info, ucontext); +} +static void inline_signalgracefulhandler(int sig, siginfo_t *info, void *ucontext) { + inline_emergencyrestore(); + if (inline_callprevious(sig, info, ucontext)) return; // If the previous signal handler was called and returned, we do too + inline_restoredisposition(sig); + kill(getpid(), sig); + _Exit(128 + sig); +} +static void inline_signalcrashhandler(int sig, siginfo_t *info, void *ucontext) { + inline_emergencyrestore(); + inline_restoredisposition(sig); + kill(getpid(), sig); + _Exit(128 + sig); +} + +static signalhandlerstate_t siglist[] = { + { SIGWINCH, inline_signalwinchhandler, SA_SIGINFO | SA_RESTART, false, false, { {0} } }, + { SIGTERM, inline_signalgracefulhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGQUIT, inline_signalgracefulhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGHUP, inline_signalgracefulhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGSEGV, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGABRT, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGBUS, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, + { SIGFPE, inline_signalcrashhandler, SA_SIGINFO, false, false, { {0} } }, +}; + +signalhandlerstate_t *inline_findsighandler(int sig) { + for (size_t i = 0; i < sizeof(siglist)/sizeof(siglist[0]); i++) if (siglist[i].sig == sig) return &siglist[i]; + return NULL; +} +#endif + +static int install_count = 0; + +/** Register emergency exit and signal handlers */ +static void inline_registeremergencyhandlers(void) { + install_count++; + if (install_count>1) return; + + static bool atexit_registered=false; + if (!atexit_registered) { atexit(inline_atexitrestore); atexit_registered=true; } +#ifdef _WIN32 + if (SetConsoleCtrlHandler(inline_consolehandler, TRUE)) consolehandler_installed=true; +#else + #ifndef INLINE_NO_SIGNALS + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + + for (size_t i = 0; i < sizeof(siglist)/sizeof(siglist[0]); i++) { + siglist[i].has_previous = false; + siglist[i].installed = false; + + if (sigaction(siglist[i].sig, NULL, &siglist[i].previous) == 0) { // Get previous action + if (siglist[i].previous.sa_handler == SIG_IGN) continue; // Skip ignored signals + siglist[i].has_previous = true; + } else memset(&siglist[i].previous, 0, sizeof(siglist[i].previous)); // Wipe + + sa.sa_sigaction = siglist[i].handler; + sa.sa_flags = siglist[i].flags; + if (sigaction(siglist[i].sig, &sa, NULL) == 0) siglist[i].installed=true; + } + #endif +#endif +} + +/** Restore emergency handlers previously installed */ +static void inline_restoreemergencyhandlers(void) { + if (install_count>0) install_count--; + if (install_count>0) return; +#ifdef _WIN32 + if (consolehandler_installed) + if (SetConsoleCtrlHandler(inline_consolehandler, FALSE)) consolehandler_installed = false; +#else + #ifndef INLINE_NO_SIGNALS + for (size_t i = 0; i < sizeof(siglist)/sizeof(siglist[0]); i++) { + if (!siglist[i].has_previous || !siglist[i].installed) continue; + sigaction(siglist[i].sig, &siglist[i].previous, NULL); // Restore previous handler + + siglist[i].installed = false; // Wipe + siglist[i].has_previous = false; + memset(&siglist[i].previous, 0, sizeof(siglist[i].previous)); + } + #endif +#endif +} + +/* ---------------------------------------- + * Switch to/from raw mode + * ---------------------------------------- */ + +/** Enable utf8 */ +void inline_setutf8(void) { +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); +#endif +} + +/** Enter raw mode */ +static bool inline_enablerawmode(inline_editor *edit) { + if (edit->rawmode_enabled) return true; + +#ifdef _WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + if (!GetConsoleMode(hIn, &mode)) return false; + edit->termstate_in = mode; + mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); // Disable cooked mode + mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + if (!SetConsoleMode(hIn, mode)) return false; // Disable cooked mode + + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (!GetConsoleMode(hOut, &edit->termstate_out)) return false; + DWORD newOut = edit->termstate_out | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if (!SetConsoleMode(hOut, newOut)) return false; // Enable VT output + + if (!termstate_set) { + termstate_in = edit->termstate_in; + termstate_out = edit->termstate_out; + termstate_set = true; + } +#else + if (tcgetattr(STDIN_FILENO, &edit->termstate) == -1) return false; + + struct termios raw = edit->termstate; + /* Input: Turn off: IXON - software flow control (ctrl-s and ctrl-q) + ICRNL - translate CR into NL (ctrl-m) + BRKINT - parity checking + ISTRIP - strip bit 8 of each input byte */ + raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP); + /* Output: Turn off: OPOST - output processing */ + raw.c_oflag &= ~(OPOST); + /* Character: CS8 Set 8 bits per byte */ + raw.c_cflag |= (CS8); + /* Turn off: ECHO - causes keypresses to be printed immediately + ICANON - canonical mode, reads line by line + IEXTEN - literal (ctrl-v) + ISIG - turn off signals (ctrl-c and ctrl-z) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* Set return condition for control characters */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) return false; + if (!termstate_set) { + termstate = edit->termstate; + termstate_set = true; + } +#endif + inline_lasteditor = edit; // Record last editor + inline_registeremergencyhandlers(); + + edit->rawmode_enabled = true; + return true; +} + +/** Restore terminal state to normal */ +static void inline_disablerawmode(inline_editor *edit) { + if (!edit || !edit->rawmode_enabled) return; + +#ifdef _WIN32 + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleMode(hIn, edit->termstate_in); + SetConsoleMode(hOut, edit->termstate_out); +#else + tcsetattr(STDIN_FILENO, TCSAFLUSH, &edit->termstate); +#endif + + fputs("\r", stdout); // Print a carriage return to ensure we're back on the left hand side + edit->rawmode_enabled = false; + inline_restoreemergencyhandlers(); +} + +/* ********************************************************************** + * Utility functions + * ********************************************************************** */ + +/** Min and max */ +#define imin(a,b) ( ab ? a : b) + +/** Duplicate a string */ +static char *inline_strdup(const char *s) { + if (!s) return NULL; + + size_t n = strlen(s) + 1; + char *p = malloc(n); + if (!p) return NULL; + + memcpy(p, s, n); + return p; +} + +/** Ensure the buffer can grow by at least `extra` bytes. */ +static bool inline_extendbufferby(inline_editor *edit, size_t extra) { + if (extra > SIZE_MAX - edit->buffer_len - 1) return false; // Prevent overflow + size_t required = edit->buffer_len + extra + 1; // +1 for null terminator + + if (required <= edit->buffer_size) return true; // Sufficient space already + + size_t newcap = edit->buffer_size ? edit->buffer_size : INLINE_DEFAULT_BUFFER_SIZE; + while (newcap < required) { + if (newcap > SIZE_MAX / 2) return false; + newcap *= 2; // Grow exponentially + } + + void *p = realloc(edit->buffer, newcap); + if (!p) return false; + edit->buffer = p; + edit->buffer_size = newcap; + + return true; +} + +/* ---------------------------------------- + * Grapheme splitting + * ---------------------------------------- */ + +/** Determine length of utf8 character from the first byte */ +static inline int inline_utf8length(unsigned char b) { + if ((b & 0x80) == 0x00) return 1; // 0xxxxxxx + if ((b & 0xE0) == 0xC0) return 2; // 110xxxxx + if ((b & 0xF0) == 0xE0) return 3; // 1110xxxx + if ((b & 0xF8) == 0xF0) return 4; // 11110xxx + return 0; // Invalid or continuation +} + +/** Decode utf8 into an integer */ +static inline uint32_t inline_utf8decode(const unsigned char *p) { + int len = inline_utf8length(*p); + switch (len) { + case 1: return p[0]; + case 2: return ((p[0] & 0x1F) << 6) | (p[1] & 0x3F); + case 3: return ((p[0] & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F); + case 4: return ((p[0] & 0x07) << 18) | ((p[1] & 0x3F) << 12) | ((p[2] & 0x3F) << 6) | (p[3] & 0x3F); + default: break; + } + return 0; +} + +/** Codepoint definition */ +typedef struct { + const unsigned char *seq; + size_t len; +} codepoint_t; + +#define CODEPOINT(s) { (const unsigned char *) s, sizeof(s)-1 } + +/** Suffix codepoints modify the previous codepoint, but don't join */ +static const codepoint_t suffix_extenders[] = { + CODEPOINT("\xEF\xB8\x8E"), // VS15 (U+FE0E) text presentation + CODEPOINT("\xEF\xB8\x8F"), // VS16 (U+FE0F) emoji presentation + CODEPOINT("\xE2\x83\xA3"), // U+20E3 Keycap combining mark + + // Emoji skin tone modifiers U+1F3FB–U+1F3FF + CODEPOINT("\xF0\x9F\x8F\xBB"), // light skin tone + CODEPOINT("\xF0\x9F\x8F\xBC"), // medium-light skin tone + CODEPOINT("\xF0\x9F\x8F\xBD"), // medium skin tone + CODEPOINT("\xF0\x9F\x8F\xBE"), // medium-dark skin tone + CODEPOINT("\xF0\x9F\x8F\xBF"), // dark skin tone +}; +static const size_t suffix_count = sizeof(suffix_extenders) / sizeof(suffix_extenders[0]); +#undef CODEPOINT + +/** Matches a codepoint against a table of possible matches */ +static size_t inline_matchcodepoint(size_t table_count, const codepoint_t *table, const unsigned char *p, const unsigned char *end) { + size_t remaining = (size_t)(end - p); + for (size_t i = 0; i < table_count; i++) { + const codepoint_t *cp = &table[i]; + if (remaining >= cp->len && memcmp(p, cp->seq, cp->len) == 0) { + return cp->len; + } + } + return 0; // no match +} + +static bool inline_isextendedpictographic(uint32_t cp) { + if (cp >= 0x1F300 && cp <= 0x1FAFF) return true; // Emoji blocks + if (cp >= 0x2600 && cp <= 0x26FF) return true; // Misc symbols + if (cp >= 0x2700 && cp <= 0x27BF) return true; // Dingbats + if (cp == 0x1F3F3 || cp == 0x1F3F4) return true; // White/black flag + return false; +} + +static inline bool inline_isregionalindicator(uint32_t cp) { + return (cp >= 0x1F1E6 && cp <= 0x1F1FF); +} + +/** Heuristic grapheme splitter */ +static size_t inline_graphemesplit(const char *in, const char *end) { + const unsigned char *p = (const unsigned char *)in; + const unsigned char *uend = (const unsigned char *)end; + if (p >= uend) return 0; + + size_t len = inline_utf8length(*p); + if (len == 0 || (size_t)(uend - p) < len) len = 1; + + uint32_t prev_cp = inline_utf8decode(p); // Set basepoint + bool prev_had_vs16 = false; + p += len; + + while (p < uend && *p >= 0xCC && *p <= 0xCF) { // Combining marks + len = inline_utf8length(*p); + if (len == 0 || (size_t)(uend - p) < len) break; + p += len; + } + + while ((len = inline_matchcodepoint(suffix_count, suffix_extenders, p, uend)) != 0) { + if (len == 3 && p[0]==0xEF && p[1]==0xB8 && p[2]==0x8F) prev_had_vs16 = true; // VS16 + p += len; + } + + if (p < uend && inline_isregionalindicator(prev_cp)) { // Regional indicator pair + size_t next_len = inline_utf8length(*p); + if (next_len > 0 && (size_t)(uend - p) >= next_len) { + uint32_t next_cp = inline_utf8decode(p); + if (inline_isregionalindicator(next_cp)) { + p += next_len; + return (size_t)(p - (const unsigned char *)in); + } + } + } + + for (;;) { // ZWJ chains + if ((size_t)(uend - p) < 3 || p[0]!=0xE2 || p[1]!=0x80 || p[2]!=0x8D) break; // ZWJ + p += 3; + if (p >= uend) break; + + size_t next_len = inline_utf8length(*p); + if (next_len == 0 || (size_t)(uend - p) < next_len) break; + + uint32_t next_cp = inline_utf8decode(p); + + if (!(inline_isextendedpictographic(prev_cp) || prev_had_vs16) || + !inline_isextendedpictographic(next_cp)) break; + + prev_cp = next_cp; // consume base + prev_had_vs16 = false; + p += next_len; + + while (p < uend && *p >= 0xCC && *p <= 0xCF) { // Combining mark + next_len = inline_utf8length(*p); + if (next_len == 0 || (size_t)(uend - p) < next_len) break; + p += next_len; + } + + while ((next_len = inline_matchcodepoint(suffix_count, suffix_extenders, p, uend)) != 0) { + if (next_len == 3 && p[0]==0xEF && p[1]==0xB8 && p[2]==0x8F) prev_had_vs16 = true; + p += next_len; + } + } + + return (size_t)(p - (const unsigned char *)in); +} + +/* ---------------------------------------- + * Grapheme buffer + * ---------------------------------------- */ + +/** Compute grapheme locations */ +static void inline_recomputegraphemes(inline_editor *edit) { + size_t needed = edit->buffer_len + 1; // Assume 1 byte per character as a worst case + sentinel + + size_t required_bytes = needed * sizeof(size_t); // Ensure capacity + if (required_bytes > edit->grapheme_size) { + size_t newsize = (edit->grapheme_size ? edit->grapheme_size : INLINE_DEFAULT_BUFFER_SIZE); + while (newsize < required_bytes) { + if (newsize > SIZE_MAX / 2) return; + newsize *= 2; + } + + size_t *new = realloc(edit->graphemes, newsize); + if (!new) { + edit->grapheme_count = 0; + return; + } + + edit->graphemes = new; + edit->grapheme_size = newsize; + } + + // Select splitter + inline_graphemefn fn = (edit->grapheme_fn ? edit->grapheme_fn : inline_graphemesplit); + + size_t count = 0; + const char *p = edit->buffer, *end = edit->buffer + edit->buffer_len; + + while (p < end) { // Walk the buffer and record grapheme boundaries + edit->graphemes[count++] = (size_t)(p - edit->buffer); + size_t len = fn(p, end); + if (len == 0) len = 1; // Malformed grapheme + if (len > (size_t)(end - p)) len = (size_t)(end - p); // Size longer than buffer + p += len; + } + + edit->graphemes[count] = edit->buffer_len; // Ensure last entry points to end of buffer + edit->grapheme_count = (int) count; +} + +/** Finds the start and end of grapheme i in bytes */ +static inline void inline_graphemerange(inline_editor *edit, int i, size_t *start, size_t *end) { + if (i < 0 || i >= edit->grapheme_count) { // Handle out of bounds access (incl. i representing end of line) + if (start) *start = edit->buffer_len; + if (end) *end = edit->buffer_len; + return; + } + + if (start) *start = edit->graphemes[i]; + if (end) *end = edit->graphemes[i+1]; +} + +/** Finds the first grapheme index using binary search whose start byte is >= byte_off */ +static int inline_findgraphemeindex(inline_editor *edit, size_t byte_off) { + int lo = 0, hi = edit->grapheme_count; + + while (lo < hi) { + int mid = (lo + hi) / 2; + size_t mid_off = edit->graphemes[mid]; + + if (mid_off < byte_off) lo = mid + 1; + else hi = mid; + } + + return lo; +} + +/* ---------------------------------------- + * Line buffer + * ---------------------------------------- */ + +/** Compute line locations */ +static void inline_recomputelines(inline_editor *edit) { + int count = 0; + + for (int g = 0; g < edit->grapheme_count; g++) // Count newline graphemes + if (edit->buffer[edit->graphemes[g]] == '\n') count++; + + size_t needed = sizeof(size_t) * (count + 2); // Need count+2 entries: first line + each newline + sentinel + if (needed > edit->line_size) { + size_t *new = realloc(edit->lines, needed); + if (!new) return; + edit->lines = new; + edit->line_size = needed; + } + + int i = 0; + edit->lines[i++] = 0; // First line always starts at 0 + + for (int g = 0; g < edit->grapheme_count; g++) // Subsequent lines start after each newline + if (edit->buffer[edit->graphemes[g]] == '\n') + edit->lines[i++] = edit->graphemes[g] + 1; + + edit->lines[i] = edit->buffer_len; // Sentinel + edit->line_count = i; +} + +/* ---------------------------------------- + * Grapheme display width + * ---------------------------------------- */ + +/** Check for ZWJ, VS16, keycap */ +static bool inline_checkextenders(const unsigned char *g, size_t len) { + for (size_t i = 0; i < len; ) { + uint32_t cp = inline_utf8decode(g + i); + if (cp == 0x200D || cp == 0xFE0E || cp == 0xFE0F || cp == 0x20E3) return true; // ZWJ, VS15, VS16, Keycap + if (cp >= 0x1F3FB && cp <= 0x1F3FF) return true; // Skin tones + if (cp >= 0x1F9B0 && cp <= 0x1F9B3) return true; // Hair modifiers + i += inline_utf8length(g[i]); + } + return false; +} + +/** Predict the display width of a grapheme */ +static int inline_graphemewidth(const char *p, size_t len) { + if (!len) return 0; + const unsigned char *g = (const unsigned char *)p; + uint32_t cp = inline_utf8decode(g); + + if (cp == '\t') return INLINE_TAB_WIDTH; // Tab + if (cp < 0x80) return 1; // ASCII code + if (cp >= 0x0300 && cp <= 0x036F) return 0; // Combining marks + if (cp >= 0xAC00 && cp <= 0xD7A3) return 2; // Hangul syllables + if (cp >= 0xFF01 && cp <= 0xFF60) return 2; // Fullwidth forms + if (cp >= 0xFFE0 && cp <= 0xFFE6) return 2; + if (inline_isregionalindicator(cp) && // Regional indicator sequence + len > 4 && inline_isregionalindicator(inline_utf8decode(g + inline_utf8length(*g)))) return 2; + if (inline_checkextenders(g, len)) return 2; // Emoji extenders (ZWJ, VS16, skin tones, etc.) + if (cp >= 0x1F300 && cp <= 0x1FAFF) return 2; // Emoji + if (cp >= 0x4E00 && cp <= 0x9FFF) return 2; // CJK Unified Ideographs + return 1; // Anything else +} + + +/** Calculate the display width of a utf8 string using current grapheme splitter/width estimator */ +static bool inline_stringwidth(inline_editor *edit, const char *str, int *width) { + inline_graphemefn split_fn = (edit->grapheme_fn ? edit->grapheme_fn : inline_graphemesplit); + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + + const char *p = str, *end = str + strlen(str); + *width = 0; + + while (p < end) { + size_t glen = split_fn(p, end); + if (glen == 0) return false; // Malformed utf8 codepoint + *width += width_fn(p, glen); + p += glen; + } + return true; +} + +/** Compute terminal width of grapheme range [g_start, g_end) */ +static inline int inline_graphemerangewidth(inline_editor *edit, int g_start, int g_end) { + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + int width = 0; + for (int g = g_start; g < g_end; g++) { + size_t s, e; + inline_graphemerange(edit, g, &s, &e); + width += width_fn(edit->buffer + s, e - s); + } + return width; +} + +/* ---------------------------------------- + * String lists + * ---------------------------------------- */ + +/** Initialize a stringlist structure */ +static void inline_stringlist_init(inline_stringlist_t *list) { + list->items=NULL; + list->count=0; + list->index=INLINE_INVALID; +} + +/** Add an entry to a stringlist */ +static bool inline_stringlist_add(inline_stringlist_t *list, const char *s) { + if (!s) return false; // Never add a null pointer + char *copy = inline_strdup(s); + if (!copy) return false; + char **newitems = realloc(list->items, sizeof(char*) * (list->count + 1)); + if (!newitems) { free(copy); return false; } // Don't update if realloc fails + + list->items = newitems; + list->items[list->count] = copy; + list->count++; + return true; +} + +/** Removes and frees the first element of the stringlist; */ +static void inline_stringlist_popfront(inline_stringlist_t *list) { + if (list->count == 0) return; + free(list->items[0]); + + // Shift pointers down safely (overlapping copy) + if (list->count > 1) memmove(list->items, list->items + 1, sizeof(char*) * (list->count - 1)); + list->count--; +} + +/** Clear a stringlist */ +static void inline_stringlist_clear(inline_stringlist_t *list) { + if (list->items) { + for (int i = 0; i < list->count; i++) free(list->items[i]); + free(list->items); + } + + inline_stringlist_init(list); +} + +/** Get the current string in a stringlist */ +static int inline_stringlist_count(inline_stringlist_t *list) { + return list->count; +} + +/** Get the current string in a stringlist */ +static const char *inline_stringlist_current(inline_stringlist_t *list) { + if (list->count == 0 || list->index<0 || list->index >= list->count) return NULL; + return list->items[list->index]; +} + +/** Advance the current index by delta with optional wrapping */ +static void inline_stringlist_advance(inline_stringlist_t *list, int delta, bool wrap) { + if (list->count == 0 || list->index<0) return; + if (list->index >= list->count) list->index = list->count - 1; // Clamp + if (wrap) { + list->index = (list->index + delta + list->count) % list->count; + } else { + list->index = list->index + delta; + if (list->index<0) list->index=0; + if (list->index>=list->count) list->index=list->count-1; + } +} + +/* ---------------------------------------- + * Selections + * ---------------------------------------- */ + +/** Find the start and end points of a selection, if one is present. */ +static bool inline_selectionrange(inline_editor *edit, int *sel_l, int *sel_r, size_t *start, size_t *end) { + if (edit->selection_posn == INLINE_INVALID) return false; + + int l = imin(edit->selection_posn, edit->cursor_posn); + int r = imax(edit->selection_posn, edit->cursor_posn); + + if (sel_l) *sel_l = l; + if (sel_r) *sel_r = r; + if (start) inline_graphemerange(edit, l, start, NULL); + if (end) inline_graphemerange(edit, r, end, NULL); + + return true; +} + +/* ---------------------------------------- + * Clipboard + * ---------------------------------------- */ + +/** Copies a string of given length onto the clipboard. */ +static bool inline_copytoclipboard(inline_editor *edit, const char *string, size_t length) { + if (!string || length==0) { // Empty clipboard + if (edit->clipboard) edit->clipboard[0] = '\0'; + edit->clipboard_len = 0; + return true; + } + + size_t needed = length + 1; // Check if we have sufficient capacity and realloc if necessary + if (needed > edit->clipboard_size) { + size_t newsize = edit->clipboard_size ? edit->clipboard_size : INLINE_DEFAULT_BUFFER_SIZE; + while (newsize < needed) { + if (newsize > SIZE_MAX / 2) return false; + newsize *= 2; + } + + char *newbuf = realloc(edit->clipboard, newsize); + if (!newbuf) return false; // Leave clipboard unchanged on allocation failure + + edit->clipboard = newbuf; + edit->clipboard_size = newsize; + } + + memmove(edit->clipboard, string, length); // Copy onto clipboard + edit->clipboard[length] = '\0'; // Ensure null termination + edit->clipboard_len = length; + return true; +} + +/* ---------------------------------------- + * Autocomplete / Suggestions + * ---------------------------------------- */ + +/** Check if the cursor is at the end of the buffer */ +static bool inline_atend(inline_editor *edit) { + return edit->cursor_posn == edit->grapheme_count; +} + +/** Adds a suggestion to the suggestion list */ +static void inline_addsuggestion(inline_editor *edit, const char *s) { + inline_stringlist_add(&edit->suggestions, s); +} + +/** Clears the suggestion list */ +static void inline_clearsuggestions(inline_editor *edit) { + inline_stringlist_clear(&edit->suggestions); +} + +/** Generates suggestions by repeatedly calling the completion callback */ +static void inline_generatesuggestions(inline_editor *edit) { + if (!edit->complete_fn) return; + inline_clearsuggestions(edit); + if (edit->selection_posn!=INLINE_INVALID) return; // Enforce that suggestions cannot be generated while a selection is active + + if (edit->buffer && inline_atend(edit)) { + size_t index = 0; + const char *s; + while ((s=edit->complete_fn(edit->buffer, edit->complete_ref, &index))!=0) { + inline_addsuggestion(edit, s); + } + if (edit->suggestions.count > 0) edit->suggestions.index = 0; + } +} + +/** Check if suggestions are available */ +static bool inline_havesuggestions(inline_editor *edit) { + return inline_stringlist_count(&edit->suggestions) > 0; +} + +/** Returns the current suggestion */ +static const char *inline_currentsuggestion(inline_editor *edit) { + if (!inline_havesuggestions(edit)) return NULL; + return inline_stringlist_current(&edit->suggestions); +} + +/** Advance through the suggestions by delta (can be negative; we wrap around) */ +static void inline_advancesuggestions(inline_editor *edit, int delta) { + inline_stringlist_advance(&edit->suggestions, delta, true); +} + +/* ---------------------------------------- + * History + * ---------------------------------------- */ + +/** Set the history length. */ +void inline_sethistorylength(inline_editor *edit, int maxlen) { + edit->max_history_length=maxlen; + + if (maxlen > 0) { // Remove excess entries if necessary + while (edit->history.count > maxlen) inline_stringlist_popfront(&edit->history); + } else if (maxlen == 0) { // Clear history entirely + inline_stringlist_clear(&edit->history); + } +} + +/** Adds an entry to the history list */ +bool inline_addhistory(inline_editor *edit, const char *entry) { + if (!entry || !*entry || !edit->max_history_length) return false; // Skip empty buffers + + if (edit->history.count > 0) { // Avoid duplicate consecutive entries + const char *last = edit->history.items[edit->history.count - 1]; + if (strcmp(last, entry) == 0) return false; + } + + inline_stringlist_add(&edit->history, entry); + if (edit->max_history_length > 0 && edit->history.count > edit->max_history_length) inline_stringlist_popfront(&edit->history); + return true; +} + +/** Advances the current history */ +static void inline_advancehistory(inline_editor *edit, int delta) { + int count = inline_stringlist_count(&edit->history); + if (count == 0) return; + + // Enter history mode if we're not in it + if (edit->history.index == INLINE_INVALID) edit->history.index = count - 1; + else inline_stringlist_advance(&edit->history, delta, false); // No wrap + + // Load entry or exit history mode + const char *s = inline_stringlist_current(&edit->history); + inline_clear(edit); + if (s) { + inline_insert(edit, s, strlen(s)); + } else edit->history.index = INLINE_INVALID; // Exit history mode +} + +/** End browsing */ +static void inline_endhistorybrowsing(inline_editor *edit) { + edit->history.index = INLINE_INVALID; +} + +/* ---------------------------------------- + * Reset + * ---------------------------------------- */ + +/** Resets the editor before a new session */ +static void inline_reset(inline_editor *edit) { + inline_clear(edit); + inline_clearselection(edit); + inline_endhistorybrowsing(edit); + inline_stringlist_clear(&edit->suggestions); + edit->rawmode_enabled = false; + edit->term_cursor_row = 0; + edit->term_lines_drawn = 0; +} + +/* ---------------------------------------- + * Viewport + * ---------------------------------------- */ + +/** Initialize the viewport */ +static void inline_initviewport(inline_editor *edit) { + edit->viewport.first_visible_line = 0; + edit->viewport.first_visible_col = 0; + edit->viewport.screen_rows = 1; // Will adjust for multiline editing later + inline_updateviewportwidth(edit); +} + +/** Compute logical cursor position in rows and columns */ +static void inline_cursorposn(inline_editor *edit, int *out_row, int *out_col) { + size_t byte_pos = edit->graphemes[edit->cursor_posn]; // byte offset of cursor + + int row = 0; // Find the row containing the cursor + while (row + 1 < edit->line_count && edit->lines[row + 1] <= byte_pos) row++; + + if (out_row) *out_row = row; + // The column is found by subtracting the grapheme offset of the start of the row + if (out_col) *out_col = edit->cursor_posn - inline_findgraphemeindex(edit, edit->lines[row]); +} + +/** Check the cursor is visible */ +static void inline_ensurecursorvisible(inline_editor *edit) { + int cursor_row, cursor_col; + inline_cursorposn(edit, &cursor_row, &cursor_col); + + int line_start_g = inline_findgraphemeindex(edit, edit->lines[cursor_row]); + int cursor_g = line_start_g + cursor_col; + + int cursor_term_col = inline_graphemerangewidth(edit, line_start_g, cursor_g); + + int first = edit->viewport.first_visible_col; + int end = first + edit->viewport.screen_cols; // exclusive + + if (cursor_term_col < first) { + edit->viewport.first_visible_col = cursor_term_col; + + } else if (cursor_term_col >= end) { + edit->viewport.first_visible_col = + cursor_term_col - edit->viewport.screen_cols; + } +} + +/* ********************************************************************** + * Rendering + * ********************************************************************** */ + +#define TERM_RESETCOLOR "\x1b[0m" +#define TERM_CLEAR "\x1b[K" +#define TERM_RESETFOREGROUND "\x1b[39m" +#define TERM_HIDECURSOR "\x1b[?25l" +#define TERM_SHOWCURSOR "\x1b[?25h" +#define TERM_FAINT "\x1b[2m" +#define TERM_INVERSEVIDEO "\x1b[7m" + +/** Write bytes to stdout (return value checked to silence warn_unused_result). */ +static void inline_write(const void *buf, size_t n) { +#ifdef _WIN32 + (void) write(STDOUT_FILENO, buf, (unsigned int) n); +#else + if (write(STDOUT_FILENO, buf, n) < 0) { /* ignore */ } +#endif +} + +/** Write an escape sequence to the terminal */ +void inline_emit(const char *seq) { + inline_write(seq, strlen(seq)); +} + +/** Writes an escape sequence to produce a given color */ +void inline_emitcolor(int color) { + if (color < 0) return; // default + char seq[INLINE_ESCAPECODE_MAXLENGTH]; + int n = 0; + + if (color < 16) { // ANSI 8 or bright 8 + int base = (color < 8 ? 30 : 90); + n = snprintf(seq, sizeof(seq), "\x1b[%dm", base + (color & 7)); + } else if (color <= 255) { // 256-color palette 8–255 + n = snprintf(seq, sizeof(seq), "\x1b[38;5;%dm", color); + } else { // Assume RGB packed as 0x01RRGGBB + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color >> 0) & 0xFF; + n = snprintf(seq, sizeof(seq), "\x1b[38;2;%d;%d;%dm", r, g, b); + } + + if (n > 0) inline_write(seq, (size_t) n); +} + +/** Clip grapheme range [*g_start, *g_end) horizontally based on viewport */ +static inline void inline_clipgraphemerange(inline_editor *edit, int line_start, int *g_start, int *g_end) { + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + + int start_col = edit->viewport.first_visible_col; + int end_col = start_col + edit->viewport.screen_cols; + + int col = inline_graphemerangewidth(edit, line_start, *g_start); + int start = -1; + int end = *g_start; + + for (int i = *g_start; i < *g_end; i++) { + size_t s, e; + inline_graphemerange(edit, i, &s, &e); + int w = width_fn(edit->buffer + s, e - s); + + if ((col >= start_col) && (col < end_col)) { + if (start < 0) start = i; // First visible grapheme + end = i + 1; // extend visible range + } + + if (col + w > end_col) break; + col += w; + } + + if (start < 0) start = *g_end; // Clamp if line is empty or viewport is beyond end + if (end < start) end = start; + else if (end > start && edit->buffer[edit->graphemes[end-1]] == '\n') end--; + + *g_start = start; + *g_end = end; +} + +/** Move terminal cursor to the editor's origin */ +static inline void inline_movetoorigin(inline_editor *edit) { + inline_write("\r", 1); // Move to start of current line + + if (edit->term_cursor_row > 0) { // Move up cursor_row lines + char seq[INLINE_ESCAPECODE_MAXLENGTH]; + int n = snprintf(seq, sizeof(seq), "\x1b[%dA", edit->term_cursor_row); + if (n > 0) inline_write(seq, (size_t) n); + } +} + +/** Move the cursor by a specified delta; down is positive dy */ +static inline void inline_moveby(int dx, int dy) { + char seq[INLINE_ESCAPECODE_MAXLENGTH]; + + if (dy<0) { // Up + int n = snprintf(seq, sizeof(seq), "\x1b[%dA", abs(dy)); + if (n > 0) inline_write(seq, (size_t) n); + } else { + for (int i = 0; i < dy; i++) inline_emit("\n"); // Ensure scroll + } + + if (dx!=0) { // Horizontal + int n = snprintf(seq, sizeof(seq), "\x1b[%d%c", abs(dx), (dx < 0 ? 'D' : 'C')); + if (n > 0) inline_write(seq, (size_t) n); + } +} + +/** Render a single line of text + * @param[in] - edit - the editor + * @param[in] - prompt - prompt for this line + * @param[in] - byte_start - byte offset for the start of the line + * @param[in] - byte_end - byte offset for the end of the line + * @param[in] - logical_cursor_col - column the cursor should be displayed in logical coordinates, or -1 if not on this line + * @param[in] - is_last - whether this is the last line + * @param[out] - rendered_cursor_col - if logical_cursor_col indicates the cursor is on this line, + * set to logical column the cursor should be rendered on, incuding clipping + * and prompt widt, or -1 if outside clipping window; otherwise not changed. */ +static void inline_renderline(inline_editor *edit, const char *prompt, size_t byte_start, size_t byte_end, + int logical_cursor_col, bool is_last, int *rendered_cursor_col) { + inline_write(prompt, strlen(prompt)); // Write prompt + int prompt_width = 0; // Calculate its display width + if (!inline_stringwidth(edit, prompt, &prompt_width)) prompt_width = 0; + + int rendered_width = prompt_width; // Track rendered width + int rendered_cursor_posn = -1; + + // Compute selection bounds, if active + int sel_l = INLINE_INVALID, sel_r = INLINE_INVALID; + if (edit->selection_posn != INLINE_INVALID) { + sel_l = imin(edit->selection_posn, edit->cursor_posn); + sel_r = imax(edit->selection_posn, edit->cursor_posn); + } + + // Compute grapheme range for this line; remember the true start of the line + int line_start = inline_findgraphemeindex(edit, byte_start); + int g_start = line_start, g_end = inline_findgraphemeindex(edit, byte_end); + + // Apply horizontal clipping + inline_clipgraphemerange(edit, line_start, &g_start, &g_end); + + int current_color = -1; + bool selection_on = false; // Track the terminal inverse video state + + // Render syntax-colored, clipped graphemes + int g = g_start; + size_t off = edit->graphemes[g_start]; + + inline_syntaxcolorfn syntax_fn = (edit->palette_count>0 ? edit->syntax_fn : NULL); + inline_widthfn width_fn = (edit->width_fn ? edit->width_fn : inline_graphemewidth); + + // Render syntax-colored, clipped graphemes + while (g < g_end && off < byte_end) { + // Compute color span from current point + inline_colorspan_t span = { .byte_end = off + 1, .color = 0 }; + bool ok=false; + if (syntax_fn) ok=syntax_fn(edit->buffer, edit->syntax_ref, off, &span); + if (!ok || span.byte_end <= off) span.byte_end = byte_end; // treat rest of line as uncolored + + int span_color = (span.color>=0 && span.color < edit->palette_count ? edit->palette[span.color] : -1); + + // Change color only if needed + if (span_color != current_color) { + if (current_color != -1) { + inline_emit(TERM_RESETCOLOR); + selection_on = false; + } + if (span_color >= 0) inline_emitcolor(span_color); + current_color = span_color; + } + + // Print graphemes until we reach span.byte_end (clipped) + for (; g < g_end; g++) { + size_t gs, ge; + inline_graphemerange(edit, g, &gs, &ge); + + if (gs >= span.byte_end) break; + + bool in_selection = (g >= sel_l && g < sel_r); // Are we in a selection? + if (in_selection != selection_on) { // Does terminal state match? + if (in_selection) inline_emit(TERM_INVERSEVIDEO); // Start reverse video + else { + inline_emit(TERM_RESETCOLOR); + if (current_color >= 0) inline_emitcolor(current_color); // Reapply syntax color + } + selection_on = in_selection; + } + + if (edit->buffer[gs] == '\n') break; + + if (logical_cursor_col >= 0 && // Check if this grapheme was where the cursor is + line_start + logical_cursor_col == g) rendered_cursor_posn = rendered_width; + + if (edit->buffer[gs] == '\t') { + for (int i=0; ibuffer + gs, ge - gs); + rendered_width += width_fn(edit->buffer + gs, ge - gs); + } + + off = span.byte_end; + } + + if (selection_on || current_color != -1) inline_emit(TERM_RESETCOLOR); + + // Ghosted suggestion suffix (only if at right edge on last line) + if (is_last && g_end == edit->grapheme_count && logical_cursor_col >= 0) { + const char *suffix = inline_currentsuggestion(edit); + edit->suggestion_shown=false; + if (suffix && *suffix) { + int remaining_cols = edit->viewport.screen_cols - rendered_width; + + // Width of suggestion + int ghost_width = 0; + if (!inline_stringwidth(edit, suffix, &ghost_width)) ghost_width = 0; + + if (ghost_width <= remaining_cols) { // Show suggestion as faint text + edit->suggestion_shown=true; + inline_emit(TERM_FAINT); + inline_write(suffix, strlen(suffix)); + inline_emit(TERM_RESETCOLOR); + } + } + } + + if (logical_cursor_col >= 0 && rendered_cursor_col) { // Update cursor position if on this line + if (rendered_cursor_posn >= 0) *rendered_cursor_col = rendered_cursor_posn; + else *rendered_cursor_col = rendered_width; // cursor at end + } + + if (rendered_width < edit->viewport.screen_cols) inline_emit(TERM_CLEAR); // Clear to end of line +} + +/** Redraw the entire buffer in multiline mode */ +static void inline_redraw(inline_editor *edit) { + inline_emit(TERM_HIDECURSOR); // Prevent flickering + inline_movetoorigin(edit); + + int cursor_row, cursor_col; // Compute logical cursor column and row (pre-clipping) + inline_cursorposn(edit, &cursor_row, &cursor_col); + + int rendered_cursor_col = -1; // To be filled out by inline_renderline + for (int i = 0; i < edit->line_count; i++) { + size_t byte_start = edit->lines[i]; // Render lines + size_t byte_end = edit->lines[i+1]; + bool is_last = (i == edit->line_count - 1); + + inline_emit("\r"); // Move cursor to start of line + + inline_renderline(edit, (i==0 ? edit->prompt : edit->continuation_prompt), // prompt + byte_start, byte_end, + (cursor_row == i ? cursor_col : -1), // cursor column if on this line + is_last, // whether we're on the last line or not + &rendered_cursor_col ); + + if (i + 1 < edit->line_count) inline_emit("\n"); // Move to next line if not at end + } + + int extra = (edit->term_lines_drawn > edit->line_count ? edit->term_lines_drawn - edit->line_count : 0); + for (int i = 0; i < extra; i++) { + inline_emit("\n\r"); + inline_emit(TERM_CLEAR); + } + + inline_write("\r", 1); // Move to start of line + inline_moveby(rendered_cursor_col, cursor_row - edit->line_count - extra + 1); + edit->term_cursor_row = cursor_row; // Record cursor row + edit->term_lines_drawn = edit->line_count; // Record no. of lines drawn + inline_emit(TERM_SHOWCURSOR); +} + +/** API function to print a syntax colored string */ +void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string) { + if (!edit || !string) return; + fflush(stdout); + size_t len = strlen(string); + + if (!edit->syntax_fn || !edit->palette_count) { // Syntax highlighting not configured, fallback to plain + inline_write(string, len); + return; + } + + inline_reset(edit); + edit->viewport.screen_cols=INT_MAX; + inline_insert(edit, string, len); + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + + for (int i = 0; i < edit->line_count; i++) { + size_t byte_start = edit->lines[i], byte_end = edit->lines[i+1]; + bool is_last = (i == edit->line_count - 1); + inline_renderline(edit, "", byte_start, byte_end, -1, is_last, NULL ); + if (i + 1 < edit->line_count) inline_emit("\n\r"); // Move to next line if not at end + } + + inline_clear(edit); + fflush(stdout); +} + +/* ********************************************************************** + * Keypress decoding + * ********************************************************************** */ + +/* ---------------------------------------- + * Raw input layer + * ---------------------------------------- */ + +/** Type that represents a single unit of input */ +typedef unsigned char rawinput_t; + +#ifdef _WIN32 +static bool inline_readkeyevent(KEY_EVENT_RECORD *k) { + INPUT_RECORD rec; + DWORD nread; + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + + for (;;) { + if (!ReadConsoleInputW(hIn, &rec, 1, &nread)) return false; + + if (rec.EventType == WINDOW_BUFFER_SIZE_EVENT) { + resize_pending = 1; + if (inline_lasteditor) inline_lasteditor->refresh = true; + continue; + } + + if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown) { + *k = rec.Event.KeyEvent; + return true; + } + } +} + +/** Helper to emit an escape sequence */ +static int _emitstr(const char *s, unsigned char out[8]) { + int n = 0; + while (s[n] && n < 8) out[n] = (unsigned char)s[n++]; + return n; +} + +/** Mapping from Windows VK codes to POSIX escape sequences */ +typedef struct { + WORD vk; + const char *seq; +} vkmap_t; + +static const vkmap_t vk_table[] = { + { VK_RETURN, "\n" }, + { VK_BACK, "\b" }, + { VK_DELETE, "\x7f" }, + { VK_UP, "\x1b[A" }, + { VK_DOWN, "\x1b[B" }, + { VK_RIGHT, "\x1b[C" }, + { VK_LEFT, "\x1b[D" }, + { VK_HOME, "\x1b[H" }, + { VK_END, "\x1b[F" }, + { VK_PRIOR, "\x1b[5~" }, // Page Up + { VK_NEXT, "\x1b[6~" }, // Page Down +}; + +/** Convert windows keypress event to POSIX */ +static int inline_translatekeypress(const KEY_EVENT_RECORD *k, unsigned char out[8]) { + WORD vk = k->wVirtualKeyCode; + WCHAR wc = k->uChar.UnicodeChar; + DWORD mods = k->dwControlKeyState; + + // Shift-arrows + if ((mods & SHIFT_PRESSED) && (vk == VK_LEFT || vk == VK_RIGHT)) { + return _emitstr( (vk == VK_LEFT ? "\x1b[1;2D" : "\x1b[1;2C"), out ); + } + + // Search table of mappings + for (size_t i = 0; i < sizeof(vk_table)/sizeof(vk_table[0]); i++) { + if (vk_table[i].vk == vk) return _emitstr(vk_table[i].seq, out); + } + + // Ctrl + char + if (mods & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) { + if (vk >= 'A' && vk <= 'Z') { + out[0] = (unsigned char) (vk - 'A') + 1; + return 1; + } + } + + // Alt characters + int i=0; + if (mods & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) { + out[i++] = '\x1b'; // Prefix character with esc + } + + if (wc != 0) { // Unicode + if (wc < 0x80) { + out[i++] = (unsigned char)wc; + } else if (wc < 0x800) { + out[i++] = (unsigned char)(0xC0 | ((unsigned int)wc >> 6)); + out[i++] = (unsigned char)(0x80 | ((unsigned int)wc & 0x3F)); + } else if (wc < 0xD800 || wc > 0xDFFF) { + out[i++] = 0xE0 | (wc >> 12); + out[i++] = 0x80 | ((wc >> 6) & 0x3F); + out[i++] = 0x80 | (wc & 0x3F); + } else if (wc >= 0xD800 && wc <= 0xDBFF) { // high surrogate + // Need the next KEY_EVENT for the low surrogate + KEY_EVENT_RECORD next; + if (!inline_readkeyevent(&next)) return 0; + + WCHAR wc2 = next.uChar.UnicodeChar; + if (wc2 >= 0xDC00 && wc2 <= 0xDFFF) { + uint32_t cp = 0x10000 + (((wc - 0xD800) << 10) | (wc2 - 0xDC00)); + out[i++] = (unsigned char) (0xF0 | (cp >> 18)); + out[i++] = (unsigned char) (0x80 | ((cp >> 12) & 0x3F)); + out[i++] = (unsigned char) (0x80 | ((cp >> 6) & 0x3F)); + out[i++] = (unsigned char) (0x80 | (cp & 0x3F)); + } + } + } + + return i; // Return +} + +#endif + +/** Await a single raw unit of input and store in a rawinput_t */ +static bool inline_readraw(rawinput_t *out) { +#ifdef _WIN32 + static unsigned char buf[16]; // local ring buffer + static int len = 0; + static int pos = 0; + + if (pos < len) { // Return remaining bytes + *out = buf[pos++]; + return true; + } + + KEY_EVENT_RECORD k; // Get new key event + do { + if (!inline_readkeyevent(&k)) return false; + len = inline_translatekeypress(&k, buf); // Translate it to POSIX + pos = 0; + } while (len == 0); + + *out = buf[pos++]; // Return first byte + return true; +#else + int n = (int) read(STDIN_FILENO, out, 1); + return n == 1; +#endif +} + +/* ---------------------------------------- + * Keypress decoding layer + * ---------------------------------------- */ + +/** Identifies the type of keypress */ +typedef enum { + KEY_UNKNOWN, KEY_CHARACTER, + KEY_RETURN, KEY_CTRL_RETURN, KEY_TAB, KEY_SHIFT_TAB, KEY_DELETE, + KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, // Arrow keys + KEY_HOME, KEY_END, // Home and End + KEY_PAGE_UP, KEY_PAGE_DOWN, // Page up and page down + KEY_SHIFT_LEFT, KEY_SHIFT_RIGHT, // Shift+arrow key + KEY_CTRL, KEY_ALT // Ctrl, meta keys +} keytype_t; + +/** A single keypress event obtained and processed by the terminal */ +typedef struct { + keytype_t type; /** Type of keypress */ + unsigned char c[5]; /** Up to four bytes of utf8 encoded unicode plus null terminator */ + int nbytes; /** Number of bytes */ +} keypress_t; + +static void inline_keypressunknown(keypress_t *keypress) { + keypress->type=KEY_UNKNOWN; + keypress->c[0]='\0'; + keypress->nbytes=0; +} + +static void inline_keypresswithchar(keypress_t *keypress, keytype_t type, char c) { + keypress->type=type; + keypress->c[0]=c; keypress->c[1]='\0'; + keypress->nbytes=1; +} + +/** Decode sequence of characters into a utf8 character */ +static void inline_decodeutf8input(unsigned char first, keypress_t *out) { + out->nbytes = inline_utf8length(first); + + if (!out->nbytes) return; // Invalid first byte or stray continuation + + out->c[0] = first; + for (int i=1; inbytes; i++) { + if (!inline_readraw(&out->c[i])) { out->c[i] = '\0'; return; } + } + + out->c[out->nbytes] = '\0'; + out->type = KEY_CHARACTER; +} + +/** Map from terminal codes to keytype_t */ +typedef struct { + const char *seq; + keytype_t type; +} escmap_t; + +static const escmap_t esc_table[] = { + { "[A", KEY_UP }, + { "[B", KEY_DOWN }, + { "[C", KEY_RIGHT }, + { "[D", KEY_LEFT }, + { "[H", KEY_HOME }, + { "[F", KEY_END }, + { "[Z", KEY_SHIFT_TAB }, + { "[5~", KEY_PAGE_UP }, + { "[6~", KEY_PAGE_DOWN }, + { "[1;2C", KEY_SHIFT_RIGHT }, + { "[1;2D", KEY_SHIFT_LEFT }, +}; + +static void inline_decode_escape(keypress_t *out) { + unsigned char seq[INLINE_ESCAPECODE_MAXLENGTH+1]; + int i = 0; + out->type = KEY_UNKNOWN; + + if (!inline_readraw(&seq[i])) return; // Read byte after esc + + if (seq[0] !='[') { // Is this an alt + char combo? + inline_decodeutf8input(seq[0], out); + out->type=KEY_ALT; // Override type + return; + } + + // It's an escape code, so read until alpha terminator + for (i = 1; i < INLINE_ESCAPECODE_MAXLENGTH - 1; i++) { + if (!inline_readraw(&seq[i])) break; + if (isalpha(seq[i]) || seq[i] == '~') break; + } + seq[i + 1] = '\0'; // Ensure null terminated + + // Lookup escape code + for (size_t j = 0; j < sizeof(esc_table)/sizeof(esc_table[0]); j++) { + if (strcmp((const char *)seq, esc_table[j].seq) == 0) { + out->type = esc_table[j].type; + return; + } + } +} + +/** Raw control codes produced by POSIX terminals */ +enum keycodes { + BACKSPACE_CODE = 8, // Backspace (Ctrl+H) + TAB_CODE = 9, // Tab + LF_CODE = 10, // Line feed + RETURN_CODE = 13, // Enter / Return (CR) + ESC_CODE = 27, // Escape + DELETE_CODE = 127 // Delete (DEL) +}; + +/** Decode raw input units into a keypress */ +static void inline_decode(const rawinput_t *raw, keypress_t *out) { + inline_keypressunknown(out); // Initially UNKNOWN + unsigned char b = *raw; + + if (b < 32 || b == DELETE_CODE) { // Control keys (ASCII control range or DEL) + switch (b) { + case TAB_CODE: out->type = KEY_TAB; return; + case LF_CODE: out->type = KEY_CTRL_RETURN; return; + case RETURN_CODE: out->type = KEY_RETURN; return; + case BACKSPACE_CODE: // v fallthrough + case DELETE_CODE: out->type = KEY_DELETE; return; + case ESC_CODE: + inline_decode_escape(out); + return; + + default: // Control codes are Ctrl+A → 1, Ctrl+Z → 26 + if (b >= 1 && b <= 26) inline_keypresswithchar(out, KEY_CTRL, 'A' + (b - 1)); + return; + } + } + + if (b < 128) { // ASCII regular character + inline_keypresswithchar(out, KEY_CHARACTER, b); + return; + } + + inline_decodeutf8input(b, out); // UTF8 +} + +/** Obtain a keypress event */ +static bool inline_readkeypress(inline_editor *edit, keypress_t *out) { + (void) edit; + rawinput_t raw; + if (!inline_readraw(&raw)) return false; + inline_decode(&raw, out); + return true; +} + +/* ********************************************************************** + * Input loop + * ********************************************************************** */ + +/** Update the cursor position */ +static inline void inline_setcursorposn(inline_editor *edit, int new_posn) { + if (new_posn < 0) new_posn = 0; + if (new_posn > edit->grapheme_count) new_posn = edit->grapheme_count; + if (edit->cursor_posn == new_posn) return; + edit->refresh = true; + + int old_row; + inline_cursorposn(edit, &old_row, NULL); + + edit->cursor_posn = new_posn; + inline_ensurecursorvisible(edit); +} + +/** Insert text into the buffer */ +static bool inline_insert(inline_editor *edit, const char *bytes, size_t nbytes) { + if (!inline_extendbufferby(edit, nbytes)) return false; // Ensure capacity + + size_t offset = 0; // Obtain the byte offset of the current cursor position + if (edit->cursor_posn < edit->grapheme_count) offset = edit->graphemes[edit->cursor_posn]; + else offset = edit->buffer_len; + + // Move contents after the insertion point to make room for the inserted text + memmove(edit->buffer + offset + nbytes, edit->buffer + offset, edit->buffer_len - offset); + + memcpy(edit->buffer + offset, bytes, nbytes); // Copy new text into buffer + edit->buffer_len += nbytes; + edit->buffer[edit->buffer_len] = '\0'; // Ensure null-terminated + + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + + // Move cursor to end of inserted text + int newpos = inline_findgraphemeindex(edit, offset + nbytes); + inline_setcursorposn(edit, newpos); + + edit->refresh = true; // Redraw + return true; +} + +/** Helper to delete bytes [ start, end ) in the buffer */ +static void inline_deletebytes(inline_editor *edit, size_t start, size_t end) { + if (start >= end || end > edit->buffer_len) return; + + size_t bytes = end - start; + memmove(edit->buffer + start, edit->buffer + end, edit->buffer_len - end); // Move subsequent text + edit->buffer_len -= bytes; + + edit->buffer[edit->buffer_len] = '\0'; // Ensure null termination + + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + edit->refresh = true; +} + +/** Helper to delete a grapheme at a given index */ +static void inline_deletegrapheme(inline_editor *edit, int index) { + if (index < 0 || index >= edit->grapheme_count) return; + + size_t start, end; + inline_graphemerange(edit, index, &start, &end); + inline_deletebytes(edit, start, end); +} + +/** Deletes selected text */ +static void inline_deleteselection(inline_editor *edit) { + int sel_l; + size_t start, end; + if (!inline_selectionrange(edit, &sel_l, NULL, &start, &end)) return; + + inline_deletebytes(edit, start, end); // Delete the selection + + edit->selection_posn = INLINE_INVALID; // Clear selection + inline_setcursorposn(edit, sel_l); // Cursor moves to start of deleted region +} + +/** Delete character under cursor */ +static void inline_deletecurrent(inline_editor *edit) { + if (edit->cursor_posn < edit->grapheme_count) { // Delete grapheme under cursor if at start of line + inline_deletegrapheme(edit, edit->cursor_posn); + } +} + +/** Delete text from the buffer */ +static void inline_delete(inline_editor *edit) { + if (edit->selection_posn != INLINE_INVALID) { + inline_deleteselection(edit); + } else if (edit->cursor_posn > 0) { // Delete grapheme before cursor + inline_deletegrapheme(edit, edit->cursor_posn - 1); + inline_setcursorposn(edit, edit->cursor_posn - 1); + } else inline_deletecurrent(edit); +} + +/** Clear the buffer */ +static void inline_clear(inline_editor *edit) { + edit->buffer_len = 0; // Clear text buffer + edit->buffer[0] = '\0'; + inline_recomputegraphemes(edit); + inline_recomputelines(edit); + inline_setcursorposn(edit, 0); // Reset cursor + edit->refresh = true; + edit->suggestion_shown = false; +} + +/** Navigation keys */ +static void inline_navigatetolineboundary(inline_editor *edit, bool end) { + int row; + inline_cursorposn(edit, &row, NULL); + inline_setcursorposn(edit, inline_findgraphemeindex(edit, edit->lines[row + end])); +} + +static void inline_home(inline_editor *edit) { + inline_navigatetolineboundary(edit, false); +} + +static void inline_end(inline_editor *edit) { + inline_navigatetolineboundary(edit, true); +} + +static void inline_pageup(inline_editor *edit) { + inline_setcursorposn(edit, 0); +} + +static void inline_pagedown(inline_editor *edit) { + inline_setcursorposn(edit, edit->grapheme_count); +} + +static void inline_left(inline_editor *edit) { + if (edit->cursor_posn > 0) + inline_setcursorposn(edit, edit->cursor_posn - 1); +} + +static void inline_right(inline_editor *edit) { + if (edit->cursor_posn < edit->grapheme_count) + inline_setcursorposn(edit, edit->cursor_posn + 1); +} + +/** Selections */ +static void inline_beginselection(inline_editor *edit) { + if (edit->selection_posn==INLINE_INVALID) edit->selection_posn = edit->cursor_posn; +} + +static void inline_clearselection(inline_editor *edit) { + edit->selection_posn=INLINE_INVALID; +} + +/** Copy selected text */ +static void inline_copyselection(inline_editor *edit) { + size_t start, end; + if (inline_selectionrange(edit, NULL, NULL, &start, &end)) + inline_copytoclipboard(edit, edit->buffer + start, end - start); +} + +/** Cut selected text */ +static void inline_cutselection(inline_editor *edit) { + inline_copyselection(edit); + inline_deleteselection(edit); +} + +/** Cut part of a line */ +static void inline_cutline(inline_editor *edit, bool before) { + int row; + inline_cursorposn(edit, &row, NULL); + size_t b_line = edit->lines[row + (before ? 0 : 1)]; // line break position before or after + size_t b_cursor = edit->graphemes[edit->cursor_posn]; // Cursor position + + size_t b_start = imin(b_line, b_cursor), b_end = imax(b_line, b_cursor); + if (!before && b_end>0 && edit->buffer[b_end-1]=='\n') b_end--; // Don't include newline + if (b_start==b_end) return; // Nothing to copy + + inline_copytoclipboard(edit, edit->buffer + b_start, b_end - b_start); + inline_deletebytes(edit, b_start, b_end); + inline_setcursorposn(edit, inline_findgraphemeindex(edit, b_start)); // Cursor moves to start of deleted region +} + +/** Paste from clipboard */ +static void inline_paste(inline_editor *edit) { + if (edit->clipboard && edit->clipboard_len>0) { + if (edit->selection_posn != INLINE_INVALID) inline_deleteselection(edit); // Replace selection + inline_insert(edit, edit->clipboard, edit->clipboard_len); + } +} + +/** Process a history keypress */ +static void inline_historykey(inline_editor *edit, int delta) { + inline_advancehistory(edit, delta); + inline_setcursorposn(edit, edit->grapheme_count); // Move to end + inline_clearselection(edit); + inline_clearsuggestions(edit); +} + +/** Transpose two graphemes */ +static void inline_transpose(inline_editor *edit) { + int n = edit->grapheme_count, cur = edit->cursor_posn; + if (n < 2 || cur == 0) return; + + int a = (cur >= n ? n-2 : cur-1), b = a + 1; // The two graphemes to swap + size_t a_start, a_end, b_start, b_end; // Their byte bounds + inline_graphemerange(edit, a, &a_start, &a_end); + inline_graphemerange(edit, b, &b_start, &b_end); + + size_t a_len = a_end - a_start, b_len = b_end - b_start; // Temporary buffer + char *tmp = malloc(a_len); + if (!tmp) return; + + memcpy(tmp, edit->buffer + a_start, a_len); // Copy a into temporary buffer + memmove(edit->buffer + a_start, edit->buffer + b_start, b_len); // Copy b overwriting a + memcpy(edit->buffer + a_start + b_len, tmp, a_len); // Copy a from the temporary buffer + + free(tmp); + + inline_recomputegraphemes(edit); + if (cur < n) inline_setcursorposn(edit, edit->cursor_posn+1); +} + +/** Apply current suggestion */ +static void inline_applysuggestion(inline_editor *edit) { + const char *suffix = inline_currentsuggestion(edit); + if (suffix && *suffix) inline_insert(edit, suffix, strlen(suffix)); + inline_clearsuggestions(edit); +} + +/** Handle Ctrl+_ shortcuts */ +static bool inline_processshortcut(inline_editor *edit, char c) { + switch (c) { + case 'A': inline_home(edit); break; + case 'B': inline_left(edit); break; + case 'C': inline_clear(edit); return false; // exit on Ctrl-C + case 'D': + inline_clearselection(edit); + inline_deletecurrent(edit); + break; + case 'E': inline_end(edit); break; + case 'F': inline_right(edit); break; + case 'G': return false; // exit on Ctrl-G + case 'K': inline_cutline(edit, false); break; // Cut to end of line + case 'L': inline_clear(edit); break; + case 'N': inline_historykey(edit, 1); break; // Next history + case 'O': inline_copyselection(edit); break; + case 'P': inline_historykey(edit, -1); break; // Previous history + case 'T': inline_transpose(edit); break; + case 'U': inline_cutline(edit, true); break; // Cut to start of line + case 'X': inline_cutselection(edit); break; + case 'Y': // v fallthrough + case 'V': inline_paste(edit); break; + default: break; + } + edit->refresh = true; + return true; +} + +/** Handle Meta + _ shortcuts; upper case versions indicate Shift + Meta + _ */ +static bool inline_processmeta(inline_editor *edit, const unsigned char *c, int nbytes) { + (void) nbytes; + switch (*c) { + case 'w': case 'W': inline_copyselection(edit); break; + default: break; + } + edit->refresh = true; + return true; +} + +/** Process a keypress */ +static bool inline_processkeypress(inline_editor *edit, const keypress_t *key) { + bool generatesuggestions=true, clearselection=true, endbrowsing=true; + switch (key->type) { + case KEY_RETURN: + if (!edit->multiline_fn || !edit->multiline_fn(edit->buffer, edit->multiline_ref)) return false; + case KEY_CTRL_RETURN: // v fallthrough + if (!inline_insert(edit, "\n", 1)) return false; + generatesuggestions = false; // newline shouldn't trigger suggestion + break; + case KEY_LEFT: inline_left(edit); break; + case KEY_RIGHT: + if (edit->suggestion_shown) { + inline_applysuggestion(edit); + generatesuggestions = false; + } else inline_right(edit); + break; + case KEY_SHIFT_LEFT: + inline_beginselection(edit); + inline_left(edit); + clearselection=false; + break; + case KEY_SHIFT_RIGHT: + inline_beginselection(edit); + inline_right(edit); + clearselection=false; + break; + case KEY_UP: inline_historykey(edit, -1); endbrowsing=false; break; + case KEY_DOWN: inline_historykey(edit, +1); endbrowsing=false; break; + case KEY_HOME: inline_home(edit); break; + case KEY_END: inline_end(edit); break; + case KEY_PAGE_UP: inline_pageup(edit); break; + case KEY_PAGE_DOWN: inline_pagedown(edit); break; + case KEY_DELETE: inline_delete(edit); break; + case KEY_TAB: + if (inline_havesuggestions(edit)) { + inline_advancesuggestions(edit, 1); + generatesuggestions=false; + } else if (!inline_insert(edit, "\t", 1)) return false; + break; + case KEY_SHIFT_TAB: + if (inline_havesuggestions(edit)) { + inline_advancesuggestions(edit, -1); + generatesuggestions=false; + } + break; + case KEY_CTRL: return inline_processshortcut(edit, key->c[0]); + case KEY_ALT: return inline_processmeta(edit, key->c, key->nbytes); + case KEY_CHARACTER: + if (!inline_insert(edit, (char *) key->c, key->nbytes)) return false; + break; + case KEY_UNKNOWN: break; + } + + if (clearselection) inline_clearselection(edit); + if (generatesuggestions) inline_generatesuggestions(edit); + if (endbrowsing) inline_endhistorybrowsing(edit); + edit->refresh = true; + return true; +} + +/* ********************************************************************** + * Interface + * ********************************************************************** */ + +/** If we're not attached to a terminal, e.g. a pipe, simply read the file in. */ +static void inline_noterminal(inline_editor *edit) { + int c; + + while ((c = fgetc(stdin)) != EOF && c != '\n') { + if (!inline_extendbufferby(edit, 1)) break; // Buffer could not be extended + edit->buffer[edit->buffer_len++] = (char)c; + } + + edit->buffer[edit->buffer_len] = '\0'; // Ensure null termination +} + +/** If the terminal is unsupported, display a prompt and read the line normally. */ +static void inline_unsupported(inline_editor *edit) { + fputs(edit->prompt, stdout); + fflush(stdout); // Ensure prompt appears + + inline_noterminal(edit); + + int length = (int)edit->buffer_len - 1; // Strip trailing control characters + while (length >= 0 && iscntrl((unsigned char)edit->buffer[length])) edit->buffer[length--] = '\0'; + + edit->buffer_len = length + 1; +} + +/** Normal interface if terminal recognized */ +static void inline_supported(inline_editor *edit) { + inline_reset(edit); + inline_setutf8(); + if (!inline_enablerawmode(edit)) return; // Could not enter raw mode + inline_updateterminalwidth(edit); + inline_initviewport(edit); + inline_redraw(edit); + + keypress_t key; + while (inline_readkeypress(edit, &key)) { + if (!inline_processkeypress(edit, &key)) break; + + if (resize_pending) { + /* Update terminal width and viewport on resize */ + inline_updateterminalwidth(edit); + inline_updateviewportwidth(edit); + edit->refresh = true; // Ensure we redraw after resize + resize_pending = 0; + } + + if (edit->refresh) { + inline_redraw(edit); + edit->refresh = false; + } + } + + inline_clearselection(edit); + inline_clearsuggestions(edit); + inline_redraw(edit); + inline_disablerawmode(edit); + + if (edit->buffer_len > 0) inline_addhistory(edit, edit->buffer); // Add to history if non-empty + inline_write("\r\n", 2); +} + +/** API function to read a line of text from the user. + * @param edit - an inline_editor that has been created with inline_new. + * @returns a heap-allocated copy of the string input by the user (caller must free), + * or NULL on error. */ +char *inline_readline(inline_editor *edit) { + if (!edit) return NULL; + inline_clear(edit); // Reset buffer + + if (!inline_checktty()) inline_noterminal(edit); + else if (inline_checksupported()) inline_supported(edit); + else inline_unsupported(edit); + + return (edit->buffer ? inline_strdup(edit->buffer) : NULL); +} diff --git a/src/inline.h b/src/inline.h new file mode 100644 index 0000000..aa8f407 --- /dev/null +++ b/src/inline.h @@ -0,0 +1,248 @@ +/** @file inline.h + * @author T J Atherton + * + * @brief A simple grapheme aware line editor with history, completion, multiline editing and syntax highlighting */ + +#ifndef INLINE_H +#define INLINE_H + +#include +#include +#include + +#define INLINE_VERSION_MAJOR 0 +#define INLINE_VERSION_MINOR 1 +#define INLINE_VERSION_PATCH 0 + +/* Forward declaration of the line editor structure */ +typedef struct inline_editor inline_editor; + +/* ********************************************************************** + * Callback functions + * ********************************************************************** */ + +/* ----------------------- + * Autocomplete + * ----------------------- */ + +/** @brief Autocomplete callback function. + * + * Called repeatedly by the line editor to obtain completion + * suggestions for the given buffer contents. + * + * The editor initializes *index to zero before the first call. + * Each time the callback returns a suggestion, it should update + * *index to an opaque value representing the next iteration + * position. The editor does not interpret this value; it is + * entirely callback-defined. + * + * @param[in] utf8 Current contents of the line buffer. + * @param[in] ref User-supplied reference pointer. + * @param[in,out] index Opaque iteration state. Set to zero + * by the editor before the first call. + * + * @returns A UTF-8 string containing the completion suffix, + * or NULL if no more suggestions exist. For example, + * if the buffer ends with "pr", a suggestion might be + * "int" to form "print". + * + * @note The callback owns the returned string; it may therefore + * return pointers to static strings or internal buffers. + * The editor copies the suggestion immediately. */ +typedef const char *(*inline_completefn) (const char *utf8, void *ref, size_t *index); + +/* ----------------------- + * Syntax coloring + * ----------------------- */ + +/** @brief A single colored span of text. */ +typedef struct { + size_t byte_end; /* exclusive end of color span */ + int color; /* Index into color palette */ +} inline_colorspan_t; + +/** @brief Syntax coloring callback function, called repeatedly by + * the editor to obtain the next colored span. The span is assumed + * to begin as offset. + * @param[in] utf8 The full buffer encoded as UTF-8 to analyze. + * @param[in] ref User-supplied reference pointer. + * @param[in] offset Byte offset at which to begin scanning. + * @param[out] out Filled with the next colored span, if any. + * + * @returns true if a span was found, false if no more spans exist. */ +typedef bool (*inline_syntaxcolorfn) (const char *utf8, void *ref, size_t offset, inline_colorspan_t *out); + +/* ----------------------- + * Multiline editing + * ----------------------- */ + +/** @brief Multiline callback function + * Called when inline wants to know whether it should enter multiline mode. + * The callback should parse the input and return true if inline should go to + * multiline mode or false otherwise. + * @param[in] utf8 The full buffer encoded as UTF-8. + * @param[in] ref User-supplied reference pointer. + * + * @returns true if more lines are required, false otherwise. */ +typedef bool (*inline_multilinefn) (const char *utf8, void *ref); + +/* ----------------------- + * Grapheme support + * ----------------------- */ + +/** @brief Unicode grapheme splitter callback function + * @param[in] in - a string + * @param[in] end - end of string + * @returns number of bytes in the next grapheme or 0 if incomplete + * @details If provided, inline will use this function to split UTF8 code + * into graphemes. Shims are provided in the documentation for + * libgrapheme and libunistring. A fallback implementation is used + * if not provided. */ +typedef size_t (*inline_graphemefn) (const char *in, const char *end); + +/** @brief Unicode grapheme display width callback function + * @param[in] g - a string representing a grapheme + * @param[in] len - length of grapheme in bytes + * @returns display width of grapheme in terminal columns + * @details If provided, inline will use this function to calculate the display + * width. A fallback implementation is used if not provided. */ +typedef int (*inline_widthfn)(const char *g, size_t len); + +/* ********************************************************************** + * Core API + * ********************************************************************** */ + +/** @brief Create a new line editor. + * @param[in] prompt The prompt string to display. This is immediately copied and you may free/modify upon return. + * @returns A newly allocated line editor.*/ +inline_editor *inline_new(const char *prompt); + +/** @brief Free a line editor and all associated resources. + * @param[in] edit Line editor to free. */ +void inline_free(inline_editor *edit); + +/** @brief Read a line of input from the terminal. + * @param[in] edit Line editor to use. + * @returns A heap allocated UTF-8 string containing the user's input, or NULL on EOF or error. + * Caller owns the string and must call it later using free(). */ +char *inline_readline(inline_editor *edit); + +/** @brief Sets the maximum length of the history. + * @param[in] edit Line editor to configure. + * @param[in] maxlen Maximum number of entries in the history buffer; + * negative values mean unlimited; 0 disables history */ +void inline_sethistorylength(inline_editor *edit, int maxlen); + +/** @brief Adds an entry to the history. + * @param[in] edit Line editor to use. + * @param[in] entry Entry to add. This is copied immediately and the pointer is not stored. + * @returns true if the entry was successfully added to the history list; false otherwise */ +bool inline_addhistory(inline_editor *edit, const char *entry); + +/** @brief Enable syntax coloring. + * @param[in] edit Line editor to configure. + * @param[in] fn Syntax coloring callback. + * @param[in] ref User-supplied reference pointer. */ +void inline_syntaxcolor(inline_editor *edit, inline_syntaxcolorfn fn, void *ref); + +/** Macros for basic ANSI terminal colors */ +#define INLINE_BLACK 0 +#define INLINE_RED 1 +#define INLINE_GREEN 2 +#define INLINE_YELLOW 3 +#define INLINE_BLUE 4 +#define INLINE_MAGENTA 5 +#define INLINE_CYAN 6 +#define INLINE_WHITE 7 + +/** Macro for xterm-256 216-color RGB cube: r,g,b are in [0..5]. */ +#define INLINE_COLOR_ANSI216(r, g, b) (16 + 36 * (int)(r) + 6 * (int)(g) + (int)(b)) + +/** Macro for xterm-256 gray levels: n is in [0..23] */ +#define INLINE_GRAY_ANSI(n) (232 + (n)) + +/** Macro for RGB values */ +#define INLINE_COLOR_RGB 0x01000000u + +#define INLINE_RGB(r, g, b) \ + (INLINE_COLOR_RGB | (((uint32_t)(r) & 0xffu) << 16) | \ + (((uint32_t)(g) & 0xffu) << 8) | ((uint32_t)(b) & 0xffu)) + +/** @brief Set the color palette used for syntax highlighting. + * + * Color indices returned by a inline_syntaxcolorfn are mapped + * through this palette to a final color value. The palette is copied by + * the inline_editor. Color values are interpreted: + * + * -1 → default color + * 0–7 → ANSI basic colors (see macros above) + * 8–255 → 256-color palette + * >=0x01000000 → RGB packed as 0x01RRGGBB + * + * @param[in] edit Line editor to configure. + * @param[in] count Number of entries in the palette. + * @param[in] palette Array mapping semantic color indices to color ints. + * @returns: true on success; false otherwise */ +bool inline_setpalette(inline_editor *edit, int count, const int *palette); + +/** @brief Enable autocomplete. + * @param[in] edit Line editor to configure. + * @param[in] fn Completion callback. + * @param[in] ref User-supplied reference pointer. */ +void inline_autocomplete(inline_editor *edit, inline_completefn fn, void *ref); + +/** @brief Enable multiline editing. + * @param[in] edit Line editor to configure. + * @param[in] fn Multiline callback. + * @param[in] ref User-supplied reference pointer. + * @param[in] continuation_prompt Prompt to use for continuation lines; this is copied immediately and you may free/modify after. + * @returns true on success; false otherwise */ +bool inline_multiline(inline_editor *edit, inline_multilinefn fn, void *ref, const char *continuation_prompt); + +/** @brief Supply a custom grapheme splitter. + * @param[in] edit Line editor to configure. + * @param[in] fn Grapheme callback. */ +void inline_setgraphemesplitter(inline_editor *edit, inline_graphemefn fn); + +/** @brief Supply a custom grapheme display width calculator. + * @param[in] edit Line editor to configure. + * @param[in] fn Grapheme display width callback. */ +void inline_setgraphemewidth(inline_editor *edit, inline_widthfn fn); + +/* ********************************************************************** + * Terminal helpers + * ********************************************************************** */ + +/** @brief Check whether stdin and stdout are TTYs. + * @returns true if both stdin and stdout are terminals. */ +bool inline_checktty(void); + +/** @brief Check whether stdout is a TTY (for color/gated output when stdin may be piped). */ +bool inline_checkstdouttty(void); + +/** @brief Check is the terminal is supported (i.e. likely capable of processed output) + * @returns true if supported. */ +bool inline_checksupported(void); + +/** @brief Gets the current width of the terminal. + * @param[out] width - the width of the terminal, only set if successfully retrieved. + * @returns true on success. */ +bool inline_getterminalwidth(int *width); + +/** @brief Set UTF8 mode. */ +void inline_setutf8(void); + +/** @brief Emits a string to stdout. + * @param[in] str - String to emit.*/ +void inline_emit(const char *str); + +/** @brief Emits a terminal control code to stdout corresponding to a given palette color. + * @param[in] color - Color to emit in the format used for inline_setpalette above.*/ +void inline_emitcolor(int color); + +/** @brief Display a UTF-8 string using syntax coloring. + * @param[in] edit Line editor to use. + * @param[in] string UTF-8 string to display.*/ +void inline_displaywithsyntaxcoloring(inline_editor *edit, const char *string); + +#endif /* INLINE_H */ diff --git a/src/linedit.c b/src/linedit.c deleted file mode 100644 index c264b55..0000000 --- a/src/linedit.c +++ /dev/null @@ -1,1927 +0,0 @@ -/** @file linedit.c - * @author T J Atherton - * - * @brief A simple UTF8 aware line editor with history, completion, multiline editing and syntax highlighting - */ - -#include "linedit.h" - -#ifdef _WIN32 -#include -#include -#include -#else -#include -#include -#include -#endif - -/** Maximum escape code size */ -#define LINEDIT_CODESTRINGSIZE 24 - -/* ********************************************************************** - * Platform dependent code - * ********************************************************************** */ - -/* ---------------------------------------- - * Check terminal features - * ---------------------------------------- */ - -/** Check if stdin and stdout are a tty */ -bool linedit_isatty(void) { -#ifdef _WIN32 - return (_isatty(_fileno(stdin)) && _isatty(_fileno(stdout))); -#else - return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO); -#endif -} - -/** Get terminal name */ -bool linedit_terminalname(char *buffer, size_t size) { -#ifdef _WIN32 - strncpy(buffer, "win", size); - return true; -#else - char *term = getenv("TERM"); - if (term) strncpy(buffer, term, size); - return term; -#endif -} - -/** Enable UTF8 mode */ -void linedit_setutf8(void) { -#ifdef _WIN32 - SetConsoleOutputCP(CP_UTF8); -#endif -} - -/* ---------------------------------------- - * Switch to/from raw mode - * ---------------------------------------- */ - -/** Hold the original terminal state */ -#ifdef _WIN32 -DWORD terminit; -#else -struct termios terminit; -#endif - -bool termexitregistered=false; - -void linedit_disablerawmode(void); - -/** @brief Enables 'raw' mode in the terminal - * @details In raw mode key presses are passed directly to us rather than - * being buffered. */ -void linedit_enablerawmode(void) { -#ifdef _WIN32 - HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE); - GetConsoleMode(hConsole, &terminit); - SetConsoleMode(hConsole, (terminit & ~(ENABLE_LINE_INPUT | - ENABLE_ECHO_INPUT | - ENABLE_PROCESSED_INPUT) | - ENABLE_VIRTUAL_TERMINAL_INPUT )); - - linedit_setutf8(); -#else - struct termios termraw; /* Use to set the raw state */ - - tcgetattr(STDIN_FILENO, &terminit); /** Get the original state*/ - - termraw=terminit; - /* Input: Turn off: IXON - software flow control (ctrl-s and ctrl-q) - ICRNL - translate CR into NL (ctrl-m) - BRKINT - parity checking - ISTRIP - strip bit 8 of each input byte */ - termraw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | BRKINT | ISTRIP); - /* Output: Turn off: OPOST - output processing */ - termraw.c_oflag &= ~(OPOST); - /* Character: CS8 Set 8 bits per byte */ - termraw.c_cflag |= (CS8); - /* Turn off: ECHO - causes keypresses to be printed immediately - ICANON - canonical mode, reads line by line - IEXTEN - literal (ctrl-v) - ISIG - turn off signals (ctrl-c and ctrl-z) */ - termraw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); - /* Set return condition for control characters */ - termraw.c_cc[VMIN] = 1; termraw.c_cc[VTIME] = 0; /* 1 byte, no timer */ - - tcsetattr(STDIN_FILENO, TCSAFLUSH, &termraw); -#endif - if (!termexitregistered) { - atexit(linedit_disablerawmode); - termexitregistered=true; - } -} - -/** @brief Restore terminal state to normal */ -void linedit_disablerawmode(void) { -#ifdef _WIN32 - HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE); - SetConsoleMode(hConsole, terminit); -#else - tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminit); -#endif - printf("\r"); /** Print a carriage return to ensure we're back on the left hand side */ -} - -/** @brief Gets the terminal width */ -void linedit_getterminalwidth(lineditor* edit) { - edit->ncols = 80; -#ifdef _WIN32 - HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); - CONSOLE_SCREEN_BUFFER_INFO csbi; - - if (GetConsoleScreenBufferInfo(h, &csbi)) { - edit->ncols = csbi.srWindow.Right - csbi.srWindow.Left + 1; - } -#else - struct winsize ws; - - /* Try ioctl first */ - if (!(ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0)) { - edit->ncols = ws.ws_col; - } - else { - // Should get cursor position etc here. - } -#endif -} - -/* ---------------------------------------- - * Detect if keypresses are available - * ---------------------------------------- */ - -/** Detect if a keypress is available; non-blocking */ -bool linedit_keypressavailable(void) { -#ifdef _WIN32 - return _kbhit(); -#else - fd_set readfds; - FD_ZERO(&readfds); - FD_SET(STDIN_FILENO, &readfds); - - struct timeval timeout={ .tv_sec=0, .tv_usec=0 }; - - return (select(1, &readfds, NULL, NULL, &timeout)>0); -#endif -} - -/** Read a single character from the terminal */ -bool linedit_getchar(char* out) { -#ifdef _WIN32 - *out = _getch(); - return true; -#else - return fread(out, sizeof(char), 1, stdin) == 1; -#endif -} - -/* ********************************************************************** - * Terminal - * ********************************************************************** */ - -/** @brief Compares two c strings independently of case - * @param[in] str1 - } strings to compare - * @param[in] str2 - } - * @returns 0 if the strings are identical, otherwise a positive or negative number indicating their lexographic order */ -int linedit_cstrcasecmp(char *str1, char *str2) { - if (str1 == str2) return 0; - int result=0; - - for (char *p1=str1, *p2=str2; result==0; p1++, p2++) { - result=tolower(*p1)-tolower(*p2); - if (*p1=='\0') break; - } - - return result; -} - -#ifdef _WIN32 -char *strndup(const char *s, size_t n) { - char *out = malloc(n+1); - if (out) { - memcpy(out, s, n); - out[n]='\0'; - } - return out; -} -#endif - -/* ---------------------------------------- - * Retrieve terminal type - * ---------------------------------------- */ - -typedef enum { - LINEDIT_NOTTTY, - LINEDIT_UNSUPPORTED, - LINEDIT_SUPPORTED -} linedit_terminaltype; - - -#define LINEDIT_TERMINALNAME_BUFFERSIZE 1024 - -/** Checks whether the current terminal is supported */ -linedit_terminaltype linedit_checksupport(void) { - /* Make sure both stdin and stdout are a tty */ - if (!linedit_isatty()) return LINEDIT_NOTTTY; - - char *unsupported[]={"dumb","cons25","emacs",NULL}; - char term[LINEDIT_TERMINALNAME_BUFFERSIZE]; - if (!linedit_terminalname(term, LINEDIT_TERMINALNAME_BUFFERSIZE)) return LINEDIT_UNSUPPORTED; - - for (unsigned int i=0; unsupported[i]!=NULL; i++) { - if (!linedit_cstrcasecmp(term, unsupported[i])) return LINEDIT_UNSUPPORTED; - } - - return LINEDIT_SUPPORTED; -} - -/* ---------------------------------------- - * Get cursor position and width - * ---------------------------------------- */ - -#define LINEDIT_CURSORPOSN_BUFFERSIZE 128 -/** @brief Gets the cursor position */ -bool linedit_getcursorposition(int *x, int *y) { - char answer[LINEDIT_CURSORPOSN_BUFFERSIZE]; - int i=0, row=0, col=0; - - /* Report cursor location */ - if (fwrite("\x1b[6n", sizeof(char), 4, stdout) != 4) return false; - - /* Read the response: ESC [ rows ; cols R */ - while (i < sizeof(answer)-1) { - if (fread(answer+i, sizeof(char), 1, stdin)<1) break; - if (answer[i] == 'R') break; // Response is 'R' terminated - i++; - } - answer[i] = '\0'; // Terminal response is not null-terminated by default - - /* Parse response */ - if (answer[0] != 27 || answer[1] != '[') return false; - if (sscanf(answer+2,"%d;%d",&row,&col) != 2) return false; - - if (y) *y = row; // Return result - if (x) *x = col; - return true; -} - -/* ---------------------------------------- - * Output - * ---------------------------------------- */ - -/** @brief Writes a string to the terminal */ -bool linedit_write(char *string) { - size_t length=strlen(string); - if (fwrite(string, sizeof(char), length, stdout)0) { - snprintf(code, LINEDIT_CODESTRINGSIZE, "\r\033[%iC", posn); - return linedit_write(code); - } - return true; -} - -/** @brief Moves the cursor up by n lines */ -bool linedit_moveup(int n) { - char code[LINEDIT_CODESTRINGSIZE]; - if (n>0) { - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[%iA", n); - return linedit_write(code); - } - return true; -} - -/** @brief Moves the cursor down by n lines */ - bool linedit_movedown(int n) { - char code[LINEDIT_CODESTRINGSIZE]; - if (n>0) { - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[%iB", n); - return linedit_write(code); - } - return true; -} - -/** @brief Hides the cursor */ -bool linedit_hidecursor(void) { - return linedit_write("\033[?25l"); -} - -/** @brief Shows the cursor */ -bool linedit_showcursor(void) { - return linedit_write("\033[?25h"); -} - -/* ********************************************************************** - * Unicode support - * ********************************************************************** */ - -/* ---------------------------------------- - * Basic UTF8 support - * ---------------------------------------- */ - -/** @brief Returns the number of bytes in the next character of a given utf8 string - @returns number of bytes */ -int linedit_utf8numberofbytes(char *string) { - if (!string) return 0; - uint8_t byte = * ((uint8_t *) string); - - if ((byte & 0xc0) == 0x80) return 0; // In the middle of a utf8 string - - // Get the number of bytes from the first character - if ((byte & 0xf8) == 0xf0) return 4; - if ((byte & 0xf0) == 0xe0) return 3; - if ((byte & 0xe0) == 0xc0) return 2; - return 1; -} - -/** Decodes a utf8 encoded character pointed to by c into an int */ -int linedit_utf8toint(char *c) { - unsigned int ret = -1; - int nbytes=linedit_utf8numberofbytes(c); - switch (nbytes) { - case 1: ret=(c[0] & 0x7f); break; - case 2: ret=((c[0] & 0x1f)<<6) | (c[1] & 0x3f); break; - case 3: ret=((c[0] & 0x0f)<<12) | ((c[1] & 0x3f)<<6) | (c[2] & 0x3f); break; - case 4: ret=((c[0] & 0x0f)<<18) | ((c[1] & 0x3f)<<12) | ((c[2] & 0x3f)<<6) | (c[3] & 0x3f) ; break; - default: break; - } - - return ret; -} - -/** @brief Utf8 character loop advancer. - @details Determines the number of bytes for the code point at c, and advances the counter i by that number. - Returns true if the number of bytes is >0 */ -bool linedit_utf8next(char *c, int *i) { - int adv = linedit_utf8numberofbytes(c); - if (adv) *i+=adv; - return adv; -} - -/** @brief Count the number of UTF characters in a string - @param[in] start - start of string - @param[in] length - length of string - @param[out] count - number of UTF characters - @returns true on success */ -bool linedit_utf8count(char *start, size_t length, size_t *count) { - size_t len, n=0; - for (char *c=start; ccount=0; - dict->capacity=0; - dict->contents=NULL; -} - -void linedit_graphemeclear(linedit_graphemedictionary *dict) { - for (int i=0; icapacity; i++) { - if (dict->contents[i].grapheme) free(dict->contents[i].grapheme); - } - linedit_graphemeinit(dict); -} - -uint32_t linedit_hashstring(const char* key, size_t length) { - uint32_t hash = 2166136261u; // String hashing function FNV-1a - - for (unsigned int i=0; i < length; i++) { - hash ^= key[i]; - hash *= 16777619u; // FNV prime number for 32 bits - } - - return hash; -} - -bool linedit_graphemeinsert(linedit_graphemedictionary *dict, char *grapheme, size_t length, int width); - -bool linedit_graphemeresize(linedit_graphemedictionary *dict, int size) { - linedit_graphemeentry *new=malloc(size*sizeof(linedit_graphemeentry)), *old=dict->contents; - int osize=dict->capacity; - - if (new) { // Clear the newly allocated structure - for (unsigned int i=0; icapacity=size; - dict->contents=new; - dict->count=0; - - if (old) { // Copy old contents across - for (unsigned int i=0; icontents) return false; - uint32_t hash = linedit_hashstring(grapheme, length); - - int start = hash % dict->capacity; - int i=start; - - do { - if (!dict->contents[i].grapheme) { // Blank entry -> not found - if (posn) *posn = i; - return false; - } - - if (strncmp(grapheme, dict->contents[i].grapheme, length)==0) { // Found - if (posn) *posn = i; - return true; - } - - i = (i+1) % dict->capacity; - } while (i!=start); // Loop terminates once we return to the starting position - - return false; -} - -#define LINEDIT_MINDICTIONARYSIZE 8 -#define LINEDIT_SIZEINCREASETHRESHOLD(x) (((x)>>1) + ((x)>>2)) -#define LINEDIT_INCREASEDICTIONARYSIZE(x) (2*x) - -bool linedit_graphemeinsert(linedit_graphemedictionary *dict, char *grapheme, size_t length, int width) { - - if (!dict->contents) { - if (!linedit_graphemeresize(dict, LINEDIT_MINDICTIONARYSIZE)) return false; - } else if (dict->count+1 > LINEDIT_SIZEINCREASETHRESHOLD(dict->capacity)) { - if (!linedit_graphemeresize(dict, LINEDIT_INCREASEDICTIONARYSIZE(dict->capacity))) return false; - } - - int posn; - if (linedit_graphemefind(dict, grapheme, length, &posn)) return true; - - char *label = strndup(grapheme, length); - if (!label) return false; - dict->contents[posn].grapheme=label; - dict->contents[posn].width=width; - dict->count++; - return true; -} - -bool linedit_graphemelookup(linedit_graphemedictionary *dict, char *grapheme, size_t length, int *width) { - if (!dict->contents) return false; - int posn; - if (!linedit_graphemefind(dict, grapheme, length, &posn)) return false; - if (width) *width = dict->contents[posn].width; - return true; -} - -void linedit_graphemeshow(linedit_graphemedictionary *dict) { - for (int i=0; icapacity; i++) { - if (dict->contents[i].grapheme) { - printf("%s: %i\n", dict->contents[i].grapheme, dict->contents[i].width); - } - } -} - -/* ---------------------------------------- - * Grapheme display width - * ---------------------------------------- */ - -/** @brief Identifies the length of the next grapheme */ -size_t linedit_graphemelength(lineditor *edit, char *str, char *end) { - if (*str=='\0') return 0; // Ensure we return on null terminator - if (edit->graphemefn) return edit->graphemefn(str, end); - return (size_t) linedit_utf8numberofbytes(str); // Fallback on displaying unicode chars one by one -} - -/** @brief Returns the display with of a grapheme sequence if known */ -bool linedit_graphemedisplaywidth(lineditor *edit, char *grapheme, size_t length, int *width) { - if (length==1) { - if (iscntrl(*grapheme)) return 0; - return 1; - } - - return linedit_graphemelookup(&edit->graphemedict, grapheme, length, width); -} - -/** @brief Renders a grapheme sequence, measuring its display width */ -bool linedit_graphememeasurewidth(lineditor *edit, char *grapheme, size_t length, int *width) { - int x0=0, x1=0, w=0; - linedit_getcursorposition(&x0, NULL); - if (fwrite(grapheme, sizeof(char), length, stdout)x0 ? x1-x0 : 1); - *width = w; - return linedit_graphemeinsert(&edit->graphemedict, grapheme, length, w); -} - -/* ********************************************************************** - * Rendering - * ********************************************************************** */ - -/** @brief Renders a string, showing only characters in columns l...r */ -void linedit_renderstring(lineditor *edit, char *string, size_t length, int l, int r) { - int i=0; - size_t len=0; - - for (char *s=string; *s!='\0'; s+=len) { - len = linedit_graphemelength(edit, s, string+length); - if (!len) break; - - if (*s=='\r') { // Reset on a carriage return - if (fwrite("\r", sizeof(char), 1, stdout)<1) return; - i=0; - } else if (*s=='\n') { // Clear to end of line and return - if (!linedit_write("\x1b[K\n\r")) return; - if (fwrite(edit->cprompt.string, sizeof(char), edit->cprompt.length, stdout)cprompt.length) return; - i=0; - } else if (*s=='\t') { - if (!linedit_write(" ")) return; - i+=1; - } else if (iscntrl(*s)) { - if (*s=='\033') { // A terminal control character - char *ctl=s; // First identify its length - while (!isalpha(*ctl) && *ctl!='\0') ctl++; - size_t len = ctl-s+1; - if (fwrite(s, sizeof(char), len, stdout)=l && icapacity=0; - string->length=0; - string->next=NULL; - string->string=NULL; -} - -/** Clears a string, deallocating memory if necessary */ -void linedit_stringclear(linedit_string *string) { - if (string->string) free(string->string); - linedit_stringinit(string); -} - -/** @brief Finds the index of character i in a utf8 encoded string. - * @param[in] string - string to index - * @param[in] i - Index of character to find - * @param[in] offset - Offset into the string - set to 0 to count from the start of the string - * @param[out] out - offset in bytes from offset to character i */ -bool linedit_stringutf8index(linedit_string *string, size_t i, size_t offset, size_t *out) { - int advance=0; - size_t nchars=0; - - for (size_t j=0; j+offset<=string->length; j+=advance, nchars++) { - if (nchars==i) { *out = j; return true; } - advance=linedit_utf8numberofbytes(string->string+offset+j); - if (advance==0) break; // If advance is 0, the string is corrupted; return failure - } - - return false; -} - -/** Resizes a string - * @param string - the string to grow - * @param size - requested size - * @returns true on success, false on failure */ -bool linedit_stringresize(linedit_string *string, size_t size) { - size_t newsize=linedit_MINIMUMSTRINGSIZE; - char *old=string->string; - - /* If we're increasing the size, grow by factors of 1.5 to avoid excessive calls to allocator */ - while (newsize<=size) newsize=((newsize<<1)+newsize)>>1; // mul by x1.5 - - string->string=realloc(string->string,newsize); - if (string->string) { - if (!old) { - string->string[0]='\0'; /* Make sure a new string is zero-terminated */ - string->length=0; - } - string->capacity=newsize; - } - return (string->string!=NULL); -} - -/** Adds a string to a string */ -void linedit_stringappend(linedit_string *string, char *c, size_t nbytes) { - if (string->capacity<=string->length+nbytes) { - if (!linedit_stringresize(string, string->length+nbytes+1)) return; - } - - strncpy(string->string+string->length, c, nbytes); - string->length+=nbytes; - string->string[string->length]='\0'; /* Keep the string zero-terminated */ -} - -/** @brief Inserts characters at a given position - * @param[in] string - string to amend - * @param[in] posn - insertion position as a character index - * @param[in] c - string to insert - * @param[in] n - number of bytes to insert - * @details If the position is after the length of the string - * the new characters are instead appended. */ -void linedit_stringinsert(linedit_string *string, size_t posn, char *c, size_t n) { - size_t offset; - if (!linedit_stringutf8index(string, posn, 0, &offset)) return; - - if (offsetlength) { - if (string->capacity<=string->length+n) { - if (!linedit_stringresize(string, string->length+n+1)) return; - } - /* Move the remaining part of the string */ - memmove(string->string+offset+n, string->string+offset, string->length-offset+1); - /* Copy in the text to insert */ - memmove(string->string+offset, c, n); - string->length+=n; - } else { - linedit_stringappend(string, c, n); - } -} - -/** @brief Deletes characters at a given position. - * @param[in] string - string to amend - * @param[in] posn - Delete characters as a character index - * @param[in] n - number of characters to delete */ -void linedit_stringdelete(linedit_string *string, size_t posn, size_t n) { - size_t offset; - size_t nbytes; - - if (string->lengthlength) { - if (offset+nbyteslength) { - memmove(string->string+offset, string->string+offset+nbytes, string->length-offset-nbytes+1); - } else { - string->string[offset]='\0'; - } - string->length=strlen(string->string); - } -} - -/** Adds a c string to a string */ -void linedit_stringaddcstring(linedit_string *string, char *s) { - if (!s || !string) return; - size_t size=strlen(s); - if (string->capacity<=string->length+size+1) { - if (!linedit_stringresize(string, string->length+size+1)) return; - } - - strncpy(string->string+string->length, s, string->capacity-string->length); - string->length+=size; -} - -/** Finds the length of a string in unicode characters */ -int linedit_stringlength(linedit_string *string) { - size_t count=0; - linedit_utf8count(string->string, string->length, &count); - return (int) count; -} - -/** Finds the display width of a string */ -int linedit_stringdisplaywidth(lineditor *edit, linedit_string *string) { - int width=0; - size_t len; - for (int i=0; ilength; i+=len) { - int w=1; - len = linedit_graphemelength(edit, string->string+i, string->string+string->length); - linedit_graphemedisplaywidth(edit, string->string+i, len, &w); - width+=w; - } - return width; -} - -/** Finds the display coordinates for a given position in a string */ -void linedit_stringdisplaycoordinates(lineditor *edit, linedit_string *string, int posn, int *xout, int *yout) { - int x=0, y=0, n=0; - size_t count; - for (int i=0; ilength; n+=count) { - if (posn>=0 && n>=posn) break; - - char *c=string->string+i; - size_t len = linedit_graphemelength(edit, c, string->string+string->length); - if (!linedit_utf8count(c, len, &count)) break; - - if (*c=='\n') { - x=0; y++; - } else { - int w=1; - linedit_graphemedisplaywidth(edit, string->string+i, len, &w); - if (x+w>edit->ncols) { // Lines that are too long wrap over - x=w; y++; - } else x+=w; - } - - i+=len; - if (!len) break; - } - if (xout) *xout = x; - if (yout) *yout = y; -} - -/** Finds the line and unicode character number for a given position in the string. - * @param[in] string - the string - * @param[in] posn - character position in the string - * @param[out] xout - x coordinates corresponding to posn n - * @param[out] yout - y */ -void linedit_stringcoordinates(linedit_string *string, int posn, int *xout, int *yout) { - int x=0, y=0, n=0; - for (int i=0; ilength; n++) { - if (n==posn) break; - char *c=string->string+i; - - if (*c=='\n') { - x=0; y++; - } else x++; - - if (!linedit_utf8next(c, &i)) break; - } - if (xout) *xout = x; - if (yout) *yout = y; -} - -/** Finds the position in the string for specified coordinates - * @param[in] string - the string - * @param[in] x - character position or -1 to find the end of the line - * @param[in] y - line number - * @param[out] posn - position corresponding to */ -void linedit_stringfindposition(linedit_string *string, int x, int y, int *posn) { - int xx=0, yy=0, n=0; - for (int i=0; ilength; n++) { - if (xx==x && yy==y) break; - - char *c = string->string+i; - if (*c=='\n') { - xx=0; yy++; - if (yy>y) break; - } else xx++; - - if (!linedit_utf8next(c, &i)) break; - } - if (posn) *posn = n; -} -/** Locates a particular posn in a string, returning a pointer to the character */ -char *linedit_stringlocate(linedit_string *string, int posn) { - size_t out; - if (!linedit_stringutf8index(string, posn, 0, &out)) return NULL; - return string->string+out; -} - -/** Counts the number of lines a string occupies */ -int linedit_stringcountlines(linedit_string *string) { - int lines; - linedit_stringcoordinates(string, -1, NULL, &lines); - return lines; -} - -/** Returns a C string from a string */ -char *linedit_cstring(linedit_string *string) { - return string->string; -} - -/** Creates a new string from a C string */ -linedit_string *linedit_newstring(char *string) { - linedit_string *new = malloc(sizeof(linedit_string)); - - if (new) { - linedit_stringinit(new); - linedit_stringaddcstring(new, string); - } - - return new; -} - -/* ********************************************************************** - * Lists of strings - * ********************************************************************** */ - -/** Adds an entry to a string list */ -void linedit_stringlistadd(linedit_stringlist *list, char *string) { - linedit_string *new=linedit_newstring(string); - - if (new) { - new->next=list->first; - list->first=new; - } -} - -/** Initializes a string list */ -void linedit_stringlistinit(linedit_stringlist *list) { - list->first=NULL; - list->posn=0; -} - -/** Frees the contents of a string list */ -void linedit_stringlistclear(linedit_stringlist *list) { - while (list->first!=NULL) { - linedit_string *s = list->first; - list->first=s->next; - linedit_stringclear(s); - free(s); - } - linedit_stringlistinit(list); -} - -/** Removes a string from a list */ -void linedit_stringlistremove(linedit_stringlist *list, linedit_string *string) { - linedit_string *s=NULL, *prev=NULL; - - for (s=list->first; s!=NULL; s=s->next) { - if (s==string) { - if (prev) { - prev->next=s->next; - } else { - list->first=s->next; - } - linedit_stringclear(s); - free(s); - return; - } - - prev=s; - } -} - -/** Chooses an element of a stringlist - * @param[in] list the list to select from - * @param[in] n entry number to select - * @parma[out] *m entry number actually selected - * @returns the selected element */ -linedit_string *linedit_stringlistselect(linedit_stringlist *list, unsigned int n, unsigned int *m) { - unsigned int i=0; - linedit_string *s=NULL; - - for (s=list->first; s!=NULL && s->next!=NULL; s=s->next) { - if (i==n) break; - i++; - } - - if (m) *m=i; - - return s; -} - -/** Count the number of entries in a string list */ -int linedit_stringlistcount(linedit_stringlist *list) { - int n=0; - for (linedit_string *s=list->first; s!=NULL; s=s->next) n++; - return n; -} - -/* ********************************************************************** - * History list - * ********************************************************************** */ - -/** Adds an entry to the history list */ -void linedit_historyadd(lineditor *edit, char *string) { - linedit_stringlistadd(&edit->history, string); -} - -/** Frees the history list */ -void linedit_historyclear(lineditor *edit) { - linedit_stringlistclear(&edit->history); -} - -/** Makes a particular history entry current */ -unsigned int linedit_historyselect(lineditor *edit, unsigned int n) { - unsigned int m=n; - linedit_string *s=linedit_stringlistselect(&edit->history, n, &m); - - if (s) { - edit->current.length=0; - linedit_stringaddcstring(&edit->current, s->string); - } - - return m; -} - -/** Advances the history list */ -void linedit_historyadvance(lineditor *edit, unsigned int n) { - edit->history.posn+=n; - edit->history.posn=linedit_historyselect(edit, edit->history.posn); -} - -/** Returns the number of entries in the history list */ -int linedit_historycount(lineditor *edit) { - return linedit_stringlistcount(&edit->history); -} - -/* ********************************************************************** - * Autocompletion - * ********************************************************************** */ - -bool linedit_atend(lineditor *edit); - -/** Regenerates the list of autocomplete suggestions */ -void linedit_generatesuggestions(lineditor *edit) { - if (edit->completer) { - linedit_stringlistclear(&edit->suggestions); - - if (edit->current.string && - linedit_atend(edit)) { - (edit->completer) (edit->current.string, edit->cref, &edit->suggestions); - } - } -} - -/** Check whether any suggestions are available */ -bool linedit_aresuggestionsavailable(lineditor *edit) { - return (edit->suggestions.first!=NULL); -} - -/** Get the current suggestion */ -char *linedit_currentsuggestion(lineditor *edit) { - linedit_string *s=linedit_stringlistselect(&edit->suggestions, edit->suggestions.posn, NULL); - - if (s) return s->string; - - return NULL; -} - -/** Advance through the suggestions */ -void linedit_advancesuggestions(lineditor *edit, unsigned int n) { - unsigned int rposn=edit->suggestions.posn+n, nposn=rposn; - linedit_stringlistselect(&edit->suggestions, rposn, &nposn); - edit->suggestions.posn=nposn; - if (rposn!=edit->suggestions.posn) edit->suggestions.posn=0; /* Go back to first */ -} - -/* ********************************************************************** - * Multiline mode - * ********************************************************************** */ - -/** Test whether we should enter multiline editing mode */ -bool linedit_shouldmultiline(lineditor *edit) { - if (edit->multiline && edit->current.string) return (edit->multiline) (edit->current.string, edit->mlref); - return false; -} - -/* ********************************************************************** - * Syntax highlighting - * ********************************************************************** */ - -/** @brief Writes a control sequence to reset default text */ -void linedit_stringdefaulttext(linedit_string *out) { - char code[LINEDIT_CODESTRINGSIZE]; - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[0m"); - linedit_stringaddcstring(out, code); -} - -/** @brief Writes a control sequence to set a given color */ -void linedit_stringsetcolor(linedit_string *out, linedit_color col) { - char code[LINEDIT_CODESTRINGSIZE]; - snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[%im", (col==LINEDIT_DEFAULTCOLOR ? 0: 30+col)); - linedit_stringaddcstring(out, code); -} - -/** @brief Writes a control sequence to set a given emphasis */ -void linedit_stringsetemphasis(linedit_string *out, linedit_emphasis emph) { - char code[LINEDIT_CODESTRINGSIZE] = ""; - switch (emph) { - case LINEDIT_BOLD: snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[1m"); break; - case LINEDIT_UNDERLINE: snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[4m"); break; - case LINEDIT_REVERSE: snprintf(code, LINEDIT_CODESTRINGSIZE, "\033[7m"); break; - case LINEDIT_NONE: break; - } - - linedit_stringaddcstring(out, code); -} - -/** Adds a string with selection highlighting - * @param[in] edit - active editor - * @param[in] in - input string - * @param[in] offset - offset of string in characters - * @param[in] length - length of string in characters - * @param[in] col - color - * @param[out] out - display plus coloring information written to this string */ -void linedit_addcstringwithselection(lineditor *edit, char *in, size_t offset, size_t length, linedit_color *col, linedit_string *out) { - int lposn=-1, rposn=-1; - - /* If a selection is active, discover its bounds */ - if (edit->mode==LINEDIT_SELECTIONMODE && !(edit->sposn<0)) { - lposn=(edit->sposn < edit->posn ? edit->sposn : edit->posn) - (int) offset; - rposn = (edit->sposn < edit->posn ? edit->posn : edit->sposn) - (int) offset; - } - - /* Set the color if provided */ - if (col) linedit_stringsetcolor(out, *col); - - /* Is the text we're showing outside the selected region entirely? */ - if (rposn<0 || lposn>(int) length) { - linedit_stringappend(out, in, length); - } else { - /* If not, add the characters one by one and insert highlighting */ - if (lposn<0) { - linedit_stringsetemphasis(out, LINEDIT_REVERSE); - } - char *c=in; - for (int i=0; itype - b->type; -} - -/** Returns a color matching tokentype type */ -linedit_color linedit_colorfromtokentype(lineditor *edit, linedit_tokentype type) { - linedit_colormap key = { .type = type, .col = LINEDIT_DEFAULTCOLOR }; - - if (edit->color) { - linedit_colormap *val = bsearch(&key, edit->color->col, edit->color->ncols, sizeof(linedit_colormap), linedit_colormapcmp); - - if (val) return val->col; - } - - return LINEDIT_DEFAULTCOLOR; -} - -/** Print a string with syntax coloring */ -void linedit_syntaxcolorstring(lineditor *edit, linedit_string *in, linedit_string *out) { - linedit_tokenizefn tokenizer=edit->color->tokenizer; - linedit_color col=LINEDIT_DEFAULTCOLOR; - linedit_token tok; - unsigned int iter=0; - - for (char *c=in->string; c!=NULL && *c!='\0';) { - bool success = (tokenizer) (c, edit->color->tokref, &tok); - /* Get the next token */ - if (success && tok.length>0 && tok.start>=c) { - size_t padding=tok.start-c; - /* If there's leading unrecognized characters, print them. */ - if (tok.start>c) { - col=LINEDIT_DEFAULTCOLOR; - linedit_addcstringwithselection(edit, c, c-in->string, padding, &col, out); - } - - /* Set the color */ - col = linedit_colorfromtokentype(edit, tok.type); - - /* Copy the token across */ - if (tok.length>0) { - linedit_addcstringwithselection(edit, tok.start, tok.start-in->string, tok.length, &col, out); - } - - c=tok.start+tok.length; - } else { - col=LINEDIT_DEFAULTCOLOR; - linedit_addcstringwithselection(edit, c, c-in->string, in->length-(c-in->string), &col, out); - return; - }; - iter++; - if (iter>in->length) { - if (!edit->color->lexwarning) { - fprintf(stderr, "\n\rLinedit error: Syntax colorer appears to be stuck in an infinite loop; ensure the tokenizer returns false if it doesn't recognize a token.\n"); - edit->color->lexwarning=true; - } - return; - } - } -} - -/** Print a string without syntax coloring */ -void linedit_plainstring(lineditor *edit, linedit_string *in, linedit_string *out) { - linedit_addcstringwithselection(edit, in->string, 0, in->length, NULL, out); -} - -/* ********************************************************************** - * Keypresses - * ********************************************************************** */ - -/** Identifies the type of keypress */ -typedef enum { - KEY_UNKNOWN, KEY_CHARACTER, - KEY_RETURN, KEY_TAB, KEY_DELETE, - KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, // Arrow keys - KEY_HOME, KEY_END, // Home and End - KEY_SHIFT_LEFT, KEY_SHIFT_RIGHT, // Shift+arrow key - KEY_CTRL, -} keytype; - -/** A single keypress event obtained and processed by the terminal */ -typedef struct { - keytype type; /** Type of keypress */ - char c[5]; /** Up to four bytes of utf8 encoded unicode plus null terminator */ - int nbytes; /** Number of bytes */ -} keypress; - -#define LINEDIT_KEYPRESSGETCHAR(a) ((a)->c[0]) - -/** Raw codes produced by the terminal */ -enum keycodes { - BACKSPACE_CODE = 8, // Backspace - TAB_CODE = 9, // Tab - RETURN_CODE = 13, // Enter or return - ESC_CODE = 27, // Escape - DELETE_CODE = 127, // Delete - ARROW_CODE = 224 // Windows arrow codes -}; - -/** Enable this macro to get reports on unhandled keypresses */ -//#define LINEDIT_DEBUGKEYPRESS - -/** Initializes a keypress structure */ -void linedit_keypressinit(keypress *out) { - out->type=KEY_UNKNOWN; - for (int i=0; i<5; i++) out->c[i]='\0'; - out->nbytes=0; -} - -/** @brief Read and decode a single keypress from the terminal */ -bool linedit_readkey(lineditor *edit, keypress *out) { - out->type=KEY_UNKNOWN; - - if (linedit_getchar(out->c)) { -#ifdef _WIN32 - if (out->c[0] == -32) { - if (linedit_getchar(out->c)) { - bool shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; - - switch (LINEDIT_KEYPRESSGETCHAR(out)) { - case 'H': out->type = KEY_UP; return true; - case 'P': out->type = KEY_DOWN; return true; - case 'M': out->type = (shift ? KEY_SHIFT_RIGHT : KEY_RIGHT); return true; - case 'K': out->type = (shift ? KEY_SHIFT_LEFT : KEY_LEFT); return true; - default: return false; - } - } - } -#endif - if (iscntrl(LINEDIT_KEYPRESSGETCHAR(out))) { - switch (LINEDIT_KEYPRESSGETCHAR(out)) { - case ESC_CODE: - { /* Escape sequences */ - char seq[LINEDIT_CODESTRINGSIZE]; - - /* Read in the escape sequence */ - for (unsigned int i=0; itype=KEY_SHIFT_RIGHT; - } else if (strncmp(seq, "[1;2D", 5)==0) { - out->type=KEY_SHIFT_LEFT; - } else { -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Extended escape sequence: "); - for (unsigned int i=0; i<10; i++) { - printf("%c", seq[i]); - if (isalpha(seq[i])) break; - } - printf("\n"); -#endif - } - } else { - switch (seq[1]) { - case 'A': out->type=KEY_UP; break; - case 'B': out->type=KEY_DOWN; break; - case 'C': out->type=KEY_RIGHT; break; - case 'D': out->type=KEY_LEFT; break; - default: -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Unhandled escape sequence: %c%c%c\r\n", seq[0], seq[1], seq[2]); -#endif - break; - } - } - } - } - break; - case TAB_CODE: out->type=KEY_TAB; break; - case BACKSPACE_CODE: // v fallthrough - case DELETE_CODE: out->type=KEY_DELETE; break; - case RETURN_CODE: out->type=KEY_RETURN; break; - default: - if (LINEDIT_KEYPRESSGETCHAR(out)>0 && LINEDIT_KEYPRESSGETCHAR(out)<27) { /* Ctrl+KEY_KEY_CHARACTER */ - out->type=KEY_CTRL; - out->c[0]+='A'-1; /* Return the character code */ -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Ctrl+character: %c\r\n", out->c[0]); -#endif - } else { -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Unhandled keypress: %d\r\n", LINEDIT_KEYPRESSGETCHAR(out)); -#endif - } - } - - } else { - out->nbytes=linedit_utf8numberofbytes(out->c); - /* Read in the unicode sequence */ - for (int i=1; inbytes; i++) { - if (!linedit_getchar(&out->c[i])) break; - } - out->type=KEY_CHARACTER; -#ifdef LINEDIT_DEBUGKEYPRESS - printf("Character: %s (%i bytes)\r\n", out->c, out->nbytes); -#endif - } - } - return true; -} - -/* ********************************************************************** - * The line editor - * ********************************************************************** */ - -/* ---------------------------------------- - * Get and set editor state - * ---------------------------------------- */ - -/** @brief Gets the current mode */ -lineditormode linedit_getmode(lineditor *edit) { - return edit->mode; -} - -/** @brief Sets the current mode, setting/clearing any state dependent data */ -void linedit_setmode(lineditor *edit, lineditormode mode) { - if (mode!=LINEDIT_HISTORYMODE) { - if (edit->mode==LINEDIT_HISTORYMODE) { - linedit_stringlistremove(&edit->history, edit->history.first); - } - edit->history.posn=0; - } - if (mode==LINEDIT_SELECTIONMODE) { - if (edit->sposn<0) edit->sposn=edit->posn; - } else { - edit->sposn=-1; - } - edit->mode=mode; -} - -/** Sets the current position - * @param edit - the editor - * @param posn - position to set, or negative to move to end */ -void linedit_setposition(lineditor *edit, int posn) { - edit->posn=(posn<0 ? linedit_stringlength(&edit->current) : posn); -} - -/** @brief Advances the position by delta - * @details We ensure that the current position also lies within the string. */ -void linedit_advanceposition(lineditor *edit, int delta) { - edit->posn+=delta; - if (edit->posn<0) edit->posn=0; - int linewidth = linedit_stringlength(&edit->current); - if (edit->posn>linewidth) edit->posn=linewidth; -} - -/** @brief Checks if we're at the end of the input */ -bool linedit_atend(lineditor *edit) { - return (edit->posn==linedit_stringlength(&edit->current)); -} - -/** @brief Checks if we're at a newline character */ -bool linedit_atnewline(lineditor *edit) { - char *c=linedit_stringlocate(&edit->current, edit->posn); - return (c && *c=='\n'); -} - -/* ---------------------------------------- - * Redraw - * ---------------------------------------- */ - -/** Refreshes the display */ -void linedit_redraw(lineditor *edit) { - int sugglength=0; - linedit_string output; /* Holds the output string */ - linedit_stringinit(&output); - - // Render the current buffer to the output string - linedit_stringdefaulttext(&output); - - if (edit->color) { - linedit_syntaxcolorstring(edit, &edit->current, &output); - } else { - linedit_plainstring(edit, &edit->current, &output); - } - - // Display any autocompletion suggestions at the end - if (linedit_aresuggestionsavailable(edit)) { - char *suggestion = linedit_currentsuggestion(edit); - linedit_stringsetemphasis(&output, LINEDIT_BOLD); - linedit_stringaddcstring(&output, suggestion); - sugglength=(int) strlen(suggestion); - } - - // Reset default text - linedit_stringdefaulttext(&output); - - // Retrieve the display coordinates of the current editing position - int xpos, ypos, nlines; - linedit_stringdisplaycoordinates(edit, &edit->current, edit->posn, &xpos, &ypos); - linedit_stringdisplaycoordinates(edit, &edit->current, -1, NULL, &nlines); - - int promptwidth=linedit_stringdisplaywidth(edit, &edit->prompt); - int stringwidth=linedit_stringlength(&edit->current); - - int start=0, end=promptwidth+stringwidth+sugglength; - - linedit_hidecursor(); - linedit_moveup(ypos); // Move to the starting line - linedit_home(); - linedit_defaulttext(); - linedit_write(edit->prompt.string); - - // Now render the output string - linedit_renderstring(edit, output.string, output.length, start, end); - - linedit_erasetoendofline(); - - linedit_moveup(nlines-ypos); // Move to the cursor position - linedit_movetocolumn(promptwidth+xpos-start); - - linedit_showcursor(); - - linedit_stringclear(&output); -} - -/** @brief Changes the height of the current line editing session, erasing garbage if necessary */ -void linedit_changeheight(lineditor *edit, int oldheight, int newheight, int oldvpos, int newvpos) { - if (oldheight==newheight) { - if (oldvposoldheight) { - for (int i=0; icurrent); - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &vpos); - for (int i=vpos; icurrent, edit->posn); - size_t length=linedit_graphemelength(edit, str, edit->current.string+edit->current.length), count; - - if (!linedit_utf8count(str, length, &count)) return; - edit->posn+=count; -} - -/** @brief Moves the current posn to the previous grapheme */ -void linedit_prevgrapheme(lineditor *edit) { - char *str=linedit_stringlocate(&edit->current, edit->posn); - char *prev=edit->current.string; - - size_t len=0, count; - for (char *c=prev; ccurrent.string+edit->current.length); - if (!len) return; - prev=c; - } - - if (!linedit_utf8count(prev, str-prev, &count)) return; - edit->posn-=count; -} - -/** @brief Process a left keypress */ -void linedit_processarrowkeypress(lineditor *edit, lineditormode mode, int delta) { - linedit_setmode(edit, mode); - if (delta>0) { - linedit_nextgrapheme(edit); - } else linedit_prevgrapheme(edit); - - linedit_atnewline(edit); -} - -/** @brief Change the line */ -void linedit_processchangeline(lineditor *edit, int delta) { - int x, yinit, y; - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringcoordinates(&edit->current, edit->posn, &x, &yinit); - y=yinit+delta; - if (y<0) y=0; - linedit_stringfindposition(&edit->current, x, y, &edit->posn); -} - -/** @brief Obtain and process a single keypress */ -bool linedit_processkeypress(lineditor *edit) { - keypress key; - bool regeneratesuggestions=true; - - linedit_keypressinit(&key); - - do { - if (linedit_readkey(edit, &key)) { - switch (key.type) { - case KEY_CHARACTER: - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringinsert(&edit->current, edit->posn, key.c, key.nbytes); - linedit_advanceposition(edit, 1); - break; - case KEY_DELETE: - if (linedit_getmode(edit)==LINEDIT_SELECTIONMODE) { - /* Delete the selection */ - int lposn=(edit->sposn < edit->posn ? edit->sposn : edit->posn); - int rposn = (edit->sposn < edit->posn ? edit->posn : edit->sposn); - linedit_stringdelete(&edit->current, lposn, rposn-lposn); - edit->posn=lposn; - } else { - /* Delete a character */ - if (edit->posn>0) { - linedit_stringdelete(&edit->current, edit->posn-1, 1); - linedit_advanceposition(edit, -1); - } - } - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - break; - case KEY_LEFT: - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, -1); - break; - case KEY_RIGHT: - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, +1); - break; - case KEY_SHIFT_LEFT: - linedit_processarrowkeypress(edit, LINEDIT_SELECTIONMODE, -1); - break; - case KEY_SHIFT_RIGHT: - linedit_processarrowkeypress(edit, LINEDIT_SELECTIONMODE, +1); - break; - case KEY_UP: - { - if (linedit_getmode(edit)!=LINEDIT_HISTORYMODE) { - linedit_setmode(edit, LINEDIT_HISTORYMODE); - linedit_historyadd(edit, (edit->current.string ? edit->current.string : "")); - } - - linedit_historyadvance(edit, 1); - linedit_setposition(edit, -1); - } - break; - case KEY_DOWN: - if (linedit_getmode(edit)==LINEDIT_HISTORYMODE) { - linedit_historyadvance(edit, -1); - linedit_setposition(edit, -1); - } else if (linedit_aresuggestionsavailable(edit)) { - linedit_advancesuggestions(edit, 1); - regeneratesuggestions=false; - } - break; - case KEY_RETURN: - if (linedit_shouldmultiline(edit)) { - linedit_stringaddcstring(&edit->current, "\n"); - linedit_advanceposition(edit, +1); - } else return false; - break; - case KEY_TAB: - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - /* If suggestions are available (i.e. we're at the end of the line)... */ - if (linedit_aresuggestionsavailable(edit)) { - char *sugg = linedit_currentsuggestion(edit); - if (sugg) { - linedit_stringaddcstring(&edit->current, sugg); - linedit_movetoend(edit); - } - } else { // Otherwise simply add a tab character - linedit_stringinsert(&edit->current, edit->posn, "\t", 1); - linedit_advanceposition(edit, +1); - } - break; - case KEY_CTRL: /* Handle ctrl+letter combos */ - switch(LINEDIT_KEYPRESSGETCHAR(&key)) { - case 'A': /* Move to start of line */ - { - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - - int line; - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &line); - linedit_stringfindposition(&edit->current, 0, line, &edit->posn); - } - break; - case 'B': /* Move backward */ - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, -1); - break; - case 'C': /* Copy */ - if (linedit_getmode(edit)==LINEDIT_SELECTIONMODE) { - int lposn=(edit->sposn < edit->posn ? edit->sposn : edit->posn); - int rposn = (edit->sposn < edit->posn ? edit->posn : edit->sposn); - size_t lindx, rindx; - if (!linedit_stringutf8index(&edit->current, lposn, 0, &lindx)) break; - if (!linedit_stringutf8index(&edit->current, rposn, 0, &rindx)) break; - linedit_stringclear(&edit->clipboard); - linedit_stringappend(&edit->clipboard, edit->current.string+lindx, (size_t) rindx-lindx); - } - break; - case 'D': /* Delete the character underneath the cursor */ - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringdelete(&edit->current, edit->posn, 1); - break; - case 'E': { /* Move to end of line */ - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - int line; - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &line); - linedit_stringfindposition(&edit->current, -1, line, &edit->posn); - } - break; - case 'F': /* Move forward */ - linedit_processarrowkeypress(edit, LINEDIT_DEFAULTMODE, +1); - break; - case 'G': { /* Abort current editing session */ - linedit_stringclear(&edit->current); - edit->posn=0; - return false; - } - case 'L': /* Clear buffer */ - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_stringclear(&edit->current); - edit->posn=0; - break; - case 'N': /* Next line */ - linedit_processchangeline(edit, 1); - break; - case 'P': /* Previous line */ - linedit_processchangeline(edit, -1); - break; - case 'V': /* Paste */ // v fallthrough - case 'Y': - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - if (edit->clipboard.length>0) { - linedit_stringinsert(&edit->current, edit->posn, edit->clipboard.string, edit->clipboard.length); - linedit_advanceposition(edit, linedit_stringlength(&edit->clipboard)); - } - break; - default: break; - } - break; - default: - break; - } - } - } while (linedit_keypressavailable()); - - if (regeneratesuggestions) linedit_generatesuggestions(edit); - - return true; -} - -/* ---------------------------------------- - * Main loops for different terminal types - * ---------------------------------------- */ - -/** If we're not attached to a terminal, e.g. a pipe, simply read the - file in. */ -void linedit_noterminal(lineditor *edit) { - int c; - linedit_stringclear(&edit->current); - do { - c = fgetc(stdin); - if (c==EOF || c=='\n') return; - char a = (char) c; - linedit_stringappend(&edit->current, &a, 1); - } while (true); -} - -/** If the terminal is unsupported, default to fgets with a fixed buffer */ -#define LINEDIT_UNSUPPORTEDBUFFER 4096 -void linedit_unsupported(lineditor *edit) { - char buffer[LINEDIT_UNSUPPORTEDBUFFER]; - printf("%s",edit->prompt.string); - if (fgets(buffer, LINEDIT_UNSUPPORTEDBUFFER, stdin)==buffer) { - int length=(int) strlen(buffer); - if (length>0) for (length--; length>=0 && iscntrl(buffer[length]); length--) { - buffer[length]='\0'; /* Remove trailing ctrl chars */ - } - linedit_stringaddcstring(&edit->current, buffer); - } -} - -/** Normal interface used if terminal is present */ -void linedit_supported(lineditor *edit) { - linedit_enablerawmode(); - - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_getterminalwidth(edit); - linedit_setposition(edit, 0); - linedit_redraw(edit); - - int vpos=0, nlines=0; // Keep track of the current vertical position and line number - - while (linedit_processkeypress(edit)) { - int newvpos; - linedit_stringcoordinates(&edit->current, edit->posn, NULL, &newvpos); - int newnlines=linedit_stringcountlines(&edit->current); - linedit_changeheight(edit, nlines, newnlines, vpos, newvpos); - - linedit_redraw(edit); - vpos=newvpos; nlines=newnlines; - } - - /* Ensure we're always on the last line of the input when redrawing before exit */ - linedit_movetoend(edit); - - /* Remove any dangling suggestions */ - linedit_stringlistclear(&edit->suggestions); - linedit_setmode(edit, LINEDIT_DEFAULTMODE); - linedit_redraw(edit); - - linedit_disablerawmode(); - - if (edit->current.length>0) { - linedit_historyadd(edit, edit->current.string); - } - - linedit_linefeed(); // Move to next line -} - -/* ********************************************************************** - * Public interface - * ********************************************************************** */ - -/** Initialize a line editor */ -void linedit_init(lineditor *edit) { - if (!edit) return; - edit->color=NULL; - edit->ncols=0; - linedit_stringlistinit(&edit->history); - linedit_stringlistinit(&edit->suggestions); - edit->mode=LINEDIT_DEFAULTMODE; - linedit_stringinit(&edit->current); - linedit_stringinit(&edit->prompt); - linedit_stringinit(&edit->cprompt); - linedit_stringinit(&edit->clipboard); - linedit_setprompt(edit, LINEDIT_DEFAULTPROMPT); - edit->completer=NULL; - edit->cref=NULL; - edit->multiline=NULL; - edit->mlref=NULL; - edit->graphemefn=NULL; - linedit_graphemeinit(&edit->graphemedict); -} - -/** Finalize a line editor */ -void linedit_clear(lineditor *edit) { - if (!edit) return; - if (edit->color) { - free(edit->color); - edit->color=NULL; - } - linedit_historyclear(edit); - linedit_stringlistclear(&edit->suggestions); - linedit_stringclear(&edit->current); - linedit_stringclear(&edit->prompt); - linedit_stringclear(&edit->cprompt); - linedit_stringclear(&edit->clipboard); - linedit_graphemeclear(&edit->graphemedict); -} - -/** Public interface to the line editor. - * @param edit - a line editor that has been initialized with linedit_init. - * @returns the string input by the user, or NULL if nothing entered. */ -char *linedit(lineditor *edit) { - if (!edit) return NULL; /** Ensure we are not passed a NULL pointer */ - - linedit_stringclear(&edit->current); - - switch (linedit_checksupport()) { - case LINEDIT_NOTTTY: linedit_noterminal(edit); break; - case LINEDIT_UNSUPPORTED: linedit_unsupported(edit); break; - case LINEDIT_SUPPORTED: linedit_supported(edit); break; - } - - return linedit_cstring(&edit->current); -} - -/** @brief Configures syntax coloring - * @param[in] edit Line editor to configure - * @param[in] tokenizer Callback function that will identify the next token from a string - * @param[in] ref Reference that will be passed to the tokenizer callback function. - * @param[in] map Map from token types to colors */ -void linedit_syntaxcolor(lineditor *edit, linedit_tokenizefn tokenizer, void *ref, linedit_colormap *map) { - if (!edit) return; - if (!map) return; - if (edit->color) free(edit->color); - int ncols; - - for (ncols=0; map[ncols].type!=LINEDIT_ENDCOLORMAP; ncols++); - - edit->color = malloc(sizeof(linedit_syntaxcolordata)+ncols*sizeof(linedit_colormap)); - - if (!edit->color) return; - - edit->color->tokenizer=tokenizer; - edit->color->tokref=ref; - edit->color->ncols=ncols; - edit->color->lexwarning=false; - for (unsigned int i=0; icolor->col[i]=map[i]; - } - - qsort(edit->color->col, ncols, sizeof(linedit_colormap), linedit_colormapcmp); -} - -/** @brief Configures autocomplete - * @param[in] edit Line editor to configure - * @param[in] completer Callback function that will identify autocomplete suggestions - * @param[in] ref Reference that will be passed to the autocomplete callback function. */ -void linedit_autocomplete(lineditor *edit, linedit_completefn completer, void *ref) { - if (!edit) return; - edit->completer=completer; - edit->cref=ref; -} - -/** @brief Configures multiline editing - * @param[in] edit Line editor to configure - * @param[in] multiline Callback function to test whether to enter multiline mode - * @param[in] ref Reference that will be passed to the multiline callback function. - * @param[in] cprompt Continuation prompt, or NULL to just reuse the regular prompt */ -void linedit_multiline(lineditor *edit, linedit_multilinefn multiline, void *ref, char *cprompt) { - edit->multiline=multiline; - edit->mlref=ref; - linedit_stringclear(&edit->cprompt); - if (cprompt) { - linedit_stringaddcstring(&edit->cprompt, cprompt); - } else { - linedit_stringaddcstring(&edit->cprompt, edit->prompt.string); - } -} - -/** @brief Adds a completion suggestion - * @param completion completion data structure - * @param string string to add */ -void linedit_addsuggestion(linedit_stringlist *completion, char *string) { - linedit_stringlistadd(completion, string); -} - -/** @brief Sets the prompt - * @param edit Line editor to configure - * @param prompt prompt string to use */ -void linedit_setprompt(lineditor *edit, char *prompt) { - if (!edit) return; - linedit_stringclear(&edit->prompt); - linedit_stringaddcstring(&edit->prompt, prompt); -} - -/** @brief Sets the grapheme splitter to use - * @param[in] edit Line editor to configure - * @param[in] graphemefn Grapheme splitter to use */ -void linedit_setgraphemesplitter(lineditor *edit, linedit_graphemefn graphemefn) { - if (!edit) return; - edit->graphemefn=graphemefn; -} - -/** @brief Displays a string with a given color and emphasis - * @param edit Line editor in use - * @param string String to display */ -void linedit_displaywithstyle(lineditor *edit, char *string, linedit_color col, linedit_emphasis emph) { - if (linedit_checksupport()==LINEDIT_SUPPORTED) { - linedit_string out; - linedit_stringinit(&out); - linedit_stringsetcolor(&out, col); - linedit_stringsetemphasis(&out, emph); - linedit_stringaddcstring(&out, string); - linedit_stringdefaulttext(&out); - - printf("%s", out.string); - - linedit_stringclear(&out); - } else { - printf("%s", string); - } -} - -/** @brief Displays a string with syntax coloring - * @param edit Line editor in use - * @param string String to display - */ -void linedit_displaywithsyntaxcoloring(lineditor *edit, char *string) { - if (linedit_checksupport()==LINEDIT_SUPPORTED) { - linedit_string in, out; - linedit_stringinit(&in); - linedit_stringinit(&out); - linedit_stringaddcstring(&in, string); - - linedit_syntaxcolorstring(edit, &in, &out); - linedit_stringdefaulttext(&out); - printf("%s", out.string); - - linedit_stringclear(&in); - linedit_stringclear(&out); - } else { - printf("%s", string); - } -} - -/** @brief Gets the terminal width - * @param edit Line editor in use - * @returns The width in characters */ -int linedit_getwidth(lineditor *edit) { - linedit_getterminalwidth(edit); - return edit->ncols; -} - -/** @brief Checks whether the underlying terminal is a TTY - * @param[in] edit Line editor to use - * @returns true if stdin and stdout are ttys */ -bool linedit_checktty(void) { - return linedit_checksupport()!=LINEDIT_NOTTTY; -} diff --git a/src/linedit.h b/src/linedit.h deleted file mode 100644 index c732a17..0000000 --- a/src/linedit.h +++ /dev/null @@ -1,285 +0,0 @@ -/** @file linedit.h - * @author T J Atherton - * - * @brief A simple UTF8 aware line editor with history, completion, multiline editing and syntax highlighting -*/ - -#ifndef linedit_h -#define linedit_h - -#include -#include -#include -#include -#include -#include - -/* ********************************************************************** - * Types - * ********************************************************************** */ - -/* ----------------------- - * Linedit strings - * ----------------------- */ - -typedef struct linedit_string_s linedit_string; - -/** lineditor strings */ -struct linedit_string_s { - size_t capacity; /** Capacity of the string in bytes */ - size_t length; /** Length in bytes */ - char *string; /** String data */ - linedit_string *next; /** Enable strings to be chained together */ -} ; - -/** A list of strings */ -typedef struct { - int posn; /* We use this to keep track of where in the list the user is */ - linedit_string *first; -} linedit_stringlist; - -/* ----------------------- - * Tokenization - * ----------------------- */ - -/** Token types */ -typedef int linedit_tokentype; - -/** lineditor tokens */ -typedef struct { - linedit_tokentype type; - char *start; - size_t length; -} linedit_token; - -/** @brief Tokenizer callback function - * @param in - a string - * @param ref - pointer to a reference structure provided to linedit by the user - * @param tok - pointer to a token structure that the caller should fill out. - * @details This user function is called when linedit needs to tokenize a string. - * The function should identify the next token in the string and fill out - * the following fields: - * tok->type - should contain the token type. This is used e.g. - * an index to the color array. - * tok->start - should point to the first significant character - * in the token - * tok->length - should contain the length of the token, in bytes - * The function should return true if a token was successfully processed or - * false otherwise. */ -typedef bool (*linedit_tokenizefn) (char *in, void *ref, linedit_token *tok); - -/* ----------------------- - * Color - * ----------------------- */ - -/** Colors */ -typedef enum { - LINEDIT_BLACK, - LINEDIT_RED, - LINEDIT_GREEN, - LINEDIT_YELLOW, - LINEDIT_BLUE, - LINEDIT_MAGENTA, - LINEDIT_CYAN, - LINEDIT_WHITE, - LINEDIT_DEFAULTCOLOR, -} linedit_color; - -typedef enum { - LINEDIT_BOLD, - LINEDIT_UNDERLINE, - LINEDIT_REVERSE, - LINEDIT_NONE -} linedit_emphasis; - -#define LINEDIT_ENDCOLORMAP -1 - -typedef struct { - linedit_tokentype type; - linedit_color col; -} linedit_colormap; - -/** Structure to hold all information related to syntax coloring */ -typedef struct { - linedit_tokenizefn tokenizer; /** A tokenizer function */ - void *tokref; /** Reference passed to tokenizer callback function */ - bool lexwarning; - unsigned int ncols; /** Number of colors provided */ - linedit_colormap col[]; /** Flexible array member mapping token types to colors */ -} linedit_syntaxcolordata; - -/* ----------------------- - * Completion - * ----------------------- */ - -/** @brief Autocompletion callback function - * @param[in] in - a string - * @param[in] ref - pointer to a reference structure provided to linedit by the user - * @param[out] completion - autocompletion structure - * @details This user function is called when linedit requests autocompletion - * of a string. The function should identify any possible suggestions - * and call linedit_addcompletion to add them one by one. - * - * Only *remaining* characters from the suggestion should be added, - * e.g. for "hello" if the user has typed "he" the function should add - * "llo" as a suggestion. - * - * The function should return true if autocompletion was successfully - * processed or false otherwise. -*/ -typedef bool (*linedit_completefn) (char *in, void *ref, linedit_stringlist *completion); - -/* ----------------------- - * Multiline callback - * ----------------------- */ - -/** @brief Multiline callback function - * @param[in] in - a string - * @param[in] ref - pointer to a reference structure provided by the user - * @details This user function is called when linedit wants to know whether - * it should enter multiline mode. The function should parse the - * input and return true if linedit should go to multiline mode or - * false otherwise. Typically, return true if the input is incomplete -*/ -typedef bool (*linedit_multilinefn) (char *in, void *ref); - -/* ----------------------- - * Unicode grapheme support - * ----------------------- */ - -/** Grapheme dictionary entry */ -typedef struct { - char *grapheme; - int width; -} linedit_graphemeentry; - -/** Grapheme dictionary */ -typedef struct { - int count; - int capacity; - linedit_graphemeentry *contents; -} linedit_graphemedictionary; - -/** @brief Unicode grapheme splitter - * @param[in] in - a string - * @param[in] end - end of string - * @returns offset to next grapheme - * @details If provided, linedit will use this function to split UTF8 code into graphemes, which enables length estimation. -*/ -typedef size_t (*linedit_graphemefn) (const char *in, const char *end); - -/* ----------------------- - * lineditor structure - * ----------------------- */ - -#define LINEDIT_DEFAULTPROMPT ">" - -/** Keep track of what the line editor is doing */ -typedef enum { - LINEDIT_DEFAULTMODE, - LINEDIT_SELECTIONMODE, - LINEDIT_HISTORYMODE -} lineditormode; - -/** Holds all state information needed for a line editor */ -typedef struct { - lineditormode mode; /** Current editing mode */ - int posn; /** Position of the cursor in UTF8 characters */ - int sposn; /** Starting point of a selection */ - int ncols; /** Number of columns */ - linedit_string prompt; /** The prompt */ - linedit_string cprompt; /** Continuation prompt */ - - linedit_string current; /** Current string that's being edited */ - linedit_string clipboard;/** Copy/paste clipboard */ - - linedit_stringlist history; /** History list */ - linedit_stringlist suggestions; /** Autocompletion suggestions */ - - linedit_syntaxcolordata *color; /** Structure to handle syntax coloring */ - - linedit_completefn completer; /** Autocompletion callback function*/ - void *cref; /** Reference for autocompletion callback function */ - - linedit_multilinefn multiline; /** Multiline callback */ - void *mlref; /** Reference for multiline callback function */ - - linedit_graphemefn graphemefn; /** Grapheme splitting */ - linedit_graphemedictionary graphemedict; /** Grapheme dictionary */ -} lineditor; - -/* ********************************************************************** - * Public interface - * ********************************************************************** */ - -/** Public interface to the line editor. - * @param[in] edit - A line editor that has been initialized with linedit_init. - * @returns the string input by the user, or NULL if nothing entered. */ -char *linedit(lineditor *edit); - -/** @brief Configures syntax coloring - * @param[in] edit Line editor to configure - * @param[in] tokenizer Callback function that will identify the next token from a string - * @param[in] ref Reference that will be passed to the tokenizer callback function. - * @param[in] map Map from token types to colors */ -void linedit_syntaxcolor(lineditor *edit, linedit_tokenizefn tokenizer, void *ref, linedit_colormap *cols); - -/** @brief Configures autocomplete - * @param[in] edit Line editor to configure - * @param[in] completer Callback function that will identify autocomplete suggestions - * @param[in] ref Reference that will be passed to the autocomplete callback function. */ -void linedit_autocomplete(lineditor *edit, linedit_completefn completer, void *ref); - -/** @brief Configures multiline editing - * @param[in] edit Line editor to configure - * @param[in] multiline Callback function to test whether to enter multiline mode - * @param[in] ref Reference that will be passed to the multiline callback function. - * @param[in] cprompt Continuation prompt, or NULL to just reuse the regular prompt */ -void linedit_multiline(lineditor *edit, linedit_multilinefn multiline, void *ref, char *cprompt); - -/** @brief Adds a completion suggestion - * @param[in] completion Completion data structure - * @param[in] string String to add */ -void linedit_addsuggestion(linedit_stringlist *completion, char *string); - -/** @brief Sets the prompt - * @param[in] edit Line editor to configure - * @param[in] prompt Prompt string to use */ -void linedit_setprompt(lineditor *edit, char *prompt); - -/** @brief Sets the grapheme splitter to use - * @param[in] edit Line editor to configure - * @param[in] graphemefn Grapheme splitter to use */ -void linedit_setgraphemesplitter(lineditor *edit, linedit_graphemefn graphemefn); - -/** @brief Displays a string with a given color and emphasis - * @param[in] edit Line editor to use - * @param[in] string String to display - * @param[in] col Color - * @param[in] emph Emphasis */ -void linedit_displaywithstyle(lineditor *edit, char *string, linedit_color col, linedit_emphasis emph); - -/** @brief Displays a string with syntax coloring - * @param[in] edit Line editor to use - * @param[in] string String to display */ -void linedit_displaywithsyntaxcoloring(lineditor *edit, char *string); - -/** @brief Gets the terminal width - * @param[in] edit Line editor to use - * @returns The width in characters */ -int linedit_getwidth(lineditor *edit); - -/** @brief Checks whether the underlying terminal is a TTY - * @returns true if stdin and stdout are ttys */ -bool linedit_checktty(void); - -/** Enable UTF8 output */ -void linedit_setutf8(void); - -/** Initialize a line editor */ -void linedit_init(lineditor *edit); - -/** Finalize a line editor */ -void linedit_clear(lineditor *edit); - -#endif /* linedit_h */ diff --git a/src/main.c b/src/main.c index 18ef445..e29103b 100644 --- a/src/main.c +++ b/src/main.c @@ -1,77 +1,296 @@ /** @file main.c * @author T J Atherton * - * @brief Main entry point + * @brief Main entry point and process options */ #include -#include +#include +#include +#include +#include +#include #include #include "cli.h" #include "debugger.h" +#include "inline.h" -int main(int argc, const char * argv[]) { +/** Context passed to option handlers that need argc/argv (e.g. eval). */ +typedef struct { + int argc; + const char **argv; + int *idx; +} opt_ctx; + +/** Option handler. arg is NULL for options that take no argument. + * @returns true to continue (run file/repl), false to exit without running. */ +typedef bool (*optionfn)(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx); + +static bool opt_version(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)flags; (void)ctx; + version v; + morpho_version(&v); + char buf[VERSION_MAXSTRINGLENGTH]; + version_tostring(&v, VERSION_MAXSTRINGLENGTH, buf); + printf("Morpho v%s\n", buf); + return false; +} + +static bool opt_help(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; + /* Optional query: morpho6 --help [topic] looks up built-in language help. */ + if (ctx && ctx->idx && *ctx->idx + 1 < ctx->argc) { + const char *next = ctx->argv[*ctx->idx + 1]; + if (next && next[0] != '-') { + (*ctx->idx)++; + cli_helpquery(next, *flags); + return false; + } + } + printf("Usage: morpho6 [options] [file] [options passed to program]\n"); + printf("\nOptions:\n"); + printf(" -h, --help [query] Show this help, or look up a language help topic\n"); + printf(" -v, --version Show version information\n"); + printf(" -c, --check Check syntax without executing\n"); + printf(" -e, --eval Execute code string (before file/REPL if combined)\n"); + printf(" -i, --interactive Enter REPL after running file\n"); + printf(" -l, --list List file with syntax highlighting\n"); + printf(" -d, --disassemble Show disassembly\n"); + printf(" -D Disassemble only (no execution)\n"); + printf(" -dl Disassemble with source listing\n"); + printf(" -debug, --debug Enable debugger\n"); + printf(" -O, --optimize Enable optimizations\n"); + printf(" -profile, --profile Enable profiling\n"); + printf(" --no-color Disable syntax highlighting\n"); + printf(" -w, --workers Set number of worker threads\n"); + printf("\nIf no file is specified, morpho enters interactive REPL mode.\n"); + printf("If stdin is piped or redirected, morpho reads and executes from stdin.\n"); + printf("Any options after the file name are passed to the morpho program.\n"); + return false; +} + +static bool opt_disassembleonly(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags &= ~CLI_RUN; + *flags |= CLI_DISASSEMBLE; + return true; +} + +static bool opt_disassemblelist(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_DISASSEMBLE | CLI_DISASSEMBLESHOWSRC; + return true; +} + +static bool opt_disassemble(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_DISASSEMBLE; + return true; +} + +static bool opt_debug(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_DEBUG; + return true; +} + +static bool opt_optimize(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_OPTIMIZE; + return true; +} + +static bool opt_profile(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_PROFILE; + return true; +} + +static bool opt_workers(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)flags; (void)ctx; + if (!arg || !isdigit((unsigned char)*arg)) { + fprintf(stderr, "morpho: -w/--workers requires a number.\n"); + cli_setexitcode(EXIT_FAILURE); + return false; + } + int n = atoi(arg); + if (n < 0) n = 0; + morpho_setthreadnumber(n); + return true; +} + +static bool opt_nocolor(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_NOCOLOR; + return true; +} + +static bool opt_check(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags &= ~CLI_RUN; /* Clear RUN flag - compile only, don't execute */ + return true; +} + +static bool opt_interactive(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)arg; (void)ctx; + *flags |= CLI_INTERACTIVE; // Enter REPL after running file + return true; +} + +static bool opt_list(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)ctx; + if (!arg) { + cli_setexitcode(EXIT_FAILURE); + return false; + } + + char *src = cli_loadsource(arg); + if (src) { + cli_list(src, 1, INT_MAX, *flags); + MORPHO_FREE(src); + } else { + fprintf(stderr, "morpho: Could not open file '%s'\n", arg); + cli_setexitcode(EXIT_FAILURE); + } + return false; // Don't run program after listing +} + +static varray_char evalcode; + +static bool opt_eval(const char *opt, const char *arg, clioptions *flags, opt_ctx *ctx) { + (void)opt; (void)flags; (void)ctx; + if (!arg) return false; + /* Accumulate -e snippets; they are compiled before the file/REPL. */ + if (evalcode.count > 0) { + evalcode.count--; /* drop trailing NUL */ + varray_charwrite(&evalcode, '\n'); + } + varray_charadd(&evalcode, (char *) arg, (int) strlen(arg)); + varray_charwrite(&evalcode, '\0'); + return true; +} + +typedef struct { + const char *s, *l; + bool takes_arg; + optionfn fn; +} option_t; + +static const option_t opt_table[] = { + { "-D", NULL, false, opt_disassembleonly }, + { "-dl", NULL, false, opt_disassemblelist }, + { "-debug", "--debug", false, opt_debug }, + { "-d", "--disassemble", false, opt_disassemble }, + { "-c", "--check", false, opt_check }, + { "-e", "--eval", true, opt_eval }, + { "-h", "--help", false, opt_help }, + { "-i", "--interactive", false, opt_interactive }, + { "-l", "--list", true, opt_list }, + { "-O", "--optimize", false, opt_optimize }, + { "-profile", "--profile", false, opt_profile }, + { NULL, "--no-color", false, opt_nocolor }, + { "-v", "--version", false, opt_version }, + { "-w", "--workers", true, opt_workers }, + { NULL, NULL, false, NULL }, +}; + +/** Match argv token to an option name. Exact match, or for options that take an + * argument: name=value, or (short options only) -w4 with the value attached. */ +static bool option_matches(const char *arg, const char *name, bool takes_arg, const char **attached) { + if (!name) return false; + size_t n = strlen(name); + if (strncmp(arg, name, n) != 0) return false; + if (arg[n] == '\0') { + *attached = NULL; + return true; + } + if (!takes_arg) return false; + if (arg[n] == '=') { + *attached = arg + n + 1; + return true; + } + /* Short option with attached value, e.g. -w4 */ + if (name[0] == '-' && name[1] != '-' && isdigit((unsigned char)arg[n])) { + *attached = arg + n; + return true; + } + return false; +} + +/** Parse one option at argv[*idx]. If option takes an argument, consumes *idx+1 and advances *idx. */ +static bool parse_option(int argc, const char *argv[], int *idx, clioptions *flags) { + const char *arg = argv[*idx], *opt_arg = NULL; + for (int j = 0; opt_table[j].s || opt_table[j].l; j++) { + const char *attached = NULL; + bool match = option_matches(arg, opt_table[j].s, opt_table[j].takes_arg, &attached) || + option_matches(arg, opt_table[j].l, opt_table[j].takes_arg, &attached); + if (!match) continue; + + if (opt_table[j].takes_arg) { + if (attached) { + opt_arg = attached; + } else if (*idx + 1 >= argc) { + fprintf(stderr, "morpho: %s requires an argument.\n", arg); + cli_setexitcode(EXIT_FAILURE); + return false; + } else { + opt_arg = argv[*idx + 1]; + (*idx)++; + } + } + opt_ctx ctx = { argc, argv, idx }; + return opt_table[j].fn(arg, opt_arg, flags, &ctx); + } + fprintf(stderr, "morpho: Unknown option %s.\n", arg); + cli_setexitcode(EXIT_FAILURE); + return false; +} + +int main(int argc, const char *argv[]) { clioptions opt = CLI_RUN; const char *file = NULL; - int i=0; - + int i = 1; + bool run = true; + morpho_initialize(); - clidebugger_initialize(); - - /* Process command line arguments */ - for (i=1; i 0) ? evalcode.data : NULL; + + if (file) { + cli_run(file, opt, preamble); + } else if (!inline_checktty()) { + // stdin is piped/redirected - read and execute from stdin + char *src = cli_loadstdin(); + if (src || preamble) { + cli_runstring(src, opt, preamble); + if (src) MORPHO_FREE(src); } + } else if (preamble && !(opt & CLI_INTERACTIVE)) { + // -e alone on a TTY: run the snippet and exit + cli_runstring(preamble, opt, NULL); } else { - file = option; - break; + // stdin is a TTY - enter REPL (optionally after -e preamble) + cli(opt, preamble); } } - - if (i