#![allow(non_snake_case)]
use padzapp::api::{PadFilter, PadStatusFilter, PadzApi, TodoStatus};
use padzapp::clipboard::{copy_to_clipboard, format_for_clipboard};
use padzapp::commands::{CmdResult, NestingMode};
use padzapp::config::PadzMode;
use padzapp::error::PadzError;
use padzapp::model::{extract_title_and_body, Scope};
use padzapp::store::fs::FileStore;
use serde_json::Value;
use standout::cli::{CommandContext, Output};
use standout::OutputMode;
use standout_macros::handler;
use std::cell::RefCell;
use std::io::IsTerminal;
use super::render::{build_list_result_value, build_modification_result_value, ListOptions};
#[derive(Clone)]
pub struct ImportExtensions(pub Vec<String>);
pub struct AppState {
api: RefCell<PadzApi<FileStore>>,
pub scope: Scope,
pub import_extensions: ImportExtensions,
pub output_mode: OutputMode,
pub mode: PadzMode,
pub local_padz_dir: std::path::PathBuf,
}
impl AppState {
pub fn new(
api: PadzApi<FileStore>,
scope: Scope,
import_extensions: Vec<String>,
output_mode: OutputMode,
mode: PadzMode,
local_padz_dir: std::path::PathBuf,
) -> Self {
Self {
api: RefCell::new(api),
scope,
import_extensions: ImportExtensions(import_extensions),
output_mode,
mode,
local_padz_dir,
}
}
pub fn with_api<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut PadzApi<FileStore>) -> R,
{
f(&mut self.api.borrow_mut())
}
}
fn get_state(ctx: &CommandContext) -> &AppState {
ctx.app_state
.get::<AppState>()
.expect("AppState not initialized in app_state")
}
pub struct ScopedApi<'a> {
state: &'a AppState,
}
impl<'a> ScopedApi<'a> {
fn call<T, F>(&self, f: F) -> Result<T, anyhow::Error>
where
F: FnOnce(&mut PadzApi<FileStore>, Scope) -> Result<T, PadzError>,
{
self.state
.with_api(|api| f(api, self.state.scope).map_err(|e| anyhow::anyhow!("{}", e)))
}
fn modification(
&self,
action: &str,
result: CmdResult,
force_show_status: bool,
) -> Result<Output<Value>, anyhow::Error> {
Ok(Output::Render(build_modification_result_value(
action,
&result.affected_pads,
&result.messages,
self.state.output_mode,
self.state.mode,
force_show_status,
)))
}
fn messages(&self, result: CmdResult) -> Result<Output<Value>, anyhow::Error> {
Ok(Output::Render(serde_json::json!({
"messages": result.messages,
})))
}
pub fn pin_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.pin_pads(scope, indexes))?;
self.modification("Pinned", result, false)
}
pub fn unpin_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.unpin_pads(scope, indexes))?;
self.modification("Unpinned", result, false)
}
pub fn delete_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.delete_pads(scope, indexes))?;
self.modification("Deleted", result, false)
}
pub fn delete_completed_pads(&self) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.delete_completed_pads(scope))?;
if result.affected_pads.is_empty() {
return Ok(Output::Render(serde_json::json!({
"start_message": "",
"pads": [],
"trailing_messages": [{"content": "No completed pads to delete.", "style": "info"}]
})));
}
self.modification("Deleted", result, false)
}
pub fn restore_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.restore_pads(scope, indexes))?;
self.modification("Restored", result, false)
}
pub fn archive_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.archive_pads(scope, indexes))?;
self.modification("Archived", result, false)
}
pub fn unarchive_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.unarchive_pads(scope, indexes))?;
self.modification("Unarchived", result, false)
}
pub fn complete_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.complete_pads(scope, indexes))?;
self.modification("Completed", result, true)
}
pub fn reopen_pads(&self, indexes: &[String]) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.reopen_pads(scope, indexes))?;
self.modification("Reopened", result, true)
}
pub fn move_pads(
&self,
indexes: &[String],
to_root: bool,
) -> Result<Output<Value>, anyhow::Error> {
let result = if to_root {
self.call(|api, scope| api.move_pads(scope, indexes, None))?
} else {
if indexes.len() < 2 {
return Err(anyhow::anyhow!(
"Move requires at least 2 arguments (source and destination) or --root flag"
));
}
let (sources, dest) = indexes.split_at(indexes.len() - 1);
self.call(|api, scope| api.move_pads(scope, sources, Some(dest[0].as_str())))?
};
self.modification("Moved", result, false)
}
pub fn purge_pads(
&self,
indexes: &[String],
yes: bool,
recursive: bool,
) -> Result<Output<Value>, anyhow::Error> {
let include_done = self.state.mode == PadzMode::Todos;
let result =
self.call(|api, scope| api.purge_pads(scope, indexes, recursive, yes, include_done))?;
self.messages(result)
}
pub fn doctor(&self) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.doctor(scope))?;
self.messages(result)
}
pub fn init(&self) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.init(scope))?;
self.messages(result)
}
pub fn init_link(&self, target: &str) -> Result<Output<Value>, anyhow::Error> {
let target_path = std::path::PathBuf::from(target);
let local_padz = &self.state.local_padz_dir;
let result = self.call(|api, _scope| api.init_link(local_padz, &target_path))?;
self.messages(result)
}
pub fn init_unlink(&self) -> Result<Output<Value>, anyhow::Error> {
let local_padz = &self.state.local_padz_dir;
let result = self.call(|api, _scope| api.init_unlink(local_padz))?;
self.messages(result)
}
pub fn export_pads(
&self,
indexes: &[String],
single_file: Option<&str>,
nesting: NestingMode,
) -> Result<Output<Value>, anyhow::Error> {
let result = if let Some(title) = single_file {
self.call(|api, scope| api.export_pads_single_file(scope, indexes, title, nesting))?
} else {
self.call(|api, scope| api.export_pads(scope, indexes, nesting))?
};
self.messages(result)
}
pub fn import_pads(
&self,
paths: Vec<std::path::PathBuf>,
) -> Result<Output<Value>, anyhow::Error> {
let extensions = &self.state.import_extensions.0;
let result = self.call(|api, scope| api.import_pads(scope, paths.clone(), extensions))?;
self.messages(result)
}
pub fn list_tags(&self) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.list_tags(scope))?;
self.messages(result)
}
pub fn delete_tag(&self, name: &str) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.delete_tag(scope, name))?;
self.messages(result)
}
pub fn rename_tag(
&self,
old_name: &str,
new_name: &str,
) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.rename_tag(scope, old_name, new_name))?;
self.messages(result)
}
#[allow(clippy::too_many_arguments)]
pub fn list_pads(
&self,
filter: PadFilter,
peek: bool,
show_deleted_help: bool,
show_all_sections: bool,
ids: &[String],
show_uuid: bool,
show_status: bool,
) -> Result<Output<Value>, anyhow::Error> {
let filtered = filter.search_term.is_some()
|| filter.todo_status.is_some()
|| filter.tags.is_some()
|| !ids.is_empty();
let result = self.call(|api, scope| api.get_pads(scope, filter, ids))?;
Ok(Output::Render(build_list_result_value(
&result.listed_pads,
&result.messages,
ListOptions {
peek,
show_deleted_help,
show_all_sections,
output_mode: self.state.output_mode,
mode: self.state.mode,
show_uuid,
filtered,
show_status,
},
)))
}
pub fn view_pads(
&self,
indexes: &[String],
show_uuid: bool,
nesting: NestingMode,
) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.view_pads(scope, indexes, nesting))?;
for (i, dp) in result.listed_pads.iter().enumerate() {
let depth = result.listed_depths.get(i).copied().unwrap_or(0);
if depth == 0 {
copy_content_to_clipboard(&dp.pad.content);
}
}
let indent_per_level: usize = match nesting {
NestingMode::Indented => 4,
_ => 0,
};
let pads: Vec<serde_json::Value> = result
.listed_pads
.iter()
.enumerate()
.map(|(i, dp)| {
let depth = result.listed_depths.get(i).copied().unwrap_or(0);
let indent = " ".repeat(depth * indent_per_level);
let body = extract_title_and_body(&dp.pad.content)
.map(|(_, b)| b)
.unwrap_or_default();
let (display_title, display_body) = if indent.is_empty() {
(dp.pad.metadata.title.clone(), body)
} else {
let indented_body = indent_lines(&body, &indent);
(
format!("{}{}", indent, dp.pad.metadata.title),
indented_body,
)
};
let mut v = serde_json::json!({
"title": display_title,
"content": display_body,
"depth": depth,
});
if show_uuid {
v["uuid"] = serde_json::json!(dp.pad.metadata.id.to_string());
}
v
})
.collect();
Ok(Output::Render(serde_json::json!({ "pads": pads })))
}
pub fn copy_pads(
&self,
indexes: &[String],
nesting: NestingMode,
) -> Result<Output<Value>, anyhow::Error> {
let result = self.call(|api, scope| api.view_pads(scope, indexes, nesting))?;
let indent_per_level: usize = match nesting {
NestingMode::Indented => 4,
_ => 0,
};
let mut clipboard_text = String::new();
for (i, dp) in result.listed_pads.iter().enumerate() {
let depth = result.listed_depths.get(i).copied().unwrap_or(0);
let indent = " ".repeat(depth * indent_per_level);
let body = extract_title_and_body(&dp.pad.content)
.map(|(_, b)| b)
.unwrap_or_default();
if depth == 0 && !clipboard_text.is_empty() {
clipboard_text.push_str("\n---\n\n");
} else if depth > 0 {
clipboard_text.push_str("\n\n");
}
if indent.is_empty() {
clipboard_text.push_str(&format_for_clipboard(&dp.pad.metadata.title, &body));
} else {
clipboard_text.push_str(&format_for_clipboard(
&format!("{}{}", indent, dp.pad.metadata.title),
&indent_lines(&body, &indent),
));
}
}
let _ = copy_to_clipboard(&clipboard_text);
let root_titles: Vec<&str> = result
.listed_pads
.iter()
.enumerate()
.filter(|(i, _)| result.listed_depths.get(*i).copied().unwrap_or(0) == 0)
.map(|(_, dp)| dp.pad.metadata.title.as_str())
.collect();
let count = root_titles.len();
let label = if count == 1 { "pad" } else { "pads" };
let msg = format!(
"Copied {} {} to clipboard: {}",
count,
label,
root_titles.join(", ")
);
Ok(Output::Render(serde_json::json!({
"messages": [{ "content": msg, "style": "info" }]
})))
}
}
fn api(ctx: &CommandContext) -> ScopedApi<'_> {
ScopedApi {
state: get_state(ctx),
}
}
fn parse_nesting_mode(flat: bool, _tree: bool, indented: bool) -> NestingMode {
if flat {
NestingMode::Flat
} else if indented {
NestingMode::Indented
} else {
NestingMode::Tree
}
}
fn copy_content_to_clipboard(content: &str) {
if let Some((title, body)) = extract_title_and_body(content) {
let clipboard_text = format_for_clipboard(&title, &body);
let _ = copy_to_clipboard(&clipboard_text);
}
}
fn indent_lines(text: &str, prefix: &str) -> String {
text.lines()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!("{}{}", prefix, line)
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn try_read_stdin() -> Result<Option<String>, anyhow::Error> {
use std::io::Read;
if std::io::stdin().is_terminal() {
return Ok(None);
}
let mut content = String::new();
std::io::stdin()
.read_to_string(&mut content)
.map_err(|e| anyhow::anyhow!("Failed to read stdin: {}", e))?;
Ok(Some(content.trim().to_string()))
}
fn split_indexes_and_content(args: &[String]) -> (Vec<String>, Vec<String>) {
use padzapp::index::parse_index_or_range;
let mut indexes = Vec::new();
let mut content = Vec::new();
let mut past_indexes = false;
for arg in args {
if past_indexes {
content.push(arg.clone());
} else if parse_index_or_range(arg).is_ok() {
indexes.push(arg.clone());
} else {
past_indexes = true;
content.push(arg.clone());
}
}
if indexes.is_empty() && !content.is_empty() {
return (std::mem::take(&mut content), vec![]);
}
(indexes, content)
}
#[handler]
pub fn create(
#[ctx] ctx: &CommandContext,
#[flag] editor: bool,
#[flag(name = "no_editor")] no_editor: bool,
#[arg] inside: Option<String>,
#[arg] format: Option<String>,
#[arg] title: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
let state = get_state(ctx);
let title_arg = if title.is_empty() {
None
} else {
Some(title.join(" "))
};
let inside = inside.as_deref();
let format_ref = format.as_deref();
fn do_create(
state: &AppState,
title: String,
content: String,
inside: Option<&str>,
format: Option<&str>,
) -> std::result::Result<padzapp::commands::CmdResult, anyhow::Error> {
state.with_api(|api| {
if let Some(fmt) = format {
api.create_pad_with_format(state.scope, title, content, inside, fmt)
.map_err(|e| anyhow::anyhow!("{}", e))
} else {
api.create_pad(state.scope, title, content, inside)
.map_err(|e| anyhow::anyhow!("{}", e))
}
})
}
let skip_editor =
!editor && (no_editor || (state.mode == PadzMode::Todos && title_arg.is_some()));
let result = if skip_editor {
let raw_text = title_arg.unwrap_or_default();
let expanded = raw_text.replace("\\n", "\n");
let (title, body) =
extract_title_and_body(&expanded).unwrap_or_else(|| (String::new(), String::new()));
let result = do_create(state, title.clone(), body.clone(), inside, format_ref)?;
let parent_id = result.affected_pads[0].pad.metadata.parent_id;
state.with_api(|api| api.propagate_status(state.scope, parent_id))?;
let clipboard_text = format_for_clipboard(&title, &body);
let _ = copy_to_clipboard(&clipboard_text);
result
} else if let Some(raw) = try_read_stdin()? {
if raw.is_empty() {
return Ok(Output::Render(serde_json::json!({
"start_message": "",
"pads": [],
"trailing_messages": [{"content": "Aborted: empty content", "style": "warning"}]
})));
}
let parsed = padzapp::editor::EditorContent::from_buffer(&raw);
let final_title = match (&title_arg, parsed.title.is_empty()) {
(Some(t), _) => t.clone(), (_, false) => parsed.title, (None, true) => String::new(),
};
let result = do_create(state, final_title, parsed.content, inside, format_ref)?;
let parent_id = result.affected_pads[0].pad.metadata.parent_id;
state.with_api(|api| api.propagate_status(state.scope, parent_id))?;
copy_content_to_clipboard(&raw);
result
} else {
let initial_title = title_arg.clone().unwrap_or_default();
let create_result = do_create(state, initial_title, String::new(), inside, format_ref)?;
let pad_path = create_result.pad_paths[0].clone();
let pad_id = create_result.affected_pads[0].pad.metadata.id;
if let Err(e) = padzapp::editor::open_in_editor(&pad_path) {
let _ = state.with_api(|api| api.remove_pad(state.scope, pad_id));
return Err(anyhow::anyhow!("{}", e));
}
match state.with_api(|api| {
api.refresh_pad(state.scope, &pad_id)
.map_err(|e| anyhow::anyhow!("{}", e))
})? {
Some(pad) => {
let parent_id = pad.metadata.parent_id;
state.with_api(|api| {
api.propagate_status(state.scope, parent_id)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
copy_content_to_clipboard(&pad.content);
let display_pad = padzapp::index::DisplayPad {
pad,
index: padzapp::index::DisplayIndex::Regular(1),
matches: None,
children: Vec::new(),
};
CmdResult {
affected_pads: vec![display_pad],
..Default::default()
}
}
None => {
return Ok(Output::Render(serde_json::json!({
"start_message": "",
"pads": [],
"trailing_messages": [{"content": "Aborted: empty content", "style": "warning"}]
})));
}
}
};
let data = build_modification_result_value(
"Created",
&result.affected_pads,
&result.messages,
state.output_mode,
state.mode,
false,
);
Ok(Output::Render(data))
}
#[allow(clippy::too_many_arguments)]
#[handler]
pub fn list(
#[ctx] ctx: &CommandContext,
#[arg] ids: Vec<String>,
#[arg] search: Option<String>,
#[flag] deleted: bool,
#[flag] archived: bool,
#[flag] all: bool,
#[flag] peek: bool,
#[flag] planned: bool,
#[flag] completed: bool,
#[flag(name = "in_progress")] in_progress: bool,
#[arg] tags: Vec<String>,
#[flag] uuid: bool,
#[flag(name = "show_status")] show_status: bool,
) -> Result<Output<Value>, anyhow::Error> {
let todo_status = if planned {
Some(TodoStatus::Planned)
} else if completed {
Some(TodoStatus::Done)
} else if in_progress {
Some(TodoStatus::InProgress)
} else {
None
};
let filter = PadFilter {
status: if all {
PadStatusFilter::All
} else if deleted {
PadStatusFilter::Deleted
} else if archived {
PadStatusFilter::Archived
} else {
PadStatusFilter::Active
},
search_term: search,
todo_status,
tags: if tags.is_empty() { None } else { Some(tags) },
};
api(ctx).list_pads(
filter,
peek,
deleted || archived,
all,
&ids,
uuid,
show_status,
)
}
#[handler]
pub fn peek(
#[ctx] ctx: &CommandContext,
#[arg] ids: Vec<String>,
#[arg] tags: Vec<String>,
#[flag] uuid: bool,
) -> Result<Output<Value>, anyhow::Error> {
let filter = PadFilter {
status: PadStatusFilter::Active,
search_term: None,
todo_status: None,
tags: if tags.is_empty() { None } else { Some(tags) },
};
api(ctx).list_pads(filter, true, false, false, &ids, uuid, false)
}
#[allow(clippy::too_many_arguments)]
#[handler]
pub fn search(
#[ctx] ctx: &CommandContext,
#[arg] term: String,
#[flag] deleted: bool,
#[flag] archived: bool,
#[flag] all: bool,
#[flag] completed: bool,
#[arg] tags: Vec<String>,
#[flag] uuid: bool,
) -> Result<Output<Value>, anyhow::Error> {
let filter = PadFilter {
status: if all {
PadStatusFilter::All
} else if deleted {
PadStatusFilter::Deleted
} else if archived {
PadStatusFilter::Archived
} else {
PadStatusFilter::Active
},
search_term: Some(term),
todo_status: if completed {
Some(TodoStatus::Done)
} else {
None
},
tags: if tags.is_empty() { None } else { Some(tags) },
};
api(ctx).list_pads(filter, false, deleted || archived, all, &[], uuid, false)
}
#[handler]
pub fn view(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
#[flag(name = "peek")] _peek: bool, #[flag] uuid: bool,
#[flag] flat: bool,
#[flag] tree: bool,
#[flag] indented: bool,
) -> Result<Output<Value>, anyhow::Error> {
let nesting = parse_nesting_mode(flat, tree, indented);
api(ctx).view_pads(&indexes, uuid, nesting)
}
#[handler]
pub fn copy(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
#[flag(name = "peek")] _peek: bool, #[flag] flat: bool,
#[flag] tree: bool,
#[flag] indented: bool,
) -> Result<Output<Value>, anyhow::Error> {
let nesting = parse_nesting_mode(flat, tree, indented);
api(ctx).copy_pads(&indexes, nesting)
}
#[handler]
pub fn edit(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
let state = get_state(ctx);
let (index_args, content_words) = split_indexes_and_content(&indexes);
if index_args.is_empty() {
return Err(anyhow::anyhow!("No pad index provided"));
}
let inline_content = if !content_words.is_empty() {
let raw_text = content_words.join(" ");
let expanded = raw_text.replace("\\n", "\n");
Some(expanded)
} else {
None
};
let skip_editor = state.mode == PadzMode::Todos && inline_content.is_some();
if skip_editor {
let raw = inline_content.unwrap();
let result = state.with_api(|api| {
api.update_pads_from_content(state.scope, &index_args, &raw)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
let clipboard_text = raw.clone();
let _ = copy_to_clipboard(&clipboard_text);
let data = build_modification_result_value(
"Updated",
&result.affected_pads,
&result.messages,
state.output_mode,
state.mode,
false,
);
return Ok(Output::Render(data));
}
if let Some(raw) = try_read_stdin()? {
if raw.is_empty() {
return Err(anyhow::anyhow!("Aborted: empty content"));
}
let result = state.with_api(|api| {
api.update_pads_from_content(state.scope, &index_args, &raw)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
copy_content_to_clipboard(&raw);
let data = build_modification_result_value(
"Updated",
&result.affected_pads,
&result.messages,
state.output_mode,
state.mode,
false,
);
return Ok(Output::Render(data));
}
let view_result = state.with_api(|api| {
api.view_pads(
state.scope,
&index_args,
padzapp::commands::NestingMode::Flat,
)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
let pad = view_result
.listed_pads
.first()
.ok_or_else(|| anyhow::anyhow!("No pad found"))?;
let pad_id = pad.pad.metadata.id;
let display_index = pad.index.clone();
let pad_path = state.with_api(|api| {
api.get_path_by_id(state.scope, pad_id)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
padzapp::editor::open_in_editor(&pad_path)?;
match state.with_api(|api| {
api.refresh_pad(state.scope, &pad_id)
.map_err(|e| anyhow::anyhow!("{}", e))
})? {
Some(pad) => {
copy_content_to_clipboard(&pad.content);
let display_pad = padzapp::index::DisplayPad {
pad,
index: display_index,
matches: None,
children: Vec::new(),
};
let result = CmdResult {
affected_pads: vec![display_pad],
..Default::default()
};
let data = build_modification_result_value(
"Updated",
&result.affected_pads,
&result.messages,
state.output_mode,
state.mode,
false,
);
Ok(Output::Render(data))
}
None => {
Ok(Output::<Value>::Silent)
}
}
}
#[handler]
pub fn delete(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
#[flag] completed: bool,
) -> Result<Output<Value>, anyhow::Error> {
if completed {
api(ctx).delete_completed_pads()
} else {
api(ctx).delete_pads(&indexes)
}
}
#[handler]
pub fn restore(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).restore_pads(&indexes)
}
#[handler]
pub fn archive(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).archive_pads(&indexes)
}
#[handler]
pub fn unarchive(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).unarchive_pads(&indexes)
}
#[handler]
pub fn pin(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).pin_pads(&indexes)
}
#[handler]
pub fn unpin(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).unpin_pads(&indexes)
}
#[handler]
pub fn move_pads(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
#[flag] root: bool,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).move_pads(&indexes, root)
}
#[handler]
pub fn path(#[ctx] ctx: &CommandContext, #[arg] indexes: Vec<String>) -> Result<(), anyhow::Error> {
let state = get_state(ctx);
let result = state.with_api(|api| {
api.pad_paths(state.scope, &indexes)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
for path in &result.pad_paths {
println!("{}", path.display());
}
Ok(())
}
#[handler]
pub fn uuid(#[ctx] ctx: &CommandContext, #[arg] indexes: Vec<String>) -> Result<(), anyhow::Error> {
let state = get_state(ctx);
let result = state.with_api(|api| {
api.pad_uuids(state.scope, &indexes)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
for msg in &result.messages {
println!("{}", msg.content);
}
Ok(())
}
#[handler]
pub fn complete(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).complete_pads(&indexes)
}
#[handler]
pub fn reopen(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).reopen_pads(&indexes)
}
#[handler]
pub fn purge(
#[ctx] ctx: &CommandContext,
#[arg] indexes: Vec<String>,
#[flag] yes: bool,
#[flag] recursive: bool,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).purge_pads(&indexes, yes, recursive)
}
#[handler]
pub fn export(
#[ctx] ctx: &CommandContext,
#[arg(name = "single_file")] single_file: Option<String>,
#[arg] indexes: Vec<String>,
#[flag] flat: bool,
#[flag] tree: bool,
#[flag] indented: bool,
) -> Result<Output<Value>, anyhow::Error> {
let nesting = parse_nesting_mode(flat, tree, indented);
api(ctx).export_pads(&indexes, single_file.as_deref(), nesting)
}
#[handler]
pub fn import(
#[ctx] ctx: &CommandContext,
#[arg] paths: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
let paths: Vec<std::path::PathBuf> = paths.into_iter().map(std::path::PathBuf::from).collect();
api(ctx).import_pads(paths)
}
#[handler]
pub fn doctor(#[ctx] ctx: &CommandContext) -> Result<Output<Value>, anyhow::Error> {
api(ctx).doctor()
}
#[handler]
pub fn init(
#[ctx] ctx: &CommandContext,
#[arg] link: Option<String>,
#[flag] unlink: bool,
) -> Result<Output<Value>, anyhow::Error> {
if let Some(target) = link {
api(ctx).init_link(&target)
} else if unlink {
api(ctx).init_unlink()
} else {
api(ctx).init()
}
}
fn split_indexes_and_tags(args: &[String]) -> Result<(Vec<String>, Vec<String>), anyhow::Error> {
use padzapp::index::parse_index_or_range;
let mut indexes = Vec::new();
let mut tags = Vec::new();
let mut past_indexes = false;
for arg in args {
if past_indexes {
tags.push(arg.clone());
} else if parse_index_or_range(arg).is_ok() {
indexes.push(arg.clone());
} else {
past_indexes = true;
tags.push(arg.clone());
}
}
if indexes.is_empty() {
return Err(anyhow::anyhow!(
"No pad selectors provided. Usage: padz tag add <id>... <tag>..."
));
}
if tags.is_empty() {
return Err(anyhow::anyhow!(
"No tag names provided. Usage: padz tag add <id>... <tag>..."
));
}
Ok((indexes, tags))
}
pub mod tag {
use super::*;
#[handler]
pub fn add(
#[ctx] ctx: &CommandContext,
#[arg] args: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
let (indexes, tags) = split_indexes_and_tags(&args)?;
let state = get_state(ctx);
let result = state.with_api(|api| {
api.add_tags_to_pads(state.scope, &indexes, &tags)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
Ok(Output::Render(build_modification_result_value(
"Tagged",
&result.affected_pads,
&result.messages,
state.output_mode,
state.mode,
false,
)))
}
#[handler]
pub fn remove(
#[ctx] ctx: &CommandContext,
#[arg] args: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
let (indexes, tags) = split_indexes_and_tags(&args)?;
let state = get_state(ctx);
let result = state.with_api(|api| {
api.remove_tags_from_pads(state.scope, &indexes, &tags)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
Ok(Output::Render(build_modification_result_value(
"Untagged",
&result.affected_pads,
&result.messages,
state.output_mode,
state.mode,
false,
)))
}
#[handler]
pub fn rename(
#[ctx] ctx: &CommandContext,
#[arg(name = "old_name")] old_name: String,
#[arg(name = "new_name")] new_name: String,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).rename_tag(&old_name, &new_name)
}
#[handler]
pub fn delete(
#[ctx] ctx: &CommandContext,
#[arg] name: String,
) -> Result<Output<Value>, anyhow::Error> {
api(ctx).delete_tag(&name)
}
#[handler]
pub fn list(
#[ctx] ctx: &CommandContext,
#[arg] ids: Vec<String>,
) -> Result<Output<Value>, anyhow::Error> {
if ids.is_empty() {
api(ctx).list_tags()
} else {
let state = get_state(ctx);
let result = state.with_api(|api| {
api.list_pad_tags(state.scope, &ids)
.map_err(|e| anyhow::anyhow!("{}", e))
})?;
Ok(Output::Render(serde_json::json!({
"messages": result.messages,
})))
}
}
}