Skip to main content

wedi_core/
terminal.rs

1use anyhow::Result;
2use crossterm::{
3    cursor,
4    event::{
5        self, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyEvent, KeyEventKind,
6        KeyModifiers,
7    },
8    execute,
9    terminal::{self, ClearType},
10};
11
12#[cfg(feature = "mouse-support")]
13use crossterm::event::{MouseEvent, MouseEventKind};
14
15use std::io::{self, Write};
16
17/// 輸入事件類型:鍵盤事件或貼上事件
18#[derive(Debug, Clone)]
19pub enum InputEvent {
20    /// 鍵盤按鍵事件
21    Key(KeyEvent),
22    /// 貼上事件(包含已正規化的文字)
23    Paste(String),
24}
25
26pub struct Terminal {
27    size: (u16, u16),
28}
29
30impl Terminal {
31    pub fn new() -> Result<Self> {
32        let size = terminal::size()?;
33        Ok(Self { size })
34    }
35
36    pub fn enter_raw_mode() -> Result<()> {
37        terminal::enable_raw_mode()?;
38        execute!(
39            io::stdout(),
40            terminal::EnterAlternateScreen,
41            EnableBracketedPaste
42        )?;
43        Ok(())
44    }
45
46    pub fn exit_raw_mode() -> Result<()> {
47        execute!(
48            io::stdout(),
49            DisableBracketedPaste,
50            terminal::LeaveAlternateScreen
51        )?;
52        terminal::disable_raw_mode()?;
53        Ok(())
54    }
55
56    pub fn clear_screen() -> Result<()> {
57        execute!(io::stdout(), terminal::Clear(ClearType::All))?;
58        Ok(())
59    }
60
61    pub fn size(&self) -> (u16, u16) {
62        self.size
63    }
64
65    #[allow(dead_code)]
66    pub fn update_size(&mut self) -> Result<()> {
67        self.size = terminal::size()?;
68        Ok(())
69    }
70
71    #[allow(dead_code)]
72    pub fn flush() -> Result<()> {
73        io::stdout().flush()?;
74        Ok(())
75    }
76
77    pub fn read_key() -> Result<KeyEvent> {
78        loop {
79            let event = event::read()?;
80
81            match event {
82                Event::Key(key_event) => {
83                    // 處理正常的 Press 和 Repeat 事件
84                    if key_event.kind == KeyEventKind::Press
85                        || key_event.kind == KeyEventKind::Repeat
86                    {
87                        return Ok(key_event);
88                    }
89                }
90                Event::Resize(_cols, _rows) => {
91                    // 視窗大小改變,返回特殊標記
92                    return Ok(KeyEvent::new(KeyCode::F(21), KeyModifiers::NONE));
93                }
94                Event::Paste(_text) => {
95                    // Bracketed Paste 事件
96                    // 返回一個特殊按鍵標記(使用 read_input 可獲取完整文字)
97                    return Ok(KeyEvent::new(KeyCode::F(20), KeyModifiers::NONE));
98                }
99                #[cfg(feature = "mouse-support")]
100                Event::Mouse(mouse_event) => {
101                    // 滑鼠滾輪事件轉換為虛擬按鍵
102                    if let Some(key_event) = handle_mouse_event(mouse_event) {
103                        return Ok(key_event);
104                    }
105                }
106                _ => {
107                    // 忽略其他事件(鼠標、調整大小等)
108                }
109            }
110        }
111    }
112
113    /// 讀取輸入事件(支援 Bracketed Paste)
114    ///
115    /// 與 `read_key()` 不同,此方法返回 `InputEvent` 枚舉,
116    /// 可以區分鍵盤事件和貼上事件,並在貼上事件中攜帶完整文字。
117    pub fn read_input() -> Result<InputEvent> {
118        loop {
119            let event = event::read()?;
120
121            match event {
122                Event::Key(key_event) => {
123                    // 處理正常的 Press 和 Repeat 事件
124                    if key_event.kind == KeyEventKind::Press
125                        || key_event.kind == KeyEventKind::Repeat
126                    {
127                        return Ok(InputEvent::Key(key_event));
128                    }
129                }
130                Event::Resize(_cols, _rows) => {
131                    // 視窗大小改變,返回特殊標記
132                    return Ok(InputEvent::Key(KeyEvent::new(
133                        KeyCode::F(21),
134                        KeyModifiers::NONE,
135                    )));
136                }
137                Event::Paste(text) => {
138                    // Bracketed Paste 事件
139                    // 正規化行尾符號:\r\n 和 \r 都轉換為 \n
140                    let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
141                    return Ok(InputEvent::Paste(normalized));
142                }
143                #[cfg(feature = "mouse-support")]
144                Event::Mouse(mouse_event) => {
145                    // 滑鼠滾輪事件轉換為虛擬按鍵
146                    if let Some(key_event) = handle_mouse_event(mouse_event) {
147                        return Ok(InputEvent::Key(key_event));
148                    }
149                }
150                _ => {
151                    // 忽略其他事件
152                }
153            }
154        }
155    }
156
157    #[allow(dead_code)]
158    pub fn set_cursor_position(x: u16, y: u16) -> Result<()> {
159        execute!(io::stdout(), cursor::MoveTo(x, y))?;
160        Ok(())
161    }
162
163    #[allow(dead_code)]
164    pub fn hide_cursor() -> Result<()> {
165        execute!(io::stdout(), cursor::Hide)?;
166        Ok(())
167    }
168
169    pub fn show_cursor() -> Result<()> {
170        execute!(io::stdout(), cursor::Show)?;
171        Ok(())
172    }
173}
174
175#[cfg(feature = "mouse-support")]
176fn handle_mouse_event(mouse_event: MouseEvent) -> Option<KeyEvent> {
177    match mouse_event.kind {
178        MouseEventKind::ScrollUp => Some(KeyEvent::new(KeyCode::F(22), KeyModifiers::NONE)),
179        MouseEventKind::ScrollDown => Some(KeyEvent::new(KeyCode::F(23), KeyModifiers::NONE)),
180        _ => None,
181    }
182}
183
184impl Drop for Terminal {
185    fn drop(&mut self) {
186        let _ = Self::exit_raw_mode();
187        let _ = Self::show_cursor();
188    }
189}