Changelog
Next
Added
-
#393 Added inline apps:
app(viewport=Inline())draws in a region below the prompt, as tall as the root's content, rather than on the alternate screen, and leaves its last frame in the terminal when it stops (see Inline Apps).app'sviewportisFullScreen()by default, whosemouse=Falseleaves the mouse to the terminal.Inline(mouse=True)tracks the mouse over the region, asking the terminal where the region is. -
#393 Added
Print, a control that renders a component or an element once and prints it toapp'sstdoutorstderr: above an inline app's region when the stream is the app's terminal, when a full-screen app hands the screen back, and to the stream as it is otherwise, as plain text if it isn't a terminal. While an app that isn't headless runs,sys.stdoutandsys.stderrprint each line written to them the same way, unlessappis givencapture_output=False. -
#393
Screenshot(include_scrollback=True)takes everything an inline app's terminal holds, what it printed included, which is how a test reads it headless.Emulator.historyreturns the rows above an emulator's screen as it holds them now, rewrapped by resizes. -
Added
get_state, which reads state through ause_statesetter, or anyEditorthat wraps one, as every update since the last render left it, for a handler that decides from it, without changing it or queueing a render (see Deciding from the latest state), andEditor, the half of aSetterthat takes a function, which the text hooks and widgets take asset_value. -
TextInputandTextAreahavelatest_cursor, where every edit since the render left the cursor, besidecursor, where the render placed it. -
Added
Highlight, which of a list of options is highlighted, held by the option's key and by its index, so it stays on its option as the options change, skips the optionsis_disabledmarks, and wraps, stops or releases at the ends, andsubsequence_matches,substring_matchesandprefix_matches, which filter options by a typed text, with a page of the pickers built from them: a combobox, a command palette, a search box, Tab cycling, inline completion, rows that refresh, marks, headings and a preview (see Pickers). -
Added the
comboboxwidget and itsComboboxStyles: a text input whose text suggests options in a list that opens below it, or above it near the bottom of the screen, with the WAI-ARIA combobox pattern's keys.
Changed
-
#449 A component whose
use_rectsoruse_scrollread goes stale renders again before the frame is painted, rather than in the next frame, as a browser deliversResizeObservernotifications between layout and paint, so no frame shows a component drawn from rectangles or a scroll that have since moved (see When it renders again). A frame re-renders a few times at most before painting, then leaves the rest to the next frame, logging a warning that isn't repeated until a frame settles, and aFocusOnthat waits focuses a component that mounts in any of those re-renders. Withcheck_purityon, the app raises the newUnsettledMeasurementsinstead, and the purity check runs once a frame, on the tree it paints. The first frame renders each component once, rather than once to lay the tree out and again to paint it. -
#393
Quitrenders once more before the app stops, without starting effects, so the last frame shows what the last handlers set. -
Breaking: a
use_statesetter returns the state as it settled: the value it was given, or what the function returned. A handler written as alambdawhose body is a setter call, such aslambda event, _: set_count(count + 1), now returns that state as its response, which mypy rejects and the app fails on, so write it as a function that returns()to claim the event orNoneto let it bubble. A parameter typedSetter[T]needs nothing; one that spells a setter out asCallable[..., None]needsSetter[T]. -
Breaking:
Rectshasscreen, the region the app lays out in, so a component can measure the room around its element (see Measuring the room around an element), which code that builds aRectsitself, such as a test that hands a handler aReceiver, now passes. -
Breaking:
use_text_input,use_text_area,text_inputandtext_areatakeset_valueas anEditor[str], which returns the value it settled at, as ause_statesetter does, so a wrapper that validates an edit returns what its setter returns. -
Callbacks whose result the app ignores (
Suspend.to_program's andSuspend.to_edit_file'son_exit,Suspend.to_edit_text'son_edit, the handlers ofScreenshot,CopyToClipboardandSuspend, andapp'sclipboard_handler) may return anything, so a setter can be one, as inon_exit=set_outcome; one that returns an awaitable is still awaited. So mayrun_pty'son_outputandon_startanduse_pty'son_output, whose results are ignored.
0.1.1
Released 2026-10-22
Counterweight 0.1.1 fixes what the first apps to move onto 0.1.0 ran into:
empty frames in recordings, an autopilot that could stall on an app that never goes quiet,
and text that ignored its own content_color.
It also exports the names those apps had to import from private modules,
and adds text_overflow and handlers for copies.
Added
-
#448 Added
text_overflow, with thetext_overflow_clipandtext_overflow_ellipsisutilities, which ends a line too wide for itsText's content box with…in the box's last cell, as CSS'stext-overflow: ellipsisdoes (see Ending a line with an ellipsis). It changes only what's painted, never an element's size. -
#448 Added a
handlertoCopyToClipboardand aclipboard_handlertoapp, which take a copy's text in place of the terminal's clipboard, headless or not, so a test can check what an app copies (see Copying to the clipboard). -
#448
Subtree()makes a subtree that names nothing mounted, as a freshFocusHandledoes, so a test can build aReceiverto call a handler with, without reaching into the hooks. -
#448 Exported names that apps were importing from private modules, and the ones beside them:
counterweight.text, a new module ofgraphemes,grapheme_widthandvisible, for an app that cuts or pads text itself to match how counterweight measures it;ramp,Ramp,field_indicesandIndicesfromcounterweight.gradients, to read a gradient's colors;ResolvedCellStyle,resolve_cell_style,is_solid_cell_style,as_solid,with_colors,is_solid_color,rgb_ofandpalettefromcounterweight.styles;styled_grapheme,SCREENSHOT_FOREGROUNDandSCREENSHOT_BACKGROUNDfromcounterweight.paint, the last two the colors an SVG screenshot draws the terminal's defaults in, for an asciicastThemeto match;outcome_textfromcounterweight.commands; andSETTLE_LIMITfromcounterweight.autopilot.
Fixed
-
#448 A
Screenshothandler that sets state no longer renders the app twice: the render that follows the handler shows the new state, and nothing renders again for it. -
#448 A render that changes no cell writes nothing to the terminal, so a recording has no empty frames, such as the ones a clock-driven component's ticks made once its animation stopped changing anything.
-
#448 The autopilot holds a step back for an app that hasn't settled for at most
SETTLE_LIMIT(50) passes of the render loop, then takes it anyway and logs a warning, so an app that never goes quiet, such as one showing a program that prints without pause, no longer stalls it. AWaitForis checked on every pass, settled or not, so it times out on time. -
#448 A
Text's characters are drawn on its owncontent_colorwhere itstext_stylesets no background, as aCanvas's painter's cells already were, so a box filled with a color shows it under its own text (see Text in a filled box). They still don't take the color of a box they lie in.
0.1.0
Released 2026-10-21
Counterweight 0.1.0 rebuilds most of the framework, and adds a great deal on top of it.
Layout is taffy's flexbox and grid, through waxy.
A render re-runs only the components whose state changed.
Input goes to the focused element and bubbles, as in the DOM, and boxes clip and scroll.
New on top of that are headless components and the widgets built on them, Markdown and syntax highlighting,
the Canvas element and embedded terminals, animation, gradients, and recordings,
and the examples ship with the library, runnable from the counterweight CLI.
Most apps written for 0.0.9 need changes: every change that breaks one is marked Breaking: below.
Counterweight requires Python 3.15.
Added
Components and hooks
-
#394 Added
@component(memo=True), which skips re-running a component when its parent re-runs and passes it arguments equal to the previous render's, andcheck_purityonapp, which runs every component again after each render and raisescounterweight.components.ImpureComponentwhen that builds something different, because a component depends on more than its arguments and hooks. -
#394 Added
use_memo, which caches a value until its dependencies change, so a component can keep an expensive derived value, an unchanging part of its output, or a handler it passes to amemocomponent. Withcheck_purityon, a cached value that a dependency missing from its list would change raisescounterweight.hooks.StaleMemo. -
#416 Added
counterweight.hooks.toggle, which flips a flag through ause_statesetter (set_open(toggle)), so toggles that land before the next render each flip the latest value, and a type checker rejects it for state that isn't abool. Theuse_statedocs show how setting state from a value computed from the state a render saw loses updates, and when to pass a function instead. -
#400, #404 Added
use_clock, which gives a component the time (the wall-clock time, and how long since the app started and since the component mounted) while it stays pure: it re-runs when its next tick starts, ticks follow the wall clock, and components reading it in the same frame see the same time.apptakes aclockto read the time from: aSystemClock, in UTC unless it's given a timezone, or aFrozenClockfor tests and screenshots, which only an autopilot moves, and theAdvanceClockcontrol moves either forward. AnintervalofNonepauses it, as anulldelay pauses React'suseInterval: the component keeps its last reading and the clock never re-runs it, and the render that gives it an interval again takes a fresh reading. The cookbook shows how to animate a spinner with it. -
#444 Added animation:
use_progress(duration, deps), which rises from 0 to 1 overdurationand holds there, starting again whenever itsdepschange, as a CSS animation that plays once, anduse_loop(period, deps), which repeats everyperiodand pauses withis_playing=False, as one whose iteration count isinfinite. Both read the clock everyFRAME_INTERVAL(20 frames a second) while they move, unless given aninterval, and stop reading it while they don't, so a paused animation never re-runs its component, and a finished one only once more.counterweight.animationholds CSS's easing functions, to shape how an animation moves:linearandpiecewise_linear(CSS'slinear()),cubic_bezierand the curves CSS names (ease,ease_in,ease_out,ease_in_out), andsteps,step_startandstep_end, andbetween, which picks one phase out of an animation's progress, as a keyframe does. -
#429 Added
use_controls, which gives a component a sender that hands the app a control from outside an input handler, such as an effect that quits when a program exits. The app handles a sent control as it handles one a handler returns, after the events already waiting. -
#305 Added
counterweight.geometry.clamp(min_, val, max_), which clamps a number to a range.
Input, focus and the terminal
-
#402, #429, #431 Added
use_focus, which makes a component focusable, so that key presses go to it while it's focused. It takesauto_focus, to take focus on mount when nothing else has it, or when what has it is out of Tab order and the component mounts inside it, asautofocustakes focus from a page's body;handle, aFocusHandlea parent creates to focus a child it renders;is_focusable=False, to disable it; andin_focus_order=False, to leave it out of Tab order. The controlsFocusNext,FocusPrevious,FocusOn(handle)andClearFocusmove focus, andFocusNextandFocusPrevioustake awithinsubtree, or an element's handle, to move only inside one element.FocusOnalso focuses a component that mounts in the render its handler causes, before that frame is drawn, unless another control moves focus first.counterweight.handlershastab_focus,focus_receiver_on_press,focus_on_pressandclear_focus, which return them for Tab and for presses; nothing moves focus unless a handler does. AReceivercarries the element'sfocus_handle, whether itis_focused, and itssubtree. -
#402 Added
counterweight.handlers, withon_click(action), which callsactionwhen a press on its element is released inside it, andfirstandeach, which compose several handlers into the one an element takes. A handler can return a tuple of controls, applied in order and skipping anyNone, or()to stop an event with nothing more to do. -
#416 Mouse events say which modifier keys were held, in
is_shift_held,is_alt_heldandis_control_held, as the DOM'sMouseEventdoes, so a handler can match a Ctrl+click or a Shift+wheel. Some terminals keep Shift with the wheel for their own scrollback and never report it. -
#429 Added
Key.ControlBackslash,Key.ControlRightBracket,Key.ControlCaretandKey.ControlUnderscore, and their Ctrl+Alt forms, so every control byte a terminal sends is a key. -
#411, #412, #422 Added the
SetTitlecontrol, which sets the title of the terminal window, and thetitleargument toapp, which sets it when the app starts. The app saves the title it started with and restores it when it quits and while it's suspended, on terminals that support xterm's title stack, and theRestoreTitlecontrol brings it back while the app runs. -
#445 Added the
CopyToClipboardcontrol, which copies text to the system clipboard, as a browser'snavigator.clipboard.writeTextdoes, by asking the terminal to with OSC 52. It works only where the terminal supports OSC 52, and in tmux only withset -g set-clipboard on.
Layout and scrolling
-
#415 Added scroll containers: a box whose overflow is
Scroll(set byoverflow_scroll,overflow_x_scrolloroverflow_y_scroll) shows the part of its content its offset scrolls into view. Counterweight holds each container's offset, and after every layout keeps the content in view where it was, as CSS's scroll anchoring does, and clamps the offset to how far the content reaches. Theoverflow_anchorstyle field, withoverflow_anchor_autoandoverflow_anchor_none, turns anchoring off, and thestick_to_end_xandstick_to_end_ystyle fields, withstick_to_endandunstickand their_xand_yforms, keep a container that's scrolled to its end there as its content grows, as a log or a chat transcript wants. The controlsScrollToandScrollByscroll a container named by anElementHandle, which an element takes as itshandle, or by a receiver'ssubtree, andScrollIntoViewscrolls every container above an element until it's in view, as the web'sscrollIntoViewdoes, aligning it as itsblockandinlinesay. All three apply in order to the layout of the next render, so they scroll the content laid out by any state change returned beside them, in the same frame. Moving focus scrolls the newly focused element into view.counterweight.handlershasscroll_on_wheelandscroll_on_keys, which make handlers that scroll for the wheel and for the arrow keys, PageUp, PageDown, Home and End, and pass the event on to the container outside once their own reaches an end, andpage_downandpage_up, which return theScrollByfor a page of any fraction of the view.scroll_on_wheelscrolls sideways for the vertical wheel with Shift held, as browsers on Windows and Linux do.use_scrollreturns how far the calling component's element, or the element with a givenElementHandle, is scrolled and how far it can be, as aScroll, and aReceivercarries the same as itsscroll. -
#388, #415 Added the overflow utilities
overflow_visible,overflow_hiddenandoverflow_clip, withoverflow_x_*andoverflow_y_*forms that set one axis. A box whose overflow isHidden,CliporScrollclips its children to its padding box on each axis that sets it, and the mouse reaches an element only where it's painted: routing,use_hoveredandon_clickignore the parts of an element its ancestors clip away. Theclip_tostyle field chooses what clips an element: its parent's padding box (clip_to_padding, the default), its parent's border box (clip_to_border), so a child such as a border title can draw on the border of a parent that clips, or only the screen (clip_to_screen), so a popup such as a dropdown escapes every clip above it.Rectshasclip, the region an element's ancestors let it draw in, andvisible, the part of its border box inside that region. See Clipping, and the autocomplete recipe. -
#305 Added the layout utilities that taffy's flexbox and grid offer: sizes (
width(n),height(n),size(w, h), theirmin_andmax_forms, andaspect_ratio), flexing (grow(n),shrink(n),row_reverse,col_reverse,flex_wrap,flex_no_wrapandflex_wrap_reverse),display_flex,display_grid,display_blockanddisplay_none, grid (grid_template_columns,grid_template_rows,grid_row,grid_column, and thegrid_auto_flow_*,justify_items_*andjustify_self_*utilities), box sizing (border_boxandcontent_box), andfull,full_widthandfull_height, which fill the parent after the box's margins. -
#385 Added the sizing keyword utilities
min_content_width,max_content_width,fit_content_widthandstretch_width, and their_heightcounterparts, which size a box from its content or fill the space available after its margins. -
#385, #415 Added the constraint utilities
length(n),percentage(p),ratio(a, b)andfill(n), which split space along the parent's main axis the way ratatui's constraints of the same names do: a child takes its share whatever its content, in arowor acol, hiding its overflow. For grid,fr(n)is a track ofnshares that content can't widen (minmax(0, n fr)), andspan(n)places a child acrossntracks.grid_template_columnsandgrid_template_rowsaccept a plainintas a track of that many cells, andgrid_rowandgrid_columnaccept one as a grid line.center_childrencenters children on both axes. -
#402 Added
inset(n),inset_x(n)andinset_y(n), which set all four, the left and right, or the top and bottom insets;inset_sides,margin_sidesandpad_sides, which set each side separately in the order of CSS's four-value shorthands; andautomargins for every side and pair of sides, frommargin_autotomargin_right_auto.position_absolute | inset(0) | margin_auto | fit_content_width | fit_content_heightcenters a box over its parent at its own size, as in CSS.
Styles
-
#443 Added attribute utilities that draw the text or the border characters bold, dim, italic, underlined or struck through (
text_bold,border_italic, ...), each with anot_form (text_not_bold) that turns it off when merged over a style that turns it on. AChunk's style can be aStyle, of which the chunk takes thetext_style, so the utilities that style aText, such astext_bold | text_color("sky", 300), style a chunk too. -
#123, #402 Added
content_color, an element's background, which fills its border box beneath its padding, border and text, even when the element has nothing else to draw. An element with no margin, border, padding, text orcontent_colorpaints nothing, so an undecorated container laid over other elements leaves them visible. -
#429 Added colors the terminal decides, which follow the user's theme:
IndexedColor(n), entrynof the terminal's 256-color palette, andTERMINAL_DEFAULT, the terminal's default foreground where it colors text and default background where it fills one. Every style field that took aColortakes either (AnyColor), and the*_indexed(n)and*_terminal_defaultutilities set them wherever the color utilities do. Screenshots draw palette entries inDEFAULT_PALETTEand the defaults as white on black. See Terminal colors. -
#430 Added gradients: linear, radial and conic, with CSS's geometry, stops, hints and repeating, built by
linear_gradient,radial_gradientandconic_gradientincounterweight.gradients. Any color a style holds can be a gradient:margin_color,padding_color,content_color, and the foreground and background ofborder_style,text_styleand a chunk's style, each mapping it over the box it paints unless the gradient'sboxnames another, and the*_gradientutilities put one in each of those places. Colors mix in Oklab by default, or in another of CSS's color spaces, with its hue methods for the polar ones. Angles and circles are in visual units, where a row isapp'scell_aspect_ratiocolumns tall (2 by default), and a gradient spans its box's corner cells' centers, so they take its end colors. Paint keeps each element's gradient cells from frame to frame, so a gradient that doesn't change isn't computed again. -
#385 Added
counterweight.styles.merge, which merges any number ofStyles left to right, as an alternative to chaining|:merge(a, b, c)isa | b | c. It skipsNonearguments wherever they appear.
Text and rich text
-
#318 Added text wrapping for
Textelements:text_wrap_stable(greedy),text_wrap_balance(equalizes line lengths), andtext_wrap_pretty(Knuth–Plass, minimizes ragged lines).text_wrap_none, the default, doesn't wrap. Words longer than the available width are broken with a hyphen. A wrappingText's minimum size is its widest word, so wrappingTexts side by side in a row shrink to share it. -
#330, #414, #416 Added
markdown_document, a component that renders a Markdown document (GitHub Flavored Markdown with its task lists, alerts, footnotes, bare URLs and math, and YAML front matter), styled by aMarkdownStyleswith a field for each kind of block and span. It's amemocomponent, which parses its document again only when its arguments change, andmarkdown, the function it calls, returns the document as aDivof elements. Links are hyperlinks, and fenced code blocks and front matter are highlighted. Code blocks and tables too wide for the space they're given scroll sideways with the wheel, as GitHub's styles scroll them, and a table wraps its cells as narrow as their words allow before it scrolls.keeps_line_breaks=Truekeeps each line break inside a paragraph, as GitHub's comments and chat apps do. -
#330, #414 Added syntax highlighting with Pygments:
highlightreturns code as styled chunks,code_styleits theme's background and text color, andcode_blockaTextof the highlighted code on that background. -
#414, #443 Added hyperlinks:
CellStyle'slinkmakes aChunkorTexta hyperlink that the terminal opens when it's clicked, and thetext_link(uri)utility sets it, or withNoneunsets it. A link must be printable ASCII, orCellStyleraisesValueError.
Headless components and widgets
-
#391, #416 Added headless components in
counterweight.headless, hooks that give a component a common control's behavior and leave drawing it to the component, starting withuse_text_input, which edits one line of text. The caller owns the value and passes its setter; the hook owns the cursor and makes the component focusable. It returns the value split around the cursor, anon_inputhandler for Readline's editing keys and pastes, anon_presshandler that puts the cursor where a press lands and focuses the field, as a click in a browser's<input>does, and acursor_cellelement handle that its handler scrolls into view with each edit, so a value wider than its box scrolls to keep the cursor in view.counterweight.widgets.text_inputis a bordered box built on it, with a placeholder. -
#416 Added
use_text_area, which edits text that wraps onto rows. It wraps the value itself, at the width the caller gives it, and returns the rows split around the cursor, so Up and Down move between the rows on screen, keeping their column, and Home, End, Ctrl+U and Ctrl+K act on the cursor's row. Ctrl+J and Alt+Enter insert a line break, and so does Enter withenter_inserts_newline=True; otherwise Enter bubbles, as for a form or a chat that submits on it. Itson_presshandler puts the cursor where a press lands, as foruse_text_input, and itscursor_rowelement handle, which its handler scrolls into view with each edit, keeps the cursor in view in a scroll container.counterweight.widgets.text_areais a bordered box built on it, which wraps at its own width and grows to fit its rows up to amax_heightin its style, past which it scrolls. Both hooks take Readline's Alt+B, Alt+F, Alt+D and Alt+Backspace, with Alt+Left, Alt+Right and Alt+Delete, to move and delete by word. The chat example sends Markdown messages from atext_areato a transcript that sticks to its end as replies stream in. -
#416 Added
use_scroll_area, which scrolls a box with the keys, the wheel and controls, as Radix UI's ScrollArea does for React. It makes the component focusable, as browsers make a scroll container focusable, so Tab reaches it, and its handler scrolls for the arrows, PageUp, PageDown, Space, Home and End, and for the wheel, passing an event on once the area reaches an end, and focuses the area for a press nothing inside it claims. It returns the style that makes the element that scrolls a scroll container on itsaxis("x","y"or"both"), clipping the other axis and sticking to the end withstick_to_end, to merge after any constraint utility, aviewportelement handle that a parent can pass in to scroll the area from outside, itsscroll,is_at_startandis_at_end, andto_startandto_endcontrols, as for a "jump to latest" button. The element that scrolls can be inside the one that's focused, with a title or a button beside it.counterweight.widgets.scroll_areais a bordered box built on it, whose border shows focus, styled by aScrollAreaStyles. -
#416 Added
counterweight.widgets.TextFieldStyles, which thetext_inputandtext_areawidgets take asstyles, for their box while unfocused, focused and unfocusable, their cursor, and their placeholder. A widget'sstylemerges under the focused and unfocusable styles, so a custom border color doesn't hide which field is focused.
Canvas and terminals
-
#429 Added the
Canvaselement, a leaf whose content box its painter draws cell by cell, sized as an<img>is, from its painter's natural size and aspect ratio. A painter is any object with anatural_size,keeps_aspect_ratioanddraw(width, height), which returns rows of styled graphemes whose unset styles come from the canvas; a canvas draws again only when its painter isn't equal to the last one or its box changes size.counterweight.paintersholdsPixels, a picture as RGBA bytes, whichPixels.from_imagemakes from an image decoded by Pillow, andHalfBlocks, a painter that draws aPixelstwo pixels to a cell with▀, scaling and placing it as CSS'sobject-fitandobject-positiondo, and whosepixels_atfinds the pixels a cell shows, for mouse input. -
#430 Added
HalfBlockGradient, a canvas painter that fills its canvas with a gradient two pixels to a cell, andPixels.from_gradient, a picture of one. -
#444 Added
Paint.as_painter, aCanvaspainter, aPaintView, that draws a region of a paint, such as a screenshot's frame, as it was when it was made.Paint.cells_intakes a region's cells row by row, as a value, to rearrange or mix with another picture's, andcounterweight.paint.rows_ofturns cells back into a painter's rows, drawing either half of a wide character whose other half isn't beside it as a space, as a terminal does. -
#429 Added
counterweight.emulator, a terminal emulator (libghostty-vt, through pyghostty) that turns the bytes a program writes into what its terminal shows. AnEmulatoris fed bytes, answers the program's queries throughon_reply, keeps ascrollback, and snapshots into immutableScreenvalues: rows of styled graphemes, the cursor, the title, and the modes that change how the program's input is encoded.counterweight.painters.ScreenViewis theCanvaspainter that draws aScreen. A program's palette colors stay palette entries, so they follow the user's theme. Counterweight imports libghostty's bindings only when an app emulates a terminal, so an app that doesn't starts without them. See Emulators and Pseudo-Terminals. -
#429, #431 Added
counterweight.pseudoterminal, which runs a program on a pseudo-terminal, as a terminal window does, and hands back the bytes it writes, standard output and standard error together.run_ptyruns one until it exits, returningExitedwith its status,FailedToStartwith the reason, orTimedOutif it outran itstimeout, and hands out aPtyConnectionthat writes to the program and resizes its terminal.use_ptyruns one while its component is mounted, starting it once its size is positive, so a terminal sized fromuse_rectsstarts at its real size, and starting it again when itsargv,envorcwdchange. Once the program exits, or it's cancelled, its whole process group is hung up as closing a terminal window does, withSIGHUP, thenSIGTERMandSIGKILLahangup_graceapart, so nothing it started outlives it;use_ptydoes this in the background, so it doesn't hold up rendering. -
#429, #431 Added one-shot commands in
counterweight.commands: running a program that's expected to exit, and showing everything it printed, colors and all, as a terminal would have shown it.use_commandreturns aCommand, whoserun(argv)starts a program on a pseudo-terminalcolumnswide (waiting until that's positive, so it can come fromuse_rects) androwstall, incwd, with standard input on/dev/nullandPAGERandGIT_PAGERset tocat, whosecancelhangs it up, and whoselatestis aCommandRunvalue whose output updates at most once a frame while the program runs, and whose outcome isStarting,Running,Exited,FailedToStart,CancelledorTimedOut. A run's output is acounterweight.painters.Transcript, aCanvaspainter as wide as the run's terminal and as tall as the lines it printed, keeping the lastmax_lines, andTranscript.ofbuilds one from output recorded ahead of time.command_outputshows a run in ascroll_areathat follows its end, with its outcome beneath. The slides example runs a command on each slide when it's shown. -
#429, #431 Added embedded terminals: running an interactive program, such as a shell or an editor, in a box that shows its screen and sends it the input the box receives, as a multiplexer's pane does.
counterweight.headless.use_terminalruns one on a pseudo-terminal while its component is mounted, emulates its terminal, snapshotting it at most once a frame, and makes the component focusable, returning anEmbeddedTerminal: theScreen, aScreenViewpainter that draws the cursor while focused, the program's status, awritethat sends it bytes, and anon_inputhandler for theCanvasthe component draws it in. While focused, the handler sends the program every key press and paste, Ctrl+C and Ctrl+Z included, and the mouse events it asked for, and a program that asked gets focus events as the component gains and loses focus. A handler put before it withfirsttakes keys first, as a prefix key does, andskipsnames keys the program isn't sent, which bubble up to the app's handlers instead.counterweight.widgets.terminalis a bordered box built on it, sized by itsstyle, whoseintercepthandler sees each event before the program does. The multiplexer example runs shells side by side with direct hotkeys, as zellij does, showing them along the bottom, and closes a pane when its shell exits. -
#429 Added running programs on the app's terminal, as
git commitruns an editor.Suspend.to_programsuspends the app, runs a program on the app'sinput_streamandoutput_streamuntil it exits, with the app's environment andenv(or onlyenvwithclear_env) incwd, and callson_exitwithExitedorFailedToStart. While it runs, the app swallowsSIGINTandSIGQUIT, as POSIX'ssystem()does, so Ctrl+C in a pager reaches the pager without quitting the app.Suspend.to_edit_fileopens a file in the user's editor,$VISUAL, then$EDITOR, thenvi, and callson_exitwith how the editor exited.Suspend.to_edit_textedits a string the same way, through a file whose name sets the editor's highlighting, and callson_editwithEdited(text), or with how the editor failed, so vim's:cqabandons the edit.counterweight.programsholds the functions they run,run_program,edit_file,edit_textandpreferred_editor.
Testing and recording
-
#436, #431, #444 Added the autopilot steps
Wait, which holds the autopilot back for a duration of real time, andWaitFor, which holds it back until a condition is true of the frame on screen, raisingAutopilotTimedOutafter its timeout, while the app keeps handling events and rendering, so it can wait on work like a subprocess. AFrozenClockruns in real time while the autopilot waits, so a recording can show real work at its real pace between steps thatAdvanceClockpaces.typed(text)gives the key presses that type some text, one per grapheme cluster,showing(text)waits until some text is on screen,advance(by)moves the clock on a frame of the animation hooks at a time, so an animation plays out, andpaced(steps, pause)puts a step before each of some steps, such as anAdvanceClockto pace a recording. They give plain lists of steps, to combine with each other and withitertoolsandmore-itertools. -
#436 Added recording:
apptakes aRecording, and writes what the app draws, with its timing, to an asciicast v3 file that asciinema-player andasciinema playplay back, whether it's headless or on a terminal. Each frame is recorded as the cells it changed, timed by the app's clock, so with aFrozenClockan autopilot'sAdvanceClocks are the recording's timing, and it comes out the same on every run. It records the screen's size when it changes, and how the app exited, and cuts the time the app spends suspended out, with asuspendedmarker where the cut is. ARecordingalso takes atitle, anidle_time_limit, and aThemefor the colors the app leaves to the terminal.Recording(input=True)records the input the app handles too; it's off by default, since it records everything typed. TheMarkercontrol marks a point in the recording that players show as a chapter.
Examples and documentation
-
#431 Counterweight ships with its examples: a few complete apps, such as a chat, a Markdown viewer, a terminal multiplexer and Wordle, and the snippets the docs show, each of which runs on its own.
counterweight examples listlists them,runruns one,showprints its source, highlighted on a terminal, andcopywrites a file of your own that runs it, to start from.counterweight examples gallerybrowses them all, with each one's description and source, and runs the one selected in place.counterweight.examplesis importable, but not part of the public API. -
Showed every hook, element, input handling page, style page and terminal feature with runnable examples, screenshots and recordings, each screenshot with a copyable plain-text twin, and added a quick start and a showcase of the example apps to the home page.
-
Documented
appand what each of its parameters is for on a page of its own, and gave every public name a docstring and a place in the API reference of the page that covers it. -
#385 Added a Layout section to the documentation: a guide to choosing between flexbox and grid, and pages on sizing, splitting space, alignment, spacing and borders, layering, clipping and scrolling.
-
#395 Documented every style utility on the utilities page, grouped by family: what each constant sets, each function's signature, and the color palette.
-
#394 Documented how to find where a frame's time goes, which components a frame re-runs, and how to skip the rest with
memo=Trueanduse_memo, with their failure modes, in Performance. -
#394, #431 Documented how Counterweight compares with Textual, Ink, OpenTUI, Bubble Tea, Ratatui and iocraft, and what it doesn't do, in Ecosystem.
-
#436 Documented driving an app with an autopilot and recording it, in Autopilot and Recording.
-
#386 Documented how components keep their state across renders, how to use keys to move or reset that state, and that a component must call the same hooks in the same order on every render, with how to move a hook that is only sometimes needed into a child component.
-
Documented what
use_refis for, and why a component shouldn't read or write a ref while rendering. -
Added cookbook recipes for draggable components that stay inside their parent, a tmux-style prefix key, spinners, sortable tables, and autocomplete.
Changed
Modules
- Breaking: the framework's internal modules start with an underscore,
such as
counterweight._app,counterweight._layout,counterweight.hooks._hooksandcounterweight.styles._styles, and every public module lists its public names in__all__, so each public name has exactly one place to import it from. Importappfromcounterweight(from counterweight import app), whereDEFAULT_UNCLAIMED_KEYSand__version__are too, and import hooks and styles from their packages, such asfrom counterweight.styles import Style.counterweight.hooksno longer re-exportsRects, which is incounterweight.geometry.
App and components
-
#271, #401 Breaking:
apptakes the root component already called, asapp(root())rather thanapp(root), so a root component can take arguments like any other:app(root(Path(sys.argv[1]))). -
#394 Breaking: a render calls only the components whose state changed and the components they return, instead of every component, and keeps the previous render's output for the rest, so a component whose output depends on anything but its arguments and hooks goes stale.
check_puritycatches such a component in tests. An effect withdeps=Noneruns after every render of its own component, rather than after every render of the app. -
#394 Breaking: an error raised while the app runs, in a component, a handler or an effect, propagates out of
appas itself rather than wrapped in anExceptionGroup, after the terminal is restored, so its traceback ends at the line that raised it and callers can catch it by its own type. Errors raised at the same moment from more than one effect are raised together in anExceptionGroup. -
#408 Cancelling the task running
appraisesCancelledErrorout of it once the terminal is restored, as asyncio expects, soasyncio.timeoutandwait_foraround an app can tell it was cancelled. Previouslyappreturned as if the app had quit. -
#386 Breaking: a component that calls a different number of hooks than on its previous render raises
counterweight.hooks.InconsistentHookExecution. Previously calling more or fewer hooks passed silently, and a skippeduse_effectkept running. -
#386 Breaking: component keys are strings only, so
with_keyno longer accepts anint: convert numeric ids withstr(). Keyed sibling components that share a key raisecounterweight.components.DuplicateKey. -
#315 Breaking:
Style,CellStyle,Div,Text,Chunkand the hooks' types are frozen dataclasses rather than Pydantic models (Refis a mutable one), so they don't validate their fields at runtime and have none of Pydantic's methods, such asmodel_copy.DivandTextno longer have atypefield, andcounterweight.typesis removed. -
#429 Breaking: a
Suspendhandler takes one argument, the terminal the app handed back, asTerminalStreams, so it can run a program on the terminal the app draws on even when that isn't standard output. -
#386 The setter returned by
use_stateis the same object on every render of a component, so including it in an effect'sdepsdoesn't rerun the effect each render. -
#394, #429
use_rectstakes an optionalElementHandle, and reports the rectangles of the element with that handle rather than the calling component's own, as a React component measures an element through a ref, so a component can measure an element a child component draws. A component that calls it renders again as soon as any of the rectangles it read move. Previously it rendered again only once something else started a render, so a component that mounted after the first frame read empty rectangles until then. -
#385, #414, #444 Breaking: counterweight logs through the standard library's
logging, and doesn't configure structlog. While an app runs, its log records (logging.getLogger(__name__).debug("...", extra={...})) are written to the devlog off the render loop by without-logging: by default, counterweight's and the app's own at every level (the app's being the top-level package of the module that defines its root component), and every other logger's fromINFOup, so a library that logs atDEBUGon a hot path costs nothing unless an app asks for its records.app'slog_levelssets levels by logger name, aslogging.configdoes, over those defaults. The devlog is a single JSON-lines file, whichcounterweight devlogrenders for reading (--jsonprints the raw lines, and--last Nthe lastNmessages). An app that runs while another has the devlog open, such as one run headless in another's handler, writes into that devlog, at the levels the other app set, rather than truncating it. Its messages don't carryelapsed_nstimings: to see where a frame's time goes, profile the app with Python's sampling profiler (python -m profiling.sampling run app.py). -
#406 Breaking: the
counterweightCLI is built on without-cli, and shell completion (--install-completion) is gone.counterweightwith no command prints a short usage error instead of the full help, andcounterweight --versionprints the version ascounterweight versiondoes.
Input and focus
-
#416, #429 Breaking: the app reads the terminal raw, as tmux does, so Ctrl+C, Ctrl+Z, Ctrl+\, Ctrl+S and Ctrl+Q reach it as key presses rather than the terminal acting on them, and Enter, a carriage return, is
Key.Enterwhile Ctrl+J, a line feed, isKey.ControlJ, where both wereKey.Enter.apptakesunclaimed_keys, what a key press does when no handler on its path claims it, as a browser takes a key's default action unless a listener prevents it.DEFAULT_UNCLAIMED_KEYSgives Ctrl+CQuitand Ctrl+ZSuspend.to_shell(), which suspends the app as a shell job until the shell resumes it withfg, stopping its whole process group as the terminal's Ctrl+Z does, souv runand the like stop with it. Pass{}for no defaults, or merge onto them to change some. A handler that claims Ctrl+C keeps it, such as to cancel a command. See Keys the terminal would act on. Previously Ctrl+Z and Ctrl+\ stopped and killed the app without handing the terminal back, and Ctrl+S froze its output until Ctrl+Q. An interrupt sent from outside, withkill -INT, quits the app andappreturns; a second, before a stuck app has quit, goes to the interrupt handlerappreplaced, which forasyncio.runraisesKeyboardInterrupt. An app whose render loop is stuck can't be quit with Ctrl+C, since nothing reads it. -
#402 Breaking: a key press goes to the focused element, or to the root component's element when nothing is focused, then bubbles through its ancestors until a handler returns something other than
None, and a key press nothing claims gets itsunclaimed_keysdefault. Previously every handler received every key press, whatever the others returned. A component that handles keys below the root callsuse_focus, withauto_focus=Trueto have keys from the start, and puts its handler on the element it returns; one that should act on keys wherever focus is moves its handler to the root component's element. Tab has no built-in meaning: puttab_focuson the root for Tab navigation. -
#402 Breaking: a mouse event goes to one target, the element drawn on top at the pointer, then bubbles through its ancestors until a handler returns something other than
None. A press runs from the first button going down to the last coming up, and is captured by the element whose handler stops its firstMouseDown: its moves, and every button's presses and releases while it lasts, go to that element alone, wherever the pointer is, and reach no handler if nothing stopped the firstMouseDown. Previously every element under the pointer's current or previous position received every mouse event, so a drag that outran a render lost its element. A handler that acts on a release or a drag returns()for theMouseDown, or useson_click, and one that acts on any mouse event returns()to keep its ancestors from acting on it too. -
#402, #415 Breaking:
DivandTexttake oneon_inputhandler in place ofon_keyandon_mouse. It receives key presses, pastes and mouse events, so onematchcase can handleDownand the scroll wheel alike, and aReceiverdescribing the element, whoserectsandrelativelocate a mouse event inside it, and whoseis_targettells the event's target from an ancestor it bubbled to. A handler that takes only the event, or annotates it asKeyPressedorMouseEvent, fails to type-check; annotate it asInputEventand match on the event's class.Rectslives incounterweight.geometry, andcounterweight.hooksstill exports it; it's keyword-only, and hasclipandvisiblefields. -
#391, #416 Breaking: the app turns on the terminal's bracketed paste mode, so pasted text arrives as one
Pastedevent holding all of it, routed to the focused element like a key press, with its line breaks as"\n". Previously it arrived as key presses, and its line breaks as Enter.counterweight check-inputshows pastes too. -
#416 Breaking: keys pressed with Alt arrive as one key press:
Key.AltEnter,Key.AltBackspace,Key.ControlAltAand the other Ctrl+Alt letters,Key.AltUpand the other navigation keys,Key.AltF1throughKey.AltF12, and"alt+"before the text for text, such as"alt+b". Previously Alt with text, Enter or Backspace arrived as Escape and then the key, and Alt with an arrow, navigation or function key was dropped. -
#318, #402 Breaking:
Key.Spaceis" "rather than"space", andKey.ControlSpaceis"ctrl+space", like every other key, rather than"ctrl-space". Code that matched on the string value must be updated; matching on the enum member (Key.Space) is unaffected.
Layout
-
#305, #385 Breaking: layout is taffy's flexbox and grid, through waxy, with CSS's semantics.
Style.layoutis awaxy.Style, which the layout utilities set. Elements size to their content and don't grow to fill their parent unless givengrow(n), where in0.0.9every element took a share of its parent's spare space, with aweightof 1. An app keeps one layout tree for its lifetime, so taffy reuses the layout of every element whose style, text and children are unchanged, and aTextis wrapped at most once per width it is measured at. -
#305 Breaking: the spacing utilities are functions rather than one constant per size:
pad(n),pad_x(n),pad_y(n),pad_top(n)and the other sides,margin(n),margin_x(n),margin_y(n),margin_top(n)and the other sides,gap(n),gap_width(n)andgap_height(n)forgap_children_N,grow(n)forweight_N, andborder_contract(n)forborder_contract_N. Writepad(1)where you wrotepad_1.weight_noneandalign_self_noneare removed. -
#305 Breaking:
absolute(x, y)andrelative(x, y)areposition_absoluteandposition_relative, with offsets set byinset_left(n),inset_top(n),inset_right(n)andinset_bottom(n).fixed(x, y)is removed, since taffy has no fixed positioning. -
#385 Breaking: borders work as they do in Tailwind. A border side is drawn where its width in
Style.layoutis 1, andborder_kindonly chooses the characters.border_kinddefaults toBorderKind.Light, so the newborderutility alone draws a light border, and theborder_<kind>utilities don't reserve space or draw anything on their own: writeborder | border_heavywhere you wroteborder_heavy.border_top,border_bottom,border_leftandborder_rightset one side, the newborder_xandborder_yset a pair, and each has a new_0form (border_0,border_top_0, ...) that clears its sides, so sides compose:border_top | border_leftdraws both. Aborder_kindofNone(border_none) removes the border along with the space reserved for it. A border width inStyle.layoutother thanLength(0)orLength(1)raisesValueError. The edge combinations such asborder_top_leftandborder_top_bottomare removed: useborder, or compose sides as inborder_top | border_left. -
#385 The
*_centerand*_endalignment utilities (justify_children_*,align_children_*,align_self_*,justify_items_*,justify_self_*) use CSS's safe alignment: content too large for its container starts at the start edge instead of overflowing past it. Each has an*_unsafecounterpart, such asjustify_children_center_unsafe, that centers or end-aligns overflowing content anyway. -
#305, #315 Breaking:
counterweight.geometry.RectandEdgeare removed, andRectsholdsRegions. ARegionis keyword-only (left,top,right,bottom), itsrightandbottomare exclusive (the first column and row outside it), sowidthandheightcount cells, and its corner properties (such astop_left) arePositions. APositionis a frozen dataclass rather than aNamedTuple, so it doesn't unpack or index like a tuple. -
#416 Breaking: a
Textoverflows its box, as CSS's text does: a line wider than its content box runs past it, and rows beyond its height draw below it, until an ancestor whose overflow isHidden,CliporScrollcuts them off. Previously aTextcut its lines off at its content box. ATextthat should cut its text off takesoverflow_hidden, which cuts at its padding box, as the constraint utilities set. -
#402 Breaking:
zfollows CSS's stacking contexts. An element that setsz,z(0)included, paints with its descendants as one layer at itsz, and its descendants'zorders them only among each other, while an element that sets nozleaves its descendants to be layered alongside it. Children always paint over their parent, and a negativezpaints beneath the rest of its stacking context but over the element that forms it. Previously every element was sorted byzin one flat order, where an element that set none hadz0, so an element with a higherzthan its children painted over them, and an element with a negativezpainted beneath every element, its own parent included.
Styles
-
#385 Breaking: every field of a
CellStyle, and of aStyleexceptlayout,border_styleandtext_style, starts out as thecounterweight.styles.UNSETsentinel rather than its default value, so reading a field of a style you built can returnUNSET;border_styleandtext_stylestart out as emptyCellStyles. Merging with|takes the right side's value wherever it's set, even to the default, and unset fields take their values fromcounterweight.styles.STYLE_DEFAULTSandCELL_STYLE_DEFAULTS. -
#305, #443 Breaking: the color utilities are functions rather than one constant per color and shade:
text_color,text_bg,border_color,border_bg,margin_colorandpadding_color, and the newcontent_color. Writetext_color("slate", 500)where you wrotetext_slate_500, andborder_color("slate", 500)forborder_slate_500. Each takes a palette name and shade, or any color: aColor, such asrose_200orColor.from_hex("#fbbf24"), anIndexedColor,TERMINAL_DEFAULT, or a gradient. -
#443 Breaking:
from counterweight.styles.utilities import *brings in only the utilities, the palette's colors, and theColorName,ShadeandSidetypes, not the names the module imports, such asStyle,Color,CellStyleandBorderKind. Import those fromcounterweight.stylesinstead. -
#402 Breaking: an element's margin is painted only where its style sets
margin_color, so a margin leaves whatever lies beneath it visible, as on the web. Previously every element that painted anything filled its whole margin box black, so an absolutely positioned box with large margins blanked out its parent. -
#414
Color.from_hexaccepts the CSS shorthand#rgbas well as#rrggbb, and raisesValueErrorfor any other number of digits instead of dropping the extra digits of a color like#rrggbbaa.
Elements, painting and output
-
#414 Breaking: a
Div'schildrenis a tuple, and so is aText'scontentwhen it's chunks, so an element is a value that can be cached and shared without anything changing it afterwards. Pass(a, b)ortuple(...)where you passed a list. -
#385 Breaking: a
Screenshothandler receives acounterweight.paint.Frameinstead of an SVGElementTree; callframe.svg()orframe.text()to encode it. A frame'spaintis acounterweight.paint.Paint, a screen-sized grid of cells: index it with aPositionor an(x, y)pair, or read it a row at a time withrows().Screenshot.to_filechooses the encoding from the path's suffix (.svgfor SVG,.txtfor text with ANSI escape codes, andValueErrorfor any other), always indents SVGs, and doesn't takeindent.Screenshot.to_streamprints a screenshot as a text grid (with or without ANSI escape codes), useful for debugging layout and in tests. EveryScreenshotrequested in the same render cycle receives the same frame, where previously only the last one ran. -
#385, #398 Breaking: the painted cell
counterweight.paint.Pis renamedCell, and itscharis renamedgrapheme. It is a frozen dataclass of a grapheme cluster and its resolved style, and doesn't carry its element'sz. A cluster two cells wide is followed by a continuationCellwhosegraphemeis"".counterweight.elements.CellPaint, a grapheme cluster and its unresolvedCellStyle, is renamedStyledGrapheme, itscharis renamedgrapheme, and itswidthis the number of cells the cluster takes. -
#176, #410 Each frame is written as one synchronized update, so terminals that support it show the whole frame at once instead of tearing partway through drawing it, and redrawing the screen after a resize or a suspension doesn't flash it blank first.
-
#385, #425, #430 Painting and writing a frame costs less. Elements paint straight into one screen-sized grid of cells, a row at a time, an element's text and border are cached wherever it moves on the screen, and a long
Textpaints only the rows inside its clip. The changed cells are written with one cursor move per run of them, and a style change only where the style changes, sending only the colors when nothing else changes.
Dependencies
- #305, #315, #385, #398, #406, #409, #414, #429 Counterweight depends on waxy for layout, wcwidth for text width, markdown-it-py, mdit-py-plugins and Pygments for rich text, pyghostty for terminal emulation, Pillow for images, and without-cli, without-logging and without-streams for the CLI and the devlog. It no longer depends on typer (nor, through it, click, rich and shellingham), pydantic, structlog, parsy or cachetools.
Fixed
-
#431 An autopilot hands the app each step once the renders the last one caused are painted, and the effects they started have run up to where each first waits on something, as a person's next key press would. Previously effects didn't start until the autopilot ran out of steps, so
use_mouseanduse_hoverednever saw its mouse events. -
#444 An app run inside another, such as one rendered headless in another's
Screenshothandler, doesn't leave the other's state setters sending their updates to it, where they were lost, so the other app never re-rendered for them. -
#407, #409 Terminal input is parsed incrementally, so typed text arrives intact:
-
Text beyond ASCII is decoded as UTF-8, one
KeyPressedper grapheme cluster ("é","日"). Previously it was dropped, along with everything else read alongside it. - An escape sequence Counterweight doesn't recognize (such as Ctrl+digit in some terminals) is skipped. Previously it arrived as an Escape key press followed by its characters, or was dropped with the rest of the read.
-
An escape sequence or UTF-8 character split between two reads is held over and completed by the next one, and a lone
ESCis the Escape key once no more input follows for 50 milliseconds. Previously a split sequence turned into stray key presses, most often during a flood of mouse motion. -
#125, #402 The mouse wheel is reported as the new
MouseScrolledUpandMouseScrolledDownevents, as xterm defines them, with Shift, Alt or Control held too, and the horizontal wheel asMouseScrolledLeftandMouseScrolledRight. Previously all of these were reported as presses of buttons 1 to 4, so scrolling, or a sideways trackpad swipe, clicked whatever was under the pointer. Buttons 8 to 11, such as back and forward, are reported by their own numbers rather than as buttons 1 to 4. -
#125
counterweight check-inputworks. Previously its input thread crashed at startup. -
#402 Home, Page Up and Page Down are reported, as the new
Key.Home,Key.PageUpandKey.PageDown, and so is every key theKeyenum names with Shift, Control, or both, such asKey.ShiftHome,Key.ControlPageDownandKey.ControlF5, with Shift and F1 to F12 reported as F13 to F24, as xterm does. Previously only the arrow keys and Delete were reported with modifiers. Control+Shift+Delete is reported asKey.ControlShiftDeleterather thanKey.ControlShiftInsert. -
#392, #398 Text is measured, wrapped and painted by grapheme cluster and its display width, as measured by wcwidth, so CJK characters and emoji take two cells and don't push the rest of their line out of place, combining marks and joined emoji (
👩💻) stay whole, and zero-width characters such as zero-width spaces take no cells. See wide characters. -
#392, #398 Control characters in text other than line breaks, such as tab and escape, are drawn as symbols (
␉,␛) rather than written to the terminal, where they ran as commands. -
#386 Calling a
use_statesetter after its component has unmounted does nothing, instead of updating orphaned state and triggering a render that changes nothing. -
#386 A component replaced by a plain element at the same position is unmounted: its effects are cancelled, and components nested under it start with fresh state. Previously the element inherited the component's hooks, so its effects (including
use_mouselisteners) kept running. -
#386 Effects that rerun or unmount in a render are cancelled concurrently, and all of them finish before any effect starts, so an effect's cleanup always finishes before its replacement or a sibling's setup begins. Previously a rerun effect's new task started before the old one was cancelled.
-
#386 Keyed sibling components are matched by key across renders rather than by position, so reordering, inserting, or removing keyed children keeps each one's state and effects. Unkeyed children match the unkeyed child at the same index.
-
#408
appstops reading keys from the terminal before it returns, so input typed afterward reaches whatever runs next. Previously its key-reading thread kept running and consumed that input. -
#414 SVG screenshots draw text that is bold, dim, italic, underlined or struck through, instead of drawing it plain.
Removed
-
#305 Breaking: the style types
Border,BorderEdge,Margin,Padding,Span,Typography,Relative,Absolute,Fixed,FlexandInsetare removed. Sizes, spacing and position live inStyle.layout, awaxy.Style, which the layout utilities set, and colors, borders and text settings are fields ofStyleitself, such asmargin_color,border_kind,border_style,text_style,text_justifyandtext_wrap. -
#165, #385 Breaking: dropped support for Python
3.11through3.14. Python3.15or later is required.
0.0.9
Released 2024-02-27
Fixed
- #121
A change in effect reconciliation introduced in
0.0.8caused a regression in the behavior ofuse_effect, where if thesetupfunctionreturned (i.e., stopped itself), Counterweight would crash when trying to cancel the effect. This now works again.
0.0.8
Released 2024-02-17
Changed
-
#110 Calling a
use_statesetter with avalueequal to the current value of the state no longer emits aSetStateevent, to avoid triggering unnecessary render cycles. -
#111 Border healing is now more efficient, especially when there are many non-border characters in the UI.
-
#112 Major, backwards-incompatible changes to how Counterweight handles mouse interactions. The
on_mouse_downandon_mouse_upevent handlers have been removed; use the new combinedon_mouseevent handler instead, which receives all mouse events (MouseMoved,MouseDown, andMouseUp). Theon_hoverstyle attribute on elements has been removed; use the newuse_mouse,use_rects, anduse_hoveredhooks instead, and calculate the desired style in your component. The goal of these changes is to provide more flexibility and control over mouse interactions to application authors while minimizing the work that Counterweight needs to do while rendering, at the cost of more complex application code for simple cases like detecting hover state.
0.0.7
Released 2024-02-02
Changed
- #105
ScreenshotandSuspendhandlers can now beasyncfunctions.
0.0.6
Released 2024-01-28
Added
-
#92 Added an
insetattribute toAbsolutethat chooses which corner of the parent's context box to use as the origin for the absolute positioning. -
#98 Added a
zattribute toFlexthat controls the stacking order of elements.
Changed
Removed
- #96
Chunks can no longer be created withCellPaints directly.
0.0.5
Released 2024-01-06
Added
-
#86 Added a
Suspendcontrol which suspends the Counterweight application while a user-supplied callback function runs. Counterweight will stop controlling the terminal while the callback runs, and will resume when the callback returns. This can be used to run a subprocess that also wants control of the terminal (e.g., a text editor). -
#88 #90 Implemented
AbsoluteandFixedpositioning, which allow for precise placement of elements outside the normal layout flow.
0.0.4
Released 2023-12-31
Fixed
- #83
Fixed virtual terminal escape code parsing for mouse tracking when the moues coordinates are large (>94 or so).
Mouse tracking should now work for any terminal size.
Various
Keymembers that aren't currently parseable have been removed to avoid confusion.
0.0.3
Released 2023-12-30
Changed
-
#71 Controls are now a union over different
@dataclasses (instead of anEnum) to allow for more flexibility. TheScreenshotcontrol now takes a callback function which will be called with the SVG screenshot as an XML tree. -
#81 The minimum Python version has been raised to
3.11.2due to a bug in CPython3.11.1
0.0.2
Released 2023-12-26
Added
-
#65 Added ability to take an SVG "screenshot" by returning
Control.Screenshotfrom an event handler. The screenshot will be saved to the current working directory asscreenshot.svg; more options will be added in the future. -
#66 Added "border healing": when border elements for certain border types are adjacent to each other and appear as if they should "join up", but don't because they belong to different elements, they will now be joined up.
Changed
- Various namespaces moved around to make more sense (especially in documentation) and separate concerns better as part of #68.
0.0.1
Released 2023-12-19