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

Conversation

NSPC911
Copy link

@NSPC911 NSPC911 commented Jul 15, 2025

You can now super a textual.renderable.bar.Bar class and change HALF_BAR_LEFT, BAR, and HALF_BAR_RIGHT, before passing the class as a keyword argument in the textual.widgets.ProgressBar widget as bar_renderable. The keyword arg's name might be weird.

resolves #5350

Image:
image

Code Snippet

from textual.app import App, ComposeResult
from textual.renderables.bar import Bar as BarRenderable
from textual.widgets import ProgressBar
from textual.color import Gradient
class SignsRenderable(BarRenderable):
    HALF_BAR_LEFT = "-"
    BAR = "="
    HALF_BAR_RIGHT = "-"
class ArrowsRenderable(BarRenderable):
    HALF_BAR_LEFT = ">"
    BAR = "="
    HALF_BAR_RIGHT = ">"
class ThickBarRenderable(BarRenderable):
    HALF_BAR_LEFT = "▐"
    BAR = "█"
    HALF_BAR_RIGHT = "▌"
class SlashBarRenderable(BarRenderable):
    HALF_BAR_LEFT = "\\"
    BAR = "|"
    HALF_BAR_RIGHT = "/"
class Application(App):
    DEFAULT_CSS = """
    .bar--indeterminate { color: #bf616a !important; background: #2e3440 !important }
    """
    def compose(self) -> ComposeResult:
        gradient = Gradient.from_colors("#8fbcbb", "#88c0d0", "#81a1c1", "#5e81ac")
        for widget in [ThickBarRenderable, SignsRenderable, SlashBarRenderable, ArrowsRenderable]:
            yield ProgressBar(bar_renderable=widget, gradient=gradient)
        yield ProgressBar(gradient=gradient)
    def on_mount(self) -> None:
        self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)
        self.set_timer(4.0, self.start_progress)
    def start_progress(self) -> None:
        for bar in self.query(ProgressBar):
            bar.update(total=100)
        self.progress_timer.resume()
    def make_progress(self) -> None:
        for bar in self.query(ProgressBar):
            bar.advance(1)
Application().run()

Please review the following checklist.

  • Docstrings on all new or modified functions / classes
  • Updated documentation
  • Updated CHANGELOG.md (where appropriate)

@@ -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

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of a classvar, but I see your problem, @NSPC911. It is ProgressBar -> Bar -> BarRenderable and you want to change the renderable. It feels a bit heavy-handed to create two classvars where every user should have to do:

class FancyBarRenderable(BarRenderable):
    ...

class FancyBar(Bar):
    BAR_RENDERABLE_CLASS = FancyBarRenderable
    
class FancyProgressBar(ProgressBar):
    BAR_CLASS = FancyBar

where the whole FancyBar is just needed to connect the dots between FancyProgressBar and FancyBarRenderable. Maybe use a classvar only on ProgressBar and do pass the BarRenderable into the constructor of Bar? So the user can do:

class FancyBarRenderable(BarRenderable):
    ...

class FancyProgressBar(ProgressBar):
    BAR_RENDERABLE_CLASS = FancyBarRenderable

Copy link
Author

@NSPC911 NSPC911 Aug 2, 2025

Choose a reason for hiding this comment

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

How should ProgressBar send the renderable to the Bar widget without it having an input in the constructor of the Bar class?

Copy link
Contributor

Choose a reason for hiding this comment

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

How should ProgressBar send the renderable to the Bar widget without it having an input in the constructor of the Bar class?

Don't use it in the init of ProgressBar, but do use it in the init of Bar. So you get the benefit of quickly defining your fancy progress bar without having to pass in the renderable every time you need it or needing to override the init. You just pass it as a class var. Internally, the ProgressBar does pass on the renderable from the classvar to the Bar widget.

@NSPC911 NSPC911 requested a review from willmcgugan July 31, 2025 05:25
@NSPC911 NSPC911 force-pushed the custom-progress-bar branch from 58e5656 to 1339ce7 Compare August 2, 2025 12:41
@NSPC911 NSPC911 requested a review from davidfokkema August 2, 2025 12:44
Copy link
Contributor

@davidfokkema davidfokkema left a comment

Choose a reason for hiding this comment

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

LGTM!

@NSPC911 NSPC911 force-pushed the custom-progress-bar branch from 1339ce7 to b2aded2 Compare August 2, 2025 15:46
@NSPC911 NSPC911 requested a review from davidfokkema August 2, 2025 15:47
Copy link
Contributor

@davidfokkema davidfokkema left a comment

Choose a reason for hiding this comment

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

I had two comments regarding type hinting, otherwise seems to work great!

@davidfokkema
Copy link
Contributor

I see that you are force pushing commits to this PR instead of pushing new commits. I've never done that myself, but is there a benefit to that I'm unfamiliar with?

@NSPC911
Copy link
Author

NSPC911 commented Aug 3, 2025

I see that you are force pushing commits to this PR instead of pushing new commits. I've never done that myself, but is there a benefit to that I'm unfamiliar with?

keeps changes to a single commit, I'm just used to doing this

You can super a textual.renderable.bar.Bar class and change `HALF_BAR_LEFT`, `BAR`, and `HALF_BAR_RIGHT`, before passing the class as an keyword argument in the textual.widgets.ProgressBar widget as `bar_renderable`.
The keyword arg's name is weird, I'm not sure what to name it
@NSPC911 NSPC911 force-pushed the custom-progress-bar branch from b2aded2 to f23a571 Compare August 3, 2025 11:18
@NSPC911 NSPC911 requested a review from davidfokkema August 3, 2025 11:18
@davidfokkema
Copy link
Contributor

keeps changes to a single commit, I'm just used to doing this

The downside of force-pushing is that git complains when I want to test your new changes:

hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

I'll force it and have another look.

Copy link
Contributor

@davidfokkema davidfokkema left a comment

Choose a reason for hiding this comment

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

I think this is ready, pending @willmcgugan's blessing.

@NSPC911
Copy link
Author

NSPC911 commented Aug 4, 2025

keeps changes to a single commit, I'm just used to doing this

The downside of force-pushing is that git complains when I want to test your new changes:

git fetch
git reset --hard <remote>/<branch>

usually solves it for me

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.

Customize progress bar with arbitrary bar text
3 participants