1use gloo::{console::debug, timers::callback::Timeout};
2use molt::prelude::*;
4pub use molt_forked as molt;
5use std::{mem, rc::Rc};
6use web_sys::HtmlTextAreaElement;
7use yew::prelude::*;
8use yew_icons::{Icon, IconId};
9
10pub struct Terminal {
11 input_div_ref: NodeRef,
12 hist_div_ref: NodeRef,
13 input: String,
14 input_tmp: String,
15 current_hist_idx: Option<usize>,
16}
17#[derive(Debug, PartialEq, Clone, Copy, Default)]
18pub enum RunState {
19 #[default]
20 Ok,
21 Err,
22 Uncompleted,
23}
24
25#[derive(Debug, Properties, PartialEq)]
26pub struct TerminalProp {
27 pub class: &'static str,
28 pub hist: Rc<Vec<(RunState, String, Html)>>,
29 pub on_run_cmd: Callback<(String, bool)>,
31}
32
33pub enum TerminalMsg {
34 None,
35 UpdateInput(String),
36 KeyDown(Key),
38}
39
40pub enum Key {
41 Enter,
42 ArrowUp,
43 ArrowDown,
44}
45
46impl Terminal {
47 pub fn to_hist(
48 cmd_ctx: String,
49 outs: Vec<Result<Value, Exception>>,
50 ) -> (RunState, String, Html) {
51 let mut run_state = RunState::Ok;
52 let out_html = html! {
53 {for outs.iter().enumerate().map(
54 |(i,out)|{
55 match out{
56 Ok(s) => html!(<code class="stdout" style="margin:0px;white-space:pre-wrap;"> { s.to_string() }{if i==(outs.len()-1){html!()}else{html!(<br />)}}</code>),
57 Err(s) => {
58 run_state=RunState::Err;
59 html!(<code class="stderr" style="margin:0px;white-space:pre-wrap;"> { s.error_info().to_string() }{if i==(outs.len()-1){html!()}else{html!(<br />)}}</code>)},
60 }
61 }
62 )}
63 };
64 if run_state == RunState::Err {
65 if let Some(Err(e)) = outs.last() {
66 if e.is_uncompleted() {
67 run_state = RunState::Uncompleted;
68 }
69 }
70 }
71 (run_state, cmd_ctx, out_html)
72 }
73 fn input_div_cursor_to_end(&mut self) {
74 if let Some(textarea) = self.input_div_ref.cast::<HtmlTextAreaElement>() {
75 let length = self.input.chars().count() as u32;
76 Timeout::new(5, move || {
77 _ = textarea.set_selection_range(length, length);
78 })
79 .forget();
80 }
81 }
82}
83
84impl Component for Terminal {
85 type Message = TerminalMsg;
86 type Properties = TerminalProp;
87
88 fn create(_ctx: &Context<Self>) -> Self {
89 Self {
90 hist_div_ref: NodeRef::default(),
91 input_div_ref: NodeRef::default(),
92 input: String::new(),
93 input_tmp: String::new(),
94 current_hist_idx: None,
95 }
96 }
97
98 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
99 match msg {
100 TerminalMsg::KeyDown(key) => match key {
101 Key::Enter => {
102 let cmd = mem::take(&mut self.input);
103 if let Some((RunState::Uncompleted, _, _)) = ctx.props().hist.last() {
104 ctx.props().on_run_cmd.emit((cmd, true));
105 } else {
106 if !cmd.is_empty() {
107 ctx.props().on_run_cmd.emit((cmd, false));
108 }
109 }
110 if let Some(element) = self.hist_div_ref.cast::<web_sys::Element>() {
111 Timeout::new(5 as u32, move || {
112 element.set_scroll_top(element.scroll_height());
113 })
114 .forget();
115 }
116 self.current_hist_idx = None;
117 self.input_tmp.clear();
118 true
119 }
120 Key::ArrowUp => match self.current_hist_idx.as_mut() {
121 Some(0) => false,
122 Some(i) => {
123 if *i == ctx.props().hist.len() {
124 self.input_tmp = mem::take(&mut self.input);
125 }
126 *i -= 1;
127 if let Some((_, hist_cmd, _)) = ctx.props().hist.get(*i) {
128 self.input = hist_cmd.clone();
129 }
130 self.input_div_cursor_to_end();
131 true
132 }
133 None => {
134 let i = ctx.props().hist.len() - 1;
135 self.current_hist_idx = Some(i);
136 if let Some((_, hist_cmd, _)) = ctx.props().hist.get(i) {
137 self.input_tmp = mem::take(&mut self.input);
138 self.input = hist_cmd.clone();
139 }
140 self.input_div_cursor_to_end();
141 true
142 }
143 },
144 Key::ArrowDown => match self.current_hist_idx.as_mut() {
145 Some(i) => {
146 if *i == ctx.props().hist.len() {
147 false
148 } else if *i == ctx.props().hist.len() - 1 {
149 *i += 1;
150 self.input = mem::take(&mut self.input_tmp);
151 true
152 } else {
153 *i += 1;
154 if let Some((_, hist_cmd, _)) = ctx.props().hist.get(*i) {
155 self.input = hist_cmd.clone();
156 }
157 true
158 }
159 }
160 None => false,
161 },
162 },
163 TerminalMsg::UpdateInput(s) => {
164 self.input = s;
165 self.current_hist_idx = None;
166 self.input_tmp.clear();
167 if self.input == "\n" {
168 self.input.clear();
169 }
170 true
171 }
172 TerminalMsg::None => false,
173 }
174 }
175
176 fn rendered(&mut self, _ctx: &Context<Self>, first_render: bool) {
177 if first_render {
178 if let Some(element) = self.hist_div_ref.cast::<web_sys::Element>() {
180 let target_scroll_top = element.scroll_height();
181 let current_scroll_top = element.scroll_top();
182 let distance = target_scroll_top - current_scroll_top;
183 let steps = 40;
184 let step_duration = 20;
185 for step in 0..steps {
186 let current_scroll_top = current_scroll_top + distance * step / steps;
187 let element = element.clone();
188 Timeout::new((step_duration * step) as u32, move || {
189 element.set_scroll_top(current_scroll_top);
190 })
191 .forget();
192 }
193 }
194 }
195 }
196 fn view(&self, ctx: &Context<Self>) -> Html {
197 html! {
198 <div class={ctx.props().class}>
199 <ul ref={self.hist_div_ref.clone()}
200 class="history"
201 style="text-wrap:nowrap;margin:0px;overflow-y:auto;overflow-x:auto;">
202 { for ctx.props().hist.iter().map(|(run_state,cmd_ctx,out_html)|{
203 let (icon_class,icon,last_line,out_html) = match run_state{
204 RunState::Ok => ("stdout-icon",IconId::BootstrapCheckLg,html!(),out_html.clone()),
205 RunState::Err => ("stderr-icon",IconId::FontAwesomeSolidXmark,html!(),out_html.clone()),
206 RunState::Uncompleted => ("command",IconId::FontAwesomeSolidEllipsis,html!(
207 <div style="display:flex;flex-wrap:nowrap;">
208 <Icon class="command" icon_id={IconId::FontAwesomeSolidEllipsis} height={"10px".to_owned()} width={"15px".to_owned()}/>
209 </div>
210 ),html!()),
211 };
212 html!{
213 <li style="padding:0px;margin:0px;list-style:none;white-space:nowrap;">
214 <div style="display:flex;flex-wrap:nowrap;">
215 <Icon class={icon_class} icon_id={icon} height={"10px".to_owned()} width={"15px".to_owned()}/>
216 <code class="command" style="white-space: pre-wrap;">
217 {cmd_ctx}
218 </code>
219 </div>
220 {last_line}
221 <div style="padding-left:15px">
222 {out_html.clone()}
223 </div>
224 </li>
225 }
226 })}
227 </ul>
228 <textarea
229 class="input"
230 style="width:100%;"
231 ref={self.input_div_ref.clone()}
232 value={self.input.clone()}
233 oninput={ctx.link().callback(|e: InputEvent| {
234 let input: web_sys::HtmlInputElement = e.target_unchecked_into();
235 TerminalMsg::UpdateInput(input.value())
236 })}
237 onkeydown={ctx.link().callback(|e: KeyboardEvent| {
238 match e.key().as_str(){
239 "Enter" => TerminalMsg::KeyDown(Key::Enter),
240 "ArrowUp" => TerminalMsg::KeyDown(Key::ArrowUp),
241 "ArrowDown" => TerminalMsg::KeyDown(Key::ArrowDown),
242 _ => TerminalMsg::None,
243 }
244 })}
245 ></textarea>
246 </div>
247 }
248 }
249}