prompt-toolkit / prompt-toolkit/python-prompt-toolkit
Excessive use of FormattedTextControl text updating and app.invalidate causes IndexErrors
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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