A complete Node.js readline-compatible library for MoonBit, providing powerful terminal input capabilities with full async support.
- π₯ 100% Node.js readline API compatibility
- β‘ Async support - Complete async/await support
- π― Event-driven architecture - EventEmitter pattern
- π Advanced line editing - Based on GNU readline backend
- π History management - Search and navigation functionality
- π Tab completion - Customizable completion functions
β οΈ Signal handling - SIGINT, SIGTSTP, SIGCONT- π₯οΈ Terminal manipulation - Cursor control, screen clearing
- π Cross-platform support - Linux, macOS, Windows
Add this library to your MoonBit project:
moon add allwefantasy/readline.mbt
fn main {
let rl = @lib.create_interface_simple()
rl.question("What's your name? ", fn(name) {
println("Hello, \{name}!")
rl.close()
})
}
fn main {
@async.with_event_loop(fn(_) {
run_async_example()
}) catch { _ => () }
}
async fn run_async_example() -> Unit raise {
let name = await @lib.questionAsync("What's your name? ")
println("Hello, \{name}!")
}
For the best experience, it's recommended to install the GNU readline library:
brew install readline pkg-config
sudo apt-get install libreadline-dev libncurses5-dev pkg-config
# CentOS/RHEL
sudo yum install readline-devel ncurses-devel pkgconfig
# Fedora
sudo dnf install readline-devel ncurses-devel pkgconfig
pacman -S mingw-w64-x86_64-readline mingw-w64-x86_64-ncurses
This library uses intelligent detection scripts to automatically configure the readline library:
- β When readline library is found: Full functionality is used
β οΈ When not found: Automatically uses mock implementation (limited functionality)
If automatic detection fails, you can set environment variables:
export READLINE_ROOT=/path/to/readline
# or
export READLINE_INCLUDE_PATH=/path/to/readline/include
export READLINE_LIB_PATH=/path/to/readline/lib
Creates a readline interface with specified options.
let options = InterfaceOptions::new()
.with_input("stdin")
.with_output("stdout")
.with_prompt("> ")
.with_history_size(1000)
let rl = @lib.create_interface(options)
Simple interface creation.
let rl = @lib.create_interface_simple(input="stdin", output="stdout")
Ask a question and get user input.
rl.question("Enter your age: ", fn(age) {
println("You are \{age} years old")
})
Display the prompt and wait for input.
rl.set_prompt("custom> ")
rl.prompt()
Write data to the output stream.
rl.write("Output text\n")
Pause or resume the interface.
rl.pause() // Pause input processing
rl.resume() // Resume input processing
Close the interface and clean up resources.
rl.close()
The interface implements the EventEmitter pattern, compatible with Node.js:
// Line input event
rl.on_line(fn(line) {
println("Input: \{line}")
})
// Interface closed event
rl.on_close(fn() {
println("Goodbye!")
})
// Signal events
rl.on_sigint(fn() {
println("Ctrl+C pressed")
})
rl.on_sigtstp(fn() {
println("Ctrl+Z pressed")
})
The async interface provides Promise-based methods:
async fn interactive_session() -> Unit raise {
let rl = await @lib.createAsyncInterface()
defer rl.close()
let name = await rl.question("Name: ")
let age = await rl.question("Age: ")
println("Hello \{name}, age \{age}!")
}
Simple async question (automatically creates and closes interface).
let name = await @lib.questionAsync("Your name: ")
Async confirmation dialog.
let confirmed = await @lib.confirmAsync("Continue?", default=true)
Async menu selection.
let options = ["Option A", "Option B", "Option C"]
let choice = await @lib.selectFromMenuAsync("Select:", options)
Create custom completion functions:
fn my_completer(word: String) -> (Array[String], String) {
let completions = ["hello", "help", "history", "exit"]
let matches = Array::new()
for completion in completions {
if completion.starts_with(word) {
matches.push(completion)
}
}
(matches, word)
}
let options = InterfaceOptions::new().with_completer(my_completer)
let rl = @lib.create_interface(options)
// Clear history
rl.clear_history()
// Get history length
let len = mbt_readline_history_length()
// Set history size
let options = InterfaceOptions::new().with_history_size(500)
async fn user_form() -> Unit raise {
let rl = await @lib.createAsyncInterface()
defer rl.close()
let name = await rl.question("Full name: ")
let email = await rl.question("Email: ")
// Validate age input
let mut age = 0
while true {
let age_str = await rl.question("Age: ")
match age_str.parse_int() {
Ok(parsed_age) if parsed_age > 0 => {
age = parsed_age
break
}
_ => rl.write("Please enter a valid age.\n")
}
}
let confirmed = await @lib.confirmAsync("Is this information correct?")
if confirmed {
println("User registered: \{name}, \{email}, \{age}")
} else {
println("Registration cancelled")
}
}
fn main {
let commands = ["list", "create", "delete", "help", "exit"]
let rl = @lib.create_interface_simple()
rl.on_line(fn(line) {
let parts = line.trim().split(' ')
let command = parts[0]
let args = parts[1:]
match command {
"list" => list_items()
"create" => create_item(args)
"delete" => delete_item(args)
"help" => show_help()
"exit" => rl.close()
"" => rl.prompt()
_ => {
println("Unknown command: \{command}")
rl.prompt()
}
}
})
println("Welcome! Type 'help' for available commands.")
rl.prompt()
}
async fn calculator_example() -> Unit raise {
println("=== Interactive Calculator ===")
let rl = await @lib.createAsyncInterface()
defer rl.close()
rl.write("Interactive Calculator (type 'quit' to exit)\n")
rl.write("Enter expressions like: 5 + 3, 10 * 2, etc.\n\n")
while true {
let input = await rl.question("calc> ")
if input.trim().to_lower() == "quit" {
rl.write("Goodbye!\n")
break
}
match parse_and_calculate(input.trim()) {
Some(result) => rl.write("Result: \{result}\n")
None => rl.write("Invalid expression. Try something like '5 + 3'\n")
}
}
}
fn parse_and_calculate(expr: String) -> Double? {
let parts = expr.split(' ')
if parts.length() != 3 {
return None
}
let a = parts[0].parse_double().or_default(0.0)
let op = parts[1]
let b = parts[2].parse_double().or_default(0.0)
match op {
"+" => Some(a + b)
"-" => Some(a - b)
"*" => Some(a * b)
"/" if b != 0.0 => Some(a / b)
_ => None
}
}
This library is designed to be a drop-in replacement for Node.js readline. Here's a comparison:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'test> '
});
rl.on('line', (input) => {
console.log(`Received: ${input}`);
rl.prompt();
});
rl.question('What is your name? ', (name) => {
console.log(`Hello ${name}!`);
rl.close();
});
let options = InterfaceOptions::new()
.with_input("stdin")
.with_output("stdout")
.with_prompt("test> ")
let rl = @lib.create_interface(options)
rl.on_line(fn(input) {
println("Received: \{input}")
rl.prompt()
})
rl.question("What is your name? ", fn(name) {
println("Hello \{name}!")
rl.close()
})
let rl = @lib.create_interface_simple()
rl.on_close(fn() {
println("Interface closed")
})
// Handle errors in callbacks
rl.question("Input: ", fn(answer) {
try {
process_input(answer)
} catch {
err => println("Error: \{err}")
}
})
async fn safe_input() -> Unit raise {
let rl = await @lib.createAsyncInterface()
defer rl.close()
try {
let input = await rl.question("Enter data: ")
process_input(input)
} catch {
err => println("Failed to process input: \{err}")
}
}
- The library uses GNU readline for optimal terminal handling
- History is stored in memory and persists during the session
- Completion functions are called synchronously - keep them fast
- Event listeners are called in the order they were registered
- The async interface polls for input availability every 10ms by default
See examples in the examples/
directory:
# Basic usage examples
moon run examples/basic_usage.mbt
# Async examples
moon run examples/async_examples.mbt
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all examples work correctly
- Submit a pull request
MIT License - see LICENSE file for details.
- GNU readline library (libreadline)
- MoonBit async runtime
- POSIX-compatible operating system
-
"Failed to initialize readline"
- Ensure libreadline is installed
- Check that the library is in your system's library path
-
Completion not working
- Verify your completion function returns the correct format
- Check that tab completion is enabled in your terminal
-
Async functions hanging
- Ensure you're running inside
@async.with_event_loop
- Check that you're properly closing interfaces with
defer
- Ensure you're running inside
-
Signal handling not working
- Verify your terminal supports signal forwarding
- Check that signal handlers are properly registered
Enable debug output (if implemented):
// Debug mode (implementation-specific)
let options = InterfaceOptions::new().with_debug(true)
let rl = @lib.create_interface(options)
For more examples and advanced usage, see the examples/
directory in the repository.
π Need Help? Please create an issue on GitHub or check the example code.