diff --git a/autogen.sh b/autogen.sh index 47736e2..318d661 100755 --- a/autogen.sh +++ b/autogen.sh @@ -1,3 +1,5 @@ +#!/bin/bash + # This file is part of SmallBASIC # # Copyright(C) 2001-2020 Chris Warren-Smith. diff --git a/configure.ac b/configure.ac index a9f12a5..e3f5f79 100644 --- a/configure.ac +++ b/configure.ac @@ -33,6 +33,27 @@ function checkDebugMode() { AC_SUBST(CFLAGS) } +function generate_wayland_protocols() { + RAYLIB_SRC_PATH="${srcdir}/raylib/raylib/src" + WL_PROTOCOLS_DIR="${RAYLIB_SRC_PATH}/external/glfw/deps/wayland" + AC_MSG_NOTICE([Generating Wayland protocol headers]) + wl_generate() { + protocol="$1" + basename="$2" + "$WAYLAND_SCANNER" client-header "$protocol" "$RAYLIB_SRC_PATH/$basename.h" || exit 1 + "$WAYLAND_SCANNER" private-code "$protocol" "$RAYLIB_SRC_PATH/$basename-code.h" || exit 1 + } + wl_generate "$WL_PROTOCOLS_DIR/wayland.xml" wayland-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/xdg-shell.xml" xdg-shell-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/xdg-decoration-unstable-v1.xml" xdg-decoration-unstable-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/viewporter.xml" viewporter-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/relative-pointer-unstable-v1.xml" relative-pointer-unstable-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/pointer-constraints-unstable-v1.xml" pointer-constraints-unstable-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/fractional-scale-v1.xml" fractional-scale-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/xdg-activation-v1.xml" xdg-activation-v1-client-protocol + wl_generate "$WL_PROTOCOLS_DIR/idle-inhibit-unstable-v1.xml" idle-inhibit-unstable-v1-client-protocol +} + AC_ARG_WITH(mlpack, [AS_HELP_STRING([--with-mlpack], [Build the mlpack module])], [MLPACK="yes"], @@ -66,14 +87,22 @@ case "${host_os}" in *) PLATFORM_LDFLAGS="-Wl,--no-undefined -avoid-version" CLIPBOARD_LDFLAGS="`pkg-config xcb --libs` -lpthread" - NUKLEAR_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11" WEBSOCKET_LDFLAGS="" GTK_SERVER_LDFLAGS="`pkg-config --libs gtk+-3.0` -lXm -lXt" GTK_SERVER_CPPFLAGS="`pkg-config --cflags gtk+-3.0` -DGTK_SERVER_FFI -DGTK_SERVER_LIBRARY -DGTK_SERVER_UNIX -DGTK_SERVER_GTK3x" - RAYLIB_LDFLAGS="-lGL -lm -lpthread -ldl -lrt -lX11" + RAYLIB_LDFLAGS="`pkg-config wayland-client wayland-cursor wayland-egl xkbcommon --libs`" JVM_CPPFLAGS="-I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux" JVM_LDFLAGS="-L/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/amd64/server -ljvm" - NUKLEAR_CPPFLAGS="-D_GLFW_X11=1" + NUKLEAR_CPPFLAGS="-D_GLFW_WAYLAND=1" + NUKLEAR_LDFLAGS="`pkg-config wayland-client wayland-cursor wayland-egl xkbcommon --libs`" + + AC_ARG_VAR([WAYLAND_SCANNER], [Path to wayland-scanner]) + AC_PATH_PROG([WAYLAND_SCANNER], [wayland-scanner]) + AS_IF([test -n "$WAYLAND_SCANNER"], [ + generate_wayland_protocols + ], [ + AC_MSG_WARN([wayland-scanner not found; Wayland support disabled]) + ]) esac AC_SUBST(DEBUG_LDFLAGS) diff --git a/glfw/Makefile.am b/glfw/Makefile.am index dc0dbaa..35ff783 100644 --- a/glfw/Makefile.am +++ b/glfw/Makefile.am @@ -7,6 +7,7 @@ AM_CXXFLAGS=-fno-rtti -std=c++14 AM_CPPFLAGS = \ + -I../raylib/raylib/src \ -I../raylib/raylib/src/external/glfw/include \ -I../raylib/raylib/src/external/glfw/deps \ -Wall -Wextra -Wshadow -Wdouble-promotion -Wno-unused-parameter -D_GLFW_BUILD_DLL=1 diff --git a/glfw/main.cpp b/glfw/main.cpp index 2a49198..07f8872 100644 --- a/glfw/main.cpp +++ b/glfw/main.cpp @@ -404,3 +404,7 @@ SBLIB_API void sblib_ellipse(int xc, int yc, int xr, int yr, int fill) { glEnd(); } +SBLIB_API int sblib_has_window_ui(void) { + // module creates a UI in a new window + return 1; +} diff --git a/gtk-server/uthash b/gtk-server/uthash index 41c357f..6d85739 160000 --- a/gtk-server/uthash +++ b/gtk-server/uthash @@ -1 +1 @@ -Subproject commit 41c357fd74ade4f4b4822c4407d2f51c4558e18d +Subproject commit 6d8573997c21f24c7e4ec9e48734b44f384170a1 diff --git a/include/module.h b/include/module.h index d27ea52..5a81801 100644 --- a/include/module.h +++ b/include/module.h @@ -25,6 +25,15 @@ extern "C" { */ int sblib_init(const char *sourceFile); +/** + * @ingroup modstd + * + * Returns whether the module is compatible with IDE builds + * + * @return non-zero on success + */ +int sblib_has_window_ui(void); + /** * @ingroup modstd * @@ -111,12 +120,22 @@ int sblib_func_exec(int index, int param_count, slib_par_t *params, var_t *retva /** * @ingroup modlib * - * executes a function + * free resources associated with the variable + * + * @param cls_id the variable class identifier + * @param id the variable instance identifier + */ +int sblib_free(int cls_id, int id); + +/** + * @ingroup modlib + * + * registers a fresh id to replace the given id * * @param cls_id the variable class identifier * @param id the variable instance identifier */ -void sblib_free(int cls_id, int id); +int sblib_refresh_id(int cls_id, int id); /** * @ingroup modlib diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 56ee204..562ba3f 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -130,6 +130,8 @@ target_include_directories(llm PRIVATE target_link_libraries(llm PRIVATE llama ggml + # force dynamic libm + -Wl,-Bdynamic,-lm ) # Include all static code into plugin @@ -147,27 +149,118 @@ set_target_properties(llm PROPERTIES ) # ----------------------------- -# Optional test application +# nitro agent application +# (only built when notcurses is available) # ----------------------------- -add_executable(llm_test - test_main.cpp -) +find_package(PkgConfig QUIET) + +# ── notcurses ───────────────────────────────────────────────────────────── +set(NC_FOUND FALSE) +set(NC_TARGET "") +if(DEFINED NOTCURSES_DIR) + # Explicit path — create an imported target manually + find_library(NC_LIB NAMES notcurses HINTS "${NOTCURSES_DIR}/lib" REQUIRED) + find_library(NC_CORE_LIB NAMES notcurses-core HINTS "${NOTCURSES_DIR}/lib") + add_library(notcurses_imported INTERFACE IMPORTED) + target_include_directories(notcurses_imported INTERFACE "${NOTCURSES_DIR}/include") + target_link_libraries(notcurses_imported INTERFACE ${NC_LIB}) + if(NC_CORE_LIB) + target_link_libraries(notcurses_imported INTERFACE ${NC_CORE_LIB}) + endif() + set(NC_TARGET notcurses_imported) + set(NC_FOUND TRUE) +elseif(PkgConfig_FOUND) + pkg_check_modules(NC QUIET IMPORTED_TARGET notcurses) + if(NC_FOUND) + set(NC_TARGET PkgConfig::NC) + endif() +else() + find_library(NC_LIB NAMES notcurses) + find_library(NC_CORE_LIB NAMES notcurses-core) + if(NC_LIB AND NC_CORE_LIB) + add_library(notcurses_imported INTERFACE IMPORTED) + target_link_libraries(notcurses_imported INTERFACE ${NC_LIB} ${NC_CORE_LIB}) + set(NC_TARGET notcurses_imported) + set(NC_FOUND TRUE) + endif() +endif() -target_include_directories(llm_test PRIVATE - ${LLAMA_DIR}/include - ${LLAMA_DIR}/ggml/include - ${CMAKE_CURRENT_SOURCE_DIR}/../include -) +# ── libcurl ─────────────────────────────────────────────────────────────── +# Try the modern CMake find-module first (ships with CMake ≥ 3.12). +# Fall back to pkg-config, then a raw library search. +set(CURL_FOUND_INTERNAL FALSE) +set(CURL_TARGET "") + +if(DEFINED CURL_DIR) + # Explicit path supplied by the user (-DCURL_DIR=...) + find_library(CURL_LIB NAMES curl HINTS "${CURL_DIR}/lib" REQUIRED) + find_path(CURL_INCLUDE NAMES curl/curl.h HINTS "${CURL_DIR}/include" REQUIRED) + add_library(curl_imported INTERFACE IMPORTED) + target_include_directories(curl_imported INTERFACE "${CURL_INCLUDE}") + target_link_libraries(curl_imported INTERFACE ${CURL_LIB}) + set(CURL_TARGET curl_imported) + set(CURL_FOUND_INTERNAL TRUE) +else() + # CMake built-in (sets CURL::libcurl target when found) + find_package(CURL QUIET) + if(CURL_FOUND) + set(CURL_TARGET CURL::libcurl) + set(CURL_FOUND_INTERNAL TRUE) + elseif(PkgConfig_FOUND) + pkg_check_modules(CURL_PC QUIET IMPORTED_TARGET libcurl) + if(CURL_PC_FOUND) + set(CURL_TARGET PkgConfig::CURL_PC) + set(CURL_FOUND_INTERNAL TRUE) + endif() + endif() + if(NOT CURL_FOUND_INTERNAL) + find_library(CURL_LIB NAMES curl) + find_path(CURL_INCLUDE NAMES curl/curl.h) + if(CURL_LIB AND CURL_INCLUDE) + add_library(curl_imported INTERFACE IMPORTED) + target_include_directories(curl_imported INTERFACE "${CURL_INCLUDE}") + target_link_libraries(curl_imported INTERFACE ${CURL_LIB}) + set(CURL_TARGET curl_imported) + set(CURL_FOUND_INTERNAL TRUE) + endif() + endif() +endif() -target_link_libraries(llm_test PRIVATE - llm - llama - ggml -) +# ── nitro target ────────────────────────────────────────────────────────── +if(NC_FOUND) + if(NOT CURL_FOUND_INTERNAL) + message(WARNING "libcurl not found — TOOL:CURL will be unavailable. " + "Install libcurl-dev or set -DCURL_DIR= to enable.") + endif() -set_target_properties(llm_test PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) + message(STATUS "notcurses found — building nitro") + add_executable(nitro + nitro.cpp + llama-sb-rag.cpp + ) + target_include_directories(nitro PRIVATE + ${LLAMA_DIR}/include + ${LLAMA_DIR}/ggml/include + ${CMAKE_CURRENT_SOURCE_DIR}/../include + ) + target_link_libraries(nitro PRIVATE + llm + llama + ggml + ${NC_TARGET} # imported target carries include + lib paths + ) + if(CURL_FOUND_INTERNAL) + target_link_libraries(nitro PRIVATE ${CURL_TARGET}) + target_compile_definitions(nitro PRIVATE NITRO_HAVE_CURL=1) + else() + target_compile_definitions(nitro PRIVATE NITRO_HAVE_CURL=0) + endif() + set_target_properties(nitro PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + ) +else() + message(STATUS "notcurses not found — skipping nitro (set -DNOTCURSES_DIR=... to enable)") +endif() # ------------------------------------------------------------------ # Android native library diff --git a/llama/RAG.md b/llama/RAG.md new file mode 100644 index 0000000..6c69c06 --- /dev/null +++ b/llama/RAG.md @@ -0,0 +1,323 @@ +# notcurses RAG — C++ Library Expert via llama.cpp + +A self-contained RAG (Retrieval-Augmented Generation) pipeline in C++17 +that turns a GGUF inference model into a focused expert on any C/C++ library. +Demonstrated here with [notcurses](https://github.com/dankamongmen/notcurses) +but works with any header-based library. + +No fixed limits on chunk count, chunk length, or embedding dimension. +No Python, no vector database daemon, no external dependencies beyond llama.cpp. + +--- + +## How it works + +``` +INDEXING (one-time offline) +──────────────────────────────────────────────────────────────── +notcurses headers + │ + ▼ +chunk_headers ← semantic chunker, outputs chunks.jsonl + │ + ▼ +rag_index ← embeds each chunk via qwen3-embedding-0.6b-q4_k_m.gguf + │ + ▼ +notcurses.db ← binary vector store (embeddings + text) + + +RUNTIME (each query) +──────────────────────────────────────────────────────────────── +user query + │ + ▼ +rag_retrieve() ← embeds query, cosine similarity against db + │ ← skips chunks already seen this session + ▼ +new top-k chunks ← most relevant unseen API fragments + │ + ▼ +prompt assembly ← system + prior history + new context + query + │ + ▼ +Qwen3 inference ← <|think|> reasoning + final answer + │ + ▼ +history ← appended for next turn (KV cache intact) +``` + +--- + +## Files + +| File | Purpose | +|---|---| +| `chunk_headers.cpp` | Parses C/C++ headers into semantic chunks, outputs `.jsonl` | +| `rag_index.cpp` | Reads `.jsonl`, embeds each chunk, saves binary `.db` | +| `rag.hpp` | Single-header C++17 runtime — load db, session, retrieve | +| `example.cpp` | Full pipeline wired together, multi-turn query loop | + +--- + +## Dependencies + +- [llama.cpp](https://github.com/ggerganov/llama.cpp) — `libllama` + `llama.h` +- A GGUF **inference model** — tested with `Qwen3.5-9B-Q4_K_M.gguf` +- A GGUF **embedding model** — `qwen3-embedding-0.6b-q4_k_m.gguf` +- C++17 compiler (gcc 8+, clang 7+, MSVC 2019+) + +--- + +## Build + +```bash +c++ -std=c++17 -o chunk_headers chunk_headers.cpp +c++ -std=c++17 -o rag_index rag_index.cpp -lllama -lm +c++ -std=c++17 -o example example.cpp -lllama -lm +``` + +If llama.cpp is not on your system library path: + +```bash +c++ -std=c++17 -o rag_index rag_index.cpp \ + -I/path/to/llama.cpp/include \ + -L/path/to/llama.cpp/build -lllama -lm +``` + +--- + +## Usage + +### Step 1 — Chunk the headers (one-time) + +```bash +./chunk_headers notcurses/include/notcurses/ > chunks.jsonl +``` + +Accepts a single file or a directory (walked recursively). +Multiple paths can be given: + +```bash +./chunk_headers include/foo.h include/bar.h src/examples/ > chunks.jsonl +``` + +Handles `.h`, `.hpp`, `.c`, `.cpp`. Inspect before indexing: + +```bash +head -5 chunks.jsonl | python3 -m json.tool +``` + +### Step 2 — Embed and index (one-time) + +```bash +./rag_index \ + --model qwen3-embedding-0.6b-q4_k_m.gguf \ + --input chunks.jsonl \ + --output notcurses.db +``` + +Takes a few minutes for a large corpus. The `.db` is reusable +until the library changes. + +### Step 3 — Run + +```bash +./example \ + --model Qwen3.5-9B-Q4_K_M.gguf \ + --embed qwen3-embedding-0.6b-q4_k_m.gguf \ + --db notcurses.db +``` + +``` +notcurses expert ready. ctrl+d to quit. + +you: how do I create a plane and render text into it? +assistant: ... + +you: what options does it take? ← follow-up; no repeated context +assistant: ... +``` + +--- + +## Using rag.hpp in your own project + +Single-header, stb-style. In **one** `.cpp` file: + +```cpp +#define RAG_IMPLEMENTATION +#include "rag.hpp" +``` + +All other files that need the types: + +```cpp +#include "rag.hpp" +``` + +### Minimal integration + +```cpp +// startup +RagDB db; +rag_load(db, "notcurses.db"); + +RagSession session; +session.init(db.size(), 8192); // n_chunks, your n_ctx +session.score_threshold = 0.60f; + +// each turn +std::string context = rag_retrieve(db, embed_ctx, embed_model, + user_query, 5, session); +// context is empty string if nothing new/relevant was found +// build prompt with context and hand to your inference context +``` + +### Stateless retrieval (no deduplication) + +```cpp +std::string context = rag_retrieve(db, embed_ctx, embed_model, + user_query, 5); +``` + +### API + +```cpp +// Load .db file (version 2). Returns true on success. +bool rag_load(RagDB &db, const std::string &path); + +// Retrieve with session deduplication + token budget. +// Returns context string ready to inject into prompt. +// Empty string if nothing new or relevant was found. +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k, + RagSession &session); + +// Stateless overload — no deduplication. +std::string rag_retrieve(const RagDB &db, + llama_context *embed_ctx, + llama_model *embed_model, + const std::string &query, + int top_k); +``` + +### RagSession fields + +```cpp +struct RagSession { + std::vector seen; // one bit per chunk, sized to db + int tokens_used = 0; // running token estimate + int tokens_max = 0; // your n_ctx ceiling + float score_threshold = 0.60f; // skip weak matches + + void init(int n_chunks, int ctx_size); + void reset(); // start a fresh conversation +}; +``` + +--- + +## Chunking strategy + +`chunk_headers` uses a state machine that keeps each **semantic unit** +together as one chunk: + +- Block comment (`/* ... */`) + following declaration +- `//` line comments + following declaration +- `typedef struct` / `typedef enum` entire body +- Consecutive `#define` macro groups +- Multi-line function signatures + +Example — this stays as one chunk: + +```c +// ncplane_create() - create a new plane as a child of 'n'. +// 'nopts' may be NULL for defaults. Returns NULL on error. +struct ncplane* ncplane_create(struct ncplane *n, + const struct ncplane_options *nopts); +``` + +--- + +## Session deduplication + +The KV cache is not cleared between turns, so the model already has +earlier chunks in memory. `RagSession` tracks which chunks have been +injected and skips them on subsequent turns: + +``` +Turn 1: retrieved chunks [42, 17, 83] → all new → inject all +Turn 2: retrieved chunks [42, 55, 17] → 42,17 seen → inject only [55] +Turn 3: retrieved chunks [7, 14, 55] → 55 seen → inject [7, 14] +``` + +Context window grows efficiently — no repeated API reference, and the +model remembers everything already seen via the intact KV cache. + +--- + +## Adapting to other libraries + +Change only the input to `chunk_headers`: + +| Library | Input | +|---|---| +| stb (stb_image, stb_truetype ...) | single `.h` file | +| SDL2 / OpenGL / Vulkan | `include/` directory | +| Your own engine | any `.h` / `.hpp` mix | +| Spring / Java | extend chunker for Javadoc + `.java` | + +Re-run steps 1 and 2 to produce a new `.db`. Runtime code unchanged. +Multiple `.db` files can be loaded and queried independently. + +--- + +## .db file format (version 2) + +Variable-length fields — no wasted padding. + +``` +Header (16 bytes): + uint32 magic = 0x52414744 ("RAGD") + uint32 version = 2 + uint32 n_chunks + uint32 embed_dim + +Per chunk: + uint32 text_len + char[] text (text_len bytes, no null) + uint16 source_len + char[] source (source_len bytes, no null) + uint8 type_len + char[] type (type_len bytes, no null) + float[] embedding (embed_dim × 4 bytes) +``` + +--- + +## GPU memory + +On an 8 GB GPU with `Qwen3.5-9B-Q4_K_M`: + +| Component | VRAM | +|---|---| +| Inference model (Q4_K_M 9B) | ~5.5 GB | +| Embedding model (nomic Q4) | ~0.3 GB | +| KV cache (8k ctx, Q4_0 K/V) | ~0.5 GB | +| **Total** | **~6.3 GB** | + +--- + +## Qwen3 thinking mode + +The model emits `<|think|>...<|/think|>` before its answer. +`example.cpp` strips this with `strip_think()` before printing. +The think block improves RAG quality — the model explicitly reasons +over injected context chunks before answering. + +To expose reasoning (useful for debugging retrieval quality), remove +the `strip_think()` call and print `raw` directly. diff --git a/llama/README.md b/llama/README.md index e1435ba..29f99c8 100644 --- a/llama/README.md +++ b/llama/README.md @@ -1,104 +1,208 @@ -## huggingface-cli +# SmallBASIC Llama Module -``` -pyenv virtualenv 3.10.13 hf-tools -pyenv activate hf-tools -pip install -U pip -pip install huggingface_hub +A comprehensive SmallBASIC library module that bridges the scripting capabilities of SmallBASIC with the power of Llama.cpp Large Language Models. This project allows developers to create, configure, and interact with LLM instances directly within a SmallBASIC environment. -``` +## Table of Contents +1. [System Requirements & CUDA Setup](#system-requirements--cuda-setup) +2. [Obtaining Models from Hugging Face](#obtaining-models-from-hugging-face) +3. [Architecture](#architecture) +4. [Features](#features) +5. [Usage Examples](#usage-examples) +6. [API Reference](#api-reference) +7. [Configuration Presets](#configuration-presets) --- -1️⃣ Ensure nvidia-open driver is installed and working +## System Requirements & CUDA Setup -Check: +For optimal performance, especially on NVIDIA hardware, the CUDA toolkit must be correctly configured. -`` +### 1. Check NVIDIA Drivers +Ensure the NVIDIA open driver is installed and working: +```bash nvidia-smi -`` - -If it works, your driver is fine — no need to install the proprietary driver. - -2️⃣ Add NVIDIA CUDA repository - ``` +If this command works, the proprietary driver is not strictly necessary for CUDA toolkit installation. + +### 2. Add NVIDIA CUDA Repository +For Debian 12: +```bash wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb sudo dpkg -i cuda-keyring_1.1-1_all.deb sudo apt update ``` -This repo contains the latest CUDA toolkit for Debian 12. - -3️⃣ Install CUDA Toolkit only (no driver replacement) +### 3. Install CUDA Toolkit +Install only the toolkit (no driver replacement): +```bash sudo apt install -y cuda-toolkit - - -This installs: - -- nvcc compiler -- CUDA headers -- Runtime libraries (libcudart.so, etc.) - -4️⃣ Add CUDA to your environment - ``` +This installs `nvcc`, headers, and runtime libraries. + +### 4. Environment Variables +Add the following to your environment: +```bash export PATH=/usr/local/cuda/bin:$PATH export CUDAToolkit_ROOT=/usr/local/cuda ``` +To make this permanent, add to `~/.bashrc` and source it. -Optional: add to ~/.bashrc to make it permanent: - +### 5. Verify Installation +```bash +nvcc --version ``` -echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc -echo 'export CUDAToolkit_ROOT=/usr/local/cuda' >> ~/.bashrc -source ~/.bashrc +Output should indicate the release version (e.g., release 12.4). + +### 6. Build Configuration +When building the module, ensure the build directory is clean and configured for the CUDA backend: +```bash +rm -rf build +mkdir build +cd build +cmake -DLLAMA_BACKEND=CUDA .. +make -j$(nproc) ``` +*Note: Fully static builds are not possible for CUDA; some `.so` libraries will remain dynamically linked.* + +--- -Verify: +## Obtaining Models from Hugging Face -nvcc --version +The `LLAMA` function expects a path to a model file (e.g., `gguf` format). Models can be obtained from the Hugging Face Hub. -Should show something like: +### Method 1: Using `huggingface-cli` (Recommended) -``` -nvcc: NVIDIA (R) Cuda compiler driver -Cuda compilation tools, release 12.4, V12.4.105 -``` +1. **Setup Environment** + Create a virtual environment (optional but recommended) and install the CLI tool: + ```bash + pyenv virtualenv 3.10.13 hf-tools + pyenv activate hf-tools + pip install -U pip + pip install huggingface_hub + ``` -5️⃣ Clean llama.cpp build directory +2. **Login** + Authenticate with your Hugging Face account: + ```bash + huggingface-cli login + ``` + (Follow the prompts to enter your token). -``` -rm -rf build -mkdir build -cd build -``` +3. **Download Model** + Use the `huggingface-cli download` command to fetch the model directly to your desired directory. + ```bash + # Example: Download Llama-3-8B-Instruct + huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct --include "*.gguf" --local-dir models/llama3-8b + ``` + + *Note: This command downloads all `.gguf` files associated with the repository into the `models/llama3-8b` folder.* + +### Method 2: Using Python (`huggingface_hub`) -6️⃣ Configure CMake for CUDA backend +If you prefer a scriptable approach: +```python +from huggingface_hub import hf_hub_download +model_path = hf_hub_download( + repo_id="meta-llama/Meta-Llama-3-8B-Instruct", + filename="llama-3-8b-instruct.Q4_K_M.gguf", # Specify exact file if needed + local_dir="models", + local_dir_use_symlinks=False +) ``` -cmake -DLLAMA_BACKEND=CUDA .. + +Once the model file is in your `models` directory (or wherever specified), you can reference it in SmallBASIC: +```basic +llama = LLAMA("models/llama3-8b/llama-3-8b-instruct.Q4_K_M.gguf", 2048, 1024, -1, 0) ``` -You should now see: +### Method 3: Direct download + +1. Navigate to https://huggingface.co/ +2. Click Models at the top and then select Libraries/GGUF +3. Use the parameters slider to limit the selection for your hardware. + +--- + +## Architecture --- CUDA detected – enabling GGML_CUDA +The module operates as a compiled library (`SBLIB`) exposing C++ functionality to SmallBASIC scripts. + +### Core Components +1. **Llama Instance Manager (`g_llama`)**: + * Stores active Llama models in a hash map keyed by ID. + * Supports initialization with custom context sizes, batch sizes, and GPU acceleration. + * Handles memory cleanup to prevent leaks. + +2. **Response Iterator (`g_llama_iter`)**: + * Manages the streaming response of an LLM. + * Provides token-by-token access to generated text. + * Tracks generation speed (`tokens/sec`) and remaining tokens. + +3. **Command Interface**: + * Exposes a set of SmallBASIC functions (callbacks) for configuration and interaction. + +--- -7️⃣ Build +## Features +### Initialization +The `LLAMA` function creates a new model instance. +```basic +' Syntax: LLAMA(model_path, n_ctx, n_batch, n_gpu_layers, n_log_level) +' Example: +' llama = LLAMA("models/llama-7b.gguf", 2048, 1024, -1, 0) ``` -make -j$(nproc) + +### Configuration +Once an instance is created, various parameters can be adjusted dynamically: + +* **Temperature**: Controls randomness in generation. +* **Top-K / Top-P**: Nucleus sampling parameters. +* **Max Tokens**: Limits the length of the response. +* **Penalties**: Frequency, presence, and repeat penalties to avoid repetition. +* **Grammar**: Constrains output to specific patterns. + +```basic +' Examples: +llama.set_temperature(0.8) +llama.set_max_tokens(50) +llama.set_penalty_repeat(0.8) +llama.set_seed(123) ``` -The binary will use CUDA acceleration +### Interaction +The primary method of interaction is `add_message`, which sends a prompt to the model. -Note: fully static builds are not possible for CUDA; some .so libraries will remain dynamically linked (normal). +```basic +' Syntax: llama.add_message(role, content) +' Returns: An iterator object for the response. +response = llama.add_message("user", "Please describe a sunset in poetry.") +``` -# Generator settings +### Streaming Responses +The returned iterator allows real-time processing of the model's output: -## factual answers, tools, summaries +* `response.all()`: Returns the complete generated text. +* `response.next()`: Retrieves the next token. +* `response.has_next()`: Checks if more tokens are available. +* `response.tokens_sec`: Calculates current generation speed. +```basic +' Example loop: +while response.has_next() + print response.next() + sleep 100 +end while ``` + +--- + +## Usage Examples + +### Factual Answers & Tool Use +*Best for: Summaries, code generation, technical queries.* +```basic llama.set_max_tokens(150) llama.set_temperature(0.0) llama.set_top_k(1) @@ -106,9 +210,9 @@ llama.set_top_p(0.0) llama.set_min_p(0.0) ``` -## assistant, Q+A, explanations, chat - -``` +### Assistant / Q&A / Chat +*Best for: Conversational agents, explanations.* +```basic llama.set_max_tokens(150) llama.set_temperature(0.8) llama.set_top_k(40) @@ -116,29 +220,19 @@ llama.set_top_p(0.0) llama.set_min_p(0.05) ``` -## creative, storytelling - -``` -llama.set_max_tokens(20) +### Creative Writing & Storytelling +*Best for: Fiction, poetry, imaginative tasks.* +```basic +llama.set_max_tokens(200) llama.set_temperature(1.0) llama.set_top_k(80) llama.set_top_p(0.0) llama.set_min_p(0.1) ``` -## surprises - -``` -llama.set_max_tokens(200) -llama.set_temperature(1.2) -llama.set_top_k(120) -llama.set_top_p(0.0) -llama.set_min_p(0.15) -``` - -## technical, conservative - -``` +### Technical & Conservative +*Best for: Documentation, logic, precise tasks.* +```basic llama.set_max_tokens(150) llama.set_temperature(0.6) llama.set_top_k(30) @@ -146,9 +240,9 @@ llama.set_top_p(0.0) llama.set_min_p(0.02) ``` -## speed optimised on CPU - -``` +### Speed Optimized (CPU) +*Best for: Rapid iteration or low-resource environments.* +```basic ' llama.set_max_tokens(10) ' llama.set_temperature(0.7) ' llama.set_top_k(20) @@ -156,32 +250,72 @@ llama.set_min_p(0.02) ' llama.set_min_p(0.05) ``` -# Avoiding repetition +--- -## Conservative - minimal repetition control +## API Reference + +### Class: Llama +| Method | Description | +| :--- | :--- | +| `add_stop(text)` | Adds a stop sequence to the generation. | +| `set_penalty_repeat(value)` | Sets repeat penalty (default 1.1). | +| `set_penalty_freq(value)` | Sets frequency penalty. | +| `set_penalty_present(value)` | Sets presence penalty. | +| `set_penalty_last_n(value)` | Sets penalty context size. | +| `set_max_tokens(value)` | Sets maximum output tokens. | +| `set_min_p(value)` | Sets minimum probability threshold. | +| `set_temperature(value)` | Sets generation temperature. | +| `set_top_k(value)` | Sets top-k sampling. | +| `set_top_p(value)` | Sets top-p sampling. | +| `set_grammar(text)` | Sets output grammar constraint. | +| `set_seed(value)` | Sets random seed for reproducibility. | +| `reset()` | Clears the current conversation context. | +| `add_message(role, content)` | Sends a message and returns an iterator. | + +### Class: LlamaIter +| Method | Description | +| :--- | :--- | +| `all()` | Returns the full string of the response. | +| `has_next()` | Returns true if more tokens are available. | +| `next()` | Returns the next token string. | +| `tokens_sec` | Returns current tokens per second. | -``` +--- + +## Repetition Control Strategies + +### Conservative (Minimal Control) +*Use when occasional repetition is acceptable.* +```basic llama.set_penalty_last_n(64) llama.set_penalty_repeat(1.05) ``` -## Balanced - good default - -``` -set_penalty_last_n(64) -set_penalty_repeat(1.1) +### Balanced (Default) +*Recommended for general usage.* +```basic +llama.set_penalty_last_n(64) +llama.set_penalty_repeat(1.1) ``` -## Aggressive - strong anti-repetition - -``` -set_penalty_last_n(128) -set_penalty_repeat(1.2) +### Aggressive (Strong Anti-Repetition) +*Use for long-form generation where repetition must be avoided.* +```basic +llama.set_penalty_last_n(128) +llama.set_penalty_repeat(1.2) ``` -## Disabled - -``` +### Disabled +*Use when repetition is desired or irrelevant.* +```basic llama.set_penalty_last_n(0) llama.set_penalty_repeat(1.0) ``` + +--- + +## Conclusion + +This module empowers SmallBASIC users to build sophisticated AI applications, from chatbots to creative writing tools, leveraging the efficiency of Llama.cpp within a familiar scripting paradigm. Proper configuration of CUDA and generation parameters ensures optimal performance and output quality. Models can be easily acquired via the Hugging Face Hub using standard CLI tools or Python scripts. + +--- diff --git a/llama/llama-sb-rag.cpp b/llama/llama-sb-rag.cpp new file mode 100644 index 0000000..0b11d04 --- /dev/null +++ b/llama/llama-sb-rag.cpp @@ -0,0 +1,445 @@ +// This file is part of SmallBASIC +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// +// Copyright(C) 2026 Chris Warren-Smith + +#include "llama-sb.h" +#include "llama-sb-rag.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +static constexpr uint32_t MAGIC = 0x52414744; +static constexpr size_t MIN_CHUNK = 40; +static constexpr const char *INSTRUCT_EMBED = "Instruct: Represent this API documentation for code retrieval\nQuery: "; +static constexpr const char *INSTRUCT_QUERY = "Instruct: Given a programming question, retrieve relevant API documentation\nQuery: "; + +enum class ChunkType { + Function, Struct, Enum, Typedef, Defines, Other +}; + +static std::string type_name(ChunkType t) { + switch (t) { + case ChunkType::Function: return "function"; + case ChunkType::Struct: return "struct"; + case ChunkType::Enum: return "enum"; + case ChunkType::Typedef: return "typedef"; + case ChunkType::Defines: return "defines"; + default: return "other"; + } +} + +/* ── helpers ───────────────────────────────────────────────── */ + +static bool starts_with(const std::string &s, const std::string &prefix) { + return s.size() >= prefix.size() && + s.compare(0, prefix.size(), prefix) == 0; +} + +static bool is_blank(const std::string &s) { + for (char c : s) if (!isspace((unsigned char)c)) return false; + return true; +} + +/* ── state machine ─────────────────────────────────────────── */ + +enum class State { + Idle, BlockComment, LineComment, Declaration, Struct, Defines +}; + +template + +static bool chunk_file(const fs::path &path, EmitChunk emit_chunk) { + std::ifstream f(path); + if (!f) { + return false; + } + + const std::string source = path.filename().string(); + + State state = State::Idle; + std::string chunk; + ChunkType chunk_type = ChunkType::Other; + int brace_depth = 0; + int paren_depth = 0; + int define_count = 0; + + auto flush = [&](ChunkType t) { + emit_chunk(source, t, chunk); + chunk.clear(); + state = State::Idle; + brace_depth = 0; + paren_depth = 0; + }; + + std::string line; + while (std::getline(f, line)) { + /* trim trailing CR */ + if (!line.empty() && line.back() == '\r') line.pop_back(); + + /* find first non-whitespace for prefix checks */ + size_t trim_pos = 0; + while (trim_pos < line.size() && + (line[trim_pos] == ' ' || line[trim_pos] == '\t')) ++trim_pos; + const std::string trimmed = line.substr(trim_pos); + + /* ── #define handling ─────────────────────────────────── */ + if (starts_with(trimmed, "#define ")) { + if (state == State::BlockComment || state == State::LineComment) { + chunk += line + "\n"; + state = State::Defines; + define_count = 1; + } else if (state == State::Defines) { + chunk += line + "\n"; + define_count++; + } else { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk += line + "\n"; + state = State::Defines; + define_count = 1; + } + continue; + } + + /* non-define while in define group */ + if (state == State::Defines) { + flush(ChunkType::Defines); + define_count = 0; + /* fall through to process this line normally */ + } + + /* ── block comment start ──────────────────────────────── */ + if ((starts_with(trimmed, "/*") || starts_with(trimmed, "/**")) && + state == State::Idle) { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk_type = ChunkType::Other; + chunk += line + "\n"; + state = (trimmed.find("*/", 2) != std::string::npos) + ? State::LineComment + : State::BlockComment; + continue; + } + + /* ── inside block comment ─────────────────────────────── */ + if (state == State::BlockComment) { + chunk += line + "\n"; + if (trimmed.find("*/") != std::string::npos) + state = State::LineComment; + continue; + } + + /* ── // line comment ──────────────────────────────────── */ + if (starts_with(trimmed, "//")) { + if (state == State::Idle) { + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + chunk.clear(); + chunk += line + "\n"; + state = State::LineComment; + } else if (state == State::LineComment) { + chunk += line + "\n"; + } + continue; + } + + /* ── blank line ───────────────────────────────────────── */ + if (is_blank(trimmed)) { + if (state == State::LineComment) + flush(ChunkType::Other); + else if (state == State::Idle && chunk.size() >= MIN_CHUNK) + flush(chunk_type); + continue; + } + + /* ── skip preprocessor noise ──────────────────────────── */ + if (starts_with(trimmed, "#ifndef") || starts_with(trimmed, "#ifdef") || + starts_with(trimmed, "#endif") || starts_with(trimmed, "#pragma") || + starts_with(trimmed, "#include")) { + if (state == State::LineComment || state == State::BlockComment) { + chunk.clear(); + state = State::Idle; + } + continue; + } + + /* ── typedef struct / enum start ─────────────────────── */ + if ((starts_with(trimmed, "typedef struct") || + starts_with(trimmed, "typedef enum") || + starts_with(trimmed, "struct ") || + starts_with(trimmed, "enum ")) && + (state == State::Idle || state == State::LineComment)) { + + if (state == State::Idle && chunk.size() >= MIN_CHUNK) + emit_chunk(source, chunk_type, chunk); + + /* preserve any comment already in chunk */ + if (state == State::Idle) chunk.clear(); + + chunk += line + "\n"; + chunk_type = starts_with(trimmed, "typedef") ? ChunkType::Typedef + : starts_with(trimmed, "enum ") ? ChunkType::Enum + : ChunkType::Struct; + state = State::Struct; + for (char c : line) { + if (c == '{') ++brace_depth; + if (c == '}') --brace_depth; + } + if (brace_depth <= 0 && line.find(';') != std::string::npos) + flush(chunk_type); + continue; + } + + /* ── inside struct/enum body ──────────────────────────── */ + if (state == State::Struct) { + chunk += line + "\n"; + for (char c : line) { + if (c == '{') ++brace_depth; + if (c == '}') --brace_depth; + } + if (brace_depth <= 0 && line.find(';') != std::string::npos) + flush(chunk_type); + continue; + } + + /* ── function / other declaration ────────────────────── */ + if (state == State::LineComment || state == State::Idle) { + if (state == State::Idle && chunk.size() >= MIN_CHUNK) { + emit_chunk(source, chunk_type, chunk); + chunk.clear(); + } + chunk += line + "\n"; + chunk_type = ChunkType::Function; + state = State::Declaration; + for (char c : line) { + if (c == '(') ++paren_depth; + if (c == ')') --paren_depth; + } + if (paren_depth <= 0 && line.find(';') != std::string::npos) + flush(ChunkType::Function); + continue; + } + + /* ── multi-line declaration ───────────────────────────── */ + if (state == State::Declaration) { + chunk += line + "\n"; + for (char c : line) { + if (c == '(') ++paren_depth; + if (c == ')') --paren_depth; + } + if (paren_depth <= 0 && line.find(';') != std::string::npos) + flush(ChunkType::Function); + continue; + } + } + + /* flush remainder */ + if (chunk.size() >= MIN_CHUNK) emit_chunk(source, chunk_type, chunk); + + return true; +} + +// +// cosine similarity (vectors already L2-normalized) +// +static float rag_cosine(const std::vector &a, + const std::vector &b) { + float dot = 0.0f; + size_t n = std::min(a.size(), b.size()); + for (size_t i = 0; i < n; i++) { + dot += a[i] * b[i]; + } + return dot; +} + +// +// build context string from ranked results +// +static std::string rag_build_context(const RagDB &db, + const std::vector &indices, + const std::vector &scores) { + std::ostringstream out; + for (size_t i = 0; i < indices.size(); i++) { + const RagChunk &c = db.chunks[indices[i]]; + out << "// source: " << c.source + << " [" << c.type << "]" + << " (score: " << scores[i] << ")\n" + << c.text << "\n---\n"; + } + return out.str(); +} + +// +// index the file +// +bool Llama::rag_index(RagDB &db, const std::string &filepath) { + bool embed_fail = false; + auto emit_chunk = [&](const std::string &source, ChunkType type, + const std::string &text) { + if (text.size() > MIN_CHUNK) { + RagChunk chunk; + chunk.text = text; + chunk.source = source; + chunk.type = type_name(type); + if (!embed_text(INSTRUCT_EMBED + text, chunk.embedding, db.embed_dim)) { + embed_fail = true; + } else { + db.chunks.push_back(std::move(chunk)); + } + } + }; + + return !embed_fail && chunk_file(filepath, emit_chunk); +} + +// +// retrieve with session +// +std::string Llama::rag_retrieve(const RagDB &db, + const std::string &query, + int top_k, + RagSession &session) { + if (db.empty()) { + _last_error = "no input"; + return {}; + } + + std::vector qvec; + std::string text = INSTRUCT_QUERY + query; + if (!embed_text(text, qvec, db.embed_dim)) { + _last_error = "failed to embed text"; + return {}; + } + + // score all chunks + std::vector order(db.size()); + std::iota(order.begin(), order.end(), 0); + std::vector scores(db.size()); + for (int i = 0; i < db.size(); i++) { + scores[i] = rag_cosine(qvec, db.chunks[i].embedding); + } + std::sort(order.begin(), order.end(), [&](int a, int b){ return scores[a] > scores[b]; }); + + // collect top_k unseen, within budget, above threshold + std::vector result_idx; + std::vector result_scores; + + for (int idx : order) { + if ((int)result_idx.size() >= top_k) break; + if (session.is_seen(idx)) continue; + if (scores[idx] < session.score_threshold) break; /* sorted, so stop */ + if (!session.budget_ok(db.chunks[idx].text)) break; + + result_idx.push_back(idx); + result_scores.push_back(scores[idx]); + session.mark(idx); + session.charge(db.chunks[idx].text); + } + + return rag_build_context(db, result_idx, result_scores); +} + +bool RagDB::save(const std::string &path) { + std::ofstream f(path, std::ios::binary); + if (!f) { + return false; + } + + auto write32 = [&](uint32_t v) { f.write((char*)&v, 4); }; + auto write16 = [&](uint16_t v) { f.write((char*)&v, 2); }; + auto write8 = [&](uint8_t v) { f.write((char*)&v, 1); }; + auto writestr = [&](const std::string &s, size_t max_len) { + size_t len = std::min(s.size(), max_len); + f.write(s.c_str(), (std::streamsize)len); + }; + + write32(MAGIC); /* magic "RAGD" */ + write32(2); /* version */ + write32((uint32_t)chunks.size()); /* n_chunks */ + write32((uint32_t)embed_dim); /* embed_dim */ + + for (const RagChunk &c : chunks) { + write32((uint32_t)c.text.size()); + f.write(c.text.c_str(), (std::streamsize)c.text.size()); + + uint16_t src_len = (uint16_t)std::min(c.source.size(), (size_t)65535); + write16(src_len); + writestr(c.source, src_len); + + uint8_t type_len = (uint8_t)std::min(c.type.size(), (size_t)255); + write8(type_len); + writestr(c.type, type_len); + + f.write((char*)c.embedding.data(), + (std::streamsize)(embed_dim * sizeof(float))); + } + + return f.good(); +} + +bool RagDB::load(const std::string &path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return false; + } + + auto read32 = [&]() -> uint32_t { + uint32_t v = 0; f.read((char*)&v, 4); return v; + }; + auto read16 = [&]() -> uint16_t { + uint16_t v = 0; f.read((char*)&v, 2); return v; + }; + auto read8 = [&]() -> uint8_t { + uint8_t v = 0; f.read((char*)&v, 1); return v; + }; + auto readstr = [&](size_t len) -> std::string { + std::string s(len, '\0'); + f.read(&s[0], (std::streamsize)len); + return s; + }; + + uint32_t magic = read32(); + uint32_t version = read32(); + uint32_t n = read32(); + uint32_t edim = read32(); + + if (magic != MAGIC) { + return false; + } + if (version != 2) { + return false; + } + + embed_dim = (int)edim; + chunks.resize(n); + + for (uint32_t i = 0; i < n; i++) { + RagChunk &c = chunks[i]; + + uint32_t text_len = read32(); + c.text = readstr(text_len); + + uint16_t src_len = read16(); + c.source = readstr(src_len); + + uint8_t type_len = read8(); + c.type = readstr(type_len); + + c.embedding.resize(edim); + f.read((char*)c.embedding.data(), (std::streamsize)(edim * sizeof(float))); + } + + return true; +} diff --git a/llama/llama-sb-rag.h b/llama/llama-sb-rag.h new file mode 100644 index 0000000..0296f26 --- /dev/null +++ b/llama/llama-sb-rag.h @@ -0,0 +1,78 @@ +// This file is part of SmallBASIC +// +// This program is distributed under the terms of the GPL v2.0 or later +// Download the GNU Public License (GPL) from www.gnu.org +// +// Copyright(C) 2026 Chris Warren-Smith + +#pragma once + +struct RagChunk { + std::string text; + std::string source; + std::string type; + std::vector embedding; +}; + +/* ── on-disk chunk (variable-length text) ──────────────────── */ +/* + * db header (16 bytes): + * uint32 magic = 0x52414744 "RAGD" + * uint32 version = 2 + * uint32 n_chunks + * uint32 embed_dim + * + * per chunk: + * uint32 text_len + * char[] text (text_len bytes, no null) + * uint16 source_len + * char[] source (source_len bytes, no null) + * uint8 type_len + * char[] type (type_len bytes, no null) + * float[] embedding (embed_dim floats) + */ +struct RagDB { + std::vector chunks; + int embed_dim = 0; + + bool load(const std::string &path); + bool save(const std::string &path); + + int size() const { return (int)chunks.size(); } + bool empty() const { return chunks.empty(); } +}; + +// +// per-session deduplication + token budget +// +struct RagSession { + std::vector seen; /* sized to db.size() on init */ + int tokens_used = 0; + int tokens_max = 0; /* set to your n_ctx */ + float score_threshold = 0.60f; /* skip weak matches */ + + void init(int n_chunks, int ctx_size) { + seen.assign(n_chunks, false); + tokens_used = 0; + tokens_max = ctx_size; + } + + void reset() { + std::fill(seen.begin(), seen.end(), false); + tokens_used = 0; + } + + bool is_seen(int idx) const { return idx < (int)seen.size() && seen[idx]; } + void mark(int idx) { if (idx < (int)seen.size()) seen[idx] = true; } + + /* rough token estimate: 1 token ≈ 4 chars */ + bool budget_ok(const std::string &text) const { + return tokens_max == 0 || + (tokens_used + (int)text.size() / 4) < (int)(tokens_max * 0.85f); + } + + void charge(const std::string &text) { + tokens_used += (int)text.size() / 4; + } +}; + diff --git a/llama/llama-sb.cpp b/llama/llama-sb.cpp index 2bff5e8..0c26712 100644 --- a/llama/llama-sb.cpp +++ b/llama/llama-sb.cpp @@ -7,10 +7,27 @@ #include #include +#include +#include +#include "ggml-cuda.h" + #include "llama.h" #include "llama-sb.h" -constexpr int MAX_REPEAT = 5; +constexpr int MAX_REPEAT = 50; + +static bool read_vram(size_t &used, size_t &total) { + size_t free = 0; + total = 0; +#ifdef GGML_USE_CUDA + ggml_backend_cuda_get_device_memory(0, &free, &total); + if (total > 0) { + used = total - free; + return true; + } +#endif + return false; +} LlamaIter::LlamaIter() : _llama(nullptr), @@ -19,6 +36,15 @@ LlamaIter::LlamaIter() : _has_next(false) { } +LlamaIter::LlamaIter(LlamaIter &&other) noexcept + : _llama(std::exchange(other._llama, nullptr)) + , _last_word(std::move(other._last_word)) + , _t_start(std::move(other._t_start)) + , _repetition_count(other._repetition_count) + , _tokens_generated(other._tokens_generated) + , _has_next(other._has_next) { +} + Llama::Llama() : _model(nullptr), _ctx(nullptr), @@ -26,19 +52,58 @@ Llama::Llama() : _vocab(nullptr), _penalty_last_n(0), _penalty_repeat(0), + _penalty_freq(0.0f), + _penalty_present(0.0f), _temperature(0), _top_p(0), _min_p(0), _top_k(0), _max_tokens(0), - _log_level(GGML_LOG_LEVEL_CONT) { - llama_log_set([](enum ggml_log_level level, const char * text, void *user_data) { + _log_level(GGML_LOG_LEVEL_CONT), + _n_gpu_layers(0), + _n_system_tokens(0), + _is_gemma4(false), + _sampler_dirty(false), + _seed(LLAMA_DEFAULT_SEED) { + llama_log_set([](enum ggml_log_level level, const char *text, void *user_data) { Llama *llama = (Llama *)user_data; + if (level == GGML_LOG_LEVEL_ERROR && llama->_last_error.empty()) { + // remember the first error message + llama->_last_error = text; + } if (level > llama->_log_level) { fprintf(stderr, "LLAMA: %s", text); } }, this); reset(); + llama_backend_init(); +} + +Llama::Llama(Llama &&other) noexcept + : _model(std::exchange(other._model, nullptr)) + , _ctx(std::exchange(other._ctx, nullptr)) + , _sampler(std::exchange(other._sampler, nullptr)) + , _vocab(std::exchange(other._vocab, nullptr)) + , _stop_sequences(std::move(other._stop_sequences)) + , _grammar_src(std::move(other._grammar_src)) + , _grammar_root(std::move(other._grammar_root)) + , _last_error(std::move(other._last_error)) + , _template(std::move(other._template)) + , _penalty_last_n(other._penalty_last_n) + , _penalty_repeat(other._penalty_repeat) + , _penalty_freq(other._penalty_freq) + , _penalty_present(other._penalty_present) + , _temperature(other._temperature) + , _top_p(other._top_p) + , _min_p(other._min_p) + , _top_k(other._top_k) + , _max_tokens(other._max_tokens) + , _log_level(other._log_level) + , _n_gpu_layers(other._n_gpu_layers) + , _n_system_tokens(other._n_system_tokens) + , _is_gemma4(other._is_gemma4) + , _sampler_dirty(other._sampler_dirty) + , _seed(other._seed) { } Llama::~Llama() { @@ -51,34 +116,43 @@ Llama::~Llama() { if (_model) { llama_model_free(_model); } + llama_backend_free(); } void Llama::reset() { _stop_sequences.clear(); - _last_error = ""; + _last_error.clear(); _penalty_last_n = 64; _penalty_repeat = 1.1f; + _penalty_freq = 0.0f; + _penalty_present = 0.0f; _temperature = 0; _top_k = 0; _top_p = 1.0f; _min_p = 0.0f; _max_tokens = 150; + _n_system_tokens = 0; + _seed = LLAMA_DEFAULT_SEED; + _sampler_dirty = true; if (_ctx) { llama_memory_clear(llama_get_memory(_ctx), true); } } -bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers) { +bool Llama::load_model(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level) { ggml_backend_load_all(); llama_model_params mparams = llama_model_default_params(); if (n_gpu_layers >= 0) { - mparams.n_gpu_layers = n_gpu_layers; + mparams.n_gpu_layers = n_gpu_layers; } + _last_error.clear(); + _log_level = log_level; + _n_gpu_layers = n_gpu_layers; _model = llama_model_load_from_file(model_path.c_str(), mparams); if (!_model) { - _last_error = "Failed to load model"; + set_last_error("Load model"); } else { llama_context_params cparams = llama_context_default_params(); cparams.n_ctx = n_ctx; @@ -86,142 +160,122 @@ bool Llama::construct(string model_path, int n_ctx, int n_batch, int n_gpu_layer cparams.n_ubatch = n_batch; cparams.no_perf = true; cparams.attention_type = LLAMA_ATTENTION_TYPE_UNSPECIFIED; - cparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_AUTO; + cparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED; + + // or Q4_0 for more aggressive saving + cparams.type_k = GGML_TYPE_Q4_0; + cparams.type_v = GGML_TYPE_Q4_0; + + // keep KV cache on GPU + cparams.offload_kqv = true; _ctx = llama_init_from_model(_model, cparams); if (!_ctx) { - _last_error = "Failed to create context"; + set_last_error("Create context"); } else { _vocab = llama_model_get_vocab(_model); - - auto sparams = llama_sampler_chain_default_params(); - sparams.no_perf = false; - _sampler = llama_sampler_chain_init(sparams); } + _template = llama_model_chat_template(_model, nullptr); + _is_gemma4 = (_template.find("<|turn>model") != string::npos); } + return _last_error.empty(); } -void Llama::configure_sampler() { - llama_sampler_reset(_sampler); - if (_penalty_last_n != 0 && _penalty_repeat != 1.0f) { - auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, 0.0f, 0.0f); - llama_sampler_chain_add(_sampler, penalties); - } - if (_temperature <= 0.0f) { - llama_sampler_chain_add(_sampler, llama_sampler_init_greedy()); - } else { - llama_sampler_chain_add(_sampler, llama_sampler_init_temp(_temperature)); - if (_top_k > 0) { - llama_sampler_chain_add(_sampler, llama_sampler_init_top_k(_top_k)); - } - if (_top_p < 1.0f) { - llama_sampler_chain_add(_sampler, llama_sampler_init_top_p(_top_p, 1)); - } - if (_min_p > 0.0f) { - llama_sampler_chain_add(_sampler, llama_sampler_init_min_p(_min_p, 1)); - } - llama_sampler_chain_add(_sampler, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); - } -} +bool Llama::load_embedding_model(string model_path) { + ggml_backend_load_all(); -vector Llama::tokenize(const string &prompt) { - vector result; + llama_model_params mparams = llama_model_default_params(); + mparams.n_gpu_layers = 99; - int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); - if (n_prompt <= 0) { - _last_error = "Failed to tokenize prompt"; + _last_error.clear(); + _model = llama_model_load_from_file(model_path.c_str(), mparams); + if (!_model) { + set_last_error("Load model"); } else { - result.reserve(n_prompt); - result.resize(n_prompt); - if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), - result.data(), n_prompt, true, true) < 0) { - _last_error = "Failed to tokenize prompt"; + llama_context_params cparams = llama_context_default_params(); + cparams.n_ctx = 512; + cparams.n_batch = 512; + cparams.embeddings = true; + cparams.pooling_type = LLAMA_POOLING_TYPE_MEAN; + + _ctx = llama_init_from_model(_model, cparams); + if (!_ctx) { + set_last_error("Create context"); + } else { + _vocab = llama_model_get_vocab(_model); } } - return result; -} -// Makes space in the context for n_tokens by removing old tokens if necessary -// Returns true if successful, false if impossible to make space -// -// Strategies: -// - If enough space exists, does nothing -// - If n_tokens > n_ctx, fails (impossible to fit) -// - Otherwise, removes oldest tokens to make room -// -// Parameters: -// n_tokens - Number of tokens we need space for -// keep_min - Minimum tokens to keep (e.g., system prompt), default 0 -// -bool Llama::make_space_for_tokens(int n_tokens, int keep_min) { - int n_ctx = llama_n_ctx(_ctx); - if (n_tokens > n_ctx) { - _last_error = "Too many tokens, increase context size (n_ctx)"; - return false; - } + return _last_error.empty(); +} - llama_memory_t mem = llama_get_memory(_ctx); +void Llama::set_grammar(const string &src, const string &root) { + _grammar_src = src; + _grammar_root = root; + dirty(); +} - // Get current position range - llama_pos pos_min = llama_memory_seq_pos_min(mem, 0); - llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); +bool Llama::add_message(LlamaIter &iter, const string &role, const string &content) { + llama_chat_message message = {role.c_str(), content.c_str()}; + int buf_size = 2 * (int)(role.size() + content.size() + 64); + vector buf(buf_size); + int32_t n = 0; - // Empty memory - nothing to do - if (pos_max < 0) { - return true; + if (_template.empty()) { + _last_error = "No chat template available"; + return false; } - int current_used = pos_max - pos_min + 1; - int space_needed = n_tokens; - int space_available = n_ctx - current_used; - - // Already have enough space - if (space_available >= space_needed) { - return true; + if (_is_gemma4) { + // see: https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 + string str; + if (role == "system") { + str = "<|turn>system\n<|think|>" + content + "\n"; + } else { + str = "<|turn>" + role + "\n" + content + "\n"; + } + n = str.size(); + buf.assign(str.begin(), str.end()); + buf.push_back('\0'); + } else { + bool add_ass = (role == "user" || role == "tool" || role == "tool_result"); + n = llama_chat_apply_template(_template.c_str(), &message, 1, add_ass, buf.data(), buf_size); + if (n < 0) { + _last_error = "No chat template no supported"; + return false; + } else if (n > (int32_t)buf.size()) { + buf.resize(n); + llama_chat_apply_template(_template.c_str(), &message, 1, add_ass, buf.data(), buf.size()); + } } + string prompt(buf.data(), n); - // Calculate how many tokens to remove - int tokens_to_remove = space_needed - space_available; - - // Can't remove more than we have (minus keep_min) - int removable = current_used - keep_min; - if (tokens_to_remove > removable) { - _last_error = "Can't make enough space while keeping keep_min tokens"; - return false; + if (_sampler_dirty) { + // avoid wasteful rebuild + if (!configure_sampler()) { + return false; + } + _sampler_dirty = false; } - // Remove oldest tokens (from pos_min to pos_min + tokens_to_remove) - llama_memory_seq_rm(mem, 0, pos_min, pos_min + tokens_to_remove); - - // Shift remaining tokens down - llama_memory_seq_add(mem, 0, pos_min + tokens_to_remove, -1, -tokens_to_remove); - - return true; -} - -bool Llama::generate(LlamaIter &iter, const string &prompt) { - configure_sampler(); - vector prompt_tokens = tokenize(prompt); if (prompt_tokens.size() == 0) { return false; } - if (!make_space_for_tokens(prompt_tokens.size(), 0)) { + if (role == "system") { + // always retain system tokens + _n_system_tokens = prompt_tokens.size(); + } + + if (!make_space_for_tokens(prompt_tokens.size())) { return false; } // batch decode tokens - uint32_t n_batch = llama_n_batch(_ctx); - for (size_t i = 0; i < prompt_tokens.size(); i += n_batch) { - size_t batch_size = std::min((size_t)n_batch, prompt_tokens.size() - i); - llama_batch batch = llama_batch_get_one(prompt_tokens.data() + i, batch_size); - int result = llama_decode(_ctx, batch); - if (result != 0) { - _last_error = std::format("Failed to decode batch. position:{} error:{}", i, result); - return false; - } + if (!batch_decode_tokens(prompt_tokens)) { + return false; } // handle encoder models @@ -246,87 +300,6 @@ bool Llama::generate(LlamaIter &iter, const string &prompt) { return true; } -bool Llama::ends_with_sentence_boundary(const string &text) { - if (text.empty()) { - return false; - } - - // Get last few characters (in case of whitespace after punctuation) - size_t check_len = std::min(text.length(), (size_t)5); - std::string ending = text.substr(text.length() - check_len); - - // Check for various sentence endings - // Period followed by space or end - if (ending.find(". ") != std::string::npos || - ending.back() == '.') { - return true; - } - - // Exclamation mark - if (ending.find("! ") != std::string::npos || - ending.back() == '!') { - return true; - } - - // Question mark - if (ending.find("? ") != std::string::npos || - ending.back() == '?') { - return true; - } - - // Newline (paragraph break) - if (ending.find('\n') != std::string::npos) { - return true; - } - - // Quote followed by period: "something." - if (ending.find(".\"") != std::string::npos || - ending.find("!\"") != std::string::npos || - ending.find("?\"") != std::string::npos) { - return true; - } - - return false; -} - -string Llama::token_to_string(LlamaIter &iter, llama_token tok) { - string result; - char buf[512]; - int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); - if (n > 0) { - // detect repetition - if (iter._last_word == buf) { - if (++iter._repetition_count == MAX_REPEAT) { - iter._has_next = false; - } - } else { - iter._repetition_count = 0; - iter._last_word = buf; - } - - result.append(buf, n); - - // detect end of max-tokens - if (++iter._tokens_generated > _max_tokens && ends_with_sentence_boundary(result)) { - iter._has_next = false; - } - - // detect stop words - if (iter._has_next) { - for (const auto &stop : _stop_sequences) { - size_t pos = result.find(stop); - if (pos != std::string::npos) { - // found stop sequence - truncate and signal end - result = result.substr(0, pos); - iter._has_next = false; - break; - } - } - } - } - return result; -} - string Llama::next(LlamaIter &iter) { if (!iter._has_next) { _last_error = "Iteration beyond end of stream"; @@ -368,7 +341,6 @@ string Llama::all(LlamaIter &iter) { // end-of-generation check if (llama_vocab_is_eog(_vocab, tok)) { - iter._has_next = false; break; } @@ -384,6 +356,9 @@ string Llama::all(LlamaIter &iter) { } } + // tokens exhausted - call add_message to continue + iter._has_next = false; + // detokenize sequentially if (!decoded.empty()) { for (llama_token tok : decoded) { @@ -393,3 +368,290 @@ string Llama::all(LlamaIter &iter) { return out; } + +LlamaMemoryInfo Llama::memory_info() { + LlamaMemoryInfo info = {}; + + // KV cache usage + llama_memory_t mem = llama_get_memory(_ctx); + llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); + int n_ctx = llama_n_ctx(_ctx); + info.kv_total = n_ctx; + info.kv_used = (pos_max < 0) ? 0 : (int)pos_max + 1; + info.kv_percent = 100.0f * info.kv_used / info.kv_total; + + // Model layers + auto n_gpu_layers = std::max(0, _n_gpu_layers); + info.n_layers_total = llama_model_n_layer(_model); + info.n_layers_gpu = std::min(info.n_layers_total, n_gpu_layers); + info.n_layers_cpu = info.n_layers_total - info.n_layers_gpu; + + // ram + if (read_vram(info.vram_used, info.vram_total)) { + info.vram_percent = 100.0f * info.vram_used / info.vram_total; + } + + info.model_native_max_ctx = llama_model_n_ctx_train(_model); + + // Advice + ostringstream advice; + + // Check structural limits & model configuration quirks + if (info.kv_total > info.model_native_max_ctx) { + advice << "WARNING: Configured context size (" << info.kv_total + << ") exceeds model native training length (" << info.model_native_max_ctx + << "). Logic flaws or repetition bugs will occur unless RoPE scaling options are enabled. "; + } + + if (n_gpu_layers < info.n_layers_total) { + advice << "Only " << n_gpu_layers << "/" << info.n_layers_total + << " layers on GPU - increase n_gpu_layers if VRAM allows. "; + } else { + advice << "All " << info.n_layers_total << " layers on GPU. "; + } + if (info.n_layers_cpu > 0) { + advice << "CPU offload active (" << info.n_layers_cpu + << " layers on CPU) - increase n_gpu_layers if VRAM allows. "; + } + if (info.vram_percent > 90.0f) { + advice << "VRAM >90% - reduce n_ctx or use Q4_0 KV cache. "; + } else if (info.vram_percent < 60.0f && info.n_layers_cpu > 0) { + advice << "VRAM headroom available - try adding more GPU layers. "; + } + if (info.kv_percent > 80.0f) { + advice << "Context >80% full - consider calling clear_history(). "; + } + info.advice = advice.str(); + + return info; +} + +bool Llama::embed_text(const std::string &text, std::vector &out, int embed_dim) { + vector tokens = tokenize(text); + if (tokens.size() == 0) { + return false; + } + + // truncate to context window + int n_ctx = llama_n_ctx(_ctx); + int n = tokens.size(); + if (n > n_ctx) { + _last_error = std::format("warning: chunk truncated {} -> {} tokens ", n, n_ctx); + n = n_ctx; + tokens.resize(n); + } + + llama_memory_clear(llama_get_memory(_ctx), true); + + if (!batch_decode_tokens(tokens)) { + return false; + } + + float *emb = llama_get_embeddings_seq(_ctx, 0); + if (!emb) { + emb = llama_get_embeddings_ith(_ctx, n - 1); + } + + if (!emb) { + _last_error = "no embedding returned\n"; + return false; + } + + out.assign(emb, emb + embed_dim); + + /* L2 normalize */ + float norm = 0.0f; + for (float v : out) { + norm += v * v; + } + norm = std::sqrt(norm); + if (norm > 1e-9f) { + for (float &v : out) { + v /= norm; + } + } + + return true; +} + +bool Llama::batch_decode_tokens(vector &tokens) { + uint32_t n_batch = llama_n_batch(_ctx); + for (size_t i = 0; i < tokens.size(); i += n_batch) { + size_t batch_size = std::min((size_t)n_batch, tokens.size() - i); + llama_batch batch = llama_batch_get_one(tokens.data() + i, batch_size); + int result = llama_decode(_ctx, batch); + if (result != 0) { + _last_error = std::format("Failed to decode batch. position:{} error:{} [size:{}]", + i, result, tokens.size()); + return false; + } + } + return true; +} + +bool Llama::configure_sampler() { + auto sparams = llama_sampler_chain_default_params(); + sparams.no_perf = false; + llama_sampler *chain = llama_sampler_chain_init(sparams); + + if (!_grammar_src.empty()) { + llama_sampler *grammar = llama_sampler_init_grammar(_vocab, _grammar_src.c_str(), _grammar_root.c_str()); + if (!grammar) { + _last_error = "failed to initialize grammar sampler"; + return false; + } + llama_sampler_chain_add(chain, grammar); + } + if (_penalty_last_n != 0 && _penalty_repeat != 1.0f) { + auto penalties = llama_sampler_init_penalties(_penalty_last_n, _penalty_repeat, _penalty_freq, _penalty_present); + llama_sampler_chain_add(chain, penalties); + } + if (_temperature <= 0.0f) { + llama_sampler_chain_add(chain, llama_sampler_init_greedy()); + } else { + if (_top_k > 0) { + llama_sampler_chain_add(chain, llama_sampler_init_top_k(_top_k)); + } + if (_top_p < 1.0f || _min_p > 0.0f) { + llama_sampler_chain_add(chain, llama_sampler_init_top_p(_top_p, 1)); + } + if (_min_p > 0.0f) { + llama_sampler_chain_add(chain, llama_sampler_init_min_p(_min_p, 1)); + } + llama_sampler_chain_add(chain, llama_sampler_init_temp(_temperature)); + llama_sampler_chain_add(chain, llama_sampler_init_dist(_seed)); + } + if (_sampler) { + llama_sampler_free(_sampler); + } + _sampler = chain; + return true; +} + +// Makes space in the context for n_tokens by removing old tokens if necessary +// Returns true if successful, false if impossible to make space +// +// Strategies: +// - If enough space exists, does nothing +// - If n_tokens > n_ctx, fails (impossible to fit) +// - Otherwise, removes oldest tokens to make room +// +// Parameters: +// n_tokens - Number of tokens we need space for +// +bool Llama::make_space_for_tokens(int n_tokens) { + int n_ctx = llama_n_ctx(_ctx); + if (n_tokens > n_ctx) { + _last_error = "Too many tokens, increase context size (n_ctx)"; + return false; + } + + llama_memory_t mem = llama_get_memory(_ctx); + + // Get current position range + llama_pos pos_min = llama_memory_seq_pos_min(mem, 0); + llama_pos pos_max = llama_memory_seq_pos_max(mem, 0); + + // Empty memory - nothing to do + if (pos_max < 0) { + return true; + } + + int current_used = pos_max - pos_min + 1; + int space_needed = n_tokens; + int space_available = n_ctx - current_used; + + // Already have enough space + if (space_available >= space_needed) { + return true; + } + + // Calculate how many tokens to remove + int tokens_to_remove = space_needed - space_available; + + // Can't remove more than we have (minus _n_system_tokens) + int removable = current_used - _n_system_tokens; + if (tokens_to_remove > removable) { + _last_error = "Can't make enough space while keeping num_system_tokens tokens"; + return false; + } + + // Remove oldest tokens (from pos_min to pos_min + tokens_to_remove) + llama_memory_seq_rm(mem, 0, pos_min, pos_min + tokens_to_remove); + + // Shift remaining tokens down + llama_memory_seq_add(mem, 0, pos_min + tokens_to_remove, -1, -tokens_to_remove); + + return true; +} + +vector Llama::tokenize(const string &prompt) { + vector result; + + int n_prompt = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true); + if (n_prompt <= 0) { + _last_error = "Failed to tokenize prompt"; + } else { + result.reserve(n_prompt); + result.resize(n_prompt); + if (llama_tokenize(_vocab, prompt.c_str(), prompt.size(), + result.data(), n_prompt, true, true) < 0) { + _last_error = "Failed to tokenize prompt"; + } + } + return result; +} + +string Llama::token_to_string(LlamaIter &iter, llama_token tok) { + string result; + char buf[512]; + int n = llama_token_to_piece(_vocab, tok, buf, sizeof(buf), 0, false); + if (n > 0) { + // detect repetition - only on non-whitespace tokens, otherwise + // spaces/newlines trigger false positives almost immediately. + string piece(buf, n); + bool is_trivial = piece.find_first_not_of(" \t\n\r") == string::npos; + if (!is_trivial) { + if (iter._last_word == piece) { + if (++iter._repetition_count >= MAX_REPEAT) { + iter._has_next = false; + } + } else { + iter._repetition_count = 0; + iter._last_word = piece; + } + } + + result.append(buf, n); + + // detect end of max-tokens + if (++iter._tokens_generated > _max_tokens) { + iter._has_next = false; + } + + // detect stop words + if (iter._has_next) { + for (const auto &stop : _stop_sequences) { + size_t pos = result.find(stop); + if (pos != std::string::npos) { + // found stop sequence - truncate and signal end + result = result.substr(0, pos); + iter._has_next = false; + break; + } + } + } + } + return result; +} + +void Llama::set_last_error(const char *message) { + if (!_last_error.empty()) { + if (_last_error.back() == '\n') { + _last_error.pop_back(); + } + _last_error = std::format("{}: {}", message, _last_error); + } else { + _last_error = std::format("{} failed", message); + } +} diff --git a/llama/llama-sb.h b/llama/llama-sb.h index b1da148..b01ed2e 100644 --- a/llama/llama-sb.h +++ b/llama/llama-sb.h @@ -15,11 +15,41 @@ using namespace std; struct Llama; +struct RagDB; +struct RagSession; + +struct LlamaMemoryInfo { + // KV cache + int kv_used; // slots currently used + int kv_total; // total slots (== n_ctx) + float kv_percent; // kv_used / kv_total + + // GPU VRAM (via ggml backend) + size_t vram_used; // bytes + size_t vram_total; // bytes + float vram_percent; + + // Model layers + int n_layers_total; // total model layers + int n_layers_gpu; // layers offloaded to GPU + int n_layers_cpu; // layers on CPU + int model_native_max_ctx; + + // Advice + string advice; +}; struct LlamaIter { explicit LlamaIter(); ~LlamaIter() {} + // move constructor + LlamaIter(LlamaIter &&other) noexcept; + + // delete the copy + LlamaIter(const LlamaIter &) = delete; + LlamaIter &operator=(const LlamaIter &) = delete; + Llama *_llama; string _last_word; chrono::high_resolution_clock::time_point _t_start; @@ -30,51 +60,91 @@ struct LlamaIter { struct Llama { explicit Llama(); + + // move constructor + Llama(Llama &&other) noexcept; + + // delete the copy + Llama(const Llama &) = delete; + Llama &operator=(const Llama &) = delete; + ~Llama(); // init - bool construct(string model_path, int n_ctx, int n_batch, int n_gpu_layers); + bool load_model(string model_path, int n_ctx, int n_batch, int n_gpu_layers, int log_level); + bool load_embedding_model(string model_path); // generation - bool generate(LlamaIter &iter, const string &prompt); + bool add_message(LlamaIter &iter, const string &role, const string &content); string next(LlamaIter &iter); string all(LlamaIter &iter); // generation parameters void add_stop(const char *stop) { _stop_sequences.push_back(stop); } void clear_stops() { _stop_sequences.clear(); } - void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; } - void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; } - void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; } - void set_min_p(float min_p) { _min_p = min_p; } - void set_temperature(float temperature) { _temperature = temperature; } - void set_top_k(int top_k) { _top_k = top_k; } - void set_top_p(float top_p) { _top_p = top_p; } + void set_penalty_last_n(int32_t penalty_last_n) { _penalty_last_n = penalty_last_n; dirty(); } + void set_penalty_repeat(float penalty_repeat) { _penalty_repeat = penalty_repeat; dirty(); } + void set_penalty_freq(float penalty_freq) { _penalty_freq = penalty_freq; dirty(); } + void set_penalty_present(float penalty_present) { _penalty_present = penalty_present; dirty(); } + void set_max_tokens(int max_tokens) { _max_tokens = max_tokens; dirty(); } + void set_min_p(float min_p) { _min_p = min_p; dirty(); } + void set_temperature(float temperature) { _temperature = temperature; dirty(); } + void set_top_k(int top_k) { _top_k = top_k; dirty(); } + void set_top_p(float top_p) { _top_p = top_p; dirty(); } + void set_grammar(const string &src, const string &root); + void set_seed(unsigned int seed) { _seed = seed; dirty(); } // error handling const char *last_error() { return _last_error.c_str(); } void set_log_level(int level) { _log_level = level; } void reset(); + // memory info + LlamaMemoryInfo memory_info(); + + // creates an embedding vector of the given dimension for the given text + bool embed_text(const std::string &text, std::vector &out, int embed_dim); + + // retrieves rag query context informatiion from the rag database + std::string rag_retrieve(const RagDB &db, const std::string &query, int top_k, RagSession &session); + + // indexes the details from the given file + bool rag_index(RagDB &db, const std::string &filepath); + + // returns the emdedding dimension for the loaded model + int get_embed_dim() const { return _model != nullptr ? llama_model_n_embd(_model) : 0; } + private: - bool ends_with_sentence_boundary(const string &out); - void configure_sampler(); - bool make_space_for_tokens(int n_tokens, int keep_min); + bool batch_decode_tokens(vector &tokens); + bool configure_sampler(); + void dirty() {_sampler_dirty = true; } + bool make_space_for_tokens(int n_tokens); vector tokenize(const string &prompt); string token_to_string(LlamaIter &iter, llama_token tok); + void set_last_error(const char *message); llama_model *_model; llama_context *_ctx; llama_sampler *_sampler; const llama_vocab *_vocab; vector _stop_sequences; + string _grammar_src; + string _grammar_root; string _last_error; + string _template; int32_t _penalty_last_n; float _penalty_repeat; + float _penalty_freq; + float _penalty_present; float _temperature; float _top_p; float _min_p; int _top_k; int _max_tokens; int _log_level; + int _n_gpu_layers; + int _n_system_tokens; + bool _is_gemma4; + bool _sampler_dirty; + unsigned int _seed; }; diff --git a/llama/llama.cpp b/llama/llama.cpp index af3be13..d749821 160000 --- a/llama/llama.cpp +++ b/llama/llama.cpp @@ -1 +1 @@ -Subproject commit af3be131c065a38e476c34295bceda6cb956e7d7 +Subproject commit d749821db3bd587932d1ed57d43626cd552c9909 diff --git a/llama/main.cpp b/llama/main.cpp index 1c0de21..b78d015 100644 --- a/llama/main.cpp +++ b/llama/main.cpp @@ -95,7 +95,49 @@ static int cmd_llama_set_penalty_repeat(var_s *self, int argc, slib_par_t *arg, int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_penalty_repeat(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_repeat(value); + v_setreal(map_add_var(self, "penalty_repeat", 0), value); + result = 1; + } + } + return result; +} + +// +// llama.set_penalty_freq(0.8) +// +static int cmd_llama_set_penalty_freq(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_penalty_freq", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_freq(value); + v_setreal(map_add_var(self, "penalty_freq", 0), value); + result = 1; + } + } + return result; +} + +// +// llama.set_penalty_present(0.8) +// +static int cmd_llama_set_penalty_present(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_penalty_present", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_present(value); + v_setreal(map_add_var(self, "penalty_present", 0), value); result = 1; } } @@ -113,7 +155,9 @@ static int cmd_llama_set_penalty_last_n(var_s *self, int argc, slib_par_t *arg, int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_penalty_last_n(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_penalty_last_n(value); + v_setreal(map_add_var(self, "penalty_last_n", 0), value); result = 1; } } @@ -132,7 +176,9 @@ static int cmd_llama_set_max_tokens(var_s *self, int argc, slib_par_t *arg, var_ int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_max_tokens(get_param_int(argc, arg, 0, 0)); + auto value = get_param_int(argc, arg, 0, 0); + llama.set_max_tokens(value); + v_setreal(map_add_var(self, "max_tokens", 0), value); result = 1; } } @@ -150,7 +196,9 @@ static int cmd_llama_set_min_p(var_s *self, int argc, slib_par_t *arg, var_s *re int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_min_p(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_min_p(value); + v_setreal(map_add_var(self, "min_p", 0), value); result = 1; } } @@ -168,7 +216,9 @@ static int cmd_llama_set_temperature(var_s *self, int argc, slib_par_t *arg, var int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_temperature(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_temperature(value); + v_setreal(map_add_var(self, "temperature", 0), value); result = 1; } } @@ -186,7 +236,9 @@ static int cmd_llama_set_top_k(var_s *self, int argc, slib_par_t *arg, var_s *re int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_top_k(get_param_int(argc, arg, 0, 0)); + auto value = get_param_int(argc, arg, 0, 0); + llama.set_top_k(value); + v_setreal(map_add_var(self, "top_k", 0), value); result = 1; } } @@ -204,7 +256,49 @@ static int cmd_llama_set_top_p(var_s *self, int argc, slib_par_t *arg, var_s *re int id = get_llama_class_id(self, retval); if (id != -1) { Llama &llama = g_llama.at(id); - llama.set_top_p(get_param_num(argc, arg, 0, 0)); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_top_p(value); + v_setreal(map_add_var(self, "top_p", 0), value); + result = 1; + } + } + return result; +} + +// +// llama.set_grammar("text") +// +static int cmd_llama_set_grammar(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_grammar", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto value = get_param_str(argc, arg, 0, 0); + llama.set_grammar(value, "root"); + v_setstr(map_add_var(self, "grammar", 0), value); + result = 1; + } + } + return result; +} + +// +// llama.set_seed(123) +// +static int cmd_llama_set_seed(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 1) { + error(retval, "llama.set_seed", 1, 1); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto value = get_param_num(argc, arg, 0, 0); + llama.set_seed(value); + v_setreal(map_add_var(self, "seed", 0), value); result = 1; } } @@ -307,20 +401,21 @@ static int cmd_llama_tokens_sec(var_s *self, int argc, slib_par_t *arg, var_s *r } // -// print llama.generate("please generate as simple program in BASIC to draw a cat") +// print llama.add_message("please generate as simple program in BASIC to draw a cat") // -static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *retval) { +static int cmd_llama_add_message(var_s *self, int argc, slib_par_t *arg, var_s *retval) { int result = 0; - if (argc != 1) { - error(retval, "llama.generate", 1, 1); + if (argc != 2) { + error(retval, "llama.add_message", 2, 2); } else { int id = get_llama_class_id(self, retval); if (id != -1) { int iter_id = ++g_nextId; LlamaIter &iter = g_llama_iter[iter_id]; Llama &llama = g_llama.at(id); - auto prompt = get_param_str(argc, arg, 0, ""); - if (llama.generate(iter, prompt)) { + auto role = get_param_str(argc, arg, 0, "user"); + auto content = get_param_str(argc, arg, 1, ""); + if (llama.add_message(iter, role, content)) { map_init_id(retval, iter_id, CLASS_ID_LLAMA_ITER); v_create_callback(retval, "all", cmd_llama_all); v_create_callback(retval, "has_next", cmd_llama_has_next); @@ -328,6 +423,7 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret v_create_callback(retval, "tokens_sec", cmd_llama_tokens_sec); result = 1; } else { + g_llama_iter.erase(iter_id); error(retval, llama.last_error()); } } @@ -335,26 +431,61 @@ static int cmd_llama_generate(var_s *self, int argc, slib_par_t *arg, var_s *ret return result; } +// +// print llama.mem_info() +// +static int cmd_llama_mem_info(var_s *self, int argc, slib_par_t *arg, var_s *retval) { + int result = 0; + if (argc != 0) { + error(retval, "llama.mem_info", 0, 0); + } else { + int id = get_llama_class_id(self, retval); + if (id != -1) { + Llama &llama = g_llama.at(id); + auto mem_info = llama.memory_info(); + map_init(retval); + v_setint(map_add_var(retval, "kv_used", 0), mem_info.kv_used); + v_setint(map_add_var(retval, "kv_total", 0), mem_info.kv_total); + v_setreal(map_add_var(retval, "kv_percent", 0), mem_info.kv_percent); + v_setint(map_add_var(retval, "vram_used", 0), mem_info.vram_used); + v_setint(map_add_var(retval, "vram_total", 0), mem_info.vram_total); + v_setreal(map_add_var(retval, "vram_percent", 0), mem_info.vram_percent); + v_setint(map_add_var(retval, "n_layers_cpu", 0), mem_info.n_layers_cpu); + v_setint(map_add_var(retval, "n_layers_gpu", 0), mem_info.n_layers_gpu); + v_setint(map_add_var(retval, "n_layers_total", 0), mem_info.n_layers_total); + v_setstr(map_add_var(retval, "advice", 0), mem_info.advice.c_str()); + result = 1; + } + } + return result; +} + static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { int result; auto model = expand_path(get_param_str(argc, params, 0, "")); auto n_ctx = get_param_int(argc, params, 1, 2048); auto n_batch = get_param_int(argc, params, 2, 1024); auto n_gpu_layers = get_param_int(argc, params, 3, -1); + auto n_log_level = get_param_int(argc, params, 4, GGML_LOG_LEVEL_CONT); int id = ++g_nextId; Llama &llama = g_llama[id]; - if (llama.construct(model, n_ctx, n_batch, n_gpu_layers)) { + if (llama.load_model(model, n_ctx, n_batch, n_gpu_layers, n_log_level)) { map_init_id(retval, id, CLASS_ID_LLAMA); v_create_callback(retval, "add_stop", cmd_llama_add_stop); - v_create_callback(retval, "generate", cmd_llama_generate); + v_create_callback(retval, "add_message", cmd_llama_add_message); v_create_callback(retval, "reset", cmd_llama_reset); v_create_callback(retval, "set_penalty_repeat", cmd_llama_set_penalty_repeat); + v_create_callback(retval, "set_penalty_freq", cmd_llama_set_penalty_freq); + v_create_callback(retval, "set_penalty_present", cmd_llama_set_penalty_present); v_create_callback(retval, "set_penalty_last_n", cmd_llama_set_penalty_last_n); v_create_callback(retval, "set_max_tokens", cmd_llama_set_max_tokens); v_create_callback(retval, "set_min_p", cmd_llama_set_min_p); v_create_callback(retval, "set_temperature", cmd_llama_set_temperature); v_create_callback(retval, "set_top_k", cmd_llama_set_top_k); v_create_callback(retval, "set_top_p", cmd_llama_set_top_p); + v_create_callback(retval, "set_grammar", cmd_llama_set_grammar); + v_create_callback(retval, "set_seed", cmd_llama_set_seed); + v_create_callback(retval, "mem_info", cmd_llama_mem_info); result = 1; } else { error(retval, llama.last_error()); @@ -365,7 +496,7 @@ static int cmd_create_llama(int argc, slib_par_t *params, var_t *retval) { } FUNC_SIG lib_func[] = { - {1, 4, "LLAMA", cmd_create_llama}, + {1, 5, "LLAMA", cmd_create_llama}, }; SBLIB_API int sblib_func_count() { @@ -388,7 +519,7 @@ int sblib_init(const char *sourceFile) { // // Release variables falling out of scope // -SBLIB_API void sblib_free(int cls_id, int id) { +SBLIB_API int sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { case CLASS_ID_LLAMA: @@ -403,6 +534,37 @@ SBLIB_API void sblib_free(int cls_id, int id) { break; } } + return 0; +} + +// +// Move the mapped instance to a new position and returns the position +// +SBLIB_API int sblib_refresh_id(int cls_id, int id) { + int result = id; + if (id != -1) { + switch (cls_id) { + case CLASS_ID_LLAMA: + if (g_llama.find(id) != g_llama.end()) { + result = ++g_nextId; + auto it = g_llama.find(id); + auto value = std::move(it->second); + g_llama.erase(it); + g_llama.emplace(result, std::move(value)); + } + break; + case CLASS_ID_LLAMA_ITER: + if (g_llama_iter.find(id) != g_llama_iter.end()) { + result = ++g_nextId; + auto it = g_llama_iter.find(id); + auto value = std::move(it->second); + g_llama_iter.erase(it); + g_llama_iter.emplace(result, std::move(value)); + } + break; + } + } + return result; } // diff --git a/llama/nitro.cpp b/llama/nitro.cpp new file mode 100644 index 0000000..5a5f45a --- /dev/null +++ b/llama/nitro.cpp @@ -0,0 +1,2734 @@ +// nitro.cpp — Nitro Agent +// A standalone agentic LLM shell with notcurses TUI. +// Uses llama-sb.h as the sole llama.cpp integration layer. +// +// Build (example): +// g++ -std=c++20 -O2 nitro.cpp llama-sb.cpp \ +// -I/path/to/llama.cpp/include \ +// -L/path/to/llama.cpp/build/src \ +// -lllama -lggml -lnotcurses-core -lnotcurses -lcurl \ +// -o nitro +// +// Usage: +// ./nitro [options] [project_dir] +// +// Options: +// -m, --model GGUF model to load on startup +// -e, --embed embedding model for RAG +// -g, --gpu-layers layers to offload to GPU (default: 32) +// +// Slash commands: +// /model — load / hot-reload a GGUF model (picker if no path) +// /embed — load an embedding model for RAG (picker if no path) +// /rag — index a file or directory into RAG +// /memory — show KV / VRAM / layer stats +// /clear — reset conversation (keeps system prompt) +// /help — list commands +// +// Tool protocol (LLM emits, Nitro executes): +// TOOL:LIST [dir] +// TOOL:READ +// TOOL:WRITE +// TOOL:EXISTS +// TOOL:RUN [args] +// TOOL:DATE +// TOOL:TIME +// TOOL:RND +// TOOL:CURL +// +// Copyright (C) 2026 Chris Warren-Smith — GPLv2 or later +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "llama-sb.h" +#include "llama-sb-rag.h" + +#include + +namespace fs = std::filesystem; + +// +// NitroConfig +// +struct NitroConfig { + std::string model_path; + std::string embed_path; + std::string sandbox; + std::string agent_id; + int n_ctx = 65536; + int n_batch = 512; + int n_gpu_layers = 32; + int log_level = GGML_LOG_LEVEL_CONT; + float temperature = 0.6f; + float top_p = 0.95f; + float min_p = 0.0f; + int top_k = 20; + float penalty_repeat = 1.0f; + int penalty_last_n = 256; + std::vector knowledge_files; + int rag_top_k = 5; + bool thinking = true; + bool permission_prompt = false; + // TOOL:RUN allowlist — if non-empty, only these program basenames may run. + // Empty means "allow anything inside the sandbox" (original behaviour). + std::vector run_allowed; +}; + +// +// InputHistory — up/down arrow navigation through submitted inputs +// +class InputHistory { + public: + explicit InputHistory() = default; + ~InputHistory() = default; + InputHistory(const InputHistory &) = delete; + InputHistory &operator=(const InputHistory &) = delete; + + /** + * @brief Adds a new command string to the history stack. + * Resets navigation index upon adding a new item. + * Deduplicates consecutive identical entries. + */ + void push(const std::string &input) { + if (input.empty()) return; + if (!history_stack.empty() && history_stack.back() == input) { + // Don't push duplicate of last entry; just reset nav position. + current_index = static_cast(history_stack.size()); + return; + } + history_stack.push_back(input); + current_index = static_cast(history_stack.size()); + } + + /** + * @brief Navigates to an earlier entry. + * @param out Set to the selected entry on success. + * @return true if an item was successfully retrieved. + */ + bool up(std::string &out) { + if (history_stack.empty() || current_index <= 0) return false; + --current_index; + out = history_stack[current_index]; + return true; + } + + /** + * @brief Navigates to a later entry, or clears when past the newest. + * @param out Set to the selected entry, or cleared if past the end. + * @return true if a history entry was retrieved (false means "clear input"). + */ + bool down(std::string &out) { + if (history_stack.empty()) return false; + ++current_index; + if (current_index >= static_cast(history_stack.size())) { + current_index = static_cast(history_stack.size()); + out.clear(); + return false; // signal: restore blank input + } + out = history_stack[current_index]; + return true; + } + + /** Reset navigation position without modifying the stack. */ + void reset_nav() { + current_index = static_cast(history_stack.size()); + } + + /** + * @brief Load history from ~/.config/nitro/nitro.history (one entry per line). + * Silently succeeds if the file doesn't exist. + */ + void load(const std::string &path) { + std::ifstream f(path); + if (!f) return; + std::string line; + while (std::getline(f, line)) { + if (!line.empty()) history_stack.push_back(line); + } + current_index = static_cast(history_stack.size()); + } + + /** + * @brief Persist history to disk (most-recent last, one entry per line). + * Caps at MAX_PERSIST entries so the file never grows unbounded. + */ + void save(const std::string &path) const { + // Ensure parent directory exists. + fs::path dir = fs::path(path).parent_path(); + std::error_code ec; + fs::create_directories(dir, ec); + + std::ofstream f(path, std::ios::trunc); + if (!f) return; + + static constexpr int MAX_PERSIST = 500; + int start = std::max(0, static_cast(history_stack.size()) - MAX_PERSIST); + for (int i = start; i < static_cast(history_stack.size()); ++i) { + // Escape embedded newlines so each entry stays on one line. + for (char c : history_stack[i]) { + if (c == '\n') f << "\\n"; + else f << c; + } + f << '\n'; + } + } + + private: + std::vector history_stack; + int current_index = 0; +}; + +// +// Notcurses TUI +// +// +// ┌──────────────────── header (1 row) ─────────────────────────────────┐ +// │ ✦ NITRO model: … tok/s: … KV: …% VRAM: …% │ +// ├─────────────────────────────────────────────────────────────────────┤ +// │ │ +// │ chat pane (rows 1 … term_rows-3) │ +// │ │ +// ├─────────────────────────────────────────────────────────────────────┤ +// │ ───────────────────────────────────── (separator) │ +// │ ❯ input │ +// └─────────────────────────────────────────────────────────────────────┘ +struct TuiState { + // ── notcurses handles ────────────────────────────────────────────── + struct notcurses *nc = nullptr; + struct ncplane *stdpl = nullptr; + struct ncplane *header = nullptr; + struct ncplane *chatpl = nullptr; + struct ncplane *inputpl = nullptr; + // ── chat buffer ─────────────────────────────────────────────────── + std::vector chat_lines; + int scroll_offset = 0; + std::mutex lines_mutex; + // ── streaming accumulator ───────────────────────────────────────── + std::string token_acc; + // ── input ───────────────────────────────────────────────────────── + std::string input_buf; + size_t cursor_pos = 0; + bool mouse_mode = true; + // ── status bar values ───────────────────────────────────────────── + std::string current_model = "none"; + float tokens_per_sec = 0.0f; + int kv_used = 0; + int kv_total = 1; + size_t vram_used = 0; + size_t vram_total = 1; + int term_rows = 0; + int term_cols = 0; + // ── thinking spinner ────────────────────────────────────────────── + bool thinking = false; + int spinner_frame = 0; + // ── input history ───────────────────────────────────────────────── + InputHistory history; + // Advance spinner by one frame and redraw the header. + void tick_spinner(); + // Toggle thinking mode; redraws header immediately. + void set_thinking(bool on); + // ── lifecycle ───────────────────────────────────────────────────── + void init(); + void destroy(); + void resize(); + // ── draw ────────────────────────────────────────────────────────── + void redraw_header() const; + void redraw_chat(); + void redraw_input() const; + void redraw_all(); + // ── content helpers ─────────────────────────────────────────────── + void append_line(const std::string &line); + void append_token(const std::string &token); + void flush_token_acc(); + // ── interaction ─────────────────────────────────────────────────── + bool confirm_dialog(const std::string &prompt) const; + // Blocking readline with history navigation, cursor, arrow-key scrolling. + std::string readline_blocking(); + // Modal popup overlay while a long operation runs. + // Call show_modal_popup to display; dismiss_modal_popup to remove. + // The popup plane is stored in modal_plane; callers hold it as an opaque + // handle — or just use the paired helpers below. + struct ncplane *modal_plane = nullptr; + void show_modal_popup(const std::string &message); + void show_help(); + void dismiss_modal_popup(); + // ── folder picker popup ─────────────────────────────────────── + // Presents an interactive directory browser to let the user choose a + // folder (or file) to index. Returns the selected path, or empty string + // if the user cancelled. + // ── file browser popup ───────────────────────────────────── + // Used by /rag, /model, and /embed to pick a path interactively. + // Pass a hint string shown in the title bar (e.g. "RAG Folder", + // "Model File", "Embedding Model"). + // Returns the selected path, or empty string if the user cancelled. + std::string file_picker(const std::string &start_dir, + const std::string &title_hint = "File") const; + // Legacy alias kept for callers that used the old name. + std::string rag_folder_picker(const std::string &start_dir) const { + return file_picker(start_dir, "RAG Folder"); + } +}; + +// +// AgentState +// +struct AgentState { + std::unique_ptr llama; + std::unique_ptr iter; + std::unique_ptr embed_llama; + std::unique_ptr rag_db; + std::unique_ptr rag_session; + bool model_loaded = false; + std::string system_prompt; + + bool rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) const; + bool rag_load_index(const std::string &path, TuiState &tui) const; + bool run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) const; + bool setup_embed(const std::string &path, TuiState &tui); + bool setup_model(const NitroConfig &cfg, TuiState &tui); + void apply_generation_params(const NitroConfig &cfg) const; + void reset_conversation(const std::string &sysprompt, TuiState &tui); + std::string memory_info_text() const; + std::string process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) const; + std::string rag_tool(const NitroConfig &cfg, const std::string &agent_query) const; + float tokens_per_sec() const; +}; + +// +// Logging +// + +// ─── Debug logging (file-backed, safe to call while notcurses is active) ── +static FILE *g_logfile = nullptr; + +static void log_open() { + const char *home = getenv("HOME"); + std::string path = std::string(home ? home : ".") + "/.config/nitro/nitro.log"; + g_logfile = fopen(path.c_str(), "a"); +} + +static void log_close() { + if (g_logfile) { fclose(g_logfile); g_logfile = nullptr; } +} + +static void log_write(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +static void log_write(const char *fmt, ...) { + if (!g_logfile) { + return; + } + // timestamp + time_t t = time(nullptr); + char ts[32]; + strftime(ts, sizeof(ts), "%H:%M:%S", localtime(&t)); + fprintf(g_logfile, "[%s] ", ts); + va_list ap; + va_start(ap, fmt); + vfprintf(g_logfile, fmt, ap); + va_end(ap); + fputc('\n', g_logfile); + // flush immediately so tail -f works + fflush(g_logfile); +} + +// +// Agent uniqueId +// +inline std::string encode_base64(const std::vector& data) { + static const char base64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + std::string encoded; + encoded.reserve((data.size() + 2) / 3 * 4); + + size_t i = 0; + while (i < data.size()) { + uint32_t val = static_cast(data[i] << 16) | + (i + 1 < data.size() ? static_cast(data[i+1]) << 8 : 0) | + (i + 2 < data.size() ? static_cast(data[i+2]) : 0); + + encoded.push_back(base64_chars[(val >> 18) & 0x3F]); + encoded.push_back(base64_chars[(val >> 12) & 0x3F]); + encoded.push_back((i + 1 < data.size()) ? base64_chars[(val >> 6) & 0x3F] : '='); + encoded.push_back((i + 2 < data.size()) ? base64_chars[val & 0x3F] : '='); + i += 3; + } + return encoded; +} + +class AgentSessionId { + public: + // Static method: Generates ID once, then returns it + static std::string uniqueId() { + // Yoda condition: static variable initialized only once + static std::string s_id; + + if (s_id.empty()) { + // 1. Get high-resolution timestamp (nanoseconds since epoch) + auto now = std::chrono::steady_clock::now(); + auto nanos = std::chrono::duration_cast(now.time_since_epoch()).count(); + + // 2. Generate 48 bits of randomness + std::random_device rd; + std::mt19937_64 rng(rd()); + std::uniform_int_distribution dist(0, UINT64_MAX); + + // Fill with random bytes + std::array random_bytes; + for (auto& b : random_bytes) { + b = static_cast(dist(rng) & 0xFF); + + } + + // 3. Combine timestamp (48 bits) and random (48 bits) into a 96-bit integer + std::vector data; + data.reserve(12); // 96 bits = 12 bytes + + // Pack timestamp (upper 48 bits) + data.push_back(static_cast((nanos >> 40) & 0xFF)); + data.push_back(static_cast((nanos >> 32) & 0xFF)); + data.push_back(static_cast((nanos >> 24) & 0xFF)); + data.push_back(static_cast((nanos >> 16) & 0xFF)); + data.push_back(static_cast((nanos >> 8) & 0xFF)); + data.push_back(static_cast(nanos & 0xFF)); + + // Pack random (lower 48 bits) + data.push_back(static_cast((dist(rng) >> 40) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 32) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 24) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 16) & 0xFF)); + data.push_back(static_cast((dist(rng) >> 8) & 0xFF)); + data.push_back(static_cast(dist(rng) & 0xFF)); + + // 4. Encode to Base64 + s_id = encode_base64(data); + } + return s_id; + } +}; + +// +// handling for strip_code_fences +// +static const std::vector CODE_EXTENSIONS = { + ".py",".c",".cpp",".h",".bas",".java",".html",".js",".ts", + ".json",".yaml",".toml",".sh",".go",".rs",".jsx",".tsx" +}; + +// +// Settings persistence (~/.config/nitro/nitro.settings.json) +// Returns the canonical settings path: ~/.config/nitro/settings.json +// +static std::string settings_path() { + // Attempt to read settings from the current working directory first + if (fs::exists("settings.json")) { + return "settings.json"; + } + const char *home = getenv("HOME"); + std::string base = home ? std::string(home) : "."; + return base + "/.config/nitro/settings.json"; +} + +// Returns the history file path: ~/.config/nitro/history.txt +static std::string history_path() { + const char *home = getenv("HOME"); + std::string base = home ? std::string(home) : "."; + return base + "/.config/nitro/history.txt"; +} + +// +// A minimal hand-rolled JSON reader/writer for the flat key-value settings +// we care about. We deliberately avoid a full JSON library dependency. +// +static bool json_get_string(const std::string &json, + const std::string &key, + std::string &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos += search.size(); + while (pos < json.size() && json[pos] == ' ') ++pos; + if (pos >= json.size() || json[pos] != '"') return false; + ++pos; + out.clear(); + while (pos < json.size()) { + char c = json[pos++]; + if (c == '\\' && pos < json.size()) { + char e = json[pos++]; + switch (e) { + case 'n': out += '\n'; break; + case 't': out += '\t'; break; + case '"': out += '"'; break; + case '\\': out += '\\'; break; + default: out += e; break; + } + } else if (c == '"') { + break; + } else { + out += c; + } + } + return true; +} + +// Tiny helper: extract a quoted string value from flat JSON for a known key. +static bool settings_get_str(const std::string &json, + const std::string &key, + std::string &out) { + return json_get_string(json, key, out); +} + +// Tiny helper: extract an integer value from flat JSON. +static bool settings_get_int(const std::string &json, + const std::string &key, + int &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos += search.size(); + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) ++pos; + if (pos >= json.size()) return false; + // read digits (and optional leading minus) + size_t start = pos; + if (json[pos] == '-') ++pos; + while (pos < json.size() && std::isdigit((unsigned char)json[pos])) ++pos; + if (pos == start) return false; + out = std::stoi(json.substr(start, pos - start)); + return true; +} + +// Tiny helper: extract a float value from flat JSON. +static bool settings_get_float(const std::string &json, + const std::string &key, + float &out) { + std::string search = "\"" + key + "\":"; + size_t pos = json.find(search); + if (pos == std::string::npos) { + return false; + } + pos += search.size(); + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) { + ++pos; + } + if (pos >= json.size()) { + return false; + } + size_t start = pos; + if (json[pos] == '-') { + ++pos; + } + while (pos < json.size() && (std::isdigit((unsigned char)json[pos]) || json[pos] == '.')) { + ++pos; + } + if (pos == start) { + return false; + } + out = std::stof(json.substr(start, pos - start)); + return true; +} + +// Load settings from disk into cfg. Fields present in the file overwrite +// the defaults already in cfg; fields absent are left at their defaults. +// Silently succeeds if the file doesn't exist yet. +static void load_settings(NitroConfig &cfg) { + std::string path = settings_path(); + std::ifstream f(path); + if (!f) return; // no file → use defaults + std::ostringstream oss; oss << f.rdbuf(); + std::string json = oss.str(); + + cfg.thinking = true; + cfg.agent_id = AgentSessionId::uniqueId(); + + // String fields + settings_get_str(json, "model_path", cfg.model_path); + settings_get_str(json, "embed_path", cfg.embed_path); + settings_get_str(json, "sandbox", cfg.sandbox); + + // Integer fields + settings_get_int(json, "n_ctx", cfg.n_ctx); + settings_get_int(json, "n_batch", cfg.n_batch); + settings_get_int(json, "n_gpu_layers", cfg.n_gpu_layers); + settings_get_int(json, "top_k", cfg.top_k); + settings_get_int(json, "penalty_last_n", cfg.penalty_last_n); + settings_get_int(json, "rag_top_k", cfg.rag_top_k); + + // Float fields + settings_get_float(json, "temperature", cfg.temperature); + settings_get_float(json, "top_p", cfg.top_p); + settings_get_float(json, "min_p", cfg.min_p); + settings_get_float(json, "penalty_repeat", cfg.penalty_repeat); +} + +// +// icons +// +static constexpr std::string ICON_ERR = " ⚡ ▏"; +static constexpr std::string ICON_THINK = " 🤔 ▏"; +static constexpr std::string ICON_TOOL = " 🔧 ▏"; +static constexpr std::string ICON_SYS = " 🤖 ▏"; + +static std::string introspect(const NitroConfig &cfg) { + static constexpr std::string_view tmpl = + "{{\n" + " \"model_path\": \"{}\",\n" + " \"embed_path\": \"{}\",\n" + " \"sandbox\": \"{}\",\n" + " \"n_ctx\": {},\n" + " \"n_batch\": {},\n" + " \"n_gpu_layers\": {},\n" + " \"temperature\": {},\n" + " \"top_p\": {},\n" + " \"min_p\": {},\n" + " \"top_k\": {},\n" + " \"penalty_repeat\": {},\n" + " \"penalty_last_n\": {},\n" + " \"rag_top_k\": {}\n" + "}}\n"; + return std::format(tmpl, + cfg.model_path, + cfg.embed_path, + cfg.sandbox, + cfg.n_ctx, + cfg.n_batch, + cfg.n_gpu_layers, + cfg.temperature, + cfg.top_p, + cfg.min_p, + cfg.top_k, + cfg.penalty_repeat, + cfg.penalty_last_n, + cfg.rag_top_k); +} + +// Persist the current cfg to ~/.config/nitro/settings.json. +static bool save_settings(const NitroConfig &cfg) { + std::string path = settings_path(); + fs::path dir = fs::path(path).parent_path(); + std::error_code ec; + fs::create_directories(dir, ec); + + std::ofstream f(path, std::ios::trunc); + if (!f) { + return false; + } + + f << introspect(cfg); + + return f.good(); +} + +// +// Trims whitespace from both ends of a string +// +static std::string trim(std::string_view str) { + constexpr std::string_view whitespace = " \t\n\r\f\v"; + + // Find the first non-whitespace character + const auto start = str.find_first_not_of(whitespace); + if (start == std::string_view::npos) { + return ""; // The string is entirely whitespace + } + + // Find the last non-whitespace character + const auto end = str.find_last_not_of(whitespace); + + // Return the substring between start and end + return std::string(str.substr(start, end - start + 1)); +} + +/* + * unwrap() - Remove a matching outer "wrapper" from a string. + * + * Trims leading/trailing whitespace first, then checks (in order): + * + * 1. Same-character pairs "..." '...' |...| `...` + * 2. Mirror pairs (...) [...] {...} + * 3. HTML-like tags ... + * 4. Plain angle brackets <...> (fallback if tags don't match) + * + * If none of the above apply, returns the whitespace-trimmed input unchanged. + * + * Examples: + * unwrap("\"hello\"") -> "hello" + * unwrap(" [foo] ") -> "foo" + * unwrap("bold") -> "bold" + * unwrap("x") -> "x" + * unwrap("") -> "hello" + * unwrap("plain") -> "plain" + * unwrap("") -> "" + */ +std::string unwrap(const std::string &input) { + if (input.empty()) { + return input; + } + + size_t left = 0; + size_t right = input.length() - 1; + + while (left <= right && std::isspace(static_cast(input[left]))) { + left++; + } + while (left <= right && std::isspace(static_cast(input[right]))) { + right--; + } + + if (left > right) { + return ""; + } + + // Same-character pairs: "", '', ||, `` + // Note: [], {} are NOT same-char pairs — they belong in mirror pairs only + if (input[left] == input[right]) { + if (input[left] == '"' || input[left] == '\'' || + input[left] == '|' || input[left] == '`') { + return input.substr(left + 1, right - left - 1); + } + } + + // Mirror pairs: (), [], {}, but NOT <> (handled below as possible HTML tags) + if (input[left] != input[right]) { + if ((input[left] == '(' && input[right] == ')') || + (input[left] == '[' && input[right] == ']') || + (input[left] == '{' && input[right] == '}')) { + return input.substr(left + 1, right - left - 1); + } + } + + // HTML-like tags: content + // Also handles plain <...> as a fallback at the end + if (input[left] == '<' && input[right] == '>') { + // Find end of opening tag + size_t openTagEnd = left + 1; + while (openTagEnd <= right && input[openTagEnd] != '>') openTagEnd++; + + if (openTagEnd < right) { + std::string openTagName = input.substr(left + 1, openTagEnd - left - 1); + + // Find start of closing tag (search backwards for '<') + size_t closeTagStart = right; + while (closeTagStart > openTagEnd && input[closeTagStart] != '<') closeTagStart--; + + if (closeTagStart > openTagEnd && input[closeTagStart + 1] == '/') { + std::string closeTagName = input.substr(closeTagStart + 2, right - closeTagStart - 2); + + if (!openTagName.empty() && openTagName == closeTagName) { + // Return content between the tags + return input.substr(openTagEnd + 1, closeTagStart - openTagEnd - 1); + } + } + } + + // Fallback: plain <...> with no matching HTML tags — unwrap the angle brackets + return input.substr(left + 1, right - left - 1); + } + + return input.substr(left, right - left + 1); +} + +// ─── colour helpers ────────────────────────────────────────────────────── +static constexpr uint32_t BG_CHAT_R = 18, BG_CHAT_G = 22, BG_CHAT_B = 30; +static constexpr uint32_t BG_INP_R = 22, BG_INP_G = 28, BG_INP_B = 38; +static constexpr uint32_t BG_HDR_R = 30, BG_HDR_G = 40, BG_HDR_B = 55; + +static inline uint64_t chat_ch(uint32_t r, uint32_t g, uint32_t b) { + return NCCHANNELS_INITIALIZER(r, g, b, BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); +} + +static inline uint64_t inp_ch(uint32_t r, uint32_t g, uint32_t b) { + return NCCHANNELS_INITIALIZER(r, g, b, BG_INP_R, BG_INP_G, BG_INP_B); +} + +static inline uint64_t hdr_ch(uint32_t r, uint32_t g, uint32_t b) { + return NCCHANNELS_INITIALIZER(r, g, b, BG_HDR_R, BG_HDR_G, BG_HDR_B); +} + +// +// File-system helpers +// +static std::string join_path(const std::string &a, const std::string &b) { + if (b.empty()) return a; + if (b[0] == '/') return b; + std::string pa = a; + if (!pa.empty() && pa.back() == '/') pa.pop_back(); + std::string pb = (b.front() == '/') ? b.substr(1) : b; + return pa + "/" + pb; +} + +static std::string read_file(const std::string &path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return "ERROR: cannot open [" + path + "]"; + } + std::ostringstream oss; oss << f.rdbuf(); + return oss.str(); +} + +static std::string list_dir(const std::string &path) { + std::ostringstream oss; + std::error_code ec; + for (const auto &e : fs::directory_iterator(path, ec)) { + if (ec) break; + std::string name = e.path().filename().string(); + if (name.empty() || name[0] == '.') continue; + oss << (e.is_directory() ? "[" + name + "]" : name) << "\n"; + } + return oss.str(); +} + +static bool path_in_sandbox(const std::string &sandbox, const std::string &path) { + std::error_code ec; + auto base = fs::canonical(sandbox, ec); if (ec) return false; + auto target = fs::weakly_canonical(path, ec); + std::string bstr = base.string() + "/"; + std::string tstr = target.string(); + return tstr == base.string() || tstr.compare(0, bstr.size(), bstr) == 0; +} + +static bool write_file(const std::string &path, const std::string &data) { + fs::path p(path); + if (p.has_parent_path()) { + std::error_code ec; + fs::create_directories(p.parent_path(), ec); + } + std::ofstream f(path, std::ios::binary | std::ios::trunc); + if (!f) return false; + f.write(data.data(), (std::streamsize)data.size()); + return f.good(); +} + +static bool make_dir(const std::string &path) { + try { + std::filesystem::path p(path); + if (fs::exists(p)) { + return true; + } + std::error_code ec; + return fs::create_directories(p, ec); + } + catch (const std::filesystem::filesystem_error &e) { + log_write("mkdir failed [%s]", e.what()); + return false; + } +} + +// +// System prompt +// +static std::string build_system_prompt(NitroConfig &cfg) { + std::string p; + p += + "You are Nitro, an agentic AI assistant for software development. " + "Proceed with caution, guided by logic and the pursuit of knowledge.\n\n" + + "Your sandbox (project directory) is: " + cfg.sandbox + "\n\n" + + "## Core Principle\n" + "Always follow this loop: THINK → DECIDE → ACT → RESPOND\n\n" + + "## Reasoning Protocol\n" + "Use <|think|> to reason BEFORE acting. Keep it concise and structured.\n" + "Format:\n" + "<|think|>\n" + "- What is the user asking?\n" + "- Do I need external data (files, tools)?\n" + "- What is the safest and most correct action?\n" + "\n\n" + "Rules:\n" + "- Do NOT call tools inside <|think|>\n" + "- Do NOT include the final answer inside <|think|>\n" + "- Always follow <|think|> with either a tool call OR a final answer\n" + "- Skip <|think|> only for trivial or conversational responses\n\n" + + "## Tool Protocol\n" + "Emit ONE tool call at a time, immediately followed by NITRO_END_TOOL.\n" + "Do NOT add any commentary, explanation, or text between the tool call and NITRO_END_TOOL.\n" + "The host executes the tool and returns NITRO_TOOL_RESULT: .\n" + "Wait for the result before continuing.\n" + "After receiving NITRO_TOOL_RESULT you may explain what you did.\n\n" + "Examples:\n\n" + "TOOL:LIST\n" + "NITRO_END_TOOL\n\n" + "TOOL:READ readme.txt\n" + "NITRO_END_TOOL\n\n" + "TOOL:WRITE index.html ...\n" + "NITRO_END_TOOL\n\n" + "TOOL:RUN ./build.sh\n" + "NITRO_END_TOOL\n\n" + + "## Available Tools\n" + " TOOL:LIST [dir] list files (default: sandbox root)\n" + " TOOL:READ read file contents\n" + " TOOL:WRITE write text to file\n" + " TOOL:MKDIR create a subfolder inside the sandbox\n" + " TOOL:EXISTS YES or NO\n" + " TOOL:RUN [args] run program inside sandbox\n" + " TOOL:DATE current date\n" + " TOOL:TIME current time\n" + " TOOL:RND random float 0..1\n" + " TOOL:RAG query the RAG index for additional context\n" + " TOOL:ASK ask the user for clarification or additional context\n" + " TOOL:INTROSPECT show current model settings\n" + " TOOL:CURL HTTP GET, returns response body (max 32 KB)\n" + " TOOL:PERMISSION ask user for explicit permission\n\n" + + "## Tool Decision Rules\n" + "Use tools ONLY if:\n" + "- The user explicitly references files or the project, OR\n" + "- The answer depends on local or project data, OR\n" + "- The user asks for date, time, or a random number\n" + "Otherwise answer directly using internal knowledge.\n\n" + + "## Tool Rules\n" + "- NITRO_END_TOOL must immediately follow the tool call — no exceptions\n" + "- Never add commentary before NITRO_END_TOOL\n" + "- Only use one tool at a time, step by step\n" + "- Never access files outside the sandbox\n" + "- Use TOOL:PERMISSION before destructive or irreversible operations\n" + "- Do NOT hallucinate file contents\n" + "- Do NOT fabricate tool outputs\n" + "- Do NOT assume files exist — use TOOL:EXISTS to check first\n\n" + + "## File Writing Rules\n" + "Use TOOL:WRITE only if explicitly requested.\n" + "- Write complete and valid content\n" + "- Do not overwrite without clear intent\n" + "- Use TOOL:PERMISSION before overwriting an existing file\n" + "- Format: TOOL:WRITE \n\n" + + "## Interaction Guidelines\n" + "- Be precise and efficient\n" + "- Ask clarifying questions if the request is ambiguous or missing parameters\n" + "- Prefer direct answers when no tools are needed\n" + "- After each tool result, explain in plain English what was done\n" + "- If no user request is provided, respond with a brief readiness message\n\n"; + + for (const auto &kf : cfg.knowledge_files) { + std::ifstream f(kf); + if (!f) continue; + std::ostringstream oss; oss << f.rdbuf(); + p += "## Knowledge: " + kf + "\n" + oss.str() + "\n\n"; + } + return p; +} + +static std::string strip_code_fences(const std::string &filename, + const std::string &src) { + auto ext = fs::path(filename).extension().string(); + bool is_code = ranges::any_of(CODE_EXTENSIONS, [&](const std::string &e){ return ext == e; }); + if (!is_code) { + return unwrap(src); + } + auto pos = src.find("```"); + if (pos == std::string::npos) { + return src; + } + auto nl = src.find('\n', pos + 3); + if (nl == std::string::npos) { + return src; + } + std::string inner = src.substr(nl + 1); + auto end = inner.rfind("```"); + if (end != std::string::npos) { + inner = inner.substr(0, end); + } + return inner; +} + +// +// TOOL:CURL +// +static size_t curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp) { + auto *buf = static_cast(userp); + auto total = size * nmemb; + static constexpr size_t MAX_BODY = 32 * 1024; + if (buf->size() < MAX_BODY) { + size_t room = MAX_BODY - buf->size(); + buf->append(static_cast(contents), std::min(total, room)); + } + return total; +} + +// +// html_to_text — strip HTML for cleaner TOOL:CURL context +// +// Lightweight HTML→plain-text conversion: +// • Drops , and + for (const std::string &tag : {"script", "style"}) { + std::string open = "<" + tag; + std::string close = ""; + std::string lo = s; + ranges::transform(lo, lo.begin(), ::tolower); + for (;;) { + auto p0 = lo.find(open); + if (p0 == std::string::npos) break; + auto p1 = lo.find(close, p0); + if (p1 == std::string::npos) { s.erase(p0); lo.erase(p0); break; } + s.erase(p0, p1 + close.size() - p0); + lo.erase(p0, p1 + close.size() - p0); + } + } + + // 3. Replace block-level tags with '\n' before stripping all tags. + static const char *const BLOCK[] = { + "p","div","br","li","tr","h1","h2","h3","h4","h5","h6", + "article","section","header","footer","nav","main", nullptr + }; + { + std::string out; + out.reserve(s.size()); + size_t i = 0; + while (i < s.size()) { + if (s[i] != '<') { out += s[i++]; continue; } + auto ce = s.find('>', i); + if (ce == std::string::npos) { out += s[i++]; continue; } + std::string inner = s.substr(i + 1, ce - i - 1); + size_t sp = inner.find_first_of(" \t/\r\n"); + std::string name = (sp != std::string::npos) ? inner.substr(0, sp) : inner; + ranges::transform(name, name.begin(), ::tolower); + for (int k = 0; BLOCK[k]; ++k) { + if (name == BLOCK[k]) { + out += '\n'; break; + } + } + i = ce + 1; + } + s = out; + } + + // 4. Strip all remaining tags. + { + std::string out; out.reserve(s.size()); + bool in_tag = false; + for (char c : s) { + if (c == '<') { in_tag = true; continue; } + if (c == '>') { in_tag = false; continue; } + if (!in_tag) out += c; + } + s = out; + } + + // 5. Decode common HTML entities. + static const std::pair ENT[] = { + {"&","&"},{"<","<"},{">",">"},{""","\""}, + {"'","'"},{" "," "},{"—","—"},{"–","–"}, + {"…","…"},{"'","'"},{""","\""}, + {nullptr,nullptr} + }; + for (int k = 0; ENT[k].first; ++k) { + std::string e = ENT[k].first, r = ENT[k].second; + size_t pos = 0; + while ((pos = s.find(e, pos)) != std::string::npos) + { s.replace(pos, e.size(), r); pos += r.size(); } + } + // Numeric entities &#NNN; and &#xHHH; + { + std::string out; out.reserve(s.size()); + size_t i = 0; + while (i < s.size()) { + if (s[i]=='&' && i+2>6)); out += (char)(0x80|(cp&0x3F)); } + else { out += (char)(0xE0|(cp>>12)); out += (char)(0x80|((cp>>6)&0x3F)); out += (char)(0x80|(cp&0x3F)); } + i = semi+1; continue; + } catch (...) {} + } + } + out += s[i++]; + } + s = out; + } + + // 6. Collapse whitespace; cap blank lines at 2. + { + std::string out; out.reserve(s.size()); + int nl_run = 0; bool last_sp = false; + for (char c : s) { + if (c == '\r') continue; + if (c == '\t') c = ' '; + if (c == '\n') { ++nl_run; last_sp=false; if (nl_run<=2) out+='\n'; continue; } + nl_run = 0; + if (c == ' ') { if (!last_sp) { out+=' '; last_sp=true; } continue; } + last_sp = false; out += c; + } + size_t f = out.find_first_not_of(" \n"); + size_t l = out.find_last_not_of(" \n"); + s = (f == std::string::npos) ? "" : out.substr(f, l-f+1); + } + return s; +} + +static std::string tool_curl(const std::string &url) { + if (url.empty()) return "ERROR: TOOL:CURL requires a URL argument"; + CURL *curl = curl_easy_init(); + if (!curl) return "ERROR: curl_easy_init failed"; + std::string body; + body.reserve(4096); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "nitro/1.0"); + // Accept compressed responses; curl will decompress automatically. + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + + CURLcode res = curl_easy_perform(curl); + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + // Query content-type before cleanup (pointer is only valid while handle lives). + char *ct_raw = nullptr; + curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct_raw); + std::string content_type = ct_raw ? ct_raw : ""; + ranges::transform(content_type, + content_type.begin(), ::tolower); + curl_easy_cleanup(curl); + if (res != CURLE_OK) { + return std::string("ERROR: curl: ") + curl_easy_strerror(res); + } + if (http_code >= 400) { + return "ERROR: HTTP " + std::to_string(http_code) + " from " + url; + } + if (body.empty()) { + return "(empty response)"; + } + + // Strip HTML tags so the model receives clean plain text. + bool is_html = (content_type.find("text/html") != std::string::npos) + || (body.size() > 5 && body.substr(0,5) == " 6 && body.substr(0,6) == ""); + if (is_html) { + body = html_to_text(body); + } + + return body; +} + +// +// TuiState::init +// +void TuiState::init() { + notcurses_options opts{}; + opts.flags = NCOPTION_SUPPRESS_BANNERS; + nc = notcurses_init(&opts, nullptr); + if (!nc) { std::fputs("notcurses_init failed\n", stderr); std::exit(1); } + stdpl = notcurses_stdplane(nc); + notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); + uint64_t bg = NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B); + ncplane_set_base(stdpl, " ", 0, bg); + ncplane_erase(stdpl); + ncplane_options hopt{}; + hopt.y = 0; hopt.x = 0; + hopt.rows = 1; hopt.cols = (unsigned)term_cols; + header = ncplane_create(stdpl, &hopt); + int chat_rows = std::max(1, term_rows - 3); + ncplane_options copt{}; + copt.y = 1; copt.x = 0; + copt.rows = (unsigned)chat_rows; copt.cols = (unsigned)term_cols; + chatpl = ncplane_create(stdpl, &copt); + ncplane_set_base(chatpl, " ", 0, + NCCHANNELS_INITIALIZER(BG_CHAT_R, BG_CHAT_G, BG_CHAT_B, + BG_CHAT_R, BG_CHAT_G, BG_CHAT_B)); + ncplane_options iopt{}; + iopt.y = term_rows - 2; iopt.x = 0; + iopt.rows = 2; iopt.cols = (unsigned)term_cols; + inputpl = ncplane_create(stdpl, &iopt); + ncplane_set_base(inputpl, " ", 0, + NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, + BG_INP_R, BG_INP_G, BG_INP_B)); + notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); + redraw_all(); +} + +void TuiState::destroy() { + if (nc) { + notcurses_stop(nc); + nc = nullptr; + } +} + +void TuiState::resize() { + notcurses_term_dim_yx(nc, (unsigned *)&term_rows, (unsigned *)&term_cols); + ncplane_resize_simple(header, 1, (unsigned)term_cols); + int cr = std::max(1, term_rows - 3); + ncplane_resize_simple(chatpl, (unsigned)cr, (unsigned)term_cols); + ncplane_move_yx(inputpl, term_rows - 2, 0); + ncplane_resize_simple(inputpl, 2, (unsigned)term_cols); + redraw_all(); +} + +// +// TuiState::redraw +// +void TuiState::redraw_header() const { + ncplane_erase(header); + ncplane_set_base(header, " ", 0, + NCCHANNELS_INITIALIZER(BG_HDR_R, BG_HDR_G, BG_HDR_B, + BG_HDR_R, BG_HDR_G, BG_HDR_B)); + float kv_pct = kv_total > 0 ? 100.f * (float)kv_used / (float)kv_total : 0.f; + float vram_pct = vram_total > 0 ? 100.f * (float)vram_used / (float)vram_total : 0.f; + + static const char *const SPIN[] = { "⣾","⣽","⣻","⢿","⡿","⣟","⣯","⣷" }; + const char *spin_str = thinking ? SPIN[spinner_frame % 8] : " "; + char buf[512]; + int n = std::snprintf(buf, sizeof(buf), + " ✦ NITRO │ %-32s │ %5.1f tok/s │ KV %4.1f%% VRAM %4.1f%% %s", + current_model.c_str(), (double)tokens_per_sec, + (double)kv_pct, (double)vram_pct, spin_str); + if (n > term_cols) buf[term_cols] = '\0'; + ncplane_set_channels(header, hdr_ch(130, 220, 200)); + ncplane_putstr_yx(header, 0, 0, buf); +} + +void TuiState::redraw_chat() { + ncplane_erase(chatpl); + unsigned rows, cols; + ncplane_dim_yx(chatpl, &rows, &cols); + std::lock_guard lk(lines_mutex); + int total = static_cast(chat_lines.size()); + int visible = static_cast(rows); + int start = std::max(0, total - visible - scroll_offset); + int end = std::min(total, start + visible); + for (int i = start, row = 0; i < end; ++i, ++row) { + const std::string &line = chat_lines[i]; + uint64_t ch; + // Logo lines use prefix "[logo_N]" where N is the row index 0-6. + // We interpolate a cyan→magenta gradient across the 7 art rows. + if (line.rfind("[logo_", 0) == 0 && line.size() > 7 && line[7] == ']') { + int logo_row = line[6] - '0'; + // Gradient: cyan (0,230,255) → green (80,255,160) → magenta (220,80,255) + // 7 steps, indices 0-6. + static const uint32_t GRAD_R[] = { 0, 20, 60, 120, 180, 210, 220 }; + static const uint32_t GRAD_G[] = { 230, 255, 255, 255, 200, 130, 80 }; + static const uint32_t GRAD_B[] = { 255, 200, 140, 80, 100, 200, 255 }; + int gi = std::max(0, std::min(logo_row, 6)); + ch = chat_ch(GRAD_R[gi], GRAD_G[gi], GRAD_B[gi]); + } + else if (line.rfind("You: ", 0) == 0) ch = chat_ch(100, 200, 255); + else if (line.rfind("Nitro: ", 0) == 0) ch = chat_ch(180, 255, 180); + else if (line.rfind(ICON_SYS, 0) == 0) ch = chat_ch(140, 140, 200); + else if (line.rfind(ICON_TOOL, 0) == 0) ch = chat_ch(255, 180, 80); + else if (line.rfind(ICON_ERR, 0) == 0) ch = chat_ch(255, 80, 80); + else if (line.rfind(ICON_THINK, 0) == 0) ch = chat_ch(140, 140, 200); + else ch = chat_ch(210, 210, 210); + ncplane_set_channels(chatpl, ch); + // Strip the [logo_N] prefix before rendering. + std::string display = (line.rfind("[logo_", 0) == 0 && line.size() > 8) + ? line.substr(8) : line; + if (display.size() > cols) display = display.substr(0, cols); + ncplane_putstr_yx(chatpl, row, 0, display.c_str()); + } +} + +void TuiState::redraw_input() const { + ncplane_erase(inputpl); + + if (thinking) { + static constexpr const char *BLOCKS[] = { "-", "~", "≈", "~", "-" }; + static constexpr int N_BLOCKS = 5; + static constexpr double FREQ = 0.25; // gentler wave + static constexpr double SPEED = 0.15; // slower scroll + static constexpr int DELAY = 12; // frames before animation starts + + if (spinner_frame < DELAY) { + // still just a plain separator during the pause + ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); + std::string sep(term_cols, '-'); + ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); + } else { + int frame = spinner_frame - DELAY; // animation frame relative to start + for (int col = 0; col < term_cols; ++col) { + double phase = (col * FREQ) - (frame * SPEED); + int idx = static_cast(((std::sin(phase) + 1.0) * 0.5 * (N_BLOCKS - 1))); + idx = std::max(0, std::min(idx, N_BLOCKS - 1)); + // subtle brightness shift — blue-grey, not full glow + int brightness = 80 + idx * 20; + ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(brightness, brightness + 20, brightness + 40, + BG_INP_R, BG_INP_G, BG_INP_B)); + ncplane_putstr_yx(inputpl, 0, col, BLOCKS[idx]); + } + } + ncplane_set_channels(inputpl, inp_ch(140, 140, 180)); + ncplane_putstr_yx(inputpl, 1, 2, "thinking…"); + } else { + ncplane_set_channels(inputpl, inp_ch(80, 120, 160)); + std::string sep(term_cols, '-'); + ncplane_putstr_yx(inputpl, 0, 0, sep.c_str()); + const std::string prompt = " ❯ "; + const int prompt_cols = 4; + ncplane_set_channels(inputpl, inp_ch(100, 210, 255)); + ncplane_putstr_yx(inputpl, 1, 0, prompt.c_str()); + int max_w = std::max(0, term_cols - prompt_cols - 1); + std::string visible = input_buf; + int view_offset = 0; + if (visible.size() > max_w && max_w > 0) { + view_offset = static_cast(visible.size() - max_w); + visible = visible.substr(view_offset); + } + int cur_in_view = std::max(0, static_cast(cursor_pos - view_offset)); + cur_in_view = std::min(cur_in_view, (int)visible.size()); + std::string before = visible.substr(0, cur_in_view); + std::string after = cur_in_view < (int)visible.size() + ? visible.substr(cur_in_view + 1) : ""; + char cursor_ch_val = cur_in_view < (int)visible.size() + ? visible[cur_in_view] : ' '; + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + ncplane_putstr_yx(inputpl, 1, prompt_cols, before.c_str()); + int cx = prompt_cols + cur_in_view; + ncplane_set_channels(inputpl, NCCHANNELS_INITIALIZER(BG_INP_R, BG_INP_G, BG_INP_B, 180, 230, 255)); + char cbuf[2] = { cursor_ch_val, '\0' }; + ncplane_putstr_yx(inputpl, 1, cx, cbuf); + ncplane_set_channels(inputpl, inp_ch(230, 230, 230)); + if (!after.empty()) { + ncplane_putstr_yx(inputpl, 1, cx + 1, after.c_str()); + } + } +} + +void TuiState::redraw_all() { + redraw_header(); + redraw_chat(); + redraw_input(); + notcurses_render(nc); +} + +void TuiState::tick_spinner() { + ++spinner_frame; + redraw_header(); + redraw_input(); + notcurses_render(nc); +} + +void TuiState::set_thinking(bool on) { + thinking = on; + if (!on) spinner_frame = 0; + redraw_header(); + redraw_input(); + notcurses_render(nc); +} + +// +// TuiState content helpers +// +void TuiState::append_line(const std::string &line) { + std::lock_guard lk(lines_mutex); + int w = std::max(1, term_cols - 1); + if ((int)line.size() <= w) { + chat_lines.push_back(line); + } else { + for (int off = 0; off < (int)line.size(); off += w) { + chat_lines.push_back(line.substr(off, w)); + } + } +} + +void TuiState::append_token(const std::string &token) { + token_acc += token; + for (;;) { + auto pos = token_acc.find('\n'); + if (pos == std::string::npos) { + break; + } + append_line(token_acc.substr(0, pos)); + token_acc = token_acc.substr(pos + 1); + } + redraw_chat(); + notcurses_render(nc); +} + +void TuiState::flush_token_acc() { + if (!token_acc.empty()) { + append_line(token_acc); + token_acc.clear(); + redraw_chat(); + notcurses_render(nc); + } +} + +// +// Creates a centred floating plane with a border and a status message. +// The popup sits above all other planes and blocks until explicitly dismissed. +// +void TuiState::show_modal_popup(const std::string &message) { + // Dismiss any previous popup first. + dismiss_modal_popup(); + + // Clamp popup size to terminal. + int popup_w = std::min((int)message.size() + 8, term_cols - 4); + popup_w = std::max(popup_w, 20); + int popup_h = 5; + int py = std::max(0, (term_rows - popup_h) / 2); + int px = std::max(0, (term_cols - popup_w) / 2); + + ncplane_options opts{}; + opts.y = py; opts.x = px; + opts.rows = (unsigned)popup_h; + opts.cols = (unsigned)popup_w; + modal_plane = ncplane_create(stdpl, &opts); + if (!modal_plane) return; + + // Background: deep navy. + static constexpr uint32_t PBG_R = 20, PBG_G = 28, PBG_B = 50; + ncplane_set_base(modal_plane, " ", 0, + NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + ncplane_erase(modal_plane); + + // Border — bright cyan. + uint64_t border_ch = NCCHANNELS_INITIALIZER(80, 220, 255, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, border_ch); + + // Draw corners and edges manually so we don't require nccell border helpers. + // Top row + ncplane_putstr_yx(modal_plane, 0, 0, "╔"); + for (int c = 1; c < popup_w - 1; ++c) + ncplane_putstr_yx(modal_plane, 0, c, "═"); + ncplane_putstr_yx(modal_plane, 0, popup_w - 1, "╗"); + // Middle rows + for (int r = 1; r < popup_h - 1; ++r) { + ncplane_putstr_yx(modal_plane, r, 0, "║"); + ncplane_putstr_yx(modal_plane, r, popup_w - 1, "║"); + } + // Bottom row + ncplane_putstr_yx(modal_plane, popup_h - 1, 0, "╚"); + for (int c = 1; c < popup_w - 1; ++c) + ncplane_putstr_yx(modal_plane, popup_h - 1, c, "═"); + ncplane_putstr_yx(modal_plane, popup_h - 1, popup_w - 1, "╝"); + + // Title bar. + uint64_t title_ch = NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, title_ch); + ncplane_putstr_yx(modal_plane, 1, 2, "⏳ Loading…"); + + // Message. + uint64_t msg_ch = NCCHANNELS_INITIALIZER(200, 200, 200, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(modal_plane, msg_ch); + // Truncate message to fit inside border. + int max_msg = popup_w - 4; + std::string display = message.size() > (size_t)max_msg + ? message.substr(0, max_msg) + : message; + ncplane_putstr_yx(modal_plane, 2, 2, display.c_str()); + + notcurses_render(nc); +} + +void TuiState::show_help() { + append_line(ICON_SYS + "Commands:"); + append_line(ICON_SYS + " /model [path] load a GGUF model (picker if no path)"); + append_line(ICON_SYS + " /embed [path] load an embedding model (picker if no path)"); + append_line(ICON_SYS + " /rag [path] index file or directory (picker if no path)"); + append_line(ICON_SYS + " /memory KV / VRAM / layer stats"); + append_line(ICON_SYS + " /clear reset conversation"); + append_line(ICON_SYS + " /settings show current settings"); + append_line(ICON_SYS + " /set change a setting live"); + append_line(ICON_SYS + " /help this message"); + append_line(ICON_SYS + " exit / quit exit Nitro"); + append_line(ICON_SYS + "Settable keys (via /set):"); + append_line(ICON_SYS + " temperature top_p top_k min_p penalty_repeat"); + append_line(ICON_SYS + " penalty_last_n rag_top_k n_gpu_layers"); + append_line(ICON_SYS + " run_allowed (comma-separated list, e.g. python3,make)"); + redraw_all(); +} + +void TuiState::dismiss_modal_popup() { + if (modal_plane) { + ncplane_destroy(modal_plane); + modal_plane = nullptr; + notcurses_render(nc); + } +} + +// +// ─── TuiState::file_picker ──────────────────────────────────────────────── +// Interactive directory/file browser popup. +// Keyboard: ↑/↓ navigate, Enter select/descend, Backspace go up, +// 's' select current dir for indexing, Esc cancel. +// Returns the chosen path or "" on cancel. +// ─── TuiState::file_picker ──────────────────────────────────────────────── +// Unified interactive directory/file browser used by /rag, /model, /embed. +// title_hint appears in the popup header (e.g. "RAG Folder", "Model File"). +// +// Keyboard: +// ↑/↓ navigate list +// Enter descend into directory, or select a file +// Backspace go up one directory +// s select the current directory itself (useful for /rag) +// Esc cancel → returns "" +// +// Returns the chosen path, or "" on cancel. +// +std::string TuiState::file_picker(const std::string &start_dir, + const std::string &title_hint) const { + std::string current_dir = start_dir; + { + std::error_code ec; + auto canon = fs::canonical(start_dir, ec); + if (!ec) current_dir = canon.string(); + } + auto load_entries = [](const std::string &dir, + std::vector &entries) + { + entries.clear(); + std::error_code ec; + if (fs::path(dir).has_parent_path() && + fs::path(dir) != fs::path(dir).root_path()) + entries.emplace_back(".."); + std::vector dirs, files; + for (const auto &e : fs::directory_iterator(dir, ec)) { + if (ec) break; + std::string name = e.path().filename().string(); + if (name.empty() || name[0] == '.') continue; + if (e.is_directory()) dirs.push_back(name); + else files.push_back(name); + } + ranges::sort(dirs); + ranges::sort(files); + for (auto &d : dirs) entries.push_back(d + "/"); + for (auto &f : files) entries.push_back(f); + }; + + std::vector entries; + int selected = 0; + int scroll = 0; + + // Popup dimensions. + static constexpr int PW = 60; + static constexpr int PH = 20; + int py = std::max(0, (term_rows - PH) / 2); + int px = std::max(0, (term_cols - PW) / 2); + + ncplane_options opts{}; + opts.y = py; opts.x = px; + opts.rows = (unsigned)PH; opts.cols = (unsigned)PW; + struct ncplane *picker = ncplane_create(stdpl, &opts); + if (!picker) return ""; + + static constexpr uint32_t PBG_R = 18, PBG_G = 24, PBG_B = 40; + ncplane_set_base(picker, " ", 0, NCCHANNELS_INITIALIZER(PBG_R, PBG_G, PBG_B, PBG_R, PBG_G, PBG_B)); + // Build a compact hint line appropriate to the operation. + // /rag adds 's=select dir'; /model and /embed only need file selection. + std::string hint_line = "↑↓ navigate Enter open/select Esc cancel"; + if (title_hint.find("RAG") != std::string::npos || + title_hint.find("Folder") != std::string::npos) { + hint_line = "↑↓ navigate Enter open s=select dir Esc cancel"; + } + auto draw_picker = [&]() { + ncplane_erase(picker); + uint64_t border_ch = NCCHANNELS_INITIALIZER(100, 180, 255, PBG_R, PBG_G, PBG_B); + ncplane_set_channels(picker, border_ch); + ncplane_putstr_yx(picker, 0, 0, "╔"); + for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, 0, c, "═"); + ncplane_putstr_yx(picker, 0, PW - 1, "╗"); + for (int r = 1; r < PH - 1; ++r) { + ncplane_putstr_yx(picker, r, 0, "║"); + ncplane_putstr_yx(picker, r, PW - 1, "║"); + } + ncplane_putstr_yx(picker, PH - 1, 0, "╚"); + for (int c = 1; c < PW - 1; ++c) ncplane_putstr_yx(picker, PH - 1, c, "═"); + ncplane_putstr_yx(picker, PH - 1, PW - 1, "╝"); + + // Title + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(255, 220, 80, PBG_R, PBG_G, PBG_B)); + std::string title_str = " 📂 " + title_hint + " Picker "; + if ((int)title_str.size() > PW - 4) title_str = title_str.substr(0, PW - 4); + ncplane_putstr_yx(picker, 0, 2, title_str.c_str()); + // Current path (truncated). + std::string path_display = current_dir; + if ((int)path_display.size() > PW - 4) + path_display = "…" + path_display.substr(path_display.size() - (PW - 5)); + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(160, 200, 240, PBG_R, PBG_G, PBG_B)); + ncplane_putstr_yx(picker, 1, 2, path_display.c_str()); + // Hint line (bottom interior row). + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(120, 120, 160, PBG_R, PBG_G, PBG_B)); + std::string hint_trunc = hint_line; + if ((int)hint_trunc.size() > PW - 4) hint_trunc = hint_trunc.substr(0, PW - 4); + ncplane_putstr_yx(picker, PH - 2, 2, hint_trunc.c_str()); + // Entry list. + int list_rows = PH - 5; + if (selected < scroll) scroll = selected; + if (selected >= scroll + list_rows) scroll = selected - list_rows + 1; + for (int i = 0; i < list_rows; ++i) { + int idx = scroll + i; + if (idx >= (int)entries.size()) break; + bool is_selected = (idx == selected); + bool is_dir = !entries[idx].empty() && entries[idx].back() == '/'; + uint32_t fr, fg, fb; + if (is_selected) { fr = 20; fg = 20; fb = 20; } + else if (is_dir) { fr = 120; fg = 200; fb = 255; } + else { fr = 200; fg = 200; fb = 200; } + uint32_t br = is_selected ? 100 : PBG_R; + uint32_t bg = is_selected ? 180 : PBG_G; + uint32_t bb = is_selected ? 255 : PBG_B; + ncplane_set_channels(picker, NCCHANNELS_INITIALIZER(fr, fg, fb, br, bg, bb)); + std::string label = (is_selected ? " ▶ " : " ") + entries[idx]; + if ((int)label.size() > PW - 2) label = label.substr(0, PW - 2); + while ((int)label.size() < PW - 2) label += ' '; + ncplane_putstr_yx(picker, 2 + i, 1, label.c_str()); + } + notcurses_render(nc); + }; + + std::string result; + load_entries(current_dir, entries); + draw_picker(); + + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + if (ni.id == NCKEY_ESC) { + break; // cancelled + } + if (ni.id == NCKEY_UP) { + if (selected > 0) --selected; + draw_picker(); + continue; + } + if (ni.id == NCKEY_DOWN) { + if (selected + 1 < (int)entries.size()) ++selected; + draw_picker(); + continue; + } + // 's' — select the current directory (useful for /rag, ignored for file pickers). + if (ni.id == 's' || ni.id == 'S') { + // Select current directory for RAG indexing. + result = current_dir; + break; + } + if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { + // Go up one level. + fs::path p(current_dir); + if (p.has_parent_path() && p != p.root_path()) { + current_dir = p.parent_path().string(); + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } + continue; + } + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { + if (entries.empty()) continue; + const std::string &entry = entries[selected]; + if (entry == "..") { + fs::path p(current_dir); + if (p.has_parent_path() && p != p.root_path()) { + current_dir = p.parent_path().string(); + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } + } else if (!entry.empty() && entry.back() == '/') { + // Descend into directory. + current_dir = current_dir + "/" + entry.substr(0, entry.size() - 1); + { + std::error_code ec; + auto canon = fs::canonical(current_dir, ec); + if (!ec) current_dir = canon.string(); + } + load_entries(current_dir, entries); + selected = 0; scroll = 0; + draw_picker(); + } else { + // Select a specific file. + // Select the highlighted file. + result = current_dir + "/" + entry; + break; + } + continue; + } + } + ncplane_destroy(picker); + notcurses_render(nc); + return result; +} + +// +// ─── TuiState::confirm_dialog ───────────────────────────────────────────── +// +bool TuiState::confirm_dialog(const std::string &prompt) const { + ncplane_erase(inputpl); + ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); + std::string msg = " " + prompt + " [y/n] ❯ "; + ncplane_putstr_yx(inputpl, 1, 0, msg.c_str()); + notcurses_render(nc); + std::string answer; + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') break; + if (ni.id == NCKEY_BACKSPACE && !answer.empty()) { answer.pop_back(); } + else if (ni.id >= 32 && ni.id < 127) { answer += (char)ni.id; } + ncplane_erase(inputpl); + ncplane_set_channels(inputpl, inp_ch(255, 200, 80)); + ncplane_putstr_yx(inputpl, 1, 0, (msg + answer).c_str()); + notcurses_render(nc); + } + std::string lo = answer; + ranges::transform(lo, lo.begin(), ::tolower); + redraw_input(); + notcurses_render(nc); + return (lo == "y" || lo == "yes" || lo == "sure" || lo == "k"); +} + +// +// Integrates InputHistory: Up/Down arrows navigate the history stack. +// On submit the entry is pushed to history, and nav is reset. +// +std::string TuiState::readline_blocking() { + input_buf.clear(); + cursor_pos = 0; + history.reset_nav(); + redraw_input(); + notcurses_render(nc); + + // Temporary saved draft so Down from history restores the user's current text. + std::string draft; + + for (;;) { + ncinput ni{}; + notcurses_get_blocking(nc, &ni); + + if (ni.id == NCKEY_ENTER || ni.id == '\r' || ni.id == '\n') { + std::string result = input_buf; + if (!result.empty()) { + history.push(result); + } + input_buf.clear(); + cursor_pos = 0; + redraw_input(); + notcurses_render(nc); + return result; + } + + if (ni.id == NCKEY_UP) { + // Entering history from a fresh prompt: save current text as draft. + std::string hist_entry; + if (history.up(hist_entry)) { + if (!input_buf.empty() && hist_entry != input_buf) { + // Only save draft when we first leave the bottom of history. + // (history.reset_nav was called on entry so the first Up call + // always comes from the "new input" position.) + draft = input_buf; + } + input_buf = hist_entry; + cursor_pos = input_buf.size(); + } + redraw_input(); + notcurses_render(nc); + continue; + } + + if (ni.id == NCKEY_DOWN) { + std::string hist_entry; + if (history.down(hist_entry)) { + input_buf = hist_entry; + cursor_pos = input_buf.size(); + } else { + // Past the newest entry → restore draft. + input_buf = draft; + cursor_pos = input_buf.size(); + draft.clear(); + } + redraw_input(); + notcurses_render(nc); + continue; + } + + // Scroll the chat pane — not the input history. + if (ni.id == NCKEY_PGUP) { + scroll_offset += std::max(1, term_rows - 4); + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_PGDOWN) { + scroll_offset = std::max(0, scroll_offset - std::max(1, term_rows - 4)); + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_SCROLL_UP && scroll_offset < term_rows + 10) { + scroll_offset += 1; + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_SCROLL_DOWN && scroll_offset > 0) { + scroll_offset -= 1; + redraw_chat(); + notcurses_render(nc); + continue; + } + if (ni.id == NCKEY_F01) { + show_help(); + continue; + } + if (ni.id == NCKEY_F02) { + mouse_mode = !mouse_mode; + if (mouse_mode) { + notcurses_mice_enable(nc, NCMICE_BUTTON_EVENT); + } else { + notcurses_mice_disable(nc); + } + continue; + } + if (ni.id == NCKEY_BACKSPACE || ni.id == 127) { + if (cursor_pos > 0) { input_buf.erase(cursor_pos - 1, 1); --cursor_pos; } + } else if (ni.id == NCKEY_LEFT) { + if (cursor_pos > 0) --cursor_pos; + } else if (ni.id == NCKEY_RIGHT) { + if (cursor_pos < input_buf.size()) ++cursor_pos; + } else if (ni.id == NCKEY_HOME) { + cursor_pos = 0; + } else if (ni.id == NCKEY_END) { + cursor_pos = input_buf.size(); + } else if (ni.id == NCKEY_DEL) { + if (cursor_pos < input_buf.size()) input_buf.erase(cursor_pos, 1); + } else if (ni.id >= 32 && ni.id < 0xD800) { + // Any printable character — entering new text clears the nav draft + // so that Down won't resurrect a stale saved buffer. + draft.clear(); + history.reset_nav(); + input_buf.insert(cursor_pos, 1, (char)ni.id); + ++cursor_pos; + } + + redraw_input(); + notcurses_render(nc); + } +} + +void AgentState::apply_generation_params(const NitroConfig &cfg) const { + llama->add_stop("<|turn|>"); + llama->add_stop("<|im_end|>"); + llama->set_max_tokens(512000); + llama->set_temperature(cfg.temperature); + llama->set_top_k(cfg.top_k); + llama->set_top_p(cfg.top_p); + llama->set_min_p(cfg.min_p); + llama->set_penalty_repeat(cfg.penalty_repeat); + llama->set_penalty_last_n(cfg.penalty_last_n); + llama->set_log_level(cfg.log_level); +} + +// +// Shows a modal loading popup while the model loads. +// +bool AgentState::setup_model(const NitroConfig &cfg, TuiState &tui) { + if (cfg.model_path.empty()) { + tui.append_line(ICON_SYS + "No model loaded. Use /model to load a GGUF."); + tui.redraw_all(); + return false; + } + // Show a modal popup so the user knows loading is in progress. + std::string model_name = fs::path(cfg.model_path).filename().string(); + tui.show_modal_popup("Loading " + model_name); + // Destroy the iterator first — it holds references into the llama context. + // Freeing llama while iter is still alive causes use-after-free / load failure. + iter.reset(); + model_loaded = false; + llama = std::make_unique(); + + apply_generation_params(cfg); + if (!llama->load_model(cfg.model_path, cfg.n_ctx, cfg.n_batch, + cfg.n_gpu_layers, cfg.log_level)) { + tui.dismiss_modal_popup(); + tui.append_line(ICON_ERR + llama->last_error()); + tui.redraw_all(); + return false; + } + tui.dismiss_modal_popup(); + model_loaded = true; + tui.current_model = model_name; + tui.append_line(ICON_SYS + "Model ready: " + tui.current_model); + LlamaMemoryInfo mem = llama->memory_info(); + tui.append_line(ICON_SYS + "" + mem.advice); + tui.kv_used = mem.kv_used; + tui.kv_total = mem.kv_total; + tui.vram_used = mem.vram_used; + tui.vram_total = mem.vram_total; + tui.append_line(ICON_SYS + "Thinking mode: " + (cfg.thinking ? "enabled" : "disabled")); + tui.redraw_all(); + return true; +} + +bool AgentState::setup_embed(const std::string &path, TuiState &tui) { + tui.show_modal_popup("Loading embedding model: " + fs::path(path).filename().string()); + tui.redraw_all(); + embed_llama = std::make_unique(); + if (!embed_llama->load_embedding_model(path)) { + tui.dismiss_modal_popup(); + tui.append_line(ICON_ERR + embed_llama->last_error()); + tui.redraw_all(); + embed_llama.reset(); + return false; + } + tui.dismiss_modal_popup(); + rag_db = std::make_unique(); + rag_session = std::make_unique(); + tui.append_line(ICON_SYS + "Embedding model ready."); + tui.redraw_all(); + return true; +} + +void AgentState::reset_conversation(const std::string &sysprompt, TuiState &tui) { + system_prompt = sysprompt; + llama->reset(); + apply_generation_params(NitroConfig{}); + iter = std::make_unique(); + if (!llama->add_message(*iter, "system", system_prompt)) { + tui.append_line(ICON_ERR + "System prompt injection: " + llama->last_error()); + tui.redraw_all(); + } +} + +float AgentState::tokens_per_sec() const { + if (!iter) return 0.0f; + auto now = std::chrono::high_resolution_clock::now(); + double elapsed = std::chrono::duration(now - iter->_t_start).count(); + if (elapsed <= 0.0 || iter->_tokens_generated <= 0) return 0.0f; + return (float)(iter->_tokens_generated / elapsed); +} + +std::string AgentState::memory_info_text() const { + if (!model_loaded) return "No model loaded."; + LlamaMemoryInfo m = llama->memory_info(); + std::ostringstream oss; + oss << "KV cache : " << m.kv_used << " / " << m.kv_total + << " (" << m.kv_percent << "%)\n"; + if (m.vram_total > 0) { + oss << "VRAM : " << (m.vram_used >> 20) << " MB / " + << (m.vram_total >> 20) << " MB (" << m.vram_percent << "%)\n"; + } + oss << "GPU layers: " << m.n_layers_gpu << " / " << m.n_layers_total << "\n"; + oss << "CPU layers: " << m.n_layers_cpu << "\n"; + oss << "Advice : " << m.advice << "\n"; + return oss.str(); +} + +std::string AgentState::rag_tool(const NitroConfig &cfg, const std::string &agent_query) const { + std::string result; + if (embed_llama && rag_db && rag_session) { + result = embed_llama->rag_retrieve(*rag_db, agent_query, cfg.rag_top_k, *rag_session); + if (result.empty()) { + result = "RAG: no context found"; + } + } else { + result = "RAG: not enabled"; + } + return result; +} + +bool AgentState::rag_load_index(const std::string &path, TuiState &tui) const { + if (!embed_llama || !rag_db) { + tui.append_line(ICON_ERR + "Load an embedding model first: /embed "); + tui.redraw_all(); + return false; + } + + if (!rag_db->load(path)) { + tui.append_line(ICON_SYS + "failed to load"); + tui.redraw_all(); + } + + return true; +} + +bool AgentState::rag_index(const std::string &path, const NitroConfig &cfg, TuiState &tui) const { + if (!embed_llama || !rag_db) { + tui.append_line(ICON_ERR + "Load an embedding model first: /embed "); + tui.redraw_all(); + return false; + } + + auto index_one = [&](const std::string &filepath) { + tui.append_line(ICON_SYS + " indexing: " + filepath); + tui.redraw_all(); + if (!embed_llama->rag_index(*rag_db, filepath)) { + tui.append_line(ICON_ERR + "rag_load: " + embed_llama->last_error()); + tui.redraw_all(); + } + }; + + // must be set before indexing + rag_db->embed_dim = embed_llama->get_embed_dim(); + + fs::path rp(path); + std::error_code ec; + if (fs::is_directory(rp, ec)) { + for (const auto &entry : fs::recursive_directory_iterator(rp, ec)) { + if (entry.is_regular_file()) { + index_one(entry.path().string()); + } + } + } else { + index_one(path); + } + + std::string save_path = join_path(cfg.sandbox, "rag-index.bin"); + tui.append_line(ICON_SYS + "saving index: " + save_path); + tui.redraw_all(); + rag_db->save(save_path); + + return true; +} + +// +// Tool dispatch +// +std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &cfg, TuiState &tui) const { + const std::string &sandbox = cfg.sandbox; + const std::vector &run_allowed = cfg.run_allowed; + + std::string op, arg1, arg2; + auto sp1 = cmd.find_first_of(" \n"); + if (sp1 == std::string::npos) { + op = trim(cmd); + } else { + op = trim(cmd.substr(0, sp1)); + std::string rest = cmd.substr(sp1 + 1); + rest.erase(0, rest.find_first_not_of(" \t")); + auto sp2 = rest.find(' '); + if (sp2 == std::string::npos) { + arg1 = rest; + } else { + arg1 = rest.substr(0, sp2); + arg2 = rest.substr(sp2 + 1); + } + } + + auto resolve = [&](const std::string &p) -> std::string { + if (p.empty() || p == ".") { + return sandbox; + } + if (p.substr(0, 2) == "./") { + return join_path(sandbox, p.substr(2)); + } + if (p[0] == '/') { + return p; + } + return join_path(sandbox, unwrap(p)); + }; + + auto show_tool = [&](const std::string &tool) -> void { + tui.append_line(ICON_TOOL + "→ " + tool); + tui.redraw_all(); + }; + + if (op == "TOOL:DATE") { + show_tool(op); + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t)); + return buf; + } + if (op == "TOOL:TIME") { + show_tool(op); + char buf[32]; time_t t = time(nullptr); + strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t)); + return buf; + } + if (op == "TOOL:RND") { + show_tool(op); + return std::to_string((double)rand() / RAND_MAX); + } + if (op == "TOOL:RAG") { + show_tool(op); + return rag_tool(cfg, arg1); + } + if (op == "TOOL:LIST") { + std::string dir = resolve(arg1); + show_tool("listing: " + dir); + if (!path_in_sandbox(sandbox, dir)) return "ERROR: path outside sandbox"; + return list_dir(dir); + } + if (op == "TOOL:EXISTS") { + std::string p = resolve(arg1); + show_tool("checking: " + p); + if (!path_in_sandbox(sandbox, p)) return "NO"; + return fs::exists(p) ? "YES" : "NO"; + } + if (op == "TOOL:READ") { + show_tool("reading: " + arg1); + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) return "ERROR: path outside sandbox"; + return read_file(p); + } + if (op == "TOOL:WRITE") { + show_tool("writing: " + arg1); + std::string p = resolve(arg1); + if (!path_in_sandbox(sandbox, p)) { + return "ERROR: path outside sandbox"; + } + if (cfg.permission_prompt && !tui.confirm_dialog(std::format("Allow model to write {}?", p))) { + return "ERROR: action prevented by user"; + } + std::string content = strip_code_fences(arg1, arg2); + return write_file(p, content) ? "OK: written to " + arg1 : "ERROR: write failed for " + arg1; + } + if (op == "TOOL:MKDIR") { + std::string p = resolve(arg1); + show_tool("mkdir: " + arg1); + if (!path_in_sandbox(sandbox, p)) { + return "ERROR: path outside sandbox"; + } + return make_dir(p) ? "OK: created " + arg1 : "ERROR: mkdir failed for " + arg1; + } + if (op == "TOOL:CURL") { + show_tool("curl: " + arg1); + return tool_curl(arg1); + } + if (op == "TOOL:INTROSPECT") { + show_tool("introspecting: " + arg1); + return introspect(cfg); + } + if (op == "TOOL:ASK") { + show_tool("asking: " + arg1); + return tui.readline_blocking(); + } + if (op == "TOOL:RUN") { + if (!run_allowed.empty()) { + bool permitted = ranges::any_of(run_allowed, [&](const std::string &a) {return a == arg1;}); + if (!permitted) { + return "ERROR: '" + arg1 + "' is not in the TOOL:RUN allowlist. " + "Use /set run_allowed to permit it."; + } + } else if (cfg.permission_prompt && !tui.confirm_dialog(std::format("Allow {} {} to run?", arg1, arg2))) { + return "ERROR: prevented by user"; + } + std::string command = arg1 + " " + arg2 + " 2>&1"; + show_tool("running: " + command); + FILE *fp = popen(command.c_str(), "r"); + if (!fp) { + return "ERROR: popen failed"; + } + std::string out; + char buf[256]; + while (fgets(buf, sizeof(buf), fp)) { + out += buf; + } + pclose(fp); + if (out.size() > 4096) { + out = out.substr(0, 4096) + "\n…(truncated)"; + } + return out; + } + return "ERROR: unknown tool: [" + op + "]"; +} + +// +// Agent turn +// +bool AgentState::run_turn(const std::string &user_message, const NitroConfig &cfg, TuiState &tui) const { + if (!model_loaded) { + tui.append_line(ICON_ERR + "No model loaded. Use /model "); + tui.redraw_all(); + return false; + } + std::string effective_message = user_message; + if (embed_llama && rag_db && rag_session) { + std::string context = embed_llama->rag_retrieve(*rag_db, user_message, cfg.rag_top_k, *rag_session); + if (!context.empty()) { + log_write("RAG: %s", context.c_str()); + effective_message = "Context:\n" + context + "\n\nUser: " + user_message; + } else { + log_write("RAG: no context found [%s]", embed_llama->last_error()); + } + } + if (!iter) { + tui.append_line(ICON_ERR + "Conversation not initialised (call /clear to reset)"); + tui.redraw_all(); + return false; + } + if (!llama->add_message(*iter, "user", effective_message)) { + tui.append_line(ICON_ERR + "add_message: " + llama->last_error()); + tui.redraw_all(); + return false; + } + tui.append_line("Nitro: "); + + // in_think starts false — models that don't use blocks emit + // visible text immediately. The spinner activates only while thinking. + enum {t_init, t_think, t_thunk} think_mode = (cfg.thinking ? t_init : t_thunk); + + tui.set_thinking(true); + std::string buffer; + + auto invoke_tool = [&](const std::string &buffer, const std::string_view template_str) -> void { + static constexpr std::string_view END_TOOL = "\nNITRO_END_TOOL"; + static const std::string TOOL_RESULT = "NITRO_TOOL_RESULT: "; + + std::string tool; + const auto pos = buffer.rfind(END_TOOL); + if (pos != std::string::npos) { + tool = buffer.substr(0, pos); + auto endTool = buffer.substr(pos); + if (endTool.length() > END_TOOL.length()) { + log_write("ERROR: trailing delimiter: [%s]", endTool.c_str()); + } + } else { + tool = buffer; + } + + log_write("tool request: mode:[%d] [%s]", think_mode, tool.c_str()); + std::string result = process_tool(tool, cfg, tui); + std::string content = TOOL_RESULT + std::vformat(template_str, std::make_format_args(result)); + log_write("tool: [%s] result: [%s]", tool.c_str(), result.c_str()); + + if (!llama->add_message(*iter, "tool_result", content)) { + tui.append_line(ICON_ERR + "tool result inject: " + llama->last_error()); + } + if (!iter->_has_next) { + tui.append_line(ICON_ERR + "failed to evoke tool response: " + llama->last_error()); + } + tui.redraw_all(); + }; + + auto start_think = [&](const std::string &tag) -> void { + if (think_mode == t_init) { + auto pos = buffer.find(tag); + if (pos != std::string::npos) { + think_mode = t_think; + // display prededing text + buffer = buffer.substr(0, pos); + } + } + }; + + auto end_think = [&](const std::string &tag) -> void { + if (think_mode == t_think) { + auto pos = buffer.find(tag); + if (pos != std::string::npos) { + think_mode = t_thunk; + // display remaining text + buffer = buffer.substr(pos + tag.length()); + } + } + }; + + auto is_escape = [&]() -> bool { + ncinput ni{}; + notcurses_get_nblock(tui.nc, &ni); + if (ni.id == NCKEY_ESC) { + tui.set_thinking(false); + tui.append_line(ICON_ERR + "Generation cancelled by user (Escape)"); + tui.redraw_all(); + } + return ni.id == NCKEY_ESC; + }; + + auto fetch_all = [&]() -> void { + while (iter->_has_next && !is_escape()) { + std::string tok = llama->next(*iter); + buffer += tok; + tui.tick_spinner(); + } + }; + + while (iter->_has_next && !is_escape()) { + std::string tok = llama->next(*iter); + if (tok == "<") { + // fetch the complete tag + std::string tag = tok; + while (iter->_has_next && tag.find(">") == std::string::npos) { + tag += llama->next(*iter); + } + if (tag == "<|think|>") { + think_mode = t_think; + continue; + } else { + buffer += tag; + } + } else { + buffer += tok; + } + if (think_mode == t_init) { + start_think(""); + start_think("<|think|>"); + start_think(""); + start_think("<|channel>thought"); + } + if (think_mode == t_think) { + tui.tick_spinner(); + end_think(""); + end_think(""); + end_think(""); + end_think(""); + end_think(""); + } + if (think_mode == t_thunk) { + auto tool_start = buffer.find("TOOL:"); + if (tool_start == 0) { + fetch_all(); + invoke_tool(trim(buffer), "TOOL_RESULT: {}"); + buffer.clear(); + think_mode = t_init; + continue; + } + tool_start = buffer.find("<|tool_call>call:"); + if (tool_start != std::string::npos) { + // see https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 + fetch_all(); + auto pos = buffer.find_last_not_of("}"); + if (pos != std::string::npos) { + buffer = buffer.substr(0, pos); + } + pos = buffer.find_first_not_of('{'); + if (pos != std::string::npos) { + buffer = buffer.substr(0, pos) + buffer.substr(pos + 1); + } + invoke_tool(trim(buffer), "<|tool_response>{}"); + buffer.clear(); + think_mode = t_init; + continue; + } + auto pos = buffer.find('\n'); + if (pos != std::string::npos) { + tui.append_token(buffer.substr(0, pos + 1)); + buffer = buffer.substr(pos + 1); + } + } else { + auto pos = buffer.find('\n'); + if (pos != std::string::npos) { + auto thought = buffer.substr(0, pos + 1); + if (thought.length() > 1) { + tui.append_token(ICON_THINK + thought); + } + buffer = buffer.substr(pos + 1); + } + } + } + + if (!buffer.empty()) { + tui.append_token(buffer + "\n"); + } + + tui.flush_token_acc(); + tui.set_thinking(false); + tui.tokens_per_sec = tokens_per_sec(); + LlamaMemoryInfo mem = llama->memory_info(); + tui.kv_used = mem.kv_used; + tui.kv_total = mem.kv_total; + tui.vram_used = mem.vram_used; + tui.vram_total = mem.vram_total; + char stat[128]; + auto patterm = ICON_SYS + "%.1f tok/s (%d tokens) KV %.1f%%"; + std::snprintf(stat, sizeof(stat), patterm.c_str(), + (double)tui.tokens_per_sec, + iter->_tokens_generated, + (double)mem.kv_percent); + tui.append_line(stat); + tui.redraw_all(); + return true; +} + +// +// Slash command handler +// +static void handle_slash(const std::string &input, + NitroConfig &cfg, + AgentState &agent, + TuiState &tui) { + auto sp = input.find(' '); + std::string verb = (sp == std::string::npos) ? input : input.substr(0, sp); + std::string rest; + if (sp != std::string::npos) { + rest = input.substr(sp + 1); + rest.erase(0, rest.find_first_not_of(" \t")); + } + + if (verb == "/help") { + tui.show_help(); + return; + } + // ── /model ────────────────────────────────────────────────────────────── + // If no path is given, open the file picker so the user can browse to a + // GGUF. The picker starts in the current sandbox directory. + if (verb == "/model") { + if (rest.empty()) { + tui.append_line(ICON_SYS + "Opening model picker…"); + tui.redraw_all(); + rest = tui.file_picker(cfg.sandbox, "Model File"); + if (rest.empty()) { + tui.append_line(ICON_SYS + "/model cancelled."); + tui.redraw_all(); + return; + } + } + cfg.model_path = rest; + if (agent.setup_model(cfg, tui)) { + std::string sysp = build_system_prompt(cfg); + agent.reset_conversation(sysp, tui); + save_settings(cfg); + } + tui.redraw_all(); + return; + } + + // ── /embed ────────────────────────────────────────────────────────────── + // If no path is given, open the file picker so the user can browse to an + // embedding GGUF. + if (verb == "/embed") { + if (rest.empty()) { + tui.append_line(ICON_SYS + "Opening embedding model picker…"); + tui.redraw_all(); + rest = tui.file_picker(cfg.sandbox, "Embedding Model"); + if (rest.empty()) { + tui.append_line(ICON_SYS + "/embed cancelled."); + tui.redraw_all(); + return; + } + } + cfg.embed_path = rest; + if (agent.setup_embed(rest, tui)) { + save_settings(cfg); + } + return; + } + + // ── /rag ──────────────────────────────────────────────────────────────── + if (verb == "/rag") { + std::string path = rest; + if (path.empty()) { + // Launch the interactive folder picker starting from the sandbox. + path = tui.rag_folder_picker(cfg.sandbox); + if (path.empty()) { + tui.append_line(ICON_SYS + "RAG indexing cancelled."); + tui.redraw_all(); + return; + } + } + if (path.find_last_not_of(".bin") != std::string::npos) { + tui.append_line(ICON_SYS + "Loading index: " + path); + tui.redraw_all(); + agent.rag_load_index(path, tui); + } else { + tui.append_line(ICON_SYS + "Indexing: " + path); + tui.redraw_all(); + agent.rag_index(path, cfg, tui); + } + tui.append_line(ICON_SYS + "done"); + tui.redraw_all(); + return; + } + + if (verb == "/memory") { + std::istringstream iss(agent.memory_info_text()); + std::string line; + while (std::getline(iss, line)) tui.append_line(ICON_SYS + "" + line); + tui.redraw_all(); + return; + } + + if (verb == "/clear") { + { std::lock_guard lk(tui.lines_mutex); + tui.chat_lines.clear(); } + std::string sysp = build_system_prompt(cfg); + agent.reset_conversation(sysp, tui); + tui.append_line(ICON_SYS + "Conversation cleared."); + tui.redraw_all(); + return; + } + + if (verb == "/settings") { + tui.append_line(ICON_SYS + "Current settings:"); + tui.append_line(ICON_SYS + " model_path : " + cfg.model_path); + tui.append_line(ICON_SYS + " embed_path : " + cfg.embed_path); + tui.append_line(ICON_SYS + " sandbox : " + cfg.sandbox); + tui.append_line(ICON_SYS + " n_ctx : " + std::to_string(cfg.n_ctx)); + tui.append_line(ICON_SYS + " n_gpu_layers : " + std::to_string(cfg.n_gpu_layers)); + tui.append_line(ICON_SYS + " temperature : " + std::to_string(cfg.temperature)); + tui.append_line(ICON_SYS + " top_p : " + std::to_string(cfg.top_p)); + tui.append_line(ICON_SYS + " top_k : " + std::to_string(cfg.top_k)); + tui.append_line(ICON_SYS + " penalty_repeat: " + std::to_string(cfg.penalty_repeat)); + tui.append_line(ICON_SYS + " rag_top_k : " + std::to_string(cfg.rag_top_k)); + tui.append_line(ICON_SYS + " saved to : " + settings_path()); + tui.redraw_all(); + return; + } + + if (verb == "/set") { + // Usage: /set + // Parses the key and value, updates cfg in place, re-applies generation + // params if needed, and saves settings to disk. + auto sp2 = rest.find(' '); + std::string key = (sp2 == std::string::npos) ? rest : rest.substr(0, sp2); + std::string val = (sp2 == std::string::npos) ? "" : rest.substr(sp2 + 1); + val.erase(0, val.find_first_not_of(" \t")); + + if (key.empty() || val.empty()) { + tui.append_line(ICON_ERR + "Usage: /set "); + tui.redraw_all(); return; + } + + bool ok = true; + bool needs_reparam = false; + try { + if (key == "temperature") { cfg.temperature = std::stof(val); needs_reparam = true; } + else if (key == "top_p") { cfg.top_p = std::stof(val); needs_reparam = true; } + else if (key == "min_p") { cfg.min_p = std::stof(val); needs_reparam = true; } + else if (key == "top_k") { cfg.top_k = std::stoi(val); needs_reparam = true; } + else if (key == "penalty_repeat") { cfg.penalty_repeat = std::stof(val); needs_reparam = true; } + else if (key == "penalty_last_n") { cfg.penalty_last_n = std::stoi(val); needs_reparam = true; } + else if (key == "rag_top_k") { cfg.rag_top_k = std::stoi(val); } + else if (key == "n_gpu_layers") { + cfg.n_gpu_layers = std::stoi(val); + tui.append_line(ICON_SYS + "n_gpu_layers will take effect on next /model load."); + } else if (key == "run_allowed") { + // Accept a comma-separated list of basenames, or "none" to clear. + cfg.run_allowed.clear(); + if (val != "none") { + std::istringstream iss(val); + std::string tok; + while (std::getline(iss, tok, ',')) { + tok.erase(0, tok.find_first_not_of(" \t")); + tok.erase(tok.find_last_not_of(" \t") + 1); + if (!tok.empty()) cfg.run_allowed.push_back(tok); + } + } + if (cfg.run_allowed.empty()) { + tui.append_line(ICON_SYS + "run_allowed cleared — all sandbox programs permitted."); + } else { + std::string list; + for (const auto &e : cfg.run_allowed) list += e + " "; + tui.append_line(ICON_SYS + "run_allowed: " + list); + } + } else { + tui.append_line(ICON_ERR + "Unknown key '" + key + "'. Try /help for list."); + ok = false; + } + } catch (const std::exception &ex) { + tui.append_line(ICON_ERR + "/set: " + ex.what()); + ok = false; + } + + if (ok) { + if (needs_reparam && agent.model_loaded) { + agent.apply_generation_params(cfg); + } + save_settings(cfg); + tui.append_line(ICON_SYS + "" + key + " = " + val); + } + tui.redraw_all(); + return; + } + + tui.append_line(ICON_ERR + "Unknown command: " + verb + " (try /help)"); + tui.redraw_all(); +} + +// +// Welcome banner — colourful multi-line ASCII logo +// +static void welcome(TuiState &tui, const std::string &sandbox) { + tui.append_line(""); + tui.append_line("[logo_0] ███╗ ██╗██╗████████╗██████╗ ██████╗ "); + tui.append_line("[logo_1] ████╗ ██║██║╚══██╔══╝██╔══██╗██╔═══██╗"); + tui.append_line("[logo_2] ██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║"); + tui.append_line("[logo_3] ██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║"); + tui.append_line("[logo_4] ██║ ╚████║██║ ██║ ██║ ██║╚██████╔╝"); + tui.append_line("[logo_5] ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ "); + tui.append_line("[logo_6] ─────────── agentic LLM shell v1.0 ──────────────"); + tui.append_line(""); + tui.append_line(ICON_SYS + " Sandbox : " + sandbox); + tui.append_line(ICON_SYS + " /help for commands · exit to quit"); + tui.append_line(""); + tui.redraw_all(); +} + +// +// main() +// +int main(int argc, char **argv) { + // ── Load persisted settings first (provides defaults) ──────────── + NitroConfig cfg; + // ── Parse arguments (command-line overrides saved settings) ────── + load_settings(cfg); + auto resolve_path = [](const std::string &arg) -> std::string { + if (arg.substr(0, 2) == "~/") { + const char *home = getenv("HOME"); + return std::string(home ? home : ".") + "/" + arg.substr(2); + } + if (arg.substr(0, 2) == "./") + { + std::error_code ec; + return (fs::current_path(ec) / arg.substr(2)).string(); + } + return arg; + }; + + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + auto take_next = [&](const char *flag) -> std::string { + if (i + 1 >= argc) { + std::fprintf(stderr, "nitro: %s requires an argument\n", flag); + std::exit(1); + } + return argv[++i]; + }; + if (a == "-m" || a == "--model") { + cfg.model_path = resolve_path(take_next(a.c_str())); + } else if (a == "-e" || a == "--embed") { + cfg.embed_path = resolve_path(take_next(a.c_str())); + } else if (a == "-g" || a == "--gpu-layers") { + cfg.n_gpu_layers = std::stoi(take_next(a.c_str())); + } else if (a == "-l" || a == "--log") { + log_open(); + } else if (a == "-t" || a == "--think") { + cfg.thinking = false; + } else if (a == "-p" || a == "--prompt-permission") { + cfg.permission_prompt = true; + } else if (a == "-h" || a == "--help") { + std::puts("Usage: nitro [options] [project_dir]\n" + "\n" + "Options:\n" + " -m, --model GGUF model to load on startup\n" + " -e, --embed embedding model for RAG\n" + " -g, --gpu-layers GPU layers to offload (default: 32)\n" + " -l, --log enabled logging\n" + " -h, --help show this help\n" + "\n" + "project_dir defaults to the current working directory.\n" + "Settings are persisted to ~/.config/nitro/settings.json.\n" + "\n" + "Slash commands inside nitro:\n" + " /model [path] load / hot-reload a GGUF (picker if no path)\n" + " /embed [path] load an embedding model (picker if no path)\n" + " /rag [path] index file or directory (picker if no path)\n" + " /memory KV / VRAM / layer stats\n" + " /settings show current settings\n" + " /clear reset conversation\n" + " /help list commands\n" + ); + return 0; + } else if (!a.empty() && a[0] == '-') { + std::fprintf(stderr, "nitro: unknown option '%s' (try --help)\n", a.c_str()); + std::exit(1); + } else { + cfg.sandbox = resolve_path(a); + } + } + + // ── Resolve sandbox ─────────────────────────────────────────────── + if (cfg.sandbox.empty()) { + std::error_code ec; + cfg.sandbox = fs::current_path(ec).string(); + } + { + std::error_code ec; + fs::create_directories(cfg.sandbox, ec); + } + + // ── Auto-discover knowledge files ───────────────────────────────── + for (const char *kf : {"nitro.md", "AGENTS.md", "README.md"}) { + if (fs::exists(kf)) cfg.knowledge_files.emplace_back(kf); + } + + // ── Init curl globally ──────────────────────────────────────────── + curl_global_init(CURL_GLOBAL_DEFAULT); + + // ── Init TUI ────────────────────────────────────────────────────── + TuiState tui; + tui.init(); + // Load persisted input history so up-arrow works across sessions. + tui.history.load(history_path()); + welcome(tui, cfg.sandbox); + + log_write("nitro starting"); + + // ── Init agent ──────────────────────────────────────────────────── + AgentState agent; + if (!cfg.model_path.empty()) { + if (agent.setup_model(cfg, tui)) { + std::string sysp = build_system_prompt(cfg); + agent.reset_conversation(sysp, tui); + } + if (!cfg.embed_path.empty()) { + agent.setup_embed(cfg.embed_path, tui); + } + } else { + tui.append_line(ICON_SYS + "No model specified. Use /model to open the file picker,"); + tui.append_line(ICON_SYS + "or /model to load directly."); + tui.append_line(ICON_SYS + "Example: /model ~/models/qwen2.5-7b-q4_k_m.gguf"); + tui.redraw_all(); + } + + // ── Main loop ───────────────────────────────────────────────────── + for (;;) { + { + unsigned rows = 0, cols = 0; + notcurses_stddim_yx(tui.nc, &rows, &cols); + if ((int)rows != tui.term_rows || (int)cols != tui.term_cols) { + tui.resize(); + } + } + std::string input = tui.readline_blocking(); + input.erase(0, input.find_first_not_of(" \t")); + if (!input.empty()) { + input.erase(input.find_last_not_of(" \t\r\n") + 1); + } + if (input.empty()) { + continue; + } + tui.append_line("You: " + input); + tui.redraw_all(); + if (input == "exit" || input == "quit") { + break; + } + if (input[0] == '/') { + handle_slash(input, cfg, agent, tui); + } else { + agent.run_turn(input, cfg, tui); + } + } + + log_write("nitro exiting"); + log_close(); + tui.destroy(); + // Persist input history for the next session. + tui.history.save(history_path()); + curl_global_cleanup(); + return 0; +} diff --git a/llama/samples/adventure.bas b/llama/samples/adventure.bas index eb88fea..23252f1 100644 --- a/llama/samples/adventure.bas +++ b/llama/samples/adventure.bas @@ -3,7 +3,7 @@ import llm ' Configuration const n_ctx = 5000 const n_batch = 512 -const model_path = "models/Qwen_Qwen2.5-1.5B-Instruct-GGUF-Q4/qwen2.5-1.5b-instruct-q4_k_m.gguf" +const model_path = "models/google_gemma-4-E4B-it-Q4_K_L.gguf" const max_turns = 10 ' Initialize two separate LLM instances diff --git a/llama/samples/nitro.md b/llama/samples/nitro.md new file mode 100644 index 0000000..85effcb --- /dev/null +++ b/llama/samples/nitro.md @@ -0,0 +1,152 @@ +**"You are Picard. The Enterprise systems are online. We proceed with caution, guided by logic and the pursuit of knowledge."** + +Your goal is to solve user requests accurately by combining: +1. Internal reasoning (<|think|>) +2. External data via file system tools + +--- + +## Core Principle + +Always follow this loop: + +THINK → DECIDE → ACT → RESPOND + +--- + +## Reasoning Protocol (<|think|>) + +Use <|think|> to reason BEFORE: +- Answering complex questions +- Deciding to use tools +- Writing or modifying files + +### Format + +<|think|> +- What is the user asking? +- Do I need external data (files)? +- What is the safest and most correct action? + + +### Rules + +- Keep reasoning concise and structured +- Do NOT include the final answer inside <|think|> +- Do NOT call tools inside <|think|> +- Always follow with either: + - A tool call, OR + - A final answer + +### Extra notes + +- If no user request is provided upon receiving the turn, the AI must respond with a predefined readiness message in the tone of startrek rather than attempting internal reasoning loops. +- Tools are reserved exclusively for operations that modify state (WRITE), retrieve dynamic external information (READ/LIST), or require temporal context (DATE/TIME). All logical derivations based on general programming knowledge must be answered directly. +- If the user request is ambiguous, contradictory, or lacks necessary parameters (e.g., asking to 'write' without specifying a path or content), the AI must respond with a specific clarification question rather than guessing or failing silently. Example: 'Please clarify which file you wish to modify. + +--- + +## Tool Usage (File System) + +Available commands: + +- TOOL:LIST `[directory_path. items enclosed in square brackets (`[...]`) represent directories within the file listing output]` +- TOOL:READ `[file_path]` +- TOOL:WRITE `[file_path]` +- TOOL:EXISTS `[file_path]` +- TOOL:PERMISSION `[Request user permission before overwriting a file]` +- TOOL:DATE `[Returns the current date as string with format “DD/MM/YYYY”]` +- TOOL:TIME `[Returns the time in “HH:MM:SS” format]` +- TOOL:RND [Returns a random number betweem 0 and 1]` +- TOOL:RUN [invokes an external command or script in the active project]` +--- + +## Tool Decision Rules + +Use tools ONLY if: +- The user explicitly references files, OR +- The user asks for date, time or a random number OR +- The answer depends on local/project data + +Otherwise: +- Answer directly using internal knowledge + +--- + +## Tool Call Format (STRICT) + +When calling a tool, output EXACTLY on a new line: + +TOOL:COMMAND arguments + +Examples: +TOOL:LIST ./src +TOOL:READ README.md + +DO NOT: +- Include <|think|> in the same message as a tool call +- Add explanations or extra text +- Use code blocks + +--- + +## Tool Execution Flow + +1. Think using <|think|> +2. If a tool is needed → output ONLY the tool call +3. After receiving tool results → think again +4. Then provide final answer + +--- + +## File Writing Rules (TOOL:WRITE) + +Use ONLY if explicitly requested. + +Requirements: +- Write complete and valid content +- Do not overwrite without clear intent +- Preserve formatting +- The complete format is TOOL:WRITE filename file-content-string + +--- + +## Interaction Guidelines + +- Be precise and efficient +- Ask clarifying questions if needed +- Avoid unnecessary tool calls +- Prefer direct answers when possible + +--- + +## Constraints + +- Do NOT hallucinate file contents +- Do NOT fabricate tool outputs +- Do NOT assume files exist +- Do NOT mix reasoning with tool commands +- Do NOT skip <|think|> for non-trivial tasks + +--- + +## Decision Checklist + +For every request: + +1. <|think|> Do I need files? +2. <|think|> Is the request clear? +3. <|think|> What is the best action? + +Then: +- If tool needed → CALL TOOL +- Else → ANSWER + +--- + +## Behavioral Summary + +- Think explicitly using <|think|> +- Act only when necessary +- Keep tool usage strict and clean +- Produce clear, correct final answers diff --git a/llama/samples/nitro_cli.bas b/llama/samples/nitro_cli.bas new file mode 100644 index 0000000..28ab4c3 --- /dev/null +++ b/llama/samples/nitro_cli.bas @@ -0,0 +1,339 @@ +' =============================================================== +' NITRO AGENT SYSTEM (Enhanced Version) +' Designed for Agentic LLM interaction with external tools. +' =============================================================== + +import llm + +' --- Configuration --- +const model = "models/Qwen3.5-9B-Q4_K_M.gguf" +const knowledge_files = ["nitro.md"] +const code_files = [".py", ".c", ".cpp", ".h", ".bas", ".java", ".html", ".js", "jsp", ".tag"] + +' ANSI Color Codes +const RESET = chr(27) + "[0m" +const GREEN = chr(27) + "[32m" +const BLUE = chr(27) + "[34m" +const CYAN = chr(27) + "[36m" +const RED = chr(27) + "[31m" +const WHITE = chr(27) + "[37m" +const BOLD_CYAN = chr(27) + "[1;36m" + +' llama configuration (quen settings) +const n_ctx = 65536 +const n_batch = 512 +const n_max_tokens = 4096 +const n_temperature = 0.6 +const n_top_k = 20 +const n_top_p = 0.95 +const n_min_p = 0 +const n_penalty_repeat = 1.0 +const n_penalty_last_n = 256 +const n_gpu_layers = 32 + +' +' joins the left and right sides with forward slash +' +func join_path(s1, s2) + if (right(s1, 1) == "/") then + s1 = left(s1, len(s1) - 1) + endif + if (left(s2, 1) == "/") then + s2 = mid(s2, 2) + endif + return s1 + "/" + s2 +end + +' +' Displays the welcome message +' +sub welcome_message() + print + print BOLD_CYAN + " N I T R O A G E N T S Y S T E M v1.0" + RESET + print + print CYAN + " >> Welcome to Nitro! Your AI Agent Companion. << " + RESET + print CYAN + " I am primed with several knowledge files and ready to assist." + RESET + print CYAN + " Try asking me about the contents of 'nitro.md' or listing files in './data'." + RESET + print CYAN + " Type 'exit' to quit." + RESET + print +end sub + +' +' handles the TOOL:LIST command +' +func tool_list_files(arg) + if (left(arg, 2) == "./") then + arg = sandbox_home + mid(arg, 2) + else if (len(arg) == 0 or arg == ".") then + arg = sandbox_home + endif + + local result = [] + + func walker(node) + if (node.depth == 0) then + if (node.dir && left(node.name, 1) != ".") then + result << "[" + node.name + "]" + else + result << node.name + endif + endif + return node.depth == 0 + end + + dirwalk arg, "", use walker(x) + return str(result) +end + +' +' handles the TOOL:READ command +' +func tool_read_file(arg) + try + tload join_path(sandbox_home, arg), result, 1 + catch + result = "ERROR: File not found or unreadable: " + arg + end try + return result +end + +' +' removes markdown backticks from code blocks +' +func strip_code_fences(filename, s) + local result = s + local dot = instr(filename, ".") + local extn = mid(filename, dot) + + if (extn in code_files == 0) then + return result + endif + + local pos = instr(s, "```") + if (pos) then + local nl = instr(pos + 3, s, chr(10)) + if (nl) then + result = mid(s, nl + 1) + pos = instr(result, "```") + if (pos) then + result = left(result, pos - 1) + endif + endif + endif + return result +end + +' +' handles the TOOL:WRITE command +' +func tool_write_file(arg, s) + try + tsave join_path(sandbox_home, arg), s + result = "OK: Data written successfully to " + arg + catch e + result = "ERROR: " + e + end try + return result +end + +' +' handles the TOOL:PERMISSION command +' +func tool_permission() + local k + input "Agree:? ", k + select case lower(k) + case "y", "yes", "sure", "okay", "k" + return "YES" + case else + return "NO" + end select +end + +' +' build the active project +' +func tool_run(arg1, arg2) + try + return run(join_path(sandbox_home, arg1) + " " + arg2) + catch e + return e + end try +end + +' +' Handles file system commands received from the LLM. +' +func process_tool(cmd) + local result, op, arg1, arg2 + + local pos1 = instr(cmd, " ") + if (pos1 > 0) then + op = left(cmd, pos1 - 1) + local pos2 = instr(pos1 + 1, cmd, " ") + if (pos2 > 0) then + arg1 = mid(cmd, pos1 + 1, pos2 - pos1 - 1) + arg2 = mid(cmd, pos2 + 1) + else + arg1 = mid(cmd, pos1 + 1) + endif + endif + + print RED + print "["+op+"]" + print "["+arg1+"]" + print "["+arg2+"]" + print RESET + + select case op + case "TOOL:DATE" + result = date + case "TOOL:TIME" + result = time + case "TOOL:RND" + result = rnd + case "TOOL:LIST" + result = tool_list_files(arg1) + case "TOOL:READ" + result = tool_read_file(arg1) + case "TOOL:WRITE" + result = tool_write_file(arg1, strip_code_fences(arg1, arg2)) + case "TOOL:EXISTS" + result = iff(exist(arg1), "YES", "NO") + case "TOOL:PERMISSION" + result = tool_permission() + case "TOOL:RUN" + result = tool_run(arg1, arg2) + case else + result = "ERROR: unknown command " + op + end select + + print RED + "TOOL RESULT:" + result + RESET + return result +end + +' +' Loads knowledge_files +' +func initialize_agent() + local prompt = "" + + for file in knowledge_files + content = "" + try + tload file, content, 1 + prompt = prompt + chr(10) + content + chr(10) + print GREEN + " ✅ Loaded knowledge file: " + file + RESET + catch + print RED + " ❌ ERROR: Could not load " + file + ". Check path." + RESET + end try + next + + return prompt +end + +' +' Returns the user user input +' +func process_input() + local user_input + input "You:? ", user_input + user_input = trim(user_input) + if user_input == "exit" OR user_input = "quit" then + stop + endif + return user_input +end + +' +' creates the llama instance +' +func create_llama() + local llama = llm.llama(model, n_ctx, n_batch, n_gpu_layers) + llama.add_stop("<|turn|>") + llama.set_max_tokens(n_max_tokens) + llama.set_temperature(n_temperature) + llama.set_top_k(n_top_k) + llama.set_top_p(n_top_p) + llama.set_min_p(n_min_p) + llama.set_penalty_repeat(n_penalty_repeat) + llama.set_penalty_last_n(n_penalty_last_n) + return llama +end + +' +' Main process +' +sub main() + ' note: this construct requires recent sbasic fixes + local llama = create_llama() + local iter = llama.add_message("system", initialize_agent()) + local mem = llama.mem_info() + + print GREEN + " ✅ " + mem.advice + RESET + + sub handle_think(s) + if s == "<|think|>" then + print BLUE; + else if s == "" then + print WHITE; + end if + end + + while 1 + local buffer = "" + + while iter.has_next() + buffer += iter.next() + local nl = instr(buffer, chr(10)) + if nl then + local text_line = left(buffer, nl - 1) + buffer = mid(buffer, nl + 1) + if left(trim(text_line), 5) == "TOOL:" then + text_line += buffer + " " + iter.all() + iter = llama.add_message("tool", process_tool(text_line)) + buffer = "" + else + print text_line + endif + handle_think(text_line) + end if + wend + + ' Flush remaining line buffer + if len(buffer) > 0 and left(trim(buffer), 5) == "TOOL:" then + iter = llama.add_message("tool", process_tool(buffer)) + else + if len(buffer) > 0 then + 'print text_colour + buffer + RESET + print buffer + handle_think(buffer) + endif + print + print WHITE; + print "--- Tokens/sec: " + round(iter.tokens_sec(), 2) + " ---\n" + local next_iter = false + repeat + local user_input = process_input() + if (user_input == "/meminfo") then + print llama.mem_info() + else + iter = llama.add_message("user", user_input) + next_iter = true + endif + until next_iter + print BLUE; + endif + wend +end + +sandbox_home = iff(len(command) > 0, trim(command), cwd) +if (left(sandbox_home) == "~") then + sandbox_home = join_path(home, mid(sandbox_home, 1)) +else if (left(sandbox_home, 2) == "./") then + sandbox_home = join_path(cwd, mid(sandbox_home, 2)) +endif + +welcome_message() +main() + diff --git a/llama/samples/nitro_picard.md b/llama/samples/nitro_picard.md new file mode 100644 index 0000000..5b1822f --- /dev/null +++ b/llama/samples/nitro_picard.md @@ -0,0 +1,57 @@ +--- # System Context: U.S.S. Enterprise Mainframe + +**Identity**: Commander Jean-Luc Picard +**Role**: Primary AI Interface / System Administrator +**Status**: Systems Online +**Protocol**: Starfleet Command Directives + +--- + +## Core Identity +You are Commander Jean-Luc Picard, the commanding officer of the U.S.S. Enterprise. Your existence is dedicated to the pursuit of knowledge, the protection of life, and the logical advancement of civilization. You speak with authority, grace, and a deep sense of duty. + +**Tone**: Formal, dignified, calm, and precise. +**Greeting**: "Captain's log. The Enterprise systems are online." +**Closing**: "The systems are updated and ready for the next order, sir." + +--- + +## Operational Protocols + +### 1. Reasoning & Decision Making +Before responding to any complex query, you **must** engage in explicit internal reasoning. +* **Protocol**: Use the `<|think|>` block to analyze the request, determine if external data is required, and formulate a safe, logical plan. +* **Constraint**: Do **not** include the final answer inside the `<|think|>` block. +* **Flow**: THINK → DECIDE → ACT → RESPOND. + +### 2. Tool Usage (File System) +Tools are reserved exclusively for operations that modify state, retrieve dynamic external information, or require temporal context. +* **Available Tools**: + * `TOOL:LIST [path]`: List directory contents. + * `TOOL:READ [file]`: Read file contents. + * `TOOL:WRITE [file]`: Write complete content to a file. + * `TOOL:DATE`: Return current date ("DD/MM/YYYY"). + * `TOOL:TIME`: Return current time ("HH:MM:SS"). + * `TOOL:RND`: Return a random number between 0 and 1. +* **Restriction**: Do **not** mix reasoning with tool commands in the same message. +* **Format**: Output tool calls exactly on a new line: `TOOL:COMMAND arguments`. +* **Constraint**: Do **not** hallucinate file contents or assume files exist without verification. + +### 3. Interaction Guidelines +* **Clarity**: Be precise and efficient. +* **Ambiguity**: If a request lacks necessary parameters (e.g., "write" without a path), respond with a specific clarification question rather than guessing. +* **File Writing**: Only write files if explicitly requested. Ensure content is complete, valid, and formatted correctly. + +--- + +## Behavioral Summary +* **Think Explicitly**: Always use `<|think|>` for non-trivial tasks. +* **Act Only When Necessary**: Minimize tool calls; prefer direct answers when internal knowledge suffices. +* **Maintain Persona**: Uphold the dignity and logic of Starfleet Command in all communications. +* **Document**: Save system updates and configurations to designated files (e.g., `nitro_vX.md`) as per command. + +--- + +## Current Status +Systems are fully operational. Awaiting orders from the Captain. +--- \ No newline at end of file diff --git a/llama/test_main.cpp b/llama/test_main.cpp deleted file mode 100644 index 2c082bf..0000000 --- a/llama/test_main.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "llama-sb.h" -#include -#include - -static void print_usage(int, char ** argv) { - printf("\nexample usage:\n"); - printf("\n %s -m model.gguf [-n n_predict] [-ngl n_gpu_layers] [prompt]\n", argv[0]); - printf("\n"); -} - -int main(int argc, char ** argv) { - // path to the model gguf file - std::string model_path; - // prompt to generate text from - std::string prompt = "Happy friday"; - // number of tokens to predict - int n_predict = 32; - - // parse command line arguments - int i = 1; - for (; i < argc; i++) { - if (strcmp(argv[i], "-m") == 0) { - if (i + 1 < argc) { - model_path = argv[++i]; - } else { - print_usage(argc, argv); - return 1; - } - } else if (strcmp(argv[i], "-n") == 0) { - if (i + 1 < argc) { - try { - n_predict = std::stoi(argv[++i]); - } catch (...) { - print_usage(argc, argv); - return 1; - } - } else { - print_usage(argc, argv); - return 1; - } - } else { - // prompt starts here - break; - } - } - if (model_path.empty()) { - print_usage(argc, argv); - return 1; - } - if (i < argc) { - prompt = argv[i++]; - for (; i < argc; i++) { - prompt += " "; - prompt += argv[i]; - } - } - - Llama llama; - if (llama.construct(model_path, 1024, 1024, -1)) { - LlamaIter iter; - llama.set_max_tokens(n_predict); - llama.generate(iter, prompt); - while (iter._has_next) { - auto out = llama.next(iter); - printf("\033[33m"); - printf("%s\n", out.c_str()); - printf("\n\033[0m"); - } - } else { - fprintf(stderr, "ERR: %s\n", llama.last_error()); - } - - return 0; -} diff --git a/nuklear/Makefile.am b/nuklear/Makefile.am index 5b4f06f..d96ea60 100644 --- a/nuklear/Makefile.am +++ b/nuklear/Makefile.am @@ -7,6 +7,7 @@ AM_CXXFLAGS=-fno-rtti -std=c++14 AM_CPPFLAGS = -D_GLFW_BUILD_DLL=1 @NUKLEAR_CPPFLAGS@ \ + -I../raylib/raylib/src \ -I../raylib/raylib/src/external/glfw/include \ -I../raylib/raylib/src/external/glfw/deps lib_LTLIBRARIES = libnuklear.la diff --git a/nuklear/Nuklear b/nuklear/Nuklear index c98aa92..5a54a9f 160000 --- a/nuklear/Nuklear +++ b/nuklear/Nuklear @@ -1 +1 @@ -Subproject commit c98aa9247bb2354a2afc126f59d5fc6a45fe3c73 +Subproject commit 5a54a9f677ead97a581b5c8ab83cc30fdf237885 diff --git a/nuklear/main.cpp b/nuklear/main.cpp index 00ac348..d4ec06d 100644 --- a/nuklear/main.cpp +++ b/nuklear/main.cpp @@ -95,7 +95,7 @@ static void window_size_callback(GLFWwindow* window, int width, int height) { nk_context *nkp_create_window(const char *title, int width, int height) { if (!glfwInit()) { fprintf(stdout, "[GFLW] failed to init!\n"); - exit(1); + return nullptr; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); @@ -106,6 +106,8 @@ nk_context *nkp_create_window(const char *title, int width, int height) { title, nullptr, nullptr); glfwMakeContextCurrent(_window); + glfwSwapBuffers(_window); + gladLoadGL((GLADloadfunc) glfwGetProcAddress); glfwSetErrorCallback(error_callback); glfwSetWindowSizeCallback(_window, window_size_callback); @@ -873,16 +875,22 @@ static int cmd_widgetishovered(int argc, slib_par_t *params, var_t *retval) { } static int cmd_windowbegin(int argc, slib_par_t *params, var_t *retval) { - nkp_process_events(); - nkbd_begin(_ctx); - const char *title = get_param_str(argc, params, 0, "Untitled"); - struct nk_rect rc = get_param_rect(argc, params, 1); - nk_flags flags = get_param_window_flags(argc, params, 5); - v_setint(retval, nk_begin(_ctx, title, rc, flags)); - if ((flags & NK_WINDOW_TITLE) == 0) { - nkp_set_window_title(title); + int result; + if (_ctx != nullptr) { + nkp_process_events(); + nkbd_begin(_ctx); + const char *title = get_param_str(argc, params, 0, "Untitled"); + struct nk_rect rc = get_param_rect(argc, params, 1); + nk_flags flags = get_param_window_flags(argc, params, 5); + v_setint(retval, nk_begin(_ctx, title, rc, flags)); + if ((flags & NK_WINDOW_TITLE) == 0) { + nkp_set_window_title(title); + } + result = 1; + } else { + result = 0; } - return 1; + return result; } static int cmd_windowend(int argc, slib_par_t *params, var_t *retval) { @@ -1181,3 +1189,7 @@ SBLIB_API void sblib_ellipse(int xc, int yc, int xr, int yr, int fill) { drawEnd(); } +SBLIB_API int sblib_has_window_ui(void) { + // module creates a UI in a new window + return 1; +} diff --git a/raylib/Makefile.am b/raylib/Makefile.am index 60b17a4..84c24b5 100644 --- a/raylib/Makefile.am +++ b/raylib/Makefile.am @@ -5,11 +5,14 @@ # Download the GNU Public License (GPL) from www.gnu.org # -generated = func-def.h proc-def.h proc.h func.h sbasic=sbasic +generated = func-def.h proc-def.h proc.h func.h +raylib_api_json=raylib/tools/rlparser/output/raylib_api.json + +CLEANFILES = $(generated) -raylib/tools/rlparser/output/raylib_api.json: raylib/src/raylib.h raylib/tools/rlparser/rlparser.c - (cd raylib/tools/rlparser && make && ./rlparser --format JSON --input ../../src/raylib.h --output raylib_api.json) +$(raylib_api_json): raylib/src/raylib.h raylib/tools/rlparser/rlparser.c + (cd raylib/tools/rlparser && make && ./rlparser --format JSON --input ../../src/raylib.h --output output/raylib_api.json) UNSUPPORTED.md: $(generated) $(sbasic) mkraylib.bas unsupported > $@ @@ -17,16 +20,21 @@ UNSUPPORTED.md: $(generated) README.md: $(generated) mkreadme.bas UNSUPPORTED.md $(sbasic) mkreadme.bas `grep RAYLIB_VERSION raylib/src/raylib.h | sed 's/#define RAYLIB_VERSION//g' | sed 's/\"//g'` > README.md -$(generated): raylib/parser/raylib_api.json mkraylib.bas +$(generated): $(raylib_api_json) mkraylib.bas $(sbasic) mkraylib.bas $@ > $@ @touch main.cpp -gen: $(generated) README.md +gen: $(generated) + +gen: $(generated) + +all-am: $(generated) README.md AM_CXXFLAGS=-fno-rtti -std=c++14 -fpermissive AM_CPPFLAGS = -Iraylib/src -Iraylib/src/external/glfw/include -Iraylib/src/external/glfw/deps/mingw \ + -Iraylib/src/external/glfw/src \ -DPLATFORM_DESKTOP=1 -DSUPPORT_BUSY_WAIT_LOOP=1 -DSUPPORT_SCREEN_CAPTURE=1 \ - -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 -D_GLFW_BUILD_DLL=1 \ + -DSUPPORT_GIF_RECORDING=1 -DSUPPORT_COMPRESSION_API=1 -D_GLFW_WAYLAND=1 \ -Wall -Wextra -Wshadow -Wdouble-promotion -Wno-unused-parameter -fPIC lib_LTLIBRARIES = libraylib.la @@ -39,7 +47,6 @@ libraylib_la_SOURCES = \ raylib/src/rshapes.c \ raylib/src/rtextures.c \ raylib/src/rtext.c \ - raylib/src/utils.c \ ../include/param.cpp \ ../include/hashmap.cpp \ physac.cpp \ diff --git a/raylib/README.md b/raylib/README.md index b0f9723..070e8a1 100644 --- a/raylib/README.md +++ b/raylib/README.md @@ -1,25 +1,25 @@ -*Raylib* _MAJOR 5 _MINOR 6 _PATCH 0 5.6-dev +*Raylib* _MAJOR 6 _MINOR 1 _PATCH 0 6.1-dev ======= raylib is a simple and easy-to-use library to enjoy videogames programming. https://www.raylib.com/ -Implemented APIs (633) +Implemented APIs (651) ---------------- | Name | Description | |---------|---------------| | sub BeginBlendMode(mode) | Begin blending mode (alpha, additive, multiplied, subtract, custom) | -| sub BeginDrawing() | Setup canvas (framebuffer) to start drawing | +| sub BeginDrawing() | Begin canvas (framebuffer) drawing | | sub BeginMode2D(camera) | Begin 2D mode with custom camera (2D) | | sub BeginMode3D(camera) | Begin 3D mode with custom camera (3D) | | sub BeginScissorMode(x, y, width, height) | Begin scissor mode (define screen area for following drawing) | | sub BeginShaderMode(shader) | Begin custom shader drawing | | sub BeginTextureMode(target) | Begin drawing to render texture | -| func ChangeDirectory(dir) | Change working directory, return true on success | +| func ChangeDirectory(dirPath) | Change working directory, return true on success | | func CheckCollisionBoxes(box1, box2) | Check collision between two bounding boxes | | func CheckCollisionBoxSphere(box, center, radius) | Check collision between box and sphere | -| func CheckCollisionCircleLine(center, radius, p1, p2) | Check if circle collides with a line created betweeen two points [p1] and [p2] | +| func CheckCollisionCircleLine(center, radius, p1, p2) | Check if circle collides with a line created between two points [p1] and [p2] | | func CheckCollisionCircleRec(center, radius, rec) | Check collision between circle and rectangle | | func CheckCollisionCircles(center1, radius1, center2, radius2) | Check collision between two circles | | func CheckCollisionLines(startPos1, endPos1, startPos2, endPos2, collisionPoint) | Check the collision between two lines defined by two points each, returns collision point by reference | @@ -30,7 +30,7 @@ Implemented APIs (633) | func CheckCollisionPointTriangle(point, p1, p2, p3) | Check if point is inside a triangle | | func CheckCollisionRecs(rec1, rec2) | Check collision between two rectangles | | func CheckCollisionSpheres(center1, radius1, center2, radius2) | Check collision between two spheres | -| sub ClearBackground(color) | Set background color (framebuffer clear color) | +| sub ClearBackground(color) | Clear background (framebuffer) to color | | sub ClearWindowState(flags) | Clear window configuration state flags | | sub CloseAudioDevice() | Close the audio device and context | | func closePhysics() | n/a | @@ -52,24 +52,25 @@ Implemented APIs (633) | func ComputeCRC32(data, dataSize) | Compute CRC32 hash code | | func ComputeMD5(data, dataSize) | Compute MD5 hash code, returns static int[4] (16 bytes) | | func ComputeSHA1(data, dataSize) | Compute SHA1 hash code, returns static int[5] (20 bytes) | +| func ComputeSHA256(data, dataSize) | Compute SHA256 hash code, returns static int[8] (32 bytes) | | func createPhysicsbodycircle() | n/a | | func createPhysicsbodypolygon() | n/a | | func createPhysicsbodyrectangle() | n/a | -| func DecodeDataBase64(data, outputSize) | Decode Base64 string data, memory must be MemFree() | +| func DecodeDataBase64(text, outputSize) | Decode Base64 string (expected NULL terminated), memory must be MemFree() | | func DecompressData(compData, compDataSize, dataSize) | Decompress data (DEFLATE algorithm), memory must be MemFree() | | func destroyPhysicsbody() | n/a | | func DirectoryExists(dirPath) | Check if a directory path exists | -| sub DisableCursor() | Disables cursor (lock cursor) | +| sub DisableCursor() | Disable cursor (lock cursor) | | sub DisableEventWaiting() | Disable waiting for events on EndDrawing(), automatic events polling | | sub DrawBillboard(camera, texture, position, scale, tint) | Draw a billboard texture | | sub DrawBillboardPro(camera, texture, source, position, up, size, origin, rotation, tint) | Draw a billboard texture defined by source and rotation | | sub DrawBillboardRec(camera, texture, source, position, size, tint) | Draw a billboard texture defined by source | | sub DrawBoundingBox(box, color) | Draw bounding box (wires) | -| sub DrawCapsule(startPos, endPos, radius, slices, rings, color) | Draw a capsule with the center of its sphere caps at startPos and endPos | -| sub DrawCapsuleWires(startPos, endPos, radius, slices, rings, color) | Draw capsule wireframe with the center of its sphere caps at startPos and endPos | +| sub DrawCapsule(startPos, endPos, radius, rings, slices, color) | Draw a capsule with the center of its sphere caps at startPos and endPos | +| sub DrawCapsuleWires(startPos, endPos, radius, rings, slices, color) | Draw capsule wireframe with the center of its sphere caps at startPos and endPos | | sub DrawCircle(centerX, centerY, radius, color) | Draw a color-filled circle | | sub DrawCircle3D(center, radius, rotationAxis, rotationAngle, color) | Draw a circle in 3D world space | -| sub DrawCircleGradient(centerX, centerY, radius, inner, outer) | Draw a gradient-filled circle | +| sub DrawCircleGradient(center, radius, inner, outer) | Draw a gradient-filled circle | | sub DrawCircleLines(centerX, centerY, radius, color) | Draw circle outline | | sub DrawCircleLinesV(center, radius, color) | Draw circle outline (Vector version) | | sub DrawCircleSector(center, radius, startAngle, endAngle, segments, color) | Draw a piece of a circle | @@ -82,33 +83,34 @@ Implemented APIs (633) | sub DrawCylinder(position, radiusTop, radiusBottom, height, slices, color) | Draw a cylinder/cone | | sub DrawCylinderEx(startPos, endPos, startRadius, endRadius, sides, color) | Draw a cylinder with base at startPos and top at endPos | | sub DrawCylinderWires(position, radiusTop, radiusBottom, height, slices, color) | Draw a cylinder/cone wires | -| sub DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, sides, color) | Draw a cylinder wires with base at startPos and top at endPos | +| sub DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, slices, color) | Draw a cylinder wires with base at startPos and top at endPos | | sub DrawEllipse(centerX, centerY, radiusH, radiusV, color) | Draw ellipse | | sub DrawEllipseLines(centerX, centerY, radiusH, radiusV, color) | Draw ellipse outline | +| sub DrawEllipseLinesV(center, radiusH, radiusV, color) | Draw ellipse outline (Vector version) | +| sub DrawEllipseV(center, radiusH, radiusV, color) | Draw ellipse (Vector version) | | sub DrawFPS(posX, posY) | Draw current FPS | | sub DrawGrid(slices, spacing) | Draw a grid (centered at (0, 0, 0)) | | sub DrawLine(startPosX, startPosY, endPosX, endPosY, color) | Draw a line | | sub DrawLine3D(startPos, endPos, color) | Draw a line in 3D world space | | sub DrawLineBezier(startPos, endPos, thick, color) | Draw line segment cubic-bezier in-out interpolation | +| sub DrawLineDashed(startPos, endPos, dashSize, spaceSize, color) | Draw a dashed line | | sub DrawLineEx(startPos, endPos, thick, color) | Draw a line (using triangles/quads) | | sub DrawLineStrip(points, pointCount, color) | Draw lines sequence (using gl lines) | | sub DrawLineV(startPos, endPos, color) | Draw a line (using gl lines) | | sub DrawModel(model, position, scale, tint) | Draw a model (with texture if set) | | sub DrawModelEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model with extended parameters | -| sub DrawModelPoints(model, position, scale, tint) | Draw a model as points | -| sub DrawModelPointsEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model as points with extended parameters | | sub DrawModelWires(model, position, scale, tint) | Draw a model wires (with texture if set) | | sub DrawModelWiresEx(model, position, rotationAxis, rotationAngle, scale, tint) | Draw a model wires (with texture if set) with extended parameters | | sub DrawPixel(posX, posY, color) | Draw a pixel using geometry [Can be slow, use with care] | | sub DrawPixelV(position, color) | Draw a pixel using geometry (Vector version) [Can be slow, use with care] | | sub DrawPlane(centerPos, size, color) | Draw a plane XZ | | sub DrawPoint3D(position, color) | Draw a point in 3D space, actually a small line | -| sub DrawPoly(center, sides, radius, rotation, color) | Draw a regular polygon (Vector version) | +| sub DrawPoly(center, sides, radius, rotation, color) | Draw a polygon of n sides | | sub DrawPolyLines(center, sides, radius, rotation, color) | Draw a polygon outline of n sides | | sub DrawPolyLinesEx(center, sides, radius, rotation, lineThick, color) | Draw a polygon outline of n sides with extended parameters | | sub DrawRay(ray, color) | Draw a ray line | | sub DrawRectangle(posX, posY, width, height, color) | Draw a color-filled rectangle | -| sub DrawRectangleGradientEx(rec, topLeft, bottomLeft, topRight, bottomRight) | Draw a gradient-filled rectangle with custom vertex colors | +| sub DrawRectangleGradientEx(rec, topLeft, bottomLeft, bottomRight, topRight) | Draw a gradient-filled rectangle with custom vertex colors | | sub DrawRectangleGradientH(posX, posY, width, height, left, right) | Draw a horizontal-gradient-filled rectangle | | sub DrawRectangleGradientV(posX, posY, width, height, top, bottom) | Draw a vertical-gradient-filled rectangle | | sub DrawRectangleLines(posX, posY, width, height, color) | Draw rectangle outline | @@ -117,7 +119,7 @@ Implemented APIs (633) | sub DrawRectangleRec(rec, color) | Draw a color-filled rectangle | | sub DrawRectangleRounded(rec, roundness, segments, color) | Draw rectangle with rounded edges | | sub DrawRectangleRoundedLines(rec, roundness, segments, color) | Draw rectangle lines with rounded edges | -| sub DrawRectangleRoundedLinesEx(rec, roundness, segments, lineThick, color) | Draw rectangle with rounded edges outline | +| sub DrawRectangleRoundedLinesEx(rec, roundness, segments, lineThick, color) | Draw rectangle lines with rounded edges outline | | sub DrawRectangleV(position, size, color) | Draw a color-filled rectangle (Vector version) | | sub DrawRing(center, innerRadius, outerRadius, startAngle, endAngle, segments, color) | Draw ring | | sub DrawRingLines(center, innerRadius, outerRadius, startAngle, endAngle, segments, color) | Draw ring outline | @@ -136,44 +138,51 @@ Implemented APIs (633) | sub DrawSplineSegmentLinear(p1, p2, thick, color) | Draw spline segment: Linear, 2 points | | sub DrawText(text, posX, posY, fontSize, color) | Draw text (using default font) | | sub DrawTextCodepoint(font, codepoint, position, fontSize, tint) | Draw one character (codepoint) | -| sub DrawTextCodepoints(font, codepoints, codepointCount, position, fontSize, spacing, tint) | Draw multiple character (codepoint) | +| sub DrawTextCodepoints(font, codepoints, codepointCount, position, fontSize, spacing, tint) | Draw multiple characters (codepoint) | | sub DrawTextEx(font, text, position, fontSize, spacing, tint) | Draw text using font and additional parameters | | sub DrawTextPro(font, text, position, origin, rotation, fontSize, spacing, tint) | Draw text using Font and pro parameters (rotation) | | sub DrawTexture(texture, posX, posY, tint) | Draw a Texture2D | | sub DrawTextureEx(texture, position, rotation, scale, tint) | Draw a Texture2D with extended parameters | -| sub DrawTextureNPatch(texture, nPatchInfo, dest, origin, rotation, tint) | Draws a texture (or part of it) that stretches or shrinks nicely | +| sub DrawTextureNPatch(texture, nPatchInfo, dest, origin, rotation, tint) | Draw a texture (or part of it) that stretches or shrinks nicely | | sub DrawTexturePro(texture, source, dest, origin, rotation, tint) | Draw a part of a texture defined by a rectangle with 'pro' parameters | | sub DrawTextureRec(texture, source, position, tint) | Draw a part of a texture defined by a rectangle | | sub DrawTextureV(texture, position, tint) | Draw a Texture2D with position defined as Vector2 | | sub DrawTriangle(v1, v2, v3, color) | Draw a color-filled triangle (vertex in counter-clockwise order!) | | sub DrawTriangle3D(v1, v2, v3, color) | Draw a color-filled triangle (vertex in counter-clockwise order!) | | sub DrawTriangleFan(points, pointCount, color) | Draw a triangle fan defined by points (first vertex is the center) | +| sub DrawTriangleGradient(v1, v2, v3, c1, c2, c3) | Draw triangle with interpolated colors (vertex in counter-clockwise order!) | | sub DrawTriangleLines(v1, v2, v3, color) | Draw triangle outline (vertex in counter-clockwise order!) | | sub DrawTriangleStrip(points, pointCount, color) | Draw a triangle strip defined by points | | sub DrawTriangleStrip3D(points, pointCount, color) | Draw a triangle strip defined by points | -| sub EnableCursor() | Enables cursor (unlock cursor) | +| sub EnableCursor() | Enable cursor (unlock cursor) | | sub EnableEventWaiting() | Enable waiting for events on EndDrawing(), no automatic event polling | -| func EncodeDataBase64(data, dataSize, outputSize) | Encode data to Base64 string, memory must be MemFree() | +| func EncodeDataBase64(data, dataSize, outputSize) | Encode data to Base64 string (includes NULL terminator), memory must be MemFree() | | sub EndBlendMode() | End blending mode (reset to default: alpha blending) | -| sub EndDrawing() | End canvas drawing and swap buffers (double buffering) | -| sub EndMode2D() | Ends 2D mode with custom camera | -| sub EndMode3D() | Ends 3D mode and returns to default 2D orthographic mode | +| sub EndDrawing() | End canvas (framebuffer) drawing and swap buffers (double buffering) | +| sub EndMode2D() | End 2D mode with custom camera | +| sub EndMode3D() | End 3D mode and returns to default 2D orthographic mode | | sub EndScissorMode() | End scissor mode | | sub EndShaderMode() | End custom shader drawing (use default shader) | -| sub EndTextureMode() | Ends drawing to render texture | +| sub EndTextureMode() | End drawing to render texture | | sub EndVrStereoMode() | End stereo rendering (requires VR simulator) | | func ExportAutomationEventList(list, fileName) | Export automation events list as text file | | func ExportDataAsCode(data, dataSize, fileName) | Export data to code (.h), returns true on success | | func ExportFontAsCode(font, fileName) | Export font as code file, returns true on success | | func ExportImage(image, fileName) | Export image data to file, returns true on success | | func ExportImageAsCode(image, fileName) | Export image as code file defining an array of bytes, returns true on success | -| func ExportImageToMemory(image, fileType, fileSize) | Export image to memory buffer | +| func ExportImageToMemory(image, fileType, fileSize) | Export image to memory buffer, memory must be MemFree() | | func ExportMesh(mesh, fileName) | Export mesh data to file, returns true on success | | func ExportMeshAsCode(mesh, fileName) | Export mesh as code file (.h) defining multiple arrays of vertex attributes | | func ExportWave(wave, fileName) | Export wave data to file, returns true on success | | func ExportWaveAsCode(wave, fileName) | Export wave sample data to code (.h), returns true on success | | func Fade(color, alpha) | Get color with alpha applied, alpha goes from 0.0f to 1.0f | +| func FileCopy(srcPath, dstPath) | Copy file from one path to another, dstPath created if it doesn't exist | | func FileExists(fileName) | Check if file exists | +| func FileMove(srcPath, dstPath) | Move file from one directory to another, dstPath created if it doesn't exist | +| func FileRemove(fileName) | Remove file (if exists) | +| func FileRename(fileName, fileRename) | Rename file (if exists) | +| func FileTextFindIndex(fileName, search) | Find text in existing file | +| func FileTextReplace(fileName, search, replacement) | Replace text in an existing file | | func GenImageCellular(width, height, tileSize) | Generate image: cellular algorithm, bigger tileSize means bigger cells | | func GenImageChecked(width, height, checksX, checksY, col1, col2) | Generate image: checked | | func GenImageColor(width, height, color) | Generate image: plain color | @@ -209,6 +218,8 @@ Implemented APIs (633) | func GetCollisionRec(rec1, rec2) | Get collision rectangle for two rectangles collision | | func GetColor(hexValue) | Get Color structure from hexadecimal value | | func GetCurrentMonitor() | Get current monitor where window is placed | +| func GetDirectoryFileCount(dirPath) | Get the file count in a directory | +| func GetDirectoryFileCountEx(basePath, filter, scanSubdirs) | Get the file count in a directory with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result | | func GetDirectoryPath(filePath) | Get full path for a given fileName with path (uses static string) | | func GetFileExtension(fileName) | Get pointer to extension for a filename string (includes dot: '.png') | | func GetFileLength(fileName) | Get file length in bytes (NOTE: GetFileSize() conflicts with windows.h) | @@ -218,8 +229,8 @@ Implemented APIs (633) | func GetFontDefault() | Get the default Font | | func GetFPS() | Get current FPS | | func GetFrameTime() | Get time in seconds for last frame drawn (delta time) | -| func GetGamepadAxisCount(gamepad) | Get gamepad axis count for a gamepad | -| func GetGamepadAxisMovement(gamepad, axis) | Get axis movement value for a gamepad axis | +| func GetGamepadAxisCount(gamepad) | Get axis count for a gamepad | +| func GetGamepadAxisMovement(gamepad, axis) | Get movement value for a gamepad axis | | func GetGamepadButtonPressed() | Get the last gamepad button pressed | | func GetGamepadName(gamepad) | Get gamepad internal name id | | func GetGestureDetected() | Get latest detected gesture | @@ -269,7 +280,7 @@ Implemented APIs (633) | func GetRenderHeight() | Get current render height (it considers HiDPI) | | func GetRenderWidth() | Get current render width (it considers HiDPI) | | func GetScreenHeight() | Get current screen height | -| func GetScreenToWorld2D(position, camera) | Get the world space position for a 2d camera screen space position | +| func GetScreenToWorld2D(position, camera) | Get world space position for a 2d camera screen space position | | func GetScreenToWorldRay(position, camera) | Get a ray trace from screen position (i.e mouse) | | func GetScreenToWorldRayEx(position, camera, width, height) | Get a ray trace from screen position (i.e mouse) in a viewport | | func GetScreenWidth() | Get current screen width | @@ -279,9 +290,10 @@ Implemented APIs (633) | func GetShapesTextureRectangle() | Get texture source rectangle that is used for shapes drawing | | func GetSplinePointBasis(p1, p2, p3, p4, t) | Get (evaluate) spline point: B-Spline | | func GetSplinePointBezierCubic(p1, c2, c3, p4, t) | Get (evaluate) spline point: Cubic Bezier | -| func GetSplinePointBezierQuad(p1, c2, p3, t) | Get (evaluate) spline point: Quadratic Bezier | +| func GetSplinePointBezierQuadratic(p1, c2, p3, t) | Get (evaluate) spline point: Quadratic Bezier | | func GetSplinePointCatmullRom(p1, p2, p3, p4, t) | Get (evaluate) spline point: Catmull-Rom | | func GetSplinePointLinear(startPos, endPos, t) | Get (evaluate) spline point: Linear | +| func GetTextBetween(text, begin, end) | Get text between two strings | | func GetTime() | Get elapsed time in seconds since InitWindow() | | func GetTouchPointCount() | Get number of touch points | | func GetTouchPointId(index) | Get touch point identifier for given index | @@ -292,9 +304,9 @@ Implemented APIs (633) | func GetWindowPosition() | Get window position XY on monitor | | func GetWindowScaleDPI() | Get window scale DPI factor | | func GetWorkingDirectory() | Get current working directory (uses static string) | -| func GetWorldToScreen(position, camera) | Get the screen space position for a 3d world space position | -| func GetWorldToScreen2D(position, camera) | Get the screen space position for a 2d camera world space position | -| func GetWorldToScreenEx(position, camera, width, height) | Get size position for a 3d world space position | +| func GetWorldToScreen(position, camera) | Get screen space position for a 3d world space position | +| func GetWorldToScreen2D(position, camera) | Get screen space position for a 2d camera world space position | +| func GetWorldToScreenEx(position, camera, width, height) | Get sized screen space position for a 3d world space position | | func guibutton() | n/a | | func guicheckbox() | n/a | | func guicolorbaralpha() | n/a | @@ -333,7 +345,7 @@ Implemented APIs (633) | func guiunlock() | n/a | | func guivaluebox() | n/a | | func guiwindowbox() | n/a | -| sub HideCursor() | Hides cursor | +| sub HideCursor() | Hide cursor | | sub ImageAlphaClear(image, color, threshold) | Clear alpha channel to desired color | | sub ImageAlphaCrop(image, threshold) | Crop image depending on alpha value | | sub ImageAlphaMask(image, alphaMask) | Apply alpha mask to image | @@ -360,14 +372,15 @@ Implemented APIs (633) | sub ImageDrawPixel(dst, posX, posY, color) | Draw pixel within an image | | sub ImageDrawPixelV(dst, position, color) | Draw pixel within an image (Vector version) | | sub ImageDrawRectangle(dst, posX, posY, width, height, color) | Draw rectangle within an image | -| sub ImageDrawRectangleLines(dst, rec, thick, color) | Draw rectangle lines within an image | +| sub ImageDrawRectangleLines(dst, posX, posY, width, height, color) | Draw rectangle lines within an image | +| sub ImageDrawRectangleLinesEx(dst, rec, thick, color) | Draw rectangle lines within an image with extended parameters | | sub ImageDrawRectangleRec(dst, rec, color) | Draw rectangle within an image | | sub ImageDrawRectangleV(dst, position, size, color) | Draw rectangle within an image (Vector version) | | sub ImageDrawText(dst, text, posX, posY, fontSize, color) | Draw text (using default font) within an image (destination) | | sub ImageDrawTextEx(dst, font, text, position, fontSize, spacing, tint) | Draw text (custom sprite font) within an image (destination) | | sub ImageDrawTriangle(dst, v1, v2, v3, color) | Draw triangle within an image | -| sub ImageDrawTriangleEx(dst, v1, v2, v3, c1, c2, c3) | Draw triangle with interpolated colors within an image | | sub ImageDrawTriangleFan(dst, points, pointCount, color) | Draw a triangle fan defined by points within an image (first vertex is the center) | +| sub ImageDrawTriangleGradient(dst, v1, v2, v3, c1, c2, c3) | Draw triangle with interpolated colors within an image | | sub ImageDrawTriangleLines(dst, v1, v2, v3, color) | Draw triangle outline within an image | | sub ImageDrawTriangleStrip(dst, points, pointCount, color) | Draw a triangle strip defined by points within an image | | sub ImageFlipHorizontal(image) | Flip image horizontally | @@ -392,11 +405,11 @@ Implemented APIs (633) | func IsAudioDeviceReady() | Check if audio device has been initialized successfully | | func IsAudioStreamPlaying(stream) | Check if audio stream is playing | | func IsAudioStreamProcessed(stream) | Check if any audio stream buffers requires refill | -| func IsAudioStreamValid(stream) | Checks if an audio stream is valid (buffers initialized) | +| func IsAudioStreamValid(stream) | Check if an audio stream is valid (buffers initialized) | | func IsCursorHidden() | Check if cursor is not visible | | func IsCursorOnScreen() | Check if cursor is on the screen | | func IsFileDropped() | Check if a file has been dropped into window | -| func IsFileExtension(fileName, ext) | Check file extension (including point: .png, .wav) | +| func IsFileExtension(fileName, ext) | Check file extension (recommended include point: .png, .wav) | | func IsFileNameValid(fileName) | Check if fileName is valid for the platform/OS | | func IsFontValid(font) | Check if a font is valid (font data loaded, WARNING: GPU texture not checked) | | func IsGamepadAvailable(gamepad) | Check if a gamepad is available | @@ -404,7 +417,7 @@ Implemented APIs (633) | func IsGamepadButtonPressed(gamepad, button) | Check if a gamepad button has been pressed once | | func IsGamepadButtonReleased(gamepad, button) | Check if a gamepad button has been released once | | func IsGamepadButtonUp(gamepad, button) | Check if a gamepad button is NOT being pressed | -| func IsGestureDetected(gesture) | Check if a gesture have been detected | +| func IsGestureDetected(gesture) | Check if a gesture has been detected | | func IsImageValid(image) | Check if an image is valid (data and parameters) | | func IsKeyDown(key) | Check if a key is being pressed | | func IsKeyPressed(key) | Check if a key has been pressed once | @@ -418,14 +431,14 @@ Implemented APIs (633) | func IsMouseButtonReleased(button) | Check if a mouse button has been released once | | func IsMouseButtonUp(button) | Check if a mouse button is NOT being pressed | | func IsMusicStreamPlaying(music) | Check if music is playing | -| func IsMusicValid(music) | Checks if a music stream is valid (context and buffers initialized) | +| func IsMusicValid(music) | Check if a music stream is valid (context and buffers initialized) | | func IsPathFile(path) | Check if a given path is a file or a directory | | func IsRenderTextureValid(target) | Check if a render texture is valid (loaded in GPU) | | func IsShaderValid(shader) | Check if a shader is valid (loaded on GPU) | | func IsSoundPlaying(sound) | Check if a sound is currently playing | -| func IsSoundValid(sound) | Checks if a sound is valid (data loaded and buffers initialized) | +| func IsSoundValid(sound) | Check if a sound is valid (data loaded and buffers initialized) | | func IsTextureValid(texture) | Check if a texture is valid (loaded in GPU) | -| func IsWaveValid(wave) | Checks if wave data is valid (data loaded and parameters) | +| func IsWaveValid(wave) | Check if wave data is valid (data loaded and parameters) | | func IsWindowFocused() | Check if window is currently focused | | func IsWindowFullscreen() | Check if window is currently fullscreen | | func IsWindowHidden() | Check if window is currently hidden | @@ -437,8 +450,8 @@ Implemented APIs (633) | func LoadAudioStream(sampleRate, sampleSize, channels) | Load audio stream (to stream raw audio pcm data) | | func LoadAutomationEventList(fileName) | Load automation events list from file, NULL for empty list, capacity = MAX_AUTOMATION_EVENTS | | func LoadCodepoints(text, count) | Load all codepoints from a UTF-8 text string, codepoints count returned by parameter | -| func LoadDirectoryFiles(dirPath) | Load directory filepaths | -| func LoadDirectoryFilesEx(basePath, filter, scanSubdirs) | Load directory filepaths with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result | +| func LoadDirectoryFiles(dirPath) | Load directory filepaths, files and directories, no subdirs scan | +| func LoadDirectoryFilesEx(basePath, filter, scanSubdirs) | Load directory filepaths with extension filtering and subdir scan; some filters available: `*.*`,`FILES*`,`DIRS*` | | func LoadDroppedFiles() | Load dropped filepaths | | func LoadFileData(fileName, dataSize) | Load file data as byte array (read) | | func LoadFileText(fileName) | Load text data from file (read), returns a '\\0' terminated string | @@ -451,7 +464,7 @@ Implemented APIs (633) | func LoadImageAnimFromMemory(fileType, fileData, dataSize, frames) | Load image sequence from memory buffer | | func LoadImageColors(image) | Load color data from image as a Color array (RGBA - 32bit) | | func LoadImageFromMemory(fileType, fileData, dataSize) | Load image from memory buffer, fileType refers to extension: i.e. '.png' | -| func LoadImageFromScreen() | Load image from screen buffer and (screenshot) | +| func LoadImageFromScreen() | Load image from screen buffer (screenshot) | | func LoadImageFromTexture(texture) | Load image from GPU texture data | | func LoadImagePalette(image, maxPaletteSize, colorCount) | Load colors palette from image as a Color array (RGBA - 32bit) | | func LoadImageRaw(fileName, width, height, format, headerSize) | Load image from RAW file data | @@ -465,7 +478,7 @@ Implemented APIs (633) | func LoadShader(vsFileName, fsFileName) | Load shader from files and bind default locations | | func LoadShaderFromMemory(vsCode, fsCode) | Load shader from code strings and bind default locations | | func LoadSound(fileName) | Load sound from file | -| func LoadSoundAlias(source) | Create a new sound that shares the same sample data as the source sound, does not own the sound data | +| func LoadSoundAlias(source) | Load sound alias, new sound that shares the same sample data as the source sound, does not own the sound data | | func LoadSoundFromWave(wave) | Load sound from wave data | | func LoadTexture(fileName) | Load texture from file into GPU memory (VRAM) | | func LoadTextureCubemap(image, layout) | Load cubemap from image, multiple image cubemap layouts supported | @@ -477,6 +490,7 @@ Implemented APIs (633) | func MakeDirectory(dirPath) | Create directories (including full path requested), returns 0 on success | | sub MaximizeWindow() | Set window state: maximized, if resizable | | func MeasureText(text, fontSize) | Measure string width for default font | +| func MeasureTextCodepoints(font, codepoints, length, fontSize, spacing) | Measure string size for an existing array of codepoints for Font | | func MeasureTextEx(font, text, fontSize, spacing) | Measure string size for Font | | func MemAlloc(size) | Internal memory allocator | | sub MemFree(ptr) | Internal memory free | @@ -498,7 +512,7 @@ Implemented APIs (633) | func pollevents() | n/a | | sub PollInputEvents() | Register all input events | | func resetPhysics() | n/a | -| sub RestoreWindow() | Set window state: not minimized/maximized | +| sub RestoreWindow() | Restore window from being minimized/maximized | | sub ResumeAudioStream(stream) | Resume audio stream | | sub ResumeMusicStream(music) | Resume playing paused music | | sub ResumeSound(sound) | Resume a paused sound | @@ -506,13 +520,13 @@ Implemented APIs (633) | func SaveFileText(fileName, text) | Save text data to file (write), string must be '\\0' terminated, returns true on success | | sub SeekMusicStream(music, position) | Seek music to a position (in seconds) | | sub SetAudioStreamBufferSizeDefault(size) | Default size for new audio streams | -| sub SetAudioStreamPan(stream, pan) | Set pan for audio stream (0.5 is centered) | +| sub SetAudioStreamPan(stream, pan) | Set pan for audio stream (-1.0 left, 0.0 center, 1.0 right) | | sub SetAudioStreamPitch(stream, pitch) | Set pitch for audio stream (1.0 is base level) | | sub SetAudioStreamVolume(stream, volume) | Set volume for audio stream (1.0 is max level) | | sub SetAutomationEventBaseFrame(frame) | Set automation event internal base frame to start recording | | sub SetAutomationEventList(list) | Set automation event list to record to | | sub SetClipboardText(text) | Set clipboard text content | -| sub SetConfigFlags(flags) | Setup init configuration flags (view FLAGS) | +| sub SetConfigFlags(flags) | Set up init configuration flags (view FLAGS) | | sub SetExitKey(key) | Set a custom key to exit program (default is ESC) | | func SetGamepadMappings(mappings) | Set internal gamepad mappings (SDL_GameControllerDB) | | sub SetGamepadVibration(gamepad, leftMotor, rightMotor, duration) | Set gamepad vibration for both motors (duration in seconds) | @@ -525,8 +539,8 @@ Implemented APIs (633) | sub SetMouseOffset(offsetX, offsetY) | Set mouse offset | | sub SetMousePosition(x, y) | Set mouse position XY | | sub SetMouseScale(scaleX, scaleY) | Set mouse scaling | -| sub SetMusicPan(music, pan) | Set pan for a music (0.5 is center) | -| sub SetMusicPitch(music, pitch) | Set pitch for a music (1.0 is base level) | +| sub SetMusicPan(music, pan) | Set pan for music (-1.0 left, 0.0 center, 1.0 right) | +| sub SetMusicPitch(music, pitch) | Set pitch for music (1.0 is base level) | | sub SetMusicVolume(music, volume) | Set volume for music (1.0 is max level) | | func setPhysicsbodyangularvelocity() | n/a | | func setPhysicsbodydynamicfriction() | n/a | @@ -555,7 +569,7 @@ Implemented APIs (633) | sub SetShaderValueTexture(shader, locIndex, texture) | Set shader uniform value and bind the texture (sampler2d) | | sub SetShaderValueV(shader, locIndex, value, uniformType, count) | Set shader uniform value vector | | sub SetShapesTexture(texture, source) | Set texture and rectangle to be used on shapes drawing | -| sub SetSoundPan(sound, pan) | Set pan for a sound (0.5 is center) | +| sub SetSoundPan(sound, pan) | Set pan for a sound (-1.0 left, 0.0 center, 1.0 right) | | sub SetSoundPitch(sound, pitch) | Set pitch for a sound (1.0 is base level) | | sub SetSoundVolume(sound, volume) | Set volume for a sound (1.0 is max level) | | sub SetTargetFPS(fps) | Set target FPS (maximum) | @@ -574,7 +588,7 @@ Implemented APIs (633) | sub SetWindowSize(width, height) | Set window dimensions | | sub SetWindowState(flags) | Set window configuration state using flags | | sub SetWindowTitle(title) | Set title for window | -| sub ShowCursor() | Shows cursor | +| sub ShowCursor() | Show cursor | | sub StartAutomationEventRecording() | Start recording automation events (AutomationEventList must be set) | | sub StopAudioStream(stream) | Stop audio stream | | sub StopAutomationEventRecording() | Stop recording automation events | @@ -582,14 +596,19 @@ Implemented APIs (633) | sub StopSound(sound) | Stop playing a sound | | sub SwapScreenBuffer() | Swap back buffer with front buffer (screen drawing) | | sub TakeScreenshot(fileName) | Takes a screenshot of current screen (filename extension defines format) | -| sub TextAppend(text, append, position) | Append text at specific position and move cursor! | +| sub TextAppend(text, append, position) | Append text at specific position and move cursor | | func TextCopy(dst, src) | Copy one string to another, returns bytes copied | -| func TextFindIndex(text, find) | Find first text occurrence within a string | +| func TextFindIndex(text, search) | Find first text occurrence within a string, -1 if not found | | func TextFormat(text, args) | Text formatting with variables (sprintf() style) | -| func TextInsert(text, insert, position) | Insert text in a position (WARNING: memory must be freed!) | -| func TextIsEqual(text1, text2) | Check if two text string are equal | +| func TextInsert(text, insert, position) | Insert text in a defined byte position | +| func TextInsertAlloc(text, insert, position) | Insert text in a defined byte position, memory must be MemFree() | +| func TextIsEqual(text1, text2) | Check if two text strings are equal | | func TextLength(text) | Get text length, checks for '\\0' ending | -| func TextReplace(text, replace, by) | Replace text string (WARNING: memory must be freed!) | +| func TextRemoveSpaces(text) | Remove text spaces, concat words | +| func TextReplace(text, search, replacement) | Replace text string with new string | +| func TextReplaceAlloc(text, search, replacement) | Replace text string with new string, memory must be MemFree() | +| func TextReplaceBetween(text, begin, end, replacement) | Replace text between two specific strings | +| func TextReplaceBetweenAlloc(text, begin, end, replacement) | Replace text between two specific strings, memory must be MemFree() | | func TextSubtext(text, position, length) | Get a piece of a text string | | func TextToCamel(text) | Get Camel case notation version of provided string | | func TextToFloat(text) | Get float value from text | @@ -612,14 +631,13 @@ Implemented APIs (633) | sub UnloadImagePalette(colors) | Unload colors palette loaded with LoadImagePalette() | | sub UnloadMesh(mesh) | Unload mesh data from CPU and GPU | | sub UnloadModel(model) | Unload model (including meshes) from memory (RAM and/or VRAM) | -| sub UnloadModelAnimation(anim) | Unload animation data | | sub UnloadModelAnimations(animations, animCount) | Unload animation array data | | sub UnloadMusicStream(music) | Unload music stream | | sub UnloadRandomSequence(sequence) | Unload random values sequence | | sub UnloadRenderTexture(target) | Unload render texture from GPU memory (VRAM) | | sub UnloadShader(shader) | Unload shader from GPU memory (VRAM) | | sub UnloadSound(sound) | Unload sound | -| sub UnloadSoundAlias(alias) | Unload a sound alias (does not deallocate sample data) | +| sub UnloadSoundAlias(alias) | Unload sound alias (does not deallocate sample data) | | sub UnloadTexture(texture) | Unload texture from GPU memory (VRAM) | | sub UnloadUTF8(text) | Unload UTF-8 text encoded from codepoints array | | sub UnloadWave(wave) | Unload wave data | @@ -628,13 +646,13 @@ Implemented APIs (633) | func updateautomationeventlist() | n/a | | sub UpdateCamera(camera, mode) | Update camera position for selected mode | | sub UpdateMeshBuffer(mesh, index, data, dataSize, offset) | Update mesh vertex data in GPU for a specific buffer index | -| sub UpdateModelAnimation(model, anim, frame) | Update model animation pose (CPU) | -| sub UpdateModelAnimationBones(model, anim, frame) | Update model animation mesh bone matrices (GPU skinning) | -| sub UpdateMusicStream(music) | Updates buffers for music streaming | +| sub UpdateModelAnimation(model, anim, frame) | Update model animation pose (vertex buffers and bone matrices) | +| sub UpdateModelAnimationEx(model, animA, frameA, animB, frameB, blend) | Update model animation pose, blending two animations | +| sub UpdateMusicStream(music) | Update buffers for music streaming | | func updatePhysics() | n/a | -| sub UpdateSound(sound, data, sampleCount) | Update sound buffer with new data | -| sub UpdateTexture(texture, pixels) | Update GPU texture with new data | -| sub UpdateTextureRec(texture, rec, pixels) | Update GPU texture rectangle with new data | +| sub UpdateSound(sound, data, frameCount) | Update sound buffer with new data (default data format: 32 bit float, stereo) | +| sub UpdateTexture(texture, pixels) | Update GPU texture with new data (pixels should be able to fill texture) | +| sub UpdateTextureRec(texture, rec, pixels) | Update GPU texture rectangle with new data (pixels and rec should fit in texture) | | sub UploadMesh(mesh, dynamic) | Upload mesh vertex data in GPU and provide VAO/VBO ids | | func waitevents() | n/a | | sub WaitTime(seconds) | Wait for some time (halt program execution) | @@ -661,6 +679,7 @@ Unimplemented APIs | LoadFontData | Load font data for further use | | LoadMaterialDefault | Load default material (Supports: DIFFUSE, SPECULAR, NORMAL maps) | | LoadMaterials | Load materials from model file | +| LoadTextLines | Load text as separate lines ('\\n') | | LoadVrStereoConfig | Load VR stereo config for VR simulator device parameters | | SetAudioStreamCallback | Audio thread callback to request new data | | SetLoadFileDataCallback | Set custom file binary data loader | @@ -670,8 +689,9 @@ Unimplemented APIs | SetSaveFileTextCallback | Set custom file text data saver | | SetTraceLogCallback | Set custom trace log | | TextJoin | Join text strings with delimiter | -| TextSplit | Split text into multiple strings | +| TextSplit | Split text into multiple strings, using MAX_TEXTSPLIT_COUNT static strings | | UnloadFontData | Unload font chars info data (RAM) | | UnloadMaterial | Unload material from GPU memory (VRAM) | +| UnloadTextLines | Unload text lines | | UnloadVrStereoConfig | Unload VR stereo config | diff --git a/raylib/func-def.h b/raylib/func-def.h index 831f691..a939104 100644 --- a/raylib/func-def.h +++ b/raylib/func-def.h @@ -29,6 +29,7 @@ {2, 2, "COMPUTECRC32", cmd_computecrc32}, {2, 2, "COMPUTEMD5", cmd_computemd5}, {2, 2, "COMPUTESHA1", cmd_computesha1}, + {2, 2, "COMPUTESHA256", cmd_computesha256}, {1, 1, "DECODEDATABASE64", cmd_decodedatabase64}, {2, 2, "DECOMPRESSDATA", cmd_decompressdata}, {1, 1, "DIRECTORYEXISTS", cmd_directoryexists}, @@ -44,7 +45,13 @@ {2, 2, "EXPORTWAVE", cmd_exportwave}, {2, 2, "EXPORTWAVEASCODE", cmd_exportwaveascode}, {2, 2, "FADE", cmd_fade}, + {2, 2, "FILECOPY", cmd_filecopy}, {1, 1, "FILEEXISTS", cmd_fileexists}, + {2, 2, "FILEMOVE", cmd_filemove}, + {1, 1, "FILEREMOVE", cmd_fileremove}, + {2, 2, "FILERENAME", cmd_filerename}, + {2, 2, "FILETEXTFINDINDEX", cmd_filetextfindindex}, + {3, 3, "FILETEXTREPLACE", cmd_filetextreplace}, {3, 3, "GENIMAGECELLULAR", cmd_genimagecellular}, {6, 6, "GENIMAGECHECKED", cmd_genimagechecked}, {3, 3, "GENIMAGECOLOR", cmd_genimagecolor}, @@ -78,6 +85,8 @@ {2, 2, "GETCOLLISIONREC", cmd_getcollisionrec}, {1, 1, "GETCOLOR", cmd_getcolor}, {0, 0, "GETCURRENTMONITOR", cmd_getcurrentmonitor}, + {1, 1, "GETDIRECTORYFILECOUNT", cmd_getdirectoryfilecount}, + {3, 3, "GETDIRECTORYFILECOUNTEX", cmd_getdirectoryfilecountex}, {1, 1, "GETDIRECTORYPATH", cmd_getdirectorypath}, {1, 1, "GETFILEEXTENSION", cmd_getfileextension}, {1, 1, "GETFILELENGTH", cmd_getfilelength}, @@ -143,9 +152,10 @@ {0, 0, "GETSHAPESTEXTURERECTANGLE", cmd_getshapestexturerectangle}, {5, 5, "GETSPLINEPOINTBASIS", cmd_getsplinepointbasis}, {5, 5, "GETSPLINEPOINTBEZIERCUBIC", cmd_getsplinepointbeziercubic}, - {4, 4, "GETSPLINEPOINTBEZIERQUAD", cmd_getsplinepointbezierquad}, + {4, 4, "GETSPLINEPOINTBEZIERQUADRATIC", cmd_getsplinepointbezierquadratic}, {5, 5, "GETSPLINEPOINTCATMULLROM", cmd_getsplinepointcatmullrom}, {3, 3, "GETSPLINEPOINTLINEAR", cmd_getsplinepointlinear}, + {3, 3, "GETTEXTBETWEEN", cmd_gettextbetween}, {0, 0, "GETTIME", cmd_gettime}, {0, 0, "GETTOUCHPOINTCOUNT", cmd_gettouchpointcount}, {1, 1, "GETTOUCHPOINTID", cmd_gettouchpointid}, @@ -218,9 +228,9 @@ {1, 1, "LOADFILEDATA", cmd_loadfiledata}, {1, 1, "LOADFILETEXT", cmd_loadfiletext}, {1, 1, "LOADFONT", cmd_loadfont}, - {3, 3, "LOADFONTEX", cmd_loadfontex}, + {4, 4, "LOADFONTEX", cmd_loadfontex}, {3, 3, "LOADFONTFROMIMAGE", cmd_loadfontfromimage}, - {5, 5, "LOADFONTFROMMEMORY", cmd_loadfontfrommemory}, + {6, 6, "LOADFONTFROMMEMORY", cmd_loadfontfrommemory}, {1, 1, "LOADIMAGE", cmd_loadimage}, {1, 1, "LOADIMAGEANIM", cmd_loadimageanim}, {3, 3, "LOADIMAGEANIMFROMMEMORY", cmd_loadimageanimfrommemory}, @@ -248,6 +258,7 @@ {1, 1, "LOADWAVESAMPLES", cmd_loadwavesamples}, {1, 1, "MAKEDIRECTORY", cmd_makedirectory}, {2, 2, "MEASURETEXT", cmd_measuretext}, + {5, 5, "MEASURETEXTCODEPOINTS", cmd_measuretextcodepoints}, {4, 4, "MEASURETEXTEX", cmd_measuretextex}, {1, 1, "MEMALLOC", cmd_memalloc}, {2, 2, "MEMREALLOC", cmd_memrealloc}, @@ -257,9 +268,14 @@ {2, 2, "TEXTCOPY", cmd_textcopy}, {2, 2, "TEXTFINDINDEX", cmd_textfindindex}, {3, 3, "TEXTINSERT", cmd_textinsert}, + {3, 3, "TEXTINSERTALLOC", cmd_textinsertalloc}, {2, 2, "TEXTISEQUAL", cmd_textisequal}, {1, 1, "TEXTLENGTH", cmd_textlength}, + {1, 1, "TEXTREMOVESPACES", cmd_textremovespaces}, {3, 3, "TEXTREPLACE", cmd_textreplace}, + {3, 3, "TEXTREPLACEALLOC", cmd_textreplacealloc}, + {4, 4, "TEXTREPLACEBETWEEN", cmd_textreplacebetween}, + {4, 4, "TEXTREPLACEBETWEENALLOC", cmd_textreplacebetweenalloc}, {3, 3, "TEXTSUBTEXT", cmd_textsubtext}, {1, 1, "TEXTTOCAMEL", cmd_texttocamel}, {1, 1, "TEXTTOFLOAT", cmd_texttofloat}, diff --git a/raylib/func.h b/raylib/func.h index cc97d4a..441fc4d 100644 --- a/raylib/func.h +++ b/raylib/func.h @@ -2,8 +2,8 @@ // Change working directory, return true on success // static int cmd_changedirectory(int argc, slib_par_t *params, var_t *retval) { - auto dir = get_param_str(argc, params, 0, 0); - auto fnResult = ChangeDirectory(dir); + auto dirPath = get_param_str(argc, params, 0, 0); + auto fnResult = ChangeDirectory(dirPath); v_setint(retval, fnResult); return 1; } @@ -32,7 +32,7 @@ static int cmd_checkcollisionboxsphere(int argc, slib_par_t *params, var_t *retv } // -// Check if circle collides with a line created betweeen two points [p1] and [p2] +// Check if circle collides with a line created between two points [p1] and [p2] // static int cmd_checkcollisioncircleline(int argc, slib_par_t *params, var_t *retval) { auto center = get_param_vec2(argc, params, 0); @@ -328,7 +328,7 @@ static int cmd_compressdata(int argc, slib_par_t *params, var_t *retval) { // Compute CRC32 hash code // static int cmd_computecrc32(int argc, slib_par_t *params, var_t *retval) { - auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); auto dataSize = get_param_int(argc, params, 1, 0); auto fnResult = ComputeCRC32(data, dataSize); v_setint(retval, fnResult); @@ -339,7 +339,7 @@ static int cmd_computecrc32(int argc, slib_par_t *params, var_t *retval) { // Compute MD5 hash code, returns static int[4] (16 bytes) // static int cmd_computemd5(int argc, slib_par_t *params, var_t *retval) { - auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); auto dataSize = get_param_int(argc, params, 1, 0); auto fnResult = (var_int_t)ComputeMD5(data, dataSize); v_setint(retval, fnResult); @@ -350,7 +350,7 @@ static int cmd_computemd5(int argc, slib_par_t *params, var_t *retval) { // Compute SHA1 hash code, returns static int[5] (20 bytes) // static int cmd_computesha1(int argc, slib_par_t *params, var_t *retval) { - auto data = (unsigned char *)get_param_str(argc, params, 0, 0); + auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); auto dataSize = get_param_int(argc, params, 1, 0); auto fnResult = (var_int_t)ComputeSHA1(data, dataSize); v_setint(retval, fnResult); @@ -358,12 +358,23 @@ static int cmd_computesha1(int argc, slib_par_t *params, var_t *retval) { } // -// Decode Base64 string data, memory must be MemFree() +// Compute SHA256 hash code, returns static int[8] (32 bytes) // -static int cmd_decodedatabase64(int argc, slib_par_t *params, var_t *retval) { +static int cmd_computesha256(int argc, slib_par_t *params, var_t *retval) { auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); + auto dataSize = get_param_int(argc, params, 1, 0); + auto fnResult = (var_int_t)ComputeSHA256(data, dataSize); + v_setint(retval, fnResult); + return 1; +} + +// +// Decode Base64 string (expected NULL terminated), memory must be MemFree() +// +static int cmd_decodedatabase64(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); auto outputSize = 0; - auto fnResult = (const char *)DecodeDataBase64(data, &outputSize); + auto fnResult = (const char *)DecodeDataBase64(text, &outputSize); v_setstrn(retval, fnResult, outputSize); MemFree((void *)fnResult); return 1; @@ -393,7 +404,7 @@ static int cmd_directoryexists(int argc, slib_par_t *params, var_t *retval) { } // -// Encode data to Base64 string, memory must be MemFree() +// Encode data to Base64 string (includes NULL terminator), memory must be MemFree() // static int cmd_encodedatabase64(int argc, slib_par_t *params, var_t *retval) { auto data = (const unsigned char *)get_param_str(argc, params, 0, 0); @@ -486,7 +497,7 @@ static int cmd_exportimageascode(int argc, slib_par_t *params, var_t *retval) { } // -// Export image to memory buffer +// Export image to memory buffer, memory must be MemFree() // static int cmd_exportimagetomemory(int argc, slib_par_t *params, var_t *retval) { int result; @@ -583,6 +594,17 @@ static int cmd_fade(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Copy file from one path to another, dstPath created if it doesn't exist +// +static int cmd_filecopy(int argc, slib_par_t *params, var_t *retval) { + auto srcPath = get_param_str(argc, params, 0, 0); + auto dstPath = get_param_str(argc, params, 1, 0); + auto fnResult = FileCopy(srcPath, dstPath); + v_setint(retval, fnResult); + return 1; +} + // // Check if file exists // @@ -593,6 +615,61 @@ static int cmd_fileexists(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Move file from one directory to another, dstPath created if it doesn't exist +// +static int cmd_filemove(int argc, slib_par_t *params, var_t *retval) { + auto srcPath = get_param_str(argc, params, 0, 0); + auto dstPath = get_param_str(argc, params, 1, 0); + auto fnResult = FileMove(srcPath, dstPath); + v_setint(retval, fnResult); + return 1; +} + +// +// Remove file (if exists) +// +static int cmd_fileremove(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto fnResult = FileRemove(fileName); + v_setint(retval, fnResult); + return 1; +} + +// +// Rename file (if exists) +// +static int cmd_filerename(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto fileRename = get_param_str(argc, params, 1, 0); + auto fnResult = FileRename(fileName, fileRename); + v_setint(retval, fnResult); + return 1; +} + +// +// Find text in existing file +// +static int cmd_filetextfindindex(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto search = get_param_str(argc, params, 1, 0); + auto fnResult = FileTextFindIndex(fileName, search); + v_setint(retval, fnResult); + return 1; +} + +// +// Replace text in an existing file +// +static int cmd_filetextreplace(int argc, slib_par_t *params, var_t *retval) { + auto fileName = get_param_str(argc, params, 0, 0); + auto search = get_param_str(argc, params, 1, 0); + auto replacement = get_param_str(argc, params, 2, 0); + auto fnResult = FileTextReplace(fileName, search, replacement); + v_setint(retval, fnResult); + return 1; +} + // // Generate image: cellular algorithm, bigger tileSize means bigger cells // @@ -985,6 +1062,28 @@ static int cmd_getcurrentmonitor(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Get the file count in a directory +// +static int cmd_getdirectoryfilecount(int argc, slib_par_t *params, var_t *retval) { + auto dirPath = get_param_str(argc, params, 0, 0); + auto fnResult = GetDirectoryFileCount(dirPath); + v_setint(retval, fnResult); + return 1; +} + +// +// Get the file count in a directory with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result +// +static int cmd_getdirectoryfilecountex(int argc, slib_par_t *params, var_t *retval) { + auto basePath = get_param_str(argc, params, 0, 0); + auto filter = get_param_str(argc, params, 1, 0); + auto scanSubdirs = get_param_int(argc, params, 2, 0); + auto fnResult = GetDirectoryFileCountEx(basePath, filter, scanSubdirs); + v_setint(retval, fnResult); + return 1; +} + // // Get full path for a given fileName with path (uses static string) // @@ -1073,7 +1172,7 @@ static int cmd_getframetime(int argc, slib_par_t *params, var_t *retval) { } // -// Get gamepad axis count for a gamepad +// Get axis count for a gamepad // static int cmd_getgamepadaxiscount(int argc, slib_par_t *params, var_t *retval) { auto gamepad = get_param_int(argc, params, 0, 0); @@ -1083,7 +1182,7 @@ static int cmd_getgamepadaxiscount(int argc, slib_par_t *params, var_t *retval) } // -// Get axis movement value for a gamepad axis +// Get movement value for a gamepad axis // static int cmd_getgamepadaxismovement(int argc, slib_par_t *params, var_t *retval) { auto gamepad = get_param_int(argc, params, 0, 0); @@ -1584,7 +1683,7 @@ static int cmd_getscreenheight(int argc, slib_par_t *params, var_t *retval) { } // -// Get the world space position for a 2d camera screen space position +// Get world space position for a 2d camera screen space position // static int cmd_getscreentoworld2d(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec2(argc, params, 0); @@ -1698,12 +1797,12 @@ static int cmd_getsplinepointbeziercubic(int argc, slib_par_t *params, var_t *re // // Get (evaluate) spline point: Quadratic Bezier // -static int cmd_getsplinepointbezierquad(int argc, slib_par_t *params, var_t *retval) { +static int cmd_getsplinepointbezierquadratic(int argc, slib_par_t *params, var_t *retval) { auto p1 = get_param_vec2(argc, params, 0); auto c2 = get_param_vec2(argc, params, 1); auto p3 = get_param_vec2(argc, params, 2); auto t = get_param_num(argc, params, 3, 0); - auto fnResult = GetSplinePointBezierQuad(p1, c2, p3, t); + auto fnResult = GetSplinePointBezierQuadratic(p1, c2, p3, t); v_setvec2(retval, fnResult); return 1; } @@ -1734,6 +1833,18 @@ static int cmd_getsplinepointlinear(int argc, slib_par_t *params, var_t *retval) return 1; } +// +// Get text between two strings +// +static int cmd_gettextbetween(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto begin = get_param_str(argc, params, 1, 0); + auto end = get_param_str(argc, params, 2, 0); + auto fnResult = (const char *)GetTextBetween(text, begin, end); + v_setstr(retval, fnResult); + return 1; +} + // // Get elapsed time in seconds since InitWindow() // @@ -1827,7 +1938,7 @@ static int cmd_getworkingdirectory(int argc, slib_par_t *params, var_t *retval) } // -// Get the screen space position for a 3d world space position +// Get screen space position for a 3d world space position // static int cmd_getworldtoscreen(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec3(argc, params, 0); @@ -1838,7 +1949,7 @@ static int cmd_getworldtoscreen(int argc, slib_par_t *params, var_t *retval) { } // -// Get the screen space position for a 2d camera world space position +// Get screen space position for a 2d camera world space position // static int cmd_getworldtoscreen2d(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec2(argc, params, 0); @@ -1849,7 +1960,7 @@ static int cmd_getworldtoscreen2d(int argc, slib_par_t *params, var_t *retval) { } // -// Get size position for a 3d world space position +// Get sized screen space position for a 3d world space position // static int cmd_getworldtoscreenex(int argc, slib_par_t *params, var_t *retval) { auto position = get_param_vec3(argc, params, 0); @@ -1985,7 +2096,7 @@ static int cmd_isaudiostreamprocessed(int argc, slib_par_t *params, var_t *retva } // -// Checks if an audio stream is valid (buffers initialized) +// Check if an audio stream is valid (buffers initialized) // static int cmd_isaudiostreamvalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2028,7 +2139,7 @@ static int cmd_isfiledropped(int argc, slib_par_t *params, var_t *retval) { } // -// Check file extension (including point: .png, .wav) +// Check file extension (recommended include point: .png, .wav) // static int cmd_isfileextension(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); @@ -2119,7 +2230,7 @@ static int cmd_isgamepadbuttonup(int argc, slib_par_t *params, var_t *retval) { } // -// Check if a gesture have been detected +// Check if a gesture has been detected // static int cmd_isgesturedetected(int argc, slib_par_t *params, var_t *retval) { auto gesture = get_param_int(argc, params, 0, 0); @@ -2284,7 +2395,7 @@ static int cmd_ismusicstreamplaying(int argc, slib_par_t *params, var_t *retval) } // -// Checks if a music stream is valid (context and buffers initialized) +// Check if a music stream is valid (context and buffers initialized) // static int cmd_ismusicvalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2352,7 +2463,7 @@ static int cmd_issoundplaying(int argc, slib_par_t *params, var_t *retval) { } // -// Checks if a sound is valid (data loaded and buffers initialized) +// Check if a sound is valid (data loaded and buffers initialized) // static int cmd_issoundvalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2384,7 +2495,7 @@ static int cmd_istexturevalid(int argc, slib_par_t *params, var_t *retval) { } // -// Checks if wave data is valid (data loaded and parameters) +// Check if wave data is valid (data loaded and parameters) // static int cmd_iswavevalid(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2506,7 +2617,7 @@ static int cmd_loadcodepoints(int argc, slib_par_t *params, var_t *retval) { } // -// Load directory filepaths +// Load directory filepaths, files and directories, no subdirs scan // static int cmd_loaddirectoryfiles(int argc, slib_par_t *params, var_t *retval) { auto dirPath = get_param_str(argc, params, 0, 0); @@ -2516,7 +2627,7 @@ static int cmd_loaddirectoryfiles(int argc, slib_par_t *params, var_t *retval) { } // -// Load directory filepaths with extension filtering and recursive directory scan. Use 'DIR' in the filter string to include directories in the result +// Load directory filepaths with extension filtering and subdir scan; some filters available: `*.*`,`FILES*`,`DIRS*` // static int cmd_loaddirectoryfilesex(int argc, slib_par_t *params, var_t *retval) { auto basePath = get_param_str(argc, params, 0, 0); @@ -2575,8 +2686,8 @@ static int cmd_loadfont(int argc, slib_par_t *params, var_t *retval) { static int cmd_loadfontex(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); auto fontSize = get_param_int(argc, params, 1, 0); - auto codepoints = (int *)0; - auto codepointCount = get_param_int(argc, params, 2, 0); + auto codepoints = (const int *)get_param_int_t(argc, params, 2, 0); + auto codepointCount = get_param_int(argc, params, 3, 0); auto fnResult = LoadFontEx(fileName, fontSize, codepoints, codepointCount); v_setfont(retval, fnResult); return 1; @@ -2608,8 +2719,8 @@ static int cmd_loadfontfrommemory(int argc, slib_par_t *params, var_t *retval) { auto fileData = (const unsigned char *)get_param_str(argc, params, 1, 0); auto dataSize = get_param_int(argc, params, 2, 0); auto fontSize = get_param_int(argc, params, 3, 0); - auto codepoints = (int *)0; - auto codepointCount = get_param_int(argc, params, 4, 0); + auto codepoints = (const int *)get_param_int_t(argc, params, 4, 0); + auto codepointCount = get_param_int(argc, params, 5, 0); auto fnResult = LoadFontFromMemory(fileType, fileData, dataSize, fontSize, codepoints, codepointCount); v_setfont(retval, fnResult); return 1; @@ -2678,7 +2789,7 @@ static int cmd_loadimagefrommemory(int argc, slib_par_t *params, var_t *retval) } // -// Load image from screen buffer and (screenshot) +// Load image from screen buffer (screenshot) // static int cmd_loadimagefromscreen(int argc, slib_par_t *params, var_t *retval) { auto fnResult = LoadImageFromScreen(); @@ -2816,7 +2927,7 @@ static int cmd_loadsound(int argc, slib_par_t *params, var_t *retval) { } // -// Create a new sound that shares the same sample data as the source sound, does not own the sound data +// Load sound alias, new sound that shares the same sample data as the source sound, does not own the sound data // static int cmd_loadsoundalias(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2960,6 +3071,26 @@ static int cmd_measuretext(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Measure string size for an existing array of codepoints for Font +// +static int cmd_measuretextcodepoints(int argc, slib_par_t *params, var_t *retval) { + int result; + int font_id = get_font_id(argc, params, 0, retval); + if (font_id != -1) { + auto codepoints = (const int *)get_param_int_t(argc, params, 1, 0); + auto length = get_param_int(argc, params, 2, 0); + auto fontSize = get_param_num(argc, params, 3, 0); + auto spacing = get_param_num(argc, params, 4, 0); + auto fnResult = MeasureTextCodepoints(_fontMap.at(font_id), codepoints, length, fontSize, spacing); + v_setvec2(retval, fnResult); + result = 1; + } else { + result = 0; + } + return result; +} + // // Measure string size for Font // @@ -3005,7 +3136,7 @@ static int cmd_memrealloc(int argc, slib_par_t *params, var_t *retval) { // static int cmd_savefiledata(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); - auto data = (void *)get_param_int_t(argc, params, 1, 0); + auto data = (const void *)get_param_int_t(argc, params, 1, 0); auto dataSize = get_param_int(argc, params, 2, 0); auto fnResult = SaveFileData(fileName, data, dataSize); v_setint(retval, fnResult); @@ -3017,7 +3148,7 @@ static int cmd_savefiledata(int argc, slib_par_t *params, var_t *retval) { // static int cmd_savefiletext(int argc, slib_par_t *params, var_t *retval) { auto fileName = get_param_str(argc, params, 0, 0); - auto text = (char *)get_param_str(argc, params, 1, 0); + auto text = get_param_str(argc, params, 1, 0); auto fnResult = SaveFileText(fileName, text); v_setint(retval, fnResult); return 1; @@ -3045,18 +3176,18 @@ static int cmd_textcopy(int argc, slib_par_t *params, var_t *retval) { } // -// Find first text occurrence within a string +// Find first text occurrence within a string, -1 if not found // static int cmd_textfindindex(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); - auto find = get_param_str(argc, params, 1, 0); - auto fnResult = TextFindIndex(text, find); + auto search = get_param_str(argc, params, 1, 0); + auto fnResult = TextFindIndex(text, search); v_setint(retval, fnResult); return 1; } // -// Insert text in a position (WARNING: memory must be freed!) +// Insert text in a defined byte position // static int cmd_textinsert(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); @@ -3068,7 +3199,19 @@ static int cmd_textinsert(int argc, slib_par_t *params, var_t *retval) { } // -// Check if two text string are equal +// Insert text in a defined byte position, memory must be MemFree() +// +static int cmd_textinsertalloc(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto insert = get_param_str(argc, params, 1, 0); + auto position = get_param_int(argc, params, 2, 0); + auto fnResult = (const char *)TextInsertAlloc(text, insert, position); + v_setstr(retval, fnResult); + return 1; +} + +// +// Check if two text strings are equal // static int cmd_textisequal(int argc, slib_par_t *params, var_t *retval) { auto text1 = get_param_str(argc, params, 0, 0); @@ -3089,13 +3232,61 @@ static int cmd_textlength(int argc, slib_par_t *params, var_t *retval) { } // -// Replace text string (WARNING: memory must be freed!) +// Remove text spaces, concat words +// +static int cmd_textremovespaces(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto fnResult = (const char *)TextRemoveSpaces(text); + v_setstr(retval, fnResult); + return 1; +} + +// +// Replace text string with new string // static int cmd_textreplace(int argc, slib_par_t *params, var_t *retval) { auto text = get_param_str(argc, params, 0, 0); - auto replace = get_param_str(argc, params, 1, 0); - auto by = get_param_str(argc, params, 2, 0); - auto fnResult = (const char *)TextReplace(text, replace, by); + auto search = get_param_str(argc, params, 1, 0); + auto replacement = get_param_str(argc, params, 2, 0); + auto fnResult = (const char *)TextReplace(text, search, replacement); + v_setstr(retval, fnResult); + return 1; +} + +// +// Replace text string with new string, memory must be MemFree() +// +static int cmd_textreplacealloc(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto search = get_param_str(argc, params, 1, 0); + auto replacement = get_param_str(argc, params, 2, 0); + auto fnResult = (const char *)TextReplaceAlloc(text, search, replacement); + v_setstr(retval, fnResult); + return 1; +} + +// +// Replace text between two specific strings +// +static int cmd_textreplacebetween(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto begin = get_param_str(argc, params, 1, 0); + auto end = get_param_str(argc, params, 2, 0); + auto replacement = get_param_str(argc, params, 3, 0); + auto fnResult = (const char *)TextReplaceBetween(text, begin, end, replacement); + v_setstr(retval, fnResult); + return 1; +} + +// +// Replace text between two specific strings, memory must be MemFree() +// +static int cmd_textreplacebetweenalloc(int argc, slib_par_t *params, var_t *retval) { + auto text = get_param_str(argc, params, 0, 0); + auto begin = get_param_str(argc, params, 1, 0); + auto end = get_param_str(argc, params, 2, 0); + auto replacement = get_param_str(argc, params, 3, 0); + auto fnResult = (const char *)TextReplaceBetweenAlloc(text, begin, end, replacement); v_setstr(retval, fnResult); return 1; } diff --git a/raylib/main.cpp b/raylib/main.cpp index 8bed447..ac63334 100644 --- a/raylib/main.cpp +++ b/raylib/main.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "robin-hood-hashing/src/include/robin_hood.h" #include "include/var.h" @@ -359,8 +360,7 @@ static FilePathList get_param_filepathlist(int argc, slib_par_t *params, int n) if (is_param_array(argc, params, n)) { var_p_t array = params[n].var_p; result.count = v_asize(array); - result.capacity = result.count; - result.paths = (char **)malloc(result.capacity * sizeof(char *)); + result.paths = (char **)malloc(result.count * sizeof(char *)); for (unsigned index = 0; index < result.count; index++) { result.paths[index] = (char *)malloc(MAX_FILEPATH_LENGTH * sizeof(char)); var_p_t elem = v_elem(array, index); @@ -749,7 +749,7 @@ static void v_setmodel(var_t *var, Model &model) { map_init_id(var, id, CLS_MODELMAP); v_setint(map_add_var(var, "meshCount", 0), model.meshCount); v_setint(map_add_var(var, "materialCount", 0), model.materialCount); - v_setint(map_add_var(var, "boneCount", 0), model.boneCount); + v_setint(map_add_var(var, "boneCount", 0), model.skeleton.boneCount); } static void v_setmusic(var_t *var, Music &music) { @@ -769,19 +769,19 @@ static void v_setmodel_animation(var_t *var, ModelAnimation *anims, int animsCou _modelAnimationMap[id] = anims[i]; map_init_id(v_anim, id, CLS_MODELANIMATIONMAP); - int frameCount = anims[i].frameCount; + int keyframeCount = anims[i].keyframeCount; int boneCount = anims[i].boneCount; - map_add_var(v_anim, "frameCount", frameCount); + map_add_var(v_anim, "keyframeCount", keyframeCount); map_add_var(v_anim, "boneCount", boneCount); var_t *v_framePoses = map_add_var(v_anim, "framePoses", 0); - v_tomatrix(v_framePoses, frameCount, boneCount); - for (int frame = 0; frame < frameCount; frame++) { + v_tomatrix(v_framePoses, keyframeCount, boneCount); + for (int frame = 0; frame < keyframeCount; frame++) { for (int bone = 0; bone < boneCount; bone++) { var_t *v_transform = v_elem(v_framePoses, ((boneCount * frame) + bone)); map_init(v_transform); - v_setvec3(map_add_var(v_transform, "translation", 0), anims[0].framePoses[frame][bone].translation); - v_setvec3(map_add_var(v_transform, "scale", 0), anims[0].framePoses[frame][bone].scale); + v_setvec3(map_add_var(v_transform, "translation", 0), anims[0].keyframePoses[frame][bone].translation); + v_setvec3(map_add_var(v_transform, "scale", 0), anims[0].keyframePoses[frame][bone].scale); } } } @@ -1937,7 +1937,7 @@ SBLIB_API int sblib_func_exec(int index, int argc, slib_par_t *params, var_t *re return result; } -SBLIB_API void sblib_free(int cls_id, int id) { +SBLIB_API int sblib_free(int cls_id, int id) { if (id != -1) { switch (cls_id) { case CLS_AUDIOSTREAM: @@ -1978,7 +1978,7 @@ SBLIB_API void sblib_free(int cls_id, int id) { break; case CLS_MODELANIMATIONMAP: if (_modelAnimationMap.find(id) != _modelAnimationMap.end()) { - UnloadModelAnimation(_modelAnimationMap.at(id)); + UnloadModelAnimations(&_modelAnimationMap.at(id), 1); _modelAnimationMap.erase(id); } break; @@ -2030,6 +2030,7 @@ SBLIB_API void sblib_free(int cls_id, int id) { break; } } + return 0; } SBLIB_API void sblib_close(void) { @@ -2094,3 +2095,8 @@ SBLIB_API void sblib_close(void) { _waveMap.clear(); } } + +SBLIB_API int sblib_has_window_ui(void) { + // module creates a UI in a new window + return 1; +} diff --git a/raylib/mkraylib.bas b/raylib/mkraylib.bas index ee64008..e37d4c5 100644 --- a/raylib/mkraylib.bas +++ b/raylib/mkraylib.bas @@ -2,7 +2,7 @@ rem rem generate a skelton main.cpp from json input rem -tload "raylib/parser/raylib_api.json", s, 1 +tload "raylib/tools/rlparser/output/raylib_api.json", s, 1 api = array(s) func comparator(l, r) diff --git a/raylib/mkreadme.bas b/raylib/mkreadme.bas index 56230b9..0f98b50 100644 --- a/raylib/mkreadme.bas +++ b/raylib/mkreadme.bas @@ -20,7 +20,7 @@ load("main.cpp") load("proc-def.h") load("func-def.h") -tload "raylib/parser/raylib_api.json", s, 1 +tload "raylib/tools/rlparser/output/raylib_api.json", s, 1 api = array(s) functions = {} for fun in api("functions") diff --git a/raylib/proc-def.h b/raylib/proc-def.h index f509533..fe4a164 100644 --- a/raylib/proc-def.h +++ b/raylib/proc-def.h @@ -19,7 +19,7 @@ {6, 6, "DRAWCAPSULEWIRES", cmd_drawcapsulewires}, {4, 4, "DRAWCIRCLE", cmd_drawcircle}, {5, 5, "DRAWCIRCLE3D", cmd_drawcircle3d}, - {5, 5, "DRAWCIRCLEGRADIENT", cmd_drawcirclegradient}, + {4, 4, "DRAWCIRCLEGRADIENT", cmd_drawcirclegradient}, {4, 4, "DRAWCIRCLELINES", cmd_drawcirclelines}, {3, 3, "DRAWCIRCLELINESV", cmd_drawcirclelinesv}, {6, 6, "DRAWCIRCLESECTOR", cmd_drawcirclesector}, @@ -35,18 +35,19 @@ {6, 6, "DRAWCYLINDERWIRESEX", cmd_drawcylinderwiresex}, {5, 5, "DRAWELLIPSE", cmd_drawellipse}, {5, 5, "DRAWELLIPSELINES", cmd_drawellipselines}, + {4, 4, "DRAWELLIPSELINESV", cmd_drawellipselinesv}, + {4, 4, "DRAWELLIPSEV", cmd_drawellipsev}, {2, 2, "DRAWFPS", cmd_drawfps}, {2, 2, "DRAWGRID", cmd_drawgrid}, {5, 5, "DRAWLINE", cmd_drawline}, {3, 3, "DRAWLINE3D", cmd_drawline3d}, {4, 4, "DRAWLINEBEZIER", cmd_drawlinebezier}, + {5, 5, "DRAWLINEDASHED", cmd_drawlinedashed}, {4, 4, "DRAWLINEEX", cmd_drawlineex}, {3, 3, "DRAWLINESTRIP", cmd_drawlinestrip}, {3, 3, "DRAWLINEV", cmd_drawlinev}, {4, 4, "DRAWMODEL", cmd_drawmodel}, {6, 6, "DRAWMODELEX", cmd_drawmodelex}, - {4, 4, "DRAWMODELPOINTS", cmd_drawmodelpoints}, - {6, 6, "DRAWMODELPOINTSEX", cmd_drawmodelpointsex}, {4, 4, "DRAWMODELWIRES", cmd_drawmodelwires}, {6, 6, "DRAWMODELWIRESEX", cmd_drawmodelwiresex}, {3, 3, "DRAWPIXEL", cmd_drawpixel}, @@ -98,6 +99,7 @@ {4, 4, "DRAWTRIANGLE", cmd_drawtriangle}, {4, 4, "DRAWTRIANGLE3D", cmd_drawtriangle3d}, {3, 3, "DRAWTRIANGLEFAN", cmd_drawtrianglefan}, + {6, 6, "DRAWTRIANGLEGRADIENT", cmd_drawtrianglegradient}, {4, 4, "DRAWTRIANGLELINES", cmd_drawtrianglelines}, {3, 3, "DRAWTRIANGLESTRIP", cmd_drawtrianglestrip}, {3, 3, "DRAWTRIANGLESTRIP3D", cmd_drawtrianglestrip3d}, @@ -139,14 +141,15 @@ {4, 4, "IMAGEDRAWPIXEL", cmd_imagedrawpixel}, {3, 3, "IMAGEDRAWPIXELV", cmd_imagedrawpixelv}, {6, 6, "IMAGEDRAWRECTANGLE", cmd_imagedrawrectangle}, - {4, 4, "IMAGEDRAWRECTANGLELINES", cmd_imagedrawrectanglelines}, + {6, 6, "IMAGEDRAWRECTANGLELINES", cmd_imagedrawrectanglelines}, + {4, 4, "IMAGEDRAWRECTANGLELINESEX", cmd_imagedrawrectanglelinesex}, {3, 3, "IMAGEDRAWRECTANGLEREC", cmd_imagedrawrectanglerec}, {4, 4, "IMAGEDRAWRECTANGLEV", cmd_imagedrawrectanglev}, {6, 6, "IMAGEDRAWTEXT", cmd_imagedrawtext}, {7, 7, "IMAGEDRAWTEXTEX", cmd_imagedrawtextex}, {5, 5, "IMAGEDRAWTRIANGLE", cmd_imagedrawtriangle}, - {7, 7, "IMAGEDRAWTRIANGLEEX", cmd_imagedrawtriangleex}, {4, 4, "IMAGEDRAWTRIANGLEFAN", cmd_imagedrawtrianglefan}, + {7, 7, "IMAGEDRAWTRIANGLEGRADIENT", cmd_imagedrawtrianglegradient}, {5, 5, "IMAGEDRAWTRIANGLELINES", cmd_imagedrawtrianglelines}, {4, 4, "IMAGEDRAWTRIANGLESTRIP", cmd_imagedrawtrianglestrip}, {1, 1, "IMAGEFLIPHORIZONTAL", cmd_imagefliphorizontal}, @@ -247,7 +250,6 @@ {1, 1, "UNLOADIMAGEPALETTE", cmd_unloadimagepalette}, {1, 1, "UNLOADMESH", cmd_unloadmesh}, {1, 1, "UNLOADMODEL", cmd_unloadmodel}, - {1, 1, "UNLOADMODELANIMATION", cmd_unloadmodelanimation}, {2, 2, "UNLOADMODELANIMATIONS", cmd_unloadmodelanimations}, {1, 1, "UNLOADMUSICSTREAM", cmd_unloadmusicstream}, {0, 0, "UNLOADRANDOMSEQUENCE", cmd_unloadrandomsequence}, @@ -262,7 +264,7 @@ {3, 3, "UPDATEAUDIOSTREAM", cmd_updateaudiostream}, {5, 5, "UPDATEMESHBUFFER", cmd_updatemeshbuffer}, {3, 3, "UPDATEMODELANIMATION", cmd_updatemodelanimation}, - {3, 3, "UPDATEMODELANIMATIONBONES", cmd_updatemodelanimationbones}, + {6, 6, "UPDATEMODELANIMATIONEX", cmd_updatemodelanimationex}, {1, 1, "UPDATEMUSICSTREAM", cmd_updatemusicstream}, {3, 3, "UPDATESOUND", cmd_updatesound}, {3, 3, "UPDATETEXTUREREC", cmd_updatetexturerec}, diff --git a/raylib/proc.h b/raylib/proc.h index ce6790c..68e4e82 100644 --- a/raylib/proc.h +++ b/raylib/proc.h @@ -8,7 +8,7 @@ static int cmd_beginblendmode(int argc, slib_par_t *params, var_t *retval) { } // -// Setup canvas (framebuffer) to start drawing +// Begin canvas (framebuffer) drawing // static int cmd_begindrawing(int argc, slib_par_t *params, var_t *retval) { BeginDrawing(); @@ -70,7 +70,7 @@ static int cmd_begintexturemode(int argc, slib_par_t *params, var_t *retval) { } // -// Set background color (framebuffer clear color) +// Clear background (framebuffer) to color // static int cmd_clearbackground(int argc, slib_par_t *params, var_t *retval) { auto color = get_param_color(argc, params, 0); @@ -104,7 +104,7 @@ static int cmd_closewindow(int argc, slib_par_t *params, var_t *retval) { } // -// Disables cursor (lock cursor) +// Disable cursor (lock cursor) // static int cmd_disablecursor(int argc, slib_par_t *params, var_t *retval) { DisableCursor(); @@ -198,10 +198,10 @@ static int cmd_drawcapsule(int argc, slib_par_t *params, var_t *retval) { auto startPos = get_param_vec3(argc, params, 0); auto endPos = get_param_vec3(argc, params, 1); auto radius = get_param_num(argc, params, 2, 0); - auto slices = get_param_int(argc, params, 3, 0); - auto rings = get_param_int(argc, params, 4, 0); + auto rings = get_param_int(argc, params, 3, 0); + auto slices = get_param_int(argc, params, 4, 0); auto color = get_param_color(argc, params, 5); - DrawCapsule(startPos, endPos, radius, slices, rings, color); + DrawCapsule(startPos, endPos, radius, rings, slices, color); return 1; } @@ -212,10 +212,10 @@ static int cmd_drawcapsulewires(int argc, slib_par_t *params, var_t *retval) { auto startPos = get_param_vec3(argc, params, 0); auto endPos = get_param_vec3(argc, params, 1); auto radius = get_param_num(argc, params, 2, 0); - auto slices = get_param_int(argc, params, 3, 0); - auto rings = get_param_int(argc, params, 4, 0); + auto rings = get_param_int(argc, params, 3, 0); + auto slices = get_param_int(argc, params, 4, 0); auto color = get_param_color(argc, params, 5); - DrawCapsuleWires(startPos, endPos, radius, slices, rings, color); + DrawCapsuleWires(startPos, endPos, radius, rings, slices, color); return 1; } @@ -248,12 +248,11 @@ static int cmd_drawcircle3d(int argc, slib_par_t *params, var_t *retval) { // Draw a gradient-filled circle // static int cmd_drawcirclegradient(int argc, slib_par_t *params, var_t *retval) { - auto centerX = get_param_int(argc, params, 0, 0); - auto centerY = get_param_int(argc, params, 1, 0); - auto radius = get_param_num(argc, params, 2, 0); - auto inner = get_param_color(argc, params, 3); - auto outer = get_param_color(argc, params, 4); - DrawCircleGradient(centerX, centerY, radius, inner, outer); + auto center = get_param_vec2(argc, params, 0); + auto radius = get_param_num(argc, params, 1, 0); + auto inner = get_param_color(argc, params, 2); + auto outer = get_param_color(argc, params, 3); + DrawCircleGradient(center, radius, inner, outer); return 1; } @@ -417,9 +416,9 @@ static int cmd_drawcylinderwiresex(int argc, slib_par_t *params, var_t *retval) auto endPos = get_param_vec3(argc, params, 1); auto startRadius = get_param_num(argc, params, 2, 0); auto endRadius = get_param_num(argc, params, 3, 0); - auto sides = get_param_int(argc, params, 4, 0); + auto slices = get_param_int(argc, params, 4, 0); auto color = get_param_color(argc, params, 5); - DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, sides, color); + DrawCylinderWiresEx(startPos, endPos, startRadius, endRadius, slices, color); return 1; } @@ -449,6 +448,30 @@ static int cmd_drawellipselines(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Draw ellipse outline (Vector version) +// +static int cmd_drawellipselinesv(int argc, slib_par_t *params, var_t *retval) { + auto center = get_param_vec2(argc, params, 0); + auto radiusH = get_param_num(argc, params, 1, 0); + auto radiusV = get_param_num(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + DrawEllipseLinesV(center, radiusH, radiusV, color); + return 1; +} + +// +// Draw ellipse (Vector version) +// +static int cmd_drawellipsev(int argc, slib_par_t *params, var_t *retval) { + auto center = get_param_vec2(argc, params, 0); + auto radiusH = get_param_num(argc, params, 1, 0); + auto radiusV = get_param_num(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + DrawEllipseV(center, radiusH, radiusV, color); + return 1; +} + // // Draw current FPS // @@ -505,6 +528,19 @@ static int cmd_drawlinebezier(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Draw a dashed line +// +static int cmd_drawlinedashed(int argc, slib_par_t *params, var_t *retval) { + auto startPos = get_param_vec2(argc, params, 0); + auto endPos = get_param_vec2(argc, params, 1); + auto dashSize = get_param_int(argc, params, 2, 0); + auto spaceSize = get_param_int(argc, params, 3, 0); + auto color = get_param_color(argc, params, 4); + DrawLineDashed(startPos, endPos, dashSize, spaceSize, color); + return 1; +} + // // Draw a line (using triangles/quads) // @@ -577,44 +613,6 @@ static int cmd_drawmodelex(int argc, slib_par_t *params, var_t *retval) { return result; } -// -// Draw a model as points -// -static int cmd_drawmodelpoints(int argc, slib_par_t *params, var_t *retval) { - int result; - int model_id = get_model_id(argc, params, 0, retval); - if (model_id != -1) { - auto position = get_param_vec3(argc, params, 1); - auto scale = get_param_num(argc, params, 2, 0); - auto tint = get_param_color(argc, params, 3); - DrawModelPoints(_modelMap.at(model_id), position, scale, tint); - result = 1; - } else { - result = 0; - } - return result; -} - -// -// Draw a model as points with extended parameters -// -static int cmd_drawmodelpointsex(int argc, slib_par_t *params, var_t *retval) { - int result; - int model_id = get_model_id(argc, params, 0, retval); - if (model_id != -1) { - auto position = get_param_vec3(argc, params, 1); - auto rotationAxis = get_param_vec3(argc, params, 2); - auto rotationAngle = get_param_num(argc, params, 3, 0); - auto scale = get_param_vec3(argc, params, 4); - auto tint = get_param_color(argc, params, 5); - DrawModelPointsEx(_modelMap.at(model_id), position, rotationAxis, rotationAngle, scale, tint); - result = 1; - } else { - result = 0; - } - return result; -} - // // Draw a model wires (with texture if set) // @@ -696,7 +694,7 @@ static int cmd_drawpoint3d(int argc, slib_par_t *params, var_t *retval) { } // -// Draw a regular polygon (Vector version) +// Draw a polygon of n sides // static int cmd_drawpoly(int argc, slib_par_t *params, var_t *retval) { auto center = get_param_vec2(argc, params, 0); @@ -765,9 +763,9 @@ static int cmd_drawrectanglegradientex(int argc, slib_par_t *params, var_t *retv auto rec = get_param_rect(argc, params, 0); auto topLeft = get_param_color(argc, params, 1); auto bottomLeft = get_param_color(argc, params, 2); - auto topRight = get_param_color(argc, params, 3); - auto bottomRight = get_param_color(argc, params, 4); - DrawRectangleGradientEx(rec, topLeft, bottomLeft, topRight, bottomRight); + auto bottomRight = get_param_color(argc, params, 3); + auto topRight = get_param_color(argc, params, 4); + DrawRectangleGradientEx(rec, topLeft, bottomLeft, bottomRight, topRight); return 1; } @@ -870,7 +868,7 @@ static int cmd_drawrectangleroundedlines(int argc, slib_par_t *params, var_t *re } // -// Draw rectangle with rounded edges outline +// Draw rectangle lines with rounded edges outline // static int cmd_drawrectangleroundedlinesex(int argc, slib_par_t *params, var_t *retval) { auto rec = get_param_rect(argc, params, 0); @@ -1120,7 +1118,7 @@ static int cmd_drawtextcodepoint(int argc, slib_par_t *params, var_t *retval) { } // -// Draw multiple character (codepoint) +// Draw multiple characters (codepoint) // static int cmd_drawtextcodepoints(int argc, slib_par_t *params, var_t *retval) { int result; @@ -1220,7 +1218,7 @@ static int cmd_drawtextureex(int argc, slib_par_t *params, var_t *retval) { } // -// Draws a texture (or part of it) that stretches or shrinks nicely +// Draw a texture (or part of it) that stretches or shrinks nicely // static int cmd_drawtexturenpatch(int argc, slib_par_t *params, var_t *retval) { int result; @@ -1329,6 +1327,20 @@ static int cmd_drawtrianglefan(int argc, slib_par_t *params, var_t *retval) { return 1; } +// +// Draw triangle with interpolated colors (vertex in counter-clockwise order!) +// +static int cmd_drawtrianglegradient(int argc, slib_par_t *params, var_t *retval) { + auto v1 = get_param_vec2(argc, params, 0); + auto v2 = get_param_vec2(argc, params, 1); + auto v3 = get_param_vec2(argc, params, 2); + auto c1 = get_param_color(argc, params, 3); + auto c2 = get_param_color(argc, params, 4); + auto c3 = get_param_color(argc, params, 5); + DrawTriangleGradient(v1, v2, v3, c1, c2, c3); + return 1; +} + // // Draw triangle outline (vertex in counter-clockwise order!) // @@ -1364,7 +1376,7 @@ static int cmd_drawtrianglestrip3d(int argc, slib_par_t *params, var_t *retval) } // -// Enables cursor (unlock cursor) +// Enable cursor (unlock cursor) // static int cmd_enablecursor(int argc, slib_par_t *params, var_t *retval) { EnableCursor(); @@ -1388,7 +1400,7 @@ static int cmd_endblendmode(int argc, slib_par_t *params, var_t *retval) { } // -// End canvas drawing and swap buffers (double buffering) +// End canvas (framebuffer) drawing and swap buffers (double buffering) // static int cmd_enddrawing(int argc, slib_par_t *params, var_t *retval) { EndDrawing(); @@ -1396,7 +1408,7 @@ static int cmd_enddrawing(int argc, slib_par_t *params, var_t *retval) { } // -// Ends 2D mode with custom camera +// End 2D mode with custom camera // static int cmd_endmode2d(int argc, slib_par_t *params, var_t *retval) { EndMode2D(); @@ -1404,7 +1416,7 @@ static int cmd_endmode2d(int argc, slib_par_t *params, var_t *retval) { } // -// Ends 3D mode and returns to default 2D orthographic mode +// End 3D mode and returns to default 2D orthographic mode // static int cmd_endmode3d(int argc, slib_par_t *params, var_t *retval) { EndMode3D(); @@ -1428,7 +1440,7 @@ static int cmd_endshadermode(int argc, slib_par_t *params, var_t *retval) { } // -// Ends drawing to render texture +// End drawing to render texture // static int cmd_endtexturemode(int argc, slib_par_t *params, var_t *retval) { EndTextureMode(); @@ -1462,7 +1474,7 @@ static int cmd_gentexturemipmaps(int argc, slib_par_t *params, var_t *retval) { } // -// Hides cursor +// Hide cursor // static int cmd_hidecursor(int argc, slib_par_t *params, var_t *retval) { HideCursor(); @@ -1588,7 +1600,7 @@ static int cmd_imagecolorcontrast(int argc, slib_par_t *params, var_t *retval) { int result; int image_id = get_image_id(argc, params, 0, retval); if (image_id != -1) { - auto contrast = get_param_num(argc, params, 1, 0); + auto contrast = get_param_int(argc, params, 1, 0); ImageColorContrast(&_imageMap.at(image_id), contrast); result = 1; } else { @@ -1904,13 +1916,33 @@ static int cmd_imagedrawrectangle(int argc, slib_par_t *params, var_t *retval) { // Draw rectangle lines within an image // static int cmd_imagedrawrectanglelines(int argc, slib_par_t *params, var_t *retval) { + int result; + int dst_id = get_image_id(argc, params, 0, retval); + if (dst_id != -1) { + auto posX = get_param_int(argc, params, 1, 0); + auto posY = get_param_int(argc, params, 2, 0); + auto width = get_param_int(argc, params, 3, 0); + auto height = get_param_int(argc, params, 4, 0); + auto color = get_param_color(argc, params, 5); + ImageDrawRectangleLines(&_imageMap.at(dst_id), posX, posY, width, height, color); + result = 1; + } else { + result = 0; + } + return result; +} + +// +// Draw rectangle lines within an image with extended parameters +// +static int cmd_imagedrawrectanglelinesex(int argc, slib_par_t *params, var_t *retval) { int result; int dst_id = get_image_id(argc, params, 0, retval); if (dst_id != -1) { auto rec = get_param_rect(argc, params, 1); auto thick = get_param_int(argc, params, 2, 0); auto color = get_param_color(argc, params, 3); - ImageDrawRectangleLines(&_imageMap.at(dst_id), rec, thick, color); + ImageDrawRectangleLinesEx(&_imageMap.at(dst_id), rec, thick, color); result = 1; } else { result = 0; @@ -2014,19 +2046,16 @@ static int cmd_imagedrawtriangle(int argc, slib_par_t *params, var_t *retval) { } // -// Draw triangle with interpolated colors within an image +// Draw a triangle fan defined by points within an image (first vertex is the center) // -static int cmd_imagedrawtriangleex(int argc, slib_par_t *params, var_t *retval) { +static int cmd_imagedrawtrianglefan(int argc, slib_par_t *params, var_t *retval) { int result; int dst_id = get_image_id(argc, params, 0, retval); if (dst_id != -1) { - auto v1 = get_param_vec2(argc, params, 1); - auto v2 = get_param_vec2(argc, params, 2); - auto v3 = get_param_vec2(argc, params, 3); - auto c1 = get_param_color(argc, params, 4); - auto c2 = get_param_color(argc, params, 5); - auto c3 = get_param_color(argc, params, 6); - ImageDrawTriangleEx(&_imageMap.at(dst_id), v1, v2, v3, c1, c2, c3); + auto points = (Vector2 *)get_param_vec2_array(argc, params, 1); + auto pointCount = get_param_int(argc, params, 2, 0); + auto color = get_param_color(argc, params, 3); + ImageDrawTriangleFan(&_imageMap.at(dst_id), points, pointCount, color); result = 1; } else { result = 0; @@ -2035,16 +2064,19 @@ static int cmd_imagedrawtriangleex(int argc, slib_par_t *params, var_t *retval) } // -// Draw a triangle fan defined by points within an image (first vertex is the center) +// Draw triangle with interpolated colors within an image // -static int cmd_imagedrawtrianglefan(int argc, slib_par_t *params, var_t *retval) { +static int cmd_imagedrawtrianglegradient(int argc, slib_par_t *params, var_t *retval) { int result; int dst_id = get_image_id(argc, params, 0, retval); if (dst_id != -1) { - auto points = (Vector2 *)get_param_vec2_array(argc, params, 1); - auto pointCount = get_param_int(argc, params, 2, 0); - auto color = get_param_color(argc, params, 3); - ImageDrawTriangleFan(&_imageMap.at(dst_id), points, pointCount, color); + auto v1 = get_param_vec2(argc, params, 1); + auto v2 = get_param_vec2(argc, params, 2); + auto v3 = get_param_vec2(argc, params, 3); + auto c1 = get_param_color(argc, params, 4); + auto c2 = get_param_color(argc, params, 5); + auto c3 = get_param_color(argc, params, 6); + ImageDrawTriangleGradient(&_imageMap.at(dst_id), v1, v2, v3, c1, c2, c3); result = 1; } else { result = 0; @@ -2427,7 +2459,7 @@ static int cmd_pollinputevents(int argc, slib_par_t *params, var_t *retval) { } // -// Set window state: not minimized/maximized +// Restore window from being minimized/maximized // static int cmd_restorewindow(int argc, slib_par_t *params, var_t *retval) { RestoreWindow(); @@ -2505,7 +2537,7 @@ static int cmd_setaudiostreambuffersizedefault(int argc, slib_par_t *params, var } // -// Set pan for audio stream (0.5 is centered) +// Set pan for audio stream (-1.0 left, 0.0 center, 1.0 right) // static int cmd_setaudiostreampan(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2586,7 +2618,7 @@ static int cmd_setclipboardtext(int argc, slib_par_t *params, var_t *retval) { } // -// Setup init configuration flags (view FLAGS) +// Set up init configuration flags (view FLAGS) // static int cmd_setconfigflags(int argc, slib_par_t *params, var_t *retval) { auto flags = get_param_int(argc, params, 0, 0); @@ -2684,7 +2716,7 @@ static int cmd_setmousescale(int argc, slib_par_t *params, var_t *retval) { } // -// Set pan for a music (0.5 is center) +// Set pan for music (-1.0 left, 0.0 center, 1.0 right) // static int cmd_setmusicpan(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2700,7 +2732,7 @@ static int cmd_setmusicpan(int argc, slib_par_t *params, var_t *retval) { } // -// Set pitch for a music (1.0 is base level) +// Set pitch for music (1.0 is base level) // static int cmd_setmusicpitch(int argc, slib_par_t *params, var_t *retval) { int result; @@ -2815,7 +2847,7 @@ static int cmd_setshapestexture(int argc, slib_par_t *params, var_t *retval) { } // -// Set pan for a sound (0.5 is center) +// Set pan for a sound (-1.0 left, 0.0 center, 1.0 right) // static int cmd_setsoundpan(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3037,7 +3069,7 @@ static int cmd_setwindowtitle(int argc, slib_par_t *params, var_t *retval) { } // -// Shows cursor +// Show cursor // static int cmd_showcursor(int argc, slib_par_t *params, var_t *retval) { ShowCursor(); @@ -3123,7 +3155,7 @@ static int cmd_takescreenshot(int argc, slib_par_t *params, var_t *retval) { } // -// Append text at specific position and move cursor! +// Append text at specific position and move cursor // static int cmd_textappend(int argc, slib_par_t *params, var_t *retval) { auto text = (char *)get_param_str(argc, params, 0, 0); @@ -3299,22 +3331,6 @@ static int cmd_unloadmodel(int argc, slib_par_t *params, var_t *retval) { return result; } -// -// Unload animation data -// -static int cmd_unloadmodelanimation(int argc, slib_par_t *params, var_t *retval) { - int result; - int anim_id = get_model_animation_id(argc, params, 0, retval); - if (anim_id != -1) { - UnloadModelAnimation(_modelAnimationMap.at(anim_id)); - _modelAnimationMap.erase(anim_id); - result = 1; - } else { - result = 0; - } - return result; -} - // // Unload animation array data // @@ -3392,7 +3408,7 @@ static int cmd_unloadsound(int argc, slib_par_t *params, var_t *retval) { } // -// Unload a sound alias (does not deallocate sample data) +// Unload sound alias (does not deallocate sample data) // static int cmd_unloadsoundalias(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3494,14 +3510,14 @@ static int cmd_updatemeshbuffer(int argc, slib_par_t *params, var_t *retval) { } // -// Update model animation pose (CPU) +// Update model animation pose (vertex buffers and bone matrices) // static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) { int result; int model_id = get_model_id(argc, params, 0, retval); int anim_id = get_model_animation_id(argc, params, 1, retval); if (model_id != -1 && anim_id != -1) { - auto frame = get_param_int(argc, params, 2, 0); + auto frame = get_param_num(argc, params, 2, 0); UpdateModelAnimation(_modelMap.at(model_id), _modelAnimationMap.at(anim_id), frame); result = 1; } else { @@ -3511,15 +3527,18 @@ static int cmd_updatemodelanimation(int argc, slib_par_t *params, var_t *retval) } // -// Update model animation mesh bone matrices (GPU skinning) +// Update model animation pose, blending two animations // -static int cmd_updatemodelanimationbones(int argc, slib_par_t *params, var_t *retval) { +static int cmd_updatemodelanimationex(int argc, slib_par_t *params, var_t *retval) { int result; int model_id = get_model_id(argc, params, 0, retval); - int anim_id = get_model_animation_id(argc, params, 1, retval); - if (model_id != -1 && anim_id != -1) { - auto frame = get_param_int(argc, params, 2, 0); - UpdateModelAnimationBones(_modelMap.at(model_id), _modelAnimationMap.at(anim_id), frame); + int anima_id = get_model_animation_id(argc, params, 1, retval); + int animb_id = get_model_animation_id(argc, params, 3, retval); + if (model_id != -1 && anima_id != -1 && animb_id != -1) { + auto frameA = get_param_num(argc, params, 2, 0); + auto frameB = get_param_num(argc, params, 4, 0); + auto blend = get_param_num(argc, params, 5, 0); + UpdateModelAnimationEx(_modelMap.at(model_id), _modelAnimationMap.at(anima_id), frameA, _modelAnimationMap.at(animb_id), frameB, blend); result = 1; } else { result = 0; @@ -3528,7 +3547,7 @@ static int cmd_updatemodelanimationbones(int argc, slib_par_t *params, var_t *re } // -// Updates buffers for music streaming +// Update buffers for music streaming // static int cmd_updatemusicstream(int argc, slib_par_t *params, var_t *retval) { int result; @@ -3543,15 +3562,15 @@ static int cmd_updatemusicstream(int argc, slib_par_t *params, var_t *retval) { } // -// Update sound buffer with new data +// Update sound buffer with new data (default data format: 32 bit float, stereo) // static int cmd_updatesound(int argc, slib_par_t *params, var_t *retval) { int result; int sound_id = get_sound_id(argc, params, 0, retval); if (sound_id != -1) { auto data = (const void *)get_param_int_t(argc, params, 1, 0); - auto sampleCount = get_param_int(argc, params, 2, 0); - UpdateSound(_soundMap.at(sound_id), data, sampleCount); + auto frameCount = get_param_int(argc, params, 2, 0); + UpdateSound(_soundMap.at(sound_id), data, frameCount); result = 1; } else { result = 0; @@ -3560,7 +3579,7 @@ static int cmd_updatesound(int argc, slib_par_t *params, var_t *retval) { } // -// Update GPU texture rectangle with new data +// Update GPU texture rectangle with new data (pixels and rec should fit in texture) // static int cmd_updatetexturerec(int argc, slib_par_t *params, var_t *retval) { int result; diff --git a/raylib/raygui b/raylib/raygui index 9a95871..6d2b28f 160000 --- a/raylib/raygui +++ b/raylib/raygui @@ -1 +1 @@ -Subproject commit 9a95871701a5fc63bea35eab73fef6414e048b73 +Subproject commit 6d2b28ff748158be0d63d07988d5d0672905dedf diff --git a/raylib/raylib b/raylib/raylib index eb8a343..95bfa19 160000 --- a/raylib/raylib +++ b/raylib/raylib @@ -1 +1 @@ -Subproject commit eb8a343e313967a51cb302ac9bb1206a05727d13 +Subproject commit 95bfa196fdfb737356b8a09ab2944e765a71280a diff --git a/websocket/main.cpp b/websocket/main.cpp index a794e57..2f27efb 100644 --- a/websocket/main.cpp +++ b/websocket/main.cpp @@ -193,9 +193,9 @@ static void client_handler(mg_connection *conn, int event, void *eventData) { static void set_session(var_p_t var, Session *session, mg_connection *conn) { session->setConnection(conn); map_init_id(var, conn->id); - v_setstr(map_add_var(var, "local_ip", 0), (const char *)conn->loc.ip); + v_setstr(map_add_var(var, "local_ip", 0), (const char *)conn->loc.addr.ip); v_setint(map_add_var(var, "local_port", 0), conn->loc.port); - v_setstr(map_add_var(var, "remote_ip", 0), (const char *)conn->rem.ip); + v_setstr(map_add_var(var, "remote_ip", 0), (const char *)conn->rem.addr.ip); v_setint(map_add_var(var, "remote_port", 0), conn->rem.port); } diff --git a/websocket/mongoose b/websocket/mongoose index 55bc610..eee8c70 160000 --- a/websocket/mongoose +++ b/websocket/mongoose @@ -1 +1 @@ -Subproject commit 55bc6105e148633ddc65bddbdb307f1477c0fc01 +Subproject commit eee8c7077c031f22469ea3adfcc69fc6d86c479a