frozen_term/
local_terminal.rs

1use std::{sync::Arc, time::Duration};
2
3use crate::{Style, terminal};
4use async_pty::PtyProcess;
5use iced::{
6    self, Element, Length, Task,
7    task::sipper,
8    widget::{center, text},
9};
10
11#[derive(Debug, Clone)]
12pub struct Message(InnerMessage);
13
14#[derive(Debug, Clone)]
15enum InnerMessage {
16    Opened(Arc<(PtyProcess, tokio::sync::mpsc::Receiver<Vec<u8>>)>),
17    Terminal(terminal::Message),
18    Output(Vec<u8>),
19    InjectInput(Vec<u8>),
20    Closed,
21}
22
23pub enum Action {
24    Run(Task<Message>),
25    IdChanged,
26    Close,
27    None,
28}
29
30enum State {
31    Starting,
32    Active(PtyProcess),
33    Closed,
34}
35
36pub struct LocalTerminal {
37    state: State,
38    display: terminal::Terminal,
39}
40
41impl LocalTerminal {
42    pub fn start(
43        key_filter: impl 'static + Fn(&iced::keyboard::Key, &iced::keyboard::Modifiers) -> bool,
44    ) -> (Self, Task<Message>) {
45        let size = async_pty::TerminalSize { cols: 80, rows: 24 };
46        let (display, display_task) = terminal::Terminal::new();
47        let display = display.key_filter(key_filter);
48
49        let start_task = Task::future(async {
50            let (process, output) = PtyProcess::shell(size).await.unwrap();
51            Message(InnerMessage::Opened(Arc::new((process, output))))
52        });
53
54        (
55            Self {
56                state: State::Starting,
57                display,
58            },
59            Task::batch([
60                display_task.map(InnerMessage::Terminal).map(Message),
61                start_task,
62            ]),
63        )
64    }
65
66    pub fn style(mut self, style: Style) -> Self {
67        self.set_style(style);
68        self
69    }
70
71    pub fn set_style(&mut self, style: Style) {
72        self.display.set_style(style);
73    }
74
75    #[must_use]
76    pub fn update(&mut self, message: Message) -> Action {
77        match message.0 {
78            InnerMessage::Opened(arc) => {
79                let (process, output) = Arc::into_inner(arc).unwrap();
80
81                let stream = sipper(|mut sender| async move {
82                    let mut output = output;
83                    while let Some(chunk) = output.recv().await {
84                        sender.send(InnerMessage::Output(chunk)).await;
85                    }
86
87                    sender.send(InnerMessage::Closed).await;
88                });
89
90                let task = Task::stream(stream).map(Message);
91
92                self.state = State::Active(process);
93
94                Action::Run(task)
95            }
96            InnerMessage::Terminal(message) => {
97                let action = self.display.update(message);
98
99                match action {
100                    terminal::Action::None => Action::None,
101                    terminal::Action::Run(task) => {
102                        Action::Run(task.map(InnerMessage::Terminal).map(Message))
103                    }
104                    terminal::Action::IdChanged => Action::IdChanged,
105                    terminal::Action::Input(input) => {
106                        if let State::Active(pty) = &self.state {
107                            pty.try_write(input).unwrap();
108                        }
109                        Action::None
110                    }
111                    terminal::Action::Resize(size) => {
112                        if let State::Active(pty) = &self.state {
113                            pty.try_resize(async_pty::TerminalSize {
114                                rows: size.rows as u16,
115                                cols: size.cols as u16,
116                            })
117                            .unwrap();
118                        }
119                        Action::None
120                    }
121                }
122            }
123            InnerMessage::InjectInput(input) => {
124                if let State::Active(pty) = &self.state {
125                    pty.try_write(input).unwrap();
126                }
127                Action::None
128            }
129            InnerMessage::Output(output) => {
130                self.display.advance_bytes(output);
131
132                Action::None
133            }
134            InnerMessage::Closed => {
135                self.state = State::Closed;
136
137                Action::Close
138            }
139        }
140    }
141
142    pub fn view<'a>(&'a self) -> Element<'a, Message> {
143        match &self.state {
144            State::Starting => center(text!("opening pty...")).into(),
145            State::Active(_) => self.display.view().map(InnerMessage::Terminal).map(Message),
146            State::Closed => center(text!("pty closed")).height(Length::Fill).into(),
147        }
148    }
149
150    pub fn get_title(&self) -> &str {
151        self.display.get_title()
152    }
153
154    #[must_use]
155    pub fn focus<T>(&self) -> Task<T>
156    where
157        T: Send + 'static,
158    {
159        self.display.focus()
160    }
161
162    /// !!!WARNING!!!
163    ///
164    /// injected input will be directly injected into the stdin of the terminal process.
165    /// If the user has typed something, that input will still be there!
166    /// When writing commands manually, you'll need to ensure that they are not influenced by what the user has typed
167    /// and you will also have to handle key encoding and control characters yourself.
168    #[must_use]
169    pub fn inject_input(&self, input: InputSequence) -> Task<Message> {
170        if let State::Active(ref pty) = self.state {
171            match input {
172                InputSequence::Raw(input) => {
173                    let _ = pty.try_write(input);
174                    Task::none()
175                }
176                InputSequence::AbortAndRaw(input) => {
177                    let _ = pty.try_write(b"\x03".to_vec());
178                    // While I'd love to skip this weird helper task, my shell just doesn't clear the current line without it.
179                    //
180                    Task::future(async move {
181                        tokio::time::sleep(INJECTION_DELAY).await;
182                        Message(InnerMessage::InjectInput(input))
183                    })
184                }
185                InputSequence::AbortAndCommand(mut input) => {
186                    let _ = pty.try_write(b"\x03".to_vec());
187                    input.push('\n');
188                    let input = input.into_bytes();
189                    Task::future(async move {
190                        tokio::time::sleep(INJECTION_DELAY).await;
191                        Message(InnerMessage::InjectInput(input))
192                    })
193                }
194            }
195        } else {
196            Task::none()
197        }
198    }
199}
200
201const INJECTION_DELAY: Duration = Duration::from_millis(100);
202
203pub enum InputSequence {
204    /// !!!WARNING!!!
205    ///
206    /// Is is very rare to need a raw input sequence.
207    /// Please ensure you absolutely know what you are doing before using this method.
208    Raw(Vec<u8>),
209    /// This will send the equivalent of Ctrl+C to the terminal process.
210    /// Before adding your input.
211    AbortAndRaw(Vec<u8>),
212    /// This will send the equivalent of Ctrl+C to the terminal process,
213    /// add your command and finally send a newline.
214    ///
215    /// Be aware that your command will not be sanitized!.
216    AbortAndCommand(String),
217}