prompt-toolkit / prompt-toolkit/python-prompt-toolkit

Excessive use of FormattedTextControl text updating and app.invalidate causes IndexErrors

Open
#1,570 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
10.6k
Forks
815
PR merge metrics
No merged PRs in 30d

Description

In the process of trying to use a FormattedTextControl to dynamically draw text and then use the cursor to scroll around and set what part of the Window is visible, I found that I would often get an IndexError exception around fragments during a render. Basically, I want to keep the cursor at the bottom of a FormattedTextControl, so I implemented get_cursor_position give the position as the bottom of the text field by counting the number of newlines in the control's text. However, if text is updated mid-render to be longer than what's currently being rendered, get_cursor_position will return a position longer than what's being rendered, crashing the application.

I created the following minimal example that reliably recreates and solves the issue. The only reliable solution I've found is to use a lock to block updates to containers/controls during a render by implementing before_render and after_render.

I don't know if this can be considered a bug or not as it's a rather abusive use of invalidate(), but if anyone else runs into this issue then this is a reliable workaround.

from prompt_toolkit import Application
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.widgets import Frame
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.data_structures import Point

import time
import threading

t = FormattedTextControl()
w = Window(t, height=8)

def cursor_pos():
    return Point(0, t.text.count('\n'))

t.get_cursor_position = cursor_pos

root_container = HSplit([
    Frame(w)
])

layout = Layout(root_container)

kb = KeyBindings()

@kb.add('c-c')
def exit_(event):
    event.app.exit()

render_lock = threading.Lock()

def prerender(app):
    render_lock.acquire()

def postrender(app):
    render_lock.release()

app = Application(layout=layout, full_screen=True, key_bindings=kb, before_render=prerender, after_render=postrender)

def loop():
    i = 0
    while(1):
        render_lock.acquire()
        t.text += str(i) + '\n'
        i += 1
        render_lock.release()
        app.invalidate()

        time.sleep(0.001)

th = threading.Thread(target=loop, daemon=True)
th.start()

app.run()

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minimal example using FormattedTextControl, Window, Application, and concurrent text updates through invalidate(). Trace the render path around get_cursor_position and cursor-related fragment handling. Done means the example no longer raises IndexError during concurrent updates, with the behavior covered by an appropriate regression test.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.