nightshade_api/web/
terminal.rs1use leptos::html;
5use leptos::prelude::*;
6
7#[derive(Clone, Copy, PartialEq, Eq)]
9pub enum TerminalTone {
10 Normal,
12 Dim,
14 Success,
16 Warn,
18 Error,
20 Command,
22}
23
24impl TerminalTone {
25 fn class(self) -> &'static str {
26 match self {
27 TerminalTone::Normal => "normal",
28 TerminalTone::Dim => "dim",
29 TerminalTone::Success => "success",
30 TerminalTone::Warn => "warn",
31 TerminalTone::Error => "error",
32 TerminalTone::Command => "command",
33 }
34 }
35}
36
37#[derive(Clone)]
40pub struct TerminalLine {
41 pub id: usize,
43 pub text: String,
45 pub tone: TerminalTone,
47}
48
49impl TerminalLine {
50 pub fn new(id: usize, text: impl Into<String>, tone: TerminalTone) -> Self {
52 Self {
53 id,
54 text: text.into(),
55 tone,
56 }
57 }
58}
59
60#[component]
64pub fn Terminal(
65 #[prop(into)] lines: Signal<Vec<TerminalLine>>,
66 on_input: Callback<String>,
67 #[prop(into, optional)] prompt: String,
68) -> impl IntoView {
69 let draft = RwSignal::new(String::new());
70 let body_ref = NodeRef::<html::Div>::new();
71 let prompt = if prompt.is_empty() {
72 "$".to_string()
73 } else {
74 prompt
75 };
76 let prompt = StoredValue::new(prompt);
77
78 Effect::new(move |_| {
79 let _ = lines.get();
80 if let Some(body) = body_ref.get() {
81 body.set_scroll_top(body.scroll_height());
82 }
83 });
84
85 let submit = move || {
86 let text = draft.get_untracked();
87 if !text.trim().is_empty() {
88 on_input.run(text);
89 draft.set(String::new());
90 }
91 };
92
93 view! {
94 <div class="nightshade-terminal">
95 <div class="nightshade-terminal-body" node_ref=body_ref>
96 <For each=move || lines.get() key=|line| line.id let:line>
97 <div class=format!("nightshade-terminal-line {}", line.tone.class())>
98 {line.text.clone()}
99 </div>
100 </For>
101 </div>
102 <div class="nightshade-terminal-prompt">
103 <span class="nightshade-terminal-sigil">{move || prompt.get_value()}</span>
104 <input
105 class="nightshade-terminal-input"
106 spellcheck="false"
107 autocomplete="off"
108 prop:value=move || draft.get()
109 on:input=move |event| draft.set(event_target_value(&event))
110 on:keydown=move |event| {
111 if event.key() == "Enter" {
112 event.prevent_default();
113 submit();
114 }
115 }
116 />
117 </div>
118 </div>
119 }
120}