Skip to content

Commit

Permalink
feat: add select command (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
fioncat authored Feb 6, 2025
1 parent e198b39 commit 7680433
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 56 deletions.
14 changes: 7 additions & 7 deletions src/clipboard/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ use std::process::{Command, Stdio};
use anyhow::{bail, Context, Result};

/// Checks if a clipboard command is available and working
///
///
/// # Arguments
/// * `name` - Name of the command to check (e.g., "pbcopy", "wl-copy")
/// * `args` - Command line arguments for version/help check
///
///
/// # Returns
/// - `Ok(())` if command exists and works
/// - `Err` if command is not found or not working
Expand All @@ -20,12 +20,12 @@ pub fn check_command(name: &str, args: &[&str]) -> Result<()> {
}

/// Executes a command to write data to clipboard
///
///
/// # Arguments
/// * `name` - Name of the clipboard write command (e.g., "pbcopy", "wl-copy")
/// * `args` - Optional command line arguments
/// * `data` - Data to write to clipboard
///
///
/// # Returns
/// - `Ok(())` if write succeeds
/// - `Err` if command fails or writing fails
Expand Down Expand Up @@ -61,16 +61,16 @@ pub fn execute_write_command(name: &str, args: &[&str], data: &[u8]) -> Result<(
}

/// Executes a command to read data from clipboard
///
///
/// # Arguments
/// * `name` - Name of the clipboard read command (e.g., "pbpaste", "wl-paste")
/// * `args` - Optional command line arguments
///
///
/// # Returns
/// - `Ok(Vec<u8>)` containing the clipboard data if read succeeds
/// - `Ok(Vec::new())` if clipboard is empty (special case for some commands)
/// - `Err` if command fails or reading fails
///
///
/// # Special Cases
/// Handles the special case where some clipboard commands return exit code 1
/// with "Nothing is copied" message to indicate empty clipboard
Expand Down
21 changes: 13 additions & 8 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod delete;
mod get;
mod put;
mod read;
mod select;
mod server;
#[cfg(feature = "tray")]
mod tray;
Expand Down Expand Up @@ -33,40 +34,44 @@ pub struct App {

#[derive(Subcommand)]
pub enum Commands {
Cani(cani::CaniArgs),
Cb(cb::CbArgs),
Config(config::ShowConfigArgs),
Delete(delete::DeleteArgs),
Get(get::GetArgs),
Put(put::PutCommand),
Read(read::ReadCommand),
Cb(cb::CbArgs),
Whoami(whoami::WhoamiArgs),
Cani(cani::CaniArgs),
Select(select::SelectArgs),
Version(version::VersionArgs),
Whoami(whoami::WhoamiArgs),

#[cfg(feature = "tray")]
Tray(tray::TrayArgs),

Server(server::ServerArgs),
Daemon(daemon::DaemonArgs),
Server(server::ServerArgs),
}

#[async_trait]
impl RunCommand for App {
async fn run(&self) -> Result<()> {
match &self.command {
Commands::Cani(args) => args.run().await,
Commands::Cb(args) => args.run().await,
Commands::Config(args) => args.run().await,
Commands::Delete(args) => args.run().await,
Commands::Get(args) => args.run().await,
Commands::Put(args) => args.run().await,
Commands::Read(args) => args.run().await,
Commands::Cb(args) => args.run().await,
Commands::Whoami(args) => args.run().await,
Commands::Cani(args) => args.run().await,
Commands::Select(args) => args.run().await,
Commands::Version(args) => args.run().await,
Commands::Whoami(args) => args.run().await,

#[cfg(feature = "tray")]
Commands::Tray(args) => args.run().await,

Commands::Server(_) => unreachable!(),
Commands::Daemon(_) => unreachable!(),
Commands::Server(_) => unreachable!(),
}
}
}
Expand Down
20 changes: 2 additions & 18 deletions src/cmd/read/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::client::factory::ClientFactory;
use crate::client::Client;
use crate::clipboard::Clipboard;
use crate::cmd::{ConfigArgs, QueryArgs, RunCommand};
use crate::types::text::truncate_text;

/// Read text content from the server. This command allows reading multiple text contents
/// for further filtering and other operations.
Expand Down Expand Up @@ -104,9 +105,7 @@ impl TextArgs {
for text in texts {
let id = text.id;
let content = text.content.unwrap();
let line = Self::truncate_string(content, self.length);

let line = line.replace("\n", " ");
let line = truncate_text(content, self.length);
println!("{id}. {line}");
}

Expand All @@ -128,19 +127,4 @@ impl TextArgs {

input[..num_end].parse().context("parse id number")
}

pub fn truncate_string(mut s: String, max_len: usize) -> String {
if s.chars().count() <= max_len {
return s;
}

s.truncate(
s.char_indices()
.nth(max_len)
.map(|(i, _)| i)
.unwrap_or(s.len()),
);
s.push_str("...");
s
}
}
166 changes: 166 additions & 0 deletions src/cmd/select.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use std::io::{Read, Write};
use std::process::{Command, Stdio};

use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use clap::Args;

use crate::client::factory::ClientFactory;
use crate::client::Client;
use crate::clipboard::Clipboard;
use crate::config::CommonConfig;
use crate::daemon::client::DaemonClient;
use crate::daemon::config::DaemonConfig;
use crate::humanize::human_bytes;
use crate::types::text::truncate_text;

use super::{ConfigArgs, QueryArgs, RunCommand};

/// Read the latest text content list and use an external command (e.g., fzf) for searching and selecting.
/// The selected result can be processed in different ways (clipboard, daemon, or stdout).
#[derive(Args)]
pub struct SelectArgs {
/// The external selection command to execute. Text content will be written to stdin,
/// and results will be read from stdout. stderr is inherited directly.
/// Each candidate will be written as a line to stdin, truncated according to --length option.
#[arg(short, long, default_value = "fzf")]
pub cmd: String,

/// Maximum length of displayed text. Text longer than this will be truncated
#[arg(short, long, default_value = "100")]
pub length: usize,

/// Send the selected text to the daemon server
#[arg(short, long)]
pub daemon: bool,

/// Write the selected text to clipboard
#[arg(long)]
pub cb: bool,

/// Don't trim the trailing newline from the selection command output
#[arg(long)]
pub no_trim_break: bool,

#[command(flatten)]
pub query: QueryArgs,

#[command(flatten)]
pub config: ConfigArgs,
}

#[async_trait]
impl RunCommand for SelectArgs {
async fn run(&self) -> Result<()> {
let ps = self.config.build_path_set()?;

let client_factory = ClientFactory::load(&ps)?;
let client = client_factory.build_client_with_token_file().await?;

let text = self.select_text(client).await?;

if self.daemon {
let cfg: DaemonConfig = ps.load_config("daemon", DaemonConfig::default)?;
let client = DaemonClient::new(cfg.port);

let data = text.into_bytes();
let size = human_bytes(data.len() as u64);
client
.send_data(data)
.await
.context("send data to daemon")?;
println!("Send {size} data to daemon server");
return Ok(());
}

if self.cb {
let cb = Clipboard::load()?;
let size = human_bytes(text.len() as u64);
cb.write_text(text).context("write text to clipboard")?;
println!("Write {size} text to clipboard");
return Ok(());
}

print!("{text}");
Ok(())
}
}

impl SelectArgs {
const MAX_LENGTH: usize = 200;

async fn select_text(&self, client: Client) -> Result<String> {
if self.length == 0 {
bail!("Length must be greater than 0");
}
if self.length > Self::MAX_LENGTH {
bail!("Length must be less than {}", Self::MAX_LENGTH);
}

let mut texts = client.read_texts(self.query.build_query()?).await?;

if texts.is_empty() {
bail!("No text found");
}

let mut items = Vec::with_capacity(texts.len());
for text in texts.iter() {
let line = truncate_text(text.content.clone().unwrap(), self.length);
items.push(line);
}

let idx = self.execute_cmd(&items)?;
let text = texts.remove(idx).content.unwrap();
Ok(text)
}

fn execute_cmd(&self, items: &[String]) -> Result<usize> {
let mut input = String::with_capacity(items.len());
for item in items.iter() {
input.push_str(item);
input.push('\n');
}

let mut c = Command::new("bash");
c.args(["-c", &self.cmd]);

c.stdin(Stdio::piped());
c.stdout(Stdio::piped());
c.stderr(Stdio::inherit());

let mut child = c.spawn().context("launch select command")?;

let handle = child.stdin.as_mut().unwrap();
if let Err(err) = write!(handle, "{}", input) {
return Err(err).context("write data to select command");
}

drop(child.stdin.take());

let mut stdout = child.stdout.take().unwrap();

let mut out = String::new();
stdout
.read_to_string(&mut out)
.context("read select command output")?;

let result = if self.no_trim_break {
&out
} else {
if out.is_empty() {
bail!("select command output is empty");
}
&out[..out.len() - 1]
};

let status = child.wait().context("wait select command done")?;
match status.code() {
Some(0) => match items.iter().position(|s| s == result) {
Some(idx) => Ok(idx),
None => bail!("could not find item '{result}'"),
},
Some(code) => bail!("select command exited with code {code}"),
None => bail!("select command returned an unknown error"),
}
}
}
19 changes: 2 additions & 17 deletions src/tray/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::clipboard::Clipboard;
use crate::daemon::client::DaemonClient;
use crate::time::current_timestamp;
use crate::types::request::Query;
use crate::types::text::Text;
use crate::types::text::{truncate_text, Text};
use crate::types::token::TokenResponse;

pub struct TrayDaemon {
Expand Down Expand Up @@ -123,7 +123,7 @@ impl TrayDaemon {
let mut items = Vec::with_capacity(texts.len());
for text in texts {
let id = text.id.to_string();
let text = self.truncate_string(text.content.unwrap());
let text = truncate_text(text.content.unwrap(), self.truncate_size);
let text = text.replace("\n", "\\n");
items.push((id, text));
}
Expand Down Expand Up @@ -154,19 +154,4 @@ impl TrayDaemon {
self.token = Some(resp);
Ok(())
}

fn truncate_string(&self, mut s: String) -> String {
if s.chars().count() <= self.truncate_size {
return s;
}

s.truncate(
s.char_indices()
.nth(self.truncate_size)
.map(|(i, _)| i)
.unwrap_or(s.len()),
);
s.push_str("...");
s
}
}
8 changes: 2 additions & 6 deletions src/tray/ui.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
#![allow(deprecated)]

use std::sync::Arc;

use anyhow::{Context, Result};
use log::{error, info};
use tauri::menu::{Menu, MenuItem};
use tauri::AppHandle;
use tokio::sync::{mpsc, Mutex};
use tokio::sync::mpsc;

pub async fn build_and_run_tray_ui(
default_menu: Vec<(String, String)>,
menu_rx: mpsc::Receiver<Vec<(String, String)>>,
write_tx: mpsc::Sender<u64>,
) -> Result<()> {
let write_tx = Arc::new(Mutex::new(write_tx));

info!("Starting system tray event loop");
tauri::Builder::default()
.setup(|app| {
Expand Down Expand Up @@ -42,7 +38,7 @@ pub async fn build_and_run_tray_ui(
let write_tx = write_tx.clone();
tokio::spawn(async move {
info!("Sending menu item click event: {id}");
write_tx.lock().await.send(id).await.unwrap();
write_tx.send(id).await.unwrap();
});
})
.run(tauri::generate_context!())
Expand Down
Loading

0 comments on commit 7680433

Please sign in to comment.