Skip to content

feat(output): render NetBox object colors with an independent toggle#122

Merged
thomaschristory merged 3 commits into
mainfrom
feat/netbox-colors
Jun 26, 2026
Merged

feat(output): render NetBox object colors with an independent toggle#122
thomaschristory merged 3 commits into
mainfrom
feat/netbox-colors

Conversation

@thomaschristory

Copy link
Copy Markdown
Owner

Summary

Implements #120: render NetBox-native hex colors (role/tag/any color-bearing FK/choice object) in CLI table output and TUI DataTable cells, with a dedicated toggle to disable it independently of --color.

What changed

  • Root cause fixflatten._displayify used to collapse {display|label, color} to just the display string, dropping the hex before any formatter saw it. flatten(..., with_colors=True) now emits a framework-free ColoredValue(text, color) (or a list[ColoredValue] for tags) when an object carries a valid 6-hex color sibling. with_colors=False (the default) returns today's bare strings, so existing callers are untouched.
  • New nsc/output/colors.pyColoredValue dataclass + normalize_hex (strips a leading #, lowercases, rejects non-hex / wrong-length / empty / non-string). Imports nothing from cli/http/rich.
  • Table formatterrender(..., object_colors=...) passes with_colors = color and object_colors; _format_cell emits [#rrggbb]escape(text)[/] for ColoredValue and comma-joins colored lists. Inner text is escaped on every path; status-color map remains the fallback for plain strings.
  • TUIbuild_rows(..., object_colors=...) yields Rich Text(text, style="#rrggbb") cells (comma-joined for tag lists); ListScreen reads self.app.object_colors, threaded from the runtime context.
  • Toggle — new Defaults.object_colors: ObjectColorMode (auto/on/off, default auto) + --object-colors/--no-object-colors root flag, resolved by resolve_object_colors(mode, *, color) into RuntimeContext.object_colors. Always gated by the global color/no-color/tty resolution (never ANSI when color is off) but can be disabled independently while status colors stay on.
  • JSON/YAML/CSV/JSONL are unchanged — they never request colors, so ColoredValue never reaches them.

Tests

  • New tests/output/test_colors.py (normalize_hex / ColoredValue).
  • test_flatten.py: with_colors single object, list-of-tags, missing/invalid/empty color fallback, with_colors=False regression guards.
  • test_table_color.py: hex markup, escaping, list join, object-colors-off-keeps-status-on, color-gating, dispatch forwarding, non-table CSV unaffected.
  • test_runtime_color.py: resolve_object_colors matrix.
  • test_models.py: default AUTO + parse on/off.
  • test_view.py: styled Text cells + plain-string regression guards.

just lint (ruff + ruff format + mypy --strict) and just test (1183 passed, 1 skipped) both green.

Closes #120

🤖 Generated with Claude Code

Preserve NetBox-native hex colors (role.color, tag.color, any FK/choice
object exposing a 6-hex `color` sibling) so role/tag names render in their
NetBox color in CLI table output (Rich `[#rrggbb]…[/]`) and TUI DataTable
cells.

`flatten(..., with_colors=True)` now emits a framework-free `ColoredValue`
(or a list of them for tags) instead of collapsing to a bare display
string, so the hex survives to the formatters. Bare strings flow exactly
as before — existing callers are untouched unless they opt in.

Add a dedicated `object_colors` config default (ObjectColorMode
auto/on/off) and a `--object-colors/--no-object-colors` root flag,
resolved into `RuntimeContext.object_colors`. Object coloring is gated by
the existing color / no-color / non-tty resolution (never ANSI when color
is off) but can be disabled independently while status colors stay on.

JSON/YAML/CSV/JSONL are unchanged — they never request colors.

Closes #120

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Object colors made flatten return a raw Python list mixing ColoredValue
and str whenever only *some* list items carried a color (e.g. a tag list
where one tag has a color and another doesn't). Downstream formatters gate
the colored-list path on homogeneity (all() in table._is_colored_list and
tui.view._cell), so a mixed list fell through to str(value) and emitted an
ugly repr like [ColoredValue(text='a', color='ff0000'), 'b'].

Promote plain strings to ColoredValue(text, None) so the list stays uniform
whenever any item is colored; both formatters already handle a None color.

Also document the resolve_object_colors contract: ON is intentionally
identical to AUTO so --object-colors can never override the no-color
decision (--no-color, NO_COLOR, non-tty); only OFF diverges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thomaschristory

Copy link
Copy Markdown
Owner Author

Fixed the verified review findings (323b6f4):

  • MAJOR (mixed-list raw repr): flatten._displayify now emits a uniform colored list — when any item carries a color, plain strings are promoted to ColoredValue(text, None) so table/TUI formatters (which gate on all(...)) no longer fall through to str(value) and print a raw [ColoredValue(...), 'b'] repr. Now renders 'a, b' with the colored item styled and the uncolored one plain.
  • NIT (resolve_object_colors): confirmed ON==AUTO is intentional and documented the contract — ON deliberately defers to the no-color decision so --object-colors / NSC_OBJECT_COLORS=on can never force ANSI under --no-color/NO_COLOR/non-tty; only OFF diverges.

TDD: added failing-first tests in tests/output/test_flatten.py, tests/output/test_table_color.py, and tests/tui/test_view.py covering the mixed-color list at flatten + table + view layers. lint (ruff + mypy --strict) clean; full suite 1188 passed, 1 skipped.

# Conflicts:
#	nsc/cli/tui_commands.py
#	nsc/tui/__init__.py
#	nsc/tui/app.py
#	tests/cli/test_tui_command.py
@thomaschristory thomaschristory merged commit e1a4d20 into main Jun 26, 2026
12 checks passed
@thomaschristory thomaschristory deleted the feat/netbox-colors branch June 26, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI/CLI: render NetBox object colors (role/tag/etc.) with a toggle to disable

1 participant