Skip to main content

rustapi/docks/
console.rs

1use crate::editor::RUSTERIX;
2use crate::prelude::*;
3use rusterix::{Entity, Item, Value, server::ServerState};
4use theframework::prelude::*;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7enum ConsoleFocus {
8    Root,
9    Entity(u32),
10    Item(u32),
11}
12
13pub struct ConsoleDock {
14    transcript: String,
15    focus: ConsoleFocus,
16}
17
18#[derive(Clone)]
19struct RuntimeEntity {
20    entity: Entity,
21}
22
23#[derive(Clone)]
24struct RuntimeItem {
25    item: Item,
26}
27
28impl ConsoleDock {
29    fn console_input_id(ui: &mut TheUI) -> Option<TheId> {
30        ui.get_widget("Console Input")
31            .map(|widget| widget.id().clone())
32    }
33
34    fn set_output(&mut self, text: String, ui: &mut TheUI, ctx: &mut TheContext) {
35        self.transcript = text;
36        self.sync_output(ui, ctx);
37    }
38
39    fn sync_output(&self, ui: &mut TheUI, ctx: &mut TheContext) {
40        ui.set_widget_value(
41            "Console Output",
42            ctx,
43            TheValue::Text(self.transcript.clone()),
44        );
45    }
46
47    fn set_input(&self, ui: &mut TheUI, ctx: &mut TheContext, text: &str) {
48        ui.set_widget_value("Console Input", ctx, TheValue::Text(text.to_string()));
49    }
50
51    fn clear_input(&self, ui: &mut TheUI) {
52        if let Some(widget) = ui.get_widget("Console Input")
53            && let Some(edit) = widget.as_text_line_edit()
54        {
55            edit.set_text(String::new());
56        }
57    }
58
59    fn prompt(&self, project: &Project, server_ctx: &ServerContext) -> String {
60        let region_name = project
61            .get_region_ctx(server_ctx)
62            .map(|region| region.name.clone())
63            .unwrap_or_else(|| "Region".to_string());
64        match self.focus {
65            ConsoleFocus::Root => region_name,
66            ConsoleFocus::Entity(id) => {
67                let name = Self::runtime_snapshot(project, server_ctx)
68                    .ok()
69                    .and_then(|(entities, _)| {
70                        entities
71                            .iter()
72                            .find(|entity| entity.entity.id == id)
73                            .map(|entity| Self::entity_name(&entity.entity))
74                    })
75                    .unwrap_or_else(|| "Character".to_string());
76                format!("{} / {}", region_name, name)
77            }
78            ConsoleFocus::Item(id) => {
79                let name = Self::runtime_snapshot(project, server_ctx)
80                    .ok()
81                    .and_then(|(_, items)| {
82                        items
83                            .iter()
84                            .find(|item| item.item.id == id)
85                            .map(|item| Self::item_name(&item.item))
86                    })
87                    .unwrap_or_else(|| "Item".to_string());
88                format!("{} / {}", region_name, name)
89            }
90        }
91    }
92
93    fn entity_name(entity: &Entity) -> String {
94        entity
95            .get_attr_string("name")
96            .unwrap_or_else(|| format!("Entity {}", entity.id))
97    }
98
99    fn item_name(item: &Item) -> String {
100        item.get_attr_string("name")
101            .unwrap_or_else(|| format!("Item {}", item.id))
102    }
103
104    fn quoted(text: &str) -> String {
105        format!("\"{}\"", text.replace('"', "'"))
106    }
107
108    fn format_value(value: &Value) -> String {
109        value.to_string()
110    }
111
112    fn intro() -> String {
113        [
114            "Console ready.",
115            "Commands: help, list, focus <name|id>, show, get <key>, pwd, up, clear",
116            "When the game is running, `list` shows live characters and items for the current editor region.",
117        ]
118        .join("\n")
119    }
120
121    fn parse_id(text: &str) -> Option<u32> {
122        text.trim().parse::<u32>().ok()
123    }
124
125    fn entity_matches(entity: &Entity, needle: &str) -> bool {
126        entity.id.to_string() == needle || Self::entity_name(entity).eq_ignore_ascii_case(needle)
127    }
128
129    fn item_matches(item: &Item, needle: &str) -> bool {
130        item.id.to_string() == needle || Self::item_name(item).eq_ignore_ascii_case(needle)
131    }
132
133    fn collect_nested_items(items: &[Item], out: &mut Vec<RuntimeItem>) {
134        for item in items {
135            out.push(RuntimeItem { item: item.clone() });
136            if let Some(container) = &item.container {
137                Self::collect_nested_items(container, out);
138            }
139        }
140    }
141
142    fn collect_nested_items_from_entity(entity: &Entity, out: &mut Vec<RuntimeItem>) {
143        for item in entity.inventory.iter().flatten() {
144            out.push(RuntimeItem { item: item.clone() });
145            if let Some(container) = &item.container {
146                Self::collect_nested_items(container, out);
147            }
148        }
149        for item in entity.equipped.values() {
150            out.push(RuntimeItem { item: item.clone() });
151            if let Some(container) = &item.container {
152                Self::collect_nested_items(container, out);
153            }
154        }
155    }
156
157    fn collect_focusable_items(
158        entities: &[RuntimeEntity],
159        items: &[RuntimeItem],
160    ) -> Vec<RuntimeItem> {
161        let mut collected = Vec::new();
162        let mut seen = std::collections::HashSet::new();
163
164        for item in items {
165            if seen.insert(item.item.id) {
166                collected.push(item.clone());
167            }
168            if let Some(container) = &item.item.container {
169                let mut nested = Vec::new();
170                Self::collect_nested_items(container, &mut nested);
171                for child in nested {
172                    if seen.insert(child.item.id) {
173                        collected.push(child);
174                    }
175                }
176            }
177        }
178
179        for entity in entities {
180            let mut nested = Vec::new();
181            Self::collect_nested_items_from_entity(&entity.entity, &mut nested);
182            for child in nested {
183                if seen.insert(child.item.id) {
184                    collected.push(child);
185                }
186            }
187        }
188
189        collected
190    }
191
192    fn focused_entity<'a>(&self, entities: &'a [RuntimeEntity]) -> Option<&'a RuntimeEntity> {
193        match self.focus {
194            ConsoleFocus::Entity(id) => entities.iter().find(|entity| entity.entity.id == id),
195            _ => None,
196        }
197    }
198
199    fn focused_item<'a>(&self, items: &'a [RuntimeItem]) -> Option<&'a RuntimeItem> {
200        match self.focus {
201            ConsoleFocus::Item(id) => items.iter().find(|item| item.item.id == id),
202            _ => None,
203        }
204    }
205
206    fn pad(value: &str, width: usize) -> String {
207        let mut out = String::new();
208        let mut count = 0usize;
209        for ch in value.chars() {
210            if count >= width {
211                break;
212            }
213            out.push(ch);
214            count += 1;
215        }
216        while count < width {
217            out.push(' ');
218            count += 1;
219        }
220        out
221    }
222
223    fn entry_cell(name: &str, id: u32, width: usize) -> String {
224        let label = Self::quoted(name);
225        let left = Self::pad(&label, width.saturating_sub(8));
226        format!("{} {:>6}", left, id)
227    }
228
229    fn push_item_tree(lines: &mut Vec<String>, item: &Item, depth: usize) {
230        let indent = "\t".repeat(depth);
231        lines.push(format!(
232            "{}{} {}",
233            indent,
234            Self::quoted(&Self::item_name(item)),
235            item.id
236        ));
237
238        if let Some(container) = &item.container {
239            for child in container {
240                Self::push_item_tree(lines, child, depth + 1);
241            }
242        }
243    }
244
245    fn push_equipped_tree(lines: &mut Vec<String>, slot: &str, item: &Item) {
246        lines.push(format!(
247            "{} = {} {}",
248            slot,
249            Self::quoted(&Self::item_name(item)),
250            item.id
251        ));
252        if let Some(container) = &item.container {
253            for child in container {
254                Self::push_item_tree(lines, child, 1);
255            }
256        }
257    }
258
259    fn pair_row(
260        left: Option<String>,
261        right: Option<String>,
262        width: usize,
263        separator: &str,
264    ) -> String {
265        format!(
266            "{}{}{}",
267            Self::pad(left.as_deref().unwrap_or(""), width),
268            separator,
269            right.unwrap_or_default()
270        )
271    }
272
273    fn triple_row(
274        left: Option<String>,
275        middle: Option<String>,
276        right: Option<String>,
277        width: usize,
278        separator: &str,
279    ) -> String {
280        format!(
281            "{}{}{}{}{}",
282            Self::pad(left.as_deref().unwrap_or(""), width),
283            separator,
284            Self::pad(middle.as_deref().unwrap_or(""), width),
285            separator,
286            right.unwrap_or_default()
287        )
288    }
289
290    fn list_root(&self, entities: &[RuntimeEntity], items: &[RuntimeItem]) -> String {
291        let column_width = 38usize;
292        let separator = " | ";
293        let mut lines = vec![
294            Self::pair_row(
295                Some(format!("Characters ({})", entities.len())),
296                Some(format!("Items ({})", items.len())),
297                column_width,
298                separator,
299            ),
300            Self::pair_row(
301                Some("Name                               Id".to_string()),
302                Some("Name                               Id".to_string()),
303                column_width,
304                separator,
305            ),
306        ];
307
308        let row_count = entities.len().max(items.len()).max(1);
309        for index in 0..row_count {
310            let left = entities.get(index).map(|entity| {
311                Self::entry_cell(
312                    &Self::entity_name(&entity.entity),
313                    entity.entity.id,
314                    column_width,
315                )
316            });
317            let right = items.get(index).map(|item| {
318                Self::entry_cell(&Self::item_name(&item.item), item.item.id, column_width)
319            });
320            lines.push(Self::pair_row(left, right, column_width, separator));
321        }
322        lines.join("\n")
323    }
324
325    fn list_entity(&self, entity: &RuntimeEntity) -> String {
326        let mut lines = vec![
327            format!(
328                "Character {} {}",
329                Self::quoted(&Self::entity_name(&entity.entity)),
330                entity.entity.id
331            ),
332            format!(
333                "position = [{:.2}, {:.2}, {:.2}]",
334                entity.entity.position.x, entity.entity.position.y, entity.entity.position.z
335            ),
336            format!(
337                "orientation = [{:.2}, {:.2}]",
338                entity.entity.orientation.x, entity.entity.orientation.y
339            ),
340        ];
341
342        let mut attr_lines = Vec::new();
343        let keys = entity.entity.attributes.keys_sorted();
344        if keys.is_empty() {
345            attr_lines.push("<none>".to_string());
346        } else {
347            for key in keys {
348                if key == "setup" || key == "_source_seq" {
349                    continue;
350                }
351                if let Some(value) = entity.entity.attributes.get(key) {
352                    attr_lines.push(format!("{} = {}", key, Self::format_value(value)));
353                }
354            }
355        }
356        if attr_lines.is_empty() {
357            attr_lines.push("<none>".to_string());
358        }
359
360        let mut inventory_lines = Vec::new();
361        for item in entity.entity.inventory.iter().flatten() {
362            Self::push_item_tree(&mut inventory_lines, item, 1);
363        }
364        if inventory_lines.is_empty() {
365            inventory_lines.push("<empty>".to_string());
366        }
367
368        let mut equipped_lines = Vec::new();
369        for (slot, item) in &entity.entity.equipped {
370            Self::push_equipped_tree(&mut equipped_lines, slot, item);
371        }
372        if equipped_lines.is_empty() {
373            equipped_lines.push("<empty>".to_string());
374        }
375
376        let column_width = 28;
377        let separator = " | ";
378        lines.push(Self::triple_row(
379            Some("attributes".to_string()),
380            Some("inventory".to_string()),
381            Some("equipped".to_string()),
382            column_width,
383            separator,
384        ));
385        let row_count = attr_lines
386            .len()
387            .max(inventory_lines.len())
388            .max(equipped_lines.len());
389        for index in 0..row_count {
390            lines.push(Self::triple_row(
391                attr_lines.get(index).cloned(),
392                inventory_lines.get(index).cloned(),
393                equipped_lines.get(index).cloned(),
394                column_width,
395                separator,
396            ));
397        }
398
399        lines.join("\n")
400    }
401
402    fn list_item(&self, item: &RuntimeItem) -> String {
403        let mut lines = vec![
404            format!(
405                "Item {} {}",
406                Self::quoted(&Self::item_name(&item.item)),
407                item.item.id
408            ),
409            format!(
410                "position = [{:.2}, {:.2}, {:.2}]",
411                item.item.position.x, item.item.position.y, item.item.position.z
412            ),
413            "attributes".to_string(),
414        ];
415
416        let keys = item.item.attributes.keys_sorted();
417        if keys.is_empty() {
418            lines.push("<none>".to_string());
419        } else {
420            for key in keys {
421                if key == "setup" || key == "_source_seq" {
422                    continue;
423                }
424                if let Some(value) = item.item.attributes.get(key) {
425                    lines.push(format!("{} = {}", key, Self::format_value(value)));
426                }
427            }
428        }
429
430        lines.push("container".to_string());
431        if let Some(container) = &item.item.container {
432            if container.is_empty() {
433                lines.push("<empty>".to_string());
434            } else {
435                for child in container {
436                    Self::push_item_tree(&mut lines, child, 1);
437                }
438            }
439        } else {
440            lines.push("<none>".to_string());
441        }
442
443        lines.join("\n")
444    }
445
446    fn runtime_snapshot(
447        project: &Project,
448        server_ctx: &ServerContext,
449    ) -> Result<(Vec<RuntimeEntity>, Vec<RuntimeItem>), String> {
450        let rusterix = RUSTERIX.read().unwrap();
451        if rusterix.server.state != ServerState::Running {
452            return Err("Game is not running.".to_string());
453        }
454
455        let mut runtime_entities = Vec::new();
456        let mut runtime_items = Vec::new();
457
458        let (entities, items) = rusterix.server.get_entities_items(&server_ctx.curr_region);
459        if let Some(entities) = entities {
460            for entity in entities {
461                runtime_entities.push(RuntimeEntity {
462                    entity: entity.clone(),
463                });
464            }
465        }
466        if let Some(items) = items {
467            for item in items {
468                runtime_items.push(RuntimeItem { item: item.clone() });
469            }
470        }
471
472        if runtime_entities.is_empty()
473            && runtime_items.is_empty()
474            && let Some(region) = project.get_region_ctx(server_ctx)
475        {
476            for entity in &region.map.entities {
477                runtime_entities.push(RuntimeEntity {
478                    entity: entity.clone(),
479                });
480            }
481            for item in &region.map.items {
482                runtime_items.push(RuntimeItem { item: item.clone() });
483            }
484        }
485
486        Ok((runtime_entities, runtime_items))
487    }
488
489    fn focus_label(&self, entities: &[RuntimeEntity], items: &[RuntimeItem]) -> String {
490        match self.focus {
491            ConsoleFocus::Root => "root".to_string(),
492            ConsoleFocus::Entity(id) => entities
493                .iter()
494                .find(|entity| entity.entity.id == id)
495                .map(|entity| {
496                    format!(
497                        "character {} {}",
498                        Self::quoted(&Self::entity_name(&entity.entity)),
499                        id
500                    )
501                })
502                .unwrap_or_else(|| format!("character {}", id)),
503            ConsoleFocus::Item(id) => items
504                .iter()
505                .find(|item| item.item.id == id)
506                .map(|item| format!("item {} {}", Self::quoted(&Self::item_name(&item.item)), id))
507                .unwrap_or_else(|| format!("item {}", id)),
508        }
509    }
510
511    fn execute_command(
512        &mut self,
513        command: &str,
514        project: &Project,
515        server_ctx: &ServerContext,
516    ) -> String {
517        let trimmed = command.trim();
518        if trimmed.is_empty() {
519            return String::new();
520        }
521
522        if trimmed.eq_ignore_ascii_case("help") {
523            return [
524                "help  show available commands",
525                "list  list the current scope",
526                "focus <name|id>  focus a character or item from root",
527                "show  show the current character or item details",
528                "get <key>  show one attribute from the current character or item",
529                "pwd  show the current console focus",
530                "up  go back to root",
531                "clear  clear the console output",
532            ]
533            .join("\n");
534        }
535
536        if trimmed.eq_ignore_ascii_case("clear") {
537            self.transcript.clear();
538            return String::new();
539        }
540
541        let (entities, items) = match Self::runtime_snapshot(project, server_ctx) {
542            Ok(snapshot) => snapshot,
543            Err(err) => return err,
544        };
545        let focusable_items = Self::collect_focusable_items(&entities, &items);
546
547        match trimmed.split_once(' ') {
548            Some((head, tail))
549                if head.eq_ignore_ascii_case("focus") || head.eq_ignore_ascii_case("cd") =>
550            {
551                let needle = tail.trim();
552                if needle.is_empty() {
553                    return format!("Usage: {} <name|id>", head);
554                }
555                if needle == ".." || needle.eq_ignore_ascii_case("root") || needle == "/" {
556                    self.focus = ConsoleFocus::Root;
557                    return self.list_root(&entities, &items);
558                }
559
560                if let Some(id) = Self::parse_id(needle) {
561                    if entities.iter().any(|entity| entity.entity.id == id) {
562                        self.focus = ConsoleFocus::Entity(id);
563                        if let Some(entity) = entities.iter().find(|entity| entity.entity.id == id)
564                        {
565                            return self.list_entity(entity);
566                        }
567                    }
568                    if focusable_items.iter().any(|item| item.item.id == id) {
569                        self.focus = ConsoleFocus::Item(id);
570                        if let Some(item) = focusable_items.iter().find(|item| item.item.id == id) {
571                            return self.list_item(item);
572                        }
573                    }
574                }
575
576                let matching_entities: Vec<&RuntimeEntity> = entities
577                    .iter()
578                    .filter(|entity| Self::entity_matches(&entity.entity, needle))
579                    .collect();
580                let matching_items: Vec<&RuntimeItem> = focusable_items
581                    .iter()
582                    .filter(|item| Self::item_matches(&item.item, needle))
583                    .collect();
584
585                if matching_entities.len() + matching_items.len() > 1 {
586                    let mut lines = vec!["Multiple matches".to_string()];
587                    for entity in matching_entities {
588                        lines.push(format!(
589                            "character  {}  {}",
590                            Self::quoted(&Self::entity_name(&entity.entity)),
591                            entity.entity.id
592                        ));
593                    }
594                    for item in matching_items {
595                        lines.push(format!(
596                            "item       {}  {}",
597                            Self::quoted(&Self::item_name(&item.item)),
598                            item.item.id
599                        ));
600                    }
601                    return lines.join("\n");
602                }
603
604                if let Some(entity) = matching_entities.first() {
605                    self.focus = ConsoleFocus::Entity(entity.entity.id);
606                    return self.list_entity(entity);
607                }
608                if let Some(item) = matching_items.first() {
609                    self.focus = ConsoleFocus::Item(item.item.id);
610                    return self.list_item(item);
611                }
612
613                format!("No runtime character or item matched `{}`.", needle)
614            }
615            Some((head, tail)) if head.eq_ignore_ascii_case("get") => {
616                let key = tail.trim();
617                if key.is_empty() {
618                    return "Usage: get <key>".to_string();
619                }
620                match self.focus {
621                    ConsoleFocus::Entity(_) => {
622                        if let Some(entity) = self.focused_entity(&entities) {
623                            if let Some(value) = entity.entity.attributes.get(key) {
624                                format!("{} = {}", key, Self::format_value(value))
625                            } else {
626                                format!("Attribute `{}` not found.", key)
627                            }
628                        } else {
629                            self.focus = ConsoleFocus::Root;
630                            "Focused character no longer exists.".to_string()
631                        }
632                    }
633                    ConsoleFocus::Item(_) => {
634                        if let Some(item) = self.focused_item(&items) {
635                            if let Some(item) = focusable_items
636                                .iter()
637                                .find(|candidate| candidate.item.id == item.item.id)
638                            {
639                                if let Some(value) = item.item.attributes.get(key) {
640                                    format!("{} = {}", key, Self::format_value(value))
641                                } else {
642                                    format!("Attribute `{}` not found.", key)
643                                }
644                            } else {
645                                self.focus = ConsoleFocus::Root;
646                                "Focused item no longer exists.".to_string()
647                            }
648                        } else if let Some(item) = focusable_items
649                            .iter()
650                            .find(|candidate| matches!(self.focus, ConsoleFocus::Item(id) if candidate.item.id == id))
651                        {
652                            if let Some(value) = item.item.attributes.get(key) {
653                                format!("{} = {}", key, Self::format_value(value))
654                            } else {
655                                format!("Attribute `{}` not found.", key)
656                            }
657                        } else {
658                            self.focus = ConsoleFocus::Root;
659                            "Focused item no longer exists.".to_string()
660                        }
661                    }
662                    ConsoleFocus::Root => "Focus a character or item first.".to_string(),
663                }
664            }
665            _ => match trimmed.to_ascii_lowercase().as_str() {
666                "ls" => "Use `list`.".to_string(),
667                "cd .." => {
668                    self.focus = ConsoleFocus::Root;
669                    self.list_root(&entities, &items)
670                }
671                "list" => match self.focus {
672                    ConsoleFocus::Root => self.list_root(&entities, &items),
673                    ConsoleFocus::Entity(_) => {
674                        if let Some(entity) = self.focused_entity(&entities) {
675                            self.list_entity(entity)
676                        } else {
677                            self.focus = ConsoleFocus::Root;
678                            "Focused character no longer exists.".to_string()
679                        }
680                    }
681                    ConsoleFocus::Item(_) => {
682                        if let Some(item) = focusable_items
683                            .iter()
684                            .find(|candidate| matches!(self.focus, ConsoleFocus::Item(id) if candidate.item.id == id))
685                        {
686                            self.list_item(item)
687                        } else {
688                            self.focus = ConsoleFocus::Root;
689                            "Focused item no longer exists.".to_string()
690                        }
691                    }
692                },
693                "show" | "info" => match self.focus {
694                    ConsoleFocus::Root => self.list_root(&entities, &items),
695                    ConsoleFocus::Entity(_) => {
696                        if let Some(entity) = self.focused_entity(&entities) {
697                            self.list_entity(entity)
698                        } else {
699                            self.focus = ConsoleFocus::Root;
700                            "Focused character no longer exists.".to_string()
701                        }
702                    }
703                    ConsoleFocus::Item(_) => {
704                        if let Some(item) = focusable_items
705                            .iter()
706                            .find(|candidate| matches!(self.focus, ConsoleFocus::Item(id) if candidate.item.id == id))
707                        {
708                            self.list_item(item)
709                        } else {
710                            self.focus = ConsoleFocus::Root;
711                            "Focused item no longer exists.".to_string()
712                        }
713                    }
714                },
715                "pwd" => self.focus_label(&entities, &items),
716                "up" => {
717                    self.focus = ConsoleFocus::Root;
718                    self.list_root(&entities, &items)
719                }
720                _ => format!("Unknown command `{}`. Type `help`.", trimmed),
721            },
722        }
723    }
724}
725
726impl Dock for ConsoleDock {
727    fn new() -> Self
728    where
729        Self: Sized,
730    {
731        Self {
732            transcript: Self::intro(),
733            focus: ConsoleFocus::Root,
734        }
735    }
736
737    fn setup(&mut self, _ctx: &mut TheContext) -> TheCanvas {
738        let mut canvas = TheCanvas::new();
739
740        let mut output = TheTextAreaEdit::new(TheId::named("Console Output"));
741        if let Some(bytes) = crate::Embedded::get("parser/gruvbox-dark.tmTheme")
742            && let Ok(source) = std::str::from_utf8(bytes.data.as_ref())
743        {
744            output.add_theme_from_string(source);
745            output.set_code_theme("Gruvbox Dark");
746        }
747        if let Some(bytes) = crate::Embedded::get("parser/console.sublime-syntax")
748            && let Ok(source) = std::str::from_utf8(bytes.data.as_ref())
749        {
750            output.add_syntax_from_string(source);
751            output.set_code_type("Eldiron Console");
752        }
753        output.set_font_size(13.0);
754        output.set_continuous(true);
755        output.display_line_number(false);
756        output.use_global_statusbar(true);
757        output.readonly(true);
758        output.set_supports_undo(false);
759        canvas.set_widget(output);
760
761        let mut input_canvas = TheCanvas::default();
762        let mut input = TheTextLineEdit::new(TheId::named("Console Input"));
763        input.set_status_text("Enter a console command and press Return.");
764        input.set_font_size(12.5);
765        input.limiter_mut().set_max_height(24);
766        input_canvas.set_widget(input);
767        canvas.set_bottom(input_canvas);
768
769        canvas
770    }
771
772    fn activate(
773        &mut self,
774        ui: &mut TheUI,
775        ctx: &mut TheContext,
776        _project: &Project,
777        _server_ctx: &mut ServerContext,
778    ) {
779        if self.transcript.is_empty() {
780            self.transcript = Self::intro();
781        }
782        self.sync_output(ui, ctx);
783        self.set_input(ui, ctx, "");
784        if let Some(id) = Self::console_input_id(ui) {
785            ctx.ui.set_focus(&id);
786        }
787    }
788
789    fn handle_event(
790        &mut self,
791        event: &TheEvent,
792        ui: &mut TheUI,
793        ctx: &mut TheContext,
794        project: &mut Project,
795        server_ctx: &mut ServerContext,
796    ) -> bool {
797        if let TheEvent::ValueChanged(id, value) = event
798            && id.name == "Console Input"
799        {
800            let command = value.to_string().unwrap_or_default();
801            let command = command.trim().to_string();
802            if command.is_empty() {
803                self.set_input(ui, ctx, "");
804                return false;
805            }
806
807            let mut output = format!("{} > {}", self.prompt(project, server_ctx), command);
808            let result = self.execute_command(&command, project, server_ctx);
809            if !result.is_empty() {
810                output.push('\n');
811                output.push_str(&result);
812            }
813            self.set_output(output, ui, ctx);
814            self.clear_input(ui);
815            if let Some(focus_id) = Self::console_input_id(ui) {
816                ctx.ui.focus = Some(focus_id.clone());
817                ctx.ui.keyboard_focus = Some(focus_id.clone());
818                ctx.ui.send(TheEvent::GainedFocus(focus_id));
819                ui.process_events(ctx);
820            }
821            return true;
822        }
823
824        false
825    }
826
827    fn supports_actions(&self) -> bool {
828        false
829    }
830}