Coverage for src/lilbee/cli/tui/widgets/model_list.py: 100%

132 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Virtualized model list backed by Textual's OptionList. 

2 

3OptionList renders only on-screen rows, so frontier-tab populations of 

4hundreds of cloud models stay smooth. ``set_rows`` rebuilds the list 

5from a flat sequence of ``ModelListSection``; ``Selected`` is posted 

6when the user activates a non-heading row. 

7""" 

8 

9from __future__ import annotations 

10 

11from dataclasses import dataclass 

12from pathlib import Path 

13from typing import ClassVar, NamedTuple 

14 

15from textual import on 

16from textual.content import Content 

17from textual.message import Message 

18from textual.widgets import OptionList 

19from textual.widgets.option_list import Option 

20 

21from lilbee.catalog.types import ModelCompat 

22from lilbee.cli.tui.screens.catalog_utils import ( 

23 NATIVE_BACKEND, 

24 CatalogRow, 

25 CatalogRowKind, 

26 FrontierCatalogRow, 

27 LocalCatalogRow, 

28) 

29from lilbee.cli.tui.widgets.catalog_card_shared import ( 

30 _FIT_LEVEL_BACKGROUND, 

31 _FIT_LEVEL_LABEL_COMPACT, 

32 KEY_STATUS_COLOR, 

33 KEY_STATUS_LABEL, 

34 _compat_label, 

35 _spec_strip, 

36) 

37from lilbee.cli.tui.widgets.catalog_theme import MIDDLE_DOT, TASK_COLORS 

38from lilbee.cli.tui.widgets.clamped_option_list import ClampedOptionList 

39from lilbee.modelhub.models import FEATURED_STAR 

40from lilbee.runtime.hardware import FitChip 

41 

42_CSS_FILE = Path(__file__).parent / "model_list.tcss" 

43 

44 

45class ModelListSection(NamedTuple): 

46 """One contiguous block of rows under an optional heading.""" 

47 

48 heading: str | None 

49 rows: list[CatalogRow] 

50 

51 

52class ModelList(ClampedOptionList): 

53 """OptionList specialized for catalog rows, posting Selected on activate.""" 

54 

55 DEFAULT_CSS: ClassVar[str] = _CSS_FILE.read_text(encoding="utf-8") 

56 

57 @dataclass 

58 class Selected(Message): 

59 """Posted when a non-heading row is activated.""" 

60 

61 row: CatalogRow 

62 

63 def __init__(self, *, id: str | None = None) -> None: 

64 super().__init__(id=id) 

65 self._row_by_option_id: dict[str, CatalogRow] = {} 

66 

67 def set_rows(self, sections: list[ModelListSection]) -> None: 

68 """Replace the contents with options derived from *sections*.""" 

69 self._row_by_option_id.clear() 

70 self.clear_options() 

71 options = self._build_options(sections, start_idx=0) 

72 if options: 

73 self.add_options(options) 

74 

75 def append_rows(self, rows: list[CatalogRow]) -> None: 

76 """Append rows under the existing sections without rebuilding.""" 

77 if not rows: 

78 return 

79 start = len(self._row_by_option_id) 

80 section = ModelListSection(heading=None, rows=rows) 

81 options = self._build_options([section], start_idx=start) 

82 if options: 

83 self.add_options(options) 

84 

85 @property 

86 def row_count(self) -> int: 

87 """Number of selectable rows currently mounted (excludes section headings).""" 

88 return len(self._row_by_option_id) 

89 

90 def row_at(self, option_id: str) -> CatalogRow | None: 

91 """Return the CatalogRow for the given option id, or None when unknown.""" 

92 return self._row_by_option_id.get(option_id) 

93 

94 def highlighted_row(self) -> CatalogRow | None: 

95 """Return the CatalogRow under the highlight cursor, or None.""" 

96 idx = self.highlighted 

97 if idx is None: 

98 return None 

99 try: 

100 opt = self.get_option_at_index(idx) 

101 except IndexError: 

102 return None 

103 if opt.id is None: 

104 return None 

105 return self.row_at(opt.id) 

106 

107 def _build_options(self, sections: list[ModelListSection], *, start_idx: int) -> list[Option]: 

108 options: list[Option] = [] 

109 idx = start_idx 

110 for section_n, section in enumerate(sections): 

111 if section.heading: 

112 options.append(_heading_option(section.heading, start_idx + section_n)) 

113 for row in section.rows: 

114 option_id = f"row-{idx}" 

115 self._row_by_option_id[option_id] = row 

116 options.append(Option(_render_row(row), id=option_id)) 

117 idx += 1 

118 return options 

119 

120 @on(OptionList.OptionSelected) 

121 def _on_option_selected(self, event: OptionList.OptionSelected) -> None: 

122 if event.option.id is None: 

123 return 

124 row = self._row_by_option_id.get(event.option.id) 

125 if row is None: 

126 return 

127 event.stop() 

128 self.post_message(self.Selected(row)) 

129 

130 

131def _heading_option(heading: str, n: int) -> Option: 

132 return Option( 

133 Content.styled(heading, "bold $accent"), 

134 id=f"heading-{n}", 

135 disabled=True, 

136 ) 

137 

138 

139def _render_row(row: CatalogRow) -> Content: 

140 if row.kind == CatalogRowKind.FRONTIER: 

141 return _render_frontier(row) 

142 return _render_local(row) 

143 

144 

145def _render_frontier(row: FrontierCatalogRow) -> Content: 

146 # Two-line row mirroring the local layout: name + key-status tag on top, 

147 # provider strip below. Plain text styling, no colored pills. 

148 line1: list[Content] = [Content(" "), Content.styled(row.name, "bold")] 

149 line1.append( 

150 Content.styled( 

151 f" {KEY_STATUS_LABEL[row.key_status]}", 

152 f"{KEY_STATUS_COLOR[row.key_status]} italic", 

153 ) 

154 ) 

155 line2: list[Content] = [Content(" "), Content.styled(row.provider, "dim $text-muted")] 

156 return Content.assemble(*line1, Content("\n"), *line2, Content("\n")) 

157 

158 

159def _render_local(row: LocalCatalogRow) -> Content: 

160 line1 = _render_local_headline(row) 

161 line2 = _render_local_meta(row) 

162 return Content.assemble(*line1, Content("\n"), *line2, Content("\n")) 

163 

164 

165def _render_local_headline(row: LocalCatalogRow) -> list[Content]: 

166 parts: list[Content] = [ 

167 Content.styled(f"{FEATURED_STAR} ", "$warning") if row.featured else Content(" "), 

168 Content.styled(row.name, "bold"), 

169 ] 

170 if row.installed: 

171 parts.append(Content.styled(" installed", "$success italic")) 

172 parts.extend(_fit_tag(row.fit)) 

173 parts.extend(_compat_tag(row.compat)) 

174 return parts 

175 

176 

177def _fit_tag(fit: FitChip | None) -> list[Content]: 

178 """List-style fit indicator (italic colored text), matching the grid card chip set.""" 

179 if fit is None: 

180 return [] 

181 label = _FIT_LEVEL_LABEL_COMPACT[fit.level] 

182 return [Content.styled(f" {label}", f"{_FIT_LEVEL_BACKGROUND[fit.level]} italic")] 

183 

184 

185def _compat_tag(compat: ModelCompat) -> list[Content]: 

186 """List-style compat indicator. Empty for SUPPORTED to keep the row visually quiet.""" 

187 label = _compat_label(compat) 

188 if label is None: 

189 return [] 

190 color = "$warning" if compat is ModelCompat.UNSUPPORTED else "$text-muted" 

191 return [Content.styled(f" {label}", f"{color} italic")] 

192 

193 

194def _render_local_meta(row: LocalCatalogRow) -> list[Content]: 

195 parts: list[Content] = [Content(" ")] 

196 if row.task: 

197 task_color = TASK_COLORS.get(row.task, "$text-muted") 

198 parts.append(Content.styled(row.task, f"{task_color} italic")) 

199 parts.append(Content.styled(f" {MIDDLE_DOT} ", "dim $text-muted")) 

200 rest = [s for s in _local_meta_strip(row) if s] 

201 if rest: 

202 parts.append(Content.styled(f" {MIDDLE_DOT} ".join(rest), "dim $text-muted")) 

203 return parts 

204 

205 

206def _local_meta_strip(row: LocalCatalogRow) -> list[str]: 

207 rest: list[str] = [] 

208 if row.backend and row.backend != NATIVE_BACKEND: 

209 rest.append(row.backend) 

210 specs = _spec_strip(row.params, row.quant, row.size) 

211 if specs: 

212 rest.append(specs) 

213 if row.downloads and row.downloads != "--": 

214 rest.append(f"↓ {row.downloads}") 

215 return rest