diff --git a/README.md b/README.md index 6e19468..a70ccd5 100644 --- a/README.md +++ b/README.md @@ -318,11 +318,15 @@ background, and its sound joins the audio scenes that hold it. Every page shares browser profile, kept in `$XDG_DATA_HOME/moq-studio/browser` (`~/.local/share/moq-studio/browser` by default) rather than beside the scene file, so cookies and logins, such as an alert service's, survive a reload and -a restart of the studio. One studio at a time can use it: a second one's -browser sources fail saying so. Each page renders in a process of its own, so +a restart of the studio. They are not encrypted with the desktop's keyring, +so a locked keyring never holds a page back, and anything that can read your +home directory can read them. One studio at a time can use the profile: a +second one's browser sources fail saying so. Each page renders in a process of its own, so a page that crashes fails only its own source, with the reason, while the stream carries on. A crash of the browser itself fails every browser source. -Either shows at once, since the browser's processes dump no core. +Either shows at once, since the browser's processes dump no core. On Linux, +pages run without Chromium's sandbox, as in OBS, so a page that exploits a +browser bug can do anything you can: add only pages you trust. **Reload** in Sources restarts a page, failed or not. A page's dropdown lists, such as a `\ + // A text box over the whole page, then a password field and a second + // text box below it. The page turns yellow once the first is + // selected, blue and white once the password is selected and typed + // in, and aqua and lime for the same on the second. This process's id + // tells its copies from whatever the X clipboard held before. + let id = std::process::id(); + let html = format!( + "\ + \ "; - let mut studio = Studio::new(&[("page", page(html))]).await; + oninput=\"document.body.style.background = 'white'\">\ + " + ); + let mut studio = Studio::new(&[("page", page(&html))]).await; studio.until_center([255, 0, 0, 255], &[]).await; let page = studio.sources.get("page").unwrap().page().unwrap().clone(); @@ -397,21 +404,7 @@ async fn copy_and_cut_reach_the_clipboard() { ctrl: true, ..Default::default() }; - let shift = Modifiers { - shift: true, - ..Default::default() - }; - let helper = studio.helper.clone(); - let copied = async || { - let deadline = Instant::now() + TIMEOUT; - loop { - if let Some(copied) = helper.copied() { - return copied; - } - assert!(Instant::now() < deadline, "the copy never came back"); - kio::clock::sleep(Duration::from_millis(30)).await; - } - }; + let mut clipboard = Clipboard::new(studio.helper.clone()); page.send(Input::Focus(true)); for down in [true, false] { @@ -429,7 +422,7 @@ async fn copy_and_cut_reach_the_clipboard() { key(b'A', ctrl); studio.until_center(YELLOW, &[]).await; page.send(Input::Copy); - assert_eq!(copied().await, "hello world"); + assert_eq!(clipboard.next().await, format!("copy {id}")); // Text typed after the copy shows only once the copy has come back. key(b'\t', Modifiers::default()); @@ -438,18 +431,111 @@ async fn copy_and_cut_reach_the_clipboard() { page.send(Input::Copy); page.send(Input::Text("x".into())); studio.until_center([255, 255, 255, 255], &[]).await; - assert_eq!(helper.copied(), None, "a password was copied"); + assert_eq!(clipboard.take(), None, "a password was copied"); - key(b'\t', shift); + key(b'\t', Modifiers::default()); key(b'A', ctrl); - studio.until_center(YELLOW, &[]).await; + studio.until_center([0, 255, 255, 255], &[]).await; page.send(Input::Cut); studio.until_center(GREEN, &[]).await; - assert_eq!(copied().await, "hello world"); + assert_eq!(clipboard.next().await, format!("cut {id}")); }) .await; } +/// Where a page's copies land: the X clipboard, where CEF copies itself, or +/// else the studio's, through the helper. +enum Clipboard { + Relay(Helper), + /// With the text it held when last read. + X11(Option), +} + +impl Clipboard { + fn new(helper: Helper) -> Self { + match helper.relays_copies() { + true => Self::Relay(helper), + false => Self::X11(x11_clipboard()), + } + } + + /// What a page copied since this was last asked, if anything. + fn take(&mut self) -> Option { + match self { + Self::Relay(helper) => helper.copied(), + Self::X11(held) => { + let text = x11_clipboard(); + if text == *held { + return None; + } + held.clone_from(&text); + text + } + } + } + + /// Wait for a page's next copy. + async fn next(&mut self) -> String { + let deadline = Instant::now() + TIMEOUT; + loop { + if let Some(text) = self.take() { + return text; + } + assert!(Instant::now() < deadline, "the copy never landed"); + kio::clock::sleep(Duration::from_millis(30)).await; + } + } +} + +/// The X clipboard's text, as a paste takes it: none with no owner, or one +/// offering no text. +fn x11_clipboard() -> Option { + use x11rb::connection::Connection; + use x11rb::protocol::Event; + use x11rb::protocol::xproto::{AtomEnum, ConnectionExt, CreateWindowAux, WindowClass}; + + let (connection, screen) = x11rb::connect(None).expect("the X display"); + let root = connection.setup().roots[screen].root; + let window = connection.generate_id().unwrap(); + connection + .create_window( + 0, + window, + root, + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_ONLY, + 0, + &CreateWindowAux::new(), + ) + .unwrap(); + let atom = |name: &[u8]| connection.intern_atom(false, name).unwrap().reply().unwrap().atom; + let (selection, text, property) = (atom(b"CLIPBOARD"), atom(b"UTF8_STRING"), atom(b"MOQ_STUDIO_PASTE")); + connection + .convert_selection(window, selection, text, property, x11rb::CURRENT_TIME) + .unwrap(); + connection.flush().unwrap(); + loop { + if let Event::SelectionNotify(notify) = connection.wait_for_event().unwrap() + && notify.requestor == window + { + if notify.property == x11rb::NONE { + return None; + } + let value = connection + .get_property(true, window, property, AtomEnum::ANY, 0, u32::MAX) + .unwrap() + .reply() + .unwrap() + .value; + return Some(String::from_utf8(value).unwrap()); + } + } +} + /// A page that closes itself fails its own source and leaves the canvas, /// while the page beside it in the same helper keeps drawing. #[tokio::test(flavor = "multi_thread")]