Compare commits

..

2 Commits

Author SHA1 Message Date
dcc863c451 uds ipc working 2026-03-08 18:57:28 +01:00
9e56322705 uds 2026-03-08 18:37:23 +01:00
10 changed files with 303 additions and 102 deletions

3
Cargo.lock generated
View File

@ -2229,6 +2229,7 @@ dependencies = [
"ratatui-image",
"rklipd",
"serde",
"serde_json",
"uuid",
]
@ -2241,6 +2242,8 @@ dependencies = [
"directories",
"image",
"rusqlite",
"serde",
"serde_json",
"uuid",
"wayland-clipboard-listener",
]

View File

@ -11,5 +11,6 @@ image = "0.25.9"
ratatui = "0.30.0"
ratatui-image = { version = "10.0.6", features = ["crossterm"] }
rklipd = {path = "rklipd"}
serde = "1.0.228"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
uuid = "1.22.0"

3
rklipd/Cargo.lock generated
View File

@ -1303,6 +1303,8 @@ dependencies = [
"directories",
"image",
"rusqlite",
"serde",
"serde_json",
"uuid",
"wayland-clipboard-listener",
]
@ -1370,6 +1372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]

View File

@ -11,6 +11,8 @@ uuid = {version = "1.22.0", features = ["v4", "serde"]}
rusqlite = "0.38.0"
wayland-clipboard-listener = "0.6.0"
directories = "6.0.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
[features]
x11 = []

101
rklipd/src/ipc.rs Normal file
View File

@ -0,0 +1,101 @@
use crate::database::Database;
use crate::models::ClipboardData;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{Read, Write};
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::sync::{Arc, Mutex};
#[derive(Serialize, Deserialize, Debug)]
pub enum IpcRequest {
GetHistory { limit: usize },
SetClipboard { content: String },
}
#[derive(Serialize, Deserialize, Debug)]
pub enum IpcResponse {
History(Vec<String>),
}
pub fn start_server(db: Arc<Mutex<Database>>, socket_path: &Path) {
if socket_path.exists() {
let _ = fs::remove_file(socket_path);
}
let listener = match UnixListener::bind(socket_path) {
Ok(l) => l,
Err(e) => {
eprintln!("Error while creating socket {}", e);
return;
}
};
println!("ipc server listening {:?}", socket_path);
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let db_clone = Arc::clone(&db);
std::thread::spawn(move || {
let mut buffer = String::new();
if stream.read_to_string(&mut buffer).is_ok() {
if let Ok(request) = serde_json::from_str::<IpcRequest>(&buffer) {
match request {
IpcRequest::GetHistory { limit } => {
let db_lock = db_clone.lock().unwrap();
// TODO Implem read_history(limit)
let history = db_lock.read_history().unwrap_or_default();
let items: Vec<String> = history
.into_iter()
.rev()
.take(limit)
.map(|entry| match entry.content {
ClipboardData::Text(t) => t,
ClipboardData::Image(img) => format!("{}.jpg", img.id),
})
.collect();
let response = IpcResponse::History(items);
let response_json = serde_json::to_string(&response).unwrap();
let _ = stream.write_all(response_json.as_bytes());
}
IpcRequest::SetClipboard { content } => {
if let Ok(mut clipboard) = arboard::Clipboard::new() {
if content.ends_with(".jpg") || content.ends_with(".png") {
if let Some(proj_dirs) = directories::ProjectDirs::from(
"com", "zefad", "rklipd",
) {
let img_path = proj_dirs
.data_dir()
.join("images")
.join(&content);
if let Ok(img) = image::open(&img_path) {
let rgba = img.into_rgba8();
let img_data = arboard::ImageData {
width: rgba.width() as usize,
height: rgba.height() as usize,
bytes: std::borrow::Cow::Borrowed(
rgba.as_raw(),
),
};
let _ = clipboard.set_image(img_data);
}
}
} else {
let _ = clipboard.set_text(content);
}
}
}
}
}
}
});
}
Err(e) => eprintln!("Erreur de connexion IPC: {}", e),
}
}
}

View File

@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex};
mod clipboard;
mod database;
mod ipc;
mod models;
mod monitor;
mod ws;
@ -18,8 +19,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = Arc::new(Mutex::new(Database::init(&dir_path_str)?));
// println!("{:#?}", db.lock().unwrap().read_history());
let socket_path = dir_path.join("rklip.sock");
let db_for_ipc = Arc::clone(&db);
std::thread::spawn(move || {
crate::ipc::start_server(db_for_ipc, &socket_path);
});
println!("rklipd starting...");
monitor::start(db, clipboard)?;
Ok(())

View File

@ -1,7 +1,7 @@
use crate::ipc;
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
use ratatui::widgets::ListState;
use ratatui_image::{picker::Picker, protocol};
use std::path::Path;
#[derive(PartialEq)]
pub enum Mode {
@ -27,13 +27,15 @@ impl App {
pub fn new() -> Self {
let mut list_state = ListState::default();
list_state.select(Some(0));
let items = vec![
"Ceci est un texte copié.".to_string(),
"https://github.com/ratatui-org/ratatui".to_string(),
"30426b4d-26e0-45af-9fa4-25f4476387a8.jpg".to_string(),
"35789d6a-dea4-46de-90da-aee693a16031.jpg".to_string(),
"fn main() {\n println!(\"Hello\");\n}".to_string(),
];
let items = ipc::fetch_history(100).unwrap_or_default();
let mut list_state = ListState::default();
if items.is_empty() {
list_state.select(None);
} else {
list_state.select(Some(0));
}
let picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
@ -179,6 +181,17 @@ impl App {
}
pub fn get_selected_item(&self) -> Option<&String> {
self.list_state.selected().map(|i| &self.filtered_items[i])
self.list_state
.selected()
.and_then(|i| self.filtered_items.get(i))
}
pub fn sync_with_daemon(&mut self) {
if let Some(new_history) = crate::ipc::fetch_history(100) {
if self.all_items != new_history {
self.all_items = new_history;
self.update_search();
}
}
}
}

View File

@ -0,0 +1,51 @@
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
#[derive(Serialize, Deserialize, Debug)]
pub enum IpcRequest {
GetHistory { limit: usize },
SetClipboard { content: String },
}
#[derive(Serialize, Deserialize, Debug)]
pub enum IpcResponse {
History(Vec<String>),
}
pub fn fetch_history(limit: usize) -> Option<Vec<String>> {
let base_dir = directories::ProjectDirs::from("com", "zefad", "rklipd")?
.data_dir()
.to_path_buf();
let socket_path = base_dir.join("rklip.sock");
if let Ok(mut stream) = UnixStream::connect(&socket_path) {
let req = IpcRequest::GetHistory { limit };
let req_json = serde_json::to_string(&req).unwrap();
let _ = stream.write_all(req_json.as_bytes());
let _ = stream.shutdown(std::net::Shutdown::Write);
let mut response_buffer = String::new();
if stream.read_to_string(&mut response_buffer).is_ok() {
if let Ok(IpcResponse::History(items)) = serde_json::from_str(&response_buffer) {
return Some(items);
}
}
}
None
}
pub fn set_clipboard(content: String) {
if let Some(base_dir) = directories::ProjectDirs::from("com", "zefad", "rklipd") {
let socket_path = base_dir.data_dir().join("rklip.sock");
if let Ok(mut stream) = UnixStream::connect(&socket_path) {
let req = IpcRequest::SetClipboard { content };
if let Ok(req_json) = serde_json::to_string(&req) {
let _ = stream.write_all(req_json.as_bytes());
let _ = stream.shutdown(std::net::Shutdown::Write);
}
}
}
}

View File

@ -11,6 +11,7 @@ use crossterm::{
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
enable_raw_mode()?;
@ -39,105 +40,120 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App)
loop {
terminal.draw(|f| ui::render(f, app))?;
if let Event::Key(key) = event::read()? {
match app.mode {
Mode::Normal => match key.code {
KeyCode::Char('j') => {
app.next();
last_key_was_d = false;
}
KeyCode::Char('k') => {
app.previous();
last_key_was_d = false;
}
KeyCode::Char('d') => {
if last_key_was_d {
app.delete_selected();
last_key_was_d = false;
} else {
last_key_was_d = true;
if event::poll(Duration::from_millis(500))? {
if let Event::Key(key) = event::read()? {
match app.mode {
Mode::Normal => match key.code {
KeyCode::Enter => {
if let Some(selected) = app.get_selected_item() {
crate::ipc::set_clipboard(selected.clone());
app.should_quit = true;
}
}
last_key_was_g = false;
}
KeyCode::Char('u') => {
app.undo_delete();
last_key_was_d = false;
}
KeyCode::Char('g') => {
if last_key_was_g {
if !app.filtered_items.is_empty() {
app.list_state.select(Some(0));
KeyCode::Char('j') => {
app.next();
last_key_was_d = false;
}
KeyCode::Char('k') => {
app.previous();
last_key_was_d = false;
}
KeyCode::Char('d') => {
if last_key_was_d {
app.delete_selected();
last_key_was_d = false;
} else {
last_key_was_d = true;
}
last_key_was_g = false;
} else {
last_key_was_g = true;
}
last_key_was_d = false;
}
KeyCode::Char('G') => {
if !app.filtered_items.is_empty() {
app.list_state.select(Some(app.filtered_items.len() - 1));
KeyCode::Char('u') => {
app.undo_delete();
last_key_was_d = false;
}
last_key_was_d = false;
}
KeyCode::Char(':') => {
app.mode = Mode::Command;
app.input_buffer.clear();
last_key_was_d = false;
}
KeyCode::Char('/') => {
app.mode = Mode::Search;
app.input_buffer.clear();
app.update_search();
last_key_was_d = false;
}
KeyCode::Char('q') => {
app.should_quit = true;
}
_ => {
last_key_was_d = false;
}
},
Mode::Command => match key.code {
KeyCode::Esc => {
app.mode = Mode::Normal;
app.input_buffer.clear();
app.update_search();
}
KeyCode::Char(c) => app.input_buffer.push(c),
KeyCode::Backspace => {
app.input_buffer.pop();
}
KeyCode::Enter => {
if app.input_buffer == "q" {
KeyCode::Char('g') => {
if last_key_was_g {
if !app.filtered_items.is_empty() {
app.list_state.select(Some(0));
}
last_key_was_g = false;
} else {
last_key_was_g = true;
}
last_key_was_d = false;
}
KeyCode::Char('G') => {
if !app.filtered_items.is_empty() {
app.list_state.select(Some(app.filtered_items.len() - 1));
}
last_key_was_d = false;
}
KeyCode::Char(':') => {
app.mode = Mode::Command;
app.input_buffer.clear();
last_key_was_d = false;
}
KeyCode::Char('/') => {
app.mode = Mode::Search;
app.input_buffer.clear();
app.update_search();
last_key_was_d = false;
}
KeyCode::Char('q') => {
app.should_quit = true;
}
app.mode = Mode::Normal;
app.input_buffer.clear();
app.update_search();
}
_ => {}
},
_ => {
last_key_was_d = false;
}
},
Mode::Search => match key.code {
// ... ton code de recherche ...
KeyCode::Esc | KeyCode::Enter => {
app.mode = Mode::Normal;
app.input_buffer.clear();
app.update_search();
}
KeyCode::Char(c) => {
app.input_buffer.push(c);
app.update_search();
}
KeyCode::Backspace => {
app.input_buffer.pop();
app.update_search();
}
_ => {}
},
Mode::Command => match key.code {
KeyCode::Esc => {
app.mode = Mode::Normal;
app.input_buffer.clear();
app.update_search();
}
KeyCode::Char(c) => app.input_buffer.push(c),
KeyCode::Backspace => {
app.input_buffer.pop();
}
KeyCode::Enter => {
if app.input_buffer == "q" {
app.should_quit = true;
}
app.mode = Mode::Normal;
app.input_buffer.clear();
app.update_search();
}
_ => {}
},
Mode::Search => match key.code {
KeyCode::Esc => {
app.mode = Mode::Normal;
app.input_buffer.clear();
app.update_search();
}
KeyCode::Enter => {
if let Some(selected) = app.get_selected_item() {
crate::ipc::set_clipboard(selected.clone());
app.should_quit = true;
}
}
KeyCode::Char(c) => {
app.input_buffer.push(c);
app.update_search();
}
KeyCode::Backspace => {
app.input_buffer.pop();
app.update_search();
}
_ => {}
},
}
}
} else {
app.sync_with_daemon();
}
if app.should_quit {

View File

@ -1,4 +1,9 @@
use crate::app::{App, Mode};
use std::{io::Write, os::unix::net::UnixStream};
use crate::{
app::{App, Mode},
ipc::IpcRequest,
};
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout},