Fix text positioning by calculating visual width without ANSI codes
The motion effects were appearing with incorrect row offsets because the text width calculation included ANSI color escape sequences in the byte length, causing misaligned centering. Changes: - Added utils/ansi.rs module with strip_ansi() and visual_width() - Updated renderer to use visual_width() for offset positioning - Updated TerminalManager::print_centered() to use visual_width() - All text positioning now correctly ignores ANSI escape sequences This fixes the "shifted in the rows" issue where colored text was not properly centered due to escape sequence byte counts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::animation::{easing::EasingFunction, effects::Effect, timeline::Timeline};
|
||||
use crate::color::{apply, ColorEngine};
|
||||
use crate::utils::{ascii::AsciiArt, terminal::TerminalManager};
|
||||
use crate::utils::{ansi, ascii::AsciiArt, terminal::TerminalManager};
|
||||
use anyhow::Result;
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -62,7 +62,7 @@ impl<'a> Renderer<'a> {
|
||||
let (width, height) = terminal.get_size();
|
||||
let lines: Vec<&str> = colored_text.lines().collect();
|
||||
let text_height = lines.len() as i32;
|
||||
let text_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as i32;
|
||||
let text_width = lines.iter().map(|l| ansi::visual_width(l)).max().unwrap_or(0) as i32;
|
||||
|
||||
let base_x = (width as i32 - text_width) / 2;
|
||||
let base_y = (height as i32 - text_height) / 2;
|
||||
|
||||
54
src/utils/ansi.rs
Normal file
54
src/utils/ansi.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
/// Strip ANSI escape sequences from a string to get visual width
|
||||
pub fn strip_ansi(text: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut chars = text.chars().peekable();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '\x1b' {
|
||||
// Skip ANSI escape sequence
|
||||
if chars.peek() == Some(&'[') {
|
||||
chars.next(); // consume '['
|
||||
// Skip until we hit a letter (the command character)
|
||||
while let Some(&c) = chars.peek() {
|
||||
chars.next();
|
||||
if c.is_ascii_alphabetic() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Get the visual width of a string (excluding ANSI codes)
|
||||
pub fn visual_width(text: &str) -> usize {
|
||||
strip_ansi(text).chars().count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_strip_ansi() {
|
||||
let text = "\x1b[38;2;255;87;51mHello\x1b[0m";
|
||||
assert_eq!(strip_ansi(text), "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visual_width() {
|
||||
let text = "\x1b[38;2;255;87;51mHi\x1b[0m";
|
||||
assert_eq!(visual_width(text), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_ansi() {
|
||||
let text = "Plain text";
|
||||
assert_eq!(strip_ansi(text), "Plain text");
|
||||
assert_eq!(visual_width(text), 10);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod ansi;
|
||||
pub mod ascii;
|
||||
pub mod terminal;
|
||||
|
||||
@@ -5,6 +5,8 @@ use crossterm::{
|
||||
};
|
||||
use std::io::{stdout, Write};
|
||||
|
||||
use super::ansi;
|
||||
|
||||
pub struct TerminalManager {
|
||||
width: u16,
|
||||
height: u16,
|
||||
@@ -67,14 +69,14 @@ impl TerminalManager {
|
||||
|
||||
pub fn print_centered(&self, text: &str) -> Result<()> {
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let max_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16;
|
||||
let max_width = lines.iter().map(|l| ansi::visual_width(l)).max().unwrap_or(0) as u16;
|
||||
let height = lines.len() as u16;
|
||||
|
||||
let start_x = (self.width.saturating_sub(max_width)) / 2;
|
||||
let start_y = (self.height.saturating_sub(height)) / 2;
|
||||
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let line_width = line.len() as u16;
|
||||
let line_width = ansi::visual_width(line) as u16;
|
||||
let x = start_x + (max_width.saturating_sub(line_width)) / 2;
|
||||
let y = start_y + i as u16;
|
||||
self.print_at(x, y, line)?;
|
||||
|
||||
Reference in New Issue
Block a user