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
13 changes: 13 additions & 0 deletions crates/ruf4/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub enum Action {
ChooseSort,

// App
LastOutput,
Help,
SaveSettings,
Refresh,
Expand Down Expand Up @@ -135,6 +136,10 @@ pub fn default_bindings() -> Vec<Binding> {
key: vk::F10,
action: Action::Quit,
},
Binding {
key: vk::F12,
action: Action::LastOutput,
},
// Ctrl combinations (cross-platform)
Binding {
key: kbmod::CTRL | vk::Q,
Expand Down Expand Up @@ -313,6 +318,8 @@ pub fn key_display_name(key: InputKey) -> String {
k if k == vk::F8 => "F8",
k if k == vk::F9 => "F9",
k if k == vk::F10 => "F10",
k if k == vk::F11 => "F11",
k if k == vk::F12 => "F12",
k if k == vk::UP => "Up",
k if k == vk::DOWN => "Down",
k if k == vk::LEFT => "Left",
Expand Down Expand Up @@ -397,6 +404,7 @@ pub fn action_label(action: Action) -> &'static str {
Action::ChangeRoot => "Change root",
Action::DirHistory => "Directory history",
Action::CmdHistory => "Command history",
Action::LastOutput => "Last output",
}
}

Expand Down Expand Up @@ -440,6 +448,7 @@ pub fn action_str(action: Action) -> &'static str {
Action::CmdHistory => "cmd_history",
Action::FocusMenu => "focus_menu",
Action::Quit => "quit",
Action::LastOutput => "last_output",
}
}

Expand Down Expand Up @@ -481,6 +490,7 @@ pub fn parse_action(s: &str) -> Option<Action> {
"cmd_history" => Action::CmdHistory,
"focus_menu" => Action::FocusMenu,
"quit" => Action::Quit,
"last_output" => Action::LastOutput,
_ => return None,
})
}
Expand Down Expand Up @@ -519,6 +529,8 @@ pub fn parse_key_name(s: &str) -> Option<InputKey> {
"F8" => vk::F8,
"F9" => vk::F9,
"F10" => vk::F10,
"F11" => vk::F11,
"F12" => vk::F12,
"Up" => vk::UP,
"Down" => vk::DOWN,
"Left" => vk::LEFT,
Expand Down Expand Up @@ -582,6 +594,7 @@ pub fn build_help_text(bindings: &[Binding]) -> Vec<(String, &'static str, Actio
Some(Action::Delete),
Some(Action::FocusMenu),
Some(Action::Quit),
Some(Action::LastOutput),
None, // separator
Some(Action::CursorUp),
Some(Action::CursorDown),
Expand Down
2 changes: 1 addition & 1 deletion crates/ruf4/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,7 @@ fn draw_shell_output_dialog(
let w = (size.width - 4).max(20);
let h = (size.height - 4).max(8);

let title = arena_format!(ctx.arena(), "$ {command} - Esc/Enter=Close");
let title = arena_format!(ctx.arena(), "$ {command} - Ctrl+C=Copy Esc/Enter=Close");
ctx.modal_begin("shell-dialog", &title);
ctx.attr_intrinsic_size(Size {
width: w,
Expand Down
1 change: 1 addition & 0 deletions crates/ruf4/src/fileops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ pub fn execute_command(state: &mut State) {

match platform::run_command(&cmd, &cwd) {
Ok((text, _code)) => {
state.last_output = Some((cmd.clone(), text.clone()));
state.dialog = Dialog::ShellOutput {
command: cmd,
output: text,
Expand Down
38 changes: 38 additions & 0 deletions crates/ruf4/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,44 @@ pub fn remove_symlink(path: &Path) -> std::io::Result<()> {
}

/// Recreate a symlink at `dst` pointing to the same target as `src`.
/// Copy text to the system clipboard via OSC 52 escape sequence.
/// This works in most modern terminals (iTerm2, kitty, alacritty, Windows Terminal, etc.).
pub fn copy_to_clipboard(text: &str) {
use ruf4_tui::sys;
use std::io::Write;

let mut buf = Vec::new();
let encoded = base64_encode(text.as_bytes());
write!(buf, "\x1b]52;c;{encoded}\x1b\\").ok();
if let Ok(s) = std::str::from_utf8(&buf) {
sys::write_stdout(s);
}
}

fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
let triple = (b0 << 16) | (b1 << 8) | b2;
result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(CHARS[(triple & 0x3F) as usize] as char);
} else {
result.push('=');
}
}
result
}

pub fn copy_symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
let link_target = fs::read_link(src)?;
#[cfg(unix)]
Expand Down
25 changes: 23 additions & 2 deletions crates/ruf4/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub struct State {
pub bindings: Vec<Binding>,
pub help_text: Vec<(String, &'static str, Action)>,
pub theme: Theme,
pub last_output: Option<(String, String)>, // (command, output)
last_click: Option<(Instant, Point)>,
}

Expand Down Expand Up @@ -181,6 +182,7 @@ impl State {
bindings,
help_text,
theme,
last_output: None,
last_click: None,
};

Expand Down Expand Up @@ -229,6 +231,7 @@ impl State {
bindings,
help_text,
theme,
last_output: None,
last_click: None,
}
}
Expand Down Expand Up @@ -369,6 +372,15 @@ impl State {
Action::ChangeRoot => self.open_choose_root(),
Action::DirHistory => self.open_dir_history(),
Action::CmdHistory => self.open_cmd_history(),
Action::LastOutput => {
if let Some((ref cmd, ref out)) = self.last_output {
self.dialog = Dialog::ShellOutput {
command: cmd.clone(),
output: out.clone(),
scroll: 0,
};
}
}
Action::FocusMenu => self.want_menu_focus = true,
Action::Quit => {
self.dialog = Dialog::ConfirmQuit {
Expand Down Expand Up @@ -1132,12 +1144,16 @@ impl State {
}

fn handle_scrollable_dialog(&mut self, ev: &Input) {
let Dialog::ShellOutput { scroll, .. } = &mut self.dialog else {
let Dialog::ShellOutput { scroll, output, .. } = &mut self.dialog else {
return;
};
match ev {
Input::Keyboard(key) => {
let key = *key;
if key == (kbmod::CTRL | vk::C) {
platform::copy_to_clipboard(output);
return;
}
if key == vk::ESCAPE || key == vk::RETURN || key == vk::SPACE {
*scroll = usize::MAX; // signal dismiss (finalize_dialog cleans up)
} else if key == vk::UP {
Expand Down Expand Up @@ -1315,9 +1331,14 @@ impl State {
impl State {
/// Call after handle_dialog_input to finalize deferred state changes.
pub fn finalize_dialog(&mut self) {
if let Dialog::ShellOutput { scroll, .. } = &self.dialog
if let Dialog::ShellOutput {
scroll,
command,
output,
} = &self.dialog
&& *scroll == usize::MAX
{
self.last_output = Some((command.clone(), output.clone()));
self.dialog = Dialog::None;
}
}
Expand Down
Loading