proposed enhancement: ESC sequence to get actual string length #22168

Closed
opened 2026-01-31 08:05:21 +00:00 by claunia · 3 comments
Owner

Originally created by @unxed on GitHub (Aug 26, 2024).

One of the major challenges in building console interfaces is the unpredictability of the actual length of a string (in screen cells) when using Unicode characters. The width of the same characters can be displayed differently across various terminals.

Of course, it is possible to determine this width by displaying the characters and checking how much the cursor has moved (sample in python is below), but this approach is slow and clutters the console log.

It would be great if you could propose an additional terminal extension, such as an ESC sequence, that allows querying the terminal to determine how many screen cells a given string of Unicode characters will occupy.

Thank you!

import os
import sys
import tty
import termios

def get_cursor_position():
    """Gets current cursor position in terminal"""
    sys.stdout.write('\033[6n')
    sys.stdout.flush()

    buf = ""
    while True:
        ch = sys.stdin.read(1)
        buf += ch
        if ch == "R":
            break

    # Answer sample: '\033[12;40R'
    # Extracts position from string
    try:
        rows, cols = map(int, buf.lstrip('\033[').rstrip('R').split(';'))
    except ValueError:
        rows, cols = -1, -1  # In case of an error

    return rows, cols

def hide_cursor():
    """Hides cursor"""
    sys.stdout.write('\033[?25l')
    sys.stdout.flush()

def show_cursor():
    """Shows cursor"""
    sys.stdout.write('\033[?25h')
    sys.stdout.flush()

def print_teststring():
    """Measures screen cells number"""
    teststring = 'a1🙂❤️'
    
    # Save terminal settings
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    
    try:
        # Switching terminal to non blocking mode
        tty.setcbreak(sys.stdin.fileno())

        # Saving cursor position
        sys.stdout.write('\033[s')

        # Hiding cursor
        hide_cursor()

        # Moving cursor outside the visible area
        sys.stdout.write('\033[1000A')

        # Getting position before string output
        _, start_col = get_cursor_position()

        # Printing string
        sys.stdout.write(teststring)
        sys.stdout.flush()

        # Getting position after string output
        _, end_col = get_cursor_position()

        # Moving cursor back to saved position
        sys.stdout.write('\033[u')

        # Showing cursor
        show_cursor()

        # Counting screen cells used
        cells_used = end_col - start_col

        # Printing result
        print("String: \"", teststring, f"\", screen cells count: {cells_used}")
    
    finally:
        # Restoring terminal settings
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        # Showing cursor in case of an error
        show_cursor()

if __name__ == "__main__":
    print_teststring()
Originally created by @unxed on GitHub (Aug 26, 2024). One of the major challenges in building console interfaces is the unpredictability of the actual length of a string (in screen cells) when using Unicode characters. The width of the same characters can be displayed differently across various terminals. Of course, it is possible to determine this width by displaying the characters and checking how much the cursor has moved (sample in python is below), but this approach is slow and clutters the console log. It would be great if you could propose an additional terminal extension, such as an ESC sequence, that allows querying the terminal to determine how many screen cells a given string of Unicode characters will occupy. Thank you! ``` import os import sys import tty import termios def get_cursor_position(): """Gets current cursor position in terminal""" sys.stdout.write('\033[6n') sys.stdout.flush() buf = "" while True: ch = sys.stdin.read(1) buf += ch if ch == "R": break # Answer sample: '\033[12;40R' # Extracts position from string try: rows, cols = map(int, buf.lstrip('\033[').rstrip('R').split(';')) except ValueError: rows, cols = -1, -1 # In case of an error return rows, cols def hide_cursor(): """Hides cursor""" sys.stdout.write('\033[?25l') sys.stdout.flush() def show_cursor(): """Shows cursor""" sys.stdout.write('\033[?25h') sys.stdout.flush() def print_teststring(): """Measures screen cells number""" teststring = 'a1🙂❤️' # Save terminal settings fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: # Switching terminal to non blocking mode tty.setcbreak(sys.stdin.fileno()) # Saving cursor position sys.stdout.write('\033[s') # Hiding cursor hide_cursor() # Moving cursor outside the visible area sys.stdout.write('\033[1000A') # Getting position before string output _, start_col = get_cursor_position() # Printing string sys.stdout.write(teststring) sys.stdout.flush() # Getting position after string output _, end_col = get_cursor_position() # Moving cursor back to saved position sys.stdout.write('\033[u') # Showing cursor show_cursor() # Counting screen cells used cells_used = end_col - start_col # Printing result print("String: \"", teststring, f"\", screen cells count: {cells_used}") finally: # Restoring terminal settings termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) # Showing cursor in case of an error show_cursor() if __name__ == "__main__": print_teststring() ```
claunia added the Issue-FeatureResolution-Duplicate labels 2026-01-31 08:05:22 +00:00
Author
Owner

@j4james commented on GitHub (Aug 27, 2024):

Of course, it is possible to determine this width by displaying the characters and checking how much the cursor has moved (sample in python is below), but this approach is slow and clutters the console log.

Honestly this is what I would recommend. You can avoid it cluttering the log by running the tests on a background page, or on terminals that don't support pages you could fallback to using hidden attributes, and also erase the text immediately afterwards.

I can't imagine a custom length testing sequence is going to be much faster than this, and you're still going to have to fallback to the cursor movement tests on the 99% of terminals that likely won't support it. It just doesn't seem worth the effort in my opinion.

@j4james commented on GitHub (Aug 27, 2024): > Of course, it is possible to determine this width by displaying the characters and checking how much the cursor has moved (sample in python is below), but this approach is slow and clutters the console log. Honestly this is what I would recommend. You can avoid it cluttering the log by running the tests on a background page, or on terminals that don't support pages you could fallback to using hidden attributes, and also erase the text immediately afterwards. I can't imagine a custom length testing sequence is going to be much faster than this, and you're still going to have to fallback to the cursor movement tests on the 99% of terminals that likely won't support it. It just doesn't seem worth the effort in my opinion.
Author
Owner

@zadjii-msft commented on GitHub (Aug 28, 2024):

You know what, we do have a pretty related thread so I'll just move discussion over there: /dup #218

Thanks!

@zadjii-msft commented on GitHub (Aug 28, 2024): You know what, we do have a pretty related thread so I'll just move discussion over there: /dup #218 Thanks!
Author
Owner

@microsoft-github-policy-service[bot] commented on GitHub (Aug 28, 2024):

Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report!

@microsoft-github-policy-service[bot] commented on GitHub (Aug 28, 2024): Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report! <!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#22168