diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f9fe34..d88bac1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.44 LANGUAGES C) +project(fun VERSION 0.37.45 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -474,6 +474,38 @@ if(M_LIB) target_link_libraries(fun_core PUBLIC ${M_LIB}) endif() +# Optional Notcurses TUI support (per-opcode files under src/vm/notcurses) +option(FUN_WITH_NOTCURSES "Enable Notcurses TUI support" OFF) +set(NOTCURSES_INCLUDE_DIRS "") +set(NOTCURSES_LINK_LIBS "") +if(FUN_WITH_NOTCURSES) + message(STATUS "FUN_WITH_NOTCURSES=ON: enabling Notcurses TUI ops") + add_definitions(-DFUN_WITH_NOTCURSES) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + # Prefer full 'notcurses', fall back to 'notcurses-core' + pkg_check_modules(NOTCURSES QUIET notcurses) + if(NOT NOTCURSES_FOUND) + pkg_check_modules(NOTCURSES QUIET notcurses-core) + endif() + endif() + if(NOTCURSES_FOUND) + list(APPEND NOTCURSES_INCLUDE_DIRS ${NOTCURSES_INCLUDE_DIRS} ${NOTCURSES_INCLUDE_DIRS}) + list(APPEND NOTCURSES_LINK_LIBS ${NOTCURSES_LINK_LIBS} ${NOTCURSES_LIBRARIES}) + include_directories(${NOTCURSES_INCLUDE_DIRS}) + else() + message(FATAL_ERROR "FUN_WITH_NOTCURSES=ON but notcurses was not found via pkg-config (tried notcurses and notcurses-core). Install notcurses or configure with -DFUN_WITH_NOTCURSES=OFF") + endif() +endif() + +# notcurses include and link (if enabled) +if(NOTCURSES_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${NOTCURSES_INCLUDE_DIRS}) +endif() +if(NOTCURSES_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${NOTCURSES_LINK_LIBS}) +endif() + # Interpreter /usr/bin/fun option(FUN_WITH_REPL "Enable interactive REPL in the fun CLI" OFF) add_executable(fun diff --git a/examples/ini_diag.fun b/examples/ini_diag.fun old mode 100644 new mode 100755 diff --git a/examples/math_ceil.fun b/examples/math_ceil.fun old mode 100644 new mode 100755 diff --git a/examples/math_cos.fun b/examples/math_cos.fun old mode 100644 new mode 100755 diff --git a/examples/math_exp_log.fun b/examples/math_exp_log.fun old mode 100644 new mode 100755 diff --git a/examples/math_floor.fun b/examples/math_floor.fun old mode 100644 new mode 100755 diff --git a/examples/math_fmin_fmax.fun b/examples/math_fmin_fmax.fun old mode 100644 new mode 100755 diff --git a/examples/math_gcd_lcm.fun b/examples/math_gcd_lcm.fun old mode 100644 new mode 100755 diff --git a/examples/math_isqrt.fun b/examples/math_isqrt.fun old mode 100644 new mode 100755 diff --git a/examples/math_round.fun b/examples/math_round.fun old mode 100644 new mode 100755 diff --git a/examples/math_sign.fun b/examples/math_sign.fun old mode 100644 new mode 100755 diff --git a/examples/math_sin.fun b/examples/math_sin.fun old mode 100644 new mode 100755 diff --git a/examples/math_sqrt.fun b/examples/math_sqrt.fun old mode 100644 new mode 100755 diff --git a/examples/math_tan.fun b/examples/math_tan.fun old mode 100644 new mode 100755 diff --git a/examples/math_trunc.fun b/examples/math_trunc.fun old mode 100644 new mode 100755 diff --git a/examples/notcurses_clock.fun b/examples/notcurses_clock.fun new file mode 100755 index 0000000..0ae3502 --- /dev/null +++ b/examples/notcurses_clock.fun @@ -0,0 +1,42 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +// BROKEN!!! + +/* + * Notcurses clock demo: updates time every 200ms; any key to finish. + */ + +include + +n = Notcurses() +if n.init() == 0 + print("Notcurses not available. Rebuild with -DFUN_WITH_NOTCURSES=ON.") + exit(0) + +n.clear() +n.draw_text(1, 0, "Clock demo (press any key)") + +var done = 0 +while done == 0 + // current time string + var ms = time_now_ms() + var t = date_format("%Y-%m-%d %H:%M:%S", ms) + n.draw_text(3, 0, "Time: " + t) + + // poll without blocking too long + var k = n.getch(200) + if k != -1 + done = 1 + +n.shutdown() diff --git a/examples/notcurses_hello.fun b/examples/notcurses_hello.fun new file mode 100755 index 0000000..ba0c493 --- /dev/null +++ b/examples/notcurses_hello.fun @@ -0,0 +1,46 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +/* + * Minimal Notcurses hello example. + */ + +include + +n = Notcurses() + +if n.init() == 0 + print("Notcurses not available. Rebuild with -DFUN_WITH_NOTCURSES=ON.") + exit(0) + +n.clear() +n.draw_text(2, 0, "Fun + Notcurses") +n.draw_text(4, 0, "Press any key to exit...") + +// blocking until key +_ = n.getch(0) +n.shutdown() + +/* Possible output: +A TUI. + +After exit: +3 renders, 991,14µs (223,57µs min, 330,38µs avg, 531,91µs max) +3 rasters, 320,76µs (106,45µs min, 106,92µs avg, 107,72µs max) +3 writes, 198,26µs (62,54µs min, 66,09µs avg, 69,94µs max) +59B (0B min, 19B avg, 30B max) 1 input Ghpa: 0 +0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors +RGB emits:elides: def 1:38 fg 0:0 bg 0:0 +Cell emits:elides: 39:74805 (99,95%) 97,44% 0,00% 0,00% +Bmap emits:elides: 0:0 (0,00%) 0B (0,00%) SuM: 0 (0,00%) +*/ diff --git a/examples/notcurses_typing.fun b/examples/notcurses_typing.fun new file mode 100755 index 0000000..a6cb9f1 --- /dev/null +++ b/examples/notcurses_typing.fun @@ -0,0 +1,42 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +// BROKEN!!! + +/* + * Notcurses typing demo: shows last key code; ESC quits. + */ + +include + +n = Notcurses() +if n.init() == 0 + print("Notcurses not available. Rebuild with -DFUN_WITH_NOTCURSES=ON.") + exit(0) + +n.clear() +n.draw_text(1, 0, "Typing demo (ESC to quit)") + +var running = 1 +var last = -1 +while running == 1 + // poll every 100ms + var k = n.getch(100) + if k != -1 + last = k + if k == 27 // ESC + running = 0 + // draw last key code + n.draw_text(3, 0, "Last key code: " + to_string(last)) + +n.shutdown() diff --git a/examples/ripemd160_example.fun b/examples/ripemd160_example.fun old mode 100644 new mode 100755 diff --git a/lib/ui/notcurses.fun b/lib/ui/notcurses.fun new file mode 100644 index 0000000..ab8253f --- /dev/null +++ b/lib/ui/notcurses.fun @@ -0,0 +1,42 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +// Notcurses stdlib abstraction wrapping VM NC_* builtins. +// Provides a tiny OO-style helper around nc_init/nc_clear/nc_draw_text/nc_getch/nc_shutdown. +// If Fun was built without Notcurses support, init() returns 0 and methods are no-ops. + +class Notcurses() + // Initialize Notcurses core. Returns 1 on success, 0 on failure. + fun init(this) + return nc_init() + + // Clear the screen and render. Returns 0 on success, -1 on error. + fun clear(this) + return nc_clear() + + // Draw text at y, x and render. Returns 0 on success, -1 on error. + fun draw_text(this, y, x, text) + // allow flexible types + return nc_draw_text(to_number(y), to_number(x), to_string(text)) + + // Get a Unicode codepoint (int). timeout_ms<=0 blocks; >0 returns -1 if no key ready. + fun getch(this, timeout_ms) + if timeout_ms == nil + timeout_ms = 0 + return nc_getch(to_number(timeout_ms)) + + // Shut down Notcurses, restoring the terminal. + fun shutdown(this) + return nc_shutdown() + + // (banner helper removed temporarily due to parser limitations) diff --git a/src/bytecode.h b/src/bytecode.h index 7944573..c1b9434 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -261,7 +261,14 @@ typedef enum { // Min/Max variants (float-aware, C99 semantics) OP_FMIN, // pops b, a (int/float); pushes fmin(a,b) (NaN handling per C99) - OP_FMAX // pops b, a (int/float); pushes fmax(a,b) (NaN handling per C99) + OP_FMAX, // pops b, a (int/float); pushes fmax(a,b) (NaN handling per C99) + + /* Notcurses TUI (optional) */ + OP_NC_INIT, // initializes Notcurses; returns 1 on success, 0 on failure + OP_NC_SHUTDOWN, // shuts down Notcurses; returns 0 + OP_NC_CLEAR, // clears screen/plane; returns 0 + OP_NC_DRAW_TEXT, // pops text, x, y; draws; returns 0 + OP_NC_GETCH // pops timeout_ms; returns codepoint or -1 on timeout/error } OpCode; typedef struct { diff --git a/src/parser.c b/src/parser.c index d014a73..eae29c6 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1195,6 +1195,50 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* Notcurses builtins (optional) */ + if (strcmp(name, "nc_init") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "nc_init expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_NC_INIT, 0); + free(name); + return 1; + } + if (strcmp(name, "nc_shutdown") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "nc_shutdown expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_NC_SHUTDOWN, 0); + free(name); + return 1; + } + if (strcmp(name, "nc_clear") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "nc_clear expects ()"); free(name); return 0; } + bytecode_add_instruction(bc, OP_NC_CLEAR, 0); + free(name); + return 1; + } + if (strcmp(name, "nc_draw_text") == 0) { + (*pos)++; /* '(' */ + /* (y, x, text) -> push y, x, text, then opcode will pop text,x,y */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "nc_draw_text expects (y, x, text)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "nc_draw_text expects (y, x, text)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "nc_draw_text expects (y, x, text)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "nc_draw_text expects (y, x, text)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "nc_draw_text expects (y, x, text)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after nc_draw_text args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_NC_DRAW_TEXT, 0); + free(name); + return 1; + } + if (strcmp(name, "nc_getch") == 0) { + (*pos)++; /* '(' */ + /* (timeout_ms) */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "nc_getch expects (timeout_ms)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after nc_getch arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_NC_GETCH, 0); + free(name); + return 1; + } if (strcmp(name, "pcsc_release") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "pcsc_release expects 1 argument (ctx)"); free(name); return 0; } diff --git a/src/vm.c b/src/vm.c index 22a45c4..f21d110 100644 --- a/src/vm.c +++ b/src/vm.c @@ -7,12 +7,15 @@ * https://opensource.org/license/apache-2-0 */ -/* Ensure POSIX prototypes (nanosleep, clock_gettime, localtime_r, etc.) are available +/* Ensure POSIX/XSI prototypes (nanosleep, wcwidth, etc.) are available * before any system headers are included by amalgamated .c files. */ #ifndef _WIN32 #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE 700 +#endif #endif #include @@ -45,6 +48,13 @@ #include "value.h" #include "vm.h" +/* Shared Notcurses state (optional) */ +#ifdef FUN_WITH_NOTCURSES +# include /* ensure wcwidth/wcswidth prototypes present before notcurses.h on some systems */ +# include +#endif +#include "vm/notcurses/common.h" + // Optional by extensions commonly used code. #ifdef's are in each single file. #include "external/curl.c" #include "external/ini.c" @@ -781,6 +791,15 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/tk/bind.c" #endif + /* Notcurses TUI ops (optional) */ + #ifdef FUN_WITH_NOTCURSES + #include "vm/notcurses/init.c" + #include "vm/notcurses/shutdown.c" + #include "vm/notcurses/clear.c" + #include "vm/notcurses/draw_text.c" + #include "vm/notcurses/getch.c" + #endif + /* SQLite ops */ #ifdef FUN_WITH_SQLITE #include "vm/sqlite/open.c" diff --git a/src/vm.h b/src/vm.h index bff9bfb..13ad548 100644 --- a/src/vm.h +++ b/src/vm.h @@ -53,7 +53,9 @@ static const char *opcode_names[] = { "TK_BIND", "SERIAL_OPEN","SERIAL_CONFIG","SERIAL_SEND","SERIAL_RECV","SERIAL_CLOSE", "TK_EVAL","TK_RESULT","TK_LOOP","TK_WM_TITLE","TK_LABEL","TK_BUTTON","TK_PACK", - "TRY_PUSH","TRY_POP","THROW" + "TRY_PUSH","TRY_POP","THROW", + /* Notcurses TUI (optional) */ + "NC_INIT","NC_SHUTDOWN","NC_CLEAR","NC_DRAW_TEXT","NC_GETCH" }; typedef struct { @@ -132,7 +134,7 @@ void vm_debug_request_finish(VM *vm); void vm_debug_request_continue(VM *vm); static inline int opcode_is_valid(int op) { - return op >= OP_NOP && op <= OP_THROW; // all current opcodes + return op >= OP_NOP && op <= OP_NC_GETCH; // all current opcodes (including optional NC_*) } #endif diff --git a/src/vm/notcurses/clear.c b/src/vm/notcurses/clear.c new file mode 100644 index 0000000..91bd390 --- /dev/null +++ b/src/vm/notcurses/clear.c @@ -0,0 +1,27 @@ +/** +* This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +/* NC_CLEAR */ +case OP_NC_CLEAR: { +#ifdef FUN_WITH_NOTCURSES + if (_fun_nc && _fun_nc_std) { + ncplane_erase(_fun_nc_std); + notcurses_render(_fun_nc); + push_value(vm, make_int(0)); + } else { + push_value(vm, make_int(-1)); + } +#else + (void)_fun_nc; (void)_fun_nc_std; + push_value(vm, make_int(-1)); +#endif + break; +} diff --git a/src/vm/notcurses/common.h b/src/vm/notcurses/common.h new file mode 100644 index 0000000..de6d035 --- /dev/null +++ b/src/vm/notcurses/common.h @@ -0,0 +1,23 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +/* Common helpers/state for Notcurses TUI ops */ +#pragma once + +#include "value.h" + +/* Forward declarations to avoid depending on the notcurses headers unless enabled */ +struct notcurses; +struct ncplane; + +/* Shared static state within vm.c translation unit (header is included into vm.c at file scope) */ +static struct notcurses* _fun_nc = NULL; +static struct ncplane* _fun_nc_std = NULL; diff --git a/src/vm/notcurses/draw_text.c b/src/vm/notcurses/draw_text.c new file mode 100644 index 0000000..8f7ff95 --- /dev/null +++ b/src/vm/notcurses/draw_text.c @@ -0,0 +1,39 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +/* NC_DRAW_TEXT */ +case OP_NC_DRAW_TEXT: { + Value textv = pop_value(vm); + Value xv = pop_value(vm); + Value yv = pop_value(vm); + int x = (xv.type == VAL_INT ? (int)xv.i : (xv.type == VAL_FLOAT ? (int)xv.d : 0)); + int y = (yv.type == VAL_INT ? (int)yv.i : (yv.type == VAL_FLOAT ? (int)yv.d : 0)); + char *text = value_to_string_alloc(&textv); + free_value(textv); + free_value(xv); + free_value(yv); +#ifdef FUN_WITH_NOTCURSES + if (_fun_nc && _fun_nc_std && text) { + ncplane_putstr_yx(_fun_nc_std, y, x, text); + notcurses_render(_fun_nc); + free(text); + push_value(vm, make_int(0)); + } else { + if (text) free(text); + push_value(vm, make_int(-1)); + } +#else + (void)_fun_nc; (void)_fun_nc_std; + if (text) free(text); + push_value(vm, make_int(-1)); +#endif + break; +} diff --git a/src/vm/notcurses/getch.c b/src/vm/notcurses/getch.c new file mode 100644 index 0000000..29e5933 --- /dev/null +++ b/src/vm/notcurses/getch.c @@ -0,0 +1,43 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +/* NC_GETCH */ +case OP_NC_GETCH: { + Value to_ms_v = pop_value(vm); + int timeout_ms = (to_ms_v.type == VAL_INT ? (int)to_ms_v.i : (to_ms_v.type == VAL_FLOAT ? (int)to_ms_v.d : 0)); + free_value(to_ms_v); +#ifdef FUN_WITH_NOTCURSES + if (_fun_nc) { + /* For simplicity, use blocking get for now when timeout<=0 */ + if (timeout_ms <= 0) { + ncinput ni; + uint32_t id = notcurses_get_blocking(_fun_nc, &ni); + push_value(vm, make_int((int)id)); + } else { + /* Polling approximation: render then nonblocking get once */ + ncinput ni; + uint32_t id = notcurses_get_nblock(_fun_nc, &ni); + if (id == 0) { + /* no input available now: return -1 to indicate timeout */ + push_value(vm, make_int(-1)); + } else { + push_value(vm, make_int((int)id)); + } + } + } else { + push_value(vm, make_int(-1)); + } +#else + (void)_fun_nc; (void)_fun_nc_std; (void)timeout_ms; + push_value(vm, make_int(-1)); +#endif + break; +} diff --git a/src/vm/notcurses/init.c b/src/vm/notcurses/init.c new file mode 100644 index 0000000..af37dfe --- /dev/null +++ b/src/vm/notcurses/init.c @@ -0,0 +1,27 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +/* NC_INIT */ +case OP_NC_INIT: { +#ifdef FUN_WITH_NOTCURSES + if (_fun_nc) { push_value(vm, make_int(1)); break; } + struct notcurses_options opts = {0}; + _fun_nc = notcurses_core_init(&opts, NULL); + if (!_fun_nc) { push_value(vm, make_int(0)); break; } + _fun_nc_std = notcurses_stdplane(_fun_nc); + push_value(vm, make_int(1)); +#else + (void)_fun_nc; (void)_fun_nc_std; + fprintf(stderr, "Notcurses support disabled at build time. Reconfigure with -DFUN_WITH_NOTCURSES=ON.\n"); + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/src/vm/notcurses/shutdown.c b/src/vm/notcurses/shutdown.c new file mode 100644 index 0000000..6f1b06b --- /dev/null +++ b/src/vm/notcurses/shutdown.c @@ -0,0 +1,25 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-03 + */ + +/* NC_SHUTDOWN */ +case OP_NC_SHUTDOWN: { +#ifdef FUN_WITH_NOTCURSES + if (_fun_nc) { + notcurses_stop(_fun_nc); + _fun_nc = NULL; + _fun_nc_std = NULL; + } +#else + (void)_fun_nc; (void)_fun_nc_std; +#endif + push_value(vm, make_int(0)); + break; +}