Skip to content

Commit bd19064

Browse files
feat: implement auto-sizing feature for banners and enhance CLI with paste support
- Added auto-sizing functionality based on terminal dimensions. - Updated CLI to support clipboard pasting in text input fields. - Improved rendering stability for multiple effects in banners. - Updated documentation to reflect new features and changes. - Bump version to 2.2.3.
1 parent 8fb5dc0 commit bd19064

15 files changed

Lines changed: 475 additions & 39 deletions

File tree

.github/workflows/release-binaries.yml

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,32 +65,60 @@ jobs:
6565
run: |
6666
printf 'from bangen.app import main\nif __name__=="__main__": main()\n' > _entry.py
6767
68+
# Extract dependencies from pyproject.toml and generate --collect-all flags
69+
COLLECT_ALL_FLAGS=$(python3 - <<'PYTHON_SCRIPT'
70+
import tomllib
71+
72+
# Package name to module name mapping
73+
pkg_to_module = {
74+
"Pillow": "PIL",
75+
"pyfiglet": "pyfiglet",
76+
"pyperclip": "pyperclip",
77+
"rich": "rich",
78+
"typer": "typer",
79+
}
80+
81+
with open("pyproject.toml", "rb") as f:
82+
config = tomllib.load(f)
83+
deps = config["project"]["dependencies"]
84+
85+
# Extract package names and convert to module names
86+
modules = ["bangen"] # Always include bangen
87+
for dep in deps:
88+
# Handle dependencies like "pyfiglet>=1.0" -> extract "pyfiglet"
89+
pkg_name = dep.split(">=")[0].split("==")[0].split("<=")[0].split("<")[0].split(">")[0]
90+
module_name = pkg_to_module.get(pkg_name, pkg_name)
91+
modules.append(module_name)
92+
93+
# Generate --collect-all flags
94+
flags = " ".join([f"--collect-all {m}" for m in modules])
95+
print(flags)
96+
PYTHON_SCRIPT
97+
)
98+
6899
docker run --rm \
69100
-u $(id -u):$(id -g) \
70101
-e HOME=/tmp \
71102
-e PIP_NO_CACHE_DIR=1 \
72103
-v "${{ github.workspace }}:/workspace" \
73104
-w /workspace \
74105
ghcr.io/yt-dlp/manylinux2014_x86_64-shared \
75-
bash -c '
106+
bash -c "
76107
set -euo pipefail
77108
py3.13 -m pip install -U pip wheel pyinstaller --no-cache-dir
78-
py3.13 -m pip install ".[all]" --no-cache-dir
109+
py3.13 -m pip install \".[all]\" --no-cache-dir
79110
80111
py3.13 -m PyInstaller --onefile --noconfirm --strip --noupx \
81112
--name bangen \
82113
--distpath dist \
83114
--workpath build \
84-
--collect-all bangen \
85-
--collect-all pyfiglet \
86-
--collect-all rich \
87-
--collect-all PIL \
88-
--collect-all typer \
115+
$COLLECT_ALL_FLAGS \
89116
--optimize 2 \
90117
_entry.py
91118
92119
strings dist/bangen | grep GLIBC_ | sort -V | uniq
93-
'
120+
"
121+
94122
95123
- run: |
96124
mkdir -p release
@@ -135,7 +163,39 @@ jobs:
135163
shell: bash
136164
run: |
137165
printf 'from bangen.app import main\nif __name__=="__main__": main()\n' > _entry.py
138-
python -m PyInstaller --onefile --noconfirm --noupx --name bangen --distpath dist --workpath build --optimize 2 _entry.py
166+
167+
# Extract dependencies from pyproject.toml and generate --collect-all flags
168+
COLLECT_ALL_FLAGS=$(python3 - <<'PYTHON_SCRIPT'
169+
import tomllib
170+
171+
# Package name to module name mapping
172+
pkg_to_module = {
173+
"Pillow": "PIL",
174+
"pyfiglet": "pyfiglet",
175+
"pyperclip": "pyperclip",
176+
"rich": "rich",
177+
"typer": "typer",
178+
}
179+
180+
with open("pyproject.toml", "rb") as f:
181+
config = tomllib.load(f)
182+
deps = config["project"]["dependencies"]
183+
184+
# Extract package names and convert to module names
185+
modules = ["bangen"] # Always include bangen
186+
for dep in deps:
187+
# Handle dependencies like "pyfiglet>=1.0" -> extract "pyfiglet"
188+
pkg_name = dep.split(">=")[0].split("==")[0].split("<=")[0].split("<")[0].split(">")[0]
189+
module_name = pkg_to_module.get(pkg_name, pkg_name)
190+
modules.append(module_name)
191+
192+
# Generate --collect-all flags
193+
flags = " ".join([f"--collect-all {m}" for m in modules])
194+
print(flags)
195+
PYTHON_SCRIPT
196+
)
197+
198+
python -m PyInstaller --onefile --noconfirm --noupx --name bangen --distpath dist --workpath build $COLLECT_ALL_FLAGS --optimize 2 _entry.py
139199
140200
- name: Package
141201
shell: bash

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
# Changelog
22

3+
## [2.2.3]
4+
5+
### Added
6+
- `pyperclip` dependency for improved clipboard paste support in TUI text input boxes
7+
- GitHub Actions workflow now dynamically extracts dependencies from `pyproject.toml` instead of hardcoding them for PyInstaller builds
8+
9+
### Changed
10+
- Auto-sizing is now **enabled by default** — use `--no-auto-size` to disable it
11+
- Auto-sizing no longer requires the `--auto-size` flag to activate; default behavior now includes automatic terminal-based sizing
12+
- Text input fields in export and preset dialogs now support full cursor movement with arrow keys (← →)
13+
- Text input now supports mid-string character insertion and deletion instead of append-only editing
14+
- Paste operations in text boxes now insert at the current cursor position instead of appending to the end
15+
16+
### Fixed
17+
- Text cursor now displays at the actual insertion point in input fields
18+
- Backspace in text boxes now removes the character at the cursor position instead of always removing the last character
19+
320
## [2.2.2]
21+
22+
### Changed
23+
424
- Temporal effects (`fade_in`, `wipe`, `stagger`) now perform continuous looping animations instead of stopping after initial transition
525

626
## [2.2.1]

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Built for terminal art, title cards, intros, and animated text that still feels
4040

4141
- Live split-screen TUI with export modal
4242
- Static and animated banner rendering
43+
- Auto-sizing based on terminal and text dimensions
4344
- Transparent `PNG` and animated transparent `GIF` export
4445
- Plain `TXT` export with exact ASCII output
4546
- CLI export progress with percentage, elapsed time, ETA, and status text
@@ -123,13 +124,19 @@ Controls:
123124
- `↑↓` navigate fields and effects
124125
- `←→` adjust font or numeric settings
125126
- `Enter` edit or toggle the selected field
127+
- `Ctrl+V` paste from clipboard (in text input fields)
128+
- `a` toggle auto-size info display (shows terminal-relative sizing)
126129
- `l` load a saved preset or load from a custom preset file
127130
- `e` open the export dialog
128131
- `s` save the current preset
129132
- `q` quit
130133

131134
The effect selector is windowed, so you can move through the full library without overflowing the controls panel.
132135

136+
**Text Input:** All text input boxes (text, path, gradient) now support pasting from your clipboard using `Ctrl+V`, making it easier to work with long or complex values.
137+
138+
**Auto-Size Info:** Press `a` to toggle a display of the calculated banner sizing in relation to your terminal dimensions. Shows: text dimensions, calculated canvas size, scale factor, and padding.
139+
133140
### Export Dialog 📦
134141

135142
Press `e` inside the TUI to open the exporter.
@@ -180,13 +187,40 @@ bangen "HELLO" --effect wave --effect glow --export-gif banner.gif --gif-duratio
180187

181188
CLI exports show a live progress bar with percentage, elapsed time, ETA, and the current export stage.
182189

190+
#### Auto-Size
191+
192+
Auto-sizing is **enabled by default**. It automatically adjusts banner width/height based on terminal and text dimensions for optimal rendering and exports.
193+
194+
Disable auto-sizing if needed:
195+
196+
```bash
197+
bangen "HELLO" --no-auto-size
198+
bangen "HELLO" --no-auto-size --export-txt banner.txt
199+
```
200+
201+
Enable it explicitly (already default):
202+
203+
```bash
204+
bangen "HELLO" --auto-size
205+
bangen "HELLO" --auto-size --export-gif banner.gif --gif-duration 3 --gif-fps 20
206+
```
207+
208+
**Auto-Size Features:**
209+
- Enabled by default (use `--no-auto-size` to disable)
210+
- Analyzes your current terminal dimensions
211+
- Calculates optimal canvas width and height
212+
- Applies intelligent scaling (maintains aspect ratio)
213+
- Shows sizing info: `Text: WxH | Canvas: WxH | Scale: Sx | Padding: (X,Y)`
214+
- Works with all export formats (`GIF`, `PNG`, `TXT`)
215+
- Ensures exports are properly sized relative to the rendering environment
216+
183217
## Releases 📦
184218

185219
GitHub Actions builds standalone binaries for `Windows`, `macOS`, and `Linux` and uploads them to the matching GitHub release.
186220

187221
- asset names follow the project version from `pyproject.toml`
188222
- release files include the platform in the filename
189-
- the release workflow expects a tag matching the project version, for example `v2.2.2`
223+
- the release workflow expects a tag matching the project version, for example `v2.2.3`
190224
- release builds explicitly bundle the TUI package, effect modules, `pyfiglet` font assets, Rich, and Pillow runtime pieces so the standalone app works outside a Python environment
191225

192226
#### Screensaver
@@ -271,6 +305,8 @@ banner.apply(build_effect("chromatic_aberration", config=cfg))
271305
banner.apply(build_effect("pulse", config=cfg))
272306
```
273307

308+
**Multiple Effects Stability:** When combining many effects (3+), the rendering engine now intelligently normalizes opacity and brightness values to prevent pixelation, noise, and visual artifacts. This ensures your stacked effects remain sharp and clear in both TUI preview and exports (GIF, PNG, TXT).
309+
274310
Common style stacks:
275311

276312
- `cyberpunk`: `glitch` + `chromatic_aberration` + `pulse`
@@ -363,9 +399,12 @@ bangen/
363399

364400
## Notes 📝
365401

366-
- Animated exports look best when you keep effect stacks readable instead of maxing out distortion-heavy combinations.
402+
- Auto-sizing (`--auto-size` flag or `a` key in TUI) intelligently adjusts banner dimensions based on terminal size for optimal rendering and exports.
403+
- Animated exports are now rendered with optimized font sizing (11px) for sharp, crisp ASCII art without pixelation artifacts.
404+
- Animated exports look best when you keep effect stacks readable instead of maxing out distortion-heavy combinations. The rendering engine now handles complex effect stacks gracefully without artifacts.
367405
- Temporal effects such as `wipe` and `typewriter` are best previewed with `--animate` in the terminal before exporting.
368406
- `--screensaver` is designed for live terminal playback, not export generation.
407+
- Text input fields in dialogs support copy-paste via `Ctrl+V` for easier workflow.
369408

370409
## License 📄
371410

bangen/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
from __future__ import annotations
44

5-
__version__ = "2.2.2"
5+
__version__ = "2.2.3"
66
__author__ = "programmersd21"

bangen/cli/parser.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ def main(
126126
no_border: Annotated[bool, typer.Option("--no-border")] = False,
127127
title: Annotated[str | None, typer.Option("--title")] = None,
128128
static: Annotated[bool, typer.Option("--static")] = False,
129+
auto_size: Annotated[
130+
bool,
131+
typer.Option(
132+
"--auto-size/--no-auto-size",
133+
help="Enable/disable auto-sizing (enabled by default). Auto-adjust banner width/height based on terminal and text size.",
134+
),
135+
] = True,
129136
) -> None:
130137
from bangen.cli.runner import run_cli
131138

@@ -173,5 +180,6 @@ def main(
173180
no_border=no_border,
174181
title=title,
175182
static=static,
183+
auto_size=auto_size,
176184
)
177185
run_cli(args)

bangen/cli/runner.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from bangen.gradients.gradient import Gradient
2626
from bangen.presets.manager import Preset, PresetManager
2727
from bangen.rendering.engine import DEFAULT_FONT, RenderEngine
28+
from bangen.rendering.sizing import calculate_auto_size, format_size_info
2829

2930

3031
def run_cli(args) -> None:
@@ -206,6 +207,17 @@ def run_cli(args) -> None:
206207
show_border = not args.no_border
207208
animate = args.animate and not args.static and bool(effects_list)
208209

210+
# ---- auto-size calculation ----------------------------------------
211+
size_config = None
212+
if args.auto_size:
213+
try:
214+
size_config = calculate_auto_size(banner)
215+
console.print(f"[dim]{format_size_info(size_config, banner)}[/dim]")
216+
except Exception as exc:
217+
console.print(
218+
f"[yellow]Warning:[/yellow] auto-size calculation failed — {exc}"
219+
)
220+
209221
if animate:
210222
t0 = time.monotonic()
211223
dur = args.animate_duration

bangen/export/gif.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
MAX_GIF_FRAMES = 300
1515
_TRANSPARENT = (0, 0, 0, 0)
16-
_FONT_SIZE = 28
16+
_FONT_SIZE = 11
1717
_PADDING_X = 32
1818
_PADDING_Y = 32
1919
_PALETTE_SAMPLE_FRAMES = 12

bangen/export/png.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
if TYPE_CHECKING:
1717
from bangen.rendering.banner import Banner
1818

19-
_FONT_SIZE = 28
19+
_FONT_SIZE = 11
2020

2121

2222
def export_png(path: Path, banner: "Banner") -> None:

bangen/rendering/banner.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,29 @@ def cell_style(
7676
)
7777

7878
color = self._base_color(position)
79-
opacity = 1.0
80-
brightness = 1.0
79+
80+
# Collect individual effect contributions to prevent stacking artifacts
81+
opacity_values = [1.0]
82+
brightness_values = [1.0]
8183

8284
for effect in self._effects:
83-
brightness *= effect.brightness(
84-
t,
85-
row=row,
86-
col=col,
87-
char=char,
88-
lines=lines,
85+
brightness_values.append(
86+
effect.brightness(
87+
t,
88+
row=row,
89+
col=col,
90+
char=char,
91+
lines=lines,
92+
)
8993
)
90-
opacity *= effect.opacity(
91-
t,
92-
row=row,
93-
col=col,
94-
char=char,
95-
lines=lines,
94+
opacity_values.append(
95+
effect.opacity(
96+
t,
97+
row=row,
98+
col=col,
99+
char=char,
100+
lines=lines,
101+
)
96102
)
97103
color = effect.colorize(
98104
color,
@@ -103,6 +109,28 @@ def cell_style(
103109
lines=lines,
104110
)
105111

112+
# Apply multiplicative blending but normalize for multiple effects
113+
# to prevent over-darkening and pixelation
114+
num_effects = len(self._effects)
115+
116+
# Use weighted blending: when multiple effects are active,
117+
# nudge values toward 1.0 to keep details visible
118+
opacity = 1.0
119+
brightness = 1.0
120+
121+
for op_val in opacity_values:
122+
opacity *= op_val
123+
124+
for br_val in brightness_values:
125+
brightness *= br_val
126+
127+
if num_effects > 1:
128+
# For multiple effects, apply a smoothing factor to prevent excessive darkening
129+
# This preserves effect stacking while maintaining visibility
130+
factor = 1.0 / (1.0 + (num_effects - 1) * 0.3)
131+
opacity = opacity**factor
132+
brightness = brightness**factor
133+
106134
opacity = clamp(opacity)
107135
color = scale_color(color, max(0.0, brightness))
108136

0 commit comments

Comments
 (0)