Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
unxed opened this issue Aug 26, 2024 · 6 comments
Closed

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

unxed opened this issue Aug 26, 2024 · 6 comments
Labels

Comments

@unxed
Copy link

unxed commented 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()
@unxed unxed added the bug label Aug 26, 2024
@kovidgoyal
Copy link
Owner

You can use the existing escape codes to do this in a perfectly
performant and non screen clutterring manner. Start with sending
the atomic screen update escape code. Then the escape code to save the
cursor position. Then for each sequence you are interested in

  1. Move cursor to 0. 0
  2. print the sequence
  3. query cursor position
  4. repeat

finally retore saved cursor and send escape code to finish atomic
update.

@unxed
Copy link
Author

unxed commented Aug 27, 2024

Thanks for your answer! And what is atomic screen update escape code? Do you mean switching to alternate screen buffer?

@kovidgoyal
Copy link
Owner

kovidgoyal commented Aug 28, 2024 via email

@unxed
Copy link
Author

unxed commented Sep 13, 2024

@magiblot, the developer of a modern Turbo Vision implementation with Unicode support, makes a fairly compelling argument as to why the approach of outputting characters and measuring cursor offsets might not be the ideal solution. Let me quote him:

I'm afraid that solution is only feasible on Windows. In order to do that in a Unix terminal:

  • Turbo Vision uses the alternate screen buffer, which results in scrollback being disabled in most terminal emulators. You would have to print the characters you want to measure in the same screen area where your application is being drawn. For example: if you tried to print an emoji, then measure the cursor movement, then move the cursor back to its initial position, and then overwrite the emoji with the characters that used to be in that part of the screen, it is very likely that the terminal emulator would display the emoji on screen for some time.
  • Even if the characters being measured didn't appear on screen, or if that wasn't an issue, the performance of the whole process would be very poor.
  • Even if the above weren't an issue, the input stream used for reading the terminal state is the same that's used for reading user input. Therefore, you would have to either ignore user input while measuring text width, or write code that is able to keep the input events that are received while measuring text width. And then you would also have to consider the risk of waiting forever for an answer from the terminal...

So, in my opinion, you would end up with a poor experience for both the user and the programmer.

magiblot/tvision#51 (comment)

@unxed
Copy link
Author

unxed commented Sep 14, 2024

https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec

Is there any terminal other than iterm2 that supports this? It doesn't work for me in Kitty.

@kovidgoyal
Copy link
Owner

kovidgoyal commented Sep 14, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants
@unxed @kovidgoyal and others