use std::io::{self, IsTerminal, Write};
use std::thread;
use std::time::Duration;
use clap::builder::styling::{Color, Effects, RgbColor, Style, Styles};
use clap::{CommandFactory, Parser, Subcommand};
const TISH_BANNER_LINES: &[&str] = &[
"",
"████████╗██╗███████╗██╗ ██╗",
"╚══██╔══╝██║██╔════╝██║ ██║",
" ██║ ██║███████╗███████║",
" ██║ ██║╚════██║██╔══██║",
" ██║ ██║███████║██║ ██║",
" ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝",
];
const BANNER_REVEAL_FRAMES: usize = 14;
const BANNER_CYCLE_FRAMES: usize = 4;
const BANNER_FRAME_MS: u64 = 20;
const PALETTE: &[(u8, u8, u8)] = &[
(255, 159, 64), (255, 213, 64), (52, 199, 89), (48, 209, 188), (10, 132, 255), (175, 82, 222), (255, 55, 148), ];
fn ease_out_cubic(t: f32) -> f32 {
let u = 1.0 - t.clamp(0.0, 1.0);
1.0 - u * u * u
}
fn lerp_color(a: (u8, u8, u8), b: (u8, u8, u8), t: f32) -> (u8, u8, u8) {
let t = t.clamp(0.0, 1.0);
(
(a.0 as f32 + (b.0 as f32 - a.0 as f32) * t).round() as u8,
(a.1 as f32 + (b.1 as f32 - a.1 as f32) * t).round() as u8,
(a.2 as f32 + (b.2 as f32 - a.2 as f32) * t).round() as u8,
)
}
fn palette_color(row: usize, col: usize, color_frame: usize) -> (u8, u8, u8) {
let n = PALETTE.len();
let scroll = color_frame as f32 * 0.22;
let pos = ((col as f32 / 5.0) + (row as f32 * 0.25) + scroll).rem_euclid(n as f32);
let lo = pos.floor() as usize % n;
let hi = (lo + 1) % n;
lerp_color(PALETTE[lo], PALETTE[hi], pos.fract())
}
fn write_tish_banner_frame(out: &mut impl Write, reveal_t: f32, color_frame: usize) {
for (row, line) in TISH_BANNER_LINES.iter().enumerate() {
let chars: Vec<char> = line.chars().collect();
let len = chars.len();
let visible = ((len as f32) * reveal_t).round() as usize;
let visible = visible.min(len);
for col in 0..len {
let ch = chars[col];
if col >= visible || ch == ' ' {
let _ = write!(out, " ");
} else {
let (r, g, b) = palette_color(row, col, color_frame);
let _ = write!(out, "\x1b[1;38;2;{r};{g};{b}m{ch}\x1b[0m");
}
}
let _ = writeln!(out);
}
}
fn print_tish_banner_plain(out: &mut impl Write) {
for line in TISH_BANNER_LINES {
let _ = writeln!(out, "{line}");
}
let _ = writeln!(out);
}
fn print_tish_banner_animated(out: &mut impl Write) {
let n = TISH_BANNER_LINES.len();
let total = BANNER_REVEAL_FRAMES + BANNER_CYCLE_FRAMES;
for f in 0..total {
if f > 0 {
let _ = write!(out, "\x1b[{n}A");
}
let reveal_t = if f < BANNER_REVEAL_FRAMES {
ease_out_cubic((f + 1) as f32 / BANNER_REVEAL_FRAMES as f32)
} else {
1.0
};
write_tish_banner_frame(out, reveal_t, f);
let _ = out.flush();
thread::sleep(Duration::from_millis(BANNER_FRAME_MS));
}
let _ = writeln!(out);
}
pub fn print_tish_banner() {
let mut out = io::stdout().lock();
if io::stdout().is_terminal() {
print_tish_banner_animated(&mut out);
} else {
print_tish_banner_plain(&mut out);
}
}
pub fn build_command() -> clap::Command {
Cli::command()
.after_help(cli_after_help())
.mut_subcommand("run", |sub| sub.after_help(run_after_help()))
.mut_subcommand("repl", |sub| sub.after_help(repl_after_help()))
.mut_subcommand("build", |sub| sub.after_long_help(build_after_help()))
}
fn count_help_lines(cmd: &mut clap::Command, sub_name: Option<&str>) -> usize {
let mut buf = Vec::<u8>::new();
if let Some(name) = sub_name {
if cmd.find_subcommand(name).is_some() {
let _ = cmd
.find_subcommand_mut(name)
.unwrap()
.write_long_help(&mut buf);
} else {
let _ = cmd.write_long_help(&mut buf);
}
} else {
let _ = cmd.write_long_help(&mut buf);
}
buf.iter().filter(|&&b| b == b'\n').count()
}
fn print_help_to_stdout(cmd: &mut clap::Command, sub_name: Option<&str>) {
if let Some(name) = sub_name {
if cmd.find_subcommand(name).is_some() {
let _ = cmd.find_subcommand_mut(name).unwrap().print_long_help();
return;
}
}
let _ = cmd.print_long_help();
}
fn sub_name_from_argv(argv: &[String]) -> Option<String> {
match argv.get(1).map(String::as_str) {
Some("help") => argv.get(2).map(String::to_string), Some(s) if !s.starts_with('-') => Some(s.to_string()), _ => None,
}
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
const H_PURPLE: &str = "\x1b[1;38;2;175;82;222m";
const H_GREY: &str = "\x1b[38;2;150;150;150m";
const H_PINK: &str = "\x1b[38;2;255;55;148m";
const H_RESET: &str = "\x1b[0m";
fn print_small_header() {
if io::stdout().is_terminal() {
println!("{H_PURPLE}Tish{H_RESET} {H_GREY}(version {VERSION}){H_RESET}");
println!("{H_PINK}https://tishlang.com{H_RESET}\n");
} else {
println!("Tish (version {VERSION})");
println!("https://tishlang.com\n");
}
}
const MAIN_PREFIX_LINES: usize = 4;
pub fn print_banner_with_help(argv: &[String]) {
let sub_name = sub_name_from_argv(argv);
let sub = sub_name.as_deref();
if sub.is_some() {
print_small_header();
let mut cmd = build_command();
cmd.build();
print_help_to_stdout(&mut cmd, sub);
return;
}
if !io::stdout().is_terminal() {
let mut out = io::stdout().lock();
print_tish_banner_plain(&mut out);
drop(out);
let mut cmd = build_command().color(clap::ColorChoice::Never);
cmd.build();
print_help_to_stdout(&mut cmd, sub);
return;
}
let h: usize = {
let mut cmd = build_command().color(clap::ColorChoice::Never);
cmd.build();
count_help_lines(&mut cmd, sub)
};
let n = TISH_BANNER_LINES.len();
{
let mut out = io::stdout().lock();
write_tish_banner_frame(&mut out, 1.0, 0);
let _ = writeln!(out); let _ = writeln!(
out,
"{H_PURPLE}Tish{H_RESET} {H_GREY}(version {VERSION}){H_RESET}"
);
let _ = writeln!(out, "Minimal TS/JS-ish language");
let _ = writeln!(out, "{H_PINK}https://tishlang.com{H_RESET}");
let _ = writeln!(out); let _ = out.flush();
}
{
let mut cmd = build_command();
cmd.build();
print_help_to_stdout(&mut cmd, sub);
let _ = io::stdout().flush();
}
{
let mut out = io::stdout().lock();
let _ = write!(out, "\x1b[{}A", n + 1 + MAIN_PREFIX_LINES + h);
let _ = out.flush();
let frames = BANNER_CYCLE_FRAMES;
for f in 0..frames {
write_tish_banner_frame(&mut out, 1.0, f);
if f < frames - 1 {
let _ = write!(out, "\x1b[{}A", n);
}
let _ = out.flush();
thread::sleep(Duration::from_millis(BANNER_FRAME_MS));
}
let _ = write!(out, "\x1b[{}B", 1 + MAIN_PREFIX_LINES + h);
let _ = writeln!(out);
let _ = out.flush();
}
}
pub fn argv_requests_help(argv: &[String]) -> bool {
argv.iter().any(|a| a == "--help" || a == "-h")
|| matches!(argv.get(1).map(String::as_str), Some("help"))
}
fn rgb_bold(r: u8, g: u8, b: u8) -> Style {
Style::new().fg_color(Some(Color::Rgb(RgbColor(r, g, b)))) | Effects::BOLD
}
pub fn cargo_help_styles() -> Styles {
Styles::styled()
.header(rgb_bold(255, 159, 64)) .usage(rgb_bold(255, 159, 64)) .literal(rgb_bold(48, 209, 188)) .placeholder(rgb_bold(255, 213, 64)) .error(rgb_bold(255, 55, 148)) .valid(rgb_bold(52, 199, 89)) .invalid(rgb_bold(255, 55, 148)) }
pub fn cli_after_help() -> String {
let (oh, t, r) = if io::stdout().is_terminal() {
(
"\x1b[1;38;2;255;159;64m",
"\x1b[1;38;2;48;209;188m",
"\x1b[0m",
)
} else {
("", "", "")
};
format!(
"\
{oh}Environment variables:{r}
{t}TISH_NO_OPTIMIZE=1{r}
Disable AST and bytecode optimizations for run/build
See {t}tish run --help{r} and {t}tish build --help{r} for backend and feature options."
)
}
fn capabilities_section(oh: &str, t: &str, r: &str) -> String {
format!(
"\
{oh}Backends{r} (--backend):
{t}vm{r}
Bytecode VM (default)
{t}interp{r}
Tree-walking interpreter
{oh}Capabilities{r} (--feature, repeatable; comma-separated values are split):
{t}http{r}
Network: fetch, fetchAll, serve, Promise (and `await`); enabling http also enables timers
{t}timers{r}
setTimeout, setInterval, clearTimeout, clearInterval (global + `import from \"timers\"` / tish:timers)
{t}fs{r}
Filesystem: readFile, writeFile, fileExists, isDir, readDir, mkdir
{t}process{r}
process.exit, cwd, exec, argv, env
{t}regex{r}
RegExp
{t}ws{r}
WebSocket client / server
{t}full{r}
All of the above (http, timers, fs, process, regex, ws)
Omit --feature to allow every capability compiled into this `tish` binary; pass flags to restrict what scripts may use. The CLI is normally built with all of them (Cargo default on `tishlang`)."
)
}
pub fn run_after_help() -> String {
let (oh, t, r) = if io::stdout().is_terminal() {
(
"\x1b[1;38;2;255;159;64m",
"\x1b[1;38;2;48;209;188m",
"\x1b[0m",
)
} else {
("", "", "")
};
capabilities_section(oh, t, r)
}
pub fn repl_after_help() -> String {
let (oh, t, r) = if io::stdout().is_terminal() {
(
"\x1b[1;38;2;255;159;64m",
"\x1b[1;38;2;48;209;188m",
"\x1b[0m",
)
} else {
("", "", "")
};
capabilities_section(oh, t, r)
}
pub fn build_after_help() -> String {
let (oh, t, r) = if io::stdout().is_terminal() {
(
"\x1b[1;38;2;255;159;64m",
"\x1b[1;38;2;48;209;188m",
"\x1b[0m",
)
} else {
("", "", "")
};
format!(
"\
{oh}Build targets{r} (--target, default: native):
{t}native{r}
Native executable (see --native-backend)
{t}js{r}
JavaScript bundle
{t}wasm{r}
WebAssembly (.tish project; .js source supported on some paths)
{t}wasi{r}
WASI WebAssembly
{t}bytecode{r}
Raw serialized bytecode chunk (no VM binary/JS/HTML); for hosts that already ship the runtime
{oh}Native backends{r} (--native-backend, only with --target native, default: rust):
{t}rust{r}
Emit Rust + link tishlang_runtime via cargo
{t}cranelift{r}
Embedded bytecode + Cranelift/VM runtime binary
{t}llvm{r}
Embedded bytecode + LLVM/clang link path
{oh}Capabilities{r} (--feature, repeatable; comma-separated values are split):
{t}http{r}
Network: fetch, fetchAll, serve, Promise (and `await`); enabling http also enables timers
{t}timers{r}
setTimeout, setInterval, clearTimeout, clearInterval (global + `import from \"timers\"` / tish:timers)
{t}fs{r}
Filesystem: readFile, writeFile, fileExists, isDir, readDir, mkdir
{t}process{r}
process.exit, cwd, exec, argv, env
{t}regex{r}
RegExp
{t}ws{r}
WebSocket client / server
{t}full{r}
All of the above (http, timers, fs, process, regex, ws)
For `--target native`, these choose what is linked into the **output** executable (omit = same set as this `tish` binary was built with). Minimal native outputs still use a full `tish` CLI unless you built it with `cargo build -p tishlang --no-default-features`."
)
}
#[derive(Parser)]
#[command(name = "tish")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(styles = cargo_help_styles())]
pub(crate) struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Parser)]
pub(crate) struct RunArgs {
#[arg(
required = true,
allow_hyphen_values = true,
value_name = "FILE",
help_heading = "Arguments"
)]
pub file: String,
#[arg(
long,
default_value = "vm",
value_name = "NAME",
help_heading = "Options"
)]
pub backend: String,
#[arg(
long = "feature",
value_name = "NAME",
action = clap::ArgAction::Append,
help_heading = "Options"
)]
pub features: Vec<String>,
#[arg(long, help_heading = "Options")]
pub no_optimize: bool,
}
#[derive(Parser)]
pub(crate) struct ReplArgs {
#[arg(
long,
default_value = "vm",
value_name = "NAME",
help_heading = "Options"
)]
pub backend: String,
#[arg(
long = "feature",
value_name = "NAME",
action = clap::ArgAction::Append,
help_heading = "Options"
)]
pub features: Vec<String>,
#[arg(long, help_heading = "Options")]
pub no_optimize: bool,
}
#[derive(Parser)]
pub(crate) struct BuildArgs {
#[arg(
short,
long,
default_value = "tish_out",
value_name = "PATH",
help_heading = "Options"
)]
pub output: String,
#[arg(
long,
default_value = "native",
value_name = "TARGET",
help_heading = "Options"
)]
pub target: String,
#[arg(
long,
default_value = "rust",
value_name = "BACKEND",
help_heading = "Options"
)]
pub native_backend: String,
#[arg(
long = "feature",
value_name = "NAME",
action = clap::ArgAction::Append,
help_heading = "Options"
)]
pub features: Vec<String>,
#[arg(long, value_name = "TRIPLE", help_heading = "Options")]
pub ios_triple: Option<String>,
#[arg(long, value_name = "TYPE", default_value = "bin", help_heading = "Options")]
pub crate_type: String,
#[arg(long, help_heading = "Options")]
pub no_optimize: bool,
#[arg(long, help_heading = "Options")]
pub source_map: bool,
#[arg(required = true, value_name = "FILE", help_heading = "Arguments")]
pub file: String,
}
#[derive(Subcommand)]
pub(crate) enum Commands {
Run(RunArgs),
Repl(ReplArgs),
Build(BuildArgs),
#[command(name = "dump-ast")]
DumpAst {
#[arg(required = true, value_name = "FILE", help_heading = "Arguments")]
file: String,
},
}