Replies: 28 comments
|
Yes @Jackenmen, actually I do agree with Kovid Goyal's specification,
I independently came to the same conclusion, my remarks from analysis last year
This analysis was done in response to @j4james' criticism on HN,
I asked @j4james to review, responding,
And so the matter was closed and not published. |
|
I renamed your title to be more clear, please check, that: >>> wcwidth.wcswidth("\N{WHITE HEAVY CHECK MARK}\N{VARIATION SELECTOR-15}")
2wcswidth() measures this as a "wide character" (2 cells), which matches a majority of terminals, but, in kitty, this is displayed as "narrow" (one cell). I do agree the correct interpretation of the specification should return 1 instead of 2, but python wcwidth as it is currently published returns 2. Happy to have any further feedback. |
|
Yes, returned cell width for I don't think there's really anything better to do in wcwidth then and this should just be closed. |
|
Your welcome! Yes, Kovid and I may be wrong about the interpretation of the unicode.org standard.. it's not really our fault, the for-pay membership unicode technical committee makes little to no effort to specify any concern for fixed-width/monospace rules, so they leave a lot up for interpretation. In these cases, this library aims to be most compatible with a majority of "progressive" or "actively developed" terminals. kitty is certainly one of the best, ranking number 2 in ucs-detect results which includes matching the specification of python wcwidth, (after ghostty, number one, which also appears to agree to display these characters Narrow by my testing). But, they are also alone in this regard, only 2 out of 35 terminals tested match this behavior right now. I have updated ucs-detect report to reference this issue,
So I will leave this issue open, anyway. I can certainly be persuaded to match kitty and ghostty, as these terminal authors are very progressive and thoughtful about their implementation compared to the other terminal authors. |
|
An issue where @j4james persuaded @christianparpart against implementing VS-15/U+FE0E in Contour terminal, contour-terminal/contour#404 (comment) |
|
If you think an emoji followed by VS-15 should have a width of 1, here's a little test for you to run. import sys
check = '\u2705'
check_vs15 = check+'\uFE0E'
sys.stdout.write('<--------20-------->\n')
sys.stdout.write('\033[4h') # enable insert mode
sys.stdout.write('*'*10) # write 10 asterisks
sys.stdout.write('\r') # return to start of line
sys.stdout.write(check*5) # insert 5 check marks
sys.stdout.write('\n') # move to new line
sys.stdout.write('*'*10) # write 10 asterisks
sys.stdout.write('\r') # return to start of line
sys.stdout.write(check_vs15*5) # insert 5 check marks with VS-15
sys.stdout.write('\n') # move to new line
sys.stdout.write('\033[4l') # back to replace mode
sys.stdout.write('<-----15------>\n')In the code above, we're writing out 10 asterisks, then inserting 5 emojis at the start of the same line using If VS-15 makes the emoji narrow, then the first line should span 20 columns, and the second line should only span 15 columns. Here's what it looks like in Ghostty: And here's what it looks like in Kitty: Make of that what you will. |
|
We mismatch on ghostty, versions 1.3.0-main+4b7a55a50 (Feb 26), and 1.3.2-main+4903e2821 (today) appear as:
Which includes key VS-15 related PR's, ghostty-org/ghostty#9678 and ghostty-org/ghostty#9679 (Nov 24) As for IRM behavior, you are correct that you predicted this misbehaving insert mode with a wide-to-narrow VS-15, wcwidth so far does not interpret this sequence (and I don't think it should) |
Yeah, I just came here to post an updated screenshot. My original test was on an older version. But the fundamental problem still remains. Neither of those terminals are consistently handling VS-15 characters as 1 cell wide, and any application assuming that is going to break. |
|
Btw, here's another scenario for you. Imagine you're writing an editor, and you've determined that the terminal screen width is Test case: import struct, fcntl, termios
winsz = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
h,w,_,_ = struct.unpack('HHHH', winsz)
check = '\u2705'
check_vs15 = check+'\uFE0E'
print('*' * w)
print(check_vs15 * w)With a terminal width of 40, I can fit 40 asterisks on one line, so if a VS-15 check mark is also 1 cell wide, I should also be able to fit 40 check marks on one line, right? Do you honestly still think that these are the only terminals that have got VS-15 right, and everyone else has got it wrong? |
|
For the kitty bug you present, I suggested the solution in our conversation previously,
I don't have anything more to say, I do value your feedback, and thank you again for discussing it with me. You are probably right, that this is difficult to implement correctly. If kitty/ghostty was not alone in this implementation I would suggest this code fix there, but since they are alone in this implementation, this is just an open issue for discussion. Or maybe a bug that kitty and ghostty can reference. |
I don't make any such claims, I only bring this up for discussion, not that "everyone else is wrong", but that my interpretation of the unicode standard is that VS-15's purpose is to make emoji presentation to text presentation, and that text presentation of emojis are narrow, best wishes |
No, it is not safe to assume that, precisely because VS15 exists. And the correct way to address this and various other infelicities of mapping the unicode standard to a character grid display is to specify the width in cells of tet explicitly when rendering the text rather than using wcswidth(). A protocol ans spec for which already exists: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/ It is not to make it impossble to use the text form of hundreds (and growing) characters in the Unicode standard.
They are the only two that follow the actual standard, so yes, they are the only two that get it right. |
|
On Sun, Mar 29, 2026 at 02:56:24AM -0700, James Holderness wrote:
j4james left a comment (jquast/wcwidth#211)
> They are the only two that follow the actual standard
Here's what Unicode TR51 says:
> Emoji characters can have two main kinds of presentation:
>
> * the primary emoji presentation, with colorful and perhaps whimsical shapes, even animated
> * a text presentation, such as black & white
>
> More precisely, a text presentation is a simple foreground shape whose color is determined by other information, such as setting a color on the text,
And here's an image from the same document showing example renderings for emoji with text presentation vs primary presentation.
<img width="345" height="239" alt="Image" src="https://github.com/user-attachments/assets/d2e7e980-f4d5-4782-b148-83929cb5f453" />
Can you quote the section of the standard that says a text presentation is intended to be narrower than the emoji presentation?
Not going to bother, since you know perfectly well that the Unicode
standard does not say much of anything about how text should be mapped
to a fixed width grid, so there is no "section to quote", per se. So
read on to learn how the standard *implies* it.
All emoji presentation codepoints are rendered in two cells and all text
presentation code points are rendered in one cell *unless their width is
set to two* in EastAsianWidth.txt from the standard. This has been how
Unicode is mapped to character grids since forever.
The unicode standard says quite clearly even in the passage you quoted
that VS15 makes codepoints have TEXT PRESENTATION. And TEXT PRESENTATION
means that they are rendered as text, subject to the rules for rendering
text. Your interpretation would mean that it would be impossible to render
these codepoints in one cell, even when one cell is the appropriate
width for their text presentation forms.
The vast majority of terminal developers have never even considered this
issue, let alone come to some sort of informed consensus about it so to
claim, as you did, "all terminal developers except two agree on your
position", is disingenuous, at best. *You* might have considered this
issue and come to a conclusion, don't try conflating yourself with
everyone.
|
So when you said "they are the only two that follow the actual standard", you were referring to a standard that doesn't say much of anything. So at best you could argue that nobody is wrong, because the standard doesn't say.
If the appropriate width was one cell, then the default representation should already be one cell. Unless you just mean that an app can't forcibly squeeze a two cell emoji into one cell, in which case I agree, "my interpretation" doesn't allow for that. But I'm fine with that, because it guarantees predictable behavior for applications, which your interpretation does not (unless they're also using your text-sizing protocol, but in that case why would they need VS-15 to alter the width anyway?) In any event, I'm not trying to persuade you to change the behavior in Kitty. I'm just pointing out to jquast that wcswidth is unusable with Kitty and Ghostty, regardless of whether it returns 1 or 2 for VS-15 emoji, so switching from 2 to 1 will just ensure that's it unusable for all terminals.
Fair enough - that was hyperbolic. But it has been discussed to death by a fair number of terminal devs over the years (some examples include here and here and here). And I know there are more terminal devs that weren't directly involved in those discussions, but have since adopted the terminal-unicode-core spec, which explicitly states VS-15 doesn't affect width, which suggests a conscious agreement with that behavior. |
|
On Sun, Mar 29, 2026 at 04:46:31AM -0700, James Holderness wrote:
j4james left a comment (jquast/wcwidth#211)
> Not going to bother, since you know perfectly well that the Unicode standard does not say much of anything about how text should be mapped to a fixed width grid
So when you said "they are the only two that follow the actual standard", you were referring to a standard that doesn't say much of anything. So at best you could argue that nobody is wrong, because the standard doesn't say.
The standard does say. It says VS15 converts the preceding codepoint
into TEXT PRESENTATION. The standard also says the TEXT PRESENTATION
characters should be narrow unless otherwise specified in
EastAsianWidth. Put those two statement together and you have VS15
alters width.
> Your interpretation would mean that it would be impossible to render these codepoints in one cell, even when one cell is the appropriate width for their text presentation forms
If the appropriate width was one cell, then the default representation should already be one cell. Unless you just mean that an app can't forcibly squeeze a two cell emoji into one cell, in which case I agree, "my interpretation" doesn't allow for that. But I'm fine with that, because it guarantees predictable behavior for applications, which your interpretation does not (unless they're also using your text-sizing protocol, but in that case why would they need VS-15 to alter the width anyway?)
No, the default representation, aka the emoji representation can have
different width than the alternate representation, aka the text
representation. By your argument VS16 should then not make its preceding
character wide just like you say VS 15 should not make its preceding
character narrow. The terminal-unicode-core spec you reference itself
contradicts this.
And no, the behavior with VS 15 narrowing is perfectly predictable. You
just need to know how to split up text you want to output into
graphemes, at which point you can output text so that it fits into
wcswidth() cells even if the last grapheme is narrowed by VS15. And you
can do this with classic terminal escape codes, no need for new
protocols. Since in order to implement wcswidth() correctly,
you need to know how to split into grpahemes anyway, you get it for
free.
In any event, I'm not trying to persuade you to change the behavior in Kitty. I'm just pointing out to jquast that wcswidth is unusable with Kitty and Ghostty, regardless of whether it returns 1 or 2 for VS-15 emoji, so switching from 2 to 1 will just ensure that's it unusable for all terminals.
I agree wcswidth() as an interface is broken. What you really need is
grapheme_iter() with which you can output text predictably.
|
This library does provide wcwidth.iter_graphemes() which matches implementation of python's next release 3.15 unicodedata.iter_graphemes() function, this library's |
I'm willing to accept that this is something I've overlooked in the standard, but when I asked you to quote the relevant text, you refused, saying that "the Unicode standard does not say much of anything about how text should be mapped to a fixed width grid". You then went on to say that it could be implied based on the following claim:
But that statement isn't actually true. For example, the regional indicator symbols have the Emoji_Presentation property, but have a width of 1 (EastAsianWidth.txt assigns them a neutral width, which the standard recommends should be treated as narrow). So as far as I can see, the presentation is irrelevant. Some text presentation codepoint are wide and some are narrow. Some emoji presentation codepoints are wide and some are narrow.
The difference is that the standard explicitly stated that VS-16 should make a codepoint wide, regardless of the original assigned width. See here:
That line was removed in later revisions, but that was the reason why many terminals considered it a reasonable choice to make back when this was first discussed. There was no mention of text presentation sequences having any effect on the assigned width. |
|
On Sun, Mar 29, 2026 at 07:11:01AM -0700, Jeff Quast wrote:
jquast left a comment (jquast/wcwidth#211)
> I agree wcswidth() as an interface is broken. What you really need is grapheme_iter() with which you can output text predictably.
This library does provide [wcwidth.iter_graphemes()](https://wcwidth.readthedocs.io/en/latest/intro.html#iter-graphemes) which matches implementation of python release 3.15 [unicodedata.iter_graphemes()](https://docs.python.org/3.15/library/unicodedata.html#unicodedata.iter_graphemes) function, this library's ``wcswidth()`` is then meant to be called on each grapheme, I have somewhat changed the definition of wcswidth() from Markus Khun's 2007 implementation, it no longer measures "sum of wcwidth()" as it was naively implemented then, but now to measure graphemes that may include ZWJ, VS-16, RI's etc.
Yup, iter_graphemes is what's needed. I am busy right now but a little
later I'll give you a function that can be used to output a list of
graphemes to the terminal so that it works even when the last grapheme
is a narrowed by VS15 one (the case @j4james was talking about above).
That way any application can output lines of text to the terminal robustly
regardless of use of VS15 to narrow.
|
|
On Sun, Mar 29, 2026 at 07:20:36AM -0700, James Holderness wrote:
j4james left a comment (jquast/wcwidth#211)
> The standard also says the TEXT PRESENTATION characters should be narrow unless otherwise specified in EastAsianWidth.
I'm willing to accept that this is something I've overlooked in the standard, but when I asked you to quote the relevant text, you refused, saying that "the Unicode standard does not say much of anything about how text should be mapped to a fixed width grid".
UAX 11 (not bothering to link it here am sure you know where to find
it) assign 5 categories to all code points. Wide, narrow, full width,
ambiguous and neutral. Then in ED4 it says:
East Asian Wide (W): All other characters that are always wide. These characters occur only in the context of East Asian typography where they are wide characters (such as the Unified Han Ideographs or Squared Katakana Symbols). This category includes characters that have explicit halfwidth counterparts, along with characters that have the [UTS51] property Emoji_Presentation, with the exception of characters that have the [UCD] property Regional_Indicator
Note in particular the part about Emoji_Presentation.
You then went on to say that it could be *implied* based on the following claim:
> All emoji presentation codepoints are rendered in two cells and all text presentation code points are rendered in one cell *unless their width is set to two* in EastAsianWidth.txt from the standard.
But that statement isn't actually true. For example, the regional indicator symbols have the **Emoji_Presentation** property, but have a width of 1 (EastAsianWidth.txt assigns them a neutral width, which the standard recommends should be treated as narrow).
I didnt enumerate all the corner cases. You want a full reference see:
https://sw.kovidgoyal.net/kitty/text-sizing-protocol/
The difference is that the standard explicitly stated that VS-16 should make a codepoint wide, regardless of the original assigned width. See here:
https://www.unicode.org/reports/tr11/tr11-41.html#Recommendations
> [UTS51] emoji presentation sequences behave as though they were East Asian Wide, regardless of their assigned East_Asian_Width property value.
That line was removed in later revisions, but that was the reason why many terminals considered it a reasonable choice to make back when this was first discussed. There was no mention of text presentation sequences having any effect on the assigned width.
It's still there in ED4 as I linked to above. To me it is trivially
obvious when it says that Emoji presentation is expicitly wide it
implies text presentation (modulo corner cases specified elsewhere) is
narrow.
|
|
As promised, here is a function that takes an iterator over graphemes class Grapheme:
text: str
width: int = 1
def output_in_wcswidth(iterator_over_graphemes):
' Output graphemes so that they affect exactly wcswidth cells only (works for >= 2 graphmes) '
graphemes = tuple(iterator_over_graphemes)
if not graphemes:
return
yield graphemes[0].text
for i in range(1, len(graphemes)):
g = graphemes[i]
if g is graphemes[-1]:
prev_g = graphemes[i-1]
yield f'\x1b[{prev_g.width}D' # move cursor back]
yield g.text
yield f'\x1b[{g.width}D' # move cursor back]
yield f'\x1b[{prev_g.width}@' # insert cells]
yield prev_g.text
yield f'\x1b[{g.width}C' # move cursor forward] |
|
@j4james read http://www.unicode.org/reports/tr11/#ED5 East Asian "Narrow (Na): All other characters that are always narrow and have explicit fullwidth or wide counterparts. These characters are implicitly narrow in East Asian typography and legacy character sets because they have explicit fullwidth or wide counterparts. All of ASCII is an example of East Asian Narrow characters." Then in ED7: "ED7. Neutral (Not East Asian): All other characters. Neutral characters do not occur in legacy East Asian character sets. By extension, they also do not tend to occur in East Asian typography. For example, there is no traditional Japanese way of typesetting Devanagari. Canonical equivalents of narrow and neutral characters may not themselves be narrow or neutral respectively. For example, U+00C5 Å LATIN CAPITAL LETTER A WITH RING ABOVE is Neutral, but its decomposition starts with a Narrow character. Strictly speaking, it makes no sense to talk of narrow and wide for neutral characters, but because for all practical purposes they behave like Na, they are treated as narrow characters (the same as Na) under the recommendations below. In a broad sense, wide characters include W, F, and A (when in East Asian context), and narrow characters include N, Na, H, and A (when not in East Asian context)."And I have spent as much time on this as I care too. It's both patently obvious and in the standard above that text presentation characters should be narrow. And it is in the standard in UTS51 that VS15 changes the presentation to text from emoji when the base character has emoji presentation. It is as simple as that. Good luck. I am done repeating myself. |
|
The TLDR: ED4 says Emoji_presentation characters have W class aka width 2. ED5 and ED7 say all Na and A codepoints have narrow width. ED4 is what makes emoji have W=2 when they have emoji presentation. Once an emoji is changed to text presentation, ED4 does not apply. Then ED5 and ED7 apply. Hence it has width=1 |
|
tr11 has the following to say:
none of ED4, ED5, or ED7 make those characters cease being classed as W when given text_presentation, they are still emoji_presentation characters, even when their text presentation has been requested by a following variation selector. If you want to say this is ambiguous or underspecified, then the next answer is to check the available context. In the case of a terminal with a font for rendering, this would obviously just be checking the font's width for that character in text presentation. This is something that probably should be cleared up more in the specification, but having the ability to give wcwidth the font currently in use to use for context would allow such situations to be inline with what users actually see rendered. With regard to the associated issue over on the terminal side, I don't think most apps have a good way to solve this (ask for the font and other context) with the current standards and protocols, but I'd love to be wrong about that. |
@mikeshardmind This was the reason Contour proposed the terminal-unicode-core spec, and why it has a mode which applications can query to determine whether a particular terminal is following that spec. Regardless of how different people may interpret the wording of the Unicode standard, if a terminal reports that it is following the terminal-unicode-core spec, then apps should be able to rely on that terminal rendering a sequence of glyphs with a predictable width (at least in a perfect world in which nobody has any bugs). |
|
|
Next release of This is because new function, wcstwidth() can be used to provide measurement corrections: >>> text_emoji = "\N{WHITE HEAVY CHECK MARK}\N{VARIATION SELECTOR-15}"
>>> wcwidth.wcswidth(text_emoji) # vs-15 is now narrow by default
1
>>> # get measurement with possible correction applied by process TERM_PROGRAM or TERM (ghostty)
wcwidth.wcstwidth(text_emoji)
1
>>> # get "corrected" width for contour terminal,
>>> wcwidth.wcstwidth(text_emoji, term_program='contour')
2 |
|
Rio terminal has followed ghostty, kitty, and foot's implementation regarding VS-15, making wide emoji become narrow, and now reports grapheme mode 2027 support, raphamorim/rio#1857 From https://www.jeffquast.com/post/perfecting-terminal-character-width-using-correction-tables/
I have written to Fraser Gordon of unicode.org's Text Terminal Working Group early this year referencing this issue, and again last month to more generally introduce myself and my work. I am awaiting any reply, decision, or publication on this matter. I still believe correction tables will be required for a very long time, even if all ambiguities of mode 2027 support are clarified by unicode.org, implementations will probably always vary. I intend to release a portable C11 library that includes "width of grapheme, with identifiable terminal-specific corrections applied" function, wcstwidth() in a few weeks (as wcstwidth_u32 as 32-bit codepoint array and wcstwidth_utf8 character strings). By circular dependency of ucs-detect and this wcwidth library we can publish correction tables and wcstwidth() function in any programming language. Corrections are intentionally not supplied for Terminals without common automatic identification: alacritty, TeraTerm, securecrt, suckless terminal, or windows terminal are without a custom TERM or TERM_PROGRAM environment variable and without response to XTVERSION query. Although these terminals can sometimes be reverse-identified by uncommon methods, like fingerprinting by unicode support, DA1 response strings, unique environment variable, or parent process tree/executable version identification they are excluded to reduce packaged code size due to their unlikely use. |






Uh oh!
There was an error while loading. Please reload this page.
Here's an example where it causes misalignment for me in Kitty terminal:

The code:
All reactions