..." and should be measured by their rendered width. */ public static function widthWithoutFormatting(string $text, OutputFormatterInterface $formatter): int { return self::width(Helper::removeDecoration($formatter, $text)); } /** * Measure width from already-rendered terminal output. * * This strips ANSI/control sequences only; it does not parse formatter * markup like "...". */ public static function widthWithoutAnsi(string $text): int { return self::width(self::stripAnsi($text)); } /** * Resolve the code-point offset for a target display width. * * The returned offset is always on a grapheme boundary. */ public static function offsetForWidth(string $text, int $targetWidth): int { if ($targetWidth <= 0 || $text === '') { return 0; } $offset = 0; $width = 0; foreach (self::graphemes($text) as $grapheme) { $graphemeWidth = self::width($grapheme); if ($width + $graphemeWidth > $targetWidth) { break; } $width += $graphemeWidth; $offset += \mb_strlen($grapheme); } return $offset; } /** * Truncate text to a maximum display width with optional ellipsis. */ public static function truncate(string $text, int $maxWidth, bool $withEllipsis = false): string { if ($maxWidth <= 0 || $text === '') { return ''; } if (self::width($text) <= $maxWidth) { return $text; } if (!$withEllipsis || $maxWidth <= 3) { return Helper::substr($text, 0, $maxWidth); } $targetWidth = $maxWidth - 3; $offset = self::offsetForWidth($text, $targetWidth); return \mb_substr($text, 0, $offset).'...'; } /** * Iterate grapheme clusters in a string. * * @return string[] */ private static function graphemes(string $text): array { if (\preg_match_all('/\X/u', $text, $matches) > 0) { return $matches[0]; } // Fallback to individual code points when grapheme matching fails. $length = \mb_strlen($text); $chars = []; for ($i = 0; $i < $length; $i++) { $chars[] = \mb_substr($text, $i, 1); } return $chars; } /** * Remove terminal ANSI/control sequences from rendered text. */ private static function stripAnsi(string $text): string { return \preg_replace([self::ANSI_CSI_RX, self::ANSI_OSC_RX], '', $text) ?? $text; } }