#![allow(missing_docs, clippy::pedantic)] // The Zellij harness is cross-platform, but this file stays unix-only for // reasons unrelated to the multiplexer: it builds an executable bash helper via // `std::os::unix::fs::PermissionsExt` and drives it with a POSIX shell script. // (`#![cfg(unix)]` already covers both Linux and macOS.) #![cfg(unix)] #[allow(dead_code)] mod common; use std::fs::{self, File, Permissions}; use std::io::{Read, Result, Write}; use std::os::unix::fs::PermissionsExt; use std::path::Path; use common::zellij::Keys::*; use common::zellij::{ZellijController, wait}; /// Read the whole file at `path` into a `String`. fn read_file(path: &Path) -> Result { let mut s = String::new(); File::open(path)?.read_to_string(&mut s)?; Ok(s) } /// Drive an interactive child through an `execute` action and assert it keeps /// receiving keystrokes, then that skim itself is interactive again once the /// child exits. /// /// Two independent bugs used to make a program run via `execute(...)` freeze: /// 1. skim's own input reader kept reading the terminal while the child ran, /// so skim and the child raced for keystrokes and roughly half were lost. /// 2. the child inherited skim's stdin, which is the item *pipe* here /// (`printf … | sk`), so an interactive child had no keyboard at all. /// /// The child below reads four raw single keystrokes and records them, in order. /// It only completes if it received every keystroke, so if either bug regresses /// the result file is never finished and the wait times out. Typing a query /// afterwards confirms skim restarted its reader and repainted — the latter /// exercised for both the fullscreen and inline layouts, since the post-execute /// repaint path differs from a normal render. fn run_interactive_execute(name: &str, extra_opts: &[&str]) -> Result<()> { let mut tmux = ZellijController::new_named(name)?; let dir = tmux.tempdir.path().to_path_buf(); let script = dir.join("interactive.sh"); let ready = dir.join("ready"); let result = dir.join("keys.txt"); // A tiny interactive "TUI": announce readiness, then read four raw single // keystrokes from the terminal and write them, in order, to the result // file. `read -rsn1` requires bash, which is present on the Linux and macOS // CI runners. If a keystroke is stolen, the loop blocks on `read` and the // result file is never written. let script_body = r#"#!/usr/bin/env bash out="$1" ready="$2" : > "$ready" s="" for _ in 1 2 3 4; do IFS= read -rsn1 c || break s="$s$c" done printf '%s' "$s" > "$out" "#; File::create(&script)?.write_all(script_body.as_bytes())?; fs::set_permissions(&script, Permissions::from_mode(0o755))?; let bind = format!( "--bind='enter:execute(bash {} {} {})'", script.display(), result.display(), ready.display() ); let mut opts: Vec<&str> = extra_opts.to_vec(); opts.push(bind.as_str()); // skim reads its items from a *pipe*; Enter runs the interactive child. tmux.start_sk(Some("printf 'aaa\\nbbb\\nccc'"), &opts)?; // Wait for skim to come up (prompt line present). tmux.until(|l| l.iter().any(|s| s.trim_start().starts_with(">")))?; // Trigger the execute action and wait until the child has taken over the // terminal. Waiting for the readiness marker guarantees skim has already // suspended its own reader, so there is no race for the keystrokes below. tmux.send_keys(&[Enter])?; wait(|| { if ready.exists() { Ok(()) } else { Err(std::io::Error::other("child not ready yet")) } })?; // Feed the child four distinct keystrokes. tmux.send_keys(&[Key('w'), Key('x'), Key('y'), Key('z')])?; // The child finishes only if it received every keystroke. wait(|| match read_file(&result) { Ok(s) if s == "wxyz" => Ok(()), _ => Err(std::io::Error::other("keys not complete yet")), }) .map_err(|_| { std::io::Error::other(format!( "child did not receive all keystrokes; result file = {:?}", read_file(&result).ok() )) })?; // The child has exited: skim should have restarted its reader and repainted // (skim's stdout is redirected to a file by `start_sk`, which used to stall // the post-execute repaint). Typing a query must filter the piped items. tmux.send_keys(&[Str("aaa")])?; tmux.until(|l| l.iter().any(|s| s.contains("1/3")))?; Ok(()) } #[test] fn execute_interactive_child_keeps_receiving_keys_fullscreen() -> Result<()> { run_interactive_execute("execute_interactive_fs", &[]) } #[test] fn execute_interactive_child_keeps_receiving_keys_inline() -> Result<()> { run_interactive_execute("execute_interactive_inline", &["--height=40%"]) }