Skip to content

Behind the Curtain of Inline Terminal Applications

Textual recently added the ability to run inline terminal apps. You can see this in action if you run the calculator example:

Inline Calculator

The application appears directly under the prompt, rather than occupying the full height of the screen—which is more typical of TUI applications. You can interact with this calculator using keys or the mouse. When you press Ctrl+C the calculator disappears and returns you to the prompt.

Here's another app that creates an inline code editor:

from textual.app import App, ComposeResult
from textual.widgets import TextArea


class InlineApp(App):
    CSS = """
    TextArea {
        height: auto;
        max-height: 50vh;
    }
    """

    def compose(self) -> ComposeResult:
        yield TextArea(language="python")


if __name__ == "__main__":
    InlineApp().run(inline=True)

This post will cover some of what goes on under the hood to make such inline apps work.

It's not going to go in to too much detail. I'm assuming most readers will be more interested in a birds-eye view rather than all the gory details.

Programming the terminal

Firstly, let's recap how you program the terminal. Broadly speaking, the terminal is a device for displaying text. You write (or print) text to the terminal which typically appears at the end of a continually growing text buffer. In addition to text you can also send escape codes, which are short sequences of characters that instruct the terminal to do things such as change the text color, scroll, or other more exotic things.

We only need a few of these escape codes to implement inline apps.

Note

I will gloss over the exact characters used for these escape codes. It's enough to know that they exist for now. If you implement any of this yourself, refer to the wikipedia article.

Rendering frames

The first step is to display the app, which is simply text (possibly with escape sequences to change color and style). The lines are terminated with a newline character ("\n"), except for the very last line (otherwise we get a blank line a the end which we don't need). Rather than a final newline, we write an escape code that moves the cursor back to it's prior position.

The cursor is where text will be written. It's the same cursor you see as you type. Normally it will be at the end of the text in the terminal, but it can be moved around terminal with escape codes. It can be made invisible (as in Textual apps), but the terminal will keep track of the cursor, even if it can not be seen.

Textual moves the cursor back to its original starting position so that subsequent frames will overwrite the previous frame.

Here's a diagram that shows how the cursor is positioned:

Note

I've drawn the cursor in red, although it isn't typically visible.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2caW7bSFx1MDAxNoD/51x1MDAxNIIzP7qBuFL7XHUwMDEyYDCwZStyXHUwMDFj2+1VTmZcdTAwMWFcckqiLMZcdTAwMTQpkZRspVx1MDAxMWAwZ+hcdTAwMWNjljPlJPOKdkRKMlx1MDAxNcWb1EmUwLGruDxcdTAwMTa/t5fz+5NSaSVcdTAwMTl23ZVcdTAwMTelXHUwMDE197Lh+F4zci5WntnxgVx1MDAxYsVeXHUwMDE4wFx1MDAxNE1/jsN+1EiPbCdJN37x/HnHic7dpOs7XHJcdTAwMTdccry47/hx0m96IWqEnede4nbiv9mvu07H/Ws37DSTXGJlN1l1m15cdTAwMTJGV/dyfbfjXHUwMDA2SVxmV/87/FxcKv2efs1JXHUwMDE3uY3EXHTOfDc9IZ3KXHUwMDA0JJjxyeHdMEilpVJcdCOVXHUwMDE5zXvxXHUwMDA23C5xmzDZXHUwMDAykd1sxlx1MDAwZa3UX56u8lajfr6zj/VvXHUwMDAz1yHRRT+7a8vz/cNk6KdSNaIwjlfbTtJoZ0fESVx1MDAxNJ67Na+ZtD8vXm58dG5cdTAwMWPCQmRnRWH/rFx1MDAxZLixXVx1MDAwMzJcdTAwMWFccrtOw0uG6TPi0ejVQrwoZSOX9k6UIcxcZlWGXHUwMDFhZrRQajRtLyAl0lpcdTAwMWGuXHUwMDE55lRzbiZcdTAwMDQrhz68XHUwMDBlXHUwMDEw7ClOP5lodadxflx1MDAwNvJcdTAwMDXN0TFJ5Fx1MDAwNHHXieClZcddXFw/slx1MDAwMEGYMEJJw5RmLJOj7Xpn7Vx1MDAwNFx1MDAwZWFcdTAwMTIjTbDhXFwyJoimMpPGTV9cclx1MDAxMVxcK1xml8jOtjJ0t5opJb9mLyRcdTAwMDK+tuwpQd/38+tcdTAwMTk0r9dzbKJuJzZzwGWX6nebzlx1MDAxNVx1MDAxOERcdTAwMTEmKeZSgJSjed9cdTAwMGLOJy/nh43zjKV09MOzWzCsjChCmGCl4aVcbknmhtjpVVx1MDAwNqe7XHUwMDA3bqvSOKqel/1cdTAwMWHFXHUwMDFi61x1MDAwNVx1MDAxME+AuDh8OdJcdTAwMDLAoFx1MDAxNFOjXGah4/hqhGFcdTAwMWSoxFx1MDAwMpBRWj9cdTAwMWO+xCBDODVcXGqBQZBpfKlGQlx0hTU2RmEpmZ7GXHUwMDE3c0OtXHUwMDFhfjP4ur7vdeNcdTAwMWLhhftcdTAwMTTBK7li5Gvs78nxK+dQXGYvejtcdTAwMDde69xcdTAwMDSV+JeXRfZ3Jrrk8dBlXHUwMDFjXHSARVx1MDAwMppEXHUwMDEzg8ctr5JAXHUwMDBio5JcdTAwMDHZXHUwMDE4kOJ3Qfdpy1x1MDAxMVTQaWxcdENcdTAwMWN0XHUwMDA3nFx1MDAxZOhcdTAwMGblWvNpblx0XHUwMDA1OSU3YHIxZYxyNcktXHUwMDAwXHUwMDBilvtbsrozsFU5XHUwMDFiM4Gt4kRcdTAwMWH4Oze2g+5cdTAwMWXpuoPN9ZbgZ69cdTAwMTmpXVbLtSXHVlx1MDAxMsSNXHUwMDExlFBcclx1MDAxNlx1MDAxNYtxalx1MDAwNThxooQ2XHUwMDE4XFwx0fKO1NYxXHUwMDE2XHUwMDBmRS1cdTAwMDSANtigmHxcdTAwMTfYSlWELVx1MDAwM3NLhNBsbm533uDt/dPN3v4gpju7XXZcdTAwMTJcdTAwMGZcdTAwMWG/LTm32iCwgYJDLKshvuTj3EJcdTAwMTiBOVx1MDAwNJZcdTAwMWNsMeNE3IlbQutcdTAwMTA1P1x1MDAxOLdKQKzOXHQ131xmt4l7mdxcdTAwMThcIlx1MDAxNEZcYlxcW88oclB/idmzzsn62/CiXHUwMDE19Fx1MDAwZvc3TpOeXGLeXHUwMDBivdzRLVx1MDAxN1x1MDAxNCmIXHIwY0bAs04kZ1xuI8CDXHUwMDE5XGJcdTAwMWQ0VzznfiehJa79c/voXHUwMDE2zDpEKIaBJJgqxm/glopJTjlIXHUwMDA1XGbrXHUwMDA1Yio1hORUf1x1MDAwNaaZVGGQXHUwMDFjeu/Talx1MDAwMFx1MDAxZVx1MDAxYq04XHUwMDFkz1x1MDAxZo691JTflOOo41x1MDAwNY6/Mjaz5ntnluZcdTAwMTXfbY1jnnhccsdcdTAwMWZNJ2E3m23AnVx1MDAxYy9wo+mVXHQj78ze5ajwrvCcbnVkUVDu3dSd2LWzdlxc30oniTCTo6OckzCqXHUwMDE1pGPzR0CXeu2UvD5cdTAwMWGcvCtv1JPGsLKvPLLcWkmZLYpcdTAwMTBuuMBgwlxyXHUwMDFlzzmpxogxQVx1MDAxNbf+RIviksldtZJcdTAwMTKMXGZT2cvNlDGXLVxcKSPF0qQ2YoGp5ZU2mjyl96iNWfTyWVx1MDAxYv9S6lx1MDAwZZN2XHUwMDE4lLzAXCKPusPH1ctZ95/U0Fx1MDAxYlx1MDAxNdTcUkHJ5OhIQVx1MDAwNbYhXHUwMDA1o/O7TYdcZumx13ldebllZCu52KItvrPkXG5KJVx1MDAwMlx1MDAxZsVcdTAwMDSXRFKFc4FtXG5cbjZIQZpcdTAwMDYpNzdE6clcdTAwMTg0U1DaMi7ndylcblx0JOlccvpJclx1MDAxMF8rKCNcdTAwMTCFa/pcdTAwMWSp56eP//70xz9cdTAwMTf59+N//lx1MDAxMXz6418lr9NccqOklLS9uDTzXHUwMDAzXHUwMDA3X53RXG6jktXrXHUwMDEyvP4z9yeCf37xhTPsp1x1MDAxYnlB8lPw81x1MDAxY/f4+L9Fr81/XHUwMDFm11j+oOHPQsNcXK6LiJm+a3ZnjnA5OTzyYJA9U1tcdTAwMWKe24GpzfiClVtcdTAwMGX1anHjpFLeXHUwMDFjen7052jNXHTEId9cdTAwMTKUgF9cdTAwMThrV1x1MDAwZdP0ykBgr4WEIFx1MDAxM1x1MDAxNlx1MDAwNcs71SxcdTAwMWWhN6chMIHogyy8uUHyKD9Yby5f0J/qzVx0SNgxJ/NT3H7/+vByUD1+e+iL9XpMnMBcdTAwMWK2lz1cZlOIci6UwMZcdTAwMDae48VcdTAwMGJu0yRcdTAwMDW8QDjKJeeYPVx1MDAxY7730puDlFx1MDAwZkIkyG+/XHUwMDE5fGdVi1x1MDAxNS6C1+a98FLN/H3lkzZda1x1MDAxZZQjUj1uXHUwMDFlXHUwMDFjNdpq66R6seTVYsjxXHJYPKZcdTAwMDVcdTAwMDXDy+SE7dVcdTAwMWFcdTAwMDG6WsExmuW7zsvXnKPEdpRccv4+wNW0cFOPzfkgIzTztzmq+28q4dbh+erQK7d5r856tfLakoMrXHUwMDE50jaxXHUwMDA150JcdTAwMTlcdTAwMTZ40uoqXHUwMDA040yJtHt3p/1cdTAwMTBcdTAwMGbcnlx1MDAwM72TWkqyyIrVI3Kriquq1LZ7zNdY3Jc7nlx1MDAxYuNcdTAwMWVf9VuDau3dpnpVq95qJ88jgmtcYjhpKYhSVLHc1pDP2Fx1MDAxMiWxpFx1MDAxMGOSO8Zcblx1MDAwZtye00pcdI5cdTAwMDVZeFf53rAtLDTqwiCBMNugs3ZobmYvutH+5j7ZXHUwMDEwu1x1MDAxYvuy2Ze7ZH/TXe5cYpdcdTAwMGKBIOfBXG645Fx1MDAxOPLSySiBXCJbZSRcdTAwMTAvSVx1MDAwNS+keFx1MDAwZs9CXHUwMDFhdFx1MDAwNljHkK0tvEH3UDXH77NBR1Vx/Z/BmlOt8fyJ53FP7jjbtdO1zdW9gyZdq78/7peXWy0h3kVUUS5cdTAwMTUziimWlVwi0j2hhlwiSPFsRFxm5Fx1MDAxOVpcXP5/zP6cLVx0gFx1MDAwMqtcdTAwMWb9ueyYb7Q/XHUwMDA3IXqhfnJqgcFfUVx1MDAxOPLXvM6GqO329ereK1V/XelttypLrp9UI1xi8Vx1MDAxOIUn5ZBCT2zFolx1MDAxNFx1MDAxMckoXHUwMDEznCqueXGs96jtOaGxXmhQl2on+5otV3fTzlx1MDAxZlxymVx1MDAxOWcsVUPmR3vuXHUwMDA3XHKP2Z6jhWmfpmCoXHUwMDE4IfOX2EhvfbuqlcvLb99eXjr97kbz6HLJ3Vx1MDAxN+OIQV5cdTAwMDc+Styw/1x1MDAwYuJNJLFcdTAwMDT/JpWcVVx1MDAxYXYxI4zMdF9PW62GaZhcdTAwMWJKXHUwMDE1XHUwMDE0XHQsXHUwMDA1xpRzwvGNXowypLUxIIfdlkem8z/wK5xA1irvIVx1MDAwMfyMUFx1MDAwNlx1MDAxMbtcdTAwMWX5UOzsRudkZ4/AONi+PN9cdTAwMWVe+q3tpNojvai25Veyus5cdTAwMTitTlx1MDAxNIVcdTAwMTcro5lcdTAwMGbX381ypZLl+6VcdTAwMGbZXHUwMDA2pKSwIM1cdTAwMTTmkI3kXHUwMDA0+ZK2bPT97aNcck81XHUwMDBl+/XtNVapbe3t9e5VW5phYm9/j+pcdTAwMDKJXHUwMDE4YkZAslx1MDAwNeGTytec7flcdTAwMWFzZCRcdTAwMTd2+5PUbEa0t3h1XHUwMDExttZjf1liQeryaFx1MDAwNb8rhbpcdTAwMDFmZoo3Zlx1MDAxMG53vOr5XHUwMDBifrM1fFx1MDAwMaZffVx1MDAxMWWGXHUwMDExNlxubD9k6oTh8X2Fmk7sx79LYaFcdTAwMTBlrjRcdTAwMDKGpaKQjlxiIFlOo6xcdTAwMDRcYmJ/aVBcdTAwMThObJljXG5larRS1NxH6W9cdTAwMWFlelx1MDAxN5RT26xulebEiVx1MDAxMyXrXtD0grPJU9ygmc3kRL7+T1x1MDAwNLbmXGJG0pSp0bfyr2LEIVFnRHNJMDNgMXjusDOna99cdTAwMTSCjI1rMFx1MDAxOFQoa1CmlsV34qRcdTAwMWN2Op61ub+EXHUwMDEwb06KnT7SmlXHtutMvVx1MDAwNnio/Nyk3nbtXHUwMDE1x1x1MDAxZG32XSljO/1h9P2vz248epVyg6RUXHUwMDAwN4RccpCW6/zpq1xuKWNgkCtpofzi1YoxvrrcJMHZXHUwMDA1n+T/vZ1LxoVcdTAwMDVcdTAwMTj7aNpQOn9cdTAwMDB7vMferVV3XHUwMDFijvP2/M3uprOxeeRcdTAwMWQtd1x1MDAwMKvA1UlcYl+lxvC4dLw8XG4mXHUwMDFlaSo45pxSlf8ls6VzyFx1MDAwNixcdTAwMTiwI+6hZHrP/lhqXHUwMDEwLVx1MDAxNyfM64+fXFwrzIrT7Vx1MDAxZSZwyZFo8Ghe87qGk11mZeC5XHUwMDE36zeuu/3YmDiV32Lops/54cmH/1x1MDAwM7lI91xmIn0= terminal$ python inline.py╭──────────────────────────────────────────╮│ import this ││ for n in range(10): ││ print(n) │╰──────────────────────────────────────────╯terminal$ python inline.py╭──────────────────────────────────────────╮│ import this ││ for n in range(10): ││ print(n) │╰──────────────────────────────────────────╯

There is an additional consideration that comes in to play when the output has less lines than the previous frame. If we were to write a shorter frame, it wouldn't fully overwrite the previous frame. We would be left with a few lines of a previous frame that wouldn't update.

The solution to this problem is to write an escape code that clears lines from the cursor downwards before we write a smaller frame. You can see this in action in the above video. The inline app can grow or shrink in size, and still be anchored to the bottom of the terminal.

Cursor input

The cursor tells the terminal where any text will be written by the app, but it also assumes this will be where the user enters text. If you enter CJK (Chinese Japanese Korean) text in to the terminal, you will typically see a floating control that points where new text will be written. If you are on a Mac, the emoji entry dialog (Ctrl+Cmd+Space) will also point at the current cursor position. To make this work in a sane way, we need to move the terminal's cursor to where any new text will appear.

The following diagram shows the cursor moving to the point where new text is displayed.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2a627bRlx1MDAxNoD/5ylcdTAwMDRlf7RAzMzMmWuAxcJ27Nhx41xc3NSut0VAkyOJNUWyJGVbKVxmXHUwMDE0fYbmMbq7z5Qn2TO0I0qUZKuOb73Qhi3O9fDMd+acM9RPXHUwMDBmWq12Ocxs+0mrbU9cdTAwMDI/jsLcP24/cuVHNi+iNMEqVt1cdTAwMTfpIFx1MDAwZqqWvbLMiiePXHUwMDFm9/380JZZ7Fx1MDAwN9Y7ioqBXHUwMDFmXHUwMDE35SCMUi9I+4+j0vaLf7m/237f/jNL+2GZe/UkSzaMyjQ/m8vGtm+TssDR/433rdZP1d8x6XJcdTAwMWKUftKNbdWhqqpcdTAwMDWkjJlm8XaaVNJSobk2Wkkxalx1MDAxMVx1MDAxNU9xwtKGWN1BoW1d44rar9+Q7GiLraytL29mS/L5yTc/RLv1vJ0ojnfKYVxcyVx1MDAxNeRpUSz1/DLo1S2KMk9cdTAwMGbtblx1MDAxNJa9T+pcdTAwMWIrXHUwMDFm9S1SVEXdK09cdTAwMDfdXmJcdTAwMGKnXHUwMDA1OipNMz+IyqErI2RUeqaKJ6265Fx1MDAwNO+48oBcdTAwMTFGqVx1MDAwMEo0UC5H1WdcdTAwMDNIT3AlJZdMXHRcdTAwMDGsKdlqXHUwMDFh44qgZFx1MDAwZkl11bJcdTAwMWT4wWFcdTAwMTdcdTAwMDVMwlGbMveTXCLzc1xct7rd8fkzXHUwMDBiXHUwMDA2XHUwMDFlXGIjlDSgNIBcdTAwMWG16Nmo2yuxXHRI4mlKXGbnXHUwMDEyQFDNamFcdTAwMGJbrY0mRoBSXHUwMDEyRlx1MDAxNU6EbDOsOPm+XpBcdTAwMWNcdNt0PZJBXHUwMDFjj+szXHTP9TlRceAq1saQq4dcdTAwMWFkoX9cdTAwMDZcdTAwMDZVXHUwMDE0JJOaaSlquOIoOWxcdTAwMGVcdTAwMTenwWHNUlV6+uhcblx1MDAxNFx1MDAxYkPmQqy0MIpcdTAwMThtXHUwMDE2hvjHr7rrq2+Wj9f2uqTc3szWNlx1MDAwM39jXHUwMDBlxFxyXHUwMDEw71xmX+NR4Fx1MDAwMjjnhCjJTYNe5UmpXHUwMDA1Z6CMJOPV104vNZ6hnFx1MDAxOY7zXHUwMDExZeg0vUx7Qlx0RZBRXFxcdTAwMTgpQTfpXHUwMDE1XHUwMDFjgKEhij9ccr02jqOsmMmuXHUwMDE2MI9dzXEtJfDF0e2+j168XHUwMDEw2TqYcqhXzP7zb/rv315cdTAwMDVdemvoXG7taW64UVpqQyVcdTAwMTI8yS6VXHUwMDFlXHUwMDAzMMo4KnBTa0r2u9h92PFcdTAwMDVcdTAwMTNsmltcblx1MDAxZSeMXHQj0Vx1MDAwYjCuNZ9cdTAwMDaXMk+gXHUwMDA3MLjlXHUwMDEyXHUwMDE0iXHVXHUwMDA01/UlnCui/lxu5Jqxx2ySXHUwMDBiwJkyv4Pc7bWXXHUwMDFi5bF+2SGr79ZcdTAwMGZiXHUwMDE28/f7nftNrlx1MDAxNp7SXHUwMDA0d13BiFZKq1x1MDAwNrjCI1x1MDAxNCFcdTAwMTGMaSXImJlfXHLcXHUwMDAzQsRNgUu5llxcMKn+XHUwMDEyWy7qalx1MDAxZbhC4V4jcVFcdTAwMTdcdTAwMDZ36+nhXHUwMDBifTLs7m1nvb2NV1svI1x1MDAwMUf3XHUwMDFiXFyK0GhcZlx1MDAwM1x1MDAxNG5Uwlx1MDAwMLrpXHUwMDA2udzjUtAq1EVi9GdcdTAwMDW7XHUwMDBmKTvQWt5cdTAwMTi5Qlx1MDAxYuCamj9cdTAwMGa5pT0pZ2HL5NxIgUpCccVAwcLc/rD79sSHr0Gvvn6z+3SfLNFnavN+R7lMSc9grFx1MDAwZkJcdTAwMDKT+LyNUIFg7Ek0brycUMVByrncUut+rlx1MDAxZeYqgVx0mDGYK1x1MDAwMmFcbvhcZnSZmEJVXHUwMDAyZcDUWFxi81x1MDAwN0C1lipNyp3ova3inInSdb9cdTAwMWbFw4llrVx1MDAxOK5YzvtR4sftiZrlOOo6otux7UyiXkaBXHUwMDFmj6rLNKtrXHUwMDAznMmPXHUwMDEym09rJs2jrpvl67mz4nPajdGu4o0tzoFfWFfryvWV7Fx1MDAxMoRolo7sXHUwMDEyWUVcdTAwMWUx3VnYLjl59tan20d+9JzRaOdN9mpJsfttl5J66Cw4hlx1MDAwZlxcYOJcdTAwMDeT7lx1MDAwNFxcSmhcdTAwMDTarZIuir85q2SUeFx1MDAwNsZimNpcdTAwMThrXHUwMDAz/WSMXG4loZzDXHUwMDFk+1xyReg4pNdojLUr+GSM/2hlw7KXJq0occR72fB2zfKi+ZtcdTAwMDY60z7N1eyTz81ThDuA0Fx1MDAxNOo1uMw8i63lYfx8XHUwMDA11nskXGJ3XHUwMDBls414S2T32zxcdTAwMDXxJCa9mF9cdTAwMGJcdTAwMDBkrlx1MDAxZaXChFx1MDAxYlx1MDAwZkP/KrdmnEvdkKs2T9YxlvPPOVx1MDAxYlx1MDAxMk6QWVFeLfDIPCWG4VxmQ9C7Nk82juiNmufHXHUwMDBmv3389ee7/P3wn++Sj7/+0or6WZqXrbJcdTAwMTdcdTAwMTWtXHUwMDBiL2x81qOT5i1n1y1cdTAwMDSga7+g5Msnl/RwV5ZHSflF8uVcdTAwMDJzfPjfXevmv7e7Wf5Nw1x1MDAxZoWGhVxcXHUwMDE3XHUwMDE1XHUwMDE3+q5cdTAwMGJfb1x1MDAwMJ3rwDDrk1x1MDAxMjd1tXiA2Vx1MDAwYsPO9ru9XHUwMDFmXHUwMDA3sYSlwVx0W9/v7r+731x1MDAxZYxhXGJcdIxi2qdcdTAwMThRXHUwMDA006hcdFx1MDAxN8ZcdTAwMTnxmCGcXGKXi4HiXHLBalx1MDAxN2axL9BLTto6gVx0zIzzXG7mXHRcIlx1MDAwNSHoJSknMz1cdTAwMTlcdTAwMDNPa1xmdkFSYJrOyFx1MDAwMYlcdTAwMDDsj20+3699gqjGXGLOS07nu7tRn7r3iIxju9594dM1sqo3l5dcdTAwMDa7+1n323ykiFx0Xv08T4/bo5rT80/343Ugp3PfaVx1MDAwM/IhheKLXHUwMDA3fGHk2+Dbbnm4WiztvVx1MDAxYa7oZ6+D19dqLmFauumvM+IznsRcXEtzQrTShky+zMbVQGvSXFxcdTAwMTJpwL1qvsfmXCJcdTAwMTU+giHmXHUwMDFhjkyuZC63XHUwMDA29JlBzYDZSD5cdTAwMGZmyo3WinJYfPO/2MTvYPNXl1x1MDAxZS5cYkxP8Fx1MDAxMSWbtfWDwuyFUFx1MDAwNVx1MDAwMIg7tvqcw4X5LFx1MDAwYuqhsinRWktFZ1x1MDAxZVVjloXJXHUwMDAyo0BcYroppae+k0E1+ih5Uyyz22V5TM1+Xq5ESVx1MDAxOCXdZlx1MDAxN5uEdc2YyOffWdpcXCBcdTAwMWOpsqZg4OQnuFx1MDAwNFxmd1x01LJcdTAwMTRcdTAwMTRcdTAwMTVdXHUwMDFmqjlV+pnTgoerQ1D51X5CzbSFx35Rrqb9fuT23FcpRpxNqasnWnbm2LP+1CrgM43XNe02cyNOOtr6U6tGu7pcdTAwMTl9/v7RzNZKeSCldF9cdTAwMDKQXHUwMDA0o5/xzlxcee49s+S0qmLissHmM+yuKXrr4Vx1MDAxZYz/d46+mqDtZ9lOiVx1MDAxNI2WXHUwMDE2aY7C86S3Vln7KLLHKzNtzV0uhKhcdTAwMTbHbT22Qvv0wen/XHUwMDAxc82RiyJ9 terminal$ python inline.py╭──────────────────────────────────────────╮│ import this ││ for n in range(10): ││ print(n) │╰──────────────────────────────────────────╯

This only really impacts text entry (such as the Input and TextArea widgets).

Mouse control

Inline apps in Textual support mouse input, which works the same as fullscreen apps.

To use the mouse in the terminal you send an escape code which tells the terminal to write encoded mouse coordinates to standard input. The mouse coordinates can then be parsed in much the same was as reading keys.

In inline mode this works in a similar way, with an added complication that the mouse origin is at the top left of the terminal. In other words if you move the mouse to the top left of the terminal you get coordinate (0, 0), but the app expects (0, 0) to be where it was displayed.

In order for the app to know where the mouse is relative to it's origin, we need to ask the terminal where the cursor is. We do this with an escape code, which tells the terminal to write the current cursor coordinate to standard input. We can then subtract that coordinate from the physical mouse coordinates, so we can send the app mouse events relative to its on-screen origin.

tl;dr

Escapes codes.

Found this interesting?

If you are interested in Textual, join our Discord server.

Or follow me for more terminal shenanigans.