Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resolver = "3"
[workspace.dependencies]
# Cursive TUI framework
cursive = {version = "0.21", features = ["crossterm-backend"]}
crossterm = "0.29"
crossterm = {version = "0.29", features = ["event-stream"]}

# Data processing
anyhow = "1"
Expand Down
3 changes: 2 additions & 1 deletion ostool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
name = "ostool"
readme = "../README.md"
repository = "https://github.com/ZR233/ostool"
version = "0.8.7"
version = "0.8.8"

[[bin]]
name = "ostool"
Expand All @@ -22,6 +22,7 @@ ui-log = ["jkconfig/logging"]

[dependencies]
anyhow = {workspace = true, features = ["backtrace"]}
futures = "0.3"
byte-unit = "5.1"
cargo_metadata = "0.23"
clap = {workspace = true, features = ["derive"]}
Expand Down
123 changes: 74 additions & 49 deletions ostool/src/sterm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use std::thread;
use std::time::Duration;

use crossterm::{
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
event::{Event, EventStream, KeyCode, KeyEventKind, KeyModifiers},
terminal::{disable_raw_mode, enable_raw_mode},
};
use futures::stream::StreamExt;
use tokio::task::{AbortHandle, spawn_blocking};

type Tx = Box<dyn Write + Send>;
type Rx = Box<dyn Read + Send>;
Expand Down Expand Up @@ -83,64 +85,26 @@ impl SerialTerm {
is_running: AtomicBool::new(true),
});

// 使用 EventStream 异步处理键盘事件
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Chinese comment "使用 EventStream 异步处理键盘事件" (Using EventStream to handle keyboard events asynchronously) on line 88 is duplicated on line 458. Consider removing one of these comments to avoid redundancy, or make them more specific to their respective contexts (e.g., line 88 could mention "spawn the async keyboard task" while line 458 could mention "create the event stream reader").

Suggested change
// 使用 EventStream 异步处理键盘事件
// 使用 EventStream 异步处理键盘事件:启动异步键盘任务

Copilot uses AI. Check for mistakes.
let tx_handle = tokio::spawn(Self::tx_work_async(handle.clone(), tx_port));

let tx_abort = tx_handle.abort_handle();
// 启动串口接收线程
let rx_handle = thread::spawn({
let rx_handle = spawn_blocking({
let handle = handle.clone();
move || Self::handle_serial_receive(rx_port, handle, on_line)
move || Self::handle_serial_receive(rx_port, handle, tx_abort, on_line)
});

// 主线程处理键盘输入
let mut key_state = KeySequenceState::Normal;

while handle.is_running() {
// 非阻塞读取键盘事件
if event::poll(Duration::from_millis(10)).is_ok()
&& let Ok(Event::Key(key)) = event::read()
&& key.kind == KeyEventKind::Press
{
// 检测 Ctrl+A+x 退出序列
match key_state {
KeySequenceState::Normal => {
if key.code == KeyCode::Char('a')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
key_state = KeySequenceState::CtrlAPressed;
} else {
// 普通按键,发送到串口
Self::send_key_to_serial(&tx_port, key)?;
}
}
KeySequenceState::CtrlAPressed => {
if key.code == KeyCode::Char('x') {
// 用户请求退出
eprintln!("\r\nExit by: Ctrl+A+x");
handle.stop();
break;
} else {
// 不是x键,发送上一个按键并重置状态
if let KeyCode::Char('a') = key.code {
// 如果还是 Ctrl+A,保持状态
} else {
// 发送 Ctrl+A 和当前按键
Self::send_ctrl_a_to_serial(&tx_port)?;
Self::send_key_to_serial(&tx_port, key)?;
key_state = KeySequenceState::Normal;
}
}
}
}
}
}

// 等待接收线程结束
let _ = rx_handle.join();
let _ = rx_handle.await?;
let _ = tx_handle.await;
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a subtle issue with task ordering. If rx_handle completes first and calls tx_abort.abort() on line 156, then tx_handle.await on line 99 will return a JoinError. The ? operator on line 98 will propagate errors from rx_handle, but the ignored result on line 99 means we won't know if the tx task panicked versus being aborted normally. Consider using match tx_handle.await to distinguish between Ok(_), Err(e) if e.is_cancelled(), and Err(e) if e.is_panic(), so panics in the tx task aren't silently hidden.

Suggested change
let _ = tx_handle.await;
match tx_handle.await {
Ok(_) => {}
Err(e) if e.is_cancelled() => {
// tx task was aborted (expected when rx side finishes first)
info!("Serial terminal tx task aborted");
}
Err(e) if e.is_panic() => {
// tx task panicked; log so this is not silently hidden
error!("Serial terminal tx task panicked: {}", e);
}
Err(e) => {
// other unexpected join error
error!("Serial terminal tx task failed: {}", e);
}
}

Copilot uses AI. Check for mistakes.
info!("Serial terminal exited");
Ok(())
}

fn handle_serial_receive<F>(
rx_port: Arc<Mutex<Rx>>,
handle: Arc<TermHandle>,
tx_abort: AbortHandle,
on_line: F,
) -> io::Result<()>
where
Expand Down Expand Up @@ -189,7 +153,7 @@ impl SerialTerm {
}
}
}

tx_abort.abort();
Ok(())
}

Expand Down Expand Up @@ -489,4 +453,65 @@ impl SerialTerm {
tx_port.lock().unwrap().flush()?;
Ok(())
}

async fn tx_work_async(handle: Arc<TermHandle>, tx_port: Arc<Mutex<Tx>>) -> anyhow::Result<()> {
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function naming is inconsistent with Rust conventions. The function name tx_work_async uses a _async suffix, but in Rust it's more idiomatic to let the function signature (async fn) indicate that it's asynchronous. Consider renaming to tx_work or handle_keyboard_input for better clarity and consistency with the existing handle_serial_receive function.

Copilot uses AI. Check for mistakes.
// 使用 EventStream 异步处理键盘事件
let mut reader = EventStream::new();
let mut key_state = KeySequenceState::Normal;

while handle.is_running() {
// 使用 EventStream::next() 异步等待事件,不会阻塞
match reader.next().await {
Some(Ok(Event::Key(key))) if key.kind == KeyEventKind::Press => {
// 检测 Ctrl+A+x 退出序列
match key_state {
KeySequenceState::Normal => {
if key.code == KeyCode::Char('a')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
key_state = KeySequenceState::CtrlAPressed;
} else {
// 普通按键,发送到串口
if let Err(e) = Self::send_key_to_serial(&tx_port, key) {
eprintln!("\r\n发送按键失败: {}", e);
}
}
}
KeySequenceState::CtrlAPressed => {
if key.code == KeyCode::Char('x') {
// 用户请求退出
eprintln!("\r\nExit by: Ctrl+A+x");
handle.stop();
break;
} else {
// 不是x键,发送上一个按键并重置状态
if key.code != KeyCode::Char('a') {
if let Err(e) = Self::send_ctrl_a_to_serial(&tx_port) {
eprintln!("\r\n发送 Ctrl+A 失败: {}", e);
}
if let Err(e) = Self::send_key_to_serial(&tx_port, key) {
eprintln!("\r\n发送按键失败: {}", e);
}
key_state = KeySequenceState::Normal;
}
}
}
}
}
Some(Err(e)) => {
eprintln!("\r\n键盘事件错误: {}", e);
Comment on lines +476 to +502
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error messages are inconsistent in language. Lines 476, 490, 493 use Chinese (发送按键失败, 发送 Ctrl+A 失败), while line 483 uses English (Exit by: Ctrl+A+x), and line 502 uses Chinese (键盘事件错误). Consider using a consistent language throughout the codebase for error messages to maintain code quality. The existing codebase at line 151 uses Chinese (串口读取错误), and line 72 uses Chinese (已退出串口终端模式), suggesting Chinese is the standard.

Copilot uses AI. Check for mistakes.
break;
}
None => {
// EventStream 结束
break;
}
Some(Ok(_)) => {
// 忽略非按键事件(鼠标、调整大小等)
}
Comment on lines +463 to +511
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task exit mechanism has a potential delay issue. If handle.stop() is called externally (though not currently done in the code), the task will remain blocked on reader.next().await at line 464 until the next keyboard event arrives, rather than exiting promptly. While the current code relies on tx_abort.abort() from line 156 to forcefully terminate the task, consider using tokio::select! with a shutdown signal channel or periodic timeout checks for more graceful shutdown, especially if external stop signals are added in the future.

Suggested change
// 使用 EventStream::next() 异步等待事件,不会阻塞
match reader.next().await {
Some(Ok(Event::Key(key))) if key.kind == KeyEventKind::Press => {
// 检测 Ctrl+A+x 退出序列
match key_state {
KeySequenceState::Normal => {
if key.code == KeyCode::Char('a')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
key_state = KeySequenceState::CtrlAPressed;
} else {
// 普通按键,发送到串口
if let Err(e) = Self::send_key_to_serial(&tx_port, key) {
eprintln!("\r\n发送按键失败: {}", e);
}
}
}
KeySequenceState::CtrlAPressed => {
if key.code == KeyCode::Char('x') {
// 用户请求退出
eprintln!("\r\nExit by: Ctrl+A+x");
handle.stop();
break;
} else {
// 不是x键,发送上一个按键并重置状态
if key.code != KeyCode::Char('a') {
if let Err(e) = Self::send_ctrl_a_to_serial(&tx_port) {
eprintln!("\r\n发送 Ctrl+A 失败: {}", e);
}
if let Err(e) = Self::send_key_to_serial(&tx_port, key) {
eprintln!("\r\n发送按键失败: {}", e);
}
key_state = KeySequenceState::Normal;
}
}
}
}
}
Some(Err(e)) => {
eprintln!("\r\n键盘事件错误: {}", e);
break;
}
None => {
// EventStream 结束
break;
}
Some(Ok(_)) => {
// 忽略非按键事件(鼠标、调整大小等)
}
// 使用 tokio::select! 同时等待键盘事件和定期检查退出信号
tokio::select! {
_ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {
// 定期检查是否已请求退出(例如通过外部调用 handle.stop())
if !handle.is_running() {
break;
}
}
maybe_event = reader.next() => {
match maybe_event {
Some(Ok(Event::Key(key))) if key.kind == KeyEventKind::Press => {
// 检测 Ctrl+A+x 退出序列
match key_state {
KeySequenceState::Normal => {
if key.code == KeyCode::Char('a')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
key_state = KeySequenceState::CtrlAPressed;
} else {
// 普通按键,发送到串口
if let Err(e) = Self::send_key_to_serial(&tx_port, key) {
eprintln!("\r\n发送按键失败: {}", e);
}
}
}
KeySequenceState::CtrlAPressed => {
if key.code == KeyCode::Char('x') {
// 用户请求退出
eprintln!("\r\nExit by: Ctrl+A+x");
handle.stop();
break;
} else {
// 不是x键,发送上一个按键并重置状态
if key.code != KeyCode::Char('a') {
if let Err(e) = Self::send_ctrl_a_to_serial(&tx_port) {
eprintln!("\r\n发送 Ctrl+A 失败: {}", e);
}
if let Err(e) = Self::send_key_to_serial(&tx_port, key) {
eprintln!("\r\n发送按键失败: {}", e);
}
key_state = KeySequenceState::Normal;
}
}
}
}
}
Some(Err(e)) => {
eprintln!("\r\n键盘事件错误: {}", e);
break;
}
None => {
// EventStream 结束
break;
}
Some(Ok(_)) => {
// 忽略非按键事件(鼠标、调整大小等)
}
}
}

Copilot uses AI. Check for mistakes.
}
}

Ok(())
}
}