1use std::collections::HashMap;
7use std::io::{self, Write};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Attribute {
12 Normal,
13 Bold,
14 Dim,
15 Underline,
16 Blink,
17 Reverse,
18 Standout,
19}
20
21impl Attribute {
22 pub fn to_ansi(&self) -> &'static str {
23 match self {
24 Attribute::Normal => "\x1b[0m",
25 Attribute::Bold => "\x1b[1m",
26 Attribute::Dim => "\x1b[2m",
27 Attribute::Underline => "\x1b[4m",
28 Attribute::Blink => "\x1b[5m",
29 Attribute::Reverse => "\x1b[7m",
30 Attribute::Standout => "\x1b[7m",
31 }
32 }
33
34 pub fn from_name(name: &str) -> Option<Self> {
35 match name {
36 "normal" => Some(Attribute::Normal),
37 "bold" => Some(Attribute::Bold),
38 "dim" => Some(Attribute::Dim),
39 "underline" => Some(Attribute::Underline),
40 "blink" => Some(Attribute::Blink),
41 "reverse" => Some(Attribute::Reverse),
42 "standout" => Some(Attribute::Standout),
43 _ => None,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Color {
51 Black,
52 Red,
53 Green,
54 Yellow,
55 Blue,
56 Magenta,
57 Cyan,
58 White,
59 Default,
60}
61
62impl Color {
63 pub fn fg_code(&self) -> u8 {
64 match self {
65 Color::Black => 30,
66 Color::Red => 31,
67 Color::Green => 32,
68 Color::Yellow => 33,
69 Color::Blue => 34,
70 Color::Magenta => 35,
71 Color::Cyan => 36,
72 Color::White => 37,
73 Color::Default => 39,
74 }
75 }
76
77 pub fn bg_code(&self) -> u8 {
78 match self {
79 Color::Black => 40,
80 Color::Red => 41,
81 Color::Green => 42,
82 Color::Yellow => 43,
83 Color::Blue => 44,
84 Color::Magenta => 45,
85 Color::Cyan => 46,
86 Color::White => 47,
87 Color::Default => 49,
88 }
89 }
90
91 pub fn from_name(name: &str) -> Option<Self> {
92 match name {
93 "black" => Some(Color::Black),
94 "red" => Some(Color::Red),
95 "green" => Some(Color::Green),
96 "yellow" => Some(Color::Yellow),
97 "blue" => Some(Color::Blue),
98 "magenta" => Some(Color::Magenta),
99 "cyan" => Some(Color::Cyan),
100 "white" => Some(Color::White),
101 "default" => Some(Color::Default),
102 _ => None,
103 }
104 }
105}
106
107#[derive(Debug)]
109pub struct Window {
110 pub name: String,
111 pub rows: usize,
112 pub cols: usize,
113 pub y: usize,
114 pub x: usize,
115 pub cursor_y: usize,
116 pub cursor_x: usize,
117 pub scroll: bool,
118 pub keypad: bool,
119 pub fg: Color,
120 pub bg: Color,
121 pub attrs: Vec<Attribute>,
122 buffer: Vec<Vec<char>>,
123}
124
125impl Window {
126 pub fn new(name: &str, rows: usize, cols: usize, y: usize, x: usize) -> Self {
127 Self {
128 name: name.to_string(),
129 rows,
130 cols,
131 y,
132 x,
133 cursor_y: 0,
134 cursor_x: 0,
135 scroll: false,
136 keypad: false,
137 fg: Color::Default,
138 bg: Color::Default,
139 attrs: Vec::new(),
140 buffer: vec![vec![' '; cols]; rows],
141 }
142 }
143
144 pub fn stdscr() -> Self {
145 let (rows, cols) = terminal_size().unwrap_or((24, 80));
146 Self::new("stdscr", rows, cols, 0, 0)
147 }
148
149 pub fn move_cursor(&mut self, y: usize, x: usize) {
150 if y < self.rows && x < self.cols {
151 self.cursor_y = y;
152 self.cursor_x = x;
153 }
154 }
155
156 pub fn addch(&mut self, ch: char) {
157 if self.cursor_y < self.rows && self.cursor_x < self.cols {
158 self.buffer[self.cursor_y][self.cursor_x] = ch;
159 self.cursor_x += 1;
160 if self.cursor_x >= self.cols {
161 self.cursor_x = 0;
162 self.cursor_y += 1;
163 if self.cursor_y >= self.rows {
164 if self.scroll {
165 self.scroll_up();
166 self.cursor_y = self.rows - 1;
167 } else {
168 self.cursor_y = self.rows - 1;
169 }
170 }
171 }
172 }
173 }
174
175 pub fn addstr(&mut self, s: &str) {
176 for ch in s.chars() {
177 self.addch(ch);
178 }
179 }
180
181 pub fn clear(&mut self) {
182 for row in &mut self.buffer {
183 for cell in row {
184 *cell = ' ';
185 }
186 }
187 self.cursor_y = 0;
188 self.cursor_x = 0;
189 }
190
191 pub fn erase(&mut self) {
192 self.clear();
193 }
194
195 pub fn clrtoeol(&mut self) {
196 if self.cursor_y < self.rows {
197 for x in self.cursor_x..self.cols {
198 self.buffer[self.cursor_y][x] = ' ';
199 }
200 }
201 }
202
203 pub fn clrtobot(&mut self) {
204 self.clrtoeol();
205 for y in (self.cursor_y + 1)..self.rows {
206 for x in 0..self.cols {
207 self.buffer[y][x] = ' ';
208 }
209 }
210 }
211
212 fn scroll_up(&mut self) {
213 self.buffer.remove(0);
214 self.buffer.push(vec![' '; self.cols]);
215 }
216
217 pub fn set_scroll(&mut self, enable: bool) {
218 self.scroll = enable;
219 }
220
221 pub fn set_keypad(&mut self, enable: bool) {
222 self.keypad = enable;
223 }
224
225 pub fn attron(&mut self, attr: Attribute) {
226 if !self.attrs.contains(&attr) {
227 self.attrs.push(attr);
228 }
229 }
230
231 pub fn attroff(&mut self, attr: Attribute) {
232 self.attrs.retain(|a| *a != attr);
233 }
234
235 pub fn set_color(&mut self, fg: Color, bg: Color) {
236 self.fg = fg;
237 self.bg = bg;
238 }
239
240 pub fn refresh(&self) -> io::Result<()> {
241 let mut stdout = io::stdout();
242
243 write!(stdout, "\x1b[{};{}H", self.y + 1, self.x + 1)?;
244
245 for attr in &self.attrs {
246 write!(stdout, "{}", attr.to_ansi())?;
247 }
248 write!(stdout, "\x1b[{};{}m", self.fg.fg_code(), self.bg.bg_code())?;
249
250 for (row_idx, row) in self.buffer.iter().enumerate() {
251 write!(stdout, "\x1b[{};{}H", self.y + row_idx + 1, self.x + 1)?;
252 let line: String = row.iter().collect();
253 write!(stdout, "{}", line)?;
254 }
255
256 write!(
257 stdout,
258 "\x1b[{};{}H",
259 self.y + self.cursor_y + 1,
260 self.x + self.cursor_x + 1
261 )?;
262
263 stdout.flush()
264 }
265
266 pub fn getyx(&self) -> (usize, usize) {
267 (self.cursor_y, self.cursor_x)
268 }
269
270 pub fn getmaxyx(&self) -> (usize, usize) {
271 (self.rows, self.cols)
272 }
273}
274
275#[derive(Debug, Default)]
277pub struct Curses {
278 windows: HashMap<String, Window>,
279 initialized: bool,
280 color_pairs: HashMap<i32, (Color, Color)>,
281 next_pair: i32,
282}
283
284impl Curses {
285 pub fn new() -> Self {
286 Self::default()
287 }
288
289 pub fn initscr(&mut self) -> io::Result<()> {
290 if self.initialized {
291 return Ok(());
292 }
293
294 let mut stdout = io::stdout();
295 write!(stdout, "\x1b[?1049h")?;
296 write!(stdout, "\x1b[2J")?;
297 write!(stdout, "\x1b[H")?;
298 stdout.flush()?;
299
300 let stdscr = Window::stdscr();
301 self.windows.insert("stdscr".to_string(), stdscr);
302 self.initialized = true;
303 self.next_pair = 1;
304
305 Ok(())
306 }
307
308 pub fn endwin(&mut self) -> io::Result<()> {
309 if !self.initialized {
310 return Ok(());
311 }
312
313 let mut stdout = io::stdout();
314 write!(stdout, "\x1b[?1049l")?;
315 write!(stdout, "\x1b[0m")?;
316 stdout.flush()?;
317
318 self.windows.clear();
319 self.color_pairs.clear();
320 self.initialized = false;
321
322 Ok(())
323 }
324
325 pub fn newwin(&mut self, name: &str, rows: usize, cols: usize, y: usize, x: usize) -> bool {
326 if self.windows.contains_key(name) {
327 return false;
328 }
329
330 let win = Window::new(name, rows, cols, y, x);
331 self.windows.insert(name.to_string(), win);
332 true
333 }
334
335 pub fn delwin(&mut self, name: &str) -> bool {
336 if name == "stdscr" {
337 return false;
338 }
339 self.windows.remove(name).is_some()
340 }
341
342 pub fn get_window(&self, name: &str) -> Option<&Window> {
343 self.windows.get(name)
344 }
345
346 pub fn get_window_mut(&mut self, name: &str) -> Option<&mut Window> {
347 self.windows.get_mut(name)
348 }
349
350 pub fn refresh(&self, name: &str) -> io::Result<()> {
351 if let Some(win) = self.windows.get(name) {
352 win.refresh()
353 } else {
354 Ok(())
355 }
356 }
357
358 pub fn refresh_all(&self) -> io::Result<()> {
359 for win in self.windows.values() {
360 win.refresh()?;
361 }
362 Ok(())
363 }
364
365 pub fn init_pair(&mut self, pair: i32, fg: Color, bg: Color) {
366 self.color_pairs.insert(pair, (fg, bg));
367 }
368
369 pub fn get_pair(&self, pair: i32) -> Option<(Color, Color)> {
370 self.color_pairs.get(&pair).copied()
371 }
372
373 pub fn is_initialized(&self) -> bool {
374 self.initialized
375 }
376
377 pub fn window_names(&self) -> Vec<&str> {
378 self.windows.keys().map(|s| s.as_str()).collect()
379 }
380}
381
382pub fn terminal_size() -> Option<(usize, usize)> {
384 #[cfg(unix)]
385 {
386 let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
387 let result = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
388 if result == 0 && ws.ws_row > 0 && ws.ws_col > 0 {
389 return Some((ws.ws_row as usize, ws.ws_col as usize));
390 }
391 }
392
393 std::env::var("LINES")
394 .ok()
395 .and_then(|l| l.parse().ok())
396 .zip(std::env::var("COLUMNS").ok().and_then(|c| c.parse().ok()))
397}
398
399#[cfg(unix)]
401pub fn cbreak() -> io::Result<()> {
402 let mut termios: libc::termios = unsafe { std::mem::zeroed() };
403 unsafe {
404 if libc::tcgetattr(libc::STDIN_FILENO, &mut termios) < 0 {
405 return Err(io::Error::last_os_error());
406 }
407 termios.c_lflag &= !(libc::ICANON | libc::ECHO);
408 termios.c_cc[libc::VMIN] = 1;
409 termios.c_cc[libc::VTIME] = 0;
410 if libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &termios) < 0 {
411 return Err(io::Error::last_os_error());
412 }
413 }
414 Ok(())
415}
416
417#[cfg(not(unix))]
418pub fn cbreak() -> io::Result<()> {
419 Ok(())
420}
421
422#[cfg(unix)]
424pub fn noecho() -> io::Result<()> {
425 let mut termios: libc::termios = unsafe { std::mem::zeroed() };
426 unsafe {
427 if libc::tcgetattr(libc::STDIN_FILENO, &mut termios) < 0 {
428 return Err(io::Error::last_os_error());
429 }
430 termios.c_lflag &= !libc::ECHO;
431 if libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &termios) < 0 {
432 return Err(io::Error::last_os_error());
433 }
434 }
435 Ok(())
436}
437
438#[cfg(not(unix))]
439pub fn noecho() -> io::Result<()> {
440 Ok(())
441}
442
443pub fn curs_set(visible: bool) -> io::Result<()> {
445 let mut stdout = io::stdout();
446 if visible {
447 write!(stdout, "\x1b[?25h")?;
448 } else {
449 write!(stdout, "\x1b[?25l")?;
450 }
451 stdout.flush()
452}
453
454pub fn builtin_zcurses(args: &[&str], curses: &mut Curses) -> (i32, String) {
456 if args.is_empty() {
457 return (1, "zcurses: subcommand required\n".to_string());
458 }
459
460 match args[0] {
461 "init" => {
462 if curses.initscr().is_err() {
463 return (1, "zcurses: failed to initialize\n".to_string());
464 }
465 (0, String::new())
466 }
467 "end" => {
468 if curses.endwin().is_err() {
469 return (1, "zcurses: failed to end\n".to_string());
470 }
471 (0, String::new())
472 }
473 "addwin" => {
474 if args.len() < 6 {
475 return (
476 1,
477 "zcurses addwin: name rows cols y x required\n".to_string(),
478 );
479 }
480 let name = args[1];
481 let rows: usize = args[2].parse().unwrap_or(1);
482 let cols: usize = args[3].parse().unwrap_or(1);
483 let y: usize = args[4].parse().unwrap_or(0);
484 let x: usize = args[5].parse().unwrap_or(0);
485
486 if curses.newwin(name, rows, cols, y, x) {
487 (0, String::new())
488 } else {
489 (1, format!("zcurses: window {} already exists\n", name))
490 }
491 }
492 "delwin" => {
493 if args.len() < 2 {
494 return (1, "zcurses delwin: window name required\n".to_string());
495 }
496 if curses.delwin(args[1]) {
497 (0, String::new())
498 } else {
499 (1, format!("zcurses: cannot delete window {}\n", args[1]))
500 }
501 }
502 "refresh" => {
503 let name = if args.len() > 1 { args[1] } else { "stdscr" };
504 if curses.refresh(name).is_err() {
505 return (1, format!("zcurses: failed to refresh {}\n", name));
506 }
507 (0, String::new())
508 }
509 "move" => {
510 if args.len() < 4 {
511 return (1, "zcurses move: window y x required\n".to_string());
512 }
513 let name = args[1];
514 let y: usize = args[2].parse().unwrap_or(0);
515 let x: usize = args[3].parse().unwrap_or(0);
516
517 if let Some(win) = curses.get_window_mut(name) {
518 win.move_cursor(y, x);
519 (0, String::new())
520 } else {
521 (1, format!("zcurses: window {} not found\n", name))
522 }
523 }
524 "string" => {
525 if args.len() < 3 {
526 return (1, "zcurses string: window text required\n".to_string());
527 }
528 let name = args[1];
529 let text = args[2..].join(" ");
530
531 if let Some(win) = curses.get_window_mut(name) {
532 win.addstr(&text);
533 (0, String::new())
534 } else {
535 (1, format!("zcurses: window {} not found\n", name))
536 }
537 }
538 "clear" => {
539 let name = if args.len() > 1 { args[1] } else { "stdscr" };
540 if let Some(win) = curses.get_window_mut(name) {
541 win.clear();
542 (0, String::new())
543 } else {
544 (1, format!("zcurses: window {} not found\n", name))
545 }
546 }
547 "attr" => {
548 if args.len() < 3 {
549 return (1, "zcurses attr: window attribute required\n".to_string());
550 }
551 let name = args[1];
552 let attr_name = args[2];
553
554 if let Some(win) = curses.get_window_mut(name) {
555 if let Some(attr) = Attribute::from_name(attr_name) {
556 win.attron(attr);
557 (0, String::new())
558 } else {
559 (1, format!("zcurses: unknown attribute {}\n", attr_name))
560 }
561 } else {
562 (1, format!("zcurses: window {} not found\n", name))
563 }
564 }
565 _ => (1, format!("zcurses: unknown subcommand {}\n", args[0])),
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn test_attribute_to_ansi() {
575 assert_eq!(Attribute::Bold.to_ansi(), "\x1b[1m");
576 assert_eq!(Attribute::Normal.to_ansi(), "\x1b[0m");
577 }
578
579 #[test]
580 fn test_attribute_from_name() {
581 assert_eq!(Attribute::from_name("bold"), Some(Attribute::Bold));
582 assert_eq!(Attribute::from_name("invalid"), None);
583 }
584
585 #[test]
586 fn test_color_codes() {
587 assert_eq!(Color::Red.fg_code(), 31);
588 assert_eq!(Color::Red.bg_code(), 41);
589 }
590
591 #[test]
592 fn test_color_from_name() {
593 assert_eq!(Color::from_name("red"), Some(Color::Red));
594 assert_eq!(Color::from_name("invalid"), None);
595 }
596
597 #[test]
598 fn test_window_new() {
599 let win = Window::new("test", 10, 20, 0, 0);
600 assert_eq!(win.name, "test");
601 assert_eq!(win.rows, 10);
602 assert_eq!(win.cols, 20);
603 }
604
605 #[test]
606 fn test_window_move_cursor() {
607 let mut win = Window::new("test", 10, 20, 0, 0);
608 win.move_cursor(5, 10);
609 assert_eq!(win.getyx(), (5, 10));
610 }
611
612 #[test]
613 fn test_window_addch() {
614 let mut win = Window::new("test", 10, 20, 0, 0);
615 win.addch('X');
616 assert_eq!(win.buffer[0][0], 'X');
617 assert_eq!(win.getyx(), (0, 1));
618 }
619
620 #[test]
621 fn test_window_addstr() {
622 let mut win = Window::new("test", 10, 20, 0, 0);
623 win.addstr("Hello");
624 assert_eq!(win.getyx(), (0, 5));
625 }
626
627 #[test]
628 fn test_window_clear() {
629 let mut win = Window::new("test", 10, 20, 0, 0);
630 win.addstr("Hello");
631 win.clear();
632 assert_eq!(win.buffer[0][0], ' ');
633 assert_eq!(win.getyx(), (0, 0));
634 }
635
636 #[test]
637 fn test_curses_new() {
638 let curses = Curses::new();
639 assert!(!curses.is_initialized());
640 }
641
642 #[test]
643 fn test_curses_newwin() {
644 let mut curses = Curses::new();
645 assert!(curses.newwin("test", 10, 20, 0, 0));
646 assert!(!curses.newwin("test", 10, 20, 0, 0));
647 }
648
649 #[test]
650 fn test_curses_delwin() {
651 let mut curses = Curses::new();
652 curses.newwin("test", 10, 20, 0, 0);
653 assert!(curses.delwin("test"));
654 assert!(!curses.delwin("test"));
655 }
656
657 #[test]
658 fn test_builtin_zcurses_no_args() {
659 let mut curses = Curses::new();
660 let (status, _) = builtin_zcurses(&[], &mut curses);
661 assert_eq!(status, 1);
662 }
663
664 #[test]
665 fn test_builtin_zcurses_unknown() {
666 let mut curses = Curses::new();
667 let (status, _) = builtin_zcurses(&["unknown"], &mut curses);
668 assert_eq!(status, 1);
669 }
670}