Skip to content

Commit

Permalink
Wrote usage guide for README.md
Browse files Browse the repository at this point in the history
Moved font scanning logic into figletwidget.py
added demo.gif
  • Loading branch information
edward-jazzhands committed Oct 25, 2024
1 parent 398e616 commit b205904
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 101 deletions.
166 changes: 122 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,88 +13,166 @@
```

Base package - includes 10 fonts (41kb):
`pip install textual-pyfiglet`

```
pip install textual-pyfiglet
```
Install with extended fonts collection - 519 fonts (1.6mb):
`pip install textual-pyfiglet[fonts]`

```
pip install textual-pyfiglet[fonts]
```
------------------------------------------

Textual-PyFiglet is an implementation of [PyFiglet](https://github.com/pwaller/pyfiglet) for [Textual](https://github.com/Textualize/textual).

It provides a `FigletWidget` which is designed to be easy to use, and blend in with how Textual works.
It provides a `FigletWidget` which is designed to be easy to use inside of Textual.

![Demo GIF](demo.gif)

## Key features
# Key features


### Textual-PyFiglet is a fork of PyFiglet:

The original PyFiglet has zero dependencies, since it's a full re-write of FIGlet in Python. Thus by forking, Textual-PyFiglet also has zero dependencies (aside from Textual of course). PyFiglet is bundled inside as a sub-package.
The original PyFiglet has zero dependencies, since it's a full re-write of FIGlet in Python. Thus by forking, Textual-PyFiglet also has zero dependencies (aside from Textual of course). PyFiglet is bundled inside as a sub-package.

I made sure to preserve the full git history of PyFiglet, as well as its original CLI and demo (see Demo Program below).
I made sure to preserve the full git history of PyFiglet, as well as its original CLI and demo (see Demo Program below).

### Greatly minimized:

PyFiglet wheel: **1.1 MB**. --> Textual-PyFiglet wheel: **41 KB**.
PyFiglet wheel: **1.1 MB**. --> Textual-PyFiglet wheel: **41 KB**.

99% of the size of PyFiglet is just the massive fonts collection, 519 in total. In the base textual-pyfiglet package I've included only 10 of the best minimal fonts. I've also made it very easy to download the full collection for those who still want it (use extended fonts install, shown at the top)
99% of the size of PyFiglet is just the massive fonts collection, 519 in total. In the base textual-pyfiglet package I've included only 10 of the best minimal fonts. I've also made it very easy to download the full collection for those who still want it (use extended fonts install, shown at the top)

### Widget easily drops into your Textual app:

The widget is based on `Static` and is designed to mimick its behavior. That means it can drop-in replace any Static widget, and it should just work without even adding or changing arguments (using default font). Assuming you're accounting for the size of the text somehow.

It achieves this by simply overriding the `update()` method in Static. When update is called, PyFiglet will convert the input text, and PyFiglet's output is passed to `self.renderable`. So it should drop in nicely if you have any Static widgets that are using these attributes.
The widget is based on `Static` and is designed to mimick its behavior. That means it can drop-in replace any Static widget, and it should just work without even adding or changing arguments (using default font). Assuming you're accounting for the size of the text somehow.

By default, the FigletWidget will automatically set its own size when it updates. (width and height are set to auto). But, **it will also respect any container or widget it's inside of, and wrap the text accordingly.**
It achieves this by simply overriding the `update()` method in Static. When update is called, PyFiglet will convert the input text, and PyFiglet's output is passed to `self.renderable`.

Note that **this behavior can be toggled**. By default it will wrap on resize, but this can be turned off, in which case it will crop.
By default, the FigletWidget will automatically set its own size when it updates. (width and height are set to auto). But, **it will also respect any container or widget it's inside of, and wrap the text accordingly.**

### Real-time updating:

It's easy to make ASCII text that real-time reflects text input.
This real-time updating can be seen in the included demo, as well as the OG online generator:
https://patorjk.com/software/taag/

This real-time updating feature can be seen in the included demo, as well as the OG online generator:
https://patorjk.com/software/taag/
It's easy to implement in your own Textual app. See below.

For instance, if using a TextArea widget to enter text, simply update the FigletWidget with each keystroke:
### Extended fonts collection moved to separate package:

```python
@on(TextArea.Changed)
def text_changed(self):
text = self.query_one("#text_input").text
self.query_one("#figlet").update(text)
```
PyFiglet is more than fast enough to handle this.
If you want the whole collection, simply use:
`pip install textual-pyfiglet[fonts]`

### Extended fonts collection moved to separate package:
You can install the whole thing straight from that command, or use it to add the fonts to an existing install. The fonts package is about 1.5 MB (compressed).

If you want the whole collection, simply use:
`pip install textual-pyfiglet[fonts]`
The included 10 fonts I hand-picked were chosen for being small, minimalist, and professional looking. For people that don't want to think about fonts, these 10 will probably be all you need.

You can install the whole thing straight from that command, or use it to add the fonts to an existing install. The fonts package is about 1.5 MB (compressed). Hey, when you're making CLI tools, being light-weight matters. That's why the extended collection has been made optional - Now you decide if you need it.
You can also easily add more fonts by just downloading individual font files the oldschool way, and plopping them in the fonts folder (inside the Pyfiglet folder, which will be in /lib/python/site-packages/textual-pyfiglet)

The included 10 fonts I hand-picked were chosen for being small, minimalist, and professional looking. For people that don't want to think about fonts, these 10 will probably be all you need.
A good website to download individual fonts:
http://www.jave.de/figlet/fonts/overview.html

You can also easily add more fonts by just downloading individual font files the oldschool way, and plopping them in the fonts folder (inside the Pyfiglet folder, which will be in /lib/python/site-packages/textual-pyfiglet)
# Usage

A good website to download individual fonts:
http://www.jave.de/figlet/fonts/overview.html
### Demo program, CLI:
Run the demo program with either:
`textual-pyfiglet`
Or:
`python -m textual_pyfiglet`

Note: If you download the extended fonts pack, the first time you run textual-pyfiglet, it will need a few seconds to copy all of the fonts. It will only do this one time.
PyFiglet also has its own CLI which has been kept available. (Which has its own built-in demo program.) You can access the PyFiglet CLI with:
`python -m textual_pyfiglet.pyfiglet`

### Demo program included
Try it out to see the options. For instance, try running this code:
`python -m textual_pyfiglet.pyfiglet Hey guys, whats up?`

Run the demo program with either:
`textual-pyfiglet`
Or:
`python -m textual_pyfiglet`
# How to use:

PyFiglet also has its own CLI which has been kept available. (Which has its own built-in demo program.) You can access the PyFiglet CLI with:
`python -m textual_pyfiglet.pyfiglet`
FigletWidget is designed to be used like a normal Static widget.

Try it out to see the options. For instance, try running this code:
`python -m textual_pyfiglet.pyfiglet Hey guys, whats up?`
You can simply create one with the Textual syntax:

```python
from textual_pyfiglet import FigletWidget

def compose(self):
yield FigletWidget("Label of Things", id="figlet1")
```

In this case it will use the default font, Calvin_s. You can also specify a font in the constructor:

```python
yield FigletWidget("Label of Things", id="figlet1" font="small")
```

## Resizing

The FigletWidget will try to wrap to whatever parent container it is inside of. If you want to contain it to an area, it's best to place it inside a container:

```python
with Container(id="figlet_container1", classes="figlet_labels"):
yield FigletWidget("Label of Things", id="figlet1")
```

You can resize the container, and then trigger the FigletWidget update method:

```python
def resize_container(self, width: int):
self.query("#figlet_container1").styles.width = width
self.query("#figlet1").update(resized=True)
```
Note that whenever you are updating to resize the widget's render area, you must use the `resized=True` argument.

Likewise you can set it to resize when the screen size changes by calling it from the main app. This is assuming it's inside a container which would be affected by this.

```python
class MyTextualApp(App):

def on_resize(self):
self.query("#figlet1").update(resized=True)
```
## Change the font:

The widget will update automatically when this is run:
```python
self.query("#figlet1").set_font(new_font)
```
| Base fonts | |
|-------------|----------------|
| calvin_s | smblock
| chunky | smbraille
| cybermedium | standard
| small_slant | stick_letters
| small | tmplr

If the extended fonts pack is not installed, the widget will do a quick check every launch to see if its been downloaded. So you can install it afterwards any time you feel like it.

## Live updating / Passing text

To update the FigletWidget with new text, simply pass it in the update method:

```python
self.query_one("#figlet1").update(text)
```

For instance, if you have a TextArea widget where a user can enter text, you can do this:

```python
@on(TextArea.Changed)
def text_changed(self):
text = self.query_one("#text_input").text
self.query_one("#figlet1").update(text)
```
The FigletWidget will then auto-update with every key-stroke.
Note that you cannot pass in a Rich renderable, like the normal Static widget - the text has to be a normal string for PyFiglet to work.

You can access two lists of installed fonts through methods in the FigletWidget:

```python
figlet1 = self.query_one("#figlet1")
all_fonts = figlet1.get_fonts_list(get_all=True)
base_fonts = figlet1.get_fonts_list(get_all=False) # only get standard 10
```

-----------------------------------
## Thanks and Copyright
Expand Down
Binary file added demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual-pyfiglet"
version = "0.2.0"
version = "0.3.0"
description = "A Widget implementation of PyFiglet for Textual"
authors = ["edward-jazzhands <ed.jazzhands@gmail.com>"]
license = "MIT"
Expand Down
72 changes: 20 additions & 52 deletions textual_pyfiglet/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,36 +45,6 @@
from .pyfiglet import fonts
from . import extended_fonts_installed

base_fonts = [
'calvin_s',
'chunky',
'cybermedium',
'small_slant',
'small',
'smblock',
'smbraille',
'standard',
'stick_letters',
'tmplr'
]

def get_all_fonts_list(show_all: bool = False) -> list:
"""Scans the fonts folder.
Returns a list of all font filenames (without extensions)."""

if not show_all:
return base_fonts

# first get the path of the fonts package:
package_path = os.path.dirname(fonts.__file__)
path_list = os.listdir(package_path)
fonts_list = []

for filename in path_list:
if not filename.endswith('.py') and not filename.startswith('__pycache__'):
fonts_list.append(os.path.splitext(filename)[0])
return fonts_list


class PyFigletDemo(App):

Expand All @@ -85,20 +55,15 @@ class PyFigletDemo(App):
]

CSS_PATH = "styles.tcss"

fonts_list = get_all_fonts_list()
fonts_list.sort()
font_options = [(font, font) for font in fonts_list]
current_font = 'standard'

# utility timers, used for notifications
# debounce timers, used for notifications
timer1 = None
timer2 = None

def compose(self):

self.log("PyFiglet Demo started.")
self.log(f"Available fonts: {self.fonts_list}")
self.log(f"Extended fonts installed: {extended_fonts_installed}")

yield Header("PyFiglet Demo")
Expand All @@ -114,14 +79,14 @@ def compose(self):

with VerticalScroll(id="main_window"):
with Container(id="figlet_container"):
yield FigletWidget(id="figlet_widget", font=self.current_font)
yield FigletWidget(id="figlet_widget")

with Container(id="bottom_bar"):
with Horizontal():
yield Static(id="notification_box1")
yield Static(id="notification_box2")
with Horizontal():
yield Select(options=self.font_options, value=self.current_font, id="font_select", allow_blank=False)
yield Select([], id="font_select", allow_blank=True)
with Horizontal(id="container_of_switch"):
yield Switch(False, id="switch")
yield Label("Show all \nfonts")
Expand All @@ -133,25 +98,26 @@ def on_mount(self):

self.figlet_widget = cast(FigletWidget, self.query_one("#figlet_widget"))
self.figlet_container = cast(Container, self.query_one("#figlet_container"))

self.font_select = cast(Select, self.query_one("#font_select")) # chad type hinting convenience vars
self.text_input = cast(TextArea, self.query_one("#text_input"))
self.font_select = cast(Select, self.query_one("#font_select"))
self.text_input = cast(TextArea, self.query_one("#text_input")) # chad type hinting convenience vars
self.font_switch = cast(Switch, self.query_one("#switch"))
self.notification1 = cast(Label, self.query_one("#notification_box1"))
self.notification2 = cast(Label, self.query_one("#notification_box2"))
self.width_input = cast(Input, self.query_one("#width_input"))
self.height_input = cast(Input, self.query_one("#height_input"))

self.set_timer(0.05, self.set_starter_text)
# The timer is because starting with text in the TextArea makes it glitch out.
# Giving it a 50ms delay to set the text fixes the problem.
self.fonts_list = self.figlet_widget.get_fonts_list(get_all=True)
self.base_fonts = self.figlet_widget.get_fonts_list(get_all=False)
self.fonts_list.sort()
self.font_options = [(font, font) for font in self.base_fonts]
self.font_select.set_options(self.font_options)
self.font_select.value = self.current_font

# This just sets the cursor to the end of the text in the TextArea when the app starts:
def set_starter_text(self):
self.text_input.focus()
end = self.text_input.get_cursor_line_end_location()
self.text_input.move_cursor(end)


# NOTE: about the resize event:
# The widget is not capable of automatically responding to screen resize events.
# You have to call the update method manually when the screen is resized if you want it
Expand All @@ -167,22 +133,24 @@ def on_resize(self, event: Resize):

@on(Select.Changed)
def font_changed(self, event: Select.Changed) -> None:
if event.value == Select.BLANK:
return
self.figlet_widget.set_font(event.value)
self.current_font = event.value
self.log(f"Current font set to: {self.current_font}")

@on(Switch.Changed)
def toggle_fonts(self, event: Switch.Changed) -> None:

self.fonts_list = get_all_fonts_list(event.value)
self.fonts_list.sort()
self.font_options = [(font, font) for font in self.fonts_list]
if event.value: # turn on extended
self.font_options = [(font, font) for font in self.fonts_list]
else:
self.font_options = [(font, font) for font in self.base_fonts]
self.font_select.set_options(self.font_options)

if not event.value: # if turning switch off:
if self.current_font not in base_fonts: # if the current font is not in the base fonts list,
if self.current_font not in self.base_fonts: # if the current font is not in the base fonts list,
self.current_font = 'standard' # set back to standard.
self.figlet_widget.set_font('standard') # Keeps font the same when toggling switch, if it can.
self.figlet_widget.set_font('standard') # Keeps font the same when toggling switch, if it can.
self.font_select.value = self.current_font

if event.value:
Expand Down
Loading

0 comments on commit b205904

Please sign in to comment.