use crate::{Environment, Expression};
use std::collections::BTreeMap;
use std::fs::OpenOptions;
use crate::libs::BuiltinInfo;
use crate::libs::helper::{check_args_len, check_exact_args_len, get_string_ref};
use crate::libs::lazy_module::LazyModule;
use crate::{Int, RuntimeError, reg_info, reg_lazy};
use crossterm::cursor::{
Hide, MoveDown, MoveLeft, MoveRight, MoveTo, MoveUp, RestorePosition, SavePosition, Show,
};
use crossterm::event::{Event, KeyCode, read};
use crossterm::style::Print;
use crossterm::terminal::{
Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode,
enable_raw_mode, size,
};
use crossterm::{execute, queue};
use std::io::{Write, stdout};
pub fn regist_lazy() -> LazyModule {
reg_lazy!({
width, height,
write, title, clear, flush, bell,
raw_mode, alt_screen, line_wrap,
cursor_to, cursor_up, cursor_down, cursor_left, cursor_right, cursor_save, cursor_restore, cursor_hide, cursor_show,
read_line, read_password, read_key,
keys,
print_tty, discard
})
}
pub fn regist_info() -> BTreeMap<&'static str, BuiltinInfo> {
reg_info!({
width => "console width", ""
height => "console height", ""
write => "write text at position", "<text> <x> <y>"
title => "set console title", "<string>"
clear => "clear console", ""
flush => "flush stdout", ""
bell => "ring terminal bell", ""
raw_mode => "get/set raw mode", "[bool]"
alt_screen => "enter/leave alternate screen", "<bool>"
line_wrap => "enable/disable line wrap", "<bool>"
cursor_to => "move cursor to position", "<x> <y>"
cursor_up => "move cursor up n rows", "<n>"
cursor_down => "move cursor down n rows", "<n>"
cursor_left => "move cursor left n cols", "<n>"
cursor_right => "move cursor right n cols", "<n>"
cursor_save => "save cursor position", ""
cursor_restore => "restore cursor position", ""
cursor_hide => "hide cursor", ""
cursor_show => "show cursor", ""
read_line => "read line from stdin", "[prompt]"
read_password => "read password, masked", "[prompt]"
read_key => "read one key, enters raw mode temporarily. e.g. 'enter','f1','a'", ""
keys => "list special key names", ""
print_tty => "write raw text directly to tty, bypass pipes", "<text>"
discard => "no-op, discards args", "<args>..."
})
}
fn width(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
size()
.map(|(w, _)| Expression::Integer(w as Int))
.or(Ok(Expression::None))
}
fn height(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
size()
.map(|(_, h)| Expression::Integer(h as Int))
.or(Ok(Expression::None))
}
fn write(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("write", &args, 3, ctx)?;
let x = &args[1];
let y = &args[2];
match (x, y) {
(Expression::Integer(x), Expression::Integer(y)) => {
let content_str = args[0].to_string();
let mut out = stdout();
for (y_offset, line) in content_str.lines().enumerate() {
queue!(
out,
SavePosition,
MoveTo(*x as u16, (*y + y_offset as Int) as u16),
Print(line),
RestorePosition,
)
.map_err(|e| {
RuntimeError::common(format!("Write failed: {e}").into(), ctx.clone(), 0)
})?;
}
out.flush().map_err(|e| {
RuntimeError::common(format!("Flush failed: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::None)
}
(m, n) => Err(RuntimeError::common(
format!(
"Expected integers for position, got ({} {:?}, {} {:?})",
m.type_name(),
m,
n.type_name(),
n
)
.into(),
ctx.clone(),
0,
)),
}
}
fn title(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("title", &args, 1, ctx)?;
execute!(stdout(), SetTitle(args[0].to_string())).map_err(|e| {
RuntimeError::common(format!("Failed to set title: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::None)
}
fn clear(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))
.map_err(|_| RuntimeError::common("Clear failed".into(), _ctx.clone(), 0))?;
Ok(Expression::None)
}
fn flush(
_args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
stdout()
.flush()
.map_err(|e| RuntimeError::common(format!("Flush failed: {e}").into(), ctx.clone(), 0))?;
Ok(Expression::None)
}
fn raw_mode(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
if args.is_empty() {
let r = crossterm::terminal::is_raw_mode_enabled().map_err(|_| {
RuntimeError::common(
"Failed to detect whether raw mode is enabled".into(),
ctx.clone(),
0,
)
})?;
return Ok(Expression::Boolean(r));
} else {
if args[0].is_truthy() {
enable_raw_mode().map_err(|_| {
RuntimeError::common("Failed to enable raw mode".into(), ctx.clone(), 0)
})?;
} else {
disable_raw_mode().map_err(|_| {
RuntimeError::common("Failed to disable raw mode".into(), ctx.clone(), 0)
})?;
}
return Ok(Expression::None);
}
}
fn alt_screen(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("alt_screen", &args, 1, ctx)?;
if args[0].is_truthy() {
execute!(stdout(), EnterAlternateScreen).map_err(|_| {
RuntimeError::common("Failed to enter alternate screen".into(), ctx.clone(), 0)
})?;
} else {
execute!(stdout(), LeaveAlternateScreen).map_err(|_| {
RuntimeError::common("Failed to leave alternate screen".into(), ctx.clone(), 0)
})?;
}
return Ok(Expression::None);
}
fn line_wrap(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("line_wrap", &args, 1, ctx)?;
if args[0].is_truthy() {
execute!(stdout(), crossterm::terminal::EnableLineWrap).map_err(|_| {
RuntimeError::common("Failed to enable line wrap".into(), ctx.clone(), 0)
})?;
} else {
execute!(stdout(), crossterm::terminal::DisableLineWrap).map_err(|_| {
RuntimeError::common("Failed to disable line wrap".into(), ctx.clone(), 0)
})?;
}
return Ok(Expression::None);
}
fn cursor_to(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("cursor_to", &args, 2, ctx)?;
match (&args[0], &args[1]) {
(Expression::Integer(x), Expression::Integer(y)) => {
execute!(stdout(), MoveTo(*x as u16, *y as u16)).map_err(|e| {
RuntimeError::common(format!("Failed to move cursor: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::None)
}
(m, n) => Err(RuntimeError::common(
format!(
"Expected integers for position, got ({} {:?}, {} {:?})",
m.type_name(),
m,
n.type_name(),
n
)
.into(),
ctx.clone(),
0,
)),
}
}
fn cursor_up(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("cursor_up", &args, 1, ctx)?;
if let Expression::Integer(n) = args[0] {
execute!(stdout(), MoveUp(n as u16)).map_err(|e| {
RuntimeError::common(format!("Failed to move cursor: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::None)
} else {
Err(RuntimeError::common(
format!("Expected integer for movement amount, got {:?}", args[0]).into(),
ctx.clone(),
0,
))
}
}
fn cursor_down(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("cursor_down", &args, 1, ctx)?;
if let Expression::Integer(n) = args[0] {
execute!(stdout(), MoveDown(n as u16)).map_err(|e| {
RuntimeError::common(format!("Failed to move cursor: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::None)
} else {
Err(RuntimeError::common(
format!("Expected integer for movement amount, got {:?}", args[0]).into(),
ctx.clone(),
0,
))
}
}
fn cursor_left(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("cursor_left", &args, 1, ctx)?;
if let Expression::Integer(n) = args[0] {
execute!(stdout(), MoveLeft(n as u16)).map_err(|e| {
RuntimeError::common(format!("Failed to move cursor: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::None)
} else {
Err(RuntimeError::common(
format!("Expected integer for movement amount, got {:?}", args[0]).into(),
ctx.clone(),
0,
))
}
}
fn cursor_right(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("cursor_right", &args, 1, ctx)?;
if let Expression::Integer(n) = args[0] {
execute!(stdout(), MoveRight(n as u16)).map_err(|e| {
RuntimeError::common(format!("Failed to move cursor: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::None)
} else {
Err(RuntimeError::common(
format!("Expected integer for movement amount, got {:?}", args[0]).into(),
ctx.clone(),
0,
))
}
}
fn cursor_save(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
execute!(stdout(), SavePosition).map_err(|_| {
RuntimeError::common("Failed to save cursor position".into(), _ctx.clone(), 0)
})?;
Ok(Expression::None)
}
fn cursor_restore(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
execute!(stdout(), RestorePosition).map_err(|_| {
RuntimeError::common("Failed to restore cursor position".into(), _ctx.clone(), 0)
})?;
Ok(Expression::None)
}
fn cursor_hide(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
execute!(stdout(), Hide)
.map_err(|_| RuntimeError::common("Failed to hide cursor".into(), _ctx.clone(), 0))?;
Ok(Expression::None)
}
fn cursor_show(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
execute!(stdout(), Show)
.map_err(|_| RuntimeError::common("Failed to show cursor".into(), _ctx.clone(), 0))?;
Ok(Expression::None)
}
const SPECIAL_KEY_MAPPINGS: &[(&str, KeyCode)] = &[
("space", KeyCode::Char(' ')),
("enter", KeyCode::Enter),
("backspace", KeyCode::Backspace),
("delete", KeyCode::Delete),
("left", KeyCode::Left),
("right", KeyCode::Right),
("up", KeyCode::Up),
("down", KeyCode::Down),
("home", KeyCode::Home),
("end", KeyCode::End),
("page_up", KeyCode::PageUp),
("page_down", KeyCode::PageDown),
("tab", KeyCode::Tab),
("esc", KeyCode::Esc),
("insert", KeyCode::Insert),
("f1", KeyCode::F(1)),
("f2", KeyCode::F(2)),
("f3", KeyCode::F(3)),
("f4", KeyCode::F(4)),
("f5", KeyCode::F(5)),
("f6", KeyCode::F(6)),
("f7", KeyCode::F(7)),
("f8", KeyCode::F(8)),
("f9", KeyCode::F(9)),
("f10", KeyCode::F(10)),
("f11", KeyCode::F(11)),
("f12", KeyCode::F(12)),
("null", KeyCode::Null),
("back_tab", KeyCode::BackTab),
];
fn key_code_name(code: KeyCode) -> Option<&'static str> {
SPECIAL_KEY_MAPPINGS
.iter()
.find(|(_, k)| *k == code)
.map(|(name, _)| *name)
}
fn keys(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
Ok(Expression::from(
SPECIAL_KEY_MAPPINGS
.iter()
.map(|(name, _)| Expression::String(name.to_string()))
.collect::<Vec<_>>(),
))
}
fn read_line(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
if let Some(prompt) = args.get(0) {
println!("{}", prompt.to_string())
}
let mut input = String::new();
std::io::stdin().read_line(&mut input).map_err(|e| {
RuntimeError::common(format!("Failed to read line: {e}").into(), ctx.clone(), 0)
})?;
Ok(Expression::String(input.trim_end_matches("\n").to_string()))
}
fn read_password(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_args_len("read_password", &args, 0..=1, ctx)?;
let rst = if !args.is_empty() {
rpassword::prompt_password(args[0].to_string())
} else {
rpassword::prompt_password("")
};
let r = rst.map_err(|e| {
RuntimeError::common(
format!("Failed to read password: {e}").into(),
ctx.clone(),
0,
)
})?;
Ok(Expression::String(r))
}
fn read_key(
_args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
enable_raw_mode()
.map_err(|_| RuntimeError::common("Failed to enable raw mode".into(), ctx.clone(), 0))?;
let result = loop {
match read() {
Ok(Event::Key(event)) => {
let key_str = key_code_name(event.code)
.map(|s| s.to_string())
.unwrap_or_else(|| match event.code {
KeyCode::Char(c) => c.to_string(),
_ => format!("{:?}", event.code),
});
break Ok(Expression::String(key_str));
}
Ok(_) => continue, Err(e) => {
break Err(RuntimeError::common(
format!("Failed to read key: {e}").into(),
ctx.clone(),
0,
));
}
}
};
disable_raw_mode()
.map_err(|_| RuntimeError::common("Failed to disable raw mode".into(), ctx.clone(), 0))?;
result
}
fn print_tty(
args: Vec<Expression>,
_env: &mut Environment,
ctx: &Expression,
) -> Result<Expression, RuntimeError> {
check_exact_args_len("print_tty", &args, 1, ctx)?;
let tty_path = if cfg!(windows) {
"CON" } else {
"/dev/tty" };
let mut tty = OpenOptions::new()
.write(true)
.open(tty_path)
.map_err(|e| RuntimeError::from_io_error(e, "open tty".into(), Expression::None, 0))?;
let v = get_string_ref(&args[0], ctx)?;
tty.write_all(v.as_bytes())
.map_err(|e| RuntimeError::from_io_error(e, "write tty".into(), Expression::None, 0))?;
Ok(Expression::None)
}
fn bell(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
print!("\x07");
Ok(Expression::None)
}
fn discard(
_args: Vec<Expression>,
_env: &mut Environment,
_ctx: &Expression,
) -> Result<Expression, RuntimeError> {
Ok(Expression::None)
}