initial boilerplate for the shell

This commit is contained in:
lemonsh 2023-05-13 00:35:20 +02:00
parent 84fefcda09
commit b11c20d512
4 changed files with 144 additions and 2 deletions

View File

@ -1,5 +1,7 @@
[package]
name = "connectbox-shell"
description = "A shell for managing your Connect Box router, based on the connectbox-rs library"
authors = ["lemonsh"]
version = "0.1.0"
edition = "2021"
@ -7,4 +9,10 @@ edition = "2021"
rustyline = "11"
color-eyre = "0.6"
tokio = { version = "1.0", default-features = false, features = ["macros"] }
tracing = "0.1"
tracing-subscriber = "0.3"
clap = { version = "4.2", default-features = false, features = ["suggestions", "color", "std", "help", "usage", "derive"] }
dirs = "5.0"
connectbox = { path = "../connectbox" }
color-print = "0.3"
anstream = "0.3"

View File

@ -0,0 +1,27 @@
use clap::{Command, Parser, Subcommand};
use tracing::Level;
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub(crate) struct Args {
/// Address of the modem
pub address: String,
/// Password used to log in to the modem. If not supplied, it will be asked interactively
pub password: Option<String>,
/// Log level, one of 'trace', 'debug', 'info', 'warn', 'error'
#[arg(short, default_value = "warn")]
pub log_level: Level,
}
#[derive(Parser, Debug)]
pub(crate) enum ShellCommand {
Exit,
#[command(name = "pfw")]
PortForwards,
}
pub(crate) fn shell_cmd() -> Command {
ShellCommand::augment_subcommands(Command::new("").multicall(true))
}

View File

@ -1,3 +1,70 @@
fn main() {
println!("Hello, world!");
use anstream::println;
use color_print::cstr;
use clap::{FromArgMatches, Parser};
use cli::Args;
use color_eyre::Result;
use connectbox::ConnectBox;
use rustyline::{error::ReadlineError, DefaultEditor};
use crate::{cli::ShellCommand, utils::QuotableArgs};
mod cli;
mod utils;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let args = Args::parse();
let mut shell_cmd = cli::shell_cmd();
color_eyre::install()?;
tracing_subscriber::fmt::fmt()
.with_max_level(args.log_level)
.init();
let mut rl = DefaultEditor::new()?;
let password = if let Some(password) = args.password {
password
} else {
rl.readline("Password: ")?
};
let history_path = dirs::data_dir()
.unwrap_or_default()
.join(".connectbox-shell-history");
let _err = rl.load_history(&history_path);
println!(cstr!("<blue!>Logging in..."));
let connectbox = ConnectBox::new(args.address, password, true)?;
connectbox.login().await?;
loop {
match rl.readline(cstr!("<green!> > ")) {
Ok(line) => {
if line.chars().all(char::is_whitespace) {
continue;
}
let cmd = match shell_cmd.try_get_matches_from_mut(QuotableArgs::new(&line)) {
Ok(mut matches) => ShellCommand::from_arg_matches_mut(&mut matches)?,
Err(e) => {
e.print()?;
continue;
}
};
match cmd {
ShellCommand::Exit => break,
ShellCommand::PortForwards => todo!(),
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
Err(err) => {
println!("{err:?}");
break;
}
}
}
println!("Logging out...");
connectbox.logout().await?;
rl.save_history(&history_path)?;
Ok(())
}

View File

@ -0,0 +1,40 @@
pub(crate) struct QuotableArgs<'a> {
s: &'a str,
}
impl<'a> QuotableArgs<'a> {
pub fn new(s: &'a str) -> Self {
Self { s }
}
}
impl<'a> Iterator for QuotableArgs<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
self.s = self.s.trim_start();
if self.s.is_empty() {
return None;
}
if self.s.as_bytes()[0] == b'"' {
self.s = &self.s[1..];
if let Some(pos) = self.s.find('"') {
let result = &self.s[..pos];
self.s = &self.s[pos + 1..];
return Some(result);
}
let result = self.s;
self.s = &self.s[..0];
return Some(result);
}
if let Some(pos) = self.s.find(char::is_whitespace) {
let result = &self.s[..pos];
self.s = &self.s[pos..];
Some(result)
} else {
let result = self.s;
self.s = &self.s[..0];
Some(result)
}
}
}