ansi_escape_codes
A toolkit for working with ANSI escape codes and analyzing strings containing them.
4.0.1
124K downloads/mo
#66 most downloaded on pub.dev
vi-k/ansi_escape_codes
What this package is like to depend on
Last release 7 days ago
17 Aug 2026
Release timing varies
gaps range from 9 days to 11 months
Most releases are documented
notes for 19 of 28 stable releases
Nothing withdrawn
no release was ever pulled
2 years old
28 releases · first in 2024
17 releases in the last 12 months
see the full history below
Release timeline
28 releases · Mar 2024 to Aug 2026Releases
latest 28-
4.0.117 Aug 2026Release notes
Open source →The CHANGELOG has carried a 4.0.1 section since the meta floor came down;
this is the pubspec catching up to it, on the owner's word to publish. The
release carries one change and no API movement: the constraint that made
Flutter 3.27 unable to resolve a package built for Dart 3.6.Co-Authored-By: Claude Opus 5 (1M context) [email protected]
Release notes
Open source →Changed:
- The
metaconstraint reaches down to 1.15.0 again, so that the package resolves everywhere its own SDK constraint says it does. Flutter pinsmetato an exact version out of its SDK, and Flutter 3.27 — the line carrying Dart 3.6, this package's floor — pins 1.15.0; against^1.16.0that was a version solving failure, which made the real Flutter floor 3.29. Nothing in the API or its behaviour changes: the annotations used are@immutable,@internaland@visibleForTesting, all older than 1.15.0.
- The
-
4.0.016 Aug 2026Release notes
Open source →The owner asked for everything except publication to move into their list,
so the member-signature gate, the cheap backward walk and the gate against a
constant multiplier now sit in docs/backlog.md. Each entry carries the facts
an agent needs to start — the missing transitTo in the snapshot, the 1.7ms
against 178ms, what complexity_guard does not measure — rather than a
pointer into the handoff, which is rewritten every wave and would decay.The handoff drops its own copies of all three. An item kept in two files
drifts, and the copy nobody edits is the one that ends up lying; what stays
there is the state of the tree and the findings the owner did not take.Writing into the backlog is otherwise forbidden to agents by AGENTS.md. This
edit is the owner's instruction, not an agent's initiative.Co-Authored-By: Claude Opus 5 (1M context) [email protected]
Claude-Session: https://claude.ai/code/session_01Uox8hLVY5Ho8K6fofJd9g7Release notes
Open source →Added:
State,Style,StackandStylesnow model the standard primary and nine alternative fonts, fraktur, curly/dotted/dashed underline, proportional spacing and five ideogram renditions. Existing enum case names and style APIs remain source-compatible.insertBeforeandinsertAfteronParserandStackedParser, with theansiInsertBeforeandansiInsertAfterstring extensions. Text put into a styled string takes the style of the place it lands in and gives it back, so the rest of the string is left as it was: the style it opened of its own is closed after it, and the hyperlink it landed inside — links do not nest, and the inserted text may have superseded it with one of its own — is opened again behind it. Text inserted outside every link is closed off instead, so what follows it stays outside whatever the insertion pointed at.- The hyperlink a string has open, read back the way the style is:
linkAtandfinalLinkonParserandStackedParser, andlinkonMatch, beside itsstate, for a walk over the matches. A link is state but not style — it carries no rendition, andSGR 0does not close it — so it travels on a channel of its own and is answered on its own:linkAt(pos)for the link the character atpossits inside,finalLinkfor what the string leaves open. - The independent control functions, ESC Fs: constants for all ten of them,
ControlFunctionsEscFs,EscCommon— the entity aswitchover them matches — andresetTerminalfor RIS. DEL, which the C0 set was missing.useAlternateScreenanduseMainScreen, the screen a full-screen program draws on.- Types for the control sequences that carry something worth reading:
CursorUp,CursorDown,CursorRight,CursorLeft,CursorNextLine,CursorPrevLine,CursorHPos,ScrollUpandScrollDowncarryn;CursorPosandCursorHVPoscarryrowandcol;EraseInPageandEraseInLinecarry anErasePart;ShowCursor,HideCursor,UseAlternateScreenandUseMainScreenstand for the four private modes this package writes itself. ControlString, what the five strings of the standard have in common, withDcs,Sos,PmandApcbeside theOscthat was there already, andterminatedto ask whether one of them got the terminator that ends it.Oscis aControlStringnow rather than a direct child ofEscapeCode, which leaves aswitchmatching it by name as it was, and the four new entities carryUnrecognizedEscapeCodelike the other codes this package keeps without reading.ansiHasUnderlineColorandansiRemoveUnderlineColor: the extensions knew the foreground and the background but not the underline colour.ansiRemoveControlCodes, the other half ofansiHasControlCodes: the C0 set,DELand the eight-bit C1 taken out, withexcludefor the ones to keep — a text that is to stay in lines keeps itsLF.ESCis one of them, so the escape codes come out first or their bodies are left behind as text.excludenames members ofControlFunctionsC0and so cannot spare an eight-bit C1: this package gives those no names, on the reasoning below.Colors implements Comparable, so a list of them can be sorted.ansiShowControlFunctionsandansiOptimizeControlFunctions, which used to live in the tests though the README and the examples took them for public.- The control function types the API returns —
ControlFunctionsSGR,ControlSequencesFunctionsand the rest — are exported from the main entry point. ansi_escape_codes.dartbrings theStringextensions as well, so it is one import for all the string work: the ready-to-use strings, the styles, the parser, the state, the control function tables and the extensions. Two things stand outside it.ansi.dartalways did — the ready-to-use strings are built from its raw byte tables, and neither import brings the other. Andutils.dartdoes, becausetabsandcurrentCursorPostalk to a terminal in person throughdart:io, and nothing else in the package touches a platform library: bringing them in through the umbrella would tag the whole package native-only for the sake of two names, and take the web and WebAssembly away from the thousand that run anywhere. A test walks the directives of each entry point and holds that line.extensions.dartandstyle.dartstill bring their smaller namespaces, which is what they are for — not for reaching something the main import lacks.
Verification:
- Entry-point signature closure is now backed by an exact exported-namespace
snapshot, and neither is asked anything until
lib/has been swept for analysis errors and warnings: a library that fails to analyse still has an element model, only a smaller one. The generator preflights its eight registered marker zones before writing any generated file. - Stable CI gates 95.0% hand-written
lib/coverage while retaining the full coverage artifact, and runs a separate warmed complexity guard. The ordinary complexity assertions intest/performance_guards_test.dartremain deterministic and timer-free. - The complexity guard times the single implementation each scenario side has, checks the work that implementation produced both before and after the timing series, and holds the band against the median of the paired ratios rather than against a ratio of separately sorted medians.
Performance:
-
The scanner finds the next escape code by
indexOfrather than by the regex engine: text with no escape codes at all is parsed and stripped well over a hundred times faster (about 1 ms down to under 9 µs), a coloured page parses roughly 45-50 % faster acrossmatches,removeAll,optimizeandshowControlFunctions, and a page that is mostly escape codes is not worse — about a fifth faster rather than a wash.ansiHasEscapeCodesand friends answer a clean string withcontains(ESC)outright, without touching a pattern, andlengthWithoutEscapeCodescounts without building the cleaned string — the same walk over the same matches, so what is saved is the copy and not the time: a 5 MB page is measured with megabytes less at the peak — some 8 to some 20, depending on the page — and takes about as long as it did. -
substringand the insert seams keep their place the waystateAtalways did, instead of walking from the start each time: slicing a 200-line document through one parser is about three times faster (3.95 ms down to 1.33 ms), and the shape of the cost is linear now, not quadratic — the guard that doubles the input and watches the time saw a cost of ×3.73 where it now sees ×1.65, a linear cost being ×2. -
An escape code is told apart by its second byte instead of four named regex groups, a simple SGR function comes from a cached table instead of being rebuilt, and the matched text is read out of the match once: a simpler, more correct hot path, though its own saving lands inside the numbers above rather than as one of its own — isolated, it measures at noise level once the scanner and slicing fixes are in. One visible side effect: two
SgrSimpleFunctions built for the same code used to be two separate objects, and were never==to each other either, since the class defines neither==norhashCode; now they are the same cached instance, so bothidentical()and==see them as one and the same. -
A full parse retains the match list once, not twice, and a
Textpiece cuts its own substring out of the input only the first time something reads it, so a piece nobody reads keeps no copy of its own —stateAt, which never asks a piece for itsstring, is the concrete beneficiary: a walk that never reads a piece's string leaves tens of megabytes on the table against one that reads every piece, on the benchmark's 5000-line page. A scenario that forces everything to materialize up front, the waypreparedoes, is unaffected — full materialization was never what this bought. The saving is not free of a cost, though: aTextkept alive past itsParserpins the whole original input in memory for as long as theTextitself lives, whether or not itsstringis ever read. -
What has been read of a string is kept, instead of being read again by every question asked of it. Call
preparewhen there are many. -
A control sequence is looked up in a map rather than by walking the list.
-
A
Stackkeeps its histories as frames with a shared tail rather than as lists. Immutable lists had to be copied to be grown — twice, once to build the new one and once to seal it — so a push cost the whole depth so far, and a parse, which keeps every state it passed, kept every one of those copies with it. Text that switches attributes without resetting them, which is whatls --colorand most syntax highlighters write, made that quadratic in time and in memory both: 320 kB of it took 15 s and 7.6 GB throughStackedParser, whereParsertook 51 ms and nothing above the floor. The same string now takes 55 ms, level withParser, and 2.5 MB of that shape takes 299 ms. A push and a pop are one small allocation each, and every version of a stack shares the whole of its own tail with the versions it came from. Nothing aStackanswers has moved: its histories were only ever asked what was on top and whether they were empty. -
flushon all four printers: it writes out whatever is being held back without ending the line.PrinterandStackedPrinterhold a line until awritelnends it, and until now nothing could make them let go of one --- awritewith nowritelnbehind it was lost for good.SinkPrinterandStackedSinkPrinterhold the tail of a write that stopped in the middle of a sequence, and this says that the write which would have finished it is not coming.
Fixed:
-
ansiHasSgrandansiRemoveSgrdisagreed with the parser about the same sequences:'\x1B[1<m'.ansiHasSgrwas false while the parser read it as a rendition, andansiRemoveSgrleft behind whatansiRemoveEscapeCodestook out. They readsgrPattern, whose parameter class excluded<,=,>and?everywhere rather than only in the first place, where alone they make a sequence private use. The pattern now says what the parser says. -
ansiShowEscapeSequenceswrote a dangling separator where a control string had no terminator to name:\x1B]0;titleshowed as[OSC 0;title ]. The separator goes with the name now, so it shows as[OSC 0;title]and a terminated one is unchanged. -
Parser.substringrefused amaxLengthtoo large to add tostart. Asking for everything from anywhere but the beginning ---substring(1, maxLength: <a very large number>)--- took the sum round through the negatives and came back aRangeErrorfor a slice that was only asking for the rest of the string. A length reaching past the end is the rest of it, and stays so where no sum can hold it. -
An
SGRcarrying a private byte past the first ---CSI 1 < m,CSI 99 ; < m--- was written to the terminal twice. Only the first byte of a parameter string makes a sequence private use, so these are ordinaryCSI ... mwhose parameters cannot be read: the parser puts them in the opaque rendition branch and the branch writes them again, while the output also copied their bytes over as they came. Two places were answering "is this a rendition?" --- the parser where it reads the sequence, and a pattern whose parameter class knew nothing of a private byte past the first --- and the answers could differ. Only the parser answers it now. -
A write to
SinkPrinterorStackedSinkPrinterthat stopped in the middle of a sequence corrupted it. Every write is dressed on its own and the dressing opens with a reset, so that reset landed between the halves and the terminal read it followed by the rest of the sequence as text:\x1B[31mcut anywhere inside it showed[31mand no colour, a hyperlink cut inside its url showed the url, and a surrogate pair cut between its halves came out as two replacement characters. A write is now cut where no sequence is open across it and what is left waits for the write that finishes it, so the same bytes read the same however the writes fall across them. An unterminated control string is part of this: it now waits for the write that goes on with it, where before it went out at once and was ended by the next write's reset --- which madewrite('a' + title)thenwrite('b')show abthat the same bytes written in one go make part of the title. -
ESC 8with noESC 7in front of it left a non-defaultdefaultStylebehind. DECRC without DECSC clears the rendition, taking the terminal to its own defaults rather than to the printer's, and the printer went on believing its default style was still on --- so the transition for the text after it wrote nothing and that text came out bare. The printer's ownParserread its own output as saying so. The model now goes where the terminal goes and writes the default style back on. -
ESC 7/ESC 8lost their saved rendition, hyperlink and opaque SGR at every printer line or sink-write boundary, and a restore with no preceding save incorrectly used the previous chunk's seeded state. All four printers now keep one non-consuming, replaceable cursor save slot for their session; sinkpreparerolls a probed slot back with its other carry. -
Style.callread its inner ANSI throughStack, so a selective reset after two setters revealed the earlier inner value instead of returning to the caller's default style. Style wrappers now use the same terminal reset semantics asPrinter; the explicitly selectedStackandStacked*APIs keep their hierarchical pop contract. -
optimize,substring, insertions and all printers silently discarded an ordinary SGR function thatStatedid not model, and collapsed decorated underline to a single line. Known standard functions now have typed state; truly unknown SGR is carried by a private ordered residual channel through cuts, branches and printer resets until a realSGR 0clears it. -
ESC 7andESC 8carried no style. A terminal saves the rendition along with the cursor and restores both, so${fgRed}ESC7${fgBlue}ESC8shows red where the parser said blue — and every question asked after it was answered from the wrong state. The hyperlink travels in that same bundle: a terminal keeps it among the attributes it saves, so what a restore brings back is clickable again exactly where the save was, and a save made where no link was open puts that away as readily — the restore leaves no link behind it, rather than the one the string was started inside. -
An insertion left a hyperlink open.
OscLinkcarries no style, and the closing was worked out from the style alone, so text inserted with an unclosedOSC 8swallowed everything after it. -
An unfinished escape sequence in the inserted text swallowed the original tail: a truncated
OSCconsumed it whole, while a truncatedCSItook its first byte as the missing final byte. Insertions now preserve the same text model asoptimize,substringand the printers, without rewriting completed escape codes. -
cursorDownmoved the cursor left, and the cursor functions built sequences out of any number,-1included. -
runZonedStackedPrinterprinted only the first line. -
currentCursorPosleft the terminal in raw mode, read the answer as one chunk it does not always arrive in, and took the first CSI that came for the report — an arrow key is a CSI as well, so a key pressed while the terminal answered threw away a report that had in fact arrived. The report is looked for in what comes now, rather than assumed to be at the front of it. -
tabslooped forever on a tab width that never advances, and wrote over the line it was called on. -
Stackthrew where a reset had no style to pop, and took a colour it cannot hold inunderlineColor. -
faintstood forboldinstead ofdim. -
An ESC sequence was cut after two characters, and one carrying intermediate bytes was shown without them:
ESC ( BandESC ) Bcame out alike. A bareESCat the end of a string was swallowed as text — shown as nothing and counted in the length — and is a code of its own now: it shows as[ESC], andParser('abc\x1B').lengthsays 3 where it said 4. -
optimizeandsubstringdropped every code that was not SGR, and they, withisClosed, ignored the state the parser started from. -
The printers dropped them as well:
prepare('${cursorUp}x')gave back[CSI 0 SGR]x, the cursor movement gone, and a hyperlink came out as the bare text it was written over. What is not a style passes through them untouched now. -
An empty sub-parameter threw the whole sequence away, an RGB colour cancelled the rest of it, a broken colour took the rest with it, and
CSI 4:0 mswitched the underline on. -
An OSC string ended at the wrong place, or nowhere; a URL carrying
;was refused. -
ESC P,ESC X,ESC ^andESC _—DCS,SOS,PMandAPC— were read as finished two-character escape sequences, so the body the standard gives each of them came back as text. Each of the four opens a control string that runs to itsST, the way anOSCdoes, and one that never got a terminator ends at the nextESCor at the end of the text. What such a string carries — a sixel image, aDECRQSSanswer, a termcap reply — is part of the escape code now rather than of the plain text, and is no longer counted in the length:Parser('a\x1BPq#0;2;0;0;0\x1B\\b').lengthsays 2 where it said 13, and neither a slice nor an insertion cuts through the body, an unfinished code ending the string included.STends all five; theBELthat ends anOSCis xterm's and not the standard's, and it ends none of the other four, so aDCSwhose body happens to end in one is unterminated still — and one left unterminated is held back and given its terminator the way an unterminatedOSCis. -
SaveCursor,RestoreCursorandOscLinkcarried aresetas their text, so all three were equal to one another — anEntitycompares by what it is written with — and none of them equalled the same entity read back by the parser. -
ansiHasForeground,ansiRemoveForegroundand their background pair only saw a colour that was the whole parameter list.CSI 1;31 SGR, which is the shape this package's ownoptimizewrites, matched nothing, and neither did the colon formCSI 38:5:196 SGRthe parser has always read. Removal now keeps the functions standing beside the colour:CSI 1;31 SGRbecomesCSI 1 SGRrather than going whole. -
A colour held by a
Stacknamed itself?256Red, where the same colour from aParsersaidfg256Red. AStylewritten as a constant said the same, and a colour set on one target and then held in another slot answered under the target rather than the slot —bg256Redfor the colour of the text. The slot names it now, whichever way the style was built. -
NoStylepassed for the colours of the terminal. -
A private-use sequence was reported as an unknown one.
-
Color256.rgbandColor256.graychecked their arguments in an assert only, which release builds leave out. -
The superscript and subscript pair picked its winner the other way round from every other pair.
-
DELcounted as a control code but was never shown as one. -
Entities and functions described themselves wrongly in
toString. -
On Windows the terminal modes were put back in an order the console refuses — echo first, line mode still off — so
currentCursorPosthrew and left the terminal raw. Line mode now comes back first, and each mode is restored even when the other throws. Turning them off is guarded the same way now: when a stdin refuses one change, the one already made is undone instead of being left behind. -
substringcut a hyperlink in two and kept neither half right: a slice that began inside one came out unclickable, the opening having been left behind on the other side of the cut, and a slice that ended inside one left it open, so everything printed after the slice was clickable on the slice's URL. A slice is self-contained now, the way it always was in the style: one that began inside a link opens that link again in front of its first piece of text, andclose: truecloses at the end what the slice has open. Withclose: falsethe link is left open, as the style is. Cutting a document into lines this way gives lines that are each clickable on their own.The opening is written again in the bytes it came in, parameters and all: a link opened
BEL-terminated staysBEL-terminated, and anid=— which is whatOSC 8gives for a link a line break cuts in two — travels with it. The close written isOSC 8;; STwhatever form the opening took; terminals take either. -
Once a link passed through them at all, the printers had the gap the slice had: a printed line that opened a hyperlink left it open, and everything printed after was part of it. A line now closes the link it leaves open, and the line after opens it again — in the bytes it was opened with, as the slice does it — so a link a line break falls inside of goes on being one link, which is what the
id=ofOSC 8is for.SinkPrinterandStackedSinkPrintertake a write at a time and one line may be composed of several, so there an open link is carried across the writes and closed where the line really ends — at awriteln, or at a'\n'in what is written. A styled call goes through a printer and changed with them:Styles.red('…')now closes a link its text left open, and in a multi-line string the link reaches the end of the text instead of ending with the first line of it. -
optimizeleft a hyperlink open wheresubstringclosed one. Withclose: trueit now ends the string outside every link as well as in the default style: a string that opened a link and never closed it comes back closed, so that what is printed after it is not clickable. Withclose: falseboth are left as the string leaves them. -
Copying a control string out of the string it was read from could swallow the text behind it. One that never got its terminator — an
OSC, aDCS, anSOS, aPMor anAPC, a hyperlink opening no less than a window title or anything else the terminal answers to — runs on to the nextESCor to the end of the text, which is how the parser reads it on purpose, and written again in front of text that had not followed it there, by a slice, byoptimizeor by a printed line, it read that text as part of the sequence and showed nothing. The terminator it lacks is supplied where text follows it. That is of the codes copied over as they stand: there an escape code following leaves the bytes exactly as they came, theESCof what stands behind being terminator enough. At the edge of an output that closes — a slice or anoptimizewithclose: true, a printed line — the terminator is written although nothing follows it there, for the reason the hyperlink close is written in the same place: what is printed after must not be read as more of the sequence. Withclose: falsethe bytes are left as they came.SinkPrinterandStackedSinkPrinterpay the same debt where the line really ends — at awriteln, or at a'\n'in what is written — and owe nothing at the end of awritethe line goes on past. An opening written again for a slice or a line that began inside the link carries its terminator whatever follows it. -
The insertions reached the same mechanism last.
insertAftergoes past the codes standing at the seam, and where the string ended inside a sequence that never finished it went past those bytes as well —Parser('aa\x1B]0;title').insertAfter(2, 'X')handed back a string whose plain text was stillaa, theXhaving become part of the window title, and a hyperlink opening swallowed it no differently. A bareESCturned the insertion into anSOSand aCSIwith no final byte into anECH. Both insertions now stand in front of such a sequence rather than inside it, and the tail is copied on as it came: no byte of the input is invented, which is why no terminator is supplied here as it is for a slice. A finished code ends the run and is passed along with what stands before it — the run stood in front of is the one reaching the text, not everything unfinished in the string.A sequence still waiting for the byte that ends it hands the bytes it waits through back as text — the parameters of a truncated
CSIare the case worth naming, but aLF, aDELor a letter outside ASCII breaks off the pattern of a bareESCand of anESCon an intermediate byte the same way — and a position among them has no right answer: in front of the sequence is before characters counted in front of it, and where it was asked for is inside the sequence. Both insertions refuse it with anUnfinishedSequenceException, which carries the position asked for and the offset of the sequence the text would have been read as part of. Before this the same position quietly ate what stood there:Parser('aa\x1B[31').insertAfter(3, 'X')answered a string whose plain text wasaa1, the3having become a parameter.insertBeforewas no better, though the backlog had it down as safe everywhere.Unfinished codes come in runs, and the seam is in front of a whole run rather than in a gap between two of them — a gap between two of them is the inside of the first.
Parser('aa\x1BPpay\x1B[31').insertAfter(2, 'X')answers'aaX\x1BPpay\x1B[31', where before it answered'aa\x1BPpayX\x1B[31', whose plain text wasaa31: theXhad gone into the body of theDCS.Where such a run begins behind a piece of text a sequence in front of it is still reading, the place before the run is where that sequence's ending would be written, so the seam has no end to serve and is refused along with everything past it. This takes back answers that used to come:
Parser('aa\x1B[31\x1BPpay\x1B').insertAfter(4, 'X')throws where it answered'aa\x1B[31\x1BPpayX\x1B'— plain textaa31, theXswallowed by the body of theDCS. A code that stands finished between the text and the run gives the run a seam of its own, and that one is served. -
insertBeforeandinsertAftercould put text between the halves of a surrogate pair and hand back a string that is no longer valid UTF-16. A position inside a pair now shifts to its edge —insertBeforeto the front,insertAfterpast it. Positions,lengthand the paddings are UTF-16 code units, asStringcounts them, and the docs now say so instead of promising what is seen. -
ansiHasSgrandansiRemoveSgrcounted private control sequences ending inm— xterm's modifyOtherKeys, SGR mouse reports — as SGR, and removing styles removed them too. The pattern now takes digits,;and:only, the way the parser classifies them. -
ansiRemoveForegroundand its background and underline siblings ate the parameters after a colour cut short:\x1B[38;2;1;2mlost its bold and dim along with the broken colour. A colour missing arguments now gives up only its introducer and kind, the way the parser reads it — and the same goes for a kind the package does not know. It reads the other direction too: the parameter after a bare38,48or58is that colour's kind and goes with it, so\x1B[38;41mloses its41and\x1B[38;4:3mits curly underline where both used to be left standing. TheansiHas*answers moved together with the split —\x1B[38;41mno longer has a background. -
Leading zeroes hid a colour from the same functions: removing the colour from
\x1B[38;05;196mremoved everything but it. Parameters are now read as numbers, as ECMA-48 allows them to be written. -
A style operation with nothing to change built a new object anyway, and a
NoStyleasked for a pointless reset came back aStylethat writes:NoStyle().resetItalic('x')opened with a reset. Nothing to change now answers itself, asStatepromised all along. -
NoStyle().transitTo(Style.terminalColors)wrote a reset between two surfaces that are both the terminal's own. A transition between equal surfaces is empty. -
A
PrintergivendefaultStyle: NoStyle()still opened every line with a reset and unwound it at the end. It now imposes nothing: the line goes out as it came, its own codes included —ansiCodesEnabled: falseremains the way to take those out. -
The
styleentry point returned types it could not name:ControlFunctionsSGRand its four control-function siblings were reachable from the entities but undefined to the importer. The five exports are now part of the point, and every entry point carries an exports test. Theextensionspoint had the same gap:ansiRemoveControlCodestakes aSet<ControlFunctionsC0>its own importer could not name, so the enum is now part of the point. -
prepareonSinkPrinterandStackedSinkPrintercoloured the writes that came after it. The piece it is asked about never reaches the sink, and the link open in the output, the link open in the text and the terminator an unterminated control string owes were all put back for that reason — the style the piece ended in was not, soprepare('${bold}asked')left the printer reading the nextwriteas if the bold had been sent. All four carries are put back now. -
The eight-bit forms of the C1 controls — the bytes
0x80through0x9F— went through the control-code extensions unseen.ansiHasControlCodesansweredfalsefor a string made of them,ansiRemoveControlCodesleft them where they stood andansiShowControlCodesshowed nothing, so a string cleaned or spelt out for display still carried bytes a terminal prints as rubbish. All three know them now, and the display writes them as the number of the byte in every style: they have neither an abbreviation nor a Unicode picture of their own, and this package gives them no names, since a name would suggest it reads them as sequences. It does not, and that has not changed:Parserreads0x9Bas text rather than as aCSI, andansiRemoveEscapeCodesleaves it standing. What is parsed here are decoded Dart strings rather than byte streams, where a genuine eight-bit C1 does not survive UTF-8 decoding and terminals emit the seven-bitESC [form anyway.0xA0and above are not controls and are untouched. -
substring(close: false)took an attribute off that was meant to survive. A slice left open is written by askingtransitTofor the reset half alone — it unwinds what the string took off by the cut and does not put on what belongs to the character after it. ButCSI 22takes bold and dim off together, sotransitTowrites it wherever one of the pair goes off and leans on the other half to bring the survivor back:CSI 22;1. That1belongs to the reset rather than being a set of its own, and going out without it left\x1B[1;2mAB\x1B[22;1msliced open at\x1B[22m— a slice standing in neither its own state nor the string's.skipSetleaves it in place now. The four other pairs are unaffected:24,25,54and75are written only where the far end carries nothing at all, so a change from one kind to the other is a plain set and an open slice goes on leaving it out. -
optimize,substringand the printers swallowed the text behind a code the parser could not finish. All four held a code back only where it was an unterminated control string, while three other shapes wait for a byte just as surely — a bareESC, aCSIwith no final byte, anESCleft on an intermediate byte. In the string each of those was ended by theESCof whatever stood behind it, and where that was anSGRthese loops do not copy it but write it again as a transition — which, for a redundantSGR, writes nothing at all. The code then stood against the text and read it as its own:Parser('\x1B[3\x1B[0m1m!').optimize()gave\x1B[31m!, three characters of text turned into a colour, and a truncatedCSIwent on eating until it found a final byte. A redundantSGRis whatoptimizeexists to remove, so the defect was the feature working. All four now hold back whatever the parser could not finish and supply anSTwhere what follows would otherwise be swallowed — anSTis anESCand a\, so itsESCbreaks off the waiting sequence exactly as the string's own did, and anSTthat closes nothing does nothing. WhatremoveAllcalls the text is what comes out of all four; seedocs/records/2026-08-13[6]for the invariant and for what it costs on a truncatedCSI, where this package's reading of the input and a terminal's already differed. -
linkandlinkBelwrote the address into the body of anOSC 8unchecked. AnESCthere ends the sequence where it stands, so a url carrying one handed the rest of itself to the terminal as codes of its own:link('https://ok\x1B\\\x1B[2J…')cleared the screen, and the parser read the result as seven entities where three were meant. Urls in a command-line tool arrive from git remotes, HTTP answers and registries, so the bytes are rarely the caller's. Both functions percent-escape what anOSC 8cannot carry — the C0 controls andDEL— and nothing else: an address that carries none, which is every address that is one, comes out byte for byte, its own percent-escapes untouched.Uri.encodeFull, which theOSC 8note asks for, escapes the%as well and would turn an already-encoded address into%2520. The eight-bit C1 are deliberately not escaped: one of them is a single code unit in a Dart string and two bytes in UTF-8, so a single-byte escape would name the wrong byte, and this package does not read them as control codes anyway. Thetextof a link is written as it came — styling it is what the codes are for — and where none is given the encoded address stands for it.
Renamed:
MatchisPiece,MatchesisPieces, andparser.matchesisparser.pieces. The old name shadoweddart:core.Match, and shadowed it silently: an explicit import outranks the implicit one, so the compiler never asked which was meant. Ordinary code written beside this package —for (final Match m in RegExp(r'\w+').allMatches(s))— failed with two errors that named no package, and the advice this README gave for the Flutter names did not cover it, because the Flutter names do raise the question and this one did not.Pieceis the word the package already used for the thing: the class dartdoc opened with "one piece of a parsed string", and_pieceAt,nextPieceandtakePiecewere there before the rename. There is deliberately notypedef Match<S> = Piece<S>to ease the move — it would reintroduce the shadowing this removes. A test holds the name open from the outside: it usesdart:core.Matchbeside a single import of this package, and stops compiling if the name is ever taken back.rgbandgrayarergb256andgray256. Both answer with an index into the 256-colour table --- the 6×6×6 cube and the 24-step grey ramp, taking 0..5 and 0..23 --- and stood one name away fromfgRgband its pair, which take a truecolour triple of 0..255 and write it into the sequence itself.fg256(rgb(255, 0, 0))is the mistake the old names invited, and it throws rather than showing the wrong colour, but the new names say which of the two kinds of red is being asked for. They are also two very general words to have been taking out of a caller's namespace.Color256.rgbandColor256.graykeep their names: a named constructor says whose they are.- The hyperlink entity is
OscLink, notLink.Linkshadoweddart:io.Link--- a symbolic link --- and shadowed it the silent wayMatchused to shadowdart:core.Match: an explicit import outranks the implicit one, so a command-line tool, which almost always importsdart:io, got two errors that named no package. The new name says which sequence it is, the wayEscCommonandCsiCommondo. A test holdsdart:io.Linkopen from the outside and stops compiling if the name is ever taken back. RESERVEDtoRESERVED_5F, named after its byte rather than claiming a word that plain in the namespace this package exports.toStringAsEscapeSquencestotoStringAsEscapeSequences, which was missing a letter.- The
standart_colorsdirectory is speltstandard_colors.
Removed — every name deprecated in an earlier release, and some that never were:
- The style constants renamed in 2.0.0:
faint,resetBoldAndFaint,italicized,resetItalicized,singlyUnderlined,doublyUnderlined,resetUnderlined,slowlyBlinking,rapidlyBlinking,resetBlinking,negative,resetNegative,concealed,resetConcealed,crossedOut,resetCrossedOut,framed,encircled,resetFramedAndEncircled,overlined,resetOverlined,superscripted,subscriptedandresetSuperAndSubscripted. Usedim,italic,underline,blink,inverse,invisible,strikethrough,frame,encircle,overline,superscript,subscriptand theirreset…counterparts. - The string extensions without the
ansiprefix:hasEscapeCodes,hasCsi,hasSgr,hasForeground,hasBackground,removeEscapeCodes,removeCsi,removeSgr,removeForeground,removeBackground,showEscapeCodesandshowControlCodes. Use theansi…names. - The typedefs left behind by the renaming in 3.0.0:
AnsiParser,AnsiPrinter,SgrState,SgrPlainStateandSgrStackedState. UseParser,Printer,State,StyleandStack. Style.defaultsandStack.defaults. UseterminalColors, orNoStylewhere nothing at all should be written.Parser.stateAtPosandrunZonedAnsiPrinter. UsestateAtandrunZonedPrinter.foregroundPatternandbackgroundPattern, the regular expressions that encoded the assumption a colour is the whole parameter list. Nothing else used them.MatchingState,MatchesResultandParserIterator, which the parser passes to and gets back from its own private methods and nothing else could reach.MatchesandMatchare unchanged.IntensityStyleleft the public API. It is the element aStack's intensity history holds; nothing public takes or returns it, and bold and dim — unlike the other pairs — can be on at once, so no getter could honestly answer with one of them.- The
parsingentry point. After 4.0.0 made it byte-identical tostyle, one of the two names had to go: importpackage:ansi_escape_codes/style.dart— the same 81 names — or the umbrellaansi_escape_codes.dart.
Breaking changes:
-
Stack.underlineColortakes anExtendedColorwhere it took aColor.SGR 58carries a 256-colour index or a truecolour triple and has no 16-colour form at all, so aColor16was a colour the sequence could not be written with —Style.underlineColorhad always taken the narrower type, and the two now agree. Narrowing a parameter is source-breaking:stack.underlineColor(Color16.red)no longer compiles, andColor256.red, whose index is the same colour, is what it becomes. The Fixed list below mentions the change as part of the bug it belongs to; it is named here because the compiler will name it first. -
OscLink(url)—Link(url)before the rename above — is no longerconst. It percent-escapes a control byte in the address, aslinkdoes and for the same reason, and aconstinitializer admits neither a function call nor acontains— so the address could there be neither encoded nor so much as checked. Aconst Link('…')has to lose the keyword along with the name; nothing else about it moves.OscLink.urlreads back the encoded address rather than the bytes handed in, so that it agrees with a parse ofOscLink.stringand with the equality anEntitytakes from those bytes. -
The named control sequences print as themselves:
CursorUp(4),CursorPos(3, 7),EraseInPage(ErasePart.all)whereCsi([CSI 4 CUU])was written before. Nothing readstoStringbut a person and a golden test. -
Csi,EscandEscapeCodeare sealed, and this release adds types under them. Aswitchthat covers them exhaustively has to name the new ones.ischecks, casts and the identifiers entities are shown by are unchanged. -
Color.withPrefix(String)is gone, and nothing public stands in its place: a colour is named by the slot of the state it is held in, soStyle(background: c).backgroundColor?.idis whatc.withPrefix('bg256')was for. The string was a way to be wrong —withPrefix('bg256')gavebg256256Gray5— and it let the name of a target be written out by hand, which is how the colour of the underline came to call itselfunderlineColor256Redwhere the constant isunderline256Red.ColorTargetnames the three slots now, and takes the name from the SGR function that sets them, so there is one place for it. -
The colours on
Style—red,bgYellow,rgb531and the rest of that table — now come from an extension,StyleColors, rather than from the class itself. Written the usual way they behave as they did; what an extension cannot do is answer adynamicreceiver. -
The predefined styles are constants of one class,
Styles, and there are 783 of them: the fifteen properties —Styles.bold,Styles.italic— and the 256-colour table three times over,Styles.redfor the colour of the text,Styles.bgRedfor the colour behind it, andStyles.underlineRedfor the colour of the underline, which had a name in neither the 530 nor the getters. Being constants, a style can be held in one:const error = Styles.red.The 530 top-level names are gone, among them
foreground,backgroundandunderlineColor. A chain that starts at nothing at all starts atStyle.terminalColors; the three functions are the constructor,Style(foreground: c), or the methods of the same name on a style.The colours are still getters as well — the
StyleColorsextension — soStyles.red.bold.bgYellowis one chain, asred.bold.bgYellowwas.This is what took the 31 names that
style.dartandansi_escape_codes.dartboth claimed out of the way, and the second now exports the styles as well: one import where there were two.
-
3.1.230 Apr 2026 -
3.1.108 Apr 2026 -
3.1.008 Apr 2026 -
3.0.807 Apr 2026 -
3.0.707 Apr 2026 -
3.0.603 Apr 2026 -
3.0.503 Apr 2026 -
3.0.403 Apr 2026Nothing published for this version
-
3.0.320 Mar 2026 -
3.0.206 Mar 2026Nothing published for this version
-
3.0.106 Mar 2026Nothing published for this version
-
3.0.006 Mar 2026Nothing published for this version
-
2.2.126 Feb 2026 -
2.2.025 Feb 2026Release notes
Open source →- Fix README.
- Add predefined colors:
Color256.rgb123, etc. - Add
Color256.rgbandColor256.gray. - Update
example/colors256.dart.
-
2.1.017 Sep 2025Release notes
Open source →- Add methods
indexOf,lastIndexOf,contains,startsWith,endsWiththat work with a plain string.
- Add methods
-
2.0.317 Feb 2025Nothing published for this version
-
2.0.217 Feb 2025Nothing published for this version
-
2.0.117 Feb 2025Nothing published for this version
-
2.0.016 Feb 2025Release notes
Open source →- Shift package focus to parsing and standardization.
- Add
AnsiParser. - Add
AnsiPrinter. - Add stacked
AnsiPrinter. - Add their corresponding functions to intercept the
printfunction using zones:runZonedAnsiParser.
Breaking changes:
- The names of some constants have changed:
italictoitalicized,blinkingtoslowlyBlinking. All constants of the formnot…are renamed toreset….(fg/bg/underline)Bright…ara renamed to(fg/bg/underline)High…. - Removed methods:
handle…,all…. UseAnsiParserinstead.
-
1.4.127 Jan 2025 -
1.4.025 Jan 2025Release notes
Open source →- Add analysis escape sequences (showEscapeSequences).
- Add all control codes (0x00-0x1F).
-
1.3.124 Jan 2025Nothing published for this version
-
1.3.006 Mar 2024Nothing published for this version
-
1.2.006 Mar 2024 -
1.1.005 Mar 2024 -
1.0.005 Mar 2024