Skip to main content

curses_utils/
lib.rs

1#![feature(decl_macro)]
2
3use std::{fs, io};
4use std::path::Path;
5use std::sync::LazyLock;
6use bmp;
7use log;
8use easycurses;
9use pancurses;
10use png;
11use easycurses::EasyCurses;
12use mat::Mat;
13
14pub use pancurses::COLOR_PAIR as color_pair;
15
16type PancursesResult = i32;
17
18/// Eight default color values, 10% lum (black), 50% lum (hues), 90% lum (white)
19pub static INIT_COLORS_DEFAULT : LazyLock <[[u8; 3]; 8]> = LazyLock::new (||{
20  use easycurses::Color;
21  let mut colors = [[0,0,0]; 8];
22  colors[Color::Black   as usize ] = [  25,  25,  25 ];
23  colors[Color::Red     as usize ] = [ 238,   0,   0 ];
24  colors[Color::Green   as usize ] = [   0, 163,   0 ];
25  colors[Color::Blue    as usize ] = [  67,  67, 255 ];
26  colors[Color::Yellow  as usize ] = [ 137, 137,   0 ];
27  colors[Color::Magenta as usize ] = [ 181,   0, 181 ];
28  colors[Color::Cyan    as usize ] = [   0, 147, 147 ];
29  colors[Color::White   as usize ] = [ 230, 230, 230 ];
30  colors
31});
32
33/// Log an error and panic if pancurses call returns `ERR`
34pub macro pancurses_ok {
35  ($e:expr) => {{
36    let result = $e;
37    if result != $crate::pancurses::OK {
38      $crate::log::error!("{}:l{}: pancurses error: {:?}", file!(), line!(),
39        result);
40      panic!()
41    }
42  }}
43}
44
45/// Log a warning but don't panic
46pub macro pancurses_warn_ok {
47  ($e:expr) => {{
48    let result = $e;
49    if result != $crate::pancurses::OK {
50      $crate::log::warn!("{}:l{}: pancurses error: {:?}", file!(), line!(),
51        result);
52    }
53  }}
54}
55
56/// Log an error and panic if pancurses call *does not* return `ERR`
57pub macro pancurses_err {
58  ($e:expr) => {{
59    let result = $e;
60    if result != $crate::pancurses::ERR {
61      $crate::log::error!("{}:l{}: pancurses expected error, got: {:?}",
62        file!(), line!(), result);
63      panic!()
64    }
65  }}
66}
67
68/// Log a warning if pancurses call *does not* return `ERR`
69pub macro pancurses_warn_err {
70  ($e:expr) => {{
71    let result = $e;
72    if result != $crate::pancurses::ERR {
73      $crate::log::warn!("{}:l{}: pancurses expected error, got: {:?}",
74        file!(), line!(), result);
75    }
76  }}
77}
78
79#[cfg(not(target_os = "windows"))]
80pub const ATTRIBUTE_MASK : pancurses::chtype = pancurses::A_ATTRIBUTES;
81#[cfg(target_os = "windows")]
82pub const ATTRIBUTE_MASK : pancurses::chtype =
83  !0x0 << pancurses::PDC_CHARTEXT_BITS;
84
85/// Wraps an `EasyCurses` window
86#[derive(Debug)]
87pub struct Curses {
88  pub easycurses : EasyCurses
89}
90
91/// Constructs the color pair number that `easycurses` assigned to this color
92/// combination, equal to `1 + (8 * fg + bg)`, giving a number in the range
93/// `1-64`
94#[inline]
95pub const fn color_pair_id (fg : easycurses::Color, bg : easycurses::Color) -> u8 {
96  1 + 8 * fg as u8 + bg as u8
97}
98
99/// Constructs the color pair attribute `chtype`.
100///
101/// Equivalent to `COLOR_PAIR(color_pair_id(fg,bg))`.
102#[inline]
103pub /*const*/ fn color_pair_attr (fg : easycurses::Color, bg : easycurses::Color)
104  -> pancurses::chtype
105{
106  color_pair (color_pair_id (fg, bg) as pancurses::chtype)
107}
108
109/// Get the 8-bit character data from the first byte of a `chtype`.
110#[inline]
111pub const fn chtype_character (ch : pancurses::chtype) -> char {
112  ch.to_le_bytes()[0] as char
113}
114
115/// Get the color pair number from the second byte of a `chtype`
116#[inline]
117pub const fn chtype_color_pair (ch : pancurses::chtype) -> i16 {
118  ch.to_le_bytes()[1] as i16
119}
120
121/// Returns the (fg, bg) color for the given color pair index.
122///
123/// Returns None if the index was >64.
124///
125/// Returns `Some (White, Black)` if the index is 0.
126#[inline]
127#[expect(clippy::missing_panics_doc)]
128pub const fn color_pair_colors (color_pair : i16)
129  -> Option <(easycurses::Color, easycurses::Color)>
130{
131  if color_pair == 0 {
132    Some ((easycurses::Color::White, easycurses::Color::Black))
133  } else if color_pair <= 64 {
134    let bg = (color_pair-1) & 0b0000_0111;
135    let fg = ((color_pair-1) & 0b0011_1000) >> 3;
136    Some ((
137      color_from_primitive (fg).unwrap(),
138      color_from_primitive (bg).unwrap()
139    ))
140  } else {
141    None
142  }
143}
144
145/// Get the attributes of a `chtype` (equivalent to `A_ATTRIBUTES & ch` in
146/// ncurses)
147#[inline]
148pub const fn chtype_attrs (ch : pancurses::chtype) -> pancurses::chtype {
149  ATTRIBUTE_MASK & ch
150}
151
152pub const fn color_from_primitive (color : i16) -> Option <easycurses::Color> {
153  use easycurses::Color;
154  let color = match color {
155    0 => Color::Black,
156    1 => Color::Red,
157    2 => Color::Green,
158    3 => Color::Yellow,
159    4 => Color::Blue,
160    5 => Color::Magenta,
161    6 => Color::Cyan,
162    7 => Color::White,
163    _ => return None
164  };
165  Some (color)
166}
167
168pub const fn color_complement (color : easycurses::Color) -> easycurses::Color {
169  use easycurses::Color;
170  match color {
171    Color::Black   => Color::White,
172    Color::White   => Color::Black,
173    Color::Red     => Color::Cyan,
174    Color::Green   => Color::Magenta,
175    Color::Blue    => Color::Yellow,
176    Color::Cyan    => Color::Red,
177    Color::Magenta => Color::Green,
178    Color::Yellow  => Color::Blue
179  }
180}
181
182/// Maps pure primary or secondary color (e.g. [255, 255, 0, 255]) with `a > 0` to
183/// easycurses Color
184///
185/// Returns `None` when `a == 0` or if any color bytes are something other than
186/// 0 or 255
187pub const fn rgba_to_color (rgba : [u8; 4]) -> Option <easycurses::Color> {
188  use easycurses::Color;
189  let color = match rgba {
190    [  _,   _,   _,   0] => return None,
191    [  0,   0,   0,   _] => Color::Black,
192    [255, 255, 255,   _] => Color::White,
193    [255,   0,   0,   _] => Color::Red,
194    [  0, 255,   0,   _] => Color::Green,
195    [  0,   0, 255,   _] => Color::Blue,
196    [  0, 255, 255,   _] => Color::Cyan,
197    [255,   0, 255,   _] => Color::Magenta,
198    [255, 255,   0,   _] => Color::Yellow,
199    _ => return None
200  };
201  Some (color)
202}
203
204/// Sets the background to the given rgba color and the foreground to its complement.
205#[inline]
206pub fn rgba_to_color_pair (rgba : [u8; 4]) -> Option <pancurses::ColorPair> {
207  #[expect(trivial_numeric_casts)]
208  #[expect(clippy::unnecessary_cast)]
209  rgba_to_color (rgba).map (
210    |bg| pancurses::ColorPair (color_pair_id (color_complement (bg), bg) as u8))
211}
212
213/// &#9888; Note: the curses window must be created before calling the method,
214/// as `ACS_CKBOARD()` returns `0x0` before initialization.
215///
216/// Uses `pancurses::ACS_CKBOARD()` to create half-tones from pixels with 50%
217/// intensity.
218///
219/// Foreground will always be black, but will be transparent for pure chroma
220/// pixels.
221#[inline]
222pub fn rgba_to_color_halftone (rgba : [u8; 4]) -> Option <pancurses::chtype> {
223  use easycurses::Color;
224  let fg = Color::Black;
225  let (bg, ch) = if let Some (bg) = rgba_to_color (rgba) {
226    (bg, b' ' as pancurses::chtype)
227  } else {
228    let bg = match rgba {
229      [  _,   _,   _,   0] => return None,
230      [  0,   0,   0,   _] => Color::Black,
231      [128, 128, 128,   _] => Color::White,
232      [128,   0,   0,   _] => Color::Red,
233      [  0, 128,   0,   _] => Color::Green,
234      [  0,   0, 128,   _] => Color::Blue,
235      [  0, 128, 128,   _] => Color::Cyan,
236      [128,   0, 128,   _] => Color::Magenta,
237      [128, 128,   0,   _] => Color::Yellow,
238      _ => return None
239    };
240    (bg, pancurses::ACS_CKBOARD())
241  };
242  Some (ch | color_pair_attr (fg, bg))
243}
244
245/// *Deprecated*: this doesn't really work for non-ASCII unicode, use `image_ascii_load`
246/// functions instead.
247///
248/// Load a text file
249#[deprecated]
250#[expect(clippy::missing_panics_doc)]
251pub fn image_text_load <P : AsRef <Path>> (path : P) -> Mat <char> {
252  use io::Read;
253  let mut buf = String::new();
254  let _ = fs::File::open (path).unwrap().read_to_string (&mut buf).unwrap();
255  buf.pop();  // read_to_end includes a final newline
256  #[expect(deprecated)]
257  image_text_load_str (&buf)
258}
259
260/// *Deprecated*: this doesn't really work for non-ASCII unicode, use `image_ascii_load`
261/// functions instead.
262///
263/// Load a text image from raw string data.
264///
265/// Newlines will be stripped out and the horizontal dimension will be determined by the
266/// longest line.
267#[deprecated]
268#[expect(clippy::missing_panics_doc)]
269pub fn image_text_load_str (chars : &str) -> Mat <char> {
270  let lines   = chars.lines();
271  let rows    = lines.clone().count();
272  let cols    = lines.clone().map (str::len).max().unwrap();
273  let mut vec = Vec::new();
274  for line in lines {
275    let len = line.chars().count();
276    vec.extend (line.chars());
277    vec.extend (std::iter::repeat_n (' ', cols - len));
278  }
279  Mat::from_vec ((rows, cols).into(), vec).unwrap()
280}
281
282/// Load an ASCII text file
283#[expect(clippy::missing_panics_doc)]
284pub fn image_ascii_load <P : AsRef <Path>> (path : P) -> Mat <u8> {
285  use io::Read;
286  let mut buf = Vec::new();
287  let _ = fs::File::open (path).unwrap().read_to_end (&mut buf).unwrap();
288  buf.pop();  // read_to_end includes a final newline
289  image_ascii_load_bytes (buf.as_slice())
290}
291
292/// Load a ASCII text image from raw bytes.
293///
294/// Newlines will be stripped out and the horizontal dimension will be determined by the
295/// longest line.
296#[expect(clippy::missing_panics_doc)]
297pub fn image_ascii_load_bytes (chars : &[u8]) -> Mat <u8> {
298  let split   = chars.split (|ch| ch == &b'\n');
299  let rows    = split.clone().count();
300  let cols    = split.clone().map (<[u8]>::len).max().unwrap();
301  let mut vec = Vec::new();
302  for line in split {
303    let len = line.len();
304    vec.extend (line);
305    vec.extend (std::iter::repeat_n (b' ', cols - len));
306  }
307  Mat::from_vec ((rows, cols).into(), vec).unwrap()
308}
309
310/// Load an image of pure chroma pixels
311#[inline]
312#[expect(clippy::missing_panics_doc)]
313pub fn image_color_load_bmp24 <P : AsRef <Path>> (path : P) -> Mat <easycurses::Color> {
314  let image = bmp::open (path).unwrap();
315  image_color_load_bmp24_helper (image)
316}
317
318/// Load an image of pure chroma pixels from memory
319#[inline]
320#[expect(clippy::missing_panics_doc)]
321pub fn image_color_load_bmp24_bytes (mut bytes : &[u8]) -> Mat <easycurses::Color> {
322  let image = bmp::from_reader (&mut bytes).unwrap();
323  image_color_load_bmp24_helper (image)
324}
325
326/// Load an image of pure chroma pixels with alpha channel values of `0` as
327/// transparency
328#[inline]
329#[expect(clippy::missing_panics_doc)]
330pub fn image_color_load_png <P : AsRef <Path>> (path : P)
331  -> Mat <Option <easycurses::Color>>
332{
333  let file    = io::BufReader::new (fs::File::open (path).unwrap());
334  let decoder = png::Decoder::new (file);
335  image_color_load_png_helper (decoder)
336}
337
338/// Load an image of pure chroma pixels with alpha channel values of `0` as transparency
339#[inline]
340pub fn image_color_load_png_bytes (bytes : &[u8]) -> Mat <Option <easycurses::Color>> {
341  let decoder = png::Decoder::new (io::Cursor::new (bytes));
342  image_color_load_png_helper (decoder)
343}
344
345/// Load an image of pure chroma background pixels with the color pair foreground set to
346/// the complement of the background
347#[inline]
348#[expect(clippy::missing_panics_doc)]
349pub fn image_color_bg_load_bmp24 <P : AsRef <Path>> (path : P)
350  -> Mat <pancurses::ColorPair>
351{
352  let image = bmp::open (path).unwrap();
353  image_color_bg_load_bmp24_helper (image)
354}
355
356/// Load an image of pure chroma background pixels with the color pair foreground set to
357/// the complement of the background
358#[inline]
359#[expect(clippy::missing_panics_doc)]
360pub fn image_color_bg_load_bmp24_bytes (mut bytes : &[u8])
361  -> Mat <pancurses::ColorPair>
362{
363  let image = bmp::from_reader (&mut bytes).unwrap();
364  image_color_bg_load_bmp24_helper (image)
365}
366
367/// Load foreground and background pair of 24bit BMP image color data.
368///
369/// &#9888; Images must have the same dimensions.
370#[expect(clippy::missing_panics_doc)]
371pub fn image_color_pair_load_bmp24 <P : AsRef <Path>> (fg : P, bg : P)
372  -> Mat <pancurses::ColorPair>
373{
374  let fg = bmp::open (fg).unwrap();
375  let bg = bmp::open (bg).unwrap();
376  image_color_pair_load_bmp24_helper (fg, bg)
377}
378
379/// Load foreground and background pair of 24bit BMP image color data.
380///
381/// &#9888; Images must have the same dimensions.
382#[expect(clippy::missing_panics_doc)]
383pub fn image_color_pair_load_bmp24_bytes (mut fg : &[u8], mut bg : &[u8])
384  -> Mat <pancurses::ColorPair>
385{
386  let fg = bmp::from_reader (&mut fg).unwrap();
387  let bg = bmp::from_reader (&mut bg).unwrap();
388  image_color_pair_load_bmp24_helper (fg, bg)
389}
390
391/// Load an image of pure chroma pixels
392#[expect(clippy::missing_panics_doc)]
393pub fn image_color_halftone_load_bmp24 <P : AsRef <Path>> (path : P)
394  -> Mat <pancurses::chtype>
395{
396  let image = bmp::open (path).unwrap();
397  image_color_halftone_load_bmp24_helper (image)
398}
399
400/// Load an image of pure chroma pixels
401#[expect(clippy::missing_panics_doc)]
402pub fn image_color_halftone_load_bmp24_bytes (mut bytes : &[u8])
403  -> Mat <pancurses::chtype>
404{
405  let image = bmp::from_reader (&mut bytes).unwrap();
406  image_color_halftone_load_bmp24_helper (image)
407}
408
409/// Load an image of ASCII characters as `pancurses::chtype`s
410#[expect(clippy::missing_panics_doc)]
411pub fn image_chtype (colors : &Mat <pancurses::ColorPair>, chars : &Mat <u8>)
412  -> Mat <pancurses::chtype>
413{
414  let dimensions = colors.dimensions();
415  assert_eq!(dimensions, chars.dimensions());
416  let vec = colors.elements().zip (chars.elements()).map (|(color, ch)|
417    color_pair (color.0 as pancurses::chtype) |
418    *ch as pancurses::chtype
419  ).collect::<Vec <pancurses::chtype>>();
420  Mat::from_vec (dimensions, vec).unwrap()
421}
422
423/// Load an image of UTF8 characters as `pancurses::chtype`s.
424#[expect(clippy::missing_panics_doc)]
425pub fn image_chtype_text (colors : &Mat <pancurses::ColorPair>, chars : &Mat <char>)
426  -> Mat <pancurses::chtype>
427{
428  let dimensions = colors.dimensions();
429  assert_eq!(dimensions, chars.dimensions());
430  let vec = colors.elements().zip (chars.elements()).map (|(color, ch)|
431    color_pair (color.0 as pancurses::chtype) |
432    *ch as pancurses::chtype
433  ).collect::<Vec <pancurses::chtype>>();
434  Mat::from_vec (dimensions, vec).unwrap()
435}
436
437//
438//  private
439//
440
441fn image_color_load_bmp24_helper (image : bmp::Image) -> Mat <easycurses::Color> {
442  let cols  = image.get_width()  as usize;
443  let rows  = image.get_height() as usize;
444  let vec   = image.coordinates().map (|(x, y)|{
445    let pixel = image.get_pixel (x, y);
446    rgba_to_color ([pixel.r, pixel.g, pixel.b, 255]).unwrap()
447  }).collect::<Vec <easycurses::Color>>();
448  Mat::from_vec ((rows, cols).into(), vec).unwrap()
449}
450
451fn image_color_bg_load_bmp24_helper (image : bmp::Image) -> Mat <pancurses::ColorPair> {
452  let cols  = image.get_width()  as usize;
453  let rows  = image.get_height() as usize;
454  let vec   = image.coordinates().map (|(x, y)|{
455    let pixel = image.get_pixel (x, y);
456    rgba_to_color_pair ([pixel.r, pixel.g, pixel.b, 255]).unwrap()
457  }).collect::<Vec <pancurses::ColorPair>>();
458  Mat::from_vec ((rows, cols).into(), vec).unwrap()
459}
460
461fn image_color_pair_load_bmp24_helper (fg : bmp::Image, bg : bmp::Image)
462  -> Mat <pancurses::ColorPair>
463{
464  let rows = fg.get_height() as usize;
465  let cols = fg.get_width()  as usize;
466  assert_eq!(rows, bg.get_height() as usize);
467  assert_eq!(cols, bg.get_width()  as usize);
468  let vec  = fg.coordinates().map (|(x, y)|{
469    let fg = {
470      let pixel = fg.get_pixel (x, y);
471      rgba_to_color ([pixel.r, pixel.g, pixel.b, 255]).unwrap()
472    };
473    let bg = {
474      let pixel = bg.get_pixel (x, y);
475      rgba_to_color ([pixel.r, pixel.g, pixel.b, 255]).unwrap()
476    };
477    #[expect(trivial_numeric_casts)]
478    #[expect(clippy::unnecessary_cast)]
479    pancurses::ColorPair (color_pair_id (fg, bg) as u8)
480  }).collect::<Vec <pancurses::ColorPair>>();
481  Mat::from_vec ((rows, cols).into(), vec).unwrap()
482}
483
484fn image_color_halftone_load_bmp24_helper (image : bmp::Image)
485  -> Mat <pancurses::chtype>
486{
487  let cols  = image.get_width()  as usize;
488  let rows  = image.get_height() as usize;
489  let vec   = image.coordinates().map (|(x, y)|{
490    let pixel = image.get_pixel (x, y);
491    rgba_to_color_halftone ([pixel.r, pixel.g, pixel.b, 255]).unwrap()
492  }).collect::<Vec <pancurses::chtype>>();
493  Mat::from_vec ((rows, cols).into(), vec).unwrap()
494}
495
496fn image_color_load_png_helper <R> (decoder : png::Decoder <R>)
497  -> Mat <Option <easycurses::Color>>
498where R : io::BufRead + io::Seek {
499  let mut reader = decoder.read_info().unwrap();
500  let info = reader.info();
501  match info.color_type {
502    png::ColorType::Rgba => {}
503    _ => {
504      log::error!("invalid color type: {:?}", info.color_type);
505      panic!()
506    }
507  }
508  match info.bit_depth {
509    png::BitDepth::Eight => {}
510    _ => {
511      log::error!("invalid bit depth: {:?}", info.bit_depth);
512      panic!()
513    }
514  }
515  let mut bytes = vec![0; reader.output_buffer_size().unwrap()];
516  let rows  = info.height as usize;
517  let cols  = info.width  as usize;
518  reader.next_frame (&mut bytes).unwrap();
519  let mut vec = Vec::with_capacity (rows * cols);
520  for i in 0..rows {
521    for j in 0..cols {
522      let offset = i * 4 * cols + j * 4;
523      let pixel  = &bytes[offset..offset+4];
524      debug_assert_eq!(pixel.len(), 4);
525      let color  = rgba_to_color ([pixel[0], pixel[1], pixel[2], pixel[3]]);
526      vec.push (color);
527    }
528  }
529  Mat::from_vec ((rows, cols).into(), vec).unwrap()
530}
531
532impl Curses {
533  /// Constructs an easycurses window with some default options set:
534  ///
535  /// - hidden cursor
536  /// - no input echo
537  /// - no line buffering
538  /// - modified primary colors on windows
539  ///
540  /// Colors are in the format accepted by `pancurses::init_color`
541  #[expect(clippy::missing_panics_doc)]
542  pub fn new (color_init : Option <&[[u8; 3]; 8]>) -> Self {
543    log::trace!("new...");
544    use easycurses::{CursorVisibility, InputMode};
545    let easycurses = EasyCurses::initialize_system().unwrap();
546    easycurses.win.keypad (true);                   // return special key codes
547    let mut curses = Curses { easycurses };
548    curses.set_cursor_visibility (CursorVisibility::Invisible); // hide cursor
549    curses.set_echo (false);                        // no input echo
550    curses.set_input_mode (InputMode::Character);   // no line buffering
551    if let Some (colors) = color_init {
552      if !pancurses::can_change_color() {
553        log::warn!("curses color init: pancurses backend reports terminal can \
554          not change color");
555      } else {
556        let init_color = |color_name| {
557          #[expect(clippy::cast_possible_truncation)]
558          const fn byte_to_milli_color (byte : u8) -> i16 {
559            ((byte as f64 / 255.0) * 1000.0) as i16
560          }
561          let color = colors[color_name as usize];
562          pancurses_ok!(pancurses::init_color (
563            color_name as i16,
564            byte_to_milli_color (color[0]),
565            byte_to_milli_color (color[1]),
566            byte_to_milli_color (color[2])
567          ));
568        };
569        init_color (easycurses::Color::Black);
570        init_color (easycurses::Color::Red);
571        init_color (easycurses::Color::Green);
572        init_color (easycurses::Color::Blue);
573        init_color (easycurses::Color::Yellow);
574        init_color (easycurses::Color::Magenta);
575        init_color (easycurses::Color::Cyan);
576        init_color (easycurses::Color::White);
577      }
578    }
579    log::trace!("...new");
580    curses
581  }
582
583  /// Get a reference to the underlying pancurses window
584  #[inline]
585  pub const fn win (&self) -> &pancurses::Window {
586    &self.easycurses.win
587  }
588
589  /// Blocking getch
590  #[inline]
591  pub fn getch_wait (&mut self) -> Option <easycurses::Input> {
592    use easycurses::{InputMode, TimeoutMode};
593    self.set_input_mode (InputMode::Character);
594    self.set_input_timeout (TimeoutMode::Never);
595    self.get_input()
596  }
597
598  /// Block until timeout getch
599  #[inline]
600  pub fn getch_timeout (&mut self, timeout : u32) -> Option <easycurses::Input> {
601    use easycurses::{InputMode, TimeoutMode};
602    self.set_input_mode (InputMode::Character);
603    debug_assert!(timeout <= i32::MAX as u32);
604    #[expect(clippy::cast_possible_wrap)]
605    self.set_input_timeout (TimeoutMode::WaitUpTo (timeout as i32));
606    self.get_input()
607  }
608
609  /// Non-blocking getch
610  #[inline]
611  pub fn getch_nowait (&mut self) -> Option <easycurses::Input> {
612    use easycurses::{InputMode, TimeoutMode};
613    self.set_input_mode (InputMode::Character);
614    self.set_input_timeout (TimeoutMode::Immediate);
615    self.get_input()
616  }
617
618  /// Blocking getline
619  #[inline]
620  pub fn getline_wait (&mut self) -> Option <easycurses::Input> {
621    use easycurses::{InputMode, TimeoutMode};
622    self.set_input_mode (InputMode::Cooked);
623    self.set_input_timeout (TimeoutMode::Never);
624    self.get_input()
625  }
626
627  /// Non-blocking getline
628  #[inline]
629  pub fn getline_nowait (&mut self) -> Option <easycurses::Input> {
630    use easycurses::{InputMode, TimeoutMode};
631    self.set_input_mode (InputMode::Cooked);
632    self.set_input_timeout (TimeoutMode::Immediate);
633    self.get_input()
634  }
635
636  #[inline]
637  pub fn rows (&self) -> i32 {
638    self.win().get_max_y()
639  }
640
641  #[inline]
642  pub fn columns (&self) -> i32 {
643    self.win().get_max_x()
644  }
645
646  #[inline]
647  pub fn dimensions_rc (&self) -> (i32, i32) {
648    self.easycurses.get_row_col_count()
649  }
650
651  #[inline]
652  pub fn center_col (&self) -> i32 {
653    let (_, cols) = self.get_row_col_count();
654    cols / 2
655  }
656
657  #[inline]
658  pub fn center_row (&self) -> i32 {
659    let (rows, _) = self.get_row_col_count();
660    rows / 2
661  }
662
663  #[inline]
664  pub fn center_rc (&self) -> (i32, i32) {
665    let (rows, cols) = self.get_row_col_count();
666    (rows / 2, cols / 2)
667  }
668
669  /// Returns the (row, col) of the upper left corner of the inserted text
670  pub fn print_centered (&mut self,
671    string     : &str,
672    row_offset : Option <i32>,
673    col_offset : Option <i32>
674  ) -> (i32, i32) {
675    let (longest, count) = string.lines().fold ((0,0),
676      |(longest, count), next| {
677        let next_len = next.len();
678        (usize::max (longest, next_len), count+1)
679      }
680    );
681    #[expect(clippy::cast_possible_truncation)]
682    #[expect(clippy::cast_possible_wrap)]
683    let half_longest = longest as i32 / 2;
684    let half_count   = count / 2;
685    let (center_row, center_col) = {
686      let (row, col) = self.center_rc();
687      (row + row_offset.unwrap_or (0), col + col_offset.unwrap_or (0))
688    };
689    for (i, line) in string.lines().enumerate() {
690      #[expect(clippy::cast_possible_truncation)]
691      #[expect(clippy::cast_possible_wrap)]
692      let at_row = center_row - half_count + i as i32;
693      let at_col = center_col - half_longest;
694      // NOTE: move_rc returns false if the cursor is moved outside of the
695      // window
696      let _ = self.move_rc (at_row, at_col);
697      // NOTE: print returns false if the cursor would be advanced past the end
698      // of the window
699      let _ = self.print (line);
700    }
701    (center_row - half_count, center_col - half_longest)
702  }
703
704  /// Draws a border with `pancurses::draw_bux()` using `ACS_VLINE` and
705  /// `ACS_HLINE` (corners will be `ACS_*` corners as well).
706  #[inline]
707  #[must_use]
708  pub fn draw_border_default (&mut self) -> PancursesResult {
709    self.win().draw_box (pancurses::ACS_VLINE(), pancurses::ACS_HLINE())
710  }
711
712  /// Draw a box with min (upper left) and max (lower right) coordinates.
713  ///
714  /// *Note*: this is not like the ncurses 'box' function which draws a window
715  /// border with matching top/bottom and left/right sides.
716  #[must_use]
717  pub fn draw_box (&mut self,
718    ch     : pancurses::chtype,
719    rc_min : (i32, i32),
720    rc_max : (i32, i32)
721  ) -> PancursesResult {
722    let mut out = pancurses::OK;
723    // this will return the first error and ignore all others
724    let mut result_ok = |result| if out == pancurses::OK {
725      out = result;
726    };
727    // take the min/max anyway in case they are not in the right order
728    let (min_row, min_col) =
729      (i32::min (rc_min.0, rc_max.0), i32::min (rc_min.1, rc_max.1));
730    let (max_row, max_col) =
731      (i32::max (rc_min.0, rc_max.0), i32::max (rc_min.1, rc_max.1));
732    let rows = max_row - min_row + 1;
733    let cols = max_col - min_col + 1;
734    let _ = self.move_rc (min_row, min_col);
735    result_ok (self.win().hline (ch, cols));
736    let _ = self.move_rc (min_row, min_col);
737    result_ok (self.win().vline (ch, rows));
738    let _ = self.move_rc (max_row, min_col);
739    result_ok (self.win().hline (ch, cols));
740    let _ = self.move_rc (min_row, max_col);
741    result_ok (self.win().vline (ch, rows));
742    out
743  }
744
745  /// Draws a box with explicit top/sides and corners.
746  ///
747  /// This is like the ncurses `border()` function, except around sub-area of the window
748  /// instead of the entire window.
749  // TODO: create types for border params
750  #[must_use]
751  #[expect(clippy::too_many_arguments)]
752  pub fn draw_border (&mut self,
753    left             : pancurses::chtype,
754    right            : pancurses::chtype,
755    top              : pancurses::chtype,
756    bottom           : pancurses::chtype,
757    top_left         : pancurses::chtype,
758    top_right        : pancurses::chtype,
759    bottom_left      : pancurses::chtype,
760    bottom_right     : pancurses::chtype,
761    rc_min           : (i32, i32),
762    rc_max           : (i32, i32),
763    thickness_top    : u32,
764    thickness_bottom : u32,
765    thickness_left   : u32,
766    thickness_right  : u32
767  ) -> PancursesResult {
768    let mut out = pancurses::OK;
769    // this will return the first error and ignore all others
770    let mut result_ok = |result| if out == pancurses::OK {
771      out = result;
772    };
773    #[expect(clippy::cast_possible_wrap)]
774    let (thickness_top, thickness_bottom, thickness_left, thickness_right) = (
775      thickness_top    as i32,
776      thickness_bottom as i32,
777      thickness_left   as i32,
778      thickness_right  as i32);
779    // take the min/max anyway in case they are not in the right order
780    let (min_row, min_col) =
781      (i32::min (rc_min.0, rc_max.0), i32::min (rc_min.1, rc_max.1));
782    let (max_row, max_col) =
783      (i32::max (rc_min.0, rc_max.0), i32::max (rc_min.1, rc_max.1));
784    let rows = max_row - min_row + 1;
785    let cols = max_col - min_col + 1;
786    let total_width  = thickness_left + thickness_right;
787    let total_height = thickness_top  + thickness_bottom;
788    for i in 0..thickness_top {
789      let _ = self.move_rc (min_row + i, min_col);
790      result_ok (self.win().hline (top_left, thickness_left));
791      let _ = self.move_rc (min_row + i, min_col + thickness_left);
792      result_ok (self.win().hline (top, cols - total_width));
793      let _ = self.move_rc (min_row + i, min_col + cols - thickness_right);
794      result_ok (self.win().hline (top_right, thickness_right));
795    }
796    for i in 0..thickness_bottom {
797      let _ = self.move_rc (max_row - i, min_col);
798      result_ok (self.win().hline (bottom_left, thickness_left));
799      let _ = self.move_rc (max_row - i, min_col + thickness_left);
800      result_ok (self.win().hline (bottom, cols - total_width));
801      let _ = self.move_rc (max_row - i, min_col + cols - thickness_right);
802      result_ok (self.win().hline (bottom_right, thickness_right));
803    }
804    for i in 0..thickness_left {
805      let _ = self.move_rc (min_row + thickness_top, min_col + i);
806      result_ok (self.win().vline (left, rows - total_height));
807    }
808    for i in 0..thickness_right {
809      let _ = self.move_rc (min_row + thickness_top, max_col - i);
810      result_ok (self.win().vline (right, rows - total_height));
811    }
812    out
813  }
814
815  /// Draws a filled rectangular box with the given characters.
816  pub fn draw_rect (&mut self,
817    border      : pancurses::chtype,
818    fill        : pancurses::chtype,
819    rc_min      : (i32, i32),
820    rc_max      : (i32, i32),
821    thickness_h : u32,        // thickness of top and bottom border
822    thickness_v : u32         // thickness of left and right border
823  ) {
824    let mut out = pancurses::OK;
825    // this will return the first error and ignore all others
826    let mut result_ok = |result| if out == pancurses::OK {
827      out = result;
828    };
829    // take the min/max anyway in case they are not in the right order
830    let (min_row, min_col) =
831      (i32::min (rc_min.0, rc_max.0), i32::min (rc_min.1, rc_max.1));
832    let (max_row, max_col) =
833      (i32::max (rc_min.0, rc_max.0), i32::max (rc_min.1, rc_max.1));
834    let rows = max_row - min_row + 1;
835    let cols = max_col - min_col + 1;
836    for i in 0..rows {
837      let _ = self.move_rc (min_row + i, min_col);
838      result_ok (self.win().hline (fill, cols));
839    }
840    #[expect(clippy::cast_possible_wrap)]
841    for i in 0..thickness_h as i32 {
842      let _ = self.move_rc (min_row + i, min_col);
843      result_ok (self.win().hline (border, cols));
844      let _ = self.move_rc (max_row - i, min_col);
845      result_ok (self.win().hline (border, cols));
846    }
847    #[expect(clippy::cast_possible_wrap)]
848    for i in 0..thickness_v as i32 {
849      let _ = self.move_rc (min_row, min_col + i);
850      result_ok (self.win().vline (border, rows));
851      let _ = self.move_rc (min_row, max_col - i);
852      result_ok (self.win().vline (border, rows));
853    }
854  }
855
856  /// Draw color image data using `chgat`
857  #[expect(clippy::missing_panics_doc)]
858  pub fn draw_image_color_pair (&mut self,
859    at_rc : (i32, i32), image : &Mat <pancurses::ColorPair>
860  ) {
861    let mut row = 0;
862    while row < image.height() {
863      let mut col = 0;
864      loop {
865        let color = *image.get ((row, col).into()).unwrap();
866        #[expect(clippy::cast_possible_truncation)]
867        #[expect(clippy::cast_possible_wrap)]
868        let start = col as i32;
869        col += 1;
870        while col < image.width()-1 {
871          if color == *image.get ((row, col).into()).unwrap() {
872            col += 1;
873          } else {
874            break
875          }
876        }
877        // NOTE: returns -1 if start position is outside the window
878        #[expect(clippy::cast_possible_truncation)]
879        #[expect(clippy::cast_possible_wrap)]
880        let _ = self.win().mvchgat (
881          at_rc.0 + row   as i32,
882          at_rc.1 + start,
883          col as i32 - start,
884          0x0, color.0 as i16);
885        if col == image.width() {
886          break
887        }
888      }
889      row += 1;
890    }
891  }
892
893  /// Draw chtype image data using `addch`
894  #[expect(clippy::missing_panics_doc)]
895  pub fn draw_image_chtype (&mut self,
896    at_rc : (i32, i32), image : &Mat <pancurses::chtype>
897  ) {
898    let dimensions = image.dimensions();
899    for row in 0..dimensions.rows {
900      #[expect(clippy::cast_possible_truncation)]
901      #[expect(clippy::cast_possible_wrap)]
902      for col in 0..dimensions.columns {
903        let ch = *image.get ((row, col).into()).unwrap();
904        let at_row = at_rc.0 + row as i32;
905        let at_col = at_rc.1 + col as i32;
906        // NOTE: function returns -1 when adding character outside the window
907        let _ = self.win().mvaddch (at_row, at_col, ch);
908      }
909    }
910  }
911
912  /// Logs some current curses (and terminal) attributes
913  pub fn log_info (&self) {
914    log::info!("log info...");
915    log::info!("  cursor(row,col):     {:?}", self.get_cursor_rc());
916    log::info!("  dimensions(row,col): {:?}", self.dimensions_rc());
917    log::info!("  is color terminal:   {}",   self.is_color_terminal());
918    log::info!("  can change color:    {}",   pancurses::can_change_color());
919    log::info!("  maximum color pairs: {}",   pancurses::COLOR_PAIRS());
920    log::info!("  maximum colors:      {}",   pancurses::COLORS());
921    log::info!("  colors:");
922    #[expect(clippy::cast_possible_truncation)]
923    for i in 0..pancurses::COLORS() as i16 {
924      let color_string = if let Some (color) = color_from_primitive (i) {
925        format!("{color:?}")
926      } else {
927        i.to_string()
928      };
929      let rgb = pancurses::color_content (i);
930      log::info!("    {color_string}: {rgb:?}");
931    }
932    log::info!("...log info");
933  }
934}
935
936impl Default for Curses {
937  /// Calls `Curses::new` with `None`
938  fn default() -> Self {
939    Curses::new (None)
940  }
941}
942
943impl std::ops::Deref for Curses {
944  type Target = EasyCurses;
945  fn deref (&self) -> &EasyCurses {
946    &self.easycurses
947  }
948}
949
950impl std::ops::DerefMut for Curses {
951  fn deref_mut (&mut self) -> &mut EasyCurses {
952    &mut self.easycurses
953  }
954}
955
956#[cfg(test)]
957mod tests {
958  //use super::*;
959  #[test]
960  fn color_from_primitive() {
961    use super::color_from_primitive;
962    for i in 0..8 {
963      assert_eq!(i, color_from_primitive (i).unwrap() as i16);
964    }
965  }
966}