PHPackages                             sugarcraft/candy-lister - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. sugarcraft/candy-lister

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

sugarcraft/candy-lister
=======================

PHP port of treilik/bubblelister — tree-list view component with customisable prefix/suffix rendering, line wrapping, cursor navigation, and per-item styling hooks.

00PHP

Since Jun 29Pushed 1mo agoCompare

[ Source](https://github.com/sugarcraft/candy-lister)[ Packagist](https://packagist.org/packages/sugarcraft/candy-lister)[ RSS](/packages/sugarcraft-candy-lister/feed)WikiDiscussions master Synced 3w ago

READMEChangelogDependenciesVersions (1)Used By (0)

[![candy-lister](.assets/icon.png)](.assets/icon.png)

[![CI](https://github.com/detain/sugarcraft/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/detain/sugarcraft/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/7f87540e3d0e51ac3a0bbcb452039e6854ff911043c72bb1a8684babd83ef87b/68747470733a2f2f636f6465636f762e696f2f67682f64657461696e2f737567617263726166742f6272616e63682f6d61737465722f67726170682f62616467652e7376673f666c61673d63616e64792d6c6973746572)](https://app.codecov.io/gh/detain/sugarcraft?flags%5B0%5D=candy-lister)[![Packagist Version](https://camo.githubusercontent.com/b68659db54898f33f902652be2d16df9121d15ef9849d3b3b4006950a1c4ec75/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f737567617263726166742f63616e64792d6c69737465723f6c6162656c3d7061636b6167697374)](https://packagist.org/packages/sugarcraft/candy-lister)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)[![PHP](https://camo.githubusercontent.com/e78ffc83837c0d12647811a7fd1910c3cbeae04988de94bb4fd5b67e0874696a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135382e312d3838393262662e737667)](https://www.php.net/)

CandyLister
===========

[](#candylister)

PHP port of [treilik/bubblelister](https://github.com/treilik/bubblelister) — a tree-list view component for terminal UIs. Renders items with custom prefix/suffix hooks, line wrapping, and cursor-aware styling.

Features
--------

[](#features)

- **Customisable Prefixer** — generates per-line prefix strings (line numbers, box-drawing borders, tree branches)
- **Customisable Suffixer** — generates per-line suffix strings (status markers, padding)
- **Line wrapping** — items wrap to multiple lines within a fixed viewport width
- **Cursor navigation** — current item highlighted with configurable style
- **Viewport awareness** — respects `Width` × `Height` viewport; `CursorOffset` gap from edges
- **`Stringable` items** — any PHP object with `__toString()` or `Stringable` works as a list item
- **`StringItem` adapter** — wrap plain strings as list items without a class
- **`LessFunc` / `EqualsFunc`** — plug-in sorting and equality comparison
- **Fuzzy matching** — `FuzzyMatch` scores candidates via Smith-Waterman local alignment
- **Filter state machine** — `withFilterFn()` / `withoutFilter()` with `FilterState` enum tracking (unfiltered / filtering / filtered)
- **Pure rendering** — outputs ANSI-styled strings; integrate with any TUI framework

Install
-------

[](#install)

```
composer require sugarcraft/candy-lister
```

Quick Start
-----------

[](#quick-start)

```
use SugarCraft\Lister\{Model, StringItem, DefaultPrefixer, DefaultSuffixer};

$model = Model::new();
$model->setWidth(80)->setHeight(24);
$model->addItem(new StringItem('First item'));
$model->addItem(new StringItem('Second item'));
$model->addItem(new StringItem('Third item'));
$model->setPrefixer(new DefaultPrefixer());
$model->setSuffixer(new DefaultSuffixer());

echo $model->View();
// Renders the list with ╭ ├ │ prefixes, line numbers, and > cursor marker
```

Item Types
----------

[](#item-types)

```
// Plain string adapter
$model->addItem(new StringItem('Plain string item'));

// Any Stringable object
class MyItem implements \Stringable {
    public function __toString(): string { return 'Formatted item'; }
}
$model->addItem(new MyItem());
```

Custom Prefixer
---------------

[](#custom-prefixer)

```
use SugarCraft\Lister\{Prefixer, Model};

$model->setPrefixer(new class implements Prefixer {
    public function initPrefixer(
        \Stringable $value, int $currentIndex, int $cursorIndex,
        int $lineOffset, int $width, int $height
    ): int {
        return 0; // no prefix width
    }
    public function prefix(int $currentLine, int $totalLines): string {
        return $currentLine === 0 ? '• ' : '  ';
    }
});
```

Custom Suffixer
---------------

[](#custom-suffixer)

```
use SugarCraft\Lister\{Suffixer, Model};

$model->setSuffixer(new class implements Suffixer {
    public function initSuffixer(
        \Stringable $value, int $currentIndex, int $cursorIndex,
        int $lineOffset, int $width, int $height
    ): int {
        return 0;
    }
    public function suffix(int $currentLine, int $totalLines): string {
        return '';
    }
});
```

Viewport
--------

[](#viewport)

Set the rendering viewport dimensions before calling `View()`:

```
$model->setWidth(80)->setHeight(25);
$model->setCursorOffset(3); // keep 3 lines between cursor and screen edge
```

Filtering
---------

[](#filtering)

Attach a filter function to narrow the visible items. The model tracks filter state via the `FilterState` enum:

```
use SugarCraft\Lister\{Model, StringItem, FilterState};

// Start with a list
$model = Model::new();
$model->setWidth(80)->setHeight(24);
foreach (['apple', 'banana', 'cherry', 'apricot', 'blueberry'] as $f) {
    $model->addItem(new StringItem($f));
}

// Filter to items starting with "a"
$filtered = $model->withFilterFn(
    fn(\Stringable $item) => stripos((string) $item, 'a') === 0
);
// filterState is now FilterState::filtering → FilterState::filtered

echo $filtered->length(); // 2 (apple, apricot)
echo $filtered->View();

// Remove filter and restore original items
$restored = $filtered->withoutFilter();
// filterState is now FilterState::unfiltered
echo $restored->length(); // 5
```

Filter state transitions:

FromToTrigger`unfiltered``filtering``withFilterFn()` called`filtering``filtered`filter applied, items reduced`filtered``unfiltered``withoutFilter()` called`filtering``unfiltered`filter cleared before resultFuzzy Matching
--------------

[](#fuzzy-matching)

`FuzzyMatch` implements Smith-Waterman local alignment to rank candidates by relevance to a query string. It is memory-efficient (two-row DP matrix) and penalizes gaps and mismatches while rewarding consecutive character matches:

```
use SugarCraft\Lister\FuzzyMatch;

$matcher = new FuzzyMatch();

// Score a single candidate
$score = $matcher->score('april', 'apricot'); // 13 (consecutive match bonus applied)

// Filter and rank a list of items
$items = [
    new StringItem('April'),
    new StringItem('September'),
    new StringItem('June'),
    new StringItem('July'),
    new StringItem('November'),
];

$results = $matcher->match('sep', $items);
// Returns [ [StringItem('September'), 11], ... ] sorted by score descending
```

Buffer diffing
--------------

[](#buffer-diffing)

The `Model::View()` maintains a `?Buffer $previousFrame` across renders. On each render it builds the current Buffer, computes `current->diff(previous)` (from [candy-buffer](https://github.com/detain/sugarcraft-candy-buffer)), and emits only the delta ANSI ops via `DiffEncoder::encode($ops)`. The current frame then replaces `previousFrame` for the next render.

**SSH bandwidth + flicker win:** a one-character change in an 80×24 viewport produces ~8 bytes of delta ops instead of ~1 940 bytes for a full repaint. Over an SSH session this means far less per-frame data on the wire and eliminates the full-screen flicker of rewrite-based terminals. The first render after startup or a resize still emits a full Buffer (no diff possible), so behaviour is always correct.

Shared foundations
------------------

[](#shared-foundations)

Mouse hit-testing (if needed) is self-contained via [candy-mouse](https://github.com/detain/sugarcraft-candy-mouse). The `Scanner` class handles zone registration and hit testing locally.

License
-------

[](#license)

[MIT](LICENSE)

###  Health Score

19

—

LowBetter than 9% of packages

Maintenance59

Moderate activity, may be stable

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/b1036e0717211b8030b83cbe729e8ba6ba442fdbd5285fb97a39d7dcfe339342?d=identicon)[detain](/maintainers/detain)

---

Top Contributors

[![detain](https://avatars.githubusercontent.com/u/1364504?v=4)](https://github.com/detain "detain (75 commits)")

### Embed Badge

![Health badge](/badges/sugarcraft-candy-lister/health.svg)

```
[![Health](https://phpackages.com/badges/sugarcraft-candy-lister/health.svg)](https://phpackages.com/packages/sugarcraft-candy-lister)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
