fix(settings): reduce motion in VTE flicker terminals
Harvested from PR #1527 by @axobase001. Co-authored-by: axobase001 <dengzhuoran9@gmail.com>
This commit is contained in:
@@ -392,6 +392,7 @@ Key environment variables:
|
||||
| `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, `ollama` |
|
||||
| `DEEPSEEK_PROFILE` | Config profile name |
|
||||
| `DEEPSEEK_MEMORY` | Set to `on` to enable user memory |
|
||||
| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | Allow non-local `http://` API base URLs on trusted networks |
|
||||
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
|
||||
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | Generic OpenAI-compatible endpoint and model ID |
|
||||
| `SGLANG_BASE_URL` | Self-hosted SGLang endpoint |
|
||||
|
||||
+4
-1
@@ -178,7 +178,10 @@ max_subagents = 10 # optional (1-20)
|
||||
# base_url = "https://integrate.api.nvidia.com/v1"
|
||||
# model = "deepseek-ai/deepseek-v4-pro" # or deepseek-ai/deepseek-v4-flash
|
||||
|
||||
# Generic OpenAI-compatible endpoint
|
||||
# Generic OpenAI-compatible endpoint. Use the built-in `openai` provider for
|
||||
# third-party gateways; do not invent a custom provider name. For non-local
|
||||
# http:// gateways, launch with DEEPSEEK_ALLOW_INSECURE_HTTP=1 only on a
|
||||
# trusted network.
|
||||
[providers.openai]
|
||||
# api_key = "YOUR_OPENAI_COMPATIBLE_API_KEY"
|
||||
# base_url = "https://api.openai.com/v1"
|
||||
|
||||
@@ -372,16 +372,18 @@ impl Settings {
|
||||
self.low_motion = true;
|
||||
self.fancy_animations = false;
|
||||
}
|
||||
// VS Code (TERM_PROGRAM=vscode, #1356) and Ghostty (TERM_PROGRAM=ghostty,
|
||||
// #1445) both produce visible flicker at 120 FPS: VS Code's compositor
|
||||
// cannot keep pace; Ghostty's GPU compositor flash-renders each full-screen
|
||||
// repaint. Drop to the 30 FPS low-motion cap for both automatically.
|
||||
// VS Code (TERM_PROGRAM=vscode, #1356), Ghostty (TERM_PROGRAM=ghostty,
|
||||
// #1445), and a few VTE terminals (#1470) produce visible flicker at
|
||||
// 120 FPS. Drop to the 30 FPS low-motion cap for them automatically.
|
||||
// Like NO_ANIMATIONS above, this unconditionally overrides any
|
||||
// disk-loaded value — consistent precedence: env signals always win.
|
||||
let vte_env_forces_low_motion = std::env::var_os("TILIX_ID").is_some_and(|v| !v.is_empty())
|
||||
|| std::env::var_os("TERMINATOR_UUID").is_some_and(|v| !v.is_empty());
|
||||
if matches!(
|
||||
std::env::var("TERM_PROGRAM").as_deref(),
|
||||
Ok("vscode") | Ok("ghostty")
|
||||
) {
|
||||
) || vte_env_forces_low_motion
|
||||
{
|
||||
self.low_motion = true;
|
||||
self.fancy_animations = false;
|
||||
}
|
||||
@@ -1241,6 +1243,8 @@ mod tests {
|
||||
let prev = std::env::var_os("TERM_PROGRAM");
|
||||
let prev_ssh_client = std::env::var_os("SSH_CLIENT");
|
||||
let prev_ssh_tty = std::env::var_os("SSH_TTY");
|
||||
let prev_tilix_id = std::env::var_os("TILIX_ID");
|
||||
let prev_terminator_uuid = std::env::var_os("TERMINATOR_UUID");
|
||||
// SAFETY: serialised by the guard. Clear SSH_* so a real
|
||||
// SSH session running the test suite doesn't make this
|
||||
// assertion trivially fail — the SSH path is exercised
|
||||
@@ -1248,6 +1252,8 @@ mod tests {
|
||||
unsafe {
|
||||
std::env::remove_var("SSH_CLIENT");
|
||||
std::env::remove_var("SSH_TTY");
|
||||
std::env::remove_var("TILIX_ID");
|
||||
std::env::remove_var("TERMINATOR_UUID");
|
||||
}
|
||||
for program in ["iTerm.app", "Apple_Terminal", "WezTerm", "xterm-256color"] {
|
||||
// SAFETY: serialised by the guard.
|
||||
@@ -1273,6 +1279,60 @@ mod tests {
|
||||
if let Some(v) = prev_ssh_tty {
|
||||
std::env::set_var("SSH_TTY", v);
|
||||
}
|
||||
if let Some(v) = prev_tilix_id {
|
||||
std::env::set_var("TILIX_ID", v);
|
||||
}
|
||||
if let Some(v) = prev_terminator_uuid {
|
||||
std::env::set_var("TERMINATOR_UUID", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tilix_and_terminator_env_force_low_motion_on() {
|
||||
let _g = term_program_test_guard();
|
||||
let prev_term_program = std::env::var_os("TERM_PROGRAM");
|
||||
let prev_tilix_id = std::env::var_os("TILIX_ID");
|
||||
let prev_terminator_uuid = std::env::var_os("TERMINATOR_UUID");
|
||||
|
||||
for (var, val) in [
|
||||
("TILIX_ID", "d5b5b5d6-tilix-session"),
|
||||
("TERMINATOR_UUID", "urn:uuid:terminator-session"),
|
||||
] {
|
||||
// SAFETY: serialised by the guard.
|
||||
unsafe {
|
||||
std::env::remove_var("TERM_PROGRAM");
|
||||
std::env::remove_var("TILIX_ID");
|
||||
std::env::remove_var("TERMINATOR_UUID");
|
||||
std::env::set_var(var, val);
|
||||
}
|
||||
let mut settings = Settings::default();
|
||||
assert!(!settings.low_motion, "default is animated");
|
||||
settings.apply_env_overrides();
|
||||
assert!(
|
||||
settings.low_motion,
|
||||
"{var} must enable low_motion to prevent VTE flicker (#1470)"
|
||||
);
|
||||
assert!(
|
||||
!settings.fancy_animations,
|
||||
"{var} must disable fancy_animations"
|
||||
);
|
||||
}
|
||||
|
||||
// SAFETY: cleanup under the guard.
|
||||
unsafe {
|
||||
match prev_term_program {
|
||||
Some(v) => std::env::set_var("TERM_PROGRAM", v),
|
||||
None => std::env::remove_var("TERM_PROGRAM"),
|
||||
}
|
||||
match prev_tilix_id {
|
||||
Some(v) => std::env::set_var("TILIX_ID", v),
|
||||
None => std::env::remove_var("TILIX_ID"),
|
||||
}
|
||||
match prev_terminator_uuid {
|
||||
Some(v) => std::env::set_var("TERMINATOR_UUID", v),
|
||||
None => std::env::remove_var("TERMINATOR_UUID"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6142,6 +6142,11 @@ fn render(f: &mut Frame, app: &mut App) {
|
||||
crate::config::ApiProvider::Vllm => Some("vLLM"),
|
||||
crate::config::ApiProvider::Ollama => Some("Ollama"),
|
||||
};
|
||||
let status_indicator_started_at = if app.low_motion {
|
||||
None
|
||||
} else {
|
||||
app.turn_started_at
|
||||
};
|
||||
let header_data = HeaderData::new(
|
||||
app.mode,
|
||||
&model_label,
|
||||
@@ -6158,7 +6163,7 @@ fn render(f: &mut Frame, app: &mut App) {
|
||||
.with_reasoning_effort(Some(&effort_label))
|
||||
.with_provider(provider_label)
|
||||
.with_status_indicator(crate::tui::widgets::header_status_indicator_frame(
|
||||
app.turn_started_at,
|
||||
status_indicator_started_at,
|
||||
&app.status_indicator,
|
||||
));
|
||||
let header_widget = HeaderWidget::new(header_data);
|
||||
|
||||
@@ -10,8 +10,9 @@ visual motion and density for screen-reader and low-motion users.
|
||||
| Toggle | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `NO_ANIMATIONS=1` env var | unset | At startup, forces `low_motion = true` and `fancy_animations = false`. Overrides whatever's saved in `settings.toml`. |
|
||||
| `low_motion` setting | `false` | Suppresses spinners' motion, transcript fade-ins, footer drift, and the active-cell pulse. The frame-rate limiter also slows down idle redraws so the cursor doesn't blink as aggressively. |
|
||||
| `low_motion` setting | `false` | Suppresses spinners' motion, transcript fade-ins, footer drift, the header status-indicator cycle, and the active-cell pulse. The frame-rate limiter also slows down idle redraws so the cursor doesn't blink as aggressively. |
|
||||
| `fancy_animations` setting | `false` | Footer water-spout strip and pulsing sub-agent counter. Off by default. |
|
||||
| `status_indicator` setting | `whale` | Header status chip. Set to `dots` for the compact dot cycle or `off` to hide it. |
|
||||
| `calm_mode` setting | `false` | Collapses tool-output details by default and trims status messages. Useful for screen readers that announce every redraw. |
|
||||
| `show_thinking` setting | `true` | Set to `false` to hide model `reasoning_content` blocks entirely. |
|
||||
| `show_tool_details` setting | `true` | Set to `false` to render tool calls as one-liners without expanded payloads. |
|
||||
@@ -43,11 +44,17 @@ The same toggles are reachable from the command palette:
|
||||
* `/settings set low_motion on`
|
||||
* `/settings set fancy_animations off`
|
||||
* `/settings set calm_mode on`
|
||||
* `/settings set status_indicator off`
|
||||
|
||||
Settings written this way persist to `~/.config/deepseek/settings.toml`.
|
||||
The `NO_ANIMATIONS` env var still wins at startup if it's set, so
|
||||
unsetting the env var is the way to honor your saved choice.
|
||||
|
||||
Tilix and Terminator sessions automatically start in low-motion mode because
|
||||
those VTE-based terminals have reported visible redraw flicker during active
|
||||
turns. You can still override the saved settings after launch if your terminal
|
||||
version renders cleanly.
|
||||
|
||||
## Notes for screen-reader users
|
||||
|
||||
* `low_motion` slows the idle redraw loop to ~120ms per frame so
|
||||
|
||||
@@ -82,6 +82,35 @@ URLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store
|
||||
unless API-key auth is explicitly requested; use an env var or config-file key
|
||||
when a local server does require bearer auth.
|
||||
|
||||
### Custom OpenAI-Compatible Gateways
|
||||
|
||||
For a third-party service that implements the OpenAI Chat Completions API, use
|
||||
the built-in `openai` provider name and point its provider table at the gateway:
|
||||
|
||||
```toml
|
||||
provider = "openai"
|
||||
default_text_model = "your-model-id"
|
||||
|
||||
[providers.openai]
|
||||
api_key = "YOUR_OPENAI_COMPATIBLE_API_KEY"
|
||||
base_url = "https://your-gateway.example/v1"
|
||||
```
|
||||
|
||||
Do not invent a custom provider name; `provider` must be one of the known
|
||||
providers listed above. Put the endpoint under `[providers.openai]`, not the
|
||||
legacy top-level `base_url`, so the OpenAI-compatible provider receives it.
|
||||
`default_text_model` is the model ID sent to the gateway; if you keep several
|
||||
provider tables in one config, `[providers.openai].model` can be used as the
|
||||
OpenAI-provider-specific override.
|
||||
|
||||
Local HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default
|
||||
when they use localhost or loopback addresses. For a non-local `http://`
|
||||
gateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:
|
||||
|
||||
```bash
|
||||
DEEPSEEK_ALLOW_INSECURE_HTTP=1 deepseek
|
||||
```
|
||||
|
||||
Third-party OpenAI-compatible gateways that need extra request headers can set
|
||||
`http_headers = { "X-Model-Provider-Id" = "your-model-provider" }` at the top
|
||||
level or under a provider table such as `[providers.deepseek]`. When configured,
|
||||
|
||||
Reference in New Issue
Block a user