Files
zed/crates/terminal_view
Apoorva Verma 166e52d76f terminal: Don't steal focus from an open modal on background terminal startup (#60960)
the startup terminal can take a sec to come up if your shell is slow.
when it's ready it grabs focus, and if you've opened the command palette
by then, that blurs the palette input and closes it. so it just
vanishes.

`add_center_terminal` focused the new terminal unconditionally. now it
skips the focus if a modal is open (`!has_active_modal`). one line at
the definition, covers all the call sites.

kept it terminal-specific like dinocosta framed it. could do a broader
workspace-level "don't steal from a modal" instead if you'd prefer.

two tests: modal stays focused after a background terminal spawns (fails
without the fix), and no-modal still focuses the terminal like before.

Fixes #60762.

Release Notes:

- Fixed a background terminal finishing startup closing the command
palette or an open picker
2026-07-20 05:42:44 +00:00
..
2025-03-10 01:06:11 -07:00

Terminal View

Design Notes

This crate is split into two conceptual halves:

  • The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with terminal emulator backends and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
  • Everything else. These other files integrate the Terminal struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.

ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. TerminalBuilder solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with TerminalBuilder::new(), check the result, then call TerminalBuilder::subscribe(cx) from within a model context.

The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.

Backend Boundary

terminal.rs exposes backend-neutral domain types such as terminal content, cells, modes, points, ranges, scroll commands, vi motions, hyperlinks, and search matches. UI code should depend on those types instead of importing backend-specific terminal types directly.

The current implementation is still Alacritty-backed, but the abstraction boundary keeps backend details concentrated in the terminal crate:

  • terminal_view renders TerminalContent and dispatches backend-neutral actions.
  • Panels and tools use terminal-domain types for mode, cursor, range, hyperlink, and search behavior.
  • Backend-specific conversions stay near the terminal event loop and render snapshot code.

This keeps the user-facing terminal behavior unchanged while making future backend experiments reviewable as backend implementations instead of UI-wide refactors.

Input

There are currently many distinct paths for getting keystrokes to the terminal:

  1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the try_keystroke() method on Terminal

  2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same try_keystroke() API as the above mappings

  3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the View::replace_text_in_range() method, and we then send that to the terminal directly, bypassing try_keystroke().

  4. Pasted text has a separate pathway.

Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the .input() API (which try_keystroke uses) so we have consistent input handling across the terminal