1use crossterm::{
2 event::KeyCode,
3 style::{Attribute, Color, ContentStyle, Stylize},
4 terminal,
5};
6
7use crate::{run, status_bar::StatusBar, StatusBarLayout, StatusBarLayoutItem};
8
9#[derive(Clone, PartialEq)]
11pub enum CommandType {
12 Colon(String),
14 Key(KeyCode),
16}
17
18#[derive(Clone)]
20pub struct Command {
21 pub cmd: Vec<CommandType>,
23 pub desc: String,
25 pub func: &'static dyn Fn(&mut State) -> bool,
27}
28
29pub struct CommandList(pub Vec<Command>);
31
32impl From<CommandList> for Vec<Command> {
33 fn from(val: CommandList) -> Self {
34 val.0
35 }
36}
37
38impl CommandList {
39 pub fn combine<T>(list: Vec<T>) -> Self
41 where
42 T: Into<Vec<Command>>,
43 {
44 let mut v = vec![];
45 for item in list {
46 v.append(&mut item.into());
47 }
48 Self(v)
49 }
50
51 pub fn quit() -> Self {
53 use CommandType::*;
54 Self(vec![Command {
55 cmd: vec![Key(KeyCode::Char('q')), Colon("quit".to_string())],
56 desc: "Quit".to_string(),
57 func: &|state: &mut State| {
58 state.quit();
59 false
60 },
61 }])
62 }
63
64 pub fn navigation() -> Self {
68 use CommandType::*;
69 Self(vec![
70 Command {
71 cmd: vec![Key(KeyCode::Up)],
72 desc: "Cursor up".to_string(),
73 func: &|state: &mut State| state.up(),
74 },
75 Command {
76 cmd: vec![Key(KeyCode::Down)],
77 desc: "Cursor down".to_string(),
78 func: &|state: &mut State| state.down(),
79 },
80 Command {
81 cmd: vec![Key(KeyCode::Left)],
82 desc: "Cursor left".to_string(),
83 func: &|state: &mut State| state.left(),
84 },
85 Command {
86 cmd: vec![Key(KeyCode::Right)],
87 desc: "Cursor right".to_string(),
88 func: &|state: &mut State| state.right(),
89 },
90 Command {
91 cmd: vec![Key(KeyCode::Home), Key(KeyCode::Char('g'))],
92 desc: "Go to start".to_string(),
93 func: &|state: &mut State| state.home(),
94 },
95 Command {
96 cmd: vec![Key(KeyCode::End), Key(KeyCode::Char('G'))],
97 desc: "Go to end".to_string(),
98 func: &|state: &mut State| state.end(),
99 },
100 Command {
101 cmd: vec![Key(KeyCode::PageUp)],
102 desc: "One page up".to_string(),
103 func: &|state: &mut State| state.pgup(),
104 },
105 Command {
106 cmd: vec![Key(KeyCode::PageDown)],
107 desc: "One page down".to_string(),
108 func: &|state: &mut State| state.pgdown(),
109 },
110 ])
111 }
112
113 pub fn help() -> Self {
115 use CommandType::*;
116 Self(vec![Command {
117 cmd: vec![Key(KeyCode::Char('h')), Colon("help".to_string())],
118 desc: "Toggles help text visiblity".to_string(),
119 func: &|state: &mut State| {
120 let theme = ContentStyle::new()
121 .with(Color::Black)
122 .on(Color::White)
123 .attribute(Attribute::Bold);
124 let commands =
125 CommandList::combine(vec![CommandList::quit(), CommandList::navigation()]);
126
127 let mut help = State {
128 pos: (0, 0),
129 size: state.size,
130 content: state.get_help_text(),
131 status_bar: StatusBar {
132 line_layouts: vec![StatusBarLayout {
133 left: vec![StatusBarLayoutItem::Text("Quit (q)".to_owned())],
134 right: vec![],
135 }],
136 title: "Help text".to_owned(),
137 theme,
138 },
139 commands,
140 running: true,
141 show_line_numbers: false,
142 word_wrap: false,
143 word_wrap_option: textwrap::Options::new(0),
144 };
145 run(&mut help).unwrap();
146 true
147 },
148 }])
149 }
150
151 pub fn toggle_line_numbers() -> Self {
153 use CommandType::*;
154 Self(vec![Command {
155 cmd: vec![Key(KeyCode::Char('l'))],
156 desc: "Show/Hide line numbers".to_string(),
157 func: &|state: &mut State| {
158 state.show_line_numbers = !state.show_line_numbers;
159 true
160 },
161 }])
162 }
163
164 pub fn toggle_word_wrap() -> Self {
166 use CommandType::*;
167 Self(vec![Command {
168 cmd: vec![Key(KeyCode::Char('w'))],
169 desc: "Activate/Deactivate word wrap".to_string(),
170 func: &|state: &mut State| {
171 state.word_wrap = !state.word_wrap;
172
173 true
174 },
175 }])
176 }
177}
178
179impl Default for CommandList {
180 fn default() -> Self {
181 Self::combine(vec![
182 Self::quit(),
183 Self::navigation(),
184 Self::help(),
185 Self::toggle_line_numbers(),
186 Self::toggle_word_wrap(),
187 ])
188 }
189}
190
191pub struct State<'a> {
193 pub pos: (usize, usize),
197
198 pub size: (u16, u16),
202
203 pub content: String,
205
206 pub status_bar: StatusBar,
208
209 pub commands: CommandList,
211
212 pub(crate) running: bool,
213
214 pub show_line_numbers: bool,
216
217 pub word_wrap: bool,
219
220 pub word_wrap_option: textwrap::Options<'a>,
224}
225
226impl<'a> State<'a> {
227 pub fn new(
229 content: String,
230 status_bar: StatusBar,
231 commands: CommandList,
232 ) -> std::io::Result<Self> {
233 Ok(Self {
234 pos: (0, 0),
235 size: terminal::size()?,
236 content,
237 status_bar,
238 commands,
239 running: true,
240 show_line_numbers: true,
241 word_wrap: false,
242 word_wrap_option: textwrap::Options::new(0),
243 })
244 }
245
246 pub fn is_running(&self) -> bool {
248 self.running
249 }
250
251 pub fn quit(&mut self) {
253 self.running = false;
254 }
255
256 pub fn get_help_text(&self) -> String {
258 if self.commands.0.is_empty() {
259 return String::from("No commands");
260 }
261 let items = self.commands.0.iter().map(|command| {
262 let name = command
263 .cmd
264 .iter()
265 .map(|cmd_type| match cmd_type {
266 CommandType::Key(code) => match *code {
267 KeyCode::Backspace => "Backspace".to_string(),
268 KeyCode::Enter => "Enter".to_string(),
269 KeyCode::Left => "Left".to_string(),
270 KeyCode::Right => "Right".to_string(),
271 KeyCode::Up => "Up".to_string(),
272 KeyCode::Down => "Down".to_string(),
273 KeyCode::Home => "Home".to_string(),
274 KeyCode::End => "End".to_string(),
275 KeyCode::PageUp => "PageUp".to_string(),
276 KeyCode::PageDown => "PageDown".to_string(),
277 KeyCode::Tab => "Tab".to_string(),
278 KeyCode::BackTab => "BackTab".to_string(),
279 KeyCode::Delete => "Delete".to_string(),
280 KeyCode::Insert => "Insert".to_string(),
281 KeyCode::F(n) => format!("F{}", n),
282 KeyCode::Char(c) => c.to_string(),
283 KeyCode::Null => "Null".to_string(),
284 KeyCode::Esc => "Esc".to_string(),
285 KeyCode::CapsLock => "CapsLock".to_string(),
286 KeyCode::NumLock => "NumLock".to_string(),
287 KeyCode::ScrollLock => "ScrollLock".to_string(),
288 KeyCode::PrintScreen => "PrintScreen".to_string(),
289 KeyCode::Pause => "Pause".to_string(),
290 KeyCode::Menu => "Menu".to_string(),
291 KeyCode::KeypadBegin => "KeypadBegin".to_string(),
292 KeyCode::Media(_) => "MediaKey".to_string(),
293 KeyCode::Modifier(_) => "ModifierKey".to_string(),
294 },
295 CommandType::Colon(s) => format!(":{}", s),
296 })
297 .collect::<Vec<String>>()
298 .join(", ");
299 (name, command.desc.clone())
300 });
301 let max_name_len = items.clone().map(|item| item.0.len()).max().unwrap();
302 let padding = max_name_len + 2;
303
304 items
305 .map(|(name, desc)| {
306 let name_len = name.len();
307 format!(
308 "{}{gap}{}",
309 name,
310 desc.lines()
311 .collect::<Vec<&str>>()
312 .join(format!("\n{}", " ".repeat(padding)).as_str()),
313 gap = " ".repeat(padding - name_len),
314 )
315 })
316 .collect::<Vec<String>>()
317 .join("\n\n")
318 }
319}
320
321impl<'a> State<'a> {
322 fn get_line_inducator(
324 &self,
325 line_number: usize,
326 max_line_number_width: usize,
327 blank: bool,
328 ) -> String {
329 if self.show_line_numbers {
330 let content = if blank {
331 String::from(" ")
332 } else {
333 line_number.to_string()
334 };
335
336 format!(
337 "{:line_count$}│",
338 content,
339 line_count = max_line_number_width
340 )
341 } else {
342 String::new()
343 }
344 }
345
346 pub fn get_visible(&self) -> String {
348 let max_line_number_width = self.content.lines().count().to_string().len();
349
350 let line_indicator_len = max_line_number_width + 1;
351
352 let lines: Box<dyn Iterator<Item = (usize, String)>> = match &self.word_wrap {
353 true => Box::new(self.content.lines().enumerate().flat_map(|(index, line)| {
354 let option = self
355 .word_wrap_option
356 .clone()
357 .width(self.size.0 as usize - line_indicator_len);
358 textwrap::wrap(line, option)
359 .into_iter()
360 .map(move |vline| (index, vline.to_string()))
361 })),
362 false => Box::new(
363 self.content
364 .lines()
365 .enumerate()
366 .map(|(index, line)| (index, line.to_owned())),
367 ),
368 };
369
370 let mut last_index: usize = usize::MAX;
371
372 lines
373 .skip(self.pos.1)
374 .take(self.size.1 as usize - self.status_bar.line_layouts.len())
375 .map(|(index, line)| -> String {
376 let line = format!(
377 "{line_indicator}{visible_content_line}",
378 line_indicator = self.get_line_inducator(
379 index + 1,
380 max_line_number_width,
381 last_index == index
382 ),
383 visible_content_line = line
384 .chars()
385 .skip(self.pos.0)
386 .take(self.size.0 as usize - line_indicator_len)
387 .collect::<String>()
388 );
389 last_index = index;
390 line
391 })
392 .collect::<Vec<String>>()
393 .join("\n")
394 }
395}
396
397impl<'a> State<'a> {
398 pub fn up(&mut self) -> bool {
400 if self.pos.1 != 0 {
401 self.pos.1 -= 1;
402 return true;
403 }
404 false
405 }
406
407 pub fn down(&mut self) -> bool {
409 if self.pos.1 != self.content.lines().count() - 1 {
410 self.pos.1 += 1;
411 return true;
412 }
413 false
414 }
415
416 pub fn left(&mut self) -> bool {
418 let amount = self.size.0 as usize / 2;
419 if self.pos.0 >= amount {
420 self.pos.0 -= amount;
421 return true;
422 } else if self.pos.0 != 0 {
423 self.pos.0 = 0;
424 return true;
425 }
426 false
427 }
428
429 pub fn right(&mut self) -> bool {
431 let amount = self.size.0 as usize / 2;
432 self.pos.0 += amount;
433 true
434 }
435
436 pub fn pgup(&mut self) -> bool {
438 if self.pos.1 >= self.size.1 as usize {
439 self.pos.1 -= self.size.1 as usize - 1;
440 return true;
441 } else if self.pos.1 != 0 {
442 self.pos.1 = 0;
443 return true;
444 }
445 false
446 }
447
448 pub fn pgdown(&mut self) -> bool {
450 let new = (self.pos.1 + self.size.1 as usize).min(self.content.lines().count()) - 1;
451 if new != self.pos.1 {
452 self.pos.1 = new;
453 return true;
454 }
455 false
456 }
457
458 pub fn home(&mut self) -> bool {
460 if self.pos.1 > 0 {
461 self.pos.1 = 0;
462 return true;
463 }
464 false
465 }
466
467 pub fn end(&mut self) -> bool {
469 let line_count = self.content.lines().count();
470 self.pos.1 = if line_count > self.size.1 as usize {
471 line_count - self.size.1 as usize + 1
472 } else {
473 0
474 };
475 true
476 }
477}
478
479impl<'a> State<'a> {
480 pub fn match_key_event(&mut self, code: KeyCode) -> bool {
482 let mut commands = self.commands.0.clone();
483 let found = commands
484 .iter_mut()
485 .find(|command| command.cmd.contains(&CommandType::Key(code)));
486 if let Some(Command { func, .. }) = found {
487 return func(self);
488 }
489 false
490 }
491}