Making MS Paint work in the Terminal
Did you know that you can use MS Paint within your terminal? ๐คฏ textual-paint is a program that emulates MS Paint within your terminal. To try it out, install it via pip (a package manager for Python) using pip install textual-paint. Then, run the command textual-paint within your terminal. Check out a demo below!

In this blog post, weโll dive into some of the engineering challenges we faced when enabling textual-paint to function correctly within Warp.
If youโre not yet familiar with Warp, itโs a reimagination of the command line terminal with some unique features like AI assistance, team collaboration, and modern IDE-style input. Due to the inherent nature of how the terminal interacts with the shell, via the PTY (pseudo-terminal), supporting certain user interactions can be quite interesting to implement. Read on to learn more!
The Problem
When we tried playing with textual-paint in Warp, we noticed that its hover effects donโt function correctlyโsomething that worked well in other terminals (GIFs below). After a bit of exploration, we identified the crux of the matter: Warp wasnโt conveying mouse-hover events to the PTY when in the alt-screen, a broader issue. Below, weโll dive into the interplay between the terminal and PTY, and offer a primer on the mechanics of ANSI escape codes. If youโre not familiar with these technical terms - donโt worry, weโll elaborate on them below!


Understanding the PTY & ANSI Escape Codes
Inside every terminal, the vital conduit for all interactions with the shell is the PTY, or pseudo-TTY. In essence, the PTY is the contemporary counterpart to the hardware terminals of the past, where physical wires linked devices. In the 1970โs, a "terminal" consisted of an input device, like a keyboard, and a display device, such as a monitor or even a sheet of paper. This device, known as the TTY (short for teletypewriter), was responsible for handling communications with the mainframe computers via serial connections.
Fast forward to today, where everything unfolds within a single machine, and we find pairs of file descriptors forming the PTY. These file descriptors effectively serve as the two ends of the metaphorical wire, transmitting user-entered input into programs and relaying program output back to the user. If you're intrigued and eager to delve deeper into the workings of the PTY, I encourage you to check out our blog on the subject!


The PTY can only transmit bytes. Therefore, every instruction or event must manifest as a sequence of characters. To standardize this, terminal protocols have delineated "special sequences", commonly referred to as ANSI escape sequences. Terminal emulators harness them in their input/output, deciphering them as commands, not merely text. For instance, \u001b[31m is used to render red text in your terminal output. To print red text to the terminal, you could use the following Python code:
>>> print("\u001b[31mHello, world!\u001b[0m")
Hello, world!In this particular case, ANSI escape codes are also used for reporting mouse events to the PTY.
Constructing the right escape code for hover events
In our case, we were interested in mouse events within the alt-screen, which is the alternative buffer that takes over the entire terminal window, used for programs such as Vim or textual-paint. Applications can activate "mouse reporting" by dispatching particular ANSI escape codes to the terminal, instructing it to monitor and relay mouse events. In response, the terminal communicates mouse activity to the underlying program, through the PTY, by sending corresponding ANSI escape codes. Within Warp, we already supported reporting mouse events for click events, drag events, etc, but, we didnโt support hover events i.e. mouse motion without clicking. Hence, we needed to identify the appropriate ANSI escape code for mouse motion.
We were using โ0โ and โ2โ as escape codes for left/right buttons respectively, but where could I go to find out, authoritatively, what the correct escape code for mouse motion is? Well, I dug through the xterm docs (the standard terminal emulator for the X Window System), various GitHub threads and articles on the internet which were rabbit holes of information, though it was hard to pinpoint exactly what I needed (due to the modifiers involved). We can see other terminal emulators, such as Alacritty, also having issues with following the correct specification - see threads/commits here and here.
Ultimately, I decided to bypass the manual search approach and executed a local terminal emulator, Alacritty in this instance, that was already proficient in reporting such events. This hands-on approach led me to "35" โ the quintessential escape code Iโd been seeking.
If youโre interested in the technical details: this โ35โ corresponds to xterm extension DECSM 1003, which encodes mouse hover events as 32 (base value) plus 3 (button released), giving us 35 (see Vim's libvterm seqs for a full list).
Here's a glimpse into how we craft our escape sequences:
pub mod EscCodes {
// Mouse-related escape codes
pub const MOUSE_LEFT: u8 = 0;
pub const MOUSE_RIGHT: u8 = 2;
pub const MOUSE_DRAG: u8 = 32;
pub const MOUSE_MOVE: u8 = 35;
}
...
to_escape_sequence(&self) -> Option<Vec<u8>> {
let action = match self.action() {
MouseAction::Released => 'm',
MouseAction::Pressed => 'M',
};
let (button, repeats) = match self.button() {
MouseButton::Left => (EscCodes::MOUSE_LEFT, 1),
MouseButton::Right => (EscCodes::MOUSE_RIGHT, 1),
MouseButton::Move => (EscCodes::MOUSE_MOVE, 1),
...
};
let point = self.maybe_point()?;
let msg = format!(
"{}<{};{};{}{}",
C1::to_utf8(C1::CSI),
button,
point.col + 1,
point.row + 1,
action
).repeat(repeats);
return Some(msg.into_bytes());
}Let's unravel the essence of the escape sequence directed to the PTY. Consider the sequence \u{1b}[<35;23;23M which represents mouse motion, the event we were adding. This sequence is an amalgamation of several components:
- Control Sequence Introducer: This is a universal precursor for all control sequences i.e. the C0 control code for โESC โ which is
\u{1b}[. - Button's Escape Code: It signifies which mouse button instigated the action e.g. 35 representing mouse motion (corresponds to the underlying xterm extensions).
- Alt-Screen Coordinate: This denotes the exact location of interaction within the alt-screen e.g (23, 23) from the above sequence. Note that these coordinates correspond to the one-indexed grid coordinates within the alt-screen (rows and columns). See xterm docs for more details.
- Nature of the Action: For mouse events, this refers to whether the button was pressed or released e.g.
Mrepresenting pressed above (for historical reasons, hover-events are tracked as โpressesโ with generic mouse motion escape codes).
For actions that encompass movement across a span, like scrolling, this control sequence gets reiterated for each corresponding line of motion. Note that these mouse events should only be reported when weโre in the correct ANSI terminal mode i.e. MOUSE_MOTION (defined by bit sequence flags).
A slightly related brain teaser
During our journey to iron out the hover functionality, we stumbled upon another intriguing challenge. Warp was inadvertently dispatching surplus mouse events, leading to anomalies in hover and drag behaviors. To be precise, we were generating synthetic mouse motion events. Why? To sustain consistent product behavior in particular scenarios.
Imagine this: A user decides to close a tab. The immediate tab to its right gracefully slides into place. Now, while the mouse isnโt physically โmovingโ over this new tab (we simply had a click event and the cursor is stationary), we'd still want it to exude the "hovered" aura. Hence, our need for such synthetic mouse events. However, this meant that we had to differentiate these synthetic events from the genuine mouse movements initiated by the user to ensure we didn't misreport events on the alt-screen. Notably, weโll eventually move to a world where we have the concept of โlayout changesโ to avoid such synthetic events.

Culmination: Bringing MS Paint to Warp
Post the meticulous implementation of mouse-motion reporting, we unlocked a delightful achievement. Fancy sketching on MS Paint via Warp ๐? We made it possible! Check out our demo with our brand-new hover effects:
Related articles

Jun 29, 2026 ยท 5 min
How to build a cloud software factory - add spec-driven development skills
This is my second post in a series on how to build out a fully automated cloud software factory. In this post, Iโll show how you can add spec-driven development for issues that are too complex or ambiguous to one-shot. The goal of these posts is to build a fully working...

Jun 25, 2026 ยท 7 min
How to build a cloud software factory - the automatic triage skill
This post is the first in a series Iโm doing on how to set up your own cloud software factory using skills and loops. Itโs easier than it sounds to get something simple and effective running so you can start to automate significant parts of your teamโs development flow.

Jun 18, 2026 ยท 4 min
Building a skill optimization loop
This post shows how to create a loop with automated feedback that an agent can run to optimize its own Skills. It uses an automated grader with computer use to assess how well a Skill is performing, and then iteratively improves the Skill.