use std::{io::{stdin, BufRead, Result},
process::Command};
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use crossterm::style::Stylize;
use r3bl_ansi_color::{is_stdin_piped,
is_stdout_piped,
StdinIsPipedResult,
StdoutIsPipedResult};
use r3bl_core::{call_if_true,
get_size,
get_terminal_width,
throws,
try_initialize_global_logging};
use r3bl_tuify::{select_from_list, SelectionMode, StyleSheet, DEVELOPMENT_MODE};
use reedline::{DefaultPrompt, DefaultPromptSegment, Reedline, Signal};
use StdinIsPipedResult::{StdinIsNotPiped, StdinIsPiped};
use StdoutIsPipedResult::{StdoutIsNotPiped, StdoutIsPiped};
const SELECTED_ITEM_SYMBOL: char = '%';
#[derive(Debug, Parser)]
#[command(bin_name = "rt")]
#[command(about = "Easily add lightweight TUI capabilities to any CLI apps using pipes", long_about = None)]
#[command(version)]
#[command(next_line_help = true)]
#[command(arg_required_else_help(true))]
pub struct AppArgs {
#[clap(subcommand)]
command: CLICommand,
#[clap(flatten)]
global_opts: GlobalOpts,
}
#[derive(Debug, Args)]
struct GlobalOpts {
#[arg(long, short = 'l')]
enable_logging: bool,
#[arg(value_name = "height", long, short = 'r')]
tui_height: Option<usize>,
#[arg(value_name = "width", long, short = 'c')]
tui_width: Option<usize>,
}
#[derive(Debug, Subcommand)]
enum CLICommand {
SelectFromList {
#[arg(value_name = "mode", long, short = 's')]
selection_mode: Option<SelectionMode>,
#[arg(value_name = "command", long, short = 'c')]
command_to_run_with_each_selection: Option<String>,
},
}
fn get_bin_name() -> String {
let cmd = AppArgs::command();
cmd.get_bin_name().unwrap_or("this command").to_string()
}
fn main() -> Result<()> {
throws!({
let cli_args = AppArgs::parse();
let enable_logging = DEVELOPMENT_MODE | cli_args.global_opts.enable_logging;
call_if_true!(enable_logging, {
try_initialize_global_logging(tracing_core::LevelFilter::DEBUG).ok();
tracing::debug!("Start logging... terminal window size: {:?}", get_size()?);
tracing::debug!("cli_args {cli_args:?}")
});
match cli_args.command {
CLICommand::SelectFromList {
selection_mode,
command_to_run_with_each_selection: command_to_run_with_selection,
} => {
if cfg!(target_os = "macos") {
match (is_stdin_piped(), is_stdout_piped()) {
(StdinIsPiped, _) => {
show_error_stdin_pipe_does_not_work_on_macos();
}
(_, StdoutIsPiped) => {
show_error_do_not_pipe_stdout(get_bin_name().as_ref());
}
(StdinIsNotPiped, StdoutIsNotPiped) => {
print_help()?;
}
}
}
else {
match (is_stdin_piped(), is_stdout_piped()) {
(StdinIsPiped, StdoutIsNotPiped) => {
let tui_height = cli_args.global_opts.tui_height;
let tui_width = cli_args.global_opts.tui_width;
show_tui(
selection_mode,
command_to_run_with_selection,
tui_height,
tui_width,
enable_logging,
);
}
(StdinIsPiped, StdoutIsPiped) => {
show_error_do_not_pipe_stdout(get_bin_name().as_ref());
}
(StdinIsNotPiped, StdoutIsPiped) => {
show_error_need_to_pipe_stdin(get_bin_name().as_ref());
show_error_do_not_pipe_stdout(get_bin_name().as_ref());
}
(StdinIsNotPiped, StdoutIsNotPiped) => {
show_error_need_to_pipe_stdin(get_bin_name().as_ref());
}
}
}
}
}
call_if_true!(enable_logging, {
tracing::debug!("Stop logging...");
});
});
}
fn show_error_stdin_pipe_does_not_work_on_macos() {
let msg = "Unfortunately at this time macOS `stdin` pipe does not work on macOS.\
\nhttps://github.com/crossterm-rs/crossterm/issues/396"
.blue()
.to_string();
println!("{msg}");
}
fn show_error_need_to_pipe_stdin(bin_name: &str) {
let msg = format!(
"Please pipe the output of another command into {bin_name}. \
\n✅ For example: `ls -l | {bin_name} -s single-select`",
)
.green()
.to_string();
println!("{msg}");
}
fn show_error_do_not_pipe_stdout(bin_name: &str) {
let msg = format!(
"Please do *not* pipe the output of {bin_name} to another command. \
\n❎ For eg, don't do this: `ls -l | {bin_name} -s single-select | cat`",
)
.red()
.to_string();
println!("{msg}");
}
fn show_tui(
maybe_selection_mode: Option<SelectionMode>,
maybe_command_to_run_with_each_selection: Option<String>,
tui_height: Option<usize>,
tui_width: Option<usize>,
enable_logging: bool,
) {
let lines: Vec<String> = stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>();
call_if_true!(enable_logging, {
tracing::debug!("lines: {lines:?}");
});
if lines.is_empty() {
return;
}
let max_width_col_count: usize = tui_width.unwrap_or(get_terminal_width());
let max_height_row_count: usize = tui_height.unwrap_or(5);
let selection_mode = if let Some(selection_mode) = maybe_selection_mode {
selection_mode
} else {
let possible_values_for_selection_mode =
get_possible_values_for_subcommand_and_option(
"select-from-list",
"selection-mode",
);
print_help_for_subcommand_and_option("select-from-list", "selection-mode").ok();
let user_selection = select_from_list(
"Choose selection-mode".to_string(),
possible_values_for_selection_mode,
max_height_row_count,
max_width_col_count,
SelectionMode::Single,
StyleSheet::default(),
);
let it = if let Some(user_selection) = user_selection {
if let Some(it) = user_selection.first() {
println!("selection-mode: {}", it);
SelectionMode::from_str(it, true).unwrap_or(SelectionMode::Single)
} else {
print_help_for("select-from-list").ok();
return;
}
} else {
print_help_for("select-from-list").ok();
return;
};
it
};
let command_to_run_with_each_selection =
match maybe_command_to_run_with_each_selection {
Some(it) => it,
None => {
print_help_for_subcommand_and_option(
"select-from-list",
"command-to-run-with-each-selection",
)
.ok();
let mut line_editor = Reedline::create();
let prompt = DefaultPrompt {
left_prompt: DefaultPromptSegment::Basic(
"Enter command to run w/ each selection `%`: ".to_string(),
),
right_prompt: DefaultPromptSegment::Empty,
};
let sig = line_editor.read_line(&prompt);
match sig {
Ok(Signal::Success(buffer)) => {
if buffer.is_empty() {
print_help_for("select-from-list").ok();
return;
}
println!("Command to run w/ each selection: {}", buffer);
buffer
}
_ => {
print_help_for("select-from-list").ok();
return;
}
}
}
};
let selected_items = {
let it = select_from_list(
"Select one line".to_string(),
lines,
max_height_row_count,
max_width_col_count,
selection_mode,
StyleSheet::default(),
);
convert_user_input_into_vec_of_strings(it)
};
call_if_true!(enable_logging, {
tracing::debug!("selected_items: {}", format!("{selected_items:?}").cyan());
});
for selected_item in selected_items {
let actual_command_to_run = &command_to_run_with_each_selection
.replace(SELECTED_ITEM_SYMBOL, &selected_item);
execute_command(actual_command_to_run);
}
}
fn convert_user_input_into_vec_of_strings(
user_input: Option<Vec<String>>,
) -> Vec<String> {
user_input.unwrap_or_default()
}
fn execute_command(cmd_str: &str) {
let mut command_binding = if cfg!(target_os = "windows") {
Command::new("cmd")
} else {
Command::new("sh")
};
let command = if cfg!(target_os = "windows") {
command_binding.arg("/C").arg(cmd_str)
} else {
command_binding.arg("-c").arg(cmd_str)
};
let output = command.output().expect("failed to execute process");
let result_output_str = String::from_utf8(output.stdout);
match result_output_str {
Ok(it) => {
print!("{}", it);
}
Err(e) => {
println!("Error: {}", e);
}
}
}
pub fn print_help() -> Result<()> {
throws!({
let mut cmd = AppArgs::command();
cmd.print_help()?;
});
}
fn print_help_for(subcommand: &str) -> Result<()> {
throws!({
let app_args_binding = AppArgs::command();
if let Some(it) = app_args_binding.find_subcommand(subcommand) {
it.clone().print_help()?;
}
});
}
fn print_help_for_subcommand_and_option(subcommand: &str, option: &str) -> Result<()> {
throws!({
let app_args_binding = AppArgs::command();
if let Some(it) = app_args_binding.find_subcommand(subcommand) {
for arg in it.get_arguments() {
if arg.get_long() == Some(option) {
let help = arg.get_help();
if let Some(help) = help {
let output = format!("{}", help);
println!("{}", output);
}
}
}
}
});
}
fn get_possible_values_for_subcommand_and_option(
subcommand: &str,
option: &str,
) -> Vec<String> {
let app_args_binding = AppArgs::command();
if let Some(it) = app_args_binding.find_subcommand(subcommand) {
for arg in it.get_arguments() {
if arg.get_long() == Some(option) {
let possible_values = arg.get_possible_values();
let possible_values = possible_values
.iter()
.map(|it| it.get_name().to_string())
.collect::<Vec<_>>();
call_if_true!(DEVELOPMENT_MODE, {
tracing::debug!(
"{subcommand}, {option} - possible_values: {}",
format!("{possible_values:?}").green()
);
});
return possible_values;
}
}
}
vec![]
}