终端:程序开了同步更新却没结束时,到点照样画出来,不再冻住屏幕
- 程序用 \e[?2026h 开始一帧、却没发 \e[?2026l 结束(在一帧中间被杀掉、 崩溃,连接断在一帧中间,或 cat 了含这段字节的文件)时,后面的输出全被 攒着不画:屏幕冻住、敲字没回显,要等攒够 2 MiB 或下一个程序再结束一次 同步更新才恢复。 - 解析线程等输出时带着同步更新的截止时间(150 ms,同 alacritty),到点 就把攒下的画出来;输出一直不停时在处理前补查一次。 - 进程退出、连接出错时先结束同步更新,再追加「进程已退出」「终端错误」 的提示,断在一帧中间也看得到那一帧和断开的原因。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f588a4a18
commit
7c9501e4cb
4 files changed
+223
-10
No files matched your search
@@ -259,6 +259,7 @@ cargo test -- --ignored # 会读写真实钥匙串的测试
|
||||
- 通知在解析线程上:按 `\n` 切开输出,在 LF **之前**读光标所在的逻辑行(这时 `\r` 和颜色都已生效),锁外匹配。备用屏和同步更新进行中不读。
|
||||
- 规则由 `settings::apply` 编译成全局量 `TerminalHighlights`,引擎 `observe_global` 后换进与解析线程共用的槽;重启运行时也要把槽传过去。
|
||||
- `settings.json` 里读不了的规则单条丢掉(`readable_rules`),否则整个设置文件会回退成默认值。
|
||||
- **同步更新(DEC 2026)要解析线程自己收尾**:vte 只管攒住 `\e[?2026h` 之后的输出,超时(150 ms)后调 `stop_sync` 是调用方的事,alacritty 自己的事件循环就是这么做的。解析线程等输出时带着截止时间(`receive`,用 `async_io` 的定时器),到点就画出来;输出一直不停时在 `advance` 前补查;退出、出错追加提示前也先 `stop_sync`。否则程序死在一帧中间、连接断在一帧中间时,屏幕会一直冻住。
|
||||
- **OSC 52 只写不读**(`Osc52::OnlyCopy`),只认剪贴板 `c`,主选择区忽略。
|
||||
- **通知**:OSC 9 / 777 由 `terminal/notices.rs` 的扫描器在解析线程上认(alacritty 会丢掉),OSC 9 里「数字;」开头的是 ConEmu 的命令,不算通知。投递在 `workspace/notices.rs`:窗口不在前台走 GPUI 的 `show_system_notification`(tag 是 `terminal:local:<id>` / `terminal:remote:<id>`,点击回调按 tag 切标签),在前台走应用内通知。`shellrs::init` 里的 `set_app_identity` 用 bundle id,测试平台没有它就不发系统通知;macOS 上 `cargo run` 没有 bundle,看不到系统通知。
|
||||
|
||||
|
||||
Generated
+1
@@ -7707,6 +7707,7 @@ dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
|
||||
@@ -44,6 +44,9 @@ gpui-kit = { version = "0.6.1", features = [
|
||||
alacritty_terminal = "0.26"
|
||||
anyhow = "1"
|
||||
async-channel = "2"
|
||||
# A terminal's parser thread waits for output only until a synchronized
|
||||
# update has to be drawn.
|
||||
async-io = "2"
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
dirs = "6"
|
||||
|
||||
+218
-10
@@ -14,7 +14,10 @@ use alacritty_terminal::term::cell::Flags;
|
||||
use alacritty_terminal::term::color::COUNT;
|
||||
use alacritty_terminal::term::search::RegexSearch;
|
||||
use alacritty_terminal::term::{ClipboardType, Config, Osc52, TermMode};
|
||||
use alacritty_terminal::vte::ansi::{Color, CursorShape, CursorStyle, Processor, Rgb};
|
||||
use alacritty_terminal::vte::ansi::{
|
||||
Color, CursorShape, CursorStyle, Processor, Rgb, Timeout as _,
|
||||
};
|
||||
use futures::future::{Either, select};
|
||||
|
||||
use crate::connection::{ConnectionPrompt, ConnectionPromptReply, Latency};
|
||||
use crate::host::HostOs;
|
||||
@@ -654,11 +657,26 @@ impl TerminalRuntime {
|
||||
thread::Builder::new()
|
||||
.name("shellrs-terminal-parser".into())
|
||||
.spawn(move || {
|
||||
let mut processor = Processor::new();
|
||||
let mut processor: Processor = Processor::new();
|
||||
// Alacritty drops the notifications programs ask for.
|
||||
let mut notices = NoticeScanner::default();
|
||||
let mut watcher = LineWatcher::new(current(&highlights));
|
||||
while let Ok(event) = transport_receiver.recv_blocking() {
|
||||
loop {
|
||||
// Output inside a synchronized update is held back until
|
||||
// the program ends it. Alacritty's own event loop draws
|
||||
// it anyway once the update's time is up, so a program
|
||||
// that never ends it (killed mid-frame, or its connection
|
||||
// lost) does not freeze the screen; this loop has to too.
|
||||
let deadline = processor.sync_timeout().sync_timeout();
|
||||
let event = match receive(&transport_receiver, deadline) {
|
||||
Received::Event(event) => event,
|
||||
Received::Closed => break,
|
||||
Received::Overdue => {
|
||||
processor.stop_sync(&mut *parser_term.lock());
|
||||
parser_proxy.wakeup();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match event {
|
||||
TerminalTransportEvent::Started => {
|
||||
parser_proxy.send(TerminalUiEventKind::Started);
|
||||
@@ -667,12 +685,18 @@ impl TerminalRuntime {
|
||||
watcher.sync(current(&highlights));
|
||||
// The lines are only read under the lock; the
|
||||
// rules match them once it is let go.
|
||||
let lines = advance_watching(
|
||||
&mut processor,
|
||||
&mut parser_term.lock(),
|
||||
&bytes,
|
||||
watcher.is_watching(),
|
||||
);
|
||||
let lines = {
|
||||
let mut term = parser_term.lock();
|
||||
// Output that never pauses gives the wait no
|
||||
// chance to see the time is up.
|
||||
end_overdue_sync(&mut processor, &mut term);
|
||||
advance_watching(
|
||||
&mut processor,
|
||||
&mut term,
|
||||
&bytes,
|
||||
watcher.is_watching(),
|
||||
)
|
||||
};
|
||||
if let Some(hit) = watcher.check(lines, Instant::now()) {
|
||||
parser_proxy.send(TerminalUiEventKind::Keyword(hit));
|
||||
}
|
||||
@@ -895,7 +919,55 @@ fn append_message_with_processor(
|
||||
message: &str,
|
||||
) {
|
||||
let line = format!("\r\n\x1b[2m[{message}]\x1b[0m\r\n");
|
||||
processor.advance(&mut *term.lock(), line.as_bytes());
|
||||
let mut term = term.lock();
|
||||
// A program that ended mid-frame left its synchronized update open:
|
||||
// neither that frame nor this message would show.
|
||||
if processor.sync_timeout().pending_timeout() {
|
||||
processor.stop_sync(&mut *term);
|
||||
}
|
||||
processor.advance(&mut *term, line.as_bytes());
|
||||
}
|
||||
|
||||
/// What waiting for the transport came to.
|
||||
enum Received<T> {
|
||||
Event(T),
|
||||
/// The open synchronized update's time is up before anything came.
|
||||
Overdue,
|
||||
/// The transport is gone.
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// The transport's next event; with a synchronized update open, waiting
|
||||
/// only until its `deadline`. `recv_blocking` cannot stop at a time, hence
|
||||
/// the timer.
|
||||
fn receive<T>(events: &async_channel::Receiver<T>, deadline: Option<Instant>) -> Received<T> {
|
||||
let Some(deadline) = deadline else {
|
||||
return events
|
||||
.recv_blocking()
|
||||
.map_or(Received::Closed, Received::Event);
|
||||
};
|
||||
async_io::block_on(async {
|
||||
let next = std::pin::pin!(events.recv());
|
||||
// What has come already wins over a deadline that has passed.
|
||||
match select(next, async_io::Timer::at(deadline)).await {
|
||||
Either::Left((Ok(event), _)) => Received::Event(event),
|
||||
Either::Left((Err(_), _)) => Received::Closed,
|
||||
Either::Right(_) => Received::Overdue,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw what an open synchronized update has held back, if its time is up.
|
||||
/// Says whether it was.
|
||||
fn end_overdue_sync(processor: &mut Processor, term: &mut AlacrittyTerm) -> bool {
|
||||
let overdue = processor
|
||||
.sync_timeout()
|
||||
.sync_timeout()
|
||||
.is_some_and(|deadline| deadline <= Instant::now());
|
||||
if overdue {
|
||||
processor.stop_sync(term);
|
||||
}
|
||||
overdue
|
||||
}
|
||||
|
||||
fn sanitize_title(title: &str) -> Option<String> {
|
||||
@@ -1521,4 +1593,140 @@ mod tests {
|
||||
// vim and less have the keys to themselves.
|
||||
assert!(encode_command("ls", true, true, TermMode::ALT_SCREEN).is_err());
|
||||
}
|
||||
|
||||
/// Sends its events, then stays quiet until shut down, as a shell does
|
||||
/// when nothing is typed.
|
||||
struct ScriptedTransport(Vec<TerminalTransportEvent>);
|
||||
|
||||
impl super::super::TerminalTransport for ScriptedTransport {
|
||||
fn run(
|
||||
self: Box<Self>,
|
||||
_: TerminalSize,
|
||||
commands: mpsc::Receiver<TerminalTransportCommand>,
|
||||
events: async_channel::Sender<TerminalTransportEvent>,
|
||||
) -> anyhow::Result<()> {
|
||||
for event in self.0 {
|
||||
events.send_blocking(event)?;
|
||||
}
|
||||
while let Ok(command) = commands.recv() {
|
||||
if matches!(command, TerminalTransportCommand::Shutdown) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A terminal whose transport plays `script`.
|
||||
fn run_script(script: Vec<TerminalTransportEvent>) -> TerminalRuntime {
|
||||
let (ui_events, _) = mpsc::channel();
|
||||
TerminalRuntime::start(
|
||||
1,
|
||||
TerminalSize::new(40, 4, 8, 16),
|
||||
Box::new(ScriptedTransport(script)),
|
||||
ui_events,
|
||||
Arc::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The screen's text once it holds `text`, or what it holds after two
|
||||
/// seconds of waiting.
|
||||
fn screen_once_it_shows(runtime: &TerminalRuntime, text: &str) -> String {
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
let term = runtime.term.lock();
|
||||
let screen = (0..4)
|
||||
.map(|line| {
|
||||
term.grid()[Line(line)]
|
||||
.into_iter()
|
||||
.filter(|cell| !cell.flags.contains(Flags::WIDE_CHAR_SPACER))
|
||||
.map(|cell| cell.c)
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
drop(term);
|
||||
if screen.contains(text) || started.elapsed() > Duration::from_secs(2) {
|
||||
return screen;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_update_left_open_is_drawn_once_its_time_is_up() {
|
||||
// `printf '\e[?2026h'`, or a program killed between beginning a
|
||||
// frame and ending it: nothing ends the update.
|
||||
let started = Instant::now();
|
||||
let runtime = run_script(vec![
|
||||
TerminalTransportEvent::Started,
|
||||
TerminalTransportEvent::Output(b"before \x1b[?2026hframe".to_vec()),
|
||||
]);
|
||||
assert!(screen_once_it_shows(&runtime, "before frame").contains("before frame"));
|
||||
// Held back meanwhile, as the update asked.
|
||||
assert!(started.elapsed() >= Duration::from_millis(150));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_update_ended_in_time_is_drawn_whole() {
|
||||
let runtime = run_script(vec![
|
||||
TerminalTransportEvent::Output(b"\x1b[?2026hone ".to_vec()),
|
||||
TerminalTransportEvent::Output(b"two\x1b[?2026l".to_vec()),
|
||||
]);
|
||||
assert!(screen_once_it_shows(&runtime, "one two").contains("one two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_program_that_ends_mid_frame_still_shows_the_frame_and_its_exit() {
|
||||
let runtime = run_script(vec![
|
||||
TerminalTransportEvent::Output(b"\x1b[?2026hframe".to_vec()),
|
||||
TerminalTransportEvent::Exited {
|
||||
code: 137,
|
||||
signal: None,
|
||||
},
|
||||
]);
|
||||
let screen = screen_once_it_shows(&runtime, "退出码 137");
|
||||
assert!(screen.contains("frame"), "{screen}");
|
||||
assert!(screen.contains("[进程已退出,退出码 137]"), "{screen}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_that_never_pauses_does_not_hold_an_update_open() {
|
||||
let mut term = test_term(40, 4);
|
||||
let mut processor: Processor = Processor::new();
|
||||
processor.advance(&mut term, b"\x1b[?2026hframe");
|
||||
assert!(!end_overdue_sync(&mut processor, &mut term));
|
||||
assert_eq!(line_text(&term, 0), "");
|
||||
thread::sleep(Duration::from_millis(160));
|
||||
assert!(end_overdue_sync(&mut processor, &mut term));
|
||||
assert_eq!(line_text(&term, 0), "frame");
|
||||
// Done with, the next output is drawn as it comes.
|
||||
processor.advance(&mut term, b" more");
|
||||
assert_eq!(line_text(&term, 0), "frame more");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waiting_stops_at_the_deadline_but_not_before_what_has_come() {
|
||||
// Something comes in the end, so a wait past the deadline fails the
|
||||
// test rather than hangs it.
|
||||
let (late, receiver) = async_channel::unbounded();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
let _ = late.send_blocking(0);
|
||||
});
|
||||
let soon = Instant::now() + Duration::from_millis(20);
|
||||
assert!(matches!(receive(&receiver, Some(soon)), Received::Overdue));
|
||||
assert!(Instant::now() >= soon);
|
||||
|
||||
let (sender, receiver) = async_channel::unbounded();
|
||||
sender.send_blocking(1).unwrap();
|
||||
let past = Instant::now();
|
||||
assert!(matches!(receive(&receiver, Some(past)), Received::Event(1)));
|
||||
sender.send_blocking(2).unwrap();
|
||||
assert!(matches!(receive(&receiver, None), Received::Event(2)));
|
||||
drop(sender);
|
||||
assert!(matches!(receive(&receiver, None), Received::Closed));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user