diff --git a/CHANGELOG.md b/CHANGELOG.md index d39984d..29e328f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ versions, not published artifacts, so the release below is the first one a reade ## [Unreleased] +- Fix dashboard frame clearing, terminal bounds, footer overlap, and scrolling braille graph coordinates with values rising from bottom to top. +- Add `--hide-processes` to omit the dashboard process table during screen sharing and recordings. + ## [0.1.2] - 2026-09-14 ### Changed diff --git a/README.md b/README.md index 514985a..f114e91 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ grainx shows live computer and process activity in a terminal and can share thos > [!NOTE] > **Status:** Experimental CLI tool (v0.1.2, pre-1.0); CI checks Linux and macOS. +![The grainx dashboard with the process table hidden](docs/demo/dashboard.gif) + - Shows CPU, memory, disk, network, and process activity in an interactive dashboard. - Serves host readings through a local HTTP service. - Saves local or remote readings as JSON and CSV files. @@ -25,6 +27,12 @@ cd grainx cargo run --locked ~~~ +Hide the process table when screen sharing or recording with `--hide-processes`: + +~~~bash +cargo run --locked -- --hide-processes +~~~ + The default command opens the dashboard; press `q` to quit. For a first check without an interactive terminal, export local readings instead: ~~~bash diff --git a/docs/demo/dashboard.gif b/docs/demo/dashboard.gif new file mode 100644 index 0000000..1c0c3e0 Binary files /dev/null and b/docs/demo/dashboard.gif differ diff --git a/docs/demo/dashboard.tape b/docs/demo/dashboard.tape new file mode 100644 index 0000000..c0218b8 --- /dev/null +++ b/docs/demo/dashboard.tape @@ -0,0 +1,15 @@ +Output docs/demo/dashboard.gif +Set Shell "bash" +Set FontSize 16 +Set Width 1200 +Set Height 640 +Set Padding 24 +Set Theme "Catppuccin Mocha" +Hide +Type `export PS1='$ ' PATH="$PWD/target/release:$PATH"` Enter +Type "clear" Enter +Show +Type "grainx --hide-processes" Sleep 300ms Enter +Sleep 25s +Type "q" +Sleep 500ms diff --git a/src/cli.rs b/src/cli.rs index 14e995a..fd838eb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,6 +10,9 @@ use clap_complete::Shell; pub struct Cli { #[command(subcommand)] pub command: Option, + /// Hide the dashboard process table for screen sharing and recordings + #[arg(long, global = true)] + pub hide_processes: bool, } #[derive(Subcommand, Debug)] @@ -61,6 +64,9 @@ impl From for Shell { #[derive(Args, Debug, Clone)] pub struct MonitorArgs { + /// Whether the dashboard process table is hidden + #[arg(skip)] + pub hide_processes: bool, /// Remote agent base URL (e.g. http://host:9090) #[arg(long)] pub remote: Option, @@ -96,14 +102,19 @@ pub struct ExportArgs { impl Cli { pub fn resolved_command(self) -> Commands { - self.command.unwrap_or(Commands::Monitor(MonitorArgs { + let mut command = self.command.unwrap_or(Commands::Monitor(MonitorArgs { + hide_processes: false, remote: None, config: "dashboard_config.json".to_string(), refresh_interval_ms: None, cpu_warning_threshold: None, memory_warning_threshold: None, color_theme: None, - })) + })); + if let Commands::Monitor(args) = &mut command { + args.hide_processes = self.hide_processes; + } + command } } @@ -124,6 +135,23 @@ mod tests { } } + #[test] + fn hide_processes_works_for_default_and_explicit_dashboard() { + for argv in [ + vec!["grainx", "--hide-processes"], + vec!["grainx", "monitor", "--hide-processes"], + ] { + match Cli::parse_from(argv).resolved_command() { + Commands::Monitor(args) => assert!(args.hide_processes), + _ => panic!("expected monitor command"), + } + } + match Cli::parse_from(["grainx"]).resolved_command() { + Commands::Monitor(args) => assert!(!args.hide_processes), + _ => panic!("expected monitor command"), + } + } + #[test] fn parses_agent_flags() { let cli = Cli::parse_from(["grainx", "agent", "--bind", "0.0.0.0", "-p", "8080"]); diff --git a/src/rendering.rs b/src/rendering.rs index 7d7def1..081b1fd 100644 --- a/src/rendering.rs +++ b/src/rendering.rs @@ -1,6 +1,7 @@ use crossterm::{ - cursor, execute, - style::{Color, SetForegroundColor}, + cursor, queue, + style::{Color, ResetColor, SetForegroundColor}, + terminal::{Clear, ClearType}, }; use std::io::{self, Write}; @@ -14,7 +15,6 @@ pub struct Rect { /// Dynamic dashboard layout computed from terminal dimensions pub struct DashboardLayout { pub term_width: u16, - #[allow(dead_code)] pub term_height: u16, pub cpu_rect: Rect, pub mem_rect: Rect, @@ -26,14 +26,20 @@ pub struct DashboardLayout { impl DashboardLayout { pub fn from_terminal_size(width: u16, height: u16) -> Self { - let term_width = width.max(60); // Minimum reasonable width - let term_height = height.max(20); // Minimum reasonable height - - // Proportional layout: CPU graph takes ~35%, Memory ~15%, info ~15%, processes ~35% - let cpu_h = ((term_height as f32 * 0.35) as u16).max(8); - let mem_h = ((term_height as f32 * 0.15) as u16).max(4); - let net_h: u16 = 3; - let info_section_h: u16 = 4; + let term_width = width; + let term_height = height; + + // Reserve labels, five information rows, process rows and two footer rows. + // Small terminals lose graph/process rows instead of drawing off-screen. + let desired_cpu = ((height as u32 * 35 / 100) as u16).max(8); + let desired_mem = ((height as u32 * 15 / 100) as u16).max(4); + let graph_rows = (desired_cpu + desired_mem + 3).min(height.saturating_sub(18)); + let net_h = (graph_rows / 4).min(3); + let remaining = graph_rows - net_h; + let cpu_h = (remaining as u32 * desired_cpu as u32 + / (desired_cpu as u32 + desired_mem as u32)) as u16; + let mem_h = remaining - cpu_h; + let info_section_h: u16 = 5; let cpu_rect = Rect { x: 0, @@ -71,6 +77,12 @@ impl DashboardLayout { } } + /// Number of process entries that fit before the performance/footer rows. + pub fn process_rows(&self) -> usize { + self.footer_y + .saturating_sub(self.proc_start_y.saturating_add(2)) as usize + } + /// Row for the CPU label, kept just above `cpu_rect` so the CPU graph's /// clear and braille passes can never overwrite it. pub fn cpu_label_y(&self) -> u16 { @@ -89,13 +101,14 @@ impl DashboardLayout { /// `points` use the same coordinates as [`AdvancedCanvas::draw_braille_line`]: /// x and y are scaled to `cell_width`/`cell_height` terminal cells. The result /// is indexed `[gx][gy]` and sized `cell_width * 2` by `cell_height * 4`, the -/// number of braille dots per cell. +/// number of braille dots per cell. Logical y = 0 is the bottom row and +/// y = `cell_height` is the top row. /// /// Every finite segment is clipped before its endpoints are quantized, so the /// amount of work is bounded by the viewport no matter how large the inputs /// are. Non-finite segments are skipped. A single finite point in the inclusive -/// logical viewport is rounded to the nearest dot, with the right and bottom -/// edges clamped to the final dot cell; points outside are skipped. +/// logical viewport is rounded to the nearest dot and clamped to the edge +/// dot cells; points outside are skipped. pub fn braille_grid(points: &[(f64, f64)], cell_width: u16, cell_height: u16) -> Vec> { let grid_width = cell_width as usize * 2; let grid_height = cell_height as usize * 4; @@ -151,8 +164,9 @@ fn point_cell( fn quantize_point(point: (f64, f64), grid_width: usize, grid_height: usize) -> (usize, usize) { let gx = (point.0 * 2.0).round() as usize; - let gy = (point.1 * 4.0).round() as usize; - (gx.min(grid_width - 1), gy.min(grid_height - 1)) + // Logical values rise from zero; terminal grid rows run from top to bottom. + let gy = grid_height - 1 - ((point.1 * 4.0).round() as usize).min(grid_height - 1); + (gx.min(grid_width - 1), gy) } /// Clip a segment to the logical canvas and return quantized endpoint cells. @@ -308,8 +322,20 @@ fn plot_segment(grid: &mut [Vec], x1: usize, y1: usize, x2: usize, y2: usi } } +/// Clip printable text to a row; control characters cannot move the cursor. +fn clipped_text(text: &str, columns: u16) -> String { + text.chars() + .filter(|c| !c.is_control()) + .take(columns as usize) + .collect() +} + pub struct AdvancedCanvas { stdout: io::Stdout, + output: Vec, + buffering: bool, + bounds: Option<(u16, u16)>, + position: (u16, u16), } impl Default for AdvancedCanvas { @@ -322,26 +348,73 @@ impl AdvancedCanvas { pub fn new() -> Self { AdvancedCanvas { stdout: io::stdout(), + output: Vec::new(), + buffering: false, + bounds: None, + position: (0, 0), + } + } + + /// Build a complete replacement frame before writing it to the terminal. + pub fn begin_frame(&mut self, width: u16, height: u16) -> io::Result<()> { + self.output.clear(); + self.buffering = true; + self.bounds = Some((width, height)); + self.position = (0, 0); + queue!(self.output, ResetColor, Clear(ClearType::All)) + } + + pub fn set_draw_height(&mut self, height: u16) { + if let Some((_, bound_height)) = &mut self.bounds { + *bound_height = height; } } pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { - execute!(self.stdout, cursor::MoveTo(x, y)) + self.position = (x, y); + if self.bounds.is_none_or(|(w, h)| x < w && y < h) { + queue!(self.output, cursor::MoveTo(x, y))?; + } + self.flush_if_unbuffered() } pub fn set_color(&mut self, color: Color) -> io::Result<()> { - execute!(self.stdout, SetForegroundColor(color)) + queue!(self.output, SetForegroundColor(color))?; + self.flush_if_unbuffered() } pub fn draw_str(&mut self, s: &str) -> io::Result<()> { - self.stdout.write_all(s.as_bytes()) + if let Some((width, height)) = self.bounds { + let (x, y) = self.position; + if x >= width || y >= height { + return Ok(()); + } + let text = clipped_text(s, width - x); + self.position.0 += text.chars().count() as u16; + self.output.write_all(text.as_bytes())?; + } else { + self.output.write_all(s.as_bytes())?; + } + self.flush_if_unbuffered() + } + + fn flush_if_unbuffered(&mut self) -> io::Result<()> { + if !self.buffering { + self.flush()?; + } + Ok(()) } pub fn flush(&mut self) -> io::Result<()> { - self.stdout.flush() + self.stdout.write_all(&self.output)?; + self.stdout.flush()?; + self.output.clear(); + self.buffering = false; + Ok(()) } - /// Draw a braille-based graph line for high-resolution + /// Draw occupied braille cells, preserving other lines in the same graph. + /// The caller clears the frame before drawing its graph layers. pub fn draw_braille_line(&mut self, points: &[(f64, f64)], rect: &Rect) -> io::Result<()> { let grid = braille_grid(points, rect.width, rect.height); @@ -387,6 +460,9 @@ impl AdvancedCanvas { braille_value |= 0x80; } + if braille_value == 0 { + continue; + } let braille_char = std::char::from_u32(0x2800 + braille_value).unwrap_or('?'); self.set_cursor(rect.x + x, rect.y + y)?; self.draw_str(&braille_char.to_string())?; @@ -402,14 +478,77 @@ mod tests { use super::*; #[test] - fn layout_enforces_minimum_terminal_size() { + fn graph_values_rise_from_bottom_to_top() { + let low = braille_grid(&[(1.0, 0.08)], 4, 2); + let high = braille_grid(&[(1.0, 1.92)], 4, 2); + assert!(low[2][7], "low values belong in the bottom grid row"); + assert!(high[2][0], "high values belong in the top grid row"); + assert_eq!(quantize_point((1.0, 0.0), 8, 8), (2, 7)); + assert_eq!(quantize_point((1.0, 2.0), 8, 8), (2, 0)); + } + + #[test] + fn layout_uses_actual_terminal_size() { let layout = DashboardLayout::from_terminal_size(20, 10); - assert!(layout.term_width >= 60); - assert!(layout.term_height >= 20); - assert!(layout.cpu_rect.height >= 8); - assert!(layout.mem_rect.height >= 4); - assert!(layout.net_rect.height >= 3); - assert!(layout.footer_y >= 19); + assert_eq!(layout.term_width, 20); + assert_eq!(layout.term_height, 10); + assert_eq!(layout.footer_y, 9); + } + + #[test] + fn rows_are_clipped_and_control_characters_cannot_scroll() { + assert_eq!(clipped_text("182880KB% 0KB", 8), "182880KB"); + assert_eq!(clipped_text("abc\n\r\tdef", 4), "abcd"); + assert_eq!(clipped_text("⠿↔x", 2), "⠿↔"); + assert_eq!(clipped_text("text", 0), ""); + } + + #[test] + fn frame_starts_by_erasing_previous_rows() { + let mut canvas = AdvancedCanvas::new(); + canvas.begin_frame(20, 10).unwrap(); + let clear = canvas.output.clone(); + canvas.set_cursor(0, 2).unwrap(); + canvas.draw_str("long process row").unwrap(); + canvas.begin_frame(20, 10).unwrap(); + assert_eq!(canvas.output, clear); + assert!(String::from_utf8(clear).unwrap().contains("\x1b[2J")); + let before = canvas.output.clone(); + canvas.set_cursor(0, 10).unwrap(); + canvas.draw_str("off-screen").unwrap(); + assert_eq!(canvas.output, before); + } + + #[test] + fn empty_graph_cells_do_not_erase_other_graph_layers() { + let mut canvas = AdvancedCanvas::new(); + canvas.begin_frame(4, 2).unwrap(); + let rect = Rect { + x: 0, + y: 0, + width: 4, + height: 2, + }; + canvas + .draw_braille_line(&[(0.0, 0.0), (4.0, 0.0)], &rect) + .unwrap(); + let first_layer = canvas.output.clone(); + canvas.draw_braille_line(&[], &rect).unwrap(); + assert_eq!(canvas.output, first_layer); + assert!(!String::from_utf8(first_layer).unwrap().contains('⠀')); + } + + #[test] + fn processes_and_information_do_not_overlap_the_footer() { + for height in [18, 20, 24, 32, 40, 60, 100] { + let layout = DashboardLayout::from_terminal_size(80, height); + assert!(layout.network_start_y + 4 < layout.proc_start_y); + assert!(layout.proc_start_y < layout.footer_y - 1); + for index in 0..layout.process_rows() { + assert!(layout.proc_start_y + 1 + (index as u16) < layout.footer_y - 1); + } + } + assert_eq!(DashboardLayout::from_terminal_size(20, 5).process_rows(), 0); } #[test] diff --git a/src/tui.rs b/src/tui.rs index a30ca88..1b255a3 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -20,7 +20,12 @@ pub async fn run(args: MonitorArgs) -> Result<()> { return Err(GrainxError::NoTty); } - execute!(io::stdout(), terminal::EnterAlternateScreen, cursor::Hide)?; + execute!( + io::stdout(), + terminal::EnterAlternateScreen, + terminal::DisableLineWrap, + cursor::Hide + )?; terminal::enable_raw_mode()?; let shutdown = Arc::new(AtomicBool::new(false)); @@ -36,6 +41,7 @@ pub async fn run(args: MonitorArgs) -> Result<()> { terminal::disable_raw_mode()?; execute!( io::stdout(), + terminal::EnableLineWrap, terminal::LeaveAlternateScreen, cursor::Show, ResetColor @@ -73,7 +79,11 @@ async fn run_loop(args: MonitorArgs, shutdown: Arc) -> Result<()> { while !shutdown.load(Ordering::SeqCst) { perf_monitor.start_frame(); - let processes = backend.get_processes()?; + let processes = if args.hide_processes { + Vec::new() + } else { + backend.get_processes()? + }; let current_cpu = backend.last_cpu_usage(); if perf_monitor.should_skip_frame(current_cpu) { @@ -108,6 +118,7 @@ async fn run_loop(args: MonitorArgs, shutdown: Arc) -> Result<()> { anomaly_detector: &anomaly_detector, layout: &layout, selected_process, + hide_processes: args.hide_processes, perf_monitor: &mut perf_monitor, }, ) diff --git a/src/ui.rs b/src/ui.rs index f3c91e5..0d8682f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -53,6 +53,7 @@ pub struct DrawContext<'a> { pub anomaly_detector: &'a AnomalyDetector, pub layout: &'a DashboardLayout, pub selected_process: usize, + pub hide_processes: bool, pub perf_monitor: &'a mut PerformanceMonitor, } @@ -61,7 +62,8 @@ pub async fn draw_dashboard( state: &mut DashboardState, ctx: DrawContext<'_>, ) -> Result<()> { - canvas.set_cursor(0, 0)?; + canvas.begin_frame(ctx.layout.term_width, ctx.layout.term_height)?; + canvas.set_draw_height(ctx.layout.footer_y.saturating_sub(1)); let metrics = ctx.backend.refresh()?; let cpu_usage = metrics.cpu_usage; @@ -139,20 +141,11 @@ pub async fn draw_dashboard( let target_cpu_y = (cpu_usage as f64 / 100.0) * ctx.layout.cpu_rect.height as f64; state.current_cpu_y_val = state.current_cpu_y_val * 0.8 + target_cpu_y * 0.2; - state - .cpu_points - .push((state.iteration_count as f64, state.current_cpu_y_val)); - if state.cpu_points.len() > ctx.layout.cpu_rect.width as usize { - state.cpu_points.remove(0); - for p in state.cpu_points.iter_mut() { - p.0 -= 1.0; - } - } - - for y in ctx.layout.cpu_rect.y..(ctx.layout.cpu_rect.y + ctx.layout.cpu_rect.height) { - canvas.set_cursor(ctx.layout.cpu_rect.x, y)?; - canvas.draw_str(&" ".repeat(ctx.layout.cpu_rect.width as usize))?; - } + push_graph_sample( + &mut state.cpu_points, + state.current_cpu_y_val, + ctx.layout.cpu_rect.width, + ); canvas.set_color(cpu_color)?; canvas.draw_braille_line(&state.cpu_points, &ctx.layout.cpu_rect)?; @@ -169,20 +162,11 @@ pub async fn draw_dashboard( let target_mem_y = (memory_percentage / 100.0) * ctx.layout.mem_rect.height as f64; state.current_mem_y_val = state.current_mem_y_val * 0.8 + target_mem_y * 0.2; - state - .mem_points - .push((state.iteration_count as f64, state.current_mem_y_val)); - if state.mem_points.len() > ctx.layout.mem_rect.width as usize { - state.mem_points.remove(0); - for p in state.mem_points.iter_mut() { - p.0 -= 1.0; - } - } - - for y in ctx.layout.mem_rect.y..(ctx.layout.mem_rect.y + ctx.layout.mem_rect.height) { - canvas.set_cursor(ctx.layout.mem_rect.x, y)?; - canvas.draw_str(&" ".repeat(ctx.layout.mem_rect.width as usize))?; - } + push_graph_sample( + &mut state.mem_points, + state.current_mem_y_val, + ctx.layout.mem_rect.width, + ); let mem_color = if memory_percentage > ctx.config.memory_warning_threshold as f64 { ctx.palette.critical @@ -209,27 +193,16 @@ pub async fn draw_dashboard( state.current_net_rx_y = state.current_net_rx_y * 0.8 + target_net_rx_y * 0.2; state.current_net_tx_y = state.current_net_tx_y * 0.8 + target_net_tx_y * 0.2; - state - .net_rx_points - .push((state.iteration_count as f64, state.current_net_rx_y)); - state - .net_tx_points - .push((state.iteration_count as f64, state.current_net_tx_y)); - if state.net_rx_points.len() > ctx.layout.net_rect.width as usize { - state.net_rx_points.remove(0); - state.net_tx_points.remove(0); - for p in state.net_rx_points.iter_mut() { - p.0 -= 1.0; - } - for p in state.net_tx_points.iter_mut() { - p.0 -= 1.0; - } - } - - for y in ctx.layout.net_rect.y..(ctx.layout.net_rect.y + ctx.layout.net_rect.height) { - canvas.set_cursor(ctx.layout.net_rect.x, y)?; - canvas.draw_str(&" ".repeat(ctx.layout.net_rect.width as usize))?; - } + push_graph_sample( + &mut state.net_rx_points, + state.current_net_rx_y, + ctx.layout.net_rect.width, + ); + push_graph_sample( + &mut state.net_tx_points, + state.current_net_tx_y, + ctx.layout.net_rect.width, + ); canvas.set_color(ctx.palette.accent)?; canvas.draw_braille_line(&state.net_rx_points, &ctx.layout.net_rect)?; @@ -319,42 +292,45 @@ pub async fn draw_dashboard( } } - canvas.set_cursor(0, ctx.layout.proc_start_y)?; - canvas.set_color(ctx.palette.label)?; - canvas.draw_str("Top Processes (UP/DOWN to select, 'k' to kill, 'q' to quit):")?; + if !ctx.hide_processes { + canvas.set_cursor(0, ctx.layout.proc_start_y)?; + canvas.set_color(ctx.palette.label)?; + canvas.draw_str("Top Processes (UP/DOWN to select, 'k' to kill, 'q' to quit):")?; - for (i, (pid, name, cpu, memory)) in ctx - .processes - .iter() - .take(ctx.config.max_processes) - .enumerate() - { - let y_pos = ctx.layout.proc_start_y + 1 + i as u16; - canvas.set_cursor(0, y_pos)?; - - if i == ctx.selected_process { - canvas.set_color(ctx.palette.selection)?; - canvas.draw_str(&format!( - "> {:5} {:20} {:6.1}% {:8}KB", - pid, - name, - cpu, - memory / 1024 - ))?; - } else { - canvas.set_color(ctx.palette.label)?; - canvas.draw_str(&format!( - " {:5} {:20} {:6.1}% {:8}KB", - pid, - name, - cpu, - memory / 1024 - ))?; + for (i, (pid, name, cpu, memory)) in ctx + .processes + .iter() + .take(ctx.config.max_processes.min(ctx.layout.process_rows())) + .enumerate() + { + let y_pos = ctx.layout.proc_start_y + 1 + i as u16; + canvas.set_cursor(0, y_pos)?; + + if i == ctx.selected_process { + canvas.set_color(ctx.palette.selection)?; + canvas.draw_str(&format!( + "> {:5} {:20} {:6.1}% {:8}KB", + pid, + name, + cpu, + memory / 1024 + ))?; + } else { + canvas.set_color(ctx.palette.label)?; + canvas.draw_str(&format!( + " {:5} {:20} {:6.1}% {:8}KB", + pid, + name, + cpu, + memory / 1024 + ))?; + } } } + canvas.set_draw_height(ctx.layout.term_height); let (fps, frame_time, adaptive) = ctx.perf_monitor.get_performance_stats(); - canvas.set_cursor(0, ctx.layout.footer_y - 1)?; + canvas.set_cursor(0, ctx.layout.footer_y.saturating_sub(1))?; canvas.set_color(ctx.palette.muted)?; canvas.draw_str(&format!( "Performance: {:.1}FPS | {:.1}ms | Adaptive: {}", @@ -373,3 +349,34 @@ pub async fn draw_dashboard( Ok(()) } + +/// Keep history coordinates local to the viewport, including after a resize. +fn push_graph_sample(points: &mut Vec<(f64, f64)>, value: f64, width: u16) { + points.push((0.0, value)); + let excess = points.len().saturating_sub(width as usize); + points.drain(..excess); + for (x, point) in points.iter_mut().enumerate() { + point.0 = x as f64; + } +} + +#[cfg(test)] +mod tests { + use super::push_graph_sample; + + #[test] + fn scrolling_graph_history_stays_in_the_viewport() { + let mut points = Vec::new(); + for sample in 0..100 { + push_graph_sample(&mut points, sample as f64, 4); + } + assert_eq!( + points, + vec![(0.0, 96.0), (1.0, 97.0), (2.0, 98.0), (3.0, 99.0)] + ); + push_graph_sample(&mut points, 100.0, 2); + assert_eq!(points, vec![(0.0, 99.0), (1.0, 100.0)]); + push_graph_sample(&mut points, 101.0, 0); + assert!(points.is_empty()); + } +} diff --git a/tests/braille_rendering.rs b/tests/braille_rendering.rs index 9ca489d..d03fdbc 100644 --- a/tests/braille_rendering.rs +++ b/tests/braille_rendering.rs @@ -74,17 +74,17 @@ fn rasterizer_worker() { "single" => { let grid = braille_grid(&[(1.0, 1.0)], 4, 2); assert_eq!(count_dots(&grid), 1); - assert!(grid[2][4]); + assert!(grid[2][3]); let edge = braille_grid(&[(3.9, 1.9)], 4, 2); assert_eq!(count_dots(&edge), 1); - assert!(edge[7][7], "edge-adjacent rounding must clamp in bounds"); + assert!(edge[7][0], "edge-adjacent rounding must clamp in bounds"); let boundary = braille_grid(&[(4.0, 2.0)], 4, 2); assert_eq!(count_dots(&boundary), 1); assert!( - boundary[7][7], - "inclusive right/bottom edge maps to last dot" + boundary[7][0], + "inclusive right/top edge maps to top-right dot" ); for point in [(-1.0, 0.0), (4.1, 1.0), (1.0, 2.1)] { @@ -95,19 +95,19 @@ fn rasterizer_worker() { "lines" => { let horizontal = braille_grid(&[(0.0, 1.0), (4.0, 1.0)], 4, 2); assert_eq!(count_dots(&horizontal), 8); - assert!(horizontal.iter().all(|column| column[4])); + assert!(horizontal.iter().all(|column| column[3])); let vertical = braille_grid(&[(0.0, 0.0), (0.0, 2.0)], 4, 2); assert_eq!(count_dots(&vertical), 8); assert!(vertical[0].iter().all(|dot| *dot)); let fractional = braille_grid(&[(1.0, 0.10), (2.0, 0.15)], 4, 2); - assert!(fractional[2][0]); - assert!(fractional[4][1]); + assert!(fractional[2][7]); + assert!(fractional[4][6]); let repeated = braille_grid(&[(1.0, 1.0), (1.0, 1.0)], 4, 2); assert_eq!(count_dots(&repeated), 1); - assert!(repeated[2][4]); + assert!(repeated[2][3]); draw(&[(0.0, 1.0), (4.0, 1.0)], 4, 2); draw(&[(0.0, 0.0), (0.0, 2.0)], 4, 2); @@ -124,7 +124,7 @@ fn rasterizer_worker() { 0 ); let crossing = braille_grid(&[(-4.0, -4.0), (8.0, 8.0)], 4, 2); - assert!(crossing[0][0]); + assert!(crossing[0][7]); assert!(count_dots(&crossing) > 1); draw(&[(-4.0, -4.0), (8.0, 8.0)], 4, 2); } @@ -149,16 +149,16 @@ fn rasterizer_worker() { } "huge" => { let large = braille_grid(&[(0.0, 0.0), (1.0e18, 1.0e18)], 80, 20); - assert!(large[0][0]); + assert!(large[0][79]); assert!(count_dots(&large) > 1); let horizontal = braille_grid(&[(-f64::MAX, 1.0), (f64::MAX, 1.0)], 4, 2); assert_eq!(count_dots(&horizontal), 8); - assert!(horizontal.iter().all(|column| column[4])); + assert!(horizontal.iter().all(|column| column[3])); let diagonal = braille_grid(&[(-f64::MAX, -f64::MAX), (f64::MAX, f64::MAX)], 4, 2); - assert!(diagonal[0][0]); - assert!(diagonal[4][7]); + assert!(diagonal[0][7]); + assert!(diagonal[4][0]); assert!(count_dots(&diagonal) > 1); draw(&[(-f64::MAX, 1.0), (f64::MAX, 1.0)], 4, 2);