mirror of
https://github.com/Noratrieb/clippyboard.git
synced 2026-01-14 09:55:04 +01:00
cook
This commit is contained in:
commit
1164ed1e3e
8 changed files with 4696 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use nix
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
4339
Cargo.lock
generated
Normal file
4339
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "clippyboard"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ciborium = "0.2.2"
|
||||||
|
dirs = "6.0.0"
|
||||||
|
eframe = "0.32.2"
|
||||||
|
egui_extras = { version = "0.32.2", features = ["image"] }
|
||||||
|
eyre = "0.6.12"
|
||||||
|
serde = "1.0.219"
|
||||||
|
wl-clipboard-rs = "0.9.2"
|
||||||
22
shell.nix
Normal file
22
shell.nix
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
|
pkgs.mkShell rec {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
expat
|
||||||
|
fontconfig
|
||||||
|
freetype
|
||||||
|
freetype.dev
|
||||||
|
libGL
|
||||||
|
pkg-config
|
||||||
|
xorg.libX11
|
||||||
|
xorg.libXcursor
|
||||||
|
xorg.libXi
|
||||||
|
xorg.libXrandr
|
||||||
|
wayland
|
||||||
|
libxkbcommon
|
||||||
|
];
|
||||||
|
|
||||||
|
LD_LIBRARY_PATH =
|
||||||
|
builtins.foldl' (a: b: "${a}:${b}/lib") "${pkgs.vulkan-loader}/lib" buildInputs;
|
||||||
|
}
|
||||||
|
|
||||||
131
src/daemon.rs
Normal file
131
src/daemon.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
use super::Entry;
|
||||||
|
use super::MAX_ENTRY_SIZE;
|
||||||
|
use eyre::Context;
|
||||||
|
use std::io::BufWriter;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::os::unix::net::UnixListener;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
use wl_clipboard_rs::paste::ClipboardType;
|
||||||
|
use wl_clipboard_rs::paste::MimeType;
|
||||||
|
use wl_clipboard_rs::paste::Seat;
|
||||||
|
|
||||||
|
pub(crate) fn handle_peer(
|
||||||
|
mut peer: UnixStream,
|
||||||
|
next_id: Arc<AtomicU64>,
|
||||||
|
items: Arc<Mutex<Vec<Entry>>>,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
let mut request = [0; 1];
|
||||||
|
peer.read_exact(&mut request)
|
||||||
|
.wrap_err("failed to read message type")?;
|
||||||
|
match request[0] {
|
||||||
|
super::MESSAGE_STORE => {
|
||||||
|
let mime_types =
|
||||||
|
wl_clipboard_rs::paste::get_mime_types(ClipboardType::Regular, Seat::Unspecified)
|
||||||
|
.wrap_err("getting mime types")?;
|
||||||
|
|
||||||
|
let time = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let Some(mime) = ["text/plain", "image/png"]
|
||||||
|
.iter()
|
||||||
|
.find(|mime| mime_types.contains(**mime))
|
||||||
|
else {
|
||||||
|
eprintln!("WARN: No supported mime type found. Found mime types: {mime_types:?}");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let (data_readear, _) = wl_clipboard_rs::paste::get_contents(
|
||||||
|
ClipboardType::Regular,
|
||||||
|
Seat::Unspecified,
|
||||||
|
MimeType::Specific(mime),
|
||||||
|
)
|
||||||
|
.wrap_err("getting contents")?;
|
||||||
|
|
||||||
|
let mut data_reader = data_readear.take(MAX_ENTRY_SIZE);
|
||||||
|
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data_reader
|
||||||
|
.read_to_end(&mut data)
|
||||||
|
.wrap_err("reading content data")?;
|
||||||
|
|
||||||
|
items.lock().unwrap().push(Entry {
|
||||||
|
id: next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||||
|
mime: mime.to_string(),
|
||||||
|
data,
|
||||||
|
created_time: u64::try_from(time.as_millis()).unwrap(),
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("INFO Successfully stored clipboard value of mime type {mime}");
|
||||||
|
}
|
||||||
|
super::MESSAGE_READ => {
|
||||||
|
let items = items.lock().unwrap();
|
||||||
|
|
||||||
|
ciborium::into_writer(items.as_slice(), BufWriter::new(peer))
|
||||||
|
.wrap_err("writing items to socket")?;
|
||||||
|
}
|
||||||
|
super::MESSAGE_COPY => {
|
||||||
|
let mut id = [0; 8];
|
||||||
|
peer.read_exact(&mut id).wrap_err("failed to read id")?;
|
||||||
|
let id = u64::from_le_bytes(id);
|
||||||
|
|
||||||
|
let items = items.lock().unwrap();
|
||||||
|
|
||||||
|
let Some(idx) = items.iter().position(|item| item.id == id) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = items[idx].clone();
|
||||||
|
|
||||||
|
// select
|
||||||
|
let mut opts = wl_clipboard_rs::copy::Options::new();
|
||||||
|
opts.clipboard(wl_clipboard_rs::copy::ClipboardType::Regular);
|
||||||
|
let result = wl_clipboard_rs::copy::copy(
|
||||||
|
opts,
|
||||||
|
wl_clipboard_rs::copy::Source::Bytes(entry.data.into_boxed_slice()),
|
||||||
|
wl_clipboard_rs::copy::MimeType::Specific(entry.mime),
|
||||||
|
);
|
||||||
|
if let Err(err) = result {
|
||||||
|
println!("WARNING: Copy failed: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(socket_path: &PathBuf) -> eyre::Result<()> {
|
||||||
|
let _ = std::fs::remove_file(&socket_path); // lol
|
||||||
|
let socket = UnixListener::bind(&socket_path)
|
||||||
|
.wrap_err_with(|| format!("binding path {}", socket_path.display()))?;
|
||||||
|
|
||||||
|
let next_id = Arc::new(AtomicU64::new(0));
|
||||||
|
let items = Arc::new(Mutex::new(Vec::<Entry>::new()));
|
||||||
|
|
||||||
|
println!("INFO: Listening on {}", socket_path.display());
|
||||||
|
|
||||||
|
for peer in socket.incoming() {
|
||||||
|
match peer {
|
||||||
|
Ok(peer) => {
|
||||||
|
let next_id = next_id.clone();
|
||||||
|
let items = items.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = handle_peer(peer, next_id, items);
|
||||||
|
if let Err(err) = result {
|
||||||
|
eprintln!("ERROR: Error handling peer: {err:?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("ERROR: Error accepting peer: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
139
src/display.rs
Normal file
139
src/display.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
use eframe::egui;
|
||||||
|
use eyre::Context;
|
||||||
|
|
||||||
|
use crate::MESSAGE_READ;
|
||||||
|
|
||||||
|
use super::MESSAGE_COPY;
|
||||||
|
|
||||||
|
use std::{io::{BufReader, Write}, os::unix::net::UnixStream, path::Path, time::Instant};
|
||||||
|
|
||||||
|
use super::Entry;
|
||||||
|
|
||||||
|
pub(crate) struct App {
|
||||||
|
pub(crate) items: Vec<Entry>,
|
||||||
|
pub(crate) selected_idx: usize,
|
||||||
|
pub(crate) socket: UnixStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for App {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui.input(|i| {
|
||||||
|
if i.key_pressed(egui::Key::J) || i.key_pressed(egui::Key::ArrowDown) {
|
||||||
|
if self.selected_idx + 1 != self.items.len() {
|
||||||
|
self.selected_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i.key_pressed(egui::Key::K) || i.key_pressed(egui::Key::ArrowUp) {
|
||||||
|
self.selected_idx = self.selected_idx.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.key_pressed(egui::Key::Enter)
|
||||||
|
&& let Some(item) = self.items.get(self.selected_idx)
|
||||||
|
{
|
||||||
|
let _ = self.socket.write_all(&[MESSAGE_COPY]);
|
||||||
|
let _ = self.socket.write_all(&item.id.to_le_bytes());
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.heading("clippyboard");
|
||||||
|
|
||||||
|
egui::SidePanel::left("selection_panel")
|
||||||
|
.default_width(400.0)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
ui.heading("History");
|
||||||
|
|
||||||
|
for (idx, item) in self.items.iter().enumerate() {
|
||||||
|
let mut frame = egui::Frame::new();
|
||||||
|
if self.selected_idx == idx {
|
||||||
|
frame = frame.stroke(egui::Stroke::new(1.0, egui::Color32::PURPLE));
|
||||||
|
}
|
||||||
|
frame.show(ui, |ui| match item.mime.as_str() {
|
||||||
|
"text/plain" => {
|
||||||
|
ui.label(str::from_utf8(&item.data).unwrap_or("<invalid UTF-8>"));
|
||||||
|
}
|
||||||
|
"image/png" => {
|
||||||
|
ui.label("<image>");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ui.label("<unsupported mime type>");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||||
|
ui.heading("Detail");
|
||||||
|
let Some(item) = &self.items.get(self.selected_idx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match item.mime.as_str() {
|
||||||
|
"text/plain" => {
|
||||||
|
ui.label(str::from_utf8(&item.data).unwrap_or("<invalid UTF-8>"));
|
||||||
|
}
|
||||||
|
"image/png" => {
|
||||||
|
ui.image(egui::ImageSource::Bytes {
|
||||||
|
uri: format!("bytes://{}", item.id).into(),
|
||||||
|
bytes: item.data.clone().into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ui.label("<unsupported mime type>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(socket_path: &Path) -> eyre::Result<()> {
|
||||||
|
let mut socket = UnixStream::connect(&socket_path).wrap_err_with(|| {
|
||||||
|
format!(
|
||||||
|
"connecting to socket at {}. is the daemon running?",
|
||||||
|
socket_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
socket
|
||||||
|
.write_all(&[MESSAGE_READ])
|
||||||
|
.wrap_err("writing request type")?;
|
||||||
|
|
||||||
|
println!("INFO: Reading clipboard history from socket");
|
||||||
|
let start = Instant::now();
|
||||||
|
let items =
|
||||||
|
ciborium::from_reader(BufReader::new(socket)).wrap_err("reading items from socket")?;
|
||||||
|
println!(
|
||||||
|
"INFO: Read clipboard history from socket in {:?}",
|
||||||
|
start.elapsed()
|
||||||
|
);
|
||||||
|
|
||||||
|
// heh. good design.
|
||||||
|
let socket = UnixStream::connect(&socket_path).wrap_err_with(|| {
|
||||||
|
format!(
|
||||||
|
"connecting to socket at {}. is the daemon running?",
|
||||||
|
socket_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_inner_size([500.0, 500.0]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native(
|
||||||
|
"clippyboard",
|
||||||
|
options,
|
||||||
|
Box::new(|cc| {
|
||||||
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
|
Ok(Box::new(App {
|
||||||
|
items,
|
||||||
|
selected_idx: 0,
|
||||||
|
socket,
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.map_err(|err| eyre::eyre!(err.to_string()))
|
||||||
|
.wrap_err("running GUI")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
50
src/main.rs
Normal file
50
src/main.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
mod display;
|
||||||
|
mod daemon;
|
||||||
|
|
||||||
|
use std::{io::Write, os::unix::net::UnixStream};
|
||||||
|
use eyre::{Context, OptionExt, bail};
|
||||||
|
|
||||||
|
const MAX_ENTRY_SIZE: u64 = 100_000_000;
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct Entry {
|
||||||
|
id: u64,
|
||||||
|
mime: String,
|
||||||
|
data: Vec<u8>,
|
||||||
|
created_time: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MESSAGE_STORE: u8 = 0;
|
||||||
|
const MESSAGE_READ: u8 = 1;
|
||||||
|
/// Argument: One u64-bit LE value, the ID
|
||||||
|
const MESSAGE_COPY: u8 = 2;
|
||||||
|
|
||||||
|
fn main() -> eyre::Result<()> {
|
||||||
|
let Some(mode) = std::env::args().nth(1) else {
|
||||||
|
bail!("missing mode");
|
||||||
|
};
|
||||||
|
|
||||||
|
let socket_path = dirs::runtime_dir()
|
||||||
|
.ok_or_eyre("missing XDG_RUNTIME_DIR")?
|
||||||
|
.join("clippyboard.sock");
|
||||||
|
|
||||||
|
match mode.as_str() {
|
||||||
|
"daemon" => daemon::main(&socket_path)?,
|
||||||
|
"store" => {
|
||||||
|
let mut socket = UnixStream::connect(&socket_path).wrap_err_with(|| {
|
||||||
|
format!(
|
||||||
|
"connecting to socket at {}. is the daemon running?",
|
||||||
|
socket_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
socket
|
||||||
|
.write_all(&[MESSAGE_STORE])
|
||||||
|
.wrap_err("writing request type")?;
|
||||||
|
}
|
||||||
|
"display" => display::main(&socket_path)?,
|
||||||
|
_ => panic!("invalid mode"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue