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 ®ion.map.entities {
477 runtime_entities.push(RuntimeEntity {
478 entity: entity.clone(),
479 });
480 }
481 for item in ®ion.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}