1use std::{collections::HashMap, time::Duration, io::Write};
2
3use crossterm::{QueueableCommand, cursor, style::Stylize, terminal};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 Mapping, Mappings,
8 error::LeadrError,
9 ui::{
10 area::{Area, ColumnLayout},
11 entry::Entry,
12 symbols::Symbols,
13 theme::Theme,
14 },
15};
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub enum BorderType {
19 Rounded,
20 Square,
21 Top,
22 None,
23}
24
25#[derive(Clone, Debug, Serialize, Deserialize)]
26pub struct Config {
27 pub border_type: BorderType,
28 pub column_layout: ColumnLayout,
29 pub enabled: bool,
30 pub height: u16,
31 pub padding: u16,
32 pub symbols: Symbols,
33 pub timeout: Duration,
34}
35
36impl std::default::Default for Config {
37 fn default() -> Self {
38 Self {
39 border_type: BorderType::Rounded,
40 column_layout: ColumnLayout::default(),
41 enabled: true,
42 height: 10,
43 padding: 2,
44 symbols: Symbols::default(),
45 timeout: Duration::from_millis(500),
46 }
47 }
48}
49
50pub struct Panel {
51 pub config: Config,
52 pub theme: Theme,
53 scroll_up: u16,
54}
55
56impl Panel {
57 pub fn try_new(config: Config, theme: Theme) -> Result<Self, LeadrError> {
58 let mut tty = std::fs::OpenOptions::new().write(true).open("/dev/tty")?;
59
60 let (_cols, rows) = terminal::size()?;
61 let cursor_line = std::env::var("LEADR_CURSOR_LINE")?.parse::<u16>()?;
62
63 let lines_below = rows.saturating_sub(cursor_line);
64 let scroll_up = config.height.saturating_sub(lines_below);
65
66 if scroll_up > 0 {
67 tty.queue(terminal::ScrollUp(scroll_up))?
68 .queue(cursor::MoveUp(scroll_up))?;
69 }
70
71 tty.flush()?;
72
73 Ok(Self {
74 config,
75 theme,
76 scroll_up,
77 })
78 }
79
80 pub fn clear(&self) -> std::io::Result<()> {
81 let mut stdout = std::fs::OpenOptions::new().write(true).open("/dev/tty")?;
82 let (_cols, rows) = terminal::size()?;
83 let start_y = rows.saturating_sub(self.config.height);
84
85 stdout
86 .queue(cursor::SavePosition)?
87 .queue(cursor::MoveTo(0, start_y))?
88 .queue(terminal::Clear(terminal::ClearType::FromCursorDown))?
89 .queue(cursor::RestorePosition)?;
90
91 if self.scroll_up > 0 {
92 stdout
93 .queue(terminal::ScrollDown(self.scroll_up))?
94 .queue(cursor::MoveDown(self.scroll_up))?;
95 }
96 stdout.flush()
97 }
98
99 pub fn draw(&self, sequence: &str, mappings: &Mappings) -> Result<(), LeadrError> {
100 let mut tty = std::fs::OpenOptions::new().write(true).open("/dev/tty")?;
101 let (cols, rows) = terminal::size()?;
102 let start_y = rows.saturating_sub(self.config.height);
103
104 let outer_area = Area {
105 x: self.config.padding,
106 y: start_y,
107 width: cols.saturating_sub(2 * self.config.padding),
108 height: self.config.height,
109 };
110
111 tty.queue(cursor::SavePosition)?;
112 self.draw_border(&mut tty, &outer_area)?;
113 let border_width = 1;
114 let footer_height = 2;
115
116 let next_options = mappings.grouped_next_options(sequence);
117 let mut keys: Vec<_> = next_options.keys().collect();
118 keys.sort();
119 let entry_area = Area {
120 x: outer_area.x + 1,
121 y: outer_area.y + 1,
122 width: outer_area.width.saturating_sub(2 * border_width),
123 height: outer_area
124 .height
125 .saturating_sub(2 * border_width + footer_height),
126 };
127
128 let required_num_columns = (keys.len() as f64 / entry_area.height as f64).ceil() as u16;
129 let columns =
130 entry_area.split_horizontally(&self.config.column_layout, &required_num_columns);
131 for (i, column) in columns.iter().enumerate() {
132 let column_keys = keys
133 .iter()
134 .skip(i * column.height as usize)
135 .take(column.height as usize)
136 .cloned()
137 .collect::<Vec<_>>();
138 self.draw_entries(&mut tty, column, &next_options, &column_keys)?;
139 }
140
141 let footer_area = Area {
142 x: outer_area.x + 1,
143 y: outer_area.y + outer_area.height - footer_height,
144 width: outer_area.width.saturating_sub(2 * border_width),
145 height: footer_height,
146 };
147 self.draw_footer(&mut tty, &footer_area, sequence)?;
148 tty.queue(cursor::RestorePosition)?;
149
150 Ok(())
151 }
152
153 fn draw_border(&self, tty: &mut std::fs::File, area: &Area) -> std::io::Result<()> {
154 let (top_left, top_right, bottom_left, bottom_right, horizontal, vertical) =
155 match self.config.border_type {
156 BorderType::Rounded => ('╭', '╮', '╰', '╯', '─', '│'),
157 BorderType::Square => ('┌', '┐', '└', '┘', '─', '│'),
158 BorderType::Top => ('─', '─', ' ', ' ', '─', ' '),
159 BorderType::None => (' ', ' ', ' ', ' ', ' ', ' '),
160 };
161
162 let inner_width = area.width.saturating_sub(2);
163 let horizontal_line = horizontal.to_string().repeat(inner_width.into());
164
165 tty.queue(cursor::MoveTo(area.x, area.y))?;
166
167 if !matches!(self.config.border_type, BorderType::None) {
169 let top = format!(
170 "{tl}{line}{tr}",
171 line = horizontal_line,
172 tl = top_left,
173 tr = top_right,
174 )
175 .with(self.theme.accent.into())
176 .on(self.theme.background.into());
177 write!(tty, "{}", top)?;
178 }
179
180 for i in 1..self.config.height {
182 tty.queue(cursor::MoveTo(area.x, area.y + i))?;
183 let line = format!(
184 "{vl}{space}{vr}",
185 space = " ".repeat(inner_width.into()),
186 vl = vertical,
187 vr = vertical
188 )
189 .with(self.theme.accent.into())
190 .on(self.theme.background.into());
191 write!(tty, "{}", line)?;
192 }
193
194 if matches!(
196 self.config.border_type,
197 BorderType::Rounded | BorderType::Square
198 ) {
199 tty.queue(cursor::MoveTo(area.x, area.y + area.height))?;
200 let bottom = format!(
201 "{bl}{line}{br}",
202 line = horizontal_line,
203 bl = bottom_left,
204 br = bottom_right
205 )
206 .with(self.theme.accent.into())
207 .on(self.theme.background.into());
208 write!(tty, "{}", bottom)?;
209 }
210
211 tty.flush()?;
212
213 Ok(())
214 }
215
216 pub fn draw_entries(
217 &self,
218 tty: &mut std::fs::File,
219 area: &Area,
220 next_options_map: &HashMap<String, Vec<&Mapping>>,
221 keys: &Vec<&String>,
222 ) -> std::io::Result<()> {
223 let mut line = area.y;
224
225 for key in keys {
226 if line >= area.y + area.height {
227 break; }
229 tty.queue(cursor::MoveTo(area.x, line))?;
230
231 let mappings = &next_options_map[*key];
232 let stylized_entry = Entry::new(
233 key,
234 mappings,
235 area.width,
236 &self.config.symbols,
237 &self.theme,
238 );
239 stylized_entry.to_tty(tty)?;
240
241 line += 1;
242 }
243
244 Ok(())
245 }
246
247 pub fn draw_footer(
248 &self,
249 tty: &mut std::fs::File,
250 area: &Area,
251 sequence: &str,
252 ) -> std::io::Result<()> {
253 let help_text = " close back";
254 let styled_help_text = help_text
255 .with(self.theme.text_primary.into())
256 .on(self.theme.background.into());
257 let center_x = area.x + (area.width.saturating_sub(help_text.chars().count() as u16)) / 2;
258 tty.queue(cursor::MoveTo(center_x, area.y))?;
259 write!(tty, "{}", styled_help_text)?;
260
261 tty.queue(cursor::MoveTo(area.x, area.y))?;
262 let arrow = self
263 .config
264 .symbols
265 .sequence_begin
266 .to_string()
267 .with(self.theme.text_secondary.into())
268 .on(self.theme.background.into());
269 write!(tty, "{}", arrow)?;
270 let sequence_text = sequence
271 .to_string()
272 .with(self.theme.text_primary.into())
273 .on(self.theme.background.into());
274 write!(tty, "{}", sequence_text)?;
275
276 Ok(())
277 }
278}
279
280impl Drop for Panel {
281 fn drop(&mut self) {
282 let _ = self.clear();
283 }
284}
285