Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add select command #36

Merged
merged 2 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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