use std::io::Write as _;
use rusty_bubbles::textinput;
use rusty_bubbletea::model::Model as ModelTrait;
use rusty_bubbletea::{print_f, quit, Cmd, KeyPressMsg, Msg, Program, View};
struct Model {
input: textinput::Model,
err: Option<String>,
}
impl Model {
fn new() -> Self {
let mut ti = textinput::new();
let _ = ti.focus();
ti.char_limit = 156;
ti.set_width(20);
ti.set_virtual_cursor(false);
Model {
input: ti,
err: None,
}
}
}
impl ModelTrait for Model {
fn init(&self) -> Cmd {
None
}
fn update(&mut self, msg: &dyn Msg) -> Cmd {
let mut cmds: Vec<Cmd> = Vec::new();
if let Some(k) = msg.as_any().downcast_ref::<KeyPressMsg>() {
self.err = None;
match k.0.to_string().as_str() {
"ctrl+c" => return quit(),
"enter" => {
let val = "\"".to_string() + &self.input.value() + "\"";
let seq = match unescape(&val) {
Ok(s) => s,
Err(e) => {
self.err = Some(e);
return None;
}
};
if !seq.starts_with('\u{1b}') {
self.err = Some("sequence is not an ANSI escape sequence".to_string());
return None;
}
self.input.set_value("");
return Some(Box::new(move || {
let _ = std::io::stdout().write_all(seq.as_bytes());
None
}));
}
_ => {}
}
}
if !msg.as_any().is::<KeyPressMsg>() {
if let Some(s) = go_fmt(msg) {
cmds.push(print_f(format_args!("Received message: {}", s)));
}
}
let cmd = self.input.update(msg);
cmds.push(cmd);
rusty_bubbletea::batch(cmds)
}
fn view(&self) -> View {
let mut s = self.input.view();
if let Some(err) = &self.err {
s += &format!("\n\nError: {}", err);
}
s += "\n\nPress ctrl+c to quit, enter to write the sequence to terminal";
let mut v = View::new(&s);
v.cursor = self.input.cursor();
v
}
}
fn unescape(s: &str) -> Result<String, String> {
let chars: Vec<char> = s.chars().collect();
if chars.len() < 2 || chars[0] != '"' || chars[chars.len() - 1] != '"' {
return Err("invalid syntax".to_string());
}
let mut out = String::new();
let mut i = 1;
while i < chars.len() - 1 {
let c = chars[i];
if c != '\\' {
out.push(c);
i += 1;
continue;
}
if i + 1 >= chars.len() - 1 {
return Err("invalid syntax".to_string());
}
match chars[i + 1] {
'\\' => {
out.push('\\');
i += 2;
}
'"' => {
out.push('"');
i += 2;
}
'n' => {
out.push('\n');
i += 2;
}
'r' => {
out.push('\r');
i += 2;
}
't' => {
out.push('\t');
i += 2;
}
'x' => {
if i + 3 >= chars.len() - 1 {
return Err("invalid syntax".to_string());
}
let hex: String = chars[i + 2..i + 4].iter().collect();
let byte =
u8::from_str_radix(&hex, 16).map_err(|_| "invalid syntax".to_string())?;
out.push(byte as char);
i += 4;
}
_ => return Err("invalid syntax".to_string()),
}
}
Ok(out)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let p = Program::new(Model::new());
p.run()?;
Ok(())
}
fn go_fmt(msg: &dyn rusty_bubbletea::Msg) -> Option<String> {
use rusty_bubbletea::environ::EnvMsg;
use rusty_bubbletea::profile::ColorProfileMsg;
use rusty_bubbletea::screen::WindowSizeMsg;
if let Some(m) = msg.as_any().downcast_ref::<WindowSizeMsg>() {
return Some(format!(
"tea.WindowSizeMsg {{Width:{} Height:{}}}",
m.width, m.height
));
}
if let Some(m) = msg.as_any().downcast_ref::<EnvMsg>() {
let mut kv: Vec<String> = m.vars.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
kv.sort();
return Some(format!("tea.EnvMsg [{}]", kv.join(" ")));
}
if let Some(m) = msg.as_any().downcast_ref::<ColorProfileMsg>() {
let profile = match m.profile {
rusty_bubbletea::profile::ColorProfile::TrueColor => "TrueColor",
rusty_bubbletea::profile::ColorProfile::ANSI256 => "ANSI256",
rusty_bubbletea::profile::ColorProfile::ANSI => "ANSI",
rusty_bubbletea::profile::ColorProfile::Ascii => "Ascii",
};
return Some(format!("tea.ColorProfileMsg {}", profile));
}
None
}