1
0
Fork 0
forked from fun/fun

Very basic Notcurses support. There are bugs! (0.37.45)

This commit is contained in:
Johannes Findeisen 2026-01-03 21:11:26 +01:00
commit fd582f4d3a
30 changed files with 465 additions and 5 deletions

View file

@ -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

0
examples/ini_diag.fun Normal file → Executable file
View file

0
examples/math_ceil.fun Normal file → Executable file
View file

0
examples/math_cos.fun Normal file → Executable file
View file

0
examples/math_exp_log.fun Normal file → Executable file
View file

0
examples/math_floor.fun Normal file → Executable file
View file

0
examples/math_fmin_fmax.fun Normal file → Executable file
View file

0
examples/math_gcd_lcm.fun Normal file → Executable file
View file

0
examples/math_isqrt.fun Normal file → Executable file
View file

0
examples/math_round.fun Normal file → Executable file
View file

0
examples/math_sign.fun Normal file → Executable file
View file

0
examples/math_sin.fun Normal file → Executable file
View file

0
examples/math_sqrt.fun Normal file → Executable file
View file

0
examples/math_tan.fun Normal file → Executable file
View file

0
examples/math_trunc.fun Normal file → Executable file
View file

42
examples/notcurses_clock.fun Executable file
View file

@ -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 <you@hanez.org>
* 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 <ui/notcurses.fun>
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()

46
examples/notcurses_hello.fun Executable file
View file

@ -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 <you@hanez.org>
* 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 <ui/notcurses.fun>
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%)
*/

42
examples/notcurses_typing.fun Executable file
View file

@ -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 <you@hanez.org>
* 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 <ui/notcurses.fun>
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()

0
examples/ripemd160_example.fun Normal file → Executable file
View file

42
lib/ui/notcurses.fun Normal file
View file

@ -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 <you@hanez.org>
* 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)

View file

@ -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 {

View file

@ -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; }

View file

@ -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 <stdio.h>
@ -45,6 +48,13 @@
#include "value.h"
#include "vm.h"
/* Shared Notcurses state (optional) */
#ifdef FUN_WITH_NOTCURSES
# include <wchar.h> /* ensure wcwidth/wcswidth prototypes present before notcurses.h on some systems */
# include <notcurses/notcurses.h>
#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"

View file

@ -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

27
src/vm/notcurses/clear.c Normal file
View file

@ -0,0 +1,27 @@
/**
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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;
}

23
src/vm/notcurses/common.h Normal file
View file

@ -0,0 +1,23 @@
/**
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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;

View file

@ -0,0 +1,39 @@
/**
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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;
}

43
src/vm/notcurses/getch.c Normal file
View file

@ -0,0 +1,43 @@
/**
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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;
}

27
src/vm/notcurses/init.c Normal file
View file

@ -0,0 +1,27 @@
/**
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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;
}

View file

@ -0,0 +1,25 @@
/**
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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;
}