mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-26 11:29:59 +00:00
treewide: format with rustfmt
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I6a6a69642c2865f41a4b141ddf39a198a3fc2e09
This commit is contained in:
parent
404990f928
commit
6a5cd9b95d
10 changed files with 1191 additions and 1132 deletions
|
|
@ -1,75 +1,78 @@
|
|||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::db::StashError;
|
||||
use wl_clipboard_rs::paste::{ClipboardType, MimeType, Seat, get_contents};
|
||||
|
||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||
|
||||
pub trait DecodeCommand {
|
||||
fn decode(
|
||||
&self,
|
||||
in_: impl Read,
|
||||
out: impl Write,
|
||||
input: Option<String>,
|
||||
) -> Result<(), StashError>;
|
||||
fn decode(
|
||||
&self,
|
||||
in_: impl Read,
|
||||
out: impl Write,
|
||||
input: Option<String>,
|
||||
) -> Result<(), StashError>;
|
||||
}
|
||||
|
||||
impl DecodeCommand for SqliteClipboardDb {
|
||||
fn decode(
|
||||
&self,
|
||||
mut in_: impl Read,
|
||||
mut out: impl Write,
|
||||
input: Option<String>,
|
||||
) -> Result<(), StashError> {
|
||||
let input_str = if let Some(s) = input {
|
||||
s
|
||||
fn decode(
|
||||
&self,
|
||||
mut in_: impl Read,
|
||||
mut out: impl Write,
|
||||
input: Option<String>,
|
||||
) -> Result<(), StashError> {
|
||||
let input_str = if let Some(s) = input {
|
||||
s
|
||||
} else {
|
||||
let mut buf = String::new();
|
||||
if let Err(e) = in_.read_to_string(&mut buf) {
|
||||
log::error!("Failed to read stdin for decode: {e}");
|
||||
}
|
||||
buf
|
||||
};
|
||||
|
||||
// If input is empty or whitespace, treat as error and trigger fallback
|
||||
if input_str.trim().is_empty() {
|
||||
log::info!("No input provided to decode; relaying clipboard to stdout");
|
||||
if let Ok((mut reader, _mime)) =
|
||||
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
if let Err(err) = reader.read_to_end(&mut buf) {
|
||||
log::error!("Failed to read clipboard for relay: {err}");
|
||||
} else {
|
||||
let mut buf = String::new();
|
||||
if let Err(e) = in_.read_to_string(&mut buf) {
|
||||
log::error!("Failed to read stdin for decode: {e}");
|
||||
}
|
||||
buf
|
||||
};
|
||||
|
||||
// If input is empty or whitespace, treat as error and trigger fallback
|
||||
if input_str.trim().is_empty() {
|
||||
log::info!("No input provided to decode; relaying clipboard to stdout");
|
||||
if let Ok((mut reader, _mime)) =
|
||||
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
if let Err(err) = reader.read_to_end(&mut buf) {
|
||||
log::error!("Failed to read clipboard for relay: {err}");
|
||||
} else {
|
||||
let _ = out.write_all(&buf);
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to get clipboard contents for relay");
|
||||
}
|
||||
return Ok(());
|
||||
let _ = out.write_all(&buf);
|
||||
}
|
||||
|
||||
// Try decode as usual
|
||||
match self.decode_entry(input_str.as_bytes(), &mut out, Some(input_str.clone())) {
|
||||
Ok(()) => {
|
||||
log::info!("Entry decoded");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to decode entry: {e}");
|
||||
if let Ok((mut reader, _mime)) =
|
||||
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
if let Err(err) = reader.read_to_end(&mut buf) {
|
||||
log::error!("Failed to read clipboard for relay: {err}");
|
||||
} else {
|
||||
let _ = out.write_all(&buf);
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to get clipboard contents for relay");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
log::error!("Failed to get clipboard contents for relay");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try decode as usual
|
||||
match self.decode_entry(
|
||||
input_str.as_bytes(),
|
||||
&mut out,
|
||||
Some(input_str.clone()),
|
||||
) {
|
||||
Ok(()) => {
|
||||
log::info!("Entry decoded");
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to decode entry: {e}");
|
||||
if let Ok((mut reader, _mime)) =
|
||||
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
if let Err(err) = reader.read_to_end(&mut buf) {
|
||||
log::error!("Failed to read clipboard for relay: {err}");
|
||||
} else {
|
||||
let _ = out.write_all(&buf);
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to get clipboard contents for relay");
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||
|
||||
pub trait DeleteCommand {
|
||||
fn delete(&self, input: impl Read) -> Result<usize, StashError>;
|
||||
fn delete(&self, input: impl Read) -> Result<usize, StashError>;
|
||||
}
|
||||
|
||||
impl DeleteCommand for SqliteClipboardDb {
|
||||
fn delete(&self, input: impl Read) -> Result<usize, StashError> {
|
||||
match self.delete_entries(input) {
|
||||
Ok(deleted) => {
|
||||
log::info!("Deleted {deleted} entries");
|
||||
Ok(deleted)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to delete entries: {e}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
fn delete(&self, input: impl Read) -> Result<usize, StashError> {
|
||||
match self.delete_entries(input) {
|
||||
Ok(deleted) => {
|
||||
log::info!("Deleted {deleted} entries");
|
||||
Ok(deleted)
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to delete entries: {e}");
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,274 +1,286 @@
|
|||
use std::io::Write;
|
||||
|
||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||
|
||||
pub trait ListCommand {
|
||||
fn list(&self, out: impl Write, preview_width: u32) -> Result<(), StashError>;
|
||||
fn list(&self, out: impl Write, preview_width: u32)
|
||||
-> Result<(), StashError>;
|
||||
}
|
||||
|
||||
impl ListCommand for SqliteClipboardDb {
|
||||
fn list(&self, out: impl Write, preview_width: u32) -> Result<(), StashError> {
|
||||
self.list_entries(out, preview_width)?;
|
||||
log::info!("Listed clipboard entries");
|
||||
Ok(())
|
||||
}
|
||||
fn list(
|
||||
&self,
|
||||
out: impl Write,
|
||||
preview_width: u32,
|
||||
) -> Result<(), StashError> {
|
||||
self.list_entries(out, preview_width)?;
|
||||
log::info!("Listed clipboard entries");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SqliteClipboardDb {
|
||||
/// Public TUI listing function for use in main.rs
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn list_tui(&self, preview_width: u32) -> Result<(), StashError> {
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||
},
|
||||
};
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::CrosstermBackend,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
};
|
||||
use std::io::stdout;
|
||||
/// Public TUI listing function for use in main.rs
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn list_tui(&self, preview_width: u32) -> Result<(), StashError> {
|
||||
use std::io::stdout;
|
||||
|
||||
// Query entries from DB
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC")
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let mut rows = stmt
|
||||
.query([])
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{
|
||||
EnterAlternateScreen,
|
||||
LeaveAlternateScreen,
|
||||
disable_raw_mode,
|
||||
enable_raw_mode,
|
||||
},
|
||||
};
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::CrosstermBackend,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
};
|
||||
|
||||
let mut entries: Vec<(u64, String, String)> = Vec::new();
|
||||
let mut max_id_width = 2;
|
||||
let mut max_mime_width = 8;
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?
|
||||
{
|
||||
let id: u64 = row
|
||||
.get(0)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let contents: Vec<u8> = row
|
||||
.get(1)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let mime: Option<String> = row
|
||||
.get(2)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let preview = crate::db::preview_entry(&contents, mime.as_deref(), preview_width);
|
||||
let mime_str = mime.as_deref().unwrap_or("").to_string();
|
||||
let id_str = id.to_string();
|
||||
max_id_width = max_id_width.max(id_str.width());
|
||||
max_mime_width = max_mime_width.max(mime_str.width());
|
||||
entries.push((id, preview, mime_str));
|
||||
}
|
||||
// Query entries from DB
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC")
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let mut rows = stmt
|
||||
.query([])
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
|
||||
enable_raw_mode().map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal =
|
||||
Terminal::new(backend).map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
|
||||
let mut state = ListState::default();
|
||||
if !entries.is_empty() {
|
||||
state.select(Some(0));
|
||||
}
|
||||
|
||||
let res = (|| -> Result<(), StashError> {
|
||||
loop {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let area = f.area();
|
||||
let block = Block::default()
|
||||
.title("Clipboard Entries (j/k/↑/↓ to move, q/ESC to quit)")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let border_width = 2;
|
||||
let highlight_symbol = ">";
|
||||
let highlight_width = 1;
|
||||
let content_width = area.width as usize - border_width;
|
||||
|
||||
// Minimum widths for columns
|
||||
let min_id_width = 2;
|
||||
let min_mime_width = 6;
|
||||
let min_preview_width = 4;
|
||||
let spaces = 3; // [id][ ][preview][ ][mime]
|
||||
|
||||
// Dynamically allocate widths
|
||||
let mut id_col = max_id_width.max(min_id_width);
|
||||
let mut mime_col = max_mime_width.max(min_mime_width);
|
||||
let mut preview_col = content_width
|
||||
.saturating_sub(highlight_width)
|
||||
.saturating_sub(id_col)
|
||||
.saturating_sub(mime_col)
|
||||
.saturating_sub(spaces);
|
||||
|
||||
// If not enough space, shrink columns
|
||||
if preview_col < min_preview_width {
|
||||
let needed = min_preview_width - preview_col;
|
||||
if mime_col > min_mime_width {
|
||||
let reduce = mime_col - min_mime_width;
|
||||
let take = reduce.min(needed);
|
||||
mime_col -= take;
|
||||
preview_col += take;
|
||||
}
|
||||
}
|
||||
if preview_col < min_preview_width {
|
||||
let needed = min_preview_width - preview_col;
|
||||
if id_col > min_id_width {
|
||||
let reduce = id_col - min_id_width;
|
||||
let take = reduce.min(needed);
|
||||
id_col -= take;
|
||||
preview_col += take;
|
||||
}
|
||||
}
|
||||
if preview_col < min_preview_width {
|
||||
preview_col = min_preview_width;
|
||||
}
|
||||
|
||||
let selected = state.selected();
|
||||
|
||||
let list_items: Vec<ListItem> = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| {
|
||||
// Truncate preview by grapheme clusters and display width
|
||||
let mut preview = String::new();
|
||||
let mut width = 0;
|
||||
for g in entry.1.graphemes(true) {
|
||||
let g_width = UnicodeWidthStr::width(g);
|
||||
if width + g_width > preview_col {
|
||||
preview.push('…');
|
||||
break;
|
||||
}
|
||||
preview.push_str(g);
|
||||
width += g_width;
|
||||
}
|
||||
// Truncate and pad mimetype
|
||||
let mut mime = String::new();
|
||||
let mut mwidth = 0;
|
||||
for g in entry.2.graphemes(true) {
|
||||
let g_width = UnicodeWidthStr::width(g);
|
||||
if mwidth + g_width > mime_col {
|
||||
mime.push('…');
|
||||
break;
|
||||
}
|
||||
mime.push_str(g);
|
||||
mwidth += g_width;
|
||||
}
|
||||
|
||||
// Compose the row as highlight + id + space + preview + space + mimetype
|
||||
let mut spans = Vec::new();
|
||||
let (id, preview, mime) = entry;
|
||||
if Some(i) == selected {
|
||||
spans.push(Span::styled(
|
||||
highlight_symbol,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
format!("{id:>id_col$}"),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(
|
||||
format!("{preview:<preview_col$}"),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(
|
||||
format!("{mime:>mime_col$}"),
|
||||
Style::default().fg(Color::Green),
|
||||
));
|
||||
} else {
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::raw(format!("{id:>id_col$}")));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::raw(format!("{preview:<preview_col$}")));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::raw(format!("{mime:>mime_col$}")));
|
||||
}
|
||||
ListItem::new(Line::from(spans))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(list_items)
|
||||
.block(block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(""); // handled manually
|
||||
|
||||
f.render_stateful_widget(list, area, &mut state);
|
||||
})
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
|
||||
if event::poll(std::time::Duration::from_millis(250))
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?
|
||||
{
|
||||
if let Event::Key(key) =
|
||||
event::read().map_err(|e| StashError::ListDecode(e.to_string()))?
|
||||
{
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
let i = match state.selected() {
|
||||
Some(i) => {
|
||||
if i >= entries.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
state.select(Some(i));
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
let i = match state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
entries.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
state.select(Some(i));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
disable_raw_mode().ok();
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)
|
||||
.ok();
|
||||
terminal.show_cursor().ok();
|
||||
|
||||
res
|
||||
let mut entries: Vec<(u64, String, String)> = Vec::new();
|
||||
let mut max_id_width = 2;
|
||||
let mut max_mime_width = 8;
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?
|
||||
{
|
||||
let id: u64 = row
|
||||
.get(0)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let contents: Vec<u8> = row
|
||||
.get(1)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let mime: Option<String> = row
|
||||
.get(2)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let preview =
|
||||
crate::db::preview_entry(&contents, mime.as_deref(), preview_width);
|
||||
let mime_str = mime.as_deref().unwrap_or("").to_string();
|
||||
let id_str = id.to_string();
|
||||
max_id_width = max_id_width.max(id_str.width());
|
||||
max_mime_width = max_mime_width.max(mime_str.width());
|
||||
entries.push((id, preview, mime_str));
|
||||
}
|
||||
|
||||
enable_raw_mode().map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
|
||||
let mut state = ListState::default();
|
||||
if !entries.is_empty() {
|
||||
state.select(Some(0));
|
||||
}
|
||||
|
||||
let res = (|| -> Result<(), StashError> {
|
||||
loop {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let area = f.area();
|
||||
let block = Block::default()
|
||||
.title("Clipboard Entries (j/k/↑/↓ to move, q/ESC to quit)")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let border_width = 2;
|
||||
let highlight_symbol = ">";
|
||||
let highlight_width = 1;
|
||||
let content_width = area.width as usize - border_width;
|
||||
|
||||
// Minimum widths for columns
|
||||
let min_id_width = 2;
|
||||
let min_mime_width = 6;
|
||||
let min_preview_width = 4;
|
||||
let spaces = 3; // [id][ ][preview][ ][mime]
|
||||
|
||||
// Dynamically allocate widths
|
||||
let mut id_col = max_id_width.max(min_id_width);
|
||||
let mut mime_col = max_mime_width.max(min_mime_width);
|
||||
let mut preview_col = content_width
|
||||
.saturating_sub(highlight_width)
|
||||
.saturating_sub(id_col)
|
||||
.saturating_sub(mime_col)
|
||||
.saturating_sub(spaces);
|
||||
|
||||
// If not enough space, shrink columns
|
||||
if preview_col < min_preview_width {
|
||||
let needed = min_preview_width - preview_col;
|
||||
if mime_col > min_mime_width {
|
||||
let reduce = mime_col - min_mime_width;
|
||||
let take = reduce.min(needed);
|
||||
mime_col -= take;
|
||||
preview_col += take;
|
||||
}
|
||||
}
|
||||
if preview_col < min_preview_width {
|
||||
let needed = min_preview_width - preview_col;
|
||||
if id_col > min_id_width {
|
||||
let reduce = id_col - min_id_width;
|
||||
let take = reduce.min(needed);
|
||||
id_col -= take;
|
||||
preview_col += take;
|
||||
}
|
||||
}
|
||||
if preview_col < min_preview_width {
|
||||
preview_col = min_preview_width;
|
||||
}
|
||||
|
||||
let selected = state.selected();
|
||||
|
||||
let list_items: Vec<ListItem> = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| {
|
||||
// Truncate preview by grapheme clusters and display width
|
||||
let mut preview = String::new();
|
||||
let mut width = 0;
|
||||
for g in entry.1.graphemes(true) {
|
||||
let g_width = UnicodeWidthStr::width(g);
|
||||
if width + g_width > preview_col {
|
||||
preview.push('…');
|
||||
break;
|
||||
}
|
||||
preview.push_str(g);
|
||||
width += g_width;
|
||||
}
|
||||
// Truncate and pad mimetype
|
||||
let mut mime = String::new();
|
||||
let mut mwidth = 0;
|
||||
for g in entry.2.graphemes(true) {
|
||||
let g_width = UnicodeWidthStr::width(g);
|
||||
if mwidth + g_width > mime_col {
|
||||
mime.push('…');
|
||||
break;
|
||||
}
|
||||
mime.push_str(g);
|
||||
mwidth += g_width;
|
||||
}
|
||||
|
||||
// Compose the row as highlight + id + space + preview + space +
|
||||
// mimetype
|
||||
let mut spans = Vec::new();
|
||||
let (id, preview, mime) = entry;
|
||||
if Some(i) == selected {
|
||||
spans.push(Span::styled(
|
||||
highlight_symbol,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
format!("{id:>id_col$}"),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(
|
||||
format!("{preview:<preview_col$}"),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::styled(
|
||||
format!("{mime:>mime_col$}"),
|
||||
Style::default().fg(Color::Green),
|
||||
));
|
||||
} else {
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::raw(format!("{id:>id_col$}")));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::raw(format!("{preview:<preview_col$}")));
|
||||
spans.push(Span::raw(" "));
|
||||
spans.push(Span::raw(format!("{mime:>mime_col$}")));
|
||||
}
|
||||
ListItem::new(Line::from(spans))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(list_items)
|
||||
.block(block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(""); // handled manually
|
||||
|
||||
f.render_stateful_widget(list, area, &mut state);
|
||||
})
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||
|
||||
if event::poll(std::time::Duration::from_millis(250))
|
||||
.map_err(|e| StashError::ListDecode(e.to_string()))?
|
||||
{
|
||||
if let Event::Key(key) =
|
||||
event::read().map_err(|e| StashError::ListDecode(e.to_string()))?
|
||||
{
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
let i = match state.selected() {
|
||||
Some(i) => {
|
||||
if i >= entries.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
state.select(Some(i));
|
||||
},
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
let i = match state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
entries.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
state.select(Some(i));
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
disable_raw_mode().ok();
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)
|
||||
.ok();
|
||||
terminal.show_cursor().ok();
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
||||
|
||||
use crate::db::StashError;
|
||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||
|
||||
pub trait QueryCommand {
|
||||
fn query_delete(&self, query: &str) -> Result<usize, StashError>;
|
||||
fn query_delete(&self, query: &str) -> Result<usize, StashError>;
|
||||
}
|
||||
|
||||
impl QueryCommand for SqliteClipboardDb {
|
||||
fn query_delete(&self, query: &str) -> Result<usize, StashError> {
|
||||
<SqliteClipboardDb as ClipboardDb>::delete_query(self, query)
|
||||
}
|
||||
fn query_delete(&self, query: &str) -> Result<usize, StashError> {
|
||||
<SqliteClipboardDb as ClipboardDb>::delete_query(self, query)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
||||
|
||||
pub trait StoreCommand {
|
||||
fn store(
|
||||
&self,
|
||||
input: impl Read,
|
||||
max_dedupe_search: u64,
|
||||
max_items: u64,
|
||||
state: Option<String>,
|
||||
) -> Result<(), crate::db::StashError>;
|
||||
fn store(
|
||||
&self,
|
||||
input: impl Read,
|
||||
max_dedupe_search: u64,
|
||||
max_items: u64,
|
||||
state: Option<String>,
|
||||
) -> Result<(), crate::db::StashError>;
|
||||
}
|
||||
|
||||
impl StoreCommand for SqliteClipboardDb {
|
||||
fn store(
|
||||
&self,
|
||||
input: impl Read,
|
||||
max_dedupe_search: u64,
|
||||
max_items: u64,
|
||||
state: Option<String>,
|
||||
) -> Result<(), crate::db::StashError> {
|
||||
if let Some("sensitive" | "clear") = state.as_deref() {
|
||||
self.delete_last()?;
|
||||
log::info!("Entry deleted");
|
||||
} else {
|
||||
self.store_entry(input, max_dedupe_search, max_items)?;
|
||||
log::info!("Entry stored");
|
||||
}
|
||||
Ok(())
|
||||
fn store(
|
||||
&self,
|
||||
input: impl Read,
|
||||
max_dedupe_search: u64,
|
||||
max_items: u64,
|
||||
state: Option<String>,
|
||||
) -> Result<(), crate::db::StashError> {
|
||||
if let Some("sensitive" | "clear") = state.as_deref() {
|
||||
self.delete_last()?;
|
||||
log::info!("Entry deleted");
|
||||
} else {
|
||||
self.store_entry(input, max_dedupe_search, max_items)?;
|
||||
log::info!("Entry stored");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,80 @@
|
|||
use crate::db::{ClipboardDb, Entry, SqliteClipboardDb};
|
||||
use std::{io::Read, time::Duration};
|
||||
|
||||
use smol::Timer;
|
||||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents};
|
||||
|
||||
use crate::db::{ClipboardDb, Entry, SqliteClipboardDb};
|
||||
|
||||
pub trait WatchCommand {
|
||||
fn watch(&self, max_dedupe_search: u64, max_items: u64);
|
||||
fn watch(&self, max_dedupe_search: u64, max_items: u64);
|
||||
}
|
||||
|
||||
impl WatchCommand for SqliteClipboardDb {
|
||||
fn watch(&self, max_dedupe_search: u64, max_items: u64) {
|
||||
smol::block_on(async {
|
||||
log::info!("Starting clipboard watch daemon");
|
||||
fn watch(&self, max_dedupe_search: u64, max_items: u64) {
|
||||
smol::block_on(async {
|
||||
log::info!("Starting clipboard watch daemon");
|
||||
|
||||
// Preallocate buffer for clipboard contents
|
||||
let mut last_contents: Option<Vec<u8>> = None;
|
||||
let mut buf = Vec::with_capacity(4096); // reasonable default, hopefully
|
||||
// Preallocate buffer for clipboard contents
|
||||
let mut last_contents: Option<Vec<u8>> = None;
|
||||
let mut buf = Vec::with_capacity(4096); // reasonable default, hopefully
|
||||
|
||||
// Initialize with current clipboard to avoid duplicating on startup
|
||||
if let Ok((mut reader, _)) = get_contents(
|
||||
ClipboardType::Regular,
|
||||
Seat::Unspecified,
|
||||
wl_clipboard_rs::paste::MimeType::Any,
|
||||
) {
|
||||
buf.clear();
|
||||
if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() {
|
||||
last_contents = Some(buf.clone());
|
||||
}
|
||||
// Initialize with current clipboard to avoid duplicating on startup
|
||||
if let Ok((mut reader, _)) = get_contents(
|
||||
ClipboardType::Regular,
|
||||
Seat::Unspecified,
|
||||
wl_clipboard_rs::paste::MimeType::Any,
|
||||
) {
|
||||
buf.clear();
|
||||
if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() {
|
||||
last_contents = Some(buf.clone());
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match get_contents(
|
||||
ClipboardType::Regular,
|
||||
Seat::Unspecified,
|
||||
wl_clipboard_rs::paste::MimeType::Any,
|
||||
) {
|
||||
Ok((mut reader, mime_type)) => {
|
||||
buf.clear();
|
||||
if let Err(e) = reader.read_to_end(&mut buf) {
|
||||
log::error!("Failed to read clipboard contents: {e}");
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
loop {
|
||||
match get_contents(
|
||||
ClipboardType::Regular,
|
||||
Seat::Unspecified,
|
||||
wl_clipboard_rs::paste::MimeType::Any,
|
||||
) {
|
||||
Ok((mut reader, mime_type)) => {
|
||||
buf.clear();
|
||||
if let Err(e) = reader.read_to_end(&mut buf) {
|
||||
log::error!("Failed to read clipboard contents: {e}");
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
// Only store if changed and not empty
|
||||
if !buf.is_empty() && (last_contents.as_ref() != Some(&buf)) {
|
||||
last_contents = Some(std::mem::take(&mut buf));
|
||||
let mime = Some(mime_type.to_string());
|
||||
let entry = Entry {
|
||||
contents: last_contents.as_ref().unwrap().clone(),
|
||||
mime,
|
||||
};
|
||||
let id = self.next_sequence();
|
||||
match self.store_entry(
|
||||
&entry.contents[..],
|
||||
max_dedupe_search,
|
||||
max_items,
|
||||
) {
|
||||
Ok(_) => log::info!("Stored new clipboard entry (id: {id})"),
|
||||
Err(e) => log::error!("Failed to store clipboard entry: {e}"),
|
||||
}
|
||||
|
||||
// Only store if changed and not empty
|
||||
if !buf.is_empty() && (last_contents.as_ref() != Some(&buf)) {
|
||||
last_contents = Some(std::mem::take(&mut buf));
|
||||
let mime = Some(mime_type.to_string());
|
||||
let entry = Entry {
|
||||
contents: last_contents.as_ref().unwrap().clone(),
|
||||
mime,
|
||||
};
|
||||
let id = self.next_sequence();
|
||||
match self.store_entry(
|
||||
&entry.contents[..],
|
||||
max_dedupe_search,
|
||||
max_items,
|
||||
) {
|
||||
Ok(_) => log::info!("Stored new clipboard entry (id: {id})"),
|
||||
Err(e) => log::error!("Failed to store clipboard entry: {e}"),
|
||||
}
|
||||
|
||||
// Drop clipboard contents after storing
|
||||
last_contents = None;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
if !error_msg.contains("empty") {
|
||||
log::error!("Failed to get clipboard contents: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
// Drop clipboard contents after storing
|
||||
last_contents = None;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
if !error_msg.contains("empty") {
|
||||
log::error!("Failed to get clipboard contents: {e}");
|
||||
}
|
||||
},
|
||||
}
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
||||
|
||||
use crate::db::StashError;
|
||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||
|
||||
pub trait WipeCommand {
|
||||
fn wipe(&self) -> Result<(), StashError>;
|
||||
fn wipe(&self) -> Result<(), StashError>;
|
||||
}
|
||||
|
||||
impl WipeCommand for SqliteClipboardDb {
|
||||
fn wipe(&self) -> Result<(), StashError> {
|
||||
self.wipe_db()?;
|
||||
log::info!("Database wiped");
|
||||
Ok(())
|
||||
}
|
||||
fn wipe(&self) -> Result<(), StashError> {
|
||||
self.wipe_db()?;
|
||||
log::info!("Database wiped");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue