Skip to content

Latest commit

 

History

History
1435 lines (1119 loc) · 43.4 KB

File metadata and controls

1435 lines (1119 loc) · 43.4 KB

CupidFM Cupidscript API (with New Features)


Table of Contents

CupidFM supports plugin scripting via CupidScript, a lightweight, embeddable scripting language and VM written in C99. This document details plugin loading, the plugin architecture, the fm.* API exposed to scripts in CupidFM, and a comprehensive overview of the latest CupidScript language features.


CupidScript

CupidScript is a compact, embeddable scripting VM (C99), designed for fast integration. It provides a simple C API for host embedding, plugin scripting, and seamless extension with native functions.


What's Included

  • Core runtime: Lexer, parser, AST, virtual machine, and a minimal standard library.
  • Sample CLI and main (src/main.c): Demonstrates embedding, native API registration (e.g., fm.*).
  • Public headers for C API (src/cupidscript.h, etc).
  • Examples/tests covering all language features, host extension, and API use.

Language Overview

CupidScript: small, dynamic, with a modern feature set. Tree-walk interpreter; strong runtime error reporting; simple types.

Syntax Example

let name = expr;           // declaration
name = expr;               // assignment

fn add(a, b) { return a + b; }

if (cond) { ... } else { ... }
while (cond) { ... }
return expr;
  • Operators: ||, &&, ==, !=, <, <=, >, >=, +, -, *, /, %, unary !, -
  • Types/Values: nil, true/false, int, float, string, bytes, list, map, set, tuple, strbuf, range, promise, function, native
  • Keywords: let, const, fn, async, await, yield, class, struct, enum, self, super, if, else, while, switch, case, default, for, in, break, continue, return, throw, try, catch, finally, defer, match, import, export

Modern Language Features

CupidScript includes cutting-edge features found in modern scripting languages:

  • Tuples - Immutable, fixed-size value groupings

    let point = (x: 10, y: 20);  // named tuple
    let coords = (1, 2, 3);      // positional tuple
    print(point.x, coords[0]);
  • Comprehensions - Concise syntax for transforming collections

    let squares = [x * x for x in 1..=10];
    let evens = [x for x in nums if x % 2 == 0];
    let word_map = {word: len(word) for word in words};
    let unique = #{x for x in list};  // set comprehension
  • Destructuring - Extract values into variables

    let [a, b, ...rest] = [1, 2, 3, 4, 5];
    let (x, y) = get_coords();  // function returning tuple
  • Pattern Matching - match expressions for powerful branching

    let result = match value {
      0 => "zero",
      1..=5 => "small",
      _ => "large"
    };
  • Walrus Operator - Assign and test in one expression

    if (result := compute()) {
      print("Success:", result);
    }
    while (line := read_line()) {
      process(line);
    }
  • Pipe Operator - Chain function calls fluently

    let result = data |> filter(_) |> map(_) |> sum();
  • Arrow Functions - Concise function syntax

    let add = fn(a, b) => a + b;
    let square = fn(x) => x * x;
  • Spread & Rest - Flexible argument handling

    let all = [...list1, ...list2];
    let merged = {...map1, ...map2};
    fn sum(...nums) { return list_sum(nums); }
  • Sets - Unique collections with set operations

    let s = #{1, 2, 3};
    let union = set1 | set2;
    let intersection = set1 & set2;
    let difference = set1 - set2;
  • Classes & Inheritance - Object-oriented programming

    class File {
      fn new(path) { self.path = path; }
      fn is_hidden() { return starts_with(self.path, "."); }
    }
    class ImageFile : File {
      fn is_image() { return ends_with(self.path, ".png"); }
    }
  • Structs - Lightweight data carriers

    struct Point { x, y = 0 }
    let p = Point(5, 10);
  • Enums - Named integer constants

    enum Color { Red, Green = 5, Blue }
    print(Color.Red);   // 0
  • Async/Await - Asynchronous programming

    async fn fetch_data(url) {
      let response = await http_get(url);
      return response;
    }
  • Generators - Lazy value sequences

    fn range(n) {
      let i = 0;
      while (i < n) { yield i; i += 1; }
    }
  • String Interpolation - Embed expressions in strings

    let name = "Alice";
    print("Hello ${name}, you have ${count} messages");
  • Raw Strings - Backtick strings without escape processing

    let path = `C:\Users\Frank\Documents`;
    let multiline = `line 1
    line 2`;

Recent Language Features

  • Anonymous Functions & Closures
    let double = fn(x) { return x*2; };
    let add_fn = fn(a,b) { return a+b; };
    fn make_counter() {
      let n = 0;
      return fn() { n=n+1; return n; };
    }
  • First-Class Functions: Pass/return/assign functions; store in containers.
  • Short-Circuit Logic: &&, ||
  • String Concatenation: Use +, e.g., "foo" + 123
  • Modern Errors: Stack traces, rich source location.
  • Indexed map access (m[k]), map key querying (keys(m))
  • Optional trailing commas in lists/maps
  • Improved error/stack reporting, with precise line/col info
  • Defer statements - Execute code when leaving scope
    fn process() {
      let f = open_file("data.txt");
      defer close_file(f);  // always called before returning
      // ... work with file ...
    }
  • Const bindings - Immutable variable declarations
    const PI = 3.14159;
    const MAX_SIZE = 100;
  • typeof returns detailed type names ("native", "function", "tuple", "set", etc)
  • fmt supports more specifiers (%b, %v etc)
  • assert_eq, assert_ne (testing stdlib)

Core language features (per CupidScript wiki)

These are the core language/stdlib behaviors implemented by the current lexer/parser/VM as documented in the CupidScript wiki.

  • List and map literals

    let xs = [1, 2, 3];
    let m = {"name": "Frank", "age": 30};
  • Map field access sugar (maps only)

    let m = {"name": "Frank"};
    print(m.name);     // same as m["name"]

    If the value is not a map, obj.field is a runtime error.

  • for ... in loops (iterate lists; maps iterate keys)

    for x in [10, 20, 30] {
      print(x);
    }
    for k in keys({"a": 1, "b": 2}) {
      print(k);
    }
  • C-style for (init; cond; incr) loops

    for (i = 0; i < 10; i = i + 1) {
      print(i);
    }
  • Range operator

    let nums = 0..5;    // [0,1,2,3,4]
    let incl = 0..=5;   // [0,1,2,3,4,5]
    for i in 1..=3 { print(i); }

    Ranges work in both directions (ascending/descending) automatically.

  • Ternary expression

    let max = a > b ? a : b;
  • Exceptions: throw and try/catch

    try {
      throw "boom";
    } catch (e) {
      print("caught:", e);
    }
  • Standardized error objects: error, is_error, format_error, global ERR

    try {
      throw error("Division by zero", "DIV_ZERO");
    } catch (e) {
      print(format_error(e));
    }

Built-In Types

CupidScript supports a rich set of built-in types:

Core Types

let xs = [1, 2, 3];              // list
let m = {"name": "Frank"};       // map
let s = #{1, 2, 3};              // set
let t = (x: 10, y: 20);          // tuple (named)
let coords = (1, 2, 3);          // tuple (positional)
let b = bytes([0x48, 0x65]);     // bytes
let r = 1..10;                   // range

Lists

let xs = list();
push(xs, 10);
push(xs, 20);
xs[1] = 99;
print(xs[0], xs[1], len(xs)); // 10 99 2
  • Index by integer (0-based)
  • Negative or out-of-range returns nil
  • Dynamic and mutable
  • Supports spread: [...list1, ...list2]

Maps

let m = map();
m["answer"] = 42;
print(m["answer"], keys(m)); // 42 ["answer"]
print(m.answer);  // field access sugar
  • Generalized keys: Any value can be a key (string, int, bool, list, map, etc.)
  • Key equality uses == rules (int/float compare by value, strings by content)
  • Missing keys return nil
  • Supports spread: {...map1, ...map2}

Sets

let s = #{1, 2, 3};
s.add(4);
s.remove(2);
print(s.contains(3));  // true
print(s.size());       // 3

// Set operations
let union = set1 | set2;
let intersection = set1 & set2;
let difference = set1 - set2;
let symmetric_diff = set1 ^ set2;
  • Unique values (by ==)
  • Set operations: | (union), & (intersection), - (difference), ^ (symmetric difference)
  • Literal: #{1, 2, 3} or #{} for empty
  • Comprehensions: #{x for x in list if condition}

Tuples

// Positional tuple
let coords = (10, 20, 30);
print(coords[0], coords[1], coords[2]);

// Named tuple
let point = (x: 10, y: 20);
print(point.x, point.y);

// Destructuring
let (a, b, c) = coords;
let [x, y] = get_position();
  • Immutable - cannot modify after creation
  • Access by index (positional) or field name (named)
  • Perfect for returning multiple values from functions

Bytes

let b = bytes([72, 101, 108, 108, 111]);  // "Hello"
print(b[0]);  // 72
b[0] = 104;   // modify to "hello"
  • Mutable byte buffer for binary data
  • Index returns int 0-255
  • Out-of-range returns nil

Range

let r = 0..5;      // [0,1,2,3,4] - exclusive end
let incl = 0..=5;  // [0,1,2,3,4,5] - inclusive end
for i in 1..=10 { print(i); }
  • Works in both directions (ascending/descending)
  • Can iterate with for ... in

Control Flow (Additional)

CupidScript supports:

  • break; / continue; inside while, for ... in, and C-style for loops.
  • return; / return expr; from inside any block.

Truthiness (Reminder)

Used by if, while, !, &&, ||:

  • nil is false
  • bool is its value
  • everything else is true (including 0)

Multi-File Scripts

  • load("path") — always executes (like #include)
  • require("path") — only executes the first time (like JS require)
  • New: Loads use per-VM cache; relative to current file/script.

Additional module/path behavior (per wiki):

  • The VM maintains a current-directory stack so relative module loads resolve relative to the calling script.
  • require_optional("path") behaves like require(), but returns nil if the file is missing.
  • require() returns a module exports map. Inside a required file, these globals are available:
    • exports (map)
    • __file__ (string, resolved module path)
    • __dir__ (string, directory containing the module)

Advanced Features

Data Formats

CupidScript provides built-in support for structured data formats:

JSON

let data = {"name": "Frank", "age": 30};
let json_str = json_encode(data);
let parsed = json_decode(json_str);

CSV (RFC 4180 Compliant)

let csv_data = csv_parse("name,age\nFrank,30\nAlice,25");
let csv_str = csv_format([{"name": "Frank", "age": 30}]);

YAML (RFC 9512 / YAML 1.2.2 Compliant)

let yaml_str = "name: Frank\nage: 30";
let data = yaml_parse(yaml_str);
let yaml = yaml_format(data);

XML

let xml = "<root><item>test</item></root>";
let doc = xml_parse(xml);

Regular Expressions (POSIX ERE)

// Match checking
if (regex_is_match("[0-9]+", text)) {
    print("Contains digits");
}

// Find first match
let email = regex_find("([a-z]+)@([a-z]+\\.[a-z]+)", text);
if (email != nil) {
    print(email["match"]);     // full match
    print(email.groups[0]);    // first capture group
}

// Find all matches
let nums = regex_find_all("[0-9]+", "x=7 y=42 z=105");

// Replace
let clean = regex_replace("[a-z]+@[a-z]+\\.[a-z]+", text, "<hidden>");

Async/Await & Promises

// Event loop (background thread for true async)
event_loop_start();

// Async functions
async fn fetch_data(url) {
    let response = await http_get(url);
    return response;
}

// Multiple concurrent operations
let p1 = fetch_data("https://api.example.com/1");
let p2 = fetch_data("https://api.example.com/2");
let result1 = await p1;
let result2 = await p2;

// Sleep/timers
await sleep(1000);  // milliseconds

event_loop_stop();

Promise Helpers:

  • promise() - create new promise
  • resolve(p, value) - resolve promise
  • reject(p, error) - reject promise
  • promise_all(promises) - wait for all
  • promise_race(promises) - wait for first
  • promise_any(promises) - wait for first success

Network I/O

// HTTP requests (async)
let response = await http_get("https://api.github.com/users/octocat");
let data = json_decode(response);

// TCP sockets
let sock = await tcp_connect("example.com", 80);
await socket_send(sock, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
let response = await socket_recv(sock, 4096);
socket_close(sock);

File Operations

// Read/write files
let content = read_file("config.txt");
write_file("output.txt", "Hello, world!");

// Binary files
let data = read_file_bytes("image.png");
write_file_bytes("copy.png", data);

// Directory operations
let entries = list_dir(".");
mkdir("new_folder");
rm("old_file.txt");
rename("old.txt", "new.txt");

// File info
if (exists("file.txt") && is_file("file.txt")) {
    print("File exists");
}
if (is_dir("folder")) {
    print("Directory exists");
}

// Glob patterns (platform-specific)
let txt_files = glob("*.txt");
let all_cs = glob("**/*.cs", "src");

Date & Time

// Current time
let ms = unix_ms();
let seconds = unix_s();

// Date/time maps
let now = datetime_now();
let utc = datetime_utc();
print("${now.year}-${now.month}-${now.day} ${now.hour}:${now.minute}");

// Convert from Unix timestamp
let dt = datetime_from_unix_ms(ms);

Directory Overview

  • src/cs_value.c, src/cs_lexer.c, src/cs_parser.c, src/cs_vm.c, src/cs_stdlib.c – core runtime, VM, and stdlib
  • src/main.c – CLI/embedding demo and entry point
  • src/cupidscript.h (embedding API header)
  • Build system: Makefile

Build Instructions

Requires: C99 compiler (gcc/clang), POSIX tools.

With make:

make all

Outputs:

  • bin/libcupidscript.a (static library)
  • bin/cupidscript (CLI interpreter)

Manual:

See README for explicit build commands if not using make.


Using CupidScript in C Hosts

Typical embedding workflow:

  1. Create and configure VM:
    cs_vm* vm = cs_vm_new();
    cs_register_stdlib(vm);
    cs_register_native(vm, "fm.notify", my_cb, NULL);
  2. Execute scripts:
    cs_vm_run_file(vm, "script.cs");
  3. Invoke function from C:
    cs_call(vm, "myfunc", argc, argv, &out);
  4. Query last error (as string):
    const char* err = cs_vm_last_error(vm);

Refer to the header for refcounted string/lists API, type checking, etc.


CupidFM Plugins

CupidFM exposes a native scripting API (fm.*) to plugins via CupidScript. Place .cs plugin scripts in designated directories and CupidFM will autoload them at startup.

Plugin Locations

Load order (per session):

  1. ~/.cupidfm/plugins
  2. ~/.cupidfm/plugin (legacy)
  3. ./cupidfm/plugins
  4. ./cupidfm/plugin (legacy)
  5. ./plugins

All .cs scripts in these are loaded.


Plugin Structure, Events & Lifecycle

Plugins = Any CupidScript .cs file with optional hooks:

  • fn on_load() — after file loads (like init)
  • fn on_key(key) — after keypress (return true/false to block/pass)
  • fn on_dir_change(new_cwd, old_cwd) — when the panel dir changes
  • fn on_selection_change(new_name, old_name) — selection changed
  • fn on_editor_open(path) — when a file is opened in the built-in editor
  • fn on_editor_change(line, col, text) — when editor content changes (insertions/deletions)
  • n on_editor_save(path) when a file is saved in the editor

New:

  • Plugin API can export additional custom entry points.
  • Error in hook? Stacktrace with file/line/col will display in notification area.

Key Format and Helpers

Keys use string names, e.g.:

  • "^T" = Ctrl+T
  • "F5"
  • "KEY_UP" / "KEY_DOWN"
  • "Tab"
  • Any printable char (e.g., "a")

Helpers for mapping:

  • fm.key_code(name) — string→int
  • fm.key_name(code) — int→string

on_editor_open Callback

The on_editor_open(path) callback is triggered when a file is opened in CupidFM's built-in text editor.

Parameters:

  • path (string): Absolute path to the file being opened in the editor

Use Cases:

  • Logging file access and editing history
  • Auto-formatting or linting on open
  • Setting editor configurations based on file type
  • Tracking editing patterns and statistics
  • Custom notifications or prompts based on file content
  • Integration with external tools or version control

Example:

fn on_editor_open(path) {
    fm.console(fmt("Opened in editor: %s", path));
    
    // Auto-format JSON files
    if (ends_with(path, ".json")) {
        fm.notify("JSON file detected - remember to validate syntax!");
    }
    
    // Check for specific patterns
    if (contains(path, "/config/")) {
        fm.notify("Editing configuration file - be careful!");
    }
}

Full Example Plugin: See editor_open_logger.cs for a complete example that:

  • Logs all files opened in the editor
  • Tracks statistics by file type
  • Provides helpful tips based on file extension
  • Binds Ctrl+E to show editing statistics

Note: This callback fires once when the editor window opens, not on subsequent edits or saves within the same editing session.

Related Functions:

  • Use fm.editor_get_path() to get the current editor file path at any time (not just when opening)
  • Use fm.editor_active() to check if the editor is currently open

on_editor_change Callback

Signature: fn on_editor_change(line, col, text)

Triggered when editor content changes (text insertions or deletions).

Parameters:

  • line (int): Line number where change occurred (1-indexed)
  • col (int): Column where change occurred (1-indexed)
  • text (string): Text that was inserted/deleted

Example:

letchanges = 0;

fn on_editor_change(line, col, text) {
    changes = changes + 1;
    if (changes % 100 == 0) {
        fm.notify("100 changes made!");
    }
}

See: editor_change_demo.cs


on_editor_save Callback

The on_editor_save(path) callback is triggered whenever a file is successfully saved in CupidFM's built-in text editor.

Signature: fn on_editor_save(path)

Parameters:

  • path (string): Absolute path to the file that was just saved

Use Cases:

  • Tracking save frequency and editing patterns
  • Auto-backup of important files after save
  • Running post-save actions (linting, formatting, compilation)
  • Logging save history
  • Triggering notifications or external integrations
  • Statistics gathering for productivity tracking

Example:

let total_saves = 0;

fn on_editor_save(path) {
    total_saves = total_saves + 1;
    let filename = path_basename(path);
    fm.console(fmt("Saved: %s (save #%d)", filename, total_saves));
    
    // Auto-backup for important files
    let ext = path_ext(path);
    if (ext == ".c" || ext == ".h") {
        fm.console("Auto-backup: Important C file saved");
    }
}

Full Example Plugin: See editor_save_demo.cs for a complete example that:

  • Tracks total saves per session
  • Maintains a history of recently saved files (last 20)
  • Shows congratulatory messages every 5 saves
  • Provides save statistics on Ctrl+D
  • Auto-detects and logs important file types (.c, .h)
  • Uses helper functions with substr() for safe string operations

Key Features in Example:

  • Uses path_basename() to extract filename from full path
  • Uses path_ext() for file extension checking
  • Demonstrates list management with spread operator
  • Shows proper use of slice() for list trimming
  • Includes statistics display with formatted output

Note: This callback fires after the file has been successfully written to disk. If the save operation fails, the callback will not be triggered.

Related Callbacks:

  • Use on_editor_open(path) to track when files are first opened
  • Use on_editor_change(line, col, text) to track individual edits between saves

on_editor_cursor_move Callback

The on_editor_cursor_move(old_line, old_col, new_line, new_col) callback is triggered whenever the cursor position changes in CupidFM's built-in text editor.

Signature: fn on_editor_cursor_move(old_line, old_col, new_line, new_col)

Parameters:

  • old_line (int): Previous line number (1-indexed)
  • old_col (int): Previous column number (1-indexed)
  • new_line (int): New line number (1-indexed)
  • new_col (int): New column number (1-indexed)

Use Cases:

  • Tracking cursor movement patterns and statistics
  • Implementing context-aware features based on cursor position
  • Real-time position monitoring for collaborative editing
  • Building cursor navigation analytics
  • Creating position-based triggers or notifications
  • Implementing smart suggestions based on cursor location

Example:

let move_count = 0;

fn on_editor_cursor_move(old_line, old_col, new_line, new_col) {
    move_count = move_count + 1;
    
    // Calculate movement distance
    let line_delta = new_line - old_line;
    let col_delta = new_col - old_col;
    
    // Log significant jumps (more than 10 lines)
    if (line_delta > 10 || line_delta < -10) {
        fm.console(fmt("Large jump: %d -> %d", old_line, new_line));
    }
}

Full Example Plugin: See cursor_tracker.cs for a complete example that:

  • Tracks all cursor movements
  • Calculates movement statistics
  • Displays analytics via Ctrl+M keybinding
  • Demonstrates practical use of the API

Note: This callback fires for all cursor movements including arrow keys, mouse clicks, page up/down, home/end, and any other navigation commands. It will not fire if the cursor position doesn't actually change.

Related Functions:

  • Use fm.editor_get_cursor() to get the current cursor position at any time
  • Use fm.editor_set_cursor(line, col) to programmatically move the cursor
  • Use fm.editor_active() to check if the editor is currently open

fm.editor_get_path() Function

The fm.editor_get_path() function returns the absolute path of the file currently open in the editor, or nil if no file is open.

Returns:

  • string: Absolute path to the currently open file
  • nil: If the editor is not currently open

Use Cases:

  • Checking which file is being edited
  • Building file-specific tools or commands
  • Tracking editing context
  • Copying file paths to clipboard
  • Conditional behavior based on file location

Example:

fn on_key(key) {
    if (key == "^P") {
        let path = fm.editor_get_path();
        
        if (path == nil) {
            fm.notify("No file open in editor");
        } else {
            fm.popup("Current File", fmt("Editing: %s", path));
        }
        return true;
    }
    return false;
}

Full Example Plugin: See editor_path_watcher.cs for a complete example that:

  • Binds Ctrl+P to show current editor file path
  • Binds Ctrl+Shift+P to copy path to clipboard
  • Tracks when files change in the editor
  • Extracts and displays filename separately from full path

fm.* API (Plugin Scripting API)

CupidFM hosts these native functions for scripts:

UI

  • fm.notify(msg)
  • fm.status(msg) (alias)
  • fm.popup(title, msg)
  • fm.console_print(msg) / fm.console(msg)
    • Append a line to CupidFM's in-app console log (open with key_console, default ^O).
  • fm.prompt(title, initial) -> string|nil
    • Modal input box. Returns the entered string, or nil if cancelled.
  • fm.confirm(title, msg) -> bool
    • Modal yes/no box.
  • fm.menu(title, items) -> int
    • Modal menu. items is a list of strings. Returns selected index, or -1 if cancelled.

Async variants (callback-based):

  • fm.prompt_async(title, initial, cb) -> bool
  • fm.confirm_async(title, msg, cb) -> bool
  • fm.menu_async(title, items, cb) -> bool
    • Queues a modal UI action and calls cb(result) after it completes.
    • cb may be a function value OR a function name string.

Query Context

  • fm.cwd() — current directory (left pane)
  • fm.selected_name() — selection (filename or "")
  • fm.selected_path()
  • fm.entries() -> list
    • Returns the current visible directory listing (search-filtered when fm.search_active() is true).
    • Each entry is a map: {name,is_dir,size,mtime,mode,mime}.
  • fm.cursor() — index or -1
  • fm.count() — count of files/items
  • fm.search_active() — true if search open
  • fm.search_query()
  • fm.pane() — string ("directory" or "preview")

Editor API

CupidFM provides a comprehensive API for manipulating the built-in text editor from plugins. All editor functions use 1-indexed line and column numbers.

Editor State Functions

Function Returns Description
fm.editor_active() bool True if the built-in text editor is currently open
fm.editor_get_path() string | nil Current editor file path, or nil if editor not open
fm.editor_save() bool Saves the current editor buffer to disk. Returns false if not editing or on write error. Triggers on_editor_save(path) on success
fm.editor_save_as(path) bool Saves the current editor buffer to path and updates the current editor file path on success. Triggers on_editor_save(path)
fm.editor_close() bool Closes the editor. Returns false if the editor is not open. If there are unsaved changes, CupidFM will ask to confirm discard
fm.editor_reload() bool Reloads the current editor file from disk. Returns false if the editor is not open. If there are unsaved changes, CupidFM will ask to confirm discard
fm.editor_set_readonly(readonly) bool Sets editor read-only mode. When enabled, editing and saving are blocked
fm.editor_line_count() int Total number of lines in the editor, or 0 if not open
fm.editor_get_cursor() map | nil Cursor position {line: int, col: int} (1-indexed), or nil if not editing
fm.editor_set_cursor(line, col) bool Sets cursor position (1-indexed). Returns true on success, false if not editing or invalid position
fm.editor_get_selection() map | nil Selection bounds {start_line, start_col, end_line, end_col} (1-indexed), or nil if no selection

Reading Editor Content

Function Returns Description
fm.editor_get_content() string | nil Entire editor buffer text, or nil if editor not open
fm.editor_get_line(line_num) string | nil Single line content (1-indexed), or nil if out of range
fm.editor_get_lines(start, end) list | nil List of lines in range (1-indexed, inclusive), or nil if invalid

Example:

if (fm.editor_active()) {
    let path = fm.editor_get_path();
    let line_count = fm.editor_line_count();
    let cursor = fm.editor_get_cursor();
    
    fm.notify(fmt("Editing: %s (%d lines) at line %d", path, line_count, cursor["line"]));
    
    // Get entire file content as a single string
    let content = fm.editor_get_content();
    if (content != nil) {
        let char_count = len(content);
        fm.console(fmt("File has %d characters", char_count));
        
        // Check if content contains a string (manual search)
        let has_todo = false;
        for (let i = 0; i < len(content) - 3; i = i + 1) {
            if (substr(content, i, 4) == "TODO") {
                has_todo = true;
                break;
            }
        }
        if (has_todo) {
            fm.notify("File contains TODO items");
        }
    }
    
    // Get specific lines as a list
    let lines = fm.editor_get_lines(1, 10);  // First 10 lines
    if (lines != nil) {
        for line in lines {
            fm.console(line);
        }
    }
    
    // Get a single line (using get_lines with same start and end)
    let first_lines = fm.editor_get_lines(1, 1);
    if (first_lines != nil && len(first_lines) > 0) {
        fm.console(fmt("First line: %s", first_lines[0]));
    }
    
    // Process a range of lines
    let cursor = fm.editor_get_cursor();
    if (cursor != nil) {
        let current = cursor["line"];
        // Get 5 lines before and after cursor
        let context = fm.editor_get_lines(current - 5, current + 5);
        if (context != nil) {
            fm.notify(fmt("Got %d lines of context", len(context)));
        }
    }
    
    // Move cursor to line 5, column 10
    if (fm.editor_set_cursor(5, 10)) {
        fm.notify("Cursor moved to line 5, column 10");
    }
}

Modifying Editor Content

Function Parameters Returns Description
fm.editor_insert_text(text) text: string bool Inserts text at current cursor position
fm.editor_replace_text(...) start_line, start_col, end_line, end_col, text bool Replaces text in specified range with new text
fm.editor_delete_range(...) start_line, start_col, end_line, end_col bool Deletes text in specified range
fm.editor_uppercase_selection() - bool Converts current selection to uppercase (efficient built-in)

Saving From Plugins

fm.editor_save()

  • Saves the currently open editor buffer to the current editor file on disk
  • Returns true on success, false if the editor is not open or a write error occurs
  • On success, triggers on_editor_save(path) callbacks (same as pressing the editor save key)

See plugins/examples/editor_save_api_demo.cs for a minimal example that binds ^S to fm.editor_save().

fm.editor_save_as(path)

  • Saves the currently open editor buffer to path
  • On success, updates the “current editor path” (so fm.editor_get_path() returns the new path and subsequent fm.editor_save() writes to the new file)
  • Returns true on success, false if the editor is not open or a write error occurs
  • On success, triggers on_editor_save(path) callbacks

See plugins/examples/editor_save_as_api_demo.cs for a minimal Save As example.

Closing From Plugins

fm.editor_close()

  • Requests that CupidFM close the built-in editor
  • Returns false if the editor is not open
  • If there are unsaved changes, CupidFM will prompt to confirm discard

See plugins/examples/editor_close_api_demo.cs for a minimal example binding ^Q to close the editor.

Reloading From Plugins

fm.editor_reload()

  • Reloads the current editor file from disk into the editor buffer
  • Returns false if the editor is not open
  • If there are unsaved changes, CupidFM will prompt to confirm discard before reloading

See plugins/examples/editor_reload_api_demo.cs for a minimal example binding ^R to reload.

Read-Only From Plugins

fm.editor_set_readonly(readonly)

  • Enables/disables editor read-only mode
  • Returns false if the editor is not open
  • When enabled, interactive edits and editor saves are blocked, and editor-write APIs return false

See plugins/examples/editor_readonly_api_demo.cs for a minimal toggle example (^_O).

fm.editor_replace_text(start_line, start_col, end_line, end_col, text)

  • All coordinates are 1-indexed
  • text can include newlines for multi-line replacements
  • Example: fm.editor_replace_text(1, 1, 1, 10, "new text") replaces characters 1-10 on line 1
  • See plugins/examples/editor_find_replace_demo.cs for complete find-and-replace example

fm.editor_delete_range(start_line, start_col, end_line, end_col)

  • All coordinates are 1-indexed
  • Deletes text from (start_line, start_col) to (end_line, end_col)
  • Example: fm.editor_delete_range(1, 1, 1, 10) deletes characters 1-10 on line 1
  • See plugins/examples/editor_delete_operations_demo.cs for examples including word deletion, line deletion, etc.

fm.editor_uppercase_selection()

Complete Editor Example

fn on_load() {
    fm.bind("^F", "find_and_replace");
}

fn find_and_replace(key) {
    if (!fm.editor_active()) {
        fm.notify("Editor not active");
        return true;
    }
    
    let sel = fm.editor_get_selection();
    if (sel == nil) {
        fm.notify("No selection");
        return true;
    }
    
    let find = fm.prompt("Find:", "");
    if (find == nil || find == "") return true;
    
    let replace = fm.prompt("Replace with:", "");
    if (replace == nil) return true;
    
    // Get selected text
    let start_line = sel["start_line"];
    let start_col = sel["start_col"];
    let end_line = sel["end_line"];
    let end_col = sel["end_col"];
    
    let lines = fm.editor_get_lines(start_line, end_line);
    // ... process and replace text ...
    
    return true;
}

Search Control

(Async: these actions run after script hook completes.)

  • fm.set_search(query) -> bool
    • Sets CupidFM's fuzzy filter/search query. Passing "" clears search.
  • fm.clear_search() -> bool
    • Clears CupidFM's fuzzy filter/search query.

Key Bindings & Events

  • fm.bind(key, func_name) — bind a key to your function (fn receives key string, return true to block)
  • fm.key_name(code)
  • fm.key_code(name)

UI & System Control

  • fm.reload() — refresh/reload panel UI
  • fm.exit() — exit CupidFM

File Navigation/Selection

(Async: these actions run after script hook completes.)

  • fm.cd(path)
  • fm.select(name)
  • fm.select_index(i)
  • fm.open_selected() -> bool
    • Opens the selected entry (enters directory if is_dir, otherwise opens editor for the selected file).
  • fm.enter_dir() -> bool
    • Enters the selected directory (no-op if selection is not a directory).
  • fm.parent_dir() -> bool
    • Navigates to the parent directory.

File Operations + Undo Integration

(Async: these actions run after script hook completes.)

These operations are applied by CupidFM and recorded in CupidFM's undo stack, so plugin-triggered actions work with fm.undo() / fm.redo().

Path args:

  • For path, you can pass either a single string OR a list of strings.

  • Relative paths are resolved under fm.cwd().

  • fm.copy(path, dst_dir) -> bool

    • Copy files/dirs into dst_dir (destination name uses the source basename).
    • path may be a string or a list of strings.
  • fm.move(path, dst_dir) -> bool

    • Move files/dirs into dst_dir.
    • path may be a string or a list of strings.
  • fm.rename(path, new_name) -> bool

    • Rename/move a single path.
    • If new_name is relative, it stays in the same parent dir as path.
    • If new_name is absolute, it is used as the full destination path.
  • fm.delete(path) -> bool

    • Soft-delete by moving into CupidFM's per-session trash (undoable).
    • path may be a string or a list of strings.
  • fm.mkdir(name_or_path) -> bool

    • Create a directory (relative paths are under fm.cwd()).
  • fm.touch(name_or_path) -> bool

    • Create an empty file (relative paths are under fm.cwd()).

Bulk helpers:

  • fm.selected_paths() -> list

    • Returns a list of selected paths (currently at most one: the current selection).
  • fm.each_selected(fn_or_name)

    • Calls your function once per selected path (currently at most once).
    • fn_or_name can be a function value OR the name of a function as a string.

Undo/redo:

  • fm.undo() -> bool
  • fm.redo() -> bool

Minimal Example Plugin

Save as ~/.cupidfm/plugins/example.cs:

fn on_load() {
  fm.notify("plugin loaded!");
  fm.bind("^K", "go_parent");
  fm.bind("^J", "select_readme");
  fm.bind("^D", "trash_selected");
}

fn go_parent(key) {
  fm.parent_dir();
  return true;
}

fn select_readme(key) {
  fm.select("README.md");
  return true;
}

fn trash_selected(key) {
  fm.delete(fm.selected_path());
  return true;
}

fn on_key(key) {
  return false; // let CupidFM handle
}

fn on_dir_change(new_cwd, old_cwd) {
  fm.status(fmt("dir: %s -> %s", old_cwd, new_cwd));
}

// optional:
fn on_selection_change(new_name, old_name) {
  // handle selection moved
}

Example: API demo plugin

File: plugins/examples/api_demo.cs

  • Logs the first few results from fm.entries() with fm.console.
  • Drives fuzzy search from scripts via fm.set_search(query) and fm.clear_search().
  • Drives navigation using fm.open_selected(), fm.enter_dir(), and fm.parent_dir().
  • Binds the console-friendly commands to F8–F12 so you can "stress-test" the new helpers.

Notes & Limitations

  • Plugins run in their own VMs, synchronously.
  • Side effects (cd, select) happen after event/handler returns.
  • Errors/exceptions: shown in the notification bar; full stack traces included.
  • API is intentionally minimal; expect (and request!) expansion.

CupidScript Standard Library & New Features

Registered by calling cs_register_stdlib(vm):

  • Core: print, assert, assert_eq, assert_ne, typeof, getenv
  • Lists/Maps: list, map, len, push, pop, mget, mset, mhas, keys
  • Lists/Maps (additional): insert, remove, slice, values, items, map_values, mdel
  • Copy helpers: copy, deepcopy
  • List helpers: reverse, reversed, contains
  • String utils: str_find, str_replace, str_split, string interpolation $(...)
  • String utils (additional): str_trim, str_ltrim, str_rtrim, str_lower, str_upper, str_startswith, str_endswith, str_repeat
  • Path utils: path_join, path_dirname, path_basename, path_ext
  • Formatting: fmt supports %d, %s, %b, %v, %%
  • Time/helpers: now_ms, sleep
  • Multi-file: load(path), require(path), require_optional(path)
  • Error handling: error, is_error, format_error, global ERR
  • Test helpers: assert_eq, assert_ne
  • New features:
    • Anonymous functions/closures
    • First-class functions (can pass/store/call)
    • Precise error stacktraces with file:line:col
    • String refcounting (safe for hosting APIs)
    • Top-level await support (host opt-in)
    • Plugin state via lists/maps
    • Async actions (navigation, selection happen after handlers)
    • Host can register arbitrary new natives (see API)

Error Reporting

Parse/runtime errors provide file:line:col and a full stacktrace:

Runtime error at examples/stacktrace.cs:3:15: division by zero
Stack trace:
  at inner (examples/stacktrace.cs:8:16)
  at outer (examples/stacktrace.cs:11:7)

cs_vm_last_error(vm) gives the last error message.

Host Safety Controls (CPU/timeout limits)

CupidScript includes safety controls to keep scripts from hanging the host.

Host-level (C API) examples:

  • cs_vm_set_instruction_limit(vm, N)
  • cs_vm_set_timeout(vm, ms)
  • cs_vm_interrupt(vm)

Script-level helpers (per wiki):

  • set_instruction_limit(n) / get_instruction_limit() / get_instruction_count()
  • set_timeout(ms) / get_timeout()

Embedding & C Native Extensions

To add new host→script functions (native API):

static int my_native(cs_vm* vm, void* ud, int argc, const cs_value* argv, cs_value* out) {
  // check, return result via *out (if used)
  if (out) *out = cs_nil();
  return 0; // return nonzero or use cs_error() on error
}
cs_register_native(vm, "my.native", my_native, NULL);

You may pass/retain cs_value (function refs, data) between script and C.


CupidScript API Reference/Quick Snapshots

Types: nil, bool, int, float, string, list, map, strbuf, function, native function

  • Lists/maps: list, push, pop, insert, remove, slice, reverse, reversed, contains, copy, deepcopy
  • Maps: map, mget, mset, mhas, mdel, keys, values, items, map_values
  • Strings: "..." (with \n, \t, \\, \" escapes), concatenation, indexing
  • Control flow: for x in xs {}, for (init; cond; incr) {}, ranges 0..10 / 0..=10, ternary c ? a : b
  • Errors: throw, try/catch, error, is_error, format_error, global ERR
  • Functions/closures:
    fn make_counter() {
      let n = 0;
      return fn() { n=n+1; return n; };
    }
  • String interpolation: "count: $(n)" (inject vars into strings)
  • List/map destructuring: let [a,b,...rest] = xs; (new)
  • Top-level await: (host opt-in) await sleep(100); (new)

Design Notes

  • Compact and fast stack/call VM.
  • All plugin natives registered via public C API.
  • String memory is refcounted.
  • Plugin VMs are fully isolated (no data sharing by default).
  • Modern scripting features and error handling.

License

CupidFM and CupidScript are licensed under the GNU General Public License v3.
See LICENSE or https://www.gnu.org/licenses/gpl-3.0.html for details.