Skip to content
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ Commands:
add Add a new block to the scratchpad
list List all of the blocks within your thoth scratchpad
load_backup Load backup file as the main thoth markdown file
read_clipboard Read the contents of the clipboard backup file
delete Delete a block by name
view View (STDOUT) the contents of the block by name
copy Copy the contents of a block to the system clipboard
Expand All @@ -195,6 +196,22 @@ echo "Hello, World (from STDIN)" | thoth add hello_world_stdin;
thoth view hello_world_stdin | cat
```

### Clipboard Fallback for Wayland Users

When using Thoth in Wayland environments or over SSH, the system clipboard functionality may not be available. In these cases, when you use Ctrl+Y to copy content, Thoth will:

1. Save the content to a backup file in your home directory
2. Display a message with the location of the backup file
3. Provide instructions to access the content

You can retrieve the content using:

```bash
thoth read_clipboard
```

This ensures your content is always accessible, even when the system clipboard is unavailable.

## Contributions
Contributions are always welcomed :) !!! Please take a look at this [doc](https://github.com/jooaf/thoth/blob/main/CONTRIBUTING.md) for more information.

Expand Down
18 changes: 18 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub enum Commands {
List,
/// Load backup file as the main thoth markdown file
LoadBackup,
/// Read the contents of the clipboard backup file
ReadClipboard,
/// Delete a block by name
Delete {
/// The name of the block to be deleted
Expand All @@ -48,6 +50,22 @@ pub enum Commands {
},
}

pub fn read_clipboard_backup() -> Result<()> {
let file_path = crate::get_clipboard_backup_file_path();
if !file_path.exists() {
println!("No clipboard backup file found at {}", file_path.display());
return Ok(());
}

let content = std::fs::read_to_string(&file_path)?;
if content.is_empty() {
println!("Clipboard backup file exists but is empty.");
} else {
println!("{}", content);
}
Ok(())
}

pub fn add_block(name: &str, content: &str) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.append(true)
Expand Down
36 changes: 30 additions & 6 deletions src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ use arboard::SetExtLinux;

use crate::DAEMONIZE_ARG;

pub trait ClipboardTrait {
fn set_contents(&mut self, content: String) -> anyhow::Result<()>;
fn get_content(&self) -> anyhow::Result<String>;
}

pub struct EditorClipboard {
clipboard: Arc<Mutex<Clipboard>>,
}
Expand All @@ -27,13 +32,30 @@ impl EditorClipboard {
pub fn set_contents(&mut self, content: String) -> Result<(), Error> {
#[cfg(target_os = "linux")]
{
if env::args().nth(1).as_deref() == Some(DAEMONIZE_ARG) {
let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok()
|| std::env::var("XDG_SESSION_TYPE")
.map(|v| v == "wayland")
.unwrap_or(false);

if is_wayland {
let mut clipboard = self
.clipboard
.lock()
.map_err(|_e| arboard::Error::ContentNotAvailable)?;

let result = clipboard.set().wait().text(content);
result
} else if env::args().nth(1).as_deref() == Some(DAEMONIZE_ARG) {
let mut clipboard = self
.clipboard
.lock()
.map_err(|_e| arboard::Error::ContentNotAvailable)?;
clipboard.set().wait().text(content)?;
clipboard.set().wait().text(content)
} else {
if std::env::var("THOTH_DEBUG_CLIPBOARD").is_ok() {
return Err(arboard::Error::ContentNotAvailable);
}

process::Command::new(env::current_exe().unwrap())
.arg(DAEMONIZE_ARG)
.arg(content)
Expand All @@ -43,16 +65,18 @@ impl EditorClipboard {
.current_dir("/")
.spawn()
.map_err(|_e| arboard::Error::ContentNotAvailable)?;
Ok(())
}
}

#[cfg(not(target_os = "linux"))]
{
let mut clipboard = self.clipboard.lock().unwrap();
clipboard.set_text(content)?;
let mut clipboard = self
.clipboard
.lock()
.map_err(|_e| arboard::Error::ContentNotAvailable)?;
clipboard.set_text(content)
}

Ok(())
}

pub fn get_content(&mut self) -> Result<String, Error> {
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod ui;
pub mod ui_handler;
pub mod utils;

pub use clipboard::ClipboardTrait;
pub use clipboard::EditorClipboard;
use dirs::home_dir;
pub use formatter::{format_json, format_markdown};
Expand All @@ -25,6 +26,9 @@ pub fn get_save_file_path() -> PathBuf {
pub fn get_save_backup_file_path() -> PathBuf {
home_dir().unwrap_or_default().join("thoth_notes_backup.md")
}
pub fn get_clipboard_backup_file_path() -> PathBuf {
home_dir().unwrap_or_default().join("thoth_clipboard.txt")
}

pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 165, 0);
pub const DAEMONIZE_ARG: &str = "__thoth_copy_daemonize";
Expand Down
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use std::{
thread,
};
use thoth_cli::{
cli::{add_block, copy_block, delete_block, list_blocks, replace_from_backup, view_block},
cli::{
add_block, copy_block, delete_block, list_blocks, read_clipboard_backup,
replace_from_backup, view_block,
},
get_save_backup_file_path, EditorClipboard,
};
use thoth_cli::{
Expand Down Expand Up @@ -48,6 +51,9 @@ fn main() -> Result<()> {
Some(Commands::List) => {
list_blocks()?;
}
Some(Commands::ReadClipboard) => {
read_clipboard_backup()?;
}
Some(Commands::LoadBackup) => {
replace_from_backup()?;
}
Expand Down
58 changes: 56 additions & 2 deletions src/scrollable_textarea.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,61 @@ impl ScrollableTextArea {
}

pub fn copy_focused_textarea_contents(&self) -> anyhow::Result<()> {
use std::fs::File;
use std::io::Write;

if let Some(textarea) = self.textareas.get(self.focused_index) {
let content = textarea.lines().join("\n");
let mut ctx = EditorClipboard::new().unwrap();
ctx.set_contents(content).unwrap();

// Force clipboard failure if env var is set (for testing)
if std::env::var("THOTH_TEST_CLIPBOARD_FAIL").is_ok() {
let backup_path = crate::get_clipboard_backup_file_path();
let mut file = File::create(&backup_path)?;
file.write_all(content.as_bytes())?;

return Err(anyhow::anyhow!(
"TESTING: Simulated clipboard failure.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.",
backup_path.display()
));
}

match EditorClipboard::new() {
Ok(mut ctx) => {
if let Err(e) = ctx.set_contents(content.clone()) {
let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok()
|| std::env::var("XDG_SESSION_TYPE")
.map(|v| v == "wayland")
.unwrap_or(false);

let backup_path = crate::get_clipboard_backup_file_path();
let mut file = File::create(&backup_path)?;
file.write_all(content.as_bytes())?;

if is_wayland {
return Err(anyhow::anyhow!(
"Wayland clipboard error.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.",
backup_path.display()
));
} else {
return Err(anyhow::anyhow!(
"Clipboard error: {}.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.",
e.to_string().split('\n').next().unwrap_or("Unknown error"),
backup_path.display()
));
}
}
}
Err(_) => {
let backup_path = crate::get_clipboard_backup_file_path();
let mut file = File::create(&backup_path)?;
file.write_all(content.as_bytes())?;

return Err(anyhow::anyhow!(
"Clipboard unavailable.\nContent saved to: {}\nPlease use 'thoth read_clipboard' to read the contents from STDOUT.",
backup_path.display()
));
}
}
}
Ok(())
}
Expand Down Expand Up @@ -419,6 +470,9 @@ impl ScrollableTextArea {
f.render_widget(paragraph, area);
Ok(())
}
pub fn test_get_clipboard_content(&self) -> String {
self.textareas[self.focused_index].lines().join("\n")
}
}

#[cfg(test)]
Expand Down
4 changes: 3 additions & 1 deletion src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ pub fn render_ui_popup(f: &mut Frame, popup: &UiPopup) {
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Red))
.title(format!("{} - Esc to exit", popup.popup_title)),
);
)
.wrap(ratatui::widgets::Wrap { trim: true }); // Enable text wrapping

f.render_widget(text, area);
}

Expand Down
8 changes: 2 additions & 6 deletions src/ui_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ fn handle_full_screen_input(state: &mut UIState, key: event::KeyEvent) -> Result
}
}
Err(e) => {
state
.error_popup
.show(format!("Failed to copy to system clipboard: {}", e));
state.error_popup.show(format!("{}", e));
}
}
}
Expand Down Expand Up @@ -349,9 +347,7 @@ fn handle_normal_input(
}
}
Err(e) => {
state
.error_popup
.show(format!("Failed to copy to system clipboard: {}", e));
state.error_popup.show(format!("{}", e));
}
}
}
Expand Down
Loading
Loading