From 2279004e91a8c68f423c67a522c1dd7819642841 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 22 Oct 2026 16:47:08 +0200 Subject: [PATCH] fix: negotiate modified Return keyboard reporting --- README.md | 2 ++ docs/terebi-url-playback-handoff.md | 3 ++- src/main.rs | 1 + src/ui.rs | 27 ++++++++++++++++++++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51912e8..792c983 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ Two or more checked videos launch `terebi --new-instance --queue-file '/private/ **Ctrl+Return** (`video.enqueue`) sends the same checked rows, or the hovered row when none are checked, to a running Terebi queue without replacing its current playback. The terminal must transmit the modified Return, or you can rebind the action. Enqueue always uses `terebi --enqueue --queue-file '/private/session/queue.json'`, even for one video, preserving independent per-item progress. If Terebi is not running, it starts with that queue. Plain Return still opens a new instance. These shortcuts do nothing on Miruro catalog titles or saved shows and do not submit text inputs. +Tubular requests the Kitty keyboard protocol's escape-code disambiguation for its entire terminal session and restores the previous mode on exit. Supporting terminals can then distinguish Ctrl+Return from plain Return. A terminal or multiplexer that does not support/forward this protocol may still send identical bytes for both; Tubular cannot distinguish them. In that case, configure another key, for example `[keys.video]` with `enqueue = "alt+e"`. + ### Miruro browser Choose **Miruro** at startup or use `--miruro` to start directly. Its interface is **catalog → series → episodes**, with a separate saved-show library organized by overlapping category tags, without YouTube's Search/Feed tabs or channel controls. diff --git a/docs/terebi-url-playback-handoff.md b/docs/terebi-url-playback-handoff.md index 2a01dcc..f99e0d3 100644 --- a/docs/terebi-url-playback-handoff.md +++ b/docs/terebi-url-playback-handoff.md @@ -128,7 +128,8 @@ The complete request is rejected on an identity already in the current queue, a ### Enqueue verification - Tubular: `cargo fmt --all`, `cargo test --locked` (**233 passed**), `cargo build --locked` and built `--help`; Rust launcher diagnostics were clean. -- Actual YouTube and Miruro terminal interfaces: reverse-checked rows produced display-ordered enqueue manifests, cleared checks fell back to the hovered row, Ctrl/Super+Return and an F12 override worked, and plain Return still used `--new-instance --url`. Six handoffs used private `0600` manifests in `0700` session directories with distinct report destinations. The capture launcher did not run Terebi. +- Actual YouTube and Miruro terminal interfaces with injected modified-key sequences: reverse-checked rows produced display-ordered enqueue manifests, cleared checks fell back to the hovered row, Ctrl/Super+Return and an F12 override worked, and plain Return still used `--new-instance --url`. Six handoffs used private `0600` manifests in `0700` session directories with distinct report destinations. The capture launcher did not run Terebi, and injecting already-encoded modified keys did not verify terminal protocol negotiation. - Terebi: `check-cli`, `check-queue`, `check-reporting` and `make all` passed. A regression check first failed on rebinding the original single-video identity and passed after binding it independently of appended reports. - Headless native enqueue smoke with local video-only HLS passed: repeated delivery while paused/playing, unchanged current selection/revision, lazy mixed-provider append, natural-end advance, navigation/revisits, atomic rejection/rollback, isolated original/appended reports and empty-player startup. - Full `make check-resolved` stopped in the pre-existing mixed-queue natural-end scenario at `Could not start Core Audio (560227702)` before reaching enqueue. No full A/V-suite pass is claimed. No packaged-GUI/LaunchServices smoke or visual-layout confirmation was performed; the app was built, not installed. Temporary terminal/server fixtures were removed. +- Modified-Return correction: a protocol-aware PTY driver reproduced Ctrl+Return opening a new instance when startup did not request disambiguation. After adding a session-scoped Kitty keyboard protocol push/pop, the same actual TUI driver observed Ctrl+Return using `--enqueue`, plain Return using `--new-instance`, and keyboard-mode restoration on exit. `cargo test --locked` remained **233 passed** and the build passed. This emulates a supporting terminal; it does not prove support in the user's terminal/multiplexer. Legacy identical Return bytes require a different binding. diff --git a/src/main.rs b/src/main.rs index bb758d5..c8c58f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3854,6 +3854,7 @@ fn main() -> io::Result<()> { let config_path = config::config_path()?; let config = config::load(&config_path)?; ratatui::run(|terminal| { + let _keyboard = ui::KeyboardProtocol::enable()?; if !options.miruro { let Some(miruro) = choose_provider(terminal, &config.keys, None)? else { return Ok(()); diff --git a/src/ui.rs b/src/ui.rs index bbac306..90297a9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2,7 +2,13 @@ use std::time::Duration; use ratatui::{ Frame, - crossterm::{clipboard::CopyToClipboard, execute}, + crossterm::{ + clipboard::CopyToClipboard, + event::{ + KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + }, + execute, + }, layout::{Constraint, Layout, Rect}, style::{Color, Style}, text::{Line, Span}, @@ -13,6 +19,25 @@ pub const ACCENT: Style = Style::new().fg(Color::Cyan); pub const SELECTED: Style = Style::new().fg(Color::Black).bg(Color::Cyan); pub const SELECTION_MARKER: &str = "> "; +// Keep modified Return distinct from Return on terminals supporting the Kitty protocol. +pub struct KeyboardProtocol; + +impl KeyboardProtocol { + pub fn enable() -> std::io::Result { + execute!( + std::io::stdout(), + PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) + )?; + Ok(Self) + } +} + +impl Drop for KeyboardProtocol { + fn drop(&mut self) { + let _ = execute!(std::io::stdout(), PopKeyboardEnhancementFlags); + } +} + pub fn copy_video_url(id: &str) -> String { let result = crate::youtube::video_url(id) .and_then(|url| execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from(url)));