terminal = $terminal; $this->prefix = $prefix; } /** * Set the search query for highlighting matches. */ public function setQuery(string $query): void { $this->query = $query; } /** * Render history search items in a single-column list. * * @param string[] $items History matches (newest first) * @param int $selectedIndex Index of selected item (-1 for none) * @param int|null $maxRows Maximum visible rows (null for unlimited) * @param int $scrollOffset First visible row index * @param int $totalCount Total match count for status line * * @return string[] The rendered lines */ public function render( array $items, int $selectedIndex = 0, ?int $maxRows = null, int $scrollOffset = 0, int $totalCount = 0 ): array { if (empty($items)) { return [$this->terminal->format(' (no matches)')]; } $items = \array_map([History::class, 'collapseToSingleLine'], \array_values($items)); $prefixWidth = DisplayString::width($this->prefix); $indent = \str_repeat(' ', $prefixWidth); $maxWidth = \max(1, $this->terminal->getWidth() - $prefixWidth); $totalRows = \count($items); $needsTruncation = $maxRows !== null && $totalRows > $maxRows + 1; $startRow = $needsTruncation ? $scrollOffset : 0; $endRow = $needsTruncation ? \min($totalRows, $startRow + $maxRows) : $totalRows; $lines = []; for ($row = $startRow; $row < $endRow; $row++) { $item = DisplayString::truncate($items[$row], $maxWidth, true); if ($row === $selectedIndex) { $escaped = OutputFormatter::escape($item); $itemWidth = DisplayString::width($item); $padding = \max(0, $maxWidth - $itemWidth); $lines[] = $this->terminal->format( ''.$this->prefix.$escaped.\str_repeat(' ', $padding).'', ); } else { $lines[] = $indent.$this->highlightQuery($item); } } if ($needsTruncation) { $text = \sprintf('Items %d to %d of %d', $startRow + 1, $endRow, $totalCount > 0 ? $totalCount : $totalRows); $lines[] = $this->terminal->format($indent.''.$text.''); } return $lines; } /** * Highlight the first occurrence of the search query in an item. */ private function highlightQuery(string $item): string { if ($this->query === '') { return $item; } $caseSensitive = History::isSearchCaseSensitive($this->query); $pos = $caseSensitive ? \mb_strpos($item, $this->query) : \mb_stripos($item, $this->query); if ($pos === false) { return $item; } $before = \mb_substr($item, 0, $pos); $match = \mb_substr($item, $pos, \mb_strlen($this->query)); $after = \mb_substr($item, $pos + \mb_strlen($this->query)); return $before.$this->terminal->format(''.OutputFormatter::escape($match).'').$after; } }