1use tern_sdk::{
6 Input, Options, Session, SurfaceOptions, nodes,
7 ui::{self, Side, Tone, View},
8 wire::feature,
9};
10
11#[derive(Clone, Debug)]
13enum Msg {
14 Send(String),
16 Focus,
18}
19
20const EDITOR: &str = "dock.ed";
22
23#[derive(Default)]
25struct Chat {
26 transcript: Vec<(&'static str, String)>,
28 draft: String,
30 cursor: usize,
32}
33
34impl Chat {
35 fn byte(&self, chars: usize) -> usize {
37 self
38 .draft
39 .char_indices()
40 .nth(chars)
41 .map_or(self.draft.len(), |(i, _)| i)
42 }
43
44 fn insert(&mut self, text: &str) {
46 let at = self.byte(self.cursor);
47 self.draft.insert_str(at, text);
48 self.cursor += text.chars().count();
49 }
50
51 fn send(&mut self, text: String) {
53 if text.trim().is_empty() {
54 return;
55 }
56 let reply = format!("You said *{}* ({} chars).", text.trim(), text.chars().count());
57 self.transcript.push(("you", text));
58 self.transcript.push(("bot", reply));
59 }
60
61 fn view(&self) -> View<Msg> {
63 let messages = self.transcript.iter().enumerate().map(|(i, (who, text))| {
64 let node = ui::md(text.clone()).key(i);
65 if *who == "you" {
66 ui::card().key(i).tone("user").child(node.key("t"))
67 } else {
68 ui::card().key(i).variant("bare").child(node.key("t"))
69 }
70 });
71 let cursor: usize = self
72 .draft
73 .chars()
74 .take(self.cursor)
75 .map(char::len_utf16)
76 .sum();
77 View::new().main(ui::col().children(messages)).dock(nodes![
78 ui::editor()
79 .key("ed")
80 .text(self.draft.clone())
81 .cursor(cursor)
82 .placeholder("Message (Enter sends, Escape quits)")
83 .sendable(true)
84 .on_send(|text| Msg::Send(text.to_owned()))
85 .on_focus(Msg::Focus),
86 ui::status()
87 .key("bar")
88 .child(ui::seg("chat").icon("message"))
89 .child(
90 ui::seg(format!("{} messages", self.transcript.len()))
91 .side(Side::Right)
92 .tone(if self.transcript.is_empty() {
93 Tone::Muted
94 } else {
95 Tone::Success
96 }),
97 ),
98 ])
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn composer_edits_codepoints_and_reports_utf16_cursors() {
108 let mut chat = Chat::default();
109 chat.insert("a😀é");
110 assert_eq!(chat.cursor, 3);
111 assert_eq!(chat.byte(2), 5);
112 chat.cursor = 2;
113 chat.insert("界");
114 assert_eq!(chat.draft, "a😀界é");
115 let view = chat.view();
116 let editor = &view.regions()[1].unwrap().child_nodes()[0];
117 assert_eq!(editor.props()["cursor"], 4);
118 let at = chat.byte(chat.cursor - 1);
119 chat.draft.remove(at);
120 chat.cursor -= 1;
121 assert_eq!(chat.draft, "a😀é");
122 let at = chat.byte(chat.cursor);
123 chat.draft.remove(at);
124 assert_eq!(chat.draft, "a😀");
125 }
126}
127
128fn main() -> Result<(), tern_sdk::Error> {
129 let Some(mut session) = Session::connect(Options::new().feature(feature::SEND))? else {
130 return plain();
131 };
132 let sf = session.open(SurfaceOptions::inline().title("chat"))?;
133 let mut chat = Chat::default();
134 session.render(sf, chat.view())?;
135 session.focus(sf, Some(EDITOR))?;
136 while let Some(input) = session.next(None)? {
137 match input {
138 Input::Key(key) => {
139 if key.is("escape") || key.is("ctrl+c") || key.is("ctrl+d") {
140 break;
141 }
142 match key.name.as_str() {
143 "enter" if !key.shift && !key.alt => {
144 let text = std::mem::take(&mut chat.draft);
145 chat.cursor = 0;
146 chat.send(text);
147 },
148 "enter" => chat.insert("\n"),
149 "backspace" if chat.cursor > 0 => {
150 let at = chat.byte(chat.cursor - 1);
151 chat.draft.remove(at);
152 chat.cursor -= 1;
153 },
154 "delete" if chat.cursor < chat.draft.chars().count() => {
155 let at = chat.byte(chat.cursor);
156 chat.draft.remove(at);
157 },
158 "left" => chat.cursor = chat.cursor.saturating_sub(1),
159 "right" => chat.cursor = (chat.cursor + 1).min(chat.draft.chars().count()),
160 "home" => chat.cursor = 0,
161 "end" => chat.cursor = chat.draft.chars().count(),
162 "paste" => chat.insert(key.text.as_deref().unwrap_or_default()),
163 _ => match key.typed() {
164 Some(text) => chat.insert(text),
165 None => continue,
166 },
167 }
168 },
169 Input::Msg(Msg::Send(text), _) => chat.send(text),
170 Input::Msg(Msg::Focus, _) => session.focus(sf, Some(EDITOR))?,
171 Input::Event(_) | Input::Clipboard(_) => continue,
172 }
173 session.render(sf, chat.view())?;
174 }
175 session.close()?;
176 println!("Chat closed after {} messages.", chat.transcript.len());
177 Ok(())
178}
179
180fn plain() -> Result<(), tern_sdk::Error> {
182 use std::io::{BufRead, Write};
183
184 let mut chat = Chat::default();
185 let mut out = std::io::stdout().lock();
186 write!(out, "> ")?;
187 out.flush()?;
188 for line in std::io::stdin().lock().lines() {
189 let before = chat.transcript.len();
190 chat.send(line?);
191 for (_, reply) in chat.transcript.iter().skip(before + 1) {
193 writeln!(out, "{reply}")?;
194 }
195 write!(out, "> ")?;
196 out.flush()?;
197 }
198 writeln!(out)?;
199 Ok(())
200}