diff --git a/CMakeLists.txt b/CMakeLists.txt index ce110e9..ffd71d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.47 LANGUAGES C) +project(fun VERSION 0.37.48 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/progress.fun b/examples/progress.fun new file mode 100755 index 0000000..82f754b --- /dev/null +++ b/examples/progress.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-12 + */ + +// Demonstrates system library includes after installation to /usr/lib/fun + +// includes can also be done with a leading # like in C, but some programmers maybe like more clean code without a +// leading #. +include +include // for sleep() + +c = Console() + +print("Progress demo: 0..100") + +total = 100 +for i in range(0, total + 1) + c.progress(i, total, "Downloading") + sleep(30) // milliseconds + +print("\nMultiple phases demo") + +// Phase 1 +phase_total = 40 +for i in range(0, phase_total + 1) + c.progress(i, phase_total, "Phase 1") + sleep(20) + +// Phase 2 +phase_total = 60 +for i in range(0, phase_total + 1) + c.progress(i, phase_total, "Phase 2") + sleep(15) diff --git a/examples/progress_inline.fun b/examples/progress_inline.fun new file mode 100644 index 0000000..42cb3b8 --- /dev/null +++ b/examples/progress_inline.fun @@ -0,0 +1,29 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-13 + */ + +// Realtime progress bar demo without printing extra newlines. +// It only updates a single console line in-place. The progress() +// helper emits one trailing newline automatically at 100%. + +include +include // for sleep() + +c = Console() + +total = 100 +for i in range(0, total + 1) + c.progress(i, total, "Downloading") + sleep(20) // milliseconds + +// Done. No additional prints/newlines here; progress() already +// finalized the line when it reached 100%. diff --git a/lib/io/console.fun b/lib/io/console.fun index c312e8f..c7f76ba 100644 --- a/lib/io/console.fun +++ b/lib/io/console.fun @@ -12,6 +12,7 @@ */ #include +#include class Console() // Print a prompt and read a line (no trailing newline) @@ -48,5 +49,65 @@ class Console() return 0 // otherwise loop again - fun progress(this, length) - return length + // Internal: best-effort terminal columns via `tput cols` (fallback to 80) + fun term_cols(this) + // Use builtin proc_run directly to avoid any method dispatch quirks + r = proc_run("tput cols") + if (r["code"] == 0) + s = str_trim(r["out"]) // trim trailing newline + if (len(s) > 0) + n = to_number(s) + if (n > 0) + return n + // Fallback if tput not available or not a TTY + return 80 + + // Draw/update a full-width progress bar on a single console line. + // - current: Number (0..total) + // - total: Number (>0) + // - label: Optional text shown left of the bar + // Returns the integer percent [0..100]. Adds a newline automatically at 100%. + fun progress(this, current, total, label) + cur = to_number(current) + tot = to_number(total) + if (tot <= 0) + tot = 1 + pct = to_number((cur * 100) / tot) + if (pct < 0) + pct = 0 + if (pct > 100) + pct = 100 + + cols = this.term_cols() + + lbl = to_string(label) + if (len(lbl) > 0) + prefix = lbl + " " + else + prefix = "" + + perc_str = to_string(pct) + "%" + // We render: [====....] plus percent; keep the whole line within terminal width + // Compute bar width: terminal cols minus prefix, brackets, space, and percent. + fixed = len(prefix) + 2 + 1 + len(perc_str) // [] + space + percent + bar_w = cols - fixed + if (bar_w < 10) + bar_w = 10 // minimal bar width for visibility + + filled = to_number((bar_w * pct) / 100) + if (filled < 0) + filled = 0 + if (filled > bar_w) + filled = bar_w + + bar = "[" + str_repeat("=", filled) + str_repeat(" ", bar_w - filled) + "]" + line = prefix + bar + " " + perc_str + + // In-place update: CR + full-width content (we already fit to cols), no newline + echo("\r" + line) + + // When done, end the line cleanly + if (cur >= tot) + print("") + + return pct diff --git a/src/vm.c b/src/vm.c index f21d110..61e347f 100644 --- a/src/vm.c +++ b/src/vm.c @@ -503,10 +503,6 @@ void vm_print_output(VM *vm) { printf("\n"); } } - /* If the last item was partial (from echo), terminate the line for cleanliness */ - if (vm->output_count > 0 && vm->output_is_partial[vm->output_count - 1]) { - printf("\n"); - } } void vm_run(VM *vm, Bytecode *entry) { @@ -853,6 +849,18 @@ void vm_run(VM *vm, Bytecode *entry) { } break; } + + /* Stream console output in realtime for scripts: + * When PRINT/ECHO pushed items into the VM's output buffer, flush them + * immediately to stdout and clear the buffer to avoid end-of-run bursts. + * This keeps REPL compatibility (REPL still prints after each submit), + * while regular script execution shows progress bars live. + */ + if (inst.op == OP_PRINT || inst.op == OP_ECHO) { + vm_print_output(vm); + vm_clear_output(vm); + fflush(stdout); + } } g_active_vm = NULL; }