mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
Merge remote-tracking branch 'origin/main' into pr-234-review
This commit is contained in:
@@ -5,6 +5,50 @@ All notable changes to tty7 are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Panes are told which terminal they're running in** — every pane now carries
|
||||
`TERM_PROGRAM=tty7` and `TERM_PROGRAM_VERSION`, the de-facto standard pair
|
||||
Apple Terminal introduced and iTerm2, WezTerm, Ghostty, VS Code and tmux all
|
||||
set. `TERM` names terminfo capabilities and can't answer "which program is
|
||||
this", so without the pair, capability probes (`supports-color`,
|
||||
`supports-hyperlinks`, and the CLI ecosystem built on them), editors applying
|
||||
terminal-specific workarounds, and shell prompts all fell back to their most
|
||||
conservative behaviour. tty7's own `TTY7` marker doesn't help them — it exists
|
||||
so globally-installed agent hooks stay silent in other terminals, and nothing
|
||||
third-party knows to look for it. Unlike `TERM` and `COLORTERM`, both new
|
||||
variables can be overridden from `env` in `config.json`: they name an
|
||||
identity, not a capability, and posing as another terminal is a legitimate way
|
||||
to get a tool that only recognises a fixed list to light up. Local panes only
|
||||
— ssh forwards environment variables solely by agreement between client and
|
||||
server, so a remote host still sees whatever it sets for itself. (#212)
|
||||
|
||||
- **Inactive panes only fade if you want them to** — a split tab dims every pane
|
||||
but the focused one so the active terminal reads as foreground. That is the
|
||||
right default, but it is not free: at 55% opacity a dim theme's comment color
|
||||
or a long-running build's output in the pane you are *watching* rather than
|
||||
typing into gets harder to read, and some people track panes by cursor alone
|
||||
and never needed the cue. Settings → Appearance → Transparency now carries a
|
||||
"Dim inactive panes" switch. On by default, so nothing changes for anyone who
|
||||
was happy; off renders every pane at full opacity. (#214)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Italic CJK rendered as unrelated CJK on Windows** — every character came out
|
||||
as a different character, one for one, consistently, so it read as a broken
|
||||
locale or a mangled encoding. It was neither. Hack, the bundled default, has no
|
||||
CJK, so those cells are shaped by the font-fallback chain; gpui's Windows
|
||||
backend then threw away the face DirectWrite shaped with and looked a fresh one
|
||||
up by family, weight and style. That round trip mapped DirectWrite's *italic*
|
||||
to *oblique* — the two are numbered the other way around in the API — and a
|
||||
family with no oblique face resolved to its upright one. The glyph indices were
|
||||
right; the outlines they were pointing into belonged to a different face. Fixed
|
||||
in our gpui fork by rasterizing the face DirectWrite actually chose, which also
|
||||
closes a latent use-after-free in the same cache: it keyed fonts by a raw
|
||||
pointer to a face nothing held a reference to.
|
||||
|
||||
## [26.7.5] - 2026-07-27
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+25
-25
@@ -1369,7 +1369,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"gpui_util",
|
||||
"indexmap",
|
||||
@@ -1927,7 +1927,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "derive_refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3096,7 +3096,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui"
|
||||
version = "0.2.2"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"anyhow",
|
||||
@@ -3287,7 +3287,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_linux"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_unix",
|
||||
@@ -3338,7 +3338,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_macos"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_macos",
|
||||
@@ -3385,7 +3385,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -3396,7 +3396,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_platform"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"gpui",
|
||||
@@ -3409,7 +3409,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_shared_string"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -3419,7 +3419,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
@@ -3428,7 +3428,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_web"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"console_error_panic_hook",
|
||||
@@ -3452,7 +3452,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_wgpu"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytemuck",
|
||||
@@ -3481,7 +3481,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_windows"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_windows",
|
||||
@@ -3800,7 +3800,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -3825,7 +3825,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client_tls"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
@@ -4946,7 +4946,7 @@ checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
|
||||
[[package]]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen",
|
||||
@@ -6044,7 +6044,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
[[package]]
|
||||
name = "perf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"serde",
|
||||
@@ -7021,7 +7021,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"derive_refineable",
|
||||
]
|
||||
@@ -7064,7 +7064,7 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
[[package]]
|
||||
name = "reqwest_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -7589,7 +7589,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "scheduler"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"backtrace",
|
||||
@@ -8396,7 +8396,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
[[package]]
|
||||
name = "sum_tree"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"heapless",
|
||||
"log",
|
||||
@@ -9775,7 +9775,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
[[package]]
|
||||
name = "util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-fs",
|
||||
@@ -9814,7 +9814,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "util_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"perf",
|
||||
"quote",
|
||||
@@ -11620,7 +11620,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "zlog"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -11665,7 +11665,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ztracing"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -11676,7 +11676,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ztracing_macro"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
|
||||
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#87ce3e2d5c16d5704a45196a761c8809e4a386c7"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
|
||||
+15
-6
@@ -319,12 +319,21 @@ lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
# ---- gpui fork ------------------------------------------------------------
|
||||
# Our `tty7` branch (cut from the pinned upstream rev, one commit on top) carries
|
||||
# a single patch: `prefers_ime_for_printable_keys` takes the keystroke, so an
|
||||
# input handler can answer per key instead of per view. tty7 needs it for
|
||||
# Option-as-Meta — macOS routes ⌥-chords to the IME whenever a CJK input source
|
||||
# is active, and without the keystroke there is no way to decline just those
|
||||
# chords (see `terminal::input::prefers_ime_for_printable_keys`, issue #177).
|
||||
# Our `tty7` branch (cut from the pinned upstream rev, two commits on top) carries:
|
||||
#
|
||||
# 1. `prefers_ime_for_printable_keys` takes the keystroke, so an input handler can
|
||||
# answer per key instead of per view. tty7 needs it for Option-as-Meta — macOS
|
||||
# routes ⌥-chords to the IME whenever a CJK input source is active, and without
|
||||
# the keystroke there is no way to decline just those chords (see
|
||||
# `terminal::input::prefers_ime_for_printable_keys`, issue #177).
|
||||
#
|
||||
# 2. gpui's Windows backend rasterizes a font-fallback run with the face
|
||||
# DirectWrite actually shaped it with, instead of re-deriving one from the
|
||||
# face's family/weight/style. The round trip mapped DirectWrite's italic to
|
||||
# oblique, so italic CJK — which every pane reaches through the fallback chain,
|
||||
# Hack having no CJK — drew a *different* face's outlines at the shaped glyph
|
||||
# indices. Every character rendered as an unrelated character, one for one,
|
||||
# which reads as mojibake rather than as a font bug.
|
||||
#
|
||||
# Patching by source rather than editing the `gpui`/`gpui_platform` pins above is
|
||||
# deliberate: `gpui-component` declares its own `gpui` from the upstream URL, and
|
||||
|
||||
@@ -1095,6 +1095,25 @@ mod tests {
|
||||
assert!(!newer.confirm_window_close);
|
||||
}
|
||||
|
||||
/// Also opt-*out*: every config written before the switch existed predates
|
||||
/// the choice, and those users have been looking at dimmed panes all along —
|
||||
/// defaulting to `false` would silently change how every split tab looks on
|
||||
/// upgrade. And once someone does turn it off, the `false` has to survive a
|
||||
/// save/load cycle, or the effect they opted out of returns on next launch.
|
||||
#[test]
|
||||
fn dim_inactive_panes_defaults_on_and_round_trips() {
|
||||
assert!(Config::default().dim_inactive_panes);
|
||||
|
||||
let old: Config = serde_json::from_str(r#"{"font_size": 15.0}"#).unwrap();
|
||||
assert!(old.dim_inactive_panes);
|
||||
|
||||
let off: Config = serde_json::from_str(r#"{"dim_inactive_panes": false}"#).unwrap();
|
||||
assert!(!off.dim_inactive_panes);
|
||||
let json = serde_json::to_string(&off).unwrap();
|
||||
let back: Config = serde_json::from_str(&json).unwrap();
|
||||
assert!(!back.dim_inactive_panes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_follow_system_defaults_and_round_trips() {
|
||||
// Old configs (no follow-system keys) must land on off + the built-in
|
||||
|
||||
+174
-18
@@ -422,29 +422,84 @@ fn system_locale_identifier() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// What tty7 answers to in `TERM_PROGRAM`. Terminals name themselves in the
|
||||
/// form they brand themselves in — `Apple_Terminal`, `iTerm.app`, `WezTerm`,
|
||||
/// `ghostty`, `vscode` — so ours is the lowercase product name.
|
||||
const TERM_PROGRAM_NAME: &str = "tty7";
|
||||
|
||||
/// Env keys that describe our emulator's real capabilities. A user's `env` map
|
||||
/// must not override these: the answer isn't a preference, it's a fact about
|
||||
/// what the pane on the other end can decode.
|
||||
const CAPABILITY_ENV: [&str; 2] = ["TERM", "COLORTERM"];
|
||||
|
||||
/// Whether a configured `env` key names one of [`CAPABILITY_ENV`]. Windows
|
||||
/// environment blocks are case-insensitive — `portable-pty` keeps one slot per
|
||||
/// lowercased key, so a configured `Term` there would replace `TERM` just as
|
||||
/// surely as the exact spelling — so the filter must use the platform's own
|
||||
/// notion of "the same variable". On Unix a differently-cased key is a genuinely
|
||||
/// distinct variable and stays the user's to set.
|
||||
fn names_capability_env(key: &str) -> bool {
|
||||
CAPABILITY_ENV.iter().any(|cap| {
|
||||
if cfg!(windows) {
|
||||
key.eq_ignore_ascii_case(cap)
|
||||
} else {
|
||||
key == *cap
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The environment every pane starts with, in application order — tty7's own
|
||||
/// advertisements first, then the user's `env` map, which overrides all but
|
||||
/// [`CAPABILITY_ENV`]. Returned as a list rather than applied in place so the
|
||||
/// precedence is testable without a `CommandBuilder` or a real `config.json`.
|
||||
fn pane_environment(
|
||||
extra_env: &std::collections::HashMap<String, String>,
|
||||
) -> Vec<(String, String)> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let mut env = vec![
|
||||
// A widely-available terminfo + truecolor.
|
||||
("TERM".to_string(), "xterm-256color".to_string()),
|
||||
("COLORTERM".to_string(), "truecolor".to_string()),
|
||||
// Mark the session as tty7's, for tooling that adapts to its host
|
||||
// terminal — most importantly the `tty7 agent-hook` emitter, which
|
||||
// stays silent without it so globally-installed agent hooks can't leak
|
||||
// escape sequences into other terminals (see `core::agent_hooks`).
|
||||
(
|
||||
crate::core::agent_hooks::TTY7_ENV_MARKER.to_string(),
|
||||
version.to_string(),
|
||||
),
|
||||
// The de-facto standard pair for "which terminal is this": Apple
|
||||
// Terminal introduced it, and iTerm2, WezTerm, Ghostty, VS Code and
|
||||
// tmux all set it. `TERM` describes terminfo capabilities and can't
|
||||
// answer this — but capability probes (`supports-color`,
|
||||
// `supports-hyperlinks` and the JS CLI ecosystem built on them),
|
||||
// editors applying terminal-specific workarounds, and shell prompts all
|
||||
// branch on the program name, falling back to their most conservative
|
||||
// behaviour when it's missing. `TTY7` doesn't help them: it's ours, and
|
||||
// nothing third-party knows to look for it.
|
||||
//
|
||||
// Deliberately overridable below, unlike the capability keys: this
|
||||
// names an identity, and posing as another terminal is a legitimate way
|
||||
// to get a tool that only recognises a fixed list to light up.
|
||||
("TERM_PROGRAM".to_string(), TERM_PROGRAM_NAME.to_string()),
|
||||
("TERM_PROGRAM_VERSION".to_string(), version.to_string()),
|
||||
];
|
||||
env.extend(
|
||||
extra_env
|
||||
.iter()
|
||||
.filter(|(k, _)| !names_capability_env(k))
|
||||
.map(|(k, v)| (k.clone(), v.clone())),
|
||||
);
|
||||
env
|
||||
}
|
||||
|
||||
fn apply_common_command_setup(cmd: &mut CommandBuilder, initial_cwd: &Option<PathBuf>) {
|
||||
if let Some(dir) = initial_cwd {
|
||||
cmd.cwd(dir);
|
||||
}
|
||||
// Advertise a widely-available terminfo + truecolor.
|
||||
cmd.env("TERM", "xterm-256color");
|
||||
cmd.env("COLORTERM", "truecolor");
|
||||
// Mark the session as tty7's, for tooling that adapts to its host terminal
|
||||
// — most importantly the `tty7 agent-hook` emitter, which stays silent
|
||||
// without it so globally-installed agent hooks can't leak escape sequences
|
||||
// into other terminals (see `core::agent_hooks`).
|
||||
cmd.env(
|
||||
crate::core::agent_hooks::TTY7_ENV_MARKER,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
|
||||
// User-configured environment variables override inherited values (but not
|
||||
// TERM/COLORTERM above, which reflect our emulator's real capabilities).
|
||||
let extra_env = crate::core::config::extra_env();
|
||||
for (k, v) in &extra_env {
|
||||
if k != "TERM" && k != "COLORTERM" {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
for (k, v) in pane_environment(&extra_env) {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
|
||||
// LaunchServices commonly starts a macOS app with no locale variables at
|
||||
@@ -4042,6 +4097,107 @@ mod tests {
|
||||
assert!(dead_rx.try_recv().is_err(), "on_dead must fire only once");
|
||||
}
|
||||
|
||||
/// Every pane is told which terminal it is running in, under the names the
|
||||
/// rest of the world reads (`TERM_PROGRAM`/`TERM_PROGRAM_VERSION`) as well
|
||||
/// as our own `TTY7` marker. Nothing third-party looks for the marker, so
|
||||
/// dropping the standard pair would leave capability probes guessing.
|
||||
#[test]
|
||||
fn pane_environment_advertises_the_terminal_under_the_standard_names() {
|
||||
let env: std::collections::HashMap<_, _> =
|
||||
pane_environment(&std::collections::HashMap::new())
|
||||
.into_iter()
|
||||
.collect();
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
assert_eq!(env.get("TERM_PROGRAM").map(String::as_str), Some("tty7"));
|
||||
assert_eq!(
|
||||
env.get("TERM_PROGRAM_VERSION").map(String::as_str),
|
||||
Some(version)
|
||||
);
|
||||
assert_eq!(
|
||||
env.get(crate::core::agent_hooks::TTY7_ENV_MARKER)
|
||||
.map(String::as_str),
|
||||
Some(version)
|
||||
);
|
||||
assert_eq!(
|
||||
env.get("TERM").map(String::as_str),
|
||||
Some("xterm-256color"),
|
||||
"terminfo name is what the pane's decoder actually implements"
|
||||
);
|
||||
}
|
||||
|
||||
/// The user's `env` map may rename the terminal — posing as another program
|
||||
/// is how you get a tool that only recognises a fixed list to light up —
|
||||
/// but it may not contradict what our emulator can decode. Later entries
|
||||
/// win, so the ordering is the precedence.
|
||||
#[test]
|
||||
fn pane_environment_lets_configured_env_override_identity_but_not_capability() {
|
||||
let configured = [
|
||||
("TERM_PROGRAM", "iTerm.app"),
|
||||
("TERM_PROGRAM_VERSION", "3.5.0"),
|
||||
("TERM", "dumb"),
|
||||
("COLORTERM", ""),
|
||||
("EDITOR", "hx"),
|
||||
]
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
|
||||
.collect();
|
||||
|
||||
let applied: std::collections::HashMap<_, _> =
|
||||
pane_environment(&configured).into_iter().collect();
|
||||
|
||||
assert_eq!(
|
||||
applied.get("TERM_PROGRAM").map(String::as_str),
|
||||
Some("iTerm.app")
|
||||
);
|
||||
assert_eq!(
|
||||
applied.get("TERM_PROGRAM_VERSION").map(String::as_str),
|
||||
Some("3.5.0")
|
||||
);
|
||||
assert_eq!(applied.get("EDITOR").map(String::as_str), Some("hx"));
|
||||
assert_eq!(
|
||||
applied.get("TERM").map(String::as_str),
|
||||
Some("xterm-256color")
|
||||
);
|
||||
assert_eq!(
|
||||
applied.get("COLORTERM").map(String::as_str),
|
||||
Some("truecolor")
|
||||
);
|
||||
}
|
||||
|
||||
/// Windows environment blocks are case-insensitive — `portable-pty` keeps
|
||||
/// one slot per lowercased key — so a configured `Term` would replace
|
||||
/// `TERM` just as surely as the exact spelling. The capability filter must
|
||||
/// therefore drop any casing of a capability key, not just the canonical
|
||||
/// one. (On Unix a differently-cased key is a distinct variable and passes
|
||||
/// through untouched.)
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn pane_environment_capability_keys_cannot_be_overridden_by_recasing() {
|
||||
let configured = [("Term", "dumb"), ("ColorTerm", ""), ("term_program", "x")]
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
|
||||
.collect();
|
||||
|
||||
let applied = pane_environment(&configured);
|
||||
|
||||
assert!(
|
||||
!applied.iter().any(|(k, _)| k == "Term" || k == "ColorTerm"),
|
||||
"a recased capability key must be filtered out, or it would land \
|
||||
in the same case-folded slot and win by coming later"
|
||||
);
|
||||
let get = |key: &str| {
|
||||
applied
|
||||
.iter()
|
||||
.find(|(k, _)| k == key)
|
||||
.map(|(_, v)| v.as_str())
|
||||
};
|
||||
assert_eq!(get("TERM"), Some("xterm-256color"));
|
||||
assert_eq!(get("COLORTERM"), Some("truecolor"));
|
||||
// Identity keys stay overridable in any casing the user spells.
|
||||
assert_eq!(get("term_program"), Some("x"));
|
||||
}
|
||||
|
||||
/// The macOS UTF-8 fallback applies only when the inherited environment has
|
||||
/// no locale and the user has not taken control through the generic `env`
|
||||
/// map. Key presence is authoritative there, including an empty value.
|
||||
|
||||
+149
-15
@@ -490,18 +490,51 @@ enum RowSeg {
|
||||
/// drawing, accented Latin, …) that may route to a fallback face whose
|
||||
/// advance isn't the cell width.
|
||||
Solo { col: usize },
|
||||
/// A base plus the combining marks stacked on it, shaped as one string so
|
||||
/// the marks reach the shaper. Never batched with neighbours: the marks add
|
||||
/// A base with everything that has to shape alongside it — the combining
|
||||
/// marks stacked on it, and a following SARA AM — as one string, so the
|
||||
/// shaper sees the whole cluster. Never batched with neighbours: marks add
|
||||
/// characters without adding columns, which is exactly the correspondence
|
||||
/// `force_width` relies on in a [`RowSeg::Run`] or [`RowSeg::Wide`].
|
||||
///
|
||||
/// An absorbed SARA AM takes the base's style rather than its own. Unlike
|
||||
/// a [`RowSeg::Run`], the cluster can't break on a style change: split off,
|
||||
/// SARA AM has no base to reorder its nikhahit onto and renders as a dotted
|
||||
/// circle. A recoloured vowel beats a broken one.
|
||||
Cluster {
|
||||
col: usize,
|
||||
/// Columns the base occupies — 2 once the grid marked it wide.
|
||||
/// Columns the whole cluster occupies — 2 for a wide base, or for a
|
||||
/// narrow base that absorbed a following SARA AM.
|
||||
cells: usize,
|
||||
text: String,
|
||||
/// Whether `cells == 2` because the *base* is wide, rather than because
|
||||
/// a spacing character joined it. The two need opposite pinning: a wide
|
||||
/// base is one glyph across two columns, an absorbed SARA AM is two
|
||||
/// glyphs of one column each.
|
||||
wide_base: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Append a cell's character followed by any combining marks riding on it.
|
||||
fn push_cell(text: &mut String, cell: &RenderCell) {
|
||||
text.push(cell.c);
|
||||
text.extend(cell.marks.iter().flat_map(|marks| marks.iter()));
|
||||
}
|
||||
|
||||
/// SARA AM (Thai U+0E33, Lao U+0EB3) is `Lo` and owns a column, but it is not
|
||||
/// atomic to the shaper: the Thai shaper decomposes it into NIKHAHIT + SARA AA
|
||||
/// and moves the nikhahit backwards over any above-base marks onto the base
|
||||
/// consonant. Shaped in a run of its own it has no base to reorder onto, and
|
||||
/// comes out as a dotted circle.
|
||||
fn is_sara_am(c: char) -> bool {
|
||||
matches!(c, '\u{0E33}' | '\u{0EB3}')
|
||||
}
|
||||
|
||||
/// Does `col` hold a SARA AM that should join the preceding cell's cluster?
|
||||
fn sara_am_at(row: &[RenderCell], col: usize) -> Option<&RenderCell> {
|
||||
row.get(col)
|
||||
.filter(|cell| !cell.spacer && is_sara_am(cell.c))
|
||||
}
|
||||
|
||||
/// Split one grid row into paintable segments.
|
||||
///
|
||||
/// ASCII-graphic cells batch into [`RowSeg::Run`]s: they always come from the
|
||||
@@ -530,15 +563,26 @@ fn segment_row(row: &[RenderCell]) -> Vec<RowSeg> {
|
||||
// Combining marks come first: they can sit on an ASCII base too, and
|
||||
// either way the whole cluster has to reach the shaper in one string.
|
||||
if let Some(marks) = &cell.marks {
|
||||
let cells = if col + 1 < row.len() && row[col + 1].spacer {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let wide_base = col + 1 < row.len() && row[col + 1].spacer;
|
||||
let mut cells = if wide_base { 2 } else { 1 };
|
||||
let mut text = String::with_capacity(1 + marks.len());
|
||||
text.push(cell.c);
|
||||
text.extend(marks.iter());
|
||||
segs.push(RowSeg::Cluster { col, cells, text });
|
||||
push_cell(&mut text, cell);
|
||||
// A wide base already owns both columns, so only a narrow one has a
|
||||
// column spare for SARA AM to join it in. A SARA AM is not itself a
|
||||
// base to absorb onto — two in a row stay separate.
|
||||
if !wide_base
|
||||
&& !is_sara_am(cell.c)
|
||||
&& let Some(am) = sara_am_at(row, col + 1)
|
||||
{
|
||||
push_cell(&mut text, am);
|
||||
cells = 2;
|
||||
}
|
||||
segs.push(RowSeg::Cluster {
|
||||
col,
|
||||
cells,
|
||||
text,
|
||||
wide_base,
|
||||
});
|
||||
col += cells;
|
||||
continue;
|
||||
}
|
||||
@@ -569,6 +613,23 @@ fn segment_row(row: &[RenderCell]) -> Vec<RowSeg> {
|
||||
cells: col - start,
|
||||
text,
|
||||
});
|
||||
} else if !is_sara_am(cell.c)
|
||||
&& let Some(am) = sara_am_at(row, col + 1)
|
||||
{
|
||||
// An unmarked base still has to shape with its SARA AM. A
|
||||
// baseless SARA AM is not a base for the next one: absorbing
|
||||
// there would pin the second one's glyphs outside the cluster's
|
||||
// clip, so two in a row stay separate and both stay visible.
|
||||
let mut text = String::with_capacity(2);
|
||||
push_cell(&mut text, cell);
|
||||
push_cell(&mut text, am);
|
||||
segs.push(RowSeg::Cluster {
|
||||
col,
|
||||
cells: 2,
|
||||
text,
|
||||
wide_base: false,
|
||||
});
|
||||
col += 2;
|
||||
} else {
|
||||
segs.push(RowSeg::Solo { col });
|
||||
col += 1;
|
||||
@@ -904,11 +965,22 @@ fn paint_glyphs(
|
||||
// Same pinning as the batched runs, just for one base: two
|
||||
// columns get `force_width` so a fallback emoji face can't
|
||||
// drift, one column paints at the origin like `Solo`.
|
||||
RowSeg::Cluster { col, cells, text } => (
|
||||
// Two columns pin per *base glyph*, and which that is depends
|
||||
// on why the cluster is two cells wide: a wide base is one
|
||||
// glyph spanning both, an absorbed SARA AM is two glyphs of one
|
||||
// column each. `force_width` classifies by advance, so the
|
||||
// marks ride their base under either. One column paints at the
|
||||
// origin like `Solo`.
|
||||
RowSeg::Cluster {
|
||||
col,
|
||||
cells,
|
||||
text,
|
||||
wide_base,
|
||||
} => (
|
||||
col,
|
||||
cells,
|
||||
SharedString::from(text),
|
||||
(cells == 2).then(|| geom.cell_width * 2.),
|
||||
(cells == 2).then(|| geom.cell_width * if wide_base { 2. } else { 1. }),
|
||||
cells == 1,
|
||||
),
|
||||
};
|
||||
@@ -2228,6 +2300,16 @@ mod tests {
|
||||
col,
|
||||
cells,
|
||||
text: text.to_string(),
|
||||
wide_base: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn wide_cluster(col: usize, cells: usize, text: &str) -> RowSeg {
|
||||
RowSeg::Cluster {
|
||||
col,
|
||||
cells,
|
||||
text: text.to_string(),
|
||||
wide_base: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2246,7 +2328,7 @@ mod tests {
|
||||
// spacer too (❤ + U+FE0F).
|
||||
let mut row = wide_cells("\u{2764}");
|
||||
row[0].marks = Some(Box::from(['\u{FE0F}']));
|
||||
assert_eq!(segment_row(&row), [cluster(0, 2, "\u{2764}\u{FE0F}")]);
|
||||
assert_eq!(segment_row(&row), [wide_cluster(0, 2, "\u{2764}\u{FE0F}")]);
|
||||
|
||||
// Several marks on one base: an above-base vowel and a tone mark both
|
||||
// sit on the consonant (ที่ = ท U+0E17 + ◌ี U+0E35 + ◌่ U+0E48).
|
||||
@@ -2258,6 +2340,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// SARA AM (U+0E33) is the awkward Thai vowel: `Lo`, width 1, so the grid
|
||||
/// gives it its own column — but the shaper decomposes it into NIKHAHIT +
|
||||
/// SARA AA and reorders the nikhahit backwards onto the base consonant.
|
||||
/// Shaped in its own run it has no base to reorder onto and comes out as a
|
||||
/// dotted circle, so it has to join the preceding cell's cluster.
|
||||
#[test]
|
||||
fn segment_row_absorbs_sara_am_into_its_base() {
|
||||
// น + ้ (tone) + ำ — the base already carries a mark.
|
||||
let mut row = vec![cell('\u{0E19}'), cell('\u{0E33}'), cell('a')];
|
||||
row[0].marks = Some(Box::from(['\u{0E49}']));
|
||||
assert_eq!(
|
||||
segment_row(&row),
|
||||
[cluster(0, 2, "\u{0E19}\u{0E49}\u{0E33}"), run(2, 1, "a")]
|
||||
);
|
||||
|
||||
// ก + ำ — an unmarked base still has to shape with it.
|
||||
let row = vec![cell('\u{0E01}'), cell('\u{0E33}')];
|
||||
assert_eq!(segment_row(&row), [cluster(0, 2, "\u{0E01}\u{0E33}")]);
|
||||
|
||||
// Lao SARA AM (U+0EB3) takes the same shaper path.
|
||||
let row = vec![cell('\u{0E81}'), cell('\u{0EB3}')];
|
||||
assert_eq!(segment_row(&row), [cluster(0, 2, "\u{0E81}\u{0EB3}")]);
|
||||
|
||||
// A style change does not break the cluster, unlike a `Run` or `Wide`
|
||||
// batch: split off, the vowel has no base and paints a dotted circle,
|
||||
// so it takes the base's style instead.
|
||||
let mut row = vec![cell('\u{0E01}'), cell('\u{0E33}')];
|
||||
row[1].fg = gpui::red();
|
||||
assert_eq!(segment_row(&row), [cluster(0, 2, "\u{0E01}\u{0E33}")]);
|
||||
}
|
||||
|
||||
/// With nothing to attach to, SARA AM paints alone — a dotted circle is the
|
||||
/// shaper's honest answer for an orphaned mark, and inventing a base would
|
||||
/// be worse.
|
||||
#[test]
|
||||
fn segment_row_leaves_a_baseless_sara_am_alone() {
|
||||
let row = vec![cell('\u{0E33}'), cell('a')];
|
||||
assert_eq!(segment_row(&row), [RowSeg::Solo { col: 0 }, run(1, 1, "a")]);
|
||||
|
||||
// A blank before it is not a base either.
|
||||
let row = vec![cell(' '), cell('\u{0E33}')];
|
||||
assert_eq!(segment_row(&row), [RowSeg::Solo { col: 1 }]);
|
||||
|
||||
// Nor is another SARA AM: absorbing would pin the second one's glyphs
|
||||
// past the cluster's two-cell clip and swallow it entirely.
|
||||
let row = vec![cell('\u{0E33}'), cell('\u{0E33}')];
|
||||
assert_eq!(
|
||||
segment_row(&row),
|
||||
[RowSeg::Solo { col: 0 }, RowSeg::Solo { col: 1 }]
|
||||
);
|
||||
}
|
||||
|
||||
/// A marked cell never joins a batch: marks add characters without adding
|
||||
/// columns, which would desync `force_width`'s glyph-per-column pinning.
|
||||
#[test]
|
||||
@@ -2277,7 +2411,7 @@ mod tests {
|
||||
segment_row(&row),
|
||||
[
|
||||
wide(0, 2, "你"),
|
||||
cluster(2, 2, "好\u{FE0F}"),
|
||||
wide_cluster(2, 2, "好\u{FE0F}"),
|
||||
wide(4, 2, "世"),
|
||||
]
|
||||
);
|
||||
|
||||
+8
-5
@@ -2506,8 +2506,8 @@ impl Tty7App {
|
||||
self.update_config(cx, |cfg| cfg.check_for_updates = on);
|
||||
}
|
||||
|
||||
/// Toggle inactive-pane dimming. Applies on the next render — the pane tree
|
||||
/// reads the flag from the `Config` global each frame.
|
||||
/// Toggle inactive-pane dimming. Applies on the next render — `update_config`
|
||||
/// notifies, and this view's render is what hands the flag to the pane tree.
|
||||
pub(crate) fn set_dim_inactive_panes(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
self.update_config(cx, |cfg| cfg.dim_inactive_panes = on);
|
||||
}
|
||||
@@ -5214,7 +5214,7 @@ impl Render for Tty7App {
|
||||
.get(self.active)
|
||||
.and_then(|t| t.pane.focused_or_first(window, cx))
|
||||
.and_then(|leaf| self.render_ssh_status_strip(&leaf, cx));
|
||||
// Render the active tab's pane tree; show focus rings only when split.
|
||||
// Render the active tab's pane tree.
|
||||
let body = match self.tabs.get(self.active) {
|
||||
// Zero tabs: the window's own face — the home page (see `ui::home`).
|
||||
None => self.render_home(cx).into_any_element(),
|
||||
@@ -5235,8 +5235,11 @@ impl Render for Tty7App {
|
||||
.child(leaf.clone())
|
||||
.into_any_element(),
|
||||
None => {
|
||||
let show_focus = active_tab.pane.leaves().len() > 1;
|
||||
active_tab.pane.render(show_focus, window, cx)
|
||||
// Fading the unfocused panes only says anything once the
|
||||
// tab is actually split, and the user can turn it off.
|
||||
let dim_inactive = active_tab.pane.leaves().len() > 1
|
||||
&& cx.global::<Config>().dim_inactive_panes;
|
||||
active_tab.pane.render(dim_inactive, window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-21
@@ -524,33 +524,33 @@ impl Pane<Entity<TerminalView>> {
|
||||
self.close_leaf_where(&|v| v.entity_id() == target.entity_id())
|
||||
}
|
||||
|
||||
/// Render the subtree. `show_focus` draws a focus ring on the active leaf
|
||||
/// (suppressed when the tab has a single pane).
|
||||
pub fn render(&self, show_focus: bool, window: &mut Window, cx: &mut App) -> gpui::AnyElement {
|
||||
/// Render the subtree. `dim_inactive` fades every leaf but the focused one;
|
||||
/// the caller decides it — it is off for an unsplit tab (nothing to
|
||||
/// distinguish) and off when the user turned `dim_inactive_panes` off. Kept
|
||||
/// a parameter rather than a `Config` global read here so the tree stays
|
||||
/// renderable without one, as the rest of this module is.
|
||||
pub fn render(
|
||||
&self,
|
||||
dim_inactive: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> gpui::AnyElement {
|
||||
match self {
|
||||
Pane::Empty => div().into_any_element(),
|
||||
Pane::Leaf(v) => {
|
||||
let focused = show_focus && v.read(cx).focus_handle.contains_focused(window, cx);
|
||||
let focused = v.read(cx).focus_handle.contains_focused(window, cx);
|
||||
// No full border (it reads as a hard rectangle).
|
||||
div()
|
||||
.size_full()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
// Inactive panes (only when the tab is actually split) fade back
|
||||
// so the focused terminal reads as foreground without a hard
|
||||
// border. Element opacity multiplies through the whole subtree
|
||||
// (terminal glyphs + cell fills), unlike a background-tinted
|
||||
// scrim which is near-invisible on a light theme (white on
|
||||
// white). Applied to the container, so a click still lands on
|
||||
// the terminal and focuses it. `dim_inactive_panes` opts out.
|
||||
.when(
|
||||
show_focus
|
||||
&& !focused
|
||||
&& cx
|
||||
.global::<crate::core::config::Config>()
|
||||
.dim_inactive_panes,
|
||||
|d| d.opacity(0.55),
|
||||
)
|
||||
// Inactive panes fade back so the focused terminal reads as
|
||||
// foreground without a hard border. Element opacity multiplies
|
||||
// through the whole subtree (terminal glyphs + cell fills),
|
||||
// unlike a background-tinted scrim which is near-invisible on a
|
||||
// light theme (white on white). Applied to the container, so a
|
||||
// click still lands on the terminal and focuses it.
|
||||
.when(dim_inactive && !focused, |d| d.opacity(0.55))
|
||||
.child(v.clone())
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -681,7 +681,7 @@ impl Pane<Entity<TerminalView>> {
|
||||
.flex_basis(px(0.))
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.child(a.render(show_focus, window, cx)),
|
||||
.child(a.render(dim_inactive, window, cx)),
|
||||
)
|
||||
.child(divider)
|
||||
.child(
|
||||
@@ -691,7 +691,7 @@ impl Pane<Entity<TerminalView>> {
|
||||
.flex_basis(px(0.))
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.child(b.render(show_focus, window, cx)),
|
||||
.child(b.render(dim_inactive, window, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
+20
-10
@@ -144,6 +144,11 @@ fn settings_search_entries() -> &'static [SearchEntry] {
|
||||
title: "Blur",
|
||||
keywords: "transparency translucent frosted vibrancy window background",
|
||||
},
|
||||
SearchEntry {
|
||||
section: Appearance,
|
||||
title: "Dim inactive panes",
|
||||
keywords: "fade unfocused inactive split pane focus opacity highlight active dimming",
|
||||
},
|
||||
SearchEntry {
|
||||
section: Appearance,
|
||||
title: "Font size",
|
||||
@@ -1518,10 +1523,12 @@ impl Tty7App {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Window section (Appearance): global opacity slider + blur switch that
|
||||
/// apply to every theme. Both are config *overrides* — until touched they
|
||||
/// follow the active theme's own `opacity`/`blur`, and "Follow theme"
|
||||
/// clears them back to that state.
|
||||
/// Window section (Appearance): the global opacity slider and blur switch
|
||||
/// that apply to every theme, then the inactive-pane dimming switch. The
|
||||
/// first two are config *overrides* — until touched they follow the active
|
||||
/// theme's own `opacity`/`blur`, and "Follow theme" clears them back to that
|
||||
/// state; the dimming switch is a plain flag no theme carries a value for,
|
||||
/// so it sits below that button and "Follow theme" leaves it alone.
|
||||
fn render_window_section(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(slider) = self
|
||||
.active_settings()
|
||||
@@ -1578,12 +1585,6 @@ impl Tty7App {
|
||||
blur_switch,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
"Dim inactive panes",
|
||||
"Fade unfocused panes in a split so the active one stands out.",
|
||||
dim_switch,
|
||||
cx,
|
||||
))
|
||||
// Only offered while an override is active; otherwise the values
|
||||
// already follow the theme and the button would be a no-op.
|
||||
.when(overridden, |this| {
|
||||
@@ -1598,6 +1599,14 @@ impl Tty7App {
|
||||
),
|
||||
)
|
||||
})
|
||||
// Below "Follow theme", which resets the two rows above it and not
|
||||
// this one — a plain setting with no theme value behind it.
|
||||
.child(self.settings_row(
|
||||
"Dim inactive panes",
|
||||
"Fade unfocused panes in a split so the active one stands out.",
|
||||
dim_switch,
|
||||
cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -4623,6 +4632,7 @@ mod tests {
|
||||
"Sidebar grouping",
|
||||
"Tab completion",
|
||||
"History search",
|
||||
"Dim inactive panes",
|
||||
] {
|
||||
assert!(
|
||||
settings_search_entries().iter().any(|e| e.title == title),
|
||||
|
||||
Reference in New Issue
Block a user