espressoTUI 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. espresso/__init__.py +61 -0
  2. espresso/beans/__init__.py +304 -0
  3. espresso/beans/bar_chart.py +301 -0
  4. espresso/beans/codeviewer.py +349 -0
  5. espresso/beans/command_palette.py +398 -0
  6. espresso/beans/confetti.py +272 -0
  7. espresso/beans/datepicker.py +493 -0
  8. espresso/beans/detail_selector.py +191 -0
  9. espresso/beans/dialog.py +120 -0
  10. espresso/beans/diff_viewer.py +468 -0
  11. espresso/beans/filepicker.py +276 -0
  12. espresso/beans/form.py +294 -0
  13. espresso/beans/git_tree.py +366 -0
  14. espresso/beans/help.py +200 -0
  15. espresso/beans/image.py +329 -0
  16. espresso/beans/list.py +506 -0
  17. espresso/beans/markdown.py +259 -0
  18. espresso/beans/marquee.py +161 -0
  19. espresso/beans/metric.py +177 -0
  20. espresso/beans/navstack.py +161 -0
  21. espresso/beans/paginator.py +159 -0
  22. espresso/beans/pipeline_progress.py +313 -0
  23. espresso/beans/progress.py +59 -0
  24. espresso/beans/prompt.py +269 -0
  25. espresso/beans/quickfix.py +219 -0
  26. espresso/beans/slider.py +417 -0
  27. espresso/beans/sortable_list.py +305 -0
  28. espresso/beans/sparkline.py +285 -0
  29. espresso/beans/spinner.py +69 -0
  30. espresso/beans/splitter.py +329 -0
  31. espresso/beans/spring.py +284 -0
  32. espresso/beans/statusbar.py +161 -0
  33. espresso/beans/table.py +139 -0
  34. espresso/beans/tabs.py +125 -0
  35. espresso/beans/textarea.py +425 -0
  36. espresso/beans/textinput.py +138 -0
  37. espresso/beans/timer.py +277 -0
  38. espresso/beans/toast.py +118 -0
  39. espresso/beans/tree.py +160 -0
  40. espresso/beans/viewport.py +104 -0
  41. espresso/cli.py +219 -0
  42. espresso/core/__init__.py +45 -0
  43. espresso/core/keys.py +226 -0
  44. espresso/core/mouse.py +189 -0
  45. espresso/core/program.py +388 -0
  46. espresso/core/tea.py +187 -0
  47. espresso/core/terminal.py +178 -0
  48. espresso/crema/__init__.py +74 -0
  49. espresso/crema/border.py +87 -0
  50. espresso/crema/color.py +131 -0
  51. espresso/crema/flexbox.py +257 -0
  52. espresso/crema/gradient.py +196 -0
  53. espresso/crema/grid.py +174 -0
  54. espresso/crema/layout.py +130 -0
  55. espresso/crema/overlay.py +151 -0
  56. espresso/crema/style.py +464 -0
  57. espresso/crema/width.py +119 -0
  58. espresso/crema/wrap.py +346 -0
  59. espresso/py.typed +1 -0
  60. espressotui-1.0.0.dist-info/METADATA +315 -0
  61. espressotui-1.0.0.dist-info/RECORD +64 -0
  62. espressotui-1.0.0.dist-info/WHEEL +4 -0
  63. espressotui-1.0.0.dist-info/entry_points.txt +3 -0
  64. espressotui-1.0.0.dist-info/licenses/LICENSE +21 -0
espresso/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """Espresso: A lightweight, declarative Elm Architecture (TEA) TUI framework for Python.
2
+
3
+ Inspired by Bubble Tea, Lip Gloss, and Bubbles.
4
+
5
+ - espresso: Core TEA framework, event loop, and terminal driver.
6
+ - espresso.crema: Rich styling, box model, colors, and layout engine.
7
+ - espresso.beans: Reusable UI components.
8
+ """
9
+
10
+ from espresso.core.keys import Key, KeyMsg
11
+ from espresso.core.mouse import MouseAction, MouseButton, MouseMsg
12
+ from espresso.core.program import Program
13
+ from espresso.core.tea import (
14
+ BatchMsg,
15
+ Cmd,
16
+ DisableMouseMsg,
17
+ EnableMouseMsg,
18
+ Model,
19
+ Msg,
20
+ QuitMsg,
21
+ TickMsg,
22
+ WindowSizeMsg,
23
+ batch,
24
+ disable_mouse,
25
+ enable_mouse,
26
+ quit_app,
27
+ sequence,
28
+ tick,
29
+ )
30
+ from espresso import beans, crema
31
+
32
+ __version__ = "1.0.0"
33
+
34
+ __all__ = [
35
+ "Model",
36
+ "Msg",
37
+ "Cmd",
38
+ "Program",
39
+ "Key",
40
+ "KeyMsg",
41
+ "MouseMsg",
42
+ "MouseButton",
43
+ "MouseAction",
44
+ "EnableMouseMsg",
45
+ "DisableMouseMsg",
46
+ "enable_mouse",
47
+ "disable_mouse",
48
+ "QuitMsg",
49
+ "WindowSizeMsg",
50
+ "BatchMsg",
51
+ "TickMsg",
52
+ "batch",
53
+ "sequence",
54
+ "tick",
55
+ "quit_app",
56
+ "crema",
57
+ "beans",
58
+ ]
59
+
60
+
61
+
@@ -0,0 +1,304 @@
1
+ """Beans: Standard library of reusable TUI components for Espresso.
2
+
3
+ Inspired by Bubbles.
4
+ """
5
+
6
+ from espresso.beans.bar_chart import (
7
+ BarChart,
8
+ BarChartSelectMsg,
9
+ BarItem,
10
+ BarOrientation,
11
+ )
12
+ from espresso.beans.codeviewer import (
13
+ CodeViewer,
14
+ SyntaxTheme,
15
+ THEME_DRACULA,
16
+ THEME_ESPRESSO,
17
+ THEME_MONOKAI,
18
+ highlight_code,
19
+ )
20
+ from espresso.beans.command_palette import (
21
+ CommandPalette,
22
+ CommandPaletteCloseMsg,
23
+ CommandPaletteSelectMsg,
24
+ PaletteItem,
25
+ )
26
+ from espresso.beans.git_tree import (
27
+ GitFileNode,
28
+ GitFileStatus,
29
+ GitTree,
30
+ GitTreeSelectMsg,
31
+ GitTreeToggleMsg,
32
+ )
33
+ from espresso.beans.datepicker import (
34
+ DateChangeMsg,
35
+ DatePicker,
36
+ DatePickerFocus,
37
+ DateSelectMsg,
38
+ )
39
+ from espresso.beans.detail_selector import DetailItem, DetailSelectMsg, DetailSelector
40
+ from espresso.beans.dialog import Dialog, DialogResultMsg
41
+ from espresso.beans.filepicker import FileEntry, FilePicker, FileSelectMsg, format_file_size
42
+ from espresso.beans.help import Help, KeyBinding, KeyMap
43
+ from espresso.beans.image import ImageViewer, RenderMode, parse_bmp, parse_ppm
44
+ from espresso.beans.list import List, ListItem, ListSelectMsg, PaginationMode
45
+ from espresso.beans.markdown import MarkdownViewer, render_markdown
46
+ from espresso.beans.metric import (
47
+ LayoutDirection,
48
+ Metric,
49
+ MetricGroup,
50
+ MetricLayout,
51
+ MetricTrend,
52
+ )
53
+ from espresso.beans.navstack import NavEntry, NavPopMsg, NavPushMsg, NavStack
54
+ from espresso.beans.paginator import Paginator, PaginatorType
55
+ from espresso.beans.pipeline_progress import (
56
+ PipelineCompleteMsg,
57
+ PipelineProgress,
58
+ PipelineStage,
59
+ StageCompleteMsg,
60
+ StageFailedMsg,
61
+ StageStartMsg,
62
+ StageStatus,
63
+ )
64
+ from espresso.beans.progress import Progress
65
+ from espresso.beans.prompt import (
66
+ ConfirmPrompt,
67
+ ConfirmSubmitMsg,
68
+ MultiSelectPrompt,
69
+ MultiSelectSubmitMsg,
70
+ SelectPrompt,
71
+ SelectSubmitMsg,
72
+ )
73
+ from espresso.beans.quickfix import QuickFix, QuickFixItem, QuickFixSelectMsg
74
+ from espresso.beans.spinner import (
75
+ COFFEE,
76
+ DOTS,
77
+ GLOBE,
78
+ LINE,
79
+ MOON,
80
+ POINTS,
81
+ PULSE,
82
+ Spinner,
83
+ SpinnerTickMsg,
84
+ )
85
+ from espresso.beans.confetti import (
86
+ Confetti,
87
+ ConfettiMode,
88
+ ConfettiTickMsg,
89
+ Particle,
90
+ )
91
+ from espresso.beans.diff_viewer import (
92
+ DiffHunk,
93
+ DiffLine,
94
+ DiffMode,
95
+ DiffViewer,
96
+ )
97
+ from espresso.beans.form import Form, FormField, FormSubmitMsg
98
+ from espresso.beans.marquee import Marquee, MarqueeMode, MarqueeTickMsg
99
+ from espresso.beans.slider import (
100
+ RangeSlider,
101
+ RangeSliderChangeMsg,
102
+ Slider,
103
+ SliderChangeMsg,
104
+ )
105
+ from espresso.beans.sortable_list import ItemReorderedMsg, SortableItem, SortableList
106
+ from espresso.beans.sparkline import Sparkline, SparklineMode, SparklineTickMsg
107
+ from espresso.beans.spring import Spring, SpringTickMsg, SpringValue
108
+ from espresso.beans.splitter import (
109
+ Splitter,
110
+ SplitterOrientation,
111
+ SplitterResizeMsg,
112
+ )
113
+ from espresso.beans.statusbar import StatusBar, StatusSection
114
+ from espresso.beans.table import Column, Table
115
+ from espresso.beans.tabs import TabChangeMsg, TabStyle, Tabs
116
+ from espresso.beans.textarea import TextArea
117
+ from espresso.beans.textinput import EchoMode, TextInput
118
+ from espresso.beans.timer import (
119
+ Stopwatch,
120
+ StopwatchTickMsg,
121
+ Timer,
122
+ TimerTickMsg,
123
+ TimerTimeoutMsg,
124
+ )
125
+ from espresso.beans.toast import ToastDismissMsg, ToastItem, ToastLevel, ToastManager
126
+ from espresso.beans.tree import Tree, TreeNode, TreeNodeSelectMsg
127
+ from espresso.beans.viewport import Viewport
128
+
129
+ __all__ = [
130
+ # Spinner
131
+ "Spinner",
132
+ "SpinnerTickMsg",
133
+ "DOTS",
134
+ "LINE",
135
+ "PULSE",
136
+ "POINTS",
137
+ "MOON",
138
+ "GLOBE",
139
+ "COFFEE",
140
+ # TextInput
141
+ "TextInput",
142
+ "EchoMode",
143
+ # TextArea
144
+ "TextArea",
145
+ # Help & Keys
146
+ "Help",
147
+ "KeyBinding",
148
+ "KeyMap",
149
+ # Timer & Stopwatch
150
+ "Timer",
151
+ "TimerTickMsg",
152
+ "TimerTimeoutMsg",
153
+ "Stopwatch",
154
+ "StopwatchTickMsg",
155
+ # Progress
156
+ "Progress",
157
+ # Viewport
158
+ "Viewport",
159
+ # Table
160
+ "Table",
161
+ "Column",
162
+ # Paginator
163
+ "Paginator",
164
+ "PaginatorType",
165
+ # Dialog
166
+ "Dialog",
167
+ "DialogResultMsg",
168
+ # List
169
+ "List",
170
+ "ListItem",
171
+ "ListSelectMsg",
172
+ "PaginationMode",
173
+ # FilePicker
174
+ "FilePicker",
175
+ "FileEntry",
176
+ "FileSelectMsg",
177
+ "format_file_size",
178
+ # Prompts
179
+ "SelectPrompt",
180
+ "SelectSubmitMsg",
181
+ "MultiSelectPrompt",
182
+ "MultiSelectSubmitMsg",
183
+ "ConfirmPrompt",
184
+ "ConfirmSubmitMsg",
185
+ # Toast
186
+ "ToastManager",
187
+ "ToastItem",
188
+ "ToastLevel",
189
+ "ToastDismissMsg",
190
+ # Tabs
191
+ "Tabs",
192
+ "TabStyle",
193
+ "TabChangeMsg",
194
+ # Tree
195
+ "Tree",
196
+ "TreeNode",
197
+ "TreeNodeSelectMsg",
198
+ # StatusBar
199
+ "StatusBar",
200
+ "StatusSection",
201
+ # Metric
202
+ "Metric",
203
+ "MetricGroup",
204
+ "MetricLayout",
205
+ "MetricTrend",
206
+ "LayoutDirection",
207
+ # NavStack
208
+ "NavStack",
209
+ "NavEntry",
210
+ "NavPushMsg",
211
+ "NavPopMsg",
212
+ # DatePicker
213
+ "DatePicker",
214
+ "DatePickerFocus",
215
+ "DateSelectMsg",
216
+ "DateChangeMsg",
217
+ # CodeViewer
218
+ "CodeViewer",
219
+ "SyntaxTheme",
220
+ "THEME_ESPRESSO",
221
+ "THEME_DRACULA",
222
+ "THEME_MONOKAI",
223
+ "highlight_code",
224
+ # MarkdownViewer
225
+ "MarkdownViewer",
226
+ "render_markdown",
227
+ # PipelineProgress
228
+ "PipelineProgress",
229
+ "PipelineStage",
230
+ "StageStatus",
231
+ "StageStartMsg",
232
+ "StageCompleteMsg",
233
+ "StageFailedMsg",
234
+ "PipelineCompleteMsg",
235
+ # QuickFix
236
+ "QuickFix",
237
+ "QuickFixItem",
238
+ "QuickFixSelectMsg",
239
+ # DetailSelector
240
+ "DetailSelector",
241
+ "DetailItem",
242
+ "DetailSelectMsg",
243
+ # ImageViewer
244
+ "ImageViewer",
245
+ "RenderMode",
246
+ "parse_ppm",
247
+ "parse_bmp",
248
+ # Splitter
249
+ "Splitter",
250
+ "SplitterOrientation",
251
+ "SplitterResizeMsg",
252
+ # Slider
253
+ "Slider",
254
+ "RangeSlider",
255
+ "SliderChangeMsg",
256
+ "RangeSliderChangeMsg",
257
+ # Sparkline
258
+ "Sparkline",
259
+ "SparklineMode",
260
+ "SparklineTickMsg",
261
+ # Marquee
262
+ "Marquee",
263
+ "MarqueeMode",
264
+ "MarqueeTickMsg",
265
+ # SortableList
266
+ "SortableList",
267
+ "SortableItem",
268
+ "ItemReorderedMsg",
269
+ # Spring
270
+ "Spring",
271
+ "SpringValue",
272
+ "SpringTickMsg",
273
+ # Confetti
274
+ "Confetti",
275
+ "ConfettiMode",
276
+ "ConfettiTickMsg",
277
+ "Particle",
278
+ # DiffViewer
279
+ "DiffViewer",
280
+ "DiffMode",
281
+ "DiffHunk",
282
+ "DiffLine",
283
+ # Form
284
+ "Form",
285
+ "FormField",
286
+ "FormSubmitMsg",
287
+ # BarChart
288
+ "BarChart",
289
+ "BarItem",
290
+ "BarOrientation",
291
+ "BarChartSelectMsg",
292
+ # CommandPalette
293
+ "CommandPalette",
294
+ "PaletteItem",
295
+ "CommandPaletteSelectMsg",
296
+ "CommandPaletteCloseMsg",
297
+ # GitTree
298
+ "GitTree",
299
+ "GitFileNode",
300
+ "GitFileStatus",
301
+ "GitTreeSelectMsg",
302
+ "GitTreeToggleMsg",
303
+ ]
304
+
@@ -0,0 +1,301 @@
1
+ """Bar chart bean with horizontal & vertical modes, auto-scaling, and TrueColor gradients.
2
+
3
+ Inspired by rich, asciigraph, and modern terminal analytics dashboards.
4
+ Supports sub-character precision bar rendering, TrueColor linear/multi-gradients,
5
+ interactive cursor navigation, value formatters, and mouse selection.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+ from typing import Callable, Sequence
13
+
14
+ from espresso.core.keys import KeyMsg
15
+ from espresso.core.mouse import MouseAction, MouseButton, MouseMsg
16
+ from espresso.core.tea import Cmd, Model, Msg
17
+ from espresso.crema.border import ROUNDED_BORDER
18
+ from espresso.crema.gradient import multi_gradient_colors
19
+ from espresso.crema.style import Align, Style
20
+ from espresso.crema.width import string_width, strip_ansi, truncate_ansi
21
+
22
+ # Unicode fractional blocks for horizontal sub-character precision
23
+ HORIZ_BLOCKS = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
24
+
25
+ # Unicode vertical blocks for vertical columns
26
+ VERT_BLOCKS = (" ", " ", "▂", "▃", "▄", "▅", "▆", "▇", "█")
27
+
28
+
29
+ class BarOrientation(str, Enum):
30
+ """Orientation of the bar chart."""
31
+
32
+ HORIZONTAL = "horizontal"
33
+ VERTICAL = "vertical"
34
+
35
+
36
+ @dataclass
37
+ class BarItem:
38
+ """An individual data point/bar in the chart."""
39
+
40
+ label: str
41
+ value: float
42
+ color: str | None = None
43
+ formatter: Callable[[float], str] | None = None
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class BarChartSelectMsg(Msg):
48
+ """Emitted when a bar is selected in the BarChart."""
49
+
50
+ item: BarItem
51
+ index: int
52
+
53
+
54
+ class BarChart(Model):
55
+ """Interactive bar chart component with auto-scaling and TrueColor styling."""
56
+
57
+ def __init__(
58
+ self,
59
+ items: Sequence[BarItem] = (),
60
+ title: str = "Bar Chart",
61
+ orientation: BarOrientation = BarOrientation.HORIZONTAL,
62
+ width: int = 44,
63
+ height: int = 12,
64
+ min_value: float = 0.0,
65
+ max_value: float | None = None,
66
+ gradient_colors: tuple[str, ...] = ("#00E5FF", "#7D56F4", "#F7768E"),
67
+ show_values: bool = True,
68
+ show_labels: bool = True,
69
+ cursor_enabled: bool = True,
70
+ border_color: str = "#7D56F4",
71
+ ) -> None:
72
+ self.items: list[BarItem] = list(items)
73
+ self.title = title
74
+ self.orientation = orientation
75
+ self.width = max(20, width)
76
+ self.height = max(5, height)
77
+ self.min_value = min_value
78
+ self.max_value = max_value
79
+ self.gradient_colors = gradient_colors
80
+ self.show_values = show_values
81
+ self.show_labels = show_labels
82
+ self.cursor_enabled = cursor_enabled
83
+ self.border_color = border_color
84
+
85
+ self.cursor: int = 0
86
+ self.offset_x: int = 0
87
+ self.offset_y: int = 0
88
+
89
+ def set_items(self, items: Sequence[BarItem]) -> None:
90
+ """Update chart data."""
91
+ self.items = list(items)
92
+ if self.cursor >= len(self.items):
93
+ self.cursor = max(0, len(self.items) - 1)
94
+
95
+ def set_offset(self, x: int, y: int) -> None:
96
+ """Set screen offset for mouse hit testing."""
97
+ self.offset_x = x
98
+ self.offset_y = y
99
+
100
+ def _calc_max(self) -> float:
101
+ if self.max_value is not None:
102
+ return self.max_value
103
+ if not self.items:
104
+ return 1.0
105
+ return max((it.value for it in self.items), default=1.0) or 1.0
106
+
107
+ def update(self, msg: Msg) -> tuple[Model, Cmd]:
108
+ """Handle keyboard navigation and mouse interactions."""
109
+ if not self.items:
110
+ return self, None
111
+
112
+ if isinstance(msg, KeyMsg):
113
+ key = str(msg.key).lower()
114
+
115
+ if self.orientation == BarOrientation.HORIZONTAL:
116
+ if key in ("up", "k"):
117
+ self.cursor = max(0, self.cursor - 1)
118
+ return self, None
119
+ elif key in ("down", "j"):
120
+ self.cursor = min(len(self.items) - 1, self.cursor + 1)
121
+ return self, None
122
+ else:
123
+ if key in ("left", "h"):
124
+ self.cursor = max(0, self.cursor - 1)
125
+ return self, None
126
+ elif key in ("right", "l"):
127
+ self.cursor = min(len(self.items) - 1, self.cursor + 1)
128
+ return self, None
129
+
130
+ if key == "enter":
131
+ if 0 <= self.cursor < len(self.items):
132
+ item = self.items[self.cursor]
133
+ idx = self.cursor
134
+ return self, lambda: BarChartSelectMsg(item=item, index=idx)
135
+
136
+ elif isinstance(msg, MouseMsg):
137
+ rel_x = msg.x - self.offset_x
138
+ rel_y = msg.y - self.offset_y
139
+
140
+ if self.orientation == BarOrientation.HORIZONTAL:
141
+ # Rows start at y=1 (inside border)
142
+ idx = rel_y - 1
143
+ if 0 <= idx < len(self.items):
144
+ if msg.action in (MouseAction.PRESS, MouseAction.DOUBLE_CLICK):
145
+ self.cursor = idx
146
+ item = self.items[idx]
147
+ return self, lambda: BarChartSelectMsg(item=item, index=idx)
148
+ else:
149
+ pass
150
+
151
+ return self, None
152
+
153
+ def _render_horizontal(self, inner_w: int) -> list[str]:
154
+ lines: list[str] = []
155
+ max_v = self._calc_max()
156
+
157
+ max_label_w = max((string_width(it.label) for it in self.items), default=0) if self.show_labels else 0
158
+ max_label_w = min(max_label_w, inner_w // 3)
159
+
160
+ # Estimate value text width
161
+ sample_vals = [it.formatter(it.value) if it.formatter else f"{it.value:.1f}" for it in self.items]
162
+ max_val_w = max((string_width(v) for v in sample_vals), default=0) if self.show_values else 0
163
+
164
+ bar_space = max(5, inner_w - max_label_w - max_val_w - (3 if max_label_w else 0) - (2 if max_val_w else 0) - 2)
165
+
166
+ grad_stops = multi_gradient_colors(self.gradient_colors, max(2, bar_space))
167
+
168
+ for idx, it in enumerate(self.items):
169
+ is_selected = self.cursor_enabled and idx == self.cursor
170
+
171
+ # Label part
172
+ if self.show_labels:
173
+ lbl = truncate_ansi(it.label, max_label_w)
174
+ lw = string_width(lbl)
175
+ label_part = f"{lbl}{' ' * (max_label_w - lw)} │ "
176
+ else:
177
+ label_part = ""
178
+
179
+ # Value fraction
180
+ norm = max(0.0, min(1.0, (it.value - self.min_value) / max(0.001, (max_v - self.min_value))))
181
+ raw_len = norm * bar_space
182
+ full_blocks = int(raw_len)
183
+ frac_idx = int((raw_len - full_blocks) * 8)
184
+
185
+ bar_chars: list[str] = []
186
+ for b_i in range(full_blocks):
187
+ color = it.color or grad_stops[min(b_i, len(grad_stops) - 1)]
188
+ bar_chars.append(Style().foreground(color).render("█"))
189
+
190
+ if frac_idx > 0 and full_blocks < bar_space:
191
+ color = it.color or grad_stops[min(full_blocks, len(grad_stops) - 1)]
192
+ bar_chars.append(Style().foreground(color).render(HORIZ_BLOCKS[frac_idx]))
193
+ full_blocks += 1
194
+
195
+ # Pad empty space
196
+ empty_space = " " * max(0, bar_space - full_blocks)
197
+ bar_part = "".join(bar_chars) + empty_space
198
+
199
+ # Value part
200
+ if self.show_values:
201
+ v_str = it.formatter(it.value) if it.formatter else f"{it.value:.1f}"
202
+ val_part = f" {Style().bold(True).render(v_str)}"
203
+ else:
204
+ val_part = ""
205
+
206
+ prefix = "› " if is_selected else " "
207
+ pref_st = Style().foreground("#00E5FF").bold(True).render(prefix) if is_selected else prefix
208
+
209
+ row = f"{pref_st}{label_part}{bar_part}{val_part}"
210
+ row = truncate_ansi(row, inner_w)
211
+ rw = string_width(row)
212
+ if rw < inner_w:
213
+ row += " " * (inner_w - rw)
214
+
215
+ if is_selected:
216
+ row = Style().background("#2A2A3D").render(row)
217
+
218
+ lines.append(row)
219
+
220
+ return lines
221
+
222
+ def _render_vertical(self, inner_w: int) -> list[str]:
223
+ lines: list[str] = []
224
+ max_v = self._calc_max()
225
+ # Reserved height: 1 for x-axis line, 1 for labels
226
+ chart_h = max(2, self.height - 4)
227
+
228
+ if not self.items:
229
+ return [" " * inner_w] * chart_h
230
+
231
+ col_w = max(2, inner_w // len(self.items))
232
+ grad_stops = multi_gradient_colors(self.gradient_colors, max(2, chart_h))
233
+
234
+ grid: list[list[str]] = [[" " * col_w for _ in self.items] for _ in range(chart_h)]
235
+
236
+ for col_idx, it in enumerate(self.items):
237
+ norm = max(0.0, min(1.0, (it.value - self.min_value) / max(0.001, (max_v - self.min_value))))
238
+ raw_h = norm * chart_h
239
+ full_blocks = int(raw_h)
240
+ frac_idx = int((raw_h - full_blocks) * 8)
241
+
242
+ for h in range(full_blocks):
243
+ row_idx = chart_h - 1 - h
244
+ color = it.color or grad_stops[min(h, len(grad_stops) - 1)]
245
+ grid[row_idx][col_idx] = Style().foreground(color).render("█" * (col_w - 1) + " ")
246
+
247
+ if frac_idx > 0 and full_blocks < chart_h:
248
+ row_idx = chart_h - 1 - full_blocks
249
+ color = it.color or grad_stops[min(full_blocks, len(grad_stops) - 1)]
250
+ block_ch = VERT_BLOCKS[frac_idx]
251
+ grid[row_idx][col_idx] = Style().foreground(color).render((block_ch * (col_w - 1)) + " ")
252
+
253
+ for row in grid:
254
+ r_str = "".join(row)
255
+ r_w = string_width(r_str)
256
+ if r_w < inner_w:
257
+ r_str += " " * (inner_w - r_w)
258
+ lines.append(r_str)
259
+
260
+ # Axis line
261
+ lines.append(Style().foreground("#555555").render("─" * inner_w))
262
+
263
+ # Labels line
264
+ lbl_parts: list[str] = []
265
+ for i, it in enumerate(self.items):
266
+ is_sel = self.cursor_enabled and i == self.cursor
267
+ lbl = truncate_ansi(it.label, col_w - 1)
268
+ lw = string_width(lbl)
269
+ padded = lbl + (" " * (col_w - lw))
270
+ if is_sel:
271
+ padded = Style().foreground("#00E5FF").bold(True).render(padded)
272
+ lbl_parts.append(padded)
273
+ l_row = "".join(lbl_parts)
274
+ l_w = string_width(l_row)
275
+ if l_w < inner_w:
276
+ l_row += " " * (inner_w - l_w)
277
+ lines.append(l_row)
278
+
279
+ return lines
280
+
281
+ def view(self) -> str:
282
+ """Render the bar chart."""
283
+ inner_w = self.width - 2
284
+
285
+ if self.orientation == BarOrientation.HORIZONTAL:
286
+ lines = self._render_horizontal(inner_w)
287
+ else:
288
+ lines = self._render_vertical(inner_w)
289
+
290
+ # Pad height
291
+ while len(lines) < self.height - 2:
292
+ lines.append(" " * inner_w)
293
+
294
+ border_st = (
295
+ Style()
296
+ .border(ROUNDED_BORDER)
297
+ .border_foreground(self.border_color)
298
+ .border_title(f" {self.title} " if self.title else None)
299
+ .width(inner_w)
300
+ )
301
+ return border_st.render("\n".join(lines))