use std::fs;
use tempfile::TempDir;
use super::super::common::*;
fn write_toml_config(
cable_dir: &std::path::Path,
filename: &str,
content: &str,
) {
let toml_path = cable_dir.join(filename);
fs::write(&toml_path, content).unwrap();
}
const FILES_TOML_WITH_ACTIONS: &str = r#"
[metadata]
name = "files"
description = "A channel to select files and directories"
requirements = ["fd", "bat"]
[source]
command = ["fd -t f", "fd -t f -H"]
[preview]
command = "bat -n --color=always '{}'"
env = { BAT_THEME = "ansi" }
[keybindings]
shortcut = "f1"
f8 = "actions:thebatman"
f9 = "actions:lsman"
[actions.thebatman]
description = "show file content"
command = "cat '{}'"
mode = "execute"
[actions.lsman]
description = "show stats"
command = "ls '{}'"
mode = "execute"
"#;
#[test]
fn test_external_action_lsman_with_f9() {
let pt = phantom();
let tempdir = TempDir::new().unwrap();
let cable_dir = tempdir.path().join("custom_cable");
fs::create_dir_all(&cable_dir).unwrap();
write_toml_config(&cable_dir, "files.toml", FILES_TOML_WITH_ACTIONS);
let s = tv_with_args(
&pt,
&[
"--cable-dir",
cable_dir.to_str().unwrap(),
"--config-file",
DEFAULT_CONFIG_FILE,
"files",
"--input",
"LICENSE",
],
)
.start()
.unwrap();
s.wait()
.text("1 / 1")
.text("LICENSE")
.timeout_ms(wait_timeout_ms())
.until()
.unwrap();
s.send().key("f9").unwrap();
let output = exit_and_output(&s);
assert!(
output.contains("LICENSE"),
"expected ls output to contain 'LICENSE', got:\n{output}"
);
}
#[test]
fn test_external_action_thebatman_with_f8() {
let pt = phantom();
let tempdir = TempDir::new().unwrap();
let cable_dir = tempdir.path().join("custom_cable_f8");
fs::create_dir_all(&cable_dir).unwrap();
write_toml_config(&cable_dir, "files.toml", FILES_TOML_WITH_ACTIONS);
let s = tv_with_args(
&pt,
&[
"--cable-dir",
cable_dir.to_str().unwrap(),
"--config-file",
DEFAULT_CONFIG_FILE,
"files",
"--input",
"LICENSE",
],
)
.start()
.unwrap();
s.wait()
.text("1 / 1")
.text("LICENSE")
.timeout_ms(wait_timeout_ms())
.until()
.unwrap();
s.send().key("f8").unwrap();
let output = exit_and_output(&s);
assert!(
output.contains("Copyright (c)"),
"expected cat output to contain 'Copyright (c)', got:\n{output}"
);
}
#[test]
fn test_execute_action_uses_tty_when_stdout_is_captured() {
let pt = phantom();
let tempdir = TempDir::new().unwrap();
let cable_dir = tempdir.path().join("captured_stdout");
fs::create_dir_all(&cable_dir).unwrap();
let files_toml_content = r#"
[metadata]
name = "files"
description = "A channel to select files and directories"
requirements = ["fd", "bat"]
[source]
command = ["fd -t f", "fd -t f -H"]
[preview]
command = "bat -n --color=always '{}'"
env = { BAT_THEME = "ansi" }
[keybindings]
shortcut = "f1"
f12 = "actions:ttycheck"
[actions.ttycheck]
description = "verify execute actions use the terminal tty"
command = "if test -t 1; then printf 'TTY_OK\\n'; else printf 'TTY_BAD\\n'; fi"
shell = "bash"
mode = "execute"
"#;
write_toml_config(&cable_dir, "files.toml", files_toml_content);
let script = format!(
"out=$('{}' --cable-dir '{}' --config-file '{}' files --input LICENSE); printf '\\nSHELL_CAPTURE=[%s]\\n' \"$out\"",
TV_BIN_PATH,
cable_dir.display(),
DEFAULT_CONFIG_FILE,
);
let cwd = std::env::current_dir().expect("failed to get cwd");
let s = pt
.run("bash")
.args(&["-lc", &script])
.size(DEFAULT_COLS, DEFAULT_ROWS)
.cwd(cwd.to_str().expect("cwd is not valid utf-8"))
.start()
.unwrap();
s.wait()
.text("1 / 1")
.text("LICENSE")
.timeout_ms(wait_timeout_ms())
.until()
.unwrap();
s.send().key("f12").unwrap();
s.wait()
.exit_code(0)
.timeout_ms(wait_timeout_ms())
.until()
.unwrap();
let scrollback = s.scrollback(None).unwrap();
let post_exit = s.output().unwrap();
let combined = format!("{scrollback}\n{post_exit}");
assert!(
combined.contains("TTY_OK"),
"expected action output to reach the terminal tty, got:\n{combined}"
);
assert!(
combined.contains("SHELL_CAPTURE=[]"),
"expected shell-captured stdout to stay empty, got:\n{combined}"
);
assert!(
!combined.contains("TTY_BAD"),
"expected action stdout to be reattached to the tty, got:\n{combined}"
);
}
#[test]
fn test_fork_action_uses_tty_when_stdout_is_captured() {
let pt = phantom();
let tempdir = TempDir::new().unwrap();
let cable_dir = tempdir.path().join("captured_stdout_fork");
let status_file = cable_dir.join("fork-status");
fs::create_dir_all(&cable_dir).unwrap();
let files_toml_content = r#"
[metadata]
name = "files"
description = "A channel to select files and directories"
requirements = ["fd", "bat"]
[source]
command = ["fd -t f", "fd -t f -H"]
[preview]
command = "bat -n --color=always '{}'"
env = { BAT_THEME = "ansi" }
[keybindings]
shortcut = "f1"
f12 = "actions:ttycheck"
[actions.ttycheck]
description = "verify fork actions use the terminal tty"
command = "if test -t 1; then printf 'ok' > '{status_file}'; else printf 'bad' > '{status_file}'; fi; printf 'FORK_STDOUT\\n'"
shell = "bash"
mode = "fork"
"#
.replace("{status_file}", &status_file.display().to_string());
write_toml_config(&cable_dir, "files.toml", &files_toml_content);
let script = format!(
"out=$('{}' --cable-dir '{}' --config-file '{}' files --input LICENSE); printf '\\nSHELL_CAPTURE=[%s]\\n' \"$out\"",
TV_BIN_PATH,
cable_dir.display(),
DEFAULT_CONFIG_FILE,
);
let cwd = std::env::current_dir().expect("failed to get cwd");
let s = pt
.run("bash")
.args(&["-lc", &script])
.size(DEFAULT_COLS, DEFAULT_ROWS)
.cwd(cwd.to_str().expect("cwd is not valid utf-8"))
.start()
.unwrap();
s.wait()
.text("1 / 1")
.text("LICENSE")
.timeout_ms(wait_timeout_ms())
.until()
.unwrap();
s.send().key("f12").unwrap();
let deadline = std::time::Instant::now()
+ std::time::Duration::from_millis(wait_timeout_ms());
while !status_file.exists() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(50));
}
s.send().key("ctrl-c").unwrap();
s.wait()
.exit_code(0)
.timeout_ms(wait_timeout_ms())
.until()
.unwrap();
let scrollback = s.scrollback(None).unwrap();
let post_exit = s.output().unwrap();
let combined = format!("{scrollback}\n{post_exit}");
let status = fs::read_to_string(&status_file).unwrap_or_default();
assert_eq!(
status, "ok",
"expected fork action stdout to be attached to a tty, got status {status:?}"
);
assert!(
combined.contains("SHELL_CAPTURE=[]"),
"expected shell-captured stdout to stay empty, got:\n{combined}"
);
assert!(
!combined.contains("FORK_STDOUT"),
"expected fork action stdout to bypass shell capture, got:\n{combined}"
);
}