1use thedes_dev::{CommandContext, ScriptTable};
2use thedes_tui::{
3 core::{
4 App,
5 color::BasicColor,
6 event::{Event, Key, KeyEvent},
7 screen::{self, FlushError},
8 },
9 text,
10};
11use thiserror::Error;
12
13pub type KeyBindingMap = thedes_tui::key_bindings::KeyBindingMap<Command>;
14
15pub fn default_key_bindings() -> KeyBindingMap {
16 KeyBindingMap::new()
17 .with(Key::Esc, Command::Exit)
18 .with(Key::Enter, Command::RunPrevious)
19}
20
21#[derive(Debug, Error)]
22pub enum Error {
23 #[error("Failed to render message")]
24 RenderMessage(
25 #[source]
26 #[from]
27 text::Error,
28 ),
29 #[error("TUI cancelled")]
30 Cancelled,
31 #[error("Failed to run the script")]
32 Script(
33 #[from]
34 #[source]
35 thedes_dev::Error,
36 ),
37 #[error("Failed to flush canvas")]
38 FlushCanvas(
39 #[from]
40 #[source]
41 FlushError,
42 ),
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub enum Command {
47 Run(char),
48 RunPrevious,
49 Exit,
50}
51
52#[derive(Debug, Clone)]
53pub struct Component {
54 prev: char,
55 key_bindings: KeyBindingMap,
56}
57
58impl Component {
59 pub const DEFAULT_KEY: char = '.';
60
61 pub fn new() -> Self {
62 Self { prev: Self::DEFAULT_KEY, key_bindings: default_key_bindings() }
63 }
64
65 pub fn with_keybindings(mut self, map: KeyBindingMap) -> Self {
66 self.set_keybindings(map);
67 self
68 }
69
70 pub fn set_keybindings(&mut self, map: KeyBindingMap) -> &mut Self {
71 self.key_bindings = map;
72 self
73 }
74
75 pub async fn run(
76 &mut self,
77 app: &mut App,
78 context: &mut CommandContext<'_, '_>,
79 ) -> Result<(), Error> {
80 loop {
81 match self.handle_input(app, context).await {
82 Ok(false) => break,
83 Ok(true) => (),
84 Err(Error::Script(e))
85 if matches!(
86 e.kind(),
87 thedes_dev::ErrorKind::UnknownKey(_)
88 ) =>
89 {
90 ()
91 },
92 Err(e) => Err(e)?,
93 }
94 self.render(app)?;
95 tokio::select! {
96 _ = app.tick_session.tick() => (),
97 _ = app.cancel_token.cancelled() => Err(Error::Cancelled)?,
98 }
99 }
100 Ok(())
101 }
102
103 fn render(&mut self, app: &mut App) -> Result<(), Error> {
104 app.canvas
105 .queue([screen::Command::ClearScreen(BasicColor::Black.into())]);
106 text::styled(
107 app,
108 "Development Script Mode",
109 &text::Style::default().with_top_margin(1).with_align(1, 2),
110 )?;
111 text::styled(
112 app,
113 "Press any character key to run a corresponding script.",
114 &text::Style::default().with_top_margin(4).with_align(1, 2),
115 )?;
116 text::styled(
117 app,
118 "Press enter to run previous script or the default one.",
119 &text::Style::default().with_top_margin(5).with_align(1, 2),
120 )?;
121 text::styled(
122 app,
123 "Press ESC to cancel.",
124 &text::Style::default().with_top_margin(7).with_align(1, 2),
125 )?;
126 app.canvas.flush()?;
127 Ok(())
128 }
129
130 async fn handle_input(
131 &mut self,
132 app: &mut App,
133 context: &mut CommandContext<'_, '_>,
134 ) -> Result<bool, Error> {
135 let Ok(mut events) = app.events.read_until_now() else {
136 Err(Error::Cancelled)?
137 };
138
139 let mut should_continue = true;
140 let mut hit = false;
141 while let Some(event) = events.next().filter(|_| should_continue) {
142 let Event::Key(key) = event else { continue };
143
144 let command = self.key_bindings.command_for(key).cloned();
145
146 should_continue = match command {
147 Some(command) => {
148 hit = true;
149 self.run_command(command, context).await?
150 },
151 None => match key {
152 KeyEvent {
153 main_key: Key::Char(ch),
154 ctrl: false,
155 alt: false,
156 shift: false,
157 } => {
158 hit = true;
159 self.run_command(Command::Run(ch), context).await?
160 },
161 _ => true,
162 },
163 };
164 }
165 Ok(!hit)
166 }
167
168 async fn run_command(
169 &mut self,
170 command: Command,
171 context: &mut CommandContext<'_, '_>,
172 ) -> Result<bool, Error> {
173 match command {
174 Command::Run(ch) => ScriptTable::run_reading(ch, context).await?,
175 Command::RunPrevious => {
176 ScriptTable::run_reading(self.prev, context).await?
177 },
178 Command::Exit => return Ok(false),
179 }
180 Ok(true)
181 }
182}