use crate::claims::*;
use crate::ctl::*;
use crate::drain::*;
use crate::keys::*;
use crate::par::*;
use crate::reg::*;
use crate::util::{convert_error, Result, WASH_CMD_INFO, WASH_LOG_INFO};
use crossterm::event::{poll, read, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use log::{error, info, LevelFilter};
use std::io::{self, Stdout};
use std::sync::{Arc, Mutex};
use std::{cell::RefCell, rc::Rc};
use structopt::{clap::AppSettings, StructOpt};
use tui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
use tui_logger::*;
use wasmcloud_host::HostBuilder;
const CTL_NS: &str = "default";
const WASH_PROMPT: &str = "wash> ";
#[derive(Debug, StructOpt, Clone)]
#[structopt(
global_settings(&[AppSettings::ColoredHelp, AppSettings::VersionlessSubcommands]),
name = "up")]
pub(crate) struct UpCli {
#[structopt(flatten)]
command: UpCliCommand,
}
impl UpCli {
pub(crate) fn command(self) -> UpCliCommand {
self.command
}
}
#[derive(StructOpt, Debug, Clone)]
pub(crate) struct UpCliCommand {
#[structopt(
short = "h",
long = "host",
default_value = "0.0.0.0",
env = "WASH_RPC_HOST"
)]
rpc_host: String,
#[structopt(
short = "p",
long = "port",
default_value = "4222",
env = "WASH_RPC_PORT"
)]
rpc_port: String,
#[structopt(short = "l", long = "log-level", default_value = "debug")]
log_level: LogLevel,
}
#[derive(StructOpt, Debug, Clone, PartialEq)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl std::str::FromStr for LogLevel {
type Err = std::io::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
"trace" => Ok(LogLevel::Trace),
_ => Ok(LogLevel::Trace),
}
}
}
pub(crate) async fn handle_command(command: UpCliCommand) -> Result<()> {
let UpCliCommand { .. } = command;
handle_up(command).await
}
#[derive(StructOpt, Debug, Clone)]
#[structopt(name = "wash>", global_settings(&[AppSettings::NoBinaryName, AppSettings::DisableVersion, AppSettings::ColorNever]))]
struct ReplCli {
#[structopt(flatten)]
cmd: ReplCliCommand,
}
#[derive(StructOpt, Debug, Clone)]
#[structopt(global_settings(&[AppSettings::ColorNever, AppSettings::DisableVersion, AppSettings::VersionlessSubcommands]))]
enum ReplCliCommand {
#[structopt(name = "drain")]
Drain(DrainCliCommand),
#[structopt(name = "ctl")]
Ctl(CtlCliCommand),
#[structopt(name = "claims")]
Claims(ClaimsCliCommand),
#[structopt(name = "keys")]
Keys(KeysCliCommand),
#[structopt(name = "par")]
Par(ParCliCommand),
#[structopt(name = "reg")]
Reg(RegCliCommand),
#[structopt(name = "quit", aliases = &["exit", "logout", "q", ":q!"])]
Quit,
#[structopt(name = "clear")]
Clear,
}
#[derive(Debug, Clone)]
struct InputState {
history: Vec<Vec<char>>,
history_cursor: usize,
input: Vec<char>,
input_cursor: usize,
multiline_history: u16, input_width: usize,
}
impl Default for InputState {
fn default() -> Self {
InputState {
history: vec![],
history_cursor: 0,
input: vec![],
input_cursor: 0,
multiline_history: 0,
input_width: 0,
}
}
}
impl InputState {
fn cursor_location(&self) -> (u16, u16) {
let mut position = (0, 0);
position.0 += WASH_PROMPT.len();
for _c in 0..self.input_cursor {
position.0 += 1;
if position.0 == self.input_width {
position.0 = 0;
position.1 += 1;
}
}
position.1 += self.history.len();
position.1 += self.multiline_history as usize;
(position.0 as u16, position.1 as u16)
}
}
#[derive(Debug, Clone)]
struct OutputState {
output: Vec<String>,
output_cursor: usize,
output_width: usize,
output_scroll: u16,
}
impl Default for OutputState {
fn default() -> Self {
OutputState {
output: vec![],
output_cursor: 0,
output_width: 80,
output_scroll: 0,
}
}
}
struct WashRepl {
input_state: InputState,
output_state: Arc<Mutex<OutputState>>,
tui_dispatcher: Rc<RefCell<Dispatcher<Event>>>,
tui_state: TuiWidgetState,
}
impl Default for WashRepl {
fn default() -> Self {
WashRepl {
input_state: InputState::default(),
output_state: Arc::new(Mutex::new(OutputState::default())),
tui_dispatcher: Rc::new(RefCell::new(Dispatcher::<Event>::new())),
tui_state: TuiWidgetState::new(),
}
}
}
impl WashRepl {
fn draw_ui(
&mut self,
terminal: &mut Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
) -> Result<()> {
terminal.draw(|frame| {
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(67), Constraint::Min(5)].as_ref())
.split(frame.size());
let io_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(40), Constraint::Min(10)])
.split(main_chunks[0]);
draw_input_panel(frame, &mut self.input_state, io_chunks[0]);
draw_output_panel(frame, Arc::clone(&self.output_state), io_chunks[1]);
draw_smart_logger(frame, main_chunks[1], &self.tui_state, &self.tui_dispatcher);
})?;
Ok(())
}
async fn handle_key(&mut self, code: KeyCode, modifier: KeyModifiers) -> Result<()> {
match code {
KeyCode::Char(c) => {
self.input_state
.input
.insert(self.input_state.input_cursor, c);
self.input_state.input_cursor += 1;
}
KeyCode::Left => {
if self.input_state.input_cursor > 0 {
self.input_state.input_cursor -= 1
}
}
KeyCode::Right => {
if self.input_state.input_cursor < self.input_state.input.len() {
self.input_state.input_cursor += 1
}
}
KeyCode::Up => {
if modifier == KeyModifiers::SHIFT {
let mut state = self.output_state.lock().unwrap();
if state.output_cursor > 0 && state.output_scroll > 0 {
state.output_cursor -= 1;
}
} else if self.input_state.history_cursor > 0 && modifier == KeyModifiers::NONE {
self.input_state.history_cursor -= 1;
self.input_state.input =
self.input_state.history[self.input_state.history_cursor].clone();
self.input_state.input_cursor = self.input_state.input.len();
}
}
KeyCode::Down => {
if modifier == KeyModifiers::SHIFT {
let mut state = self.output_state.lock().unwrap();
if state.output_cursor < state.output.len() {
state.output_cursor += 1;
}
} else if modifier == KeyModifiers::NONE {
if self.input_state.history.is_empty() {
return Ok(());
};
if self.input_state.history_cursor < self.input_state.history.len() - 1
&& self.input_state.history_cursor > 0
{
self.input_state.history_cursor += 1;
self.input_state.input =
self.input_state.history[self.input_state.history_cursor].clone();
self.input_state.input_cursor = self.input_state.input.len();
} else if self.input_state.history_cursor >= self.input_state.history.len() - 1
{
self.input_state.history_cursor = self.input_state.history.len();
self.input_state.input.clear();
self.input_state.input_cursor = 0;
}
}
}
KeyCode::Backspace => {
if self.input_state.input_cursor > 0
&& self.input_state.input_cursor <= self.input_state.input.len()
{
self.input_state.input_cursor -= 1;
self.input_state.input.remove(self.input_state.input_cursor);
};
}
KeyCode::Enter => {
let cmd: String = self.input_state.input.iter().collect();
let iter = cmd.split_ascii_whitespace();
let cli = ReplCli::from_iter_safe(iter);
let multilines = self.input_state.input.len() / self.input_state.input_width;
if multilines >= 1 {
self.input_state.multiline_history += multilines as u16;
};
self.input_state
.history
.push(self.input_state.input.clone());
self.input_state.history_cursor = self.input_state.history.len();
self.input_state.input.clear();
self.input_state.input_cursor = 0;
match cli {
Ok(ReplCli { cmd }) => {
use ReplCliCommand::*;
match cmd {
Clear => {
info!(target: WASH_LOG_INFO, "Clearing REPL history");
self.input_state = InputState::default();
}
Quit => {
info!(target: WASH_CMD_INFO, "Goodbye");
return Err("REPL Quit".into());
}
ReplCliCommand::Drain(draincmd) => {
let output_state = Arc::clone(&self.output_state);
std::thread::spawn(|| {
match handle_drain(draincmd, output_state) {
Ok(r) => r,
Err(e) => error!("Error handling drain: {}", e),
};
});
}
ReplCliCommand::Claims(claimscmd) => {
let output_state = Arc::clone(&self.output_state);
std::thread::spawn(|| {
let mut rt = actix_rt::System::new("cmd");
rt.block_on(async {
match handle_claims(claimscmd, output_state).await {
Ok(r) => r,
Err(e) => error!("Error handling claims: {}", e),
};
});
});
}
ReplCliCommand::Ctl(ctlcmd) => {
let output_state = Arc::clone(&self.output_state);
std::thread::spawn(|| {
let mut rt = actix_rt::System::new("cmd");
rt.block_on(async {
match handle_ctl(ctlcmd, output_state).await {
Ok(r) => r,
Err(e) => error!("Error handling ctl: {}", e),
};
});
});
}
ReplCliCommand::Keys(keyscmd) => {
let output_state = Arc::clone(&self.output_state);
std::thread::spawn(|| {
let mut rt = actix_rt::System::new("cmd");
rt.block_on(async {
match handle_keys(keyscmd, output_state).await {
Ok(r) => r,
Err(e) => error!("Error handling key: {}", e),
};
});
});
}
ReplCliCommand::Par(parcmd) => {
let output_state = Arc::clone(&self.output_state);
std::thread::spawn(|| {
let mut rt = actix_rt::System::new("cmd");
rt.block_on(async {
match handle_par(parcmd, output_state).await {
Ok(r) => r,
Err(e) => error!("Error handling par: {}", e),
};
});
});
}
ReplCliCommand::Reg(regcmd) => {
let output_state = Arc::clone(&self.output_state);
std::thread::spawn(|| {
let mut rt = actix_rt::System::new("cmd");
rt.block_on(async {
match handle_reg(regcmd, output_state).await {
Ok(r) => r,
Err(e) => error!("Error handling reg: {}", e),
};
});
});
}
}
}
Err(e) => {
use structopt::clap::ErrorKind::*;
match e.kind {
HelpDisplayed => info!(target: WASH_CMD_INFO, "{}", e.message),
_ => error!(target: WASH_CMD_INFO, "{}", e.message),
}
}
};
}
_ => (),
};
Ok(())
}
}
async fn handle_up(cmd: UpCliCommand) -> Result<()> {
use LogLevel::*;
let filter = match cmd.log_level {
Error => LevelFilter::Error,
Warn => LevelFilter::Warn,
Info => LevelFilter::Info,
Debug => LevelFilter::Debug,
Trace => LevelFilter::Trace,
};
init_logger(filter).unwrap();
set_default_level(filter);
crate::util::REPL_MODE.set("true".to_string()).unwrap();
let backend = {
crossterm::terminal::enable_raw_mode().unwrap();
let mut stdout = io::stdout();
crossterm::execute!(stdout, EnterAlternateScreen).unwrap();
CrosstermBackend::new(stdout)
};
let mut terminal = Terminal::new(backend).unwrap();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
let mut repl = WashRepl::default();
repl.draw_ui(&mut terminal)?;
info!(target: WASH_LOG_INFO, "Initializing REPL...");
let evt = Event::Key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
repl.tui_dispatcher.borrow_mut().dispatch(&evt);
repl.draw_ui(&mut terminal)?;
std::thread::spawn(move || {
let mut rt = actix_rt::System::new("replhost");
rt.block_on(async move {
let nc_rpc =
match nats::asynk::connect(&format!("{}:{}", cmd.rpc_host, cmd.rpc_port)).await {
Ok(conn) => conn,
Err(_e) => {
error!(
target: WASH_CMD_INFO,
"Error connecting to NATS at {}:{}",
cmd.rpc_host,
cmd.rpc_port
);
error!(target: WASH_CMD_INFO, "NATS is required to run control interface (ctl) commands. Please refer to
https://www.wasmcloud.dev/overview/getting-started/#starting-nats for instructions on how to launch NATS");
return;
}
};
let nc_control =
match nats::asynk::connect(&format!("{}:{}", cmd.rpc_host, cmd.rpc_port)).await {
Ok(conn) => conn,
Err(_e) => {
error!(
target: WASH_CMD_INFO,
"Error connecting to NATS at {}:{}",
cmd.rpc_host,
cmd.rpc_port
);
error!(target: WASH_CMD_INFO, "NATS is required to run control interface (ctl) commands. Please refer to
https://www.wasmcloud.dev/overview/getting-started/#starting-nats for instructions on how to launch NATS");
return;
}
};
let host = HostBuilder::new()
.with_namespace(CTL_NS)
.with_rpc_client(nc_rpc)
.with_control_client(nc_control)
.with_label("repl_mode", "true")
.oci_allow_latest()
.oci_allow_insecure(vec!["localhost:5000".to_string()])
.enable_live_updates()
.build();
if let Err(_e) = host.start().await.map_err(convert_error) {
error!(target: WASH_LOG_INFO, "Error launching REPL host");
} else {
info!(
target: WASH_LOG_INFO,
"Host ({}) started in namespace ({})",
host.id(),
CTL_NS
);
};
actix_rt::signal::ctrl_c().await.unwrap();
host.stop().await;
});
});
repl.draw_ui(&mut terminal)?;
let mut repl_focus = true;
loop {
if poll(std::time::Duration::from_millis(50))? {
let res = match read()? {
Event::Key(KeyEvent {
code: KeyCode::Tab, ..
}) => {
repl_focus = !repl_focus;
info!(
target: WASH_CMD_INFO,
"Switched command focus to {}",
if repl_focus {
"REPL"
} else {
"Logger selector"
}
);
Ok(())
}
Event::Key(KeyEvent { code, modifiers }) if repl_focus => {
repl.handle_key(code, modifiers).await
}
evt => {
repl.tui_dispatcher.borrow_mut().dispatch(&evt);
Ok(())
}
};
repl.draw_ui(&mut terminal)?;
if res.is_err() {
cleanup_terminal(&mut terminal);
break;
}
} else {
repl.draw_ui(&mut terminal)?;
}
}
cleanup_terminal(&mut terminal);
Ok(())
}
fn handle_drain(drain_cmd: DrainCliCommand, output_state: Arc<Mutex<OutputState>>) -> Result<()> {
let output = crate::drain::handle_command(drain_cmd)?;
log_to_output(output_state, output);
Ok(())
}
async fn handle_claims(
claims_cmd: ClaimsCliCommand,
output_state: Arc<Mutex<OutputState>>,
) -> Result<()> {
let output = crate::claims::handle_command(claims_cmd).await?;
log_to_output(output_state, output);
Ok(())
}
async fn handle_ctl(ctl_cmd: CtlCliCommand, output_state: Arc<Mutex<OutputState>>) -> Result<()> {
let output = crate::ctl::handle_command(ctl_cmd).await?;
log_to_output(output_state, output);
Ok(())
}
async fn handle_keys(
keys_cmd: KeysCliCommand,
output_state: Arc<Mutex<OutputState>>,
) -> Result<()> {
let output = crate::keys::handle_command(keys_cmd)?;
log_to_output(output_state, output);
Ok(())
}
async fn handle_par(par_cmd: ParCliCommand, output_state: Arc<Mutex<OutputState>>) -> Result<()> {
let output = crate::par::handle_command(par_cmd).await?;
log_to_output(output_state, output);
Ok(())
}
async fn handle_reg(reg_cmd: RegCliCommand, output_state: Arc<Mutex<OutputState>>) -> Result<()> {
let output = crate::reg::handle_command(reg_cmd).await?;
log_to_output(output_state, output);
Ok(())
}
fn cleanup_terminal(terminal: &mut Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>) {
terminal.show_cursor().unwrap();
terminal.clear().unwrap();
crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
terminal::disable_raw_mode().unwrap();
}
fn log_to_output(state: Arc<Mutex<OutputState>>, out: String) {
let mut state = state.lock().unwrap();
state.output_cursor = state.output.len();
let output_width = state.output_width - 2;
out.split('\n').for_each(|line| {
let line_len = line.chars().count();
if line_len > output_width {
let mut offset = 0;
let n_lines = (line_len + (output_width - 1)) / output_width;
for _ in 0..n_lines {
let sub_line = line.chars().skip(offset).take(output_width).collect();
state.output.push(sub_line);
offset += output_width
}
state.output_cursor += n_lines;
} else {
state.output.push(line.to_string());
state.output_cursor += 1;
}
});
state.output.push("".to_string());
state.output_cursor += 1;
}
fn format_input_for_display(input_vec: Vec<char>, input_width: usize) -> String {
let mut input = String::new();
let mut index = WASH_PROMPT.len() - 1;
let disp_iter = input_vec.iter();
for c in disp_iter {
if index == input_width - 1 {
input.push('\n');
input.push(*c);
index = 0;
} else {
input.push(*c);
index += 1;
}
}
input
}
fn draw_input_panel(
frame: &mut Frame<CrosstermBackend<Stdout>>,
state: &mut InputState,
chunk: Rect,
) {
let history: String = state
.history
.iter()
.map(|h| {
format!(
"{}{}\n",
WASH_PROMPT,
format_input_for_display(h.to_vec(), state.input_width)
)
})
.collect();
let prompt: String = WASH_PROMPT.to_string();
let display = format!(
"{}{}{}",
history,
prompt,
format_input_for_display(state.input.clone(), state.input_width)
);
let scroll_offset = if state.history.len() as u16 + state.multiline_history >= chunk.height - 3
{
state.multiline_history + state.history.len() as u16 + 5 - chunk.height
} else {
0
};
state.input_width = chunk.width as usize - 3;
let input_panel = Paragraph::new(display)
.block(Block::default().borders(Borders::ALL).title(Span::styled(
" REPL ",
Style::default().add_modifier(Modifier::BOLD),
)))
.style(Style::default().fg(Color::White))
.alignment(Alignment::Left)
.scroll((scroll_offset, 0));
frame.render_widget(input_panel, chunk);
let input_cursor = state.cursor_location();
frame.set_cursor(
chunk.x + 1 + input_cursor.0,
chunk.y + 1 + input_cursor.1 - scroll_offset,
)
}
fn draw_output_panel(
frame: &mut Frame<CrosstermBackend<Stdout>>,
state: Arc<Mutex<OutputState>>,
chunk: Rect,
) {
let mut state = state.lock().unwrap();
let output_logs: String = state.output.iter().map(|h| format!(" {}\n", h)).collect();
let output_length = state.output.len() as u16;
let output_cursor = state.output_cursor as u16;
state.output_scroll = if output_length >= chunk.height - 3 {
if output_cursor >= chunk.height {
output_cursor as u16 + 1 - chunk.height
} else {
0
}
} else {
0
};
state.output_width = chunk.width as usize - 1;
let output_panel = Paragraph::new(output_logs)
.block(Block::default().borders(Borders::ALL).title(Span::styled(
" OUTPUT (SHIFT+UP/DOWN to scroll) ",
Style::default().add_modifier(Modifier::BOLD),
)))
.style(Style::default().fg(Color::White))
.alignment(Alignment::Left)
.scroll((state.output_scroll, 0))
.wrap(Wrap { trim: false });
frame.render_widget(output_panel, chunk);
}
fn draw_smart_logger(
frame: &mut Frame<CrosstermBackend<Stdout>>,
chunk: Rect,
state: &TuiWidgetState,
dispatcher: &Rc<RefCell<Dispatcher<Event>>>,
) {
dispatcher.borrow_mut().clear();
let selector_panel = TuiLoggerSmartWidget::default()
.style_error(Style::default().fg(Color::Red))
.style_debug(Style::default().fg(Color::Green))
.style_warn(Style::default().fg(Color::Yellow))
.style_trace(Style::default().fg(Color::Magenta))
.style_info(Style::default().fg(Color::Cyan))
.state(state)
.dispatcher(dispatcher.clone());
set_level_for_target("tui_logger::dispatcher", LevelFilter::Off);
set_level_for_target("mio::poll", LevelFilter::Off);
set_level_for_target("mio::sys::unix::kqueue", LevelFilter::Off);
set_level_for_target("polling", LevelFilter::Off);
set_level_for_target("polling::kqueue", LevelFilter::Off);
set_level_for_target("async_io::driver", LevelFilter::Off);
set_level_for_target("async_io::reactor", LevelFilter::Off);
frame.render_widget(selector_panel, chunk);
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_up_comprehensive() -> Result<()> {
const LOG_LEVEL: &str = "info";
const RPC_HOST: &str = "0.0.0.0";
const RPC_PORT: &str = "4222";
let up_all_options = UpCli::from_iter_safe(&[
"up",
"--log-level",
LOG_LEVEL,
"--host",
RPC_HOST,
"--port",
RPC_PORT,
])?;
let up_all_short_options =
UpCli::from_iter_safe(&["up", "-l", LOG_LEVEL, "-h", RPC_HOST, "-p", RPC_PORT])?;
#[allow(unreachable_patterns)]
match up_all_options.command {
UpCliCommand {
rpc_host,
rpc_port,
log_level,
} => {
assert_eq!(rpc_host, RPC_HOST);
assert_eq!(rpc_port, RPC_PORT);
assert_eq!(log_level, LogLevel::Info);
}
cmd => panic!("up generated other command {:?}", cmd),
}
#[allow(unreachable_patterns)]
match up_all_short_options.command {
UpCliCommand {
rpc_host,
rpc_port,
log_level,
} => {
assert_eq!(rpc_host, RPC_HOST);
assert_eq!(rpc_port, RPC_PORT);
assert_eq!(log_level, LogLevel::Info);
}
cmd => panic!("up generated other command {:?}", cmd),
}
Ok(())
}
#[test]
fn test_up_input_format() {
const CALL_INPUT: &str = "ctl call MBCFOPM6JW2APJLXJD3Z5O4CN7CPYJ2B4FTKLJUR5YR5MITIU7HD3WD5 HandleRequest {\"method\": \"GET\", \"path\": \"/\", \"body\": \"\", \"queryString\":\"\", \"header\":{}}";
const START_ACTOR_INPUT: &str = "ctl start actor wasmcloud.azurecr.io/echo:0.2.0";
const LINK_INPUT: &str = "ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M wasmcloud:httpserver PORT=8080";
const TERMINAL_WIDTH: usize = 80;
let prompt_length = super::WASH_PROMPT.len();
let (call_first_line, call_second_line) =
CALL_INPUT.split_at(TERMINAL_WIDTH - prompt_length);
let call_input_display =
format_input_for_display(CALL_INPUT.chars().collect(), TERMINAL_WIDTH);
let mut call_iter = call_input_display.split('\n');
assert_eq!(call_first_line, call_iter.next().unwrap());
assert_eq!(call_second_line, call_iter.next().unwrap());
assert!(START_ACTOR_INPUT.len() < TERMINAL_WIDTH - prompt_length);
let start_input_display =
format_input_for_display(START_ACTOR_INPUT.chars().collect(), TERMINAL_WIDTH);
let mut start_iter = start_input_display.split('\n');
assert_eq!(START_ACTOR_INPUT, start_iter.next().unwrap());
let (link_first_line, link_second_line) =
LINK_INPUT.split_at(TERMINAL_WIDTH - prompt_length);
let link_input_display =
format_input_for_display(LINK_INPUT.chars().collect(), TERMINAL_WIDTH);
let mut link_iter = link_input_display.split('\n');
assert_eq!(link_first_line, link_iter.next().unwrap());
assert_eq!(link_second_line, link_iter.next().unwrap());
}
#[test]
fn test_key_events() {
}
#[test]
fn test_log_level_from_str() -> Result<()> {
use std::str::FromStr;
const ERROR: &str = "error";
const WARN: &str = "warn";
const DEBUG: &str = "debug";
const INFO: &str = "info";
const TRACE: &str = "trace";
const FOO: &str = "foo";
assert_eq!(LogLevel::from_str(ERROR)?, LogLevel::Error);
assert_eq!(LogLevel::from_str(WARN)?, LogLevel::Warn);
assert_eq!(LogLevel::from_str(DEBUG)?, LogLevel::Debug);
assert_eq!(LogLevel::from_str(INFO)?, LogLevel::Info);
assert_eq!(LogLevel::from_str(TRACE)?, LogLevel::Trace);
assert_eq!(LogLevel::from_str(FOO)?, LogLevel::Trace);
Ok(())
}
}