1use std::cell::Cell;
12
13use rich::color::ColorType;
14use rich::{Color, ColorTriplet, Segment, Style};
15
16use crate::component::{Component, Context, Flow, View};
17use crate::components::{Theme, fit, highlight, plain, question, text};
18use crate::event::{Button, Event, Key, KeyCode, Modifiers, MouseKind};
19use crate::fuzzy::rank;
20use crate::keymap::{Keymap, keys};
21use crate::names::COLORS;
22use crate::policy::{LineIo, NotInteractive};
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
26pub enum ColorFormat {
27 #[default]
29 Hex,
30 Name,
33 Rgb,
35}
36
37impl ColorFormat {
38 pub fn parse(name: &str) -> Option<ColorFormat> {
39 Some(match name {
40 "hex" => ColorFormat::Hex,
41 "name" => ColorFormat::Name,
42 "rgb" => ColorFormat::Rgb,
43 _ => return None,
44 })
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50struct Picked {
51 color: Color,
52 name: Option<String>,
53}
54
55impl Picked {
56 fn named(name: &str) -> Option<Picked> {
57 Some(Picked {
58 color: Color::parse(name).ok()?,
59 name: Some(name.to_string()),
60 })
61 }
62
63 fn parse(text: &str) -> Option<Picked> {
65 let text = text.trim();
66 let lower = text.to_lowercase();
67 if COLORS.contains(&lower.as_str()) {
68 return Picked::named(&lower);
69 }
70 let color = Color::parse(&lower).ok()?;
71 if color.kind == ColorType::Default {
72 return None;
73 }
74 Some(Picked { color, name: None })
75 }
76
77 fn palette(number: u8) -> Picked {
78 Picked {
79 color: Color::from_ansi(number),
80 name: None,
81 }
82 }
83
84 fn triplet(&self) -> ColorTriplet {
85 self.color
86 .get_truecolor()
87 .unwrap_or(ColorTriplet::new(0, 0, 0))
88 }
89
90 fn palette_name(&self) -> Option<&'static str> {
92 let number = match self.color.kind {
93 ColorType::Standard | ColorType::EightBit => self.color.number?,
94 _ => return None,
95 };
96 COLORS
97 .iter()
98 .copied()
99 .find(|name| Color::parse(name).is_ok_and(|color| color.number == Some(number)))
100 }
101
102 fn format(&self, format: ColorFormat) -> String {
103 let triplet = self.triplet();
104 match format {
105 ColorFormat::Hex => triplet.hex(),
106 ColorFormat::Rgb => format!("rgb({},{},{})", triplet.red, triplet.green, triplet.blue),
107 ColorFormat::Name => match (&self.name, self.palette_name(), self.color.number) {
108 (Some(name), _, _) => name.clone(),
109 (None, Some(name), _) => name.to_string(),
110 (None, None, Some(number)) if self.color.kind == ColorType::EightBit => {
111 format!("color({number})")
112 }
113 _ => triplet.hex(),
114 },
115 }
116 }
117
118 fn swatch(&self) -> Style {
120 Style::parse(&format!("on {}", self.triplet().hex())).expect("a hex colour")
121 }
122}
123
124#[must_use = "a component does nothing until it is run or mounted"]
126pub struct ColorPicker {
127 prompt: String,
128 query: String,
129 matches: Vec<(usize, Vec<usize>)>,
131 custom: Option<Picked>,
133 focus: usize,
134 offset: usize,
135 height: usize,
136 grid: bool,
137 cell: u8,
138 format: ColorFormat,
139 default: Option<String>,
140 theme: Theme,
141 mouse: bool,
142 clicked: Option<(bool, usize)>,
145 space: Cell<usize>,
147 answer: Option<Option<String>>,
148 rebound: Keymap,
150}
151
152const TOP: usize = 2;
154
155impl ColorPicker {
156 pub fn new(prompt: impl Into<String>) -> ColorPicker {
157 let mut picker = ColorPicker {
158 prompt: prompt.into(),
159 query: String::new(),
160 matches: Vec::new(),
161 custom: None,
162 focus: 0,
163 offset: 0,
164 height: 10,
165 grid: false,
166 cell: 0,
167 format: ColorFormat::Hex,
168 default: None,
169 theme: Theme::default(),
170 mouse: false,
171 clicked: None,
172 space: Cell::new(usize::MAX),
173 answer: None,
174 rebound: Keymap::new("color"),
175 };
176 picker.refilter();
177 picker
178 }
179
180 pub fn format(mut self, format: ColorFormat) -> Self {
181 self.format = format;
182 self
183 }
184
185 pub fn value(mut self, value: impl Into<String>) -> Self {
187 self.query = value.into();
188 self.refilter();
189 self
190 }
191
192 pub fn default(mut self, color: impl Into<String>) -> Self {
196 self.default = Some(color.into());
197 self
198 }
199
200 pub fn is_color(text: &str) -> bool {
203 Picked::parse(text).is_some()
204 }
205
206 fn valid_default(&self) -> Result<Option<String>, NotInteractive> {
208 let Some(default) = self.default.as_deref() else {
209 return Ok(None);
210 };
211 Picked::parse(default)
212 .map(|picked| Some(picked.format(self.format)))
213 .ok_or_else(|| {
214 NotInteractive::Invalid(format!("the default is not a colour: {default:?}"))
215 })
216 }
217
218 pub fn height(mut self, rows: usize) -> Self {
220 self.height = rows.max(1);
221 self
222 }
223
224 pub fn palette(mut self, on: bool) -> Self {
226 self.grid = on;
227 self
228 }
229
230 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
234 self.rebound.rebind(action, keys);
235 self
236 }
237
238 pub fn theme(mut self, theme: Theme) -> Self {
239 self.theme = theme;
240 self
241 }
242
243 pub fn with_mouse(mut self, on: bool) -> Self {
245 self.mouse = on;
246 self
247 }
248
249 fn refilter(&mut self) {
250 self.matches = rank(&self.query, COLORS.iter().copied())
251 .into_iter()
252 .map(|(index, found)| (index, found.positions))
253 .collect();
254 let exact = COLORS.contains(&self.query.trim().to_lowercase().as_str());
255 self.custom = Picked::parse(&self.query).filter(|_| !exact);
256 self.focus = 0;
257 self.offset = 0;
258 }
259
260 fn count(&self) -> usize {
261 self.matches.len() + usize::from(self.custom.is_some())
262 }
263
264 fn rows(&self) -> usize {
267 self.height
268 .min(self.space.get().saturating_sub(TOP + 1))
269 .max(1)
270 }
271
272 fn at(&self, position: usize) -> Option<Picked> {
274 match (&self.custom, position) {
275 (Some(custom), 0) => Some(custom.clone()),
276 (Some(_), position) => Picked::named(COLORS[self.matches.get(position - 1)?.0]),
277 (None, position) => Picked::named(COLORS[self.matches.get(position)?.0]),
278 }
279 }
280
281 fn focused(&self) -> Option<Picked> {
282 if self.grid {
283 Some(Picked::palette(self.cell))
284 } else {
285 self.at(self.focus)
286 }
287 }
288
289 fn step(&mut self, delta: isize) {
290 let count = self.count();
291 if count == 0 {
292 return;
293 }
294 self.focus = self.focus.saturating_add_signed(delta).min(count - 1);
295 if self.focus < self.offset {
296 self.offset = self.focus;
297 } else if self.focus >= self.offset + self.rows() {
298 self.offset = self.focus + 1 - self.rows();
299 }
300 }
301
302 fn pick(&mut self) -> Flow<String> {
303 match self.focused() {
304 Some(picked) => {
305 let answer = picked.format(self.format);
306 self.answer = Some(Some(answer.clone()));
307 Flow::Done(answer)
308 }
309 None => Flow::Continue,
310 }
311 }
312
313 fn move_cell(&mut self, columns: i16, rows: i16) {
314 let column = (i16::from(self.cell % 16) + columns).clamp(0, 15);
315 let row = (i16::from(self.cell / 16) + rows).clamp(0, 15);
316 self.cell = (row * 16 + column) as u8;
317 }
318
319 fn click(&mut self, column: usize, row: usize) -> Flow<String> {
320 let Some(row) = row.checked_sub(TOP) else {
321 return Flow::Continue;
322 };
323 if self.grid {
324 let column = column.saturating_sub(2) / 2;
325 if row < 16 && column < 16 {
326 let cell = (row * 16 + column) as u8;
327 if self.clicked == Some((true, cell.into())) && cell == self.cell {
328 return self.pick();
329 }
330 self.cell = cell;
331 self.clicked = Some((true, cell.into()));
332 }
333 } else if row < self.rows() {
334 let position = self.offset + row;
335 if position < self.count() {
336 if self.clicked == Some((false, position)) && position == self.focus {
337 return self.pick();
338 }
339 self.focus = position;
340 self.clicked = Some((false, position));
341 }
342 }
343 Flow::Continue
344 }
345
346 fn swatch_line(&self, width: usize) -> Vec<Segment> {
347 let theme = &self.theme;
348 let mut line = vec![plain(" ")];
349 match self.focused() {
350 Some(picked) => {
351 line.push(Segment::new(" ", Some(picked.swatch())));
352 let triplet = picked.triplet();
353 let mut about = format!(
354 " {} rgb({},{},{})",
355 triplet.hex(),
356 triplet.red,
357 triplet.green,
358 triplet.blue
359 );
360 if let Some(name) = picked.name.as_deref().or(picked.palette_name()) {
361 about.push_str(&format!(" {name}"));
362 }
363 if let Some(number) = picked.color.number {
364 about.push_str(&format!(" color({number})"));
365 }
366 line.push(text(about, &theme.hint));
367 }
368 None => line.push(text("no colour", &theme.hint)),
369 }
370 fit(line, width)
371 }
372
373 fn list(&self, width: usize) -> Vec<Vec<Segment>> {
374 let theme = &self.theme;
375 let mut lines = Vec::new();
376 for position in (self.offset..self.count()).take(self.rows()) {
377 let Some(picked) = self.at(position) else {
378 break;
379 };
380 let focused = position == self.focus;
381 let mut line = if focused {
382 vec![text(format!("{} ", theme.pointer), &theme.pointer_style)]
383 } else {
384 vec![plain(" ")]
385 };
386 line.push(Segment::new(" ", Some(picked.swatch())));
387 line.push(plain(" "));
388 let base = focused.then_some(&theme.focused);
389 match (&picked.name, self.custom.is_some() && position == 0) {
390 (_, true) => line.push(text(self.query.trim().to_string(), &theme.answer)),
391 (Some(name), false) => {
392 let positions = &self.matches[position - usize::from(self.custom.is_some())].1;
393 line.extend(highlight(name, positions, base, &theme.matched));
394 }
395 (None, false) => {}
396 }
397 line.push(text(format!(" {}", picked.triplet().hex()), &theme.hint));
398 lines.push(fit(line, width));
399 }
400 if self.count() == 0 {
401 lines.push(vec![text(" no matches", &theme.hint)]);
402 }
403 lines.resize_with(self.rows(), Vec::new);
404 lines
405 }
406
407 fn palette_grid(&self, width: usize) -> Vec<Vec<Segment>> {
408 let mut lines = Vec::with_capacity(16);
409 for row in 0..16u8 {
410 let mut line = vec![plain(" ")];
411 for column in 0..16u8 {
412 let number = row * 16 + column;
413 let picked = Picked::palette(number);
414 let mark = if number == self.cell {
415 let triplet = picked.triplet();
416 let light = u32::from(triplet.red) * 299
417 + u32::from(triplet.green) * 587
418 + u32::from(triplet.blue) * 114
419 > 128_000;
420 let ink = if light { "black" } else { "white" };
421 let style = Style::parse(&format!("bold {ink}"))
422 .expect("style")
423 .combine(&picked.swatch());
424 Segment::new("<>", Some(style))
425 } else {
426 Segment::new(" ", Some(picked.swatch()))
427 };
428 line.push(mark);
429 }
430 lines.push(fit(line, width));
431 }
432 lines
433 }
434}
435
436impl Component for ColorPicker {
437 type Output = String;
438
439 fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<String> {
440 self.space.set(context.height);
441 if let Event::Mouse(mouse) = event {
442 match mouse.kind {
443 MouseKind::Down(Button::Left) => {
444 return self.click(mouse.column as usize, mouse.row as usize);
445 }
446 MouseKind::ScrollUp if self.grid => self.move_cell(0, -1),
447 MouseKind::ScrollDown if self.grid => self.move_cell(0, 1),
448 MouseKind::ScrollUp => self.step(-1),
449 MouseKind::ScrollDown => self.step(1),
450 _ => {}
451 }
452 return Flow::Continue;
453 }
454 if let Event::Paste(pasted) = event {
455 self.clicked = None;
456 self.grid = false;
457 self.query.push_str(&crate::components::pasted(pasted, " "));
458 self.refilter();
459 return Flow::Continue;
460 }
461 let Some(key) = event.key() else {
462 return Flow::Continue;
463 };
464 self.clicked = None;
466 let keymap = self.keymap();
467 let action = keymap.action(key).or_else(|| {
470 let typing = matches!(key.code, KeyCode::Char(_));
471 (!typing && key.modifiers != Modifiers::NONE)
472 .then(|| keymap.action(Key::new(key.code)))
473 .flatten()
474 });
475 match action.map(str::to_string).as_deref() {
476 Some("pick") => return self.pick(),
477 Some("cancel") => {
478 self.answer = Some(None);
479 return Flow::Cancel;
480 }
481 Some("grid") => self.grid = !self.grid,
482 Some("up") if self.grid => self.move_cell(0, -1),
483 Some("down") if self.grid => self.move_cell(0, 1),
484 Some("left") if self.grid => self.move_cell(-1, 0),
485 Some("right") if self.grid => self.move_cell(1, 0),
486 Some("first") if self.grid => self.cell = 0,
487 Some("last") if self.grid => self.cell = 255,
488 Some("up") => self.step(-1),
489 Some("down") => self.step(1),
490 Some("page-up") => self.step(-(self.rows() as isize)),
491 Some("page-down") => self.step(self.rows() as isize),
492 Some("clear") => {
493 self.query.clear();
494 self.refilter();
495 }
496 Some("delete") => {
497 self.grid = false;
498 if self.query.pop().is_some() {
499 self.refilter();
500 }
501 }
502 _ => match key.code {
503 KeyCode::Char(c) if !key.modifiers.ctrl && !key.modifiers.alt => {
504 self.grid = false;
505 self.query.push(c);
506 self.refilter();
507 }
508 _ => return Flow::Ignored,
509 },
510 }
511 Flow::Continue
512 }
513
514 fn keymap(&self) -> Keymap {
515 let mut keymap = Keymap::new("color")
516 .bind("pick", keys("enter"), "pick")
517 .bind("cancel", keys("escape"), "cancel")
518 .bind(
519 "grid",
520 keys("tab shift+tab"),
521 "switch between the list and the grid",
522 )
523 .bind("up", keys("up"), "move up")
524 .bind("down", keys("down"), "move down")
525 .bind("left", keys("left"), "move left (grid)")
526 .bind("right", keys("right"), "move right (grid)")
527 .bind("page-up", keys("pageup"), "page up")
528 .bind("page-down", keys("pagedown"), "page down")
529 .bind("first", keys("home"), "first cell (grid)")
530 .bind("last", keys("end"), "last cell (grid)")
531 .bind("clear", keys("ctrl+u"), "clear the filter")
532 .bind("delete", keys("backspace"), "delete a character");
533 keymap.extend(self.rebound.clone());
534 keymap
535 }
536
537 fn render(&self, context: &Context<'_>) -> View {
538 self.space.set(context.height);
539 let width = context.width;
540 let theme = &self.theme;
541 let mut header = question(theme, &self.prompt);
542 if let Some(answer) = &self.answer {
543 match (answer, self.focused()) {
544 (Some(answer), Some(picked)) => {
545 header.push(Segment::new(" ", Some(picked.swatch())));
546 header.push(plain(" "));
547 header.push(text(answer.clone(), &theme.answer));
548 }
549 (Some(answer), None) => header.push(text(answer.clone(), &theme.answer)),
550 (None, _) => header.push(text("cancelled", &theme.hint)),
551 }
552 return View::new(vec![fit(header, width)]);
553 }
554 let column = crate::components::width(&header) + rich::cells::cell_len(&self.query);
555 header.push(plain(self.query.clone()));
556 let mut lines = vec![fit(header, width), self.swatch_line(width)];
557 if self.grid {
558 lines.extend(self.palette_grid(width));
559 } else {
560 lines.extend(self.list(width));
561 }
562 let hint = if self.grid {
563 " ←↑↓→ move · tab names · enter pick · esc cancel".to_string()
564 } else {
565 format!(
566 " {}/{} · type a name, #hex or rgb(r,g,b) · ↑↓ move · tab palette · enter pick · \
567 esc cancel",
568 self.count(),
569 COLORS.len()
570 )
571 };
572 lines.push(fit(vec![text(hint, &theme.hint)], width));
573 View::new(lines).with_cursor(0, column.min(width.saturating_sub(1)))
574 }
575
576 fn mouse(&self) -> bool {
577 self.mouse
578 }
579
580 fn default_value(&self) -> Option<String> {
581 self.valid_default().ok().flatten()
582 }
583
584 fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<String>, NotInteractive> {
585 let default = self.valid_default()?;
587 let mut prompt = format!("{} (a name, #rrggbb, rgb(r,g,b) or color(N))", self.prompt);
588 if let Some(default) = &self.default {
589 prompt.push_str(&format!(" [{default}]"));
590 }
591 io.write(&format!("{prompt}: "));
592 let line = io.read_line().unwrap_or_default();
593 let line = line.trim();
594 if line.is_empty() {
596 return default.map(Some).ok_or(NotInteractive::Ended);
597 }
598 Picked::parse(line)
599 .map(|picked| Some(picked.format(self.format)))
600 .ok_or_else(|| NotInteractive::Invalid(format!("not a colour: {line:?}")))
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn formats_colours() {
610 let red = Picked::parse("red").unwrap();
611 assert_eq!(red.format(ColorFormat::Name), "red");
612 assert_eq!(red.format(ColorFormat::Hex), "#800000");
613 let orange = Picked::parse("#FF8800").unwrap();
614 assert_eq!(orange.format(ColorFormat::Hex), "#ff8800");
615 assert_eq!(orange.format(ColorFormat::Rgb), "rgb(255,136,0)");
616 assert_eq!(orange.format(ColorFormat::Name), "#ff8800");
617 let palette = Picked::palette(208);
618 assert_eq!(palette.format(ColorFormat::Name), "dark_orange");
619 assert_eq!(
620 Picked::parse("rgb(1, 2, 3)")
621 .unwrap()
622 .format(ColorFormat::Hex),
623 "#010203"
624 );
625 assert!(Picked::parse("not a colour").is_none());
626 assert!(Picked::parse("default").is_none());
627 }
628
629 #[test]
630 fn every_listed_name_is_a_colour() {
631 for name in COLORS {
632 assert!(Color::parse(name).is_ok(), "{name}");
633 }
634 }
635}