Releases: bczsalba/pytermgui
v0.4.0: Better managed WindowManager
Overview
This version focuses on the window_manager
submodule, and aims to be the final release before v1.0.0
.
-
Improve mouse event API
- Regex based mouse-code parsing
- Decouple mouse clicking from finding mouse target
-
Rewrite
window_manager
- Frame-based, lazy renderer
- Inactive window caching
- Multi-threading
Details
New mouse event API
- Instead of
Widget.click()
, aMouseTarget
can be acquired usingWidget.get_target()
- This allows finding a
Widget
target without implicitly calling itsclick()
method
New window_manager
- The old version was a weekend-project with not much thoughtful structure behind it
- This version aims to be a fully integrated part of
pytermgui
, and the easiest starting point for usage of the module - The frame-based rendering currently defaults to 300 fps, the inaccuracy of
time.sleep()
messes with low framerates - The main thread handles input, while the secondary thread does all the printing:
- Main thread:
WindowManager.process_input()
- Side thread:
WindowManager._start_display_loop/_loop()
- Main thread:
- Windows can tell the WM to print by returning
True
in theirbind()
callbacks - For now, all input is handled globally by
WindowManager
. This is likely to change in the future.
End result
Nothing visual has changed in this patch, so have this nice screenshot of me testing ptg
:
ok, bye 🚀
v0.3.0: The Mark(up) of the Snake!
Overview
This release focuses on the parser
submodule, rebuilding it from the ground up and adding some much needed features & performance in the process.
-
New support for differing line-lengths in
Splitter
-
Rewrite
pytermgui.parser
:- The module is now instance based, allowing for isolated macro & tag definitions
- Much more robust markup parsing
- Up to 60% speed improvement compared to
0.2.1
- Better
macro
syntax:[!align]30:center text
->[!align(30,center)]text
- Introduce cache system for
MarkupLanguage.parse
- Note: The
WindowManager
doesn't play too nicely with this system. This will be fixed next patch.
-
Improve terminal size handling:
- Add new
pytermgui.ansi_interface.terminal
object - This object is used for getting
size
,width
&height
of the current terminal - Support subscribing to
SIGWINCH
event to gracefully handle resize events
- Add new
-
Remove
Widget
focus system- This was introduced specifically for
InputField
, which no longer uses it Widget.selected_index
should be used instead
- This was introduced specifically for
Details
parser.py
& MarkupLanguage
- The module provides a
MarkupLanguage
instance under the namemarkup
, which is instantiated at import-time, which should be used for all global language settings & definitions - Additional instances may be created to host potentially unsafe macros or non-global aliases
- Under the hood, there is a much more robust & performant set of tokenizer methods
- Name changes:
define_tag()
->MarkupLanguage.alias()
define_macro()
->MarkupLanguage.define()
markup()
->MarkupLanguage.get_markup()
ansi()
->MarkupLanguage.parse()
tokenize_markup()
->MarkupLanguage.tokenize_markup()
tokenize_ansi()
->MarkupLanguage.tokenize_ansi()
End result
parser.py
The speed difference can be showcased using a simple script to render a MarkupApplication
window:
import time
import pytermgui
from pytermgui.cmd import MarkupApplication
formatter = pytermgui.MarkupFormatter("[60 bold]{item}")
for obj in [pytermgui.Window, pytermgui.Container]:
obj.set_style("border", formatter)
obj.set_style("corner", formatter)
manager = pytermgui.WindowManager()
app = MarkupApplication(manager)
window = app.construct_window()
lines = window.get_lines()
for _ in range(2):
nums = []
for i in range(100):
start = time.time()
window.get_lines()
took = time.time() - start
nums.append(took)
print("avg:", sum(nums) / len(nums))
print("max:", max(nums))
print("min:", min(nums))
If you do the math, 0.00418 / 0.00970
comes up to 43%, meaning the improvement in this scenario is around 55%. This can be even larger in more complex applications.
Funnily enough, the aforementioned issue with WindowManager
is that it cannot handle this speed. It gets greedy with frame-rendering, and overloads what my old MacBook can handle.
ok, bye 🚀
v0.2.1: The Backwards Compatible update!
This patch removes all uses of syntax that broke Python versions before 3.9
, and uses the from __future__ import annotations
to further improve backwards compatibility. With this, the library is supported as far back as 3.7
!
ok, bye. 🚀
v0.2.0: The Widget update!
Overview:
This version primarily focuses on improving the included Widget
subclasses and their interactions
-
New module:
pytermgui.widgets.buttons
to store all theButton
subclassesButton(Widget)
: Same as beforeCheckbox(Button)
: A button with a checked and unchecked state, and toggles between themToggle(Button)
: A button that toggles between two labelsDropdown(Container)
: Essentially a proof of concept. Will need support for printing lines at custom locations as overlays.
-
Rewrite
Splitter
:- Increase stability
- Fix various spacing issues
- Properly inherit from
Container
- 2 new
auto()
structures - Note: Currently
Splitter
-s only really work with widgets of the same height. Check below for more info.
-
Rewrite
InputField
:- Inherit from
Label
, use itsget_lines()
- Support keybinding (more info below)
- Rework styles, support full line styles
- Inherit from
-
Rewrite
pytermgui.cmd
:- Use
WindowManager
with a customApplication
framework - Implement old utilities as
Application
subclasses - Support launching a specific app and dumping its output, as well as running as long as the User wants it to
- Use
-
Remove unmaintained widgets:
- Some, like
ProgressBar
will come back in the future.
- Some, like
Details:
Widget.bind(key, action, description)
Assign an action and a description to a dictionary of bindings, which is called by
Widget._execute_binding(key)
.
key: str
- The key that activates the bindingaction: Callable[[Widget, str], Any]
- The function that gets calleddescription: Optional[str]
- Description of the bindingWindowManager
implements & handles this, but otherwise the user should implement it.
Splitter()
-
The
auto()
method now takes two new forms to create aSplitter()
{left: right}
-> What used to bePrompt
, runauto()
on both sides and align them to the outside(item1, item2, item3, ...)
-> Shorthand forSplitter(item1, item2, item3, ...)
-
For now, you should use line lists of equal length to display multi-length
Widget
-s inSplitter
. -
As such, the
itertools.zip_longest
method is really useful for creatingSplitter
-s:from itertools import zip_longest from pytermgui import Container, Splitter left = ["one", "two", "three"] right = ["one", "two", "three", "four", "five"] container = ( Container() + Splitter( *[{lhs: rhs for lhs, rhs in zip_longest(left, right, fillvalue="")] ) )
cmd.py
rewrite
- A cleaner implementation, including a whole
Application
framework - Better cli:
ptg
-> openWindowManager
with all the utilities, exit onCTRL_C
ptg --app {MarkApp, Getch}
-> launch either of the utilities, dump their output once input has finished
End result
Combining all of the aforementioned changes, this is how ptg
looks at the moment:
ok, bye. 🚀
v0.1.4: Better syntax!
Overview:
This version brings some great improvements for creating Widget
-s, along with some utilities.
- Improve method
auto()
: Better conversions & implicit calling byContainer
- Rewrite method
WindowManager.run()
: More modular, has better performance & stability - New method
WindowManager.bind(key, action)
: Assign a callback for a keypress - New feature setting arbitrary attributes in
Widget
construction
Details:
auto()
- Implicit calling whenever a non-widget is added to a
Container
(or subclass, as long as its_add_widget()
method is not overwritten) - Better conversions:
"text"
->Label("text")
["label", lambda target, button: ...]
->Button(label="label", onclick=lambda target, button: ...)
("Any", ["widget"], ...)
->Splitter(Label("Any"), Button("widget", onclick=None), ...)
- Note: There are some additional conversion models, but they are likely to change in the next release.
WindowManager.bind(key, action)
key
-str
: The trigger keyaction
-Callable[[WindowManager], Any]
: The method to be called- Note: If the
WindowManager
could find & execute a binding it assumes the key to be handled, and does not do any built-in handling.
Arbitrary attribute setting for Widget
Widget
is now passed**attrs
on construction- All of these attributes are applied using
setattr()
, regardless of their respective fields - These changes should be integrated into custom
Widget
subclasses:
class MyWidget(Widget):
def __init__(self) -> None:
super().__init__()
⬇️
class MyWidget(Widget):
def __init__(self, **attrs: Any) -> None:
super().__init__(**attrs)
End result
All of this combined results in the syntax for creating a simple window going from:
root = Window()
root.forced_width = 50
root.id = "root"
root += Label("[157 italic]This is a title")
root += Label()
root += Button("Exit", lambda *_: sys.exit()]
To:
root = (
Window(forced_width=50, id="root")
+ "[157 italic]This is a title"
+ ""
+ ["Exit", lambda *_: sys.exit()]
)
Do note, however, that the traditional syntax is still available. For more information on this version, check out the linked commit.
ok, bye 🚀
v0.1.3: Window Managers!
This is the first tagged-release of this project, and it might be one of its most significant ever.
The biggest addition is the new window_manager
module, with all of its utilities. It provides:
- The
WindowManager
class, with built in mouse handling & window management (duh) - The
Window
class, which can be used as a drop-in replacement toContainer
- A simple test window (
DebugWindow
) with some buttons, a mouse & a window debugger (MouseDebugger
&WindowDebugger
) - Many improvements to the mouse-handling API
Please let me know if you have any issues with this version, or any ideas for the future!
Thanks! 🚀