Skip to content

feat(bar): Add support for custom icons #5963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions src/textual/renderables/bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class Bar:
gradient: Optional gradient object.
"""

HALF_BAR_LEFT: str = "╺"
BAR: str = "━"
HALF_BAR_RIGHT: str = "╸"

def __init__(
self,
highlight_range: tuple[float, float] = (0, 0),
Expand All @@ -40,10 +44,6 @@ def __rich_console__(
highlight_style = console.get_style(self.highlight_style)
background_style = console.get_style(self.background_style)

half_bar_right = "╸"
half_bar_left = "╺"
bar = "━"

width = self.width or options.max_width
start, end = self.highlight_range

Expand All @@ -53,7 +53,7 @@ def __rich_console__(
output_bar = Text("", end="")

if start == end == 0 or end < 0 or start > end:
output_bar.append(Text(bar * width, style=background_style, end=""))
output_bar.append(Text(self.BAR * width, style=background_style, end=""))
yield output_bar
return

Expand All @@ -67,34 +67,40 @@ def __rich_console__(

# Initial non-highlighted portion of bar
output_bar.append(
Text(bar * (int(start - 0.5)), style=background_style, end="")
Text(self.BAR * (int(start - 0.5)), style=background_style, end="")
)
if not half_start and start > 0:
output_bar.append(Text(half_bar_right, style=background_style, end=""))
output_bar.append(Text(self.HALF_BAR_RIGHT, style=background_style, end=""))

highlight_bar = Text("", end="")
# The highlighted portion
bar_width = int(end) - int(start)
if half_start:
highlight_bar.append(
Text(
half_bar_left + bar * (bar_width - 1), style=highlight_style, end=""
self.HALF_BAR_LEFT + self.BAR * (bar_width - 1),
style=highlight_style,
end="",
)
)
else:
highlight_bar.append(Text(bar * bar_width, style=highlight_style, end=""))
highlight_bar.append(
Text(self.BAR * bar_width, style=highlight_style, end="")
)
if half_end:
highlight_bar.append(Text(half_bar_right, style=highlight_style, end=""))
highlight_bar.append(
Text(self.HALF_BAR_RIGHT, style=highlight_style, end="")
)

if self.gradient is not None:
_apply_gradient(highlight_bar, self.gradient, width)
output_bar.append(highlight_bar)

# The non-highlighted tail
if not half_end and end - width != 0:
output_bar.append(Text(half_bar_left, style=background_style, end=""))
output_bar.append(Text(self.HALF_BAR_LEFT, style=background_style, end=""))
output_bar.append(
Text(bar * (int(width) - int(end) - 1), style=background_style, end="")
Text(self.BAR * (int(width) - int(end) - 1), style=background_style, end="")
)

# Fire actions when certain ranges are clicked (e.g. for tabs)
Expand Down
11 changes: 8 additions & 3 deletions src/textual/widgets/_progress_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ def __init__(
disabled: bool = False,
clock: Clock | None = None,
gradient: Gradient | None = None,
bar_renderable: BarRenderable = BarRenderable,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not convinced this is something that warrants being in the constructor. It feels more like an internal detail than configuration.

How about we expose the bar renderable as a classvar?

Something like this:

class FancyProgress(ProgressBar):
    BAR_RENDERABLE_CLASS = FancyBar

Then you wouldn't have to specify it everywhere you use the progress bar.

Copy link
Author

@NSPC911 NSPC911 Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the bar renderable is passed in as a class variable into the Bar element, what about the main ProgressBar element?

I'm not sure how the ProgressBar element can receive the Bar Renderable to be passed to the Bar widget as a class variable before being rendered

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also a tad bit confused on the comment, because your review comment is on the Bar element while your code example is for the ProgressBar element

):
"""Create a bar for a [`ProgressBar`][textual.widgets.ProgressBar]."""
self._clock = (clock or Clock()).clone()
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self.set_reactive(Bar.gradient, gradient)
self.bar_renderable = bar_renderable

def _validate_percentage(self, percentage: float | None) -> float | None:
"""Avoid updating the bar, if the percentage increase is too small to render."""
Expand Down Expand Up @@ -104,7 +106,7 @@ def render(self) -> RenderResult:
if self.percentage < 1
else self.get_component_rich_style("bar--complete")
)
return BarRenderable(
return self.bar_renderable(
highlight_range=(0, self.size.width * self.percentage),
highlight_style=Style.from_color(bar_style.color),
background_style=Style.from_color(bar_style.bgcolor),
Expand Down Expand Up @@ -133,7 +135,7 @@ def render_indeterminate(self) -> RenderResult:
end = start + highlighted_bar_width

bar_style = self.get_component_rich_style("bar--indeterminate")
return BarRenderable(
return self.bar_renderable(
highlight_range=(max(0, start), min(end, width)),
highlight_style=Style.from_color(bar_style.color),
background_style=Style.from_color(bar_style.bgcolor),
Expand Down Expand Up @@ -239,6 +241,7 @@ def __init__(
disabled: bool = False,
clock: Clock | None = None,
gradient: Gradient | None = None,
bar_renderable: BarRenderable = BarRenderable,
):
"""Create a Progress Bar widget.

Expand All @@ -265,6 +268,7 @@ def key_space(self):
disabled: Whether the widget is disabled or not.
clock: An optional clock object (leave as default unless testing).
gradient: An optional Gradient object (will replace CSS styles in the bar).
bar_renderable: A custom Bar object that is rendered as the bar.
"""
self._clock = clock or Clock()
self._eta = ETA()
Expand All @@ -274,6 +278,7 @@ def key_space(self):
self.show_percentage = show_percentage
self.show_eta = show_eta
self.set_reactive(ProgressBar.gradient, gradient)
self.bar_renderable = bar_renderable

def on_mount(self) -> None:
self.update()
Expand All @@ -283,7 +288,7 @@ def on_mount(self) -> None:
def compose(self) -> ComposeResult:
if self.show_bar:
yield (
Bar(id="bar", clock=self._clock)
Bar(id="bar", clock=self._clock, bar_renderable=self.bar_renderable)
.data_bind(ProgressBar.percentage)
.data_bind(ProgressBar.gradient)
)
Expand Down