use super::render::{
print_messages, render_full_pads, render_pad_list, render_pad_list_deleted, render_text_list,
};
use super::setup::{
parse_cli, Cli, Commands, CompletionShell, CoreCommands, DataCommands, MiscCommands,
PadCommands,
};
use outstanding::OutputMode;
use padzapp::api::{ConfigAction, PadFilter, PadStatusFilter, PadzApi, TodoStatus};
use padzapp::clipboard::{copy_to_clipboard, format_for_clipboard, get_from_clipboard};
use padzapp::editor::open_in_editor;
use padzapp::error::Result;
use padzapp::init::initialize;
use padzapp::model::Scope;
use padzapp::model::{extract_title_and_body, parse_pad_content};
use padzapp::store::fs::FileStore;
use std::io::{IsTerminal, Read};
use std::path::{Path, PathBuf};
fn copy_pad_to_clipboard(path: &Path) {
if let Ok(content) = std::fs::read_to_string(path) {
if let Some((title, body)) = extract_title_and_body(&content) {
let clipboard_text = format_for_clipboard(&title, &body);
let _ = copy_to_clipboard(&clipboard_text);
}
}
}
struct AppContext {
api: PadzApi<FileStore>,
scope: Scope,
import_extensions: Vec<String>,
output_mode: OutputMode,
}
pub fn run() -> Result<()> {
let (cli, output_mode) = parse_cli();
if let Some(Commands::Misc(MiscCommands::Completions { shell })) = &cli.command {
return handle_completions(*shell);
}
let mut ctx = init_context(&cli, output_mode)?;
match cli.command {
Some(Commands::Core(cmd)) => match cmd {
CoreCommands::Create {
title,
no_editor,
inside,
} => {
let title = if title.is_empty() {
None
} else {
Some(title.join(" "))
};
handle_create(&mut ctx, title, no_editor, inside)
}
CoreCommands::List {
search,
deleted,
peek,
planned,
done,
in_progress,
} => handle_list(&mut ctx, search, deleted, peek, planned, done, in_progress),
CoreCommands::Search { term } => handle_search(&mut ctx, term),
},
Some(Commands::Pad(cmd)) => match cmd {
PadCommands::View { indexes, peek } => handle_view(&mut ctx, indexes, peek),
PadCommands::Edit { indexes } => handle_edit(&mut ctx, indexes),
PadCommands::Open { indexes } => handle_open(&mut ctx, indexes),
PadCommands::Delete {
indexes,
done_status,
} => handle_delete(&mut ctx, indexes, done_status),
PadCommands::Restore { indexes } => handle_restore(&mut ctx, indexes),
PadCommands::Pin { indexes } => handle_pin(&mut ctx, indexes),
PadCommands::Unpin { indexes } => handle_unpin(&mut ctx, indexes),
PadCommands::Path { indexes } => handle_paths(&mut ctx, indexes),
PadCommands::Complete { indexes } => handle_complete(&mut ctx, indexes),
PadCommands::Reopen { indexes } => handle_reopen(&mut ctx, indexes),
PadCommands::Move { indexes, root } => handle_move(&mut ctx, indexes, root),
},
Some(Commands::Data(cmd)) => match cmd {
DataCommands::Purge {
indexes,
yes,
recursive,
} => handle_purge(&mut ctx, indexes, yes, recursive),
DataCommands::Export {
single_file,
indexes,
} => handle_export(&mut ctx, indexes, single_file),
DataCommands::Import { paths } => handle_import(&mut ctx, paths),
},
Some(Commands::Misc(cmd)) => match cmd {
MiscCommands::Doctor => handle_doctor(&mut ctx),
MiscCommands::Config { key, value } => handle_config(&mut ctx, key, value),
MiscCommands::Init => handle_init(&ctx),
MiscCommands::Completions { shell } => handle_completions(shell),
},
None => handle_list(&mut ctx, None, false, false, false, false, false),
}
}
fn init_context(cli: &Cli, output_mode: OutputMode) -> Result<AppContext> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let ctx = initialize(&cwd, cli.global);
Ok(AppContext {
api: ctx.api,
scope: ctx.scope,
import_extensions: ctx.config.import_extensions.clone(),
output_mode,
})
}
fn handle_create(
ctx: &mut AppContext,
title: Option<String>,
no_editor: bool,
inside: Option<String>,
) -> Result<()> {
let (final_title, initial_content, should_open_editor) = resolve_create_input(title, no_editor);
let title_to_use = final_title.unwrap_or_else(|| "Untitled".to_string());
let parent = inside.as_deref();
let result = ctx
.api
.create_pad(ctx.scope, title_to_use, initial_content, parent)?;
print_messages(&result.messages, ctx.output_mode);
if should_open_editor && !result.pad_paths.is_empty() {
let path = &result.pad_paths[0];
open_in_editor(path)?;
copy_pad_to_clipboard(path);
}
Ok(())
}
fn resolve_create_input(title: Option<String>, no_editor: bool) -> (Option<String>, String, bool) {
let mut final_title = title;
let mut initial_content = String::new();
let mut should_open_editor = !no_editor;
if !std::io::stdin().is_terminal() {
let mut buffer = String::new();
if std::io::stdin().read_to_string(&mut buffer).is_ok() && !buffer.trim().is_empty() {
if final_title.is_none() {
if let Some((parsed_title, _)) = parse_pad_content(&buffer) {
final_title = Some(parsed_title);
}
}
initial_content = buffer;
should_open_editor = false; }
}
if final_title.is_none() && initial_content.is_empty() {
if let Ok(clipboard_content) = get_from_clipboard() {
if !clipboard_content.trim().is_empty() {
if let Some((parsed_title, _)) = parse_pad_content(&clipboard_content) {
final_title = Some(parsed_title);
}
initial_content = clipboard_content;
}
}
}
(final_title, initial_content, should_open_editor)
}
fn handle_list(
ctx: &mut AppContext,
search: Option<String>,
deleted: bool,
peek: bool,
planned: bool,
done: bool,
in_progress: bool,
) -> Result<()> {
let todo_status = if planned {
Some(TodoStatus::Planned)
} else if done {
Some(TodoStatus::Done)
} else if in_progress {
Some(TodoStatus::InProgress)
} else {
None };
let filter = PadFilter {
status: if deleted {
PadStatusFilter::Deleted
} else {
PadStatusFilter::Active
},
search_term: search,
todo_status,
};
let result = ctx.api.get_pads(ctx.scope, filter)?;
let output = if deleted {
render_pad_list_deleted(&result.listed_pads, peek, ctx.output_mode)
} else {
render_pad_list(&result.listed_pads, peek, ctx.output_mode)
};
print!("{}", output);
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_view(ctx: &mut AppContext, indexes: Vec<String>, peek: bool) -> Result<()> {
let result = ctx.api.view_pads(ctx.scope, &indexes)?;
let output = if peek {
render_pad_list(&result.listed_pads, true, ctx.output_mode)
} else {
render_full_pads(&result.listed_pads, ctx.output_mode)
};
print!("{}", output);
print_messages(&result.messages, ctx.output_mode);
if !result.listed_pads.is_empty() {
let clipboard_text: String = result
.listed_pads
.iter()
.map(|dp| dp.pad.content.clone())
.collect::<Vec<_>>()
.join("\n\n---\n\n");
let _ = copy_to_clipboard(&clipboard_text);
}
Ok(())
}
fn handle_edit(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
let result = ctx.api.view_pads(ctx.scope, &indexes)?;
for path in &result.pad_paths {
open_in_editor(path)?;
copy_pad_to_clipboard(path);
}
Ok(())
}
fn handle_open(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
handle_edit(ctx, indexes)
}
fn handle_delete(ctx: &mut AppContext, indexes: Vec<String>, done_status: bool) -> Result<()> {
if done_status {
let filter = PadFilter {
status: PadStatusFilter::Active,
search_term: None,
todo_status: Some(TodoStatus::Done),
};
let pads = ctx.api.get_pads(ctx.scope, filter)?;
if pads.listed_pads.is_empty() {
println!("No done pads to delete.");
return Ok(());
}
let done_indexes: Vec<String> = pads
.listed_pads
.iter()
.map(|dp| dp.index.to_string())
.collect();
let result = ctx.api.delete_pads(ctx.scope, &done_indexes)?;
print_messages(&result.messages, ctx.output_mode);
} else {
let result = ctx.api.delete_pads(ctx.scope, &indexes)?;
print_messages(&result.messages, ctx.output_mode);
}
Ok(())
}
fn handle_restore(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
let result = ctx.api.restore_pads(ctx.scope, &indexes)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_pin(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
let result = ctx.api.pin_pads(ctx.scope, &indexes)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_unpin(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
let result = ctx.api.unpin_pads(ctx.scope, &indexes)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_complete(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
let result = ctx.api.complete_pads(ctx.scope, &indexes)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_move(ctx: &mut AppContext, mut indexes: Vec<String>, root: bool) -> Result<()> {
let destination = if root {
None
} else {
if indexes.len() < 2 {
return Err(padzapp::error::PadzError::Api(
"Missing destination. Use `padz move <SOURCE>... <DEST>` or `padz move <SOURCE>... --root`".to_string()
));
}
Some(indexes.pop().unwrap())
};
let result = ctx
.api
.move_pads(ctx.scope, &indexes, destination.as_deref())?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_reopen(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
let result = ctx.api.reopen_pads(ctx.scope, &indexes)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_search(ctx: &mut AppContext, term: String) -> Result<()> {
let filter = PadFilter {
status: PadStatusFilter::Active,
search_term: Some(term),
todo_status: None,
};
let result = ctx.api.get_pads(ctx.scope, filter)?;
let output = render_pad_list(&result.listed_pads, false, ctx.output_mode);
print!("{}", output);
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_paths(ctx: &mut AppContext, indexes: Vec<String>) -> Result<()> {
let result = ctx.api.pad_paths(ctx.scope, &indexes)?;
let lines: Vec<String> = result
.pad_paths
.iter()
.map(|path| path.display().to_string())
.collect();
let output = render_text_list(&lines, "No pad paths found.", ctx.output_mode);
print!("{}", output);
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_purge(
ctx: &mut AppContext,
indexes: Vec<String>,
yes: bool,
recursive: bool,
) -> Result<()> {
let result = ctx.api.purge_pads(ctx.scope, &indexes, recursive, yes)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_export(
ctx: &mut AppContext,
indexes: Vec<String>,
single_file: Option<String>,
) -> Result<()> {
let result = if let Some(title) = single_file {
ctx.api
.export_pads_single_file(ctx.scope, &indexes, &title)?
} else {
ctx.api.export_pads(ctx.scope, &indexes)?
};
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_import(ctx: &mut AppContext, paths: Vec<String>) -> Result<()> {
let paths: Vec<PathBuf> = paths.iter().map(PathBuf::from).collect();
let result = ctx
.api
.import_pads(ctx.scope, paths, &ctx.import_extensions)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_doctor(ctx: &mut AppContext) -> Result<()> {
let result = ctx.api.doctor(ctx.scope)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_config(ctx: &mut AppContext, key: Option<String>, value: Option<String>) -> Result<()> {
let action = match (key.clone(), value) {
(None, _) => ConfigAction::ShowAll,
(Some(k), None) => ConfigAction::ShowKey(k),
(Some(k), Some(v)) => ConfigAction::Set(k, v),
};
let result = ctx.api.config(ctx.scope, action)?;
let mut lines = Vec::new();
if let Some(config) = &result.config {
if key.is_none() {
for (k, v) in config.list_all() {
lines.push(format!("{} = {}", k, v));
}
}
}
let output = render_text_list(&lines, "No configuration values.", ctx.output_mode);
print!("{}", output);
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_init(ctx: &AppContext) -> Result<()> {
let result = ctx.api.init(ctx.scope)?;
print_messages(&result.messages, ctx.output_mode);
Ok(())
}
fn handle_completions(shell: CompletionShell) -> Result<()> {
use super::setup::build_command;
use clap_complete::env::{CompleteEnv, EnvCompleter};
let shell_name = match shell {
CompletionShell::Bash => "bash",
CompletionShell::Zsh => "zsh",
};
let completer = CompleteEnv::with_factory(build_command);
let mut buf = Vec::new();
match shell {
CompletionShell::Bash => {
clap_complete::env::Bash
.write_registration("COMPLETE", "padz", "padz", "padz", &mut buf)
.expect("Failed to generate bash completions");
}
CompletionShell::Zsh => {
clap_complete::env::Zsh
.write_registration("COMPLETE", "padz", "padz", "padz", &mut buf)
.expect("Failed to generate zsh completions");
}
}
println!("# {} completion for padz", shell_name);
println!(
"# Add to your shell rc file: eval \"$(padz completions {})\"",
shell_name
);
println!();
print!("{}", String::from_utf8_lossy(&buf));
let _ = completer;
Ok(())
}