fix(build): stable-Rust compatibility + silence deferred-code warnings

The match guard at tui/ui.rs:1603 used `&& let Some(...) = ...` inside an
`if` guard, which requires the `if_let_guard` nightly feature on Rust
< 1.94. Reported by an external user attempting `cargo install
deepseek-tui` on stable rustc — it failed with E0658.

Rewrite as a plain match guard with a nested `if let` inside the arm
body so the language-picker hotkeys compile on every supported rustc.

Workspace also now declares `rust-version = "1.88"` to match the
codebase's actual reliance on `let_chains` in if/while conditions, so
users on too-old toolchains see a clear cargo error instead of a
confusing rustc one.

`AGENTS.md` and `CLAUDE.md` gain a "stable Rust only" section
documenting the trap and how to rewrite around it.

Also annotate the deferred `TuiPrefs` (#657) and `handoff::THRESHOLDS`
(#667) APIs with `#[allow(dead_code)]` so CI's `-D warnings` flag stays
green while the call sites are staged for v0.8.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-05 01:57:14 -05:00
parent da266e2105
commit 668f2c37f5
5 changed files with 93 additions and 36 deletions
+33 -1
View File
@@ -14,7 +14,39 @@ This file provides context for AI assistants working on this project.
- Local dev shorthand: after `cargo build --release`, run `./target/release/deepseek`.
### Build Dependencies
- **Rust** 1.85+ (for the workspace)
- **Rust** 1.88+ (the workspace declares `rust-version = "1.88"` because we
use `let_chains` in `if`/`while` conditions, which stabilized in 1.88).
### Stable Rust only — no nightly features
This crate must compile on stable Rust. **Never** introduce code that
requires `#![feature(...)]`, `cargo +nightly`, or any unstable language /
library feature. Common pitfalls to avoid:
- **`if let` guards in match arms** (`if_let_guard`, tracking issue #51114)
— was nightly-only on Rust < 1.94. Rewrite as a plain match guard with a
nested `if let` inside the arm body. Example of what NOT to do:
```rust
// BAD — fails on stable rustc < 1.94 with E0658
match key {
KeyCode::Char(c) if cond && let Some(x) = find(c) => { … }
}
```
Rewrite as:
```rust
// GOOD — works on every supported rustc
match key {
KeyCode::Char(c) if cond => {
if let Some(x) = find(c) { … }
}
}
```
- `let_chains` in `if`/`while` (`&& let Some(_) = …`) **is** stable as of
Rust 1.88 and is fine to use.
- Custom `#![feature(...)]` attributes — never.
Before opening a PR, run `cargo build` (not `cargo +nightly build`) and
make sure the workspace's declared `rust-version` is enough to compile.
### Documentation
See README.md for project overview, docs/ARCHITECTURE.md for internals.
+5
View File
@@ -21,6 +21,11 @@ resolver = "2"
[workspace.package]
version = "0.8.12"
edition = "2024"
# Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the
# codebase relies on extensively. Cargo enforces this so users on older
# toolchains get a clear "package requires rustc 1.88+" error instead of a
# confusing E0658 from rustc.
rust-version = "1.88"
license = "MIT"
repository = "https://github.com/Hmbown/DeepSeek-TUI"
+13 -2
View File
@@ -1,8 +1,19 @@
// Used by the deferred context-limit handoff feature (#667). The implementation
// path is staged but not yet wired from the engine; suppress dead-code warnings
// rather than delete the table, since v0.8.13 will consume it.
#[allow(dead_code)]
pub const THRESHOLDS: [(f32, &str); 3] = [
(0.9, "Context at 90%: stop and write handoff to .deepseek/handoff.md now"),
(
0.9,
"Context at 90%: stop and write handoff to .deepseek/handoff.md now",
),
(0.8, "Context at 80%: draft handoff to .deepseek/handoff.md"),
(0.7, "Context at 70%: consider wrapping current sub-task"),
];
#[allow(dead_code)]
pub fn threshold_message(ratio: f32) -> Option<&'static str> {
THRESHOLDS.iter().find(|(t,_)| ratio >= *t).map(|(_,m)| *m)
THRESHOLDS
.iter()
.find(|(t, _)| ratio >= *t)
.map(|(_, m)| *m)
}
+18 -14
View File
@@ -34,6 +34,11 @@ use crate::localization::normalize_configured_locale;
/// submit = "ctrl+enter"
/// new_line = "enter"
/// ```
//
// NOTE: the loader is defined but not yet called from startup — wiring is
// deferred to v0.8.13 (#657). The `#[allow(dead_code)]` suppresses the CI
// `-D warnings` failure until the call site lands.
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TuiPrefs {
@@ -58,6 +63,7 @@ impl Default for TuiPrefs {
}
/// Per-action keybinding overrides stored inside [`TuiPrefs`].
#[allow(dead_code)] // see TuiPrefs note above; deferred to v0.8.13 (#657).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct KeybindPrefs {
@@ -78,6 +84,7 @@ pub struct KeybindPrefs {
pub toggle_sidebar: Option<String>,
}
#[allow(dead_code)] // see TuiPrefs note above; deferred to v0.8.13 (#657).
impl TuiPrefs {
/// Return the canonical path of the TUI preferences file:
/// `~/.deepseek/tui.toml`.
@@ -98,9 +105,8 @@ impl TuiPrefs {
}
}
let home = dirs::home_dir().context(
"Failed to resolve home directory: cannot determine tui.toml path.",
)?;
let home = dirs::home_dir()
.context("Failed to resolve home directory: cannot determine tui.toml path.")?;
Ok(home.join(".deepseek").join("tui.toml"))
}
@@ -127,14 +133,10 @@ impl TuiPrefs {
let path = Self::path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!(
"Failed to create config directory {}",
parent.display()
)
format!("Failed to create config directory {}", parent.display())
})?;
}
let content =
toml::to_string_pretty(self).context("Failed to serialize TuiPrefs")?;
let content = toml::to_string_pretty(self).context("Failed to serialize TuiPrefs")?;
std::fs::write(&path, content)
.with_context(|| format!("Failed to write tui.toml to {}", path.display()))?;
Ok(())
@@ -151,9 +153,7 @@ impl TuiPrefs {
self.theme = theme;
}
other => {
anyhow::bail!(
"Invalid tui.toml theme '{other}': expected dark, light, or system."
);
anyhow::bail!("Invalid tui.toml theme '{other}': expected dark, light, or system.");
}
}
Ok(())
@@ -805,7 +805,9 @@ mod tests {
theme: theme.to_string(),
..TuiPrefs::default()
};
prefs.validate().unwrap_or_else(|e| panic!("validate({theme}) failed: {e}"));
prefs
.validate()
.unwrap_or_else(|e| panic!("validate({theme}) failed: {e}"));
assert_eq!(prefs.theme, theme);
}
}
@@ -826,7 +828,9 @@ mod tests {
theme: "solarized".to_string(),
..TuiPrefs::default()
};
let err = prefs.validate().expect_err("solarized is not a valid theme");
let err = prefs
.validate()
.expect_err("solarized is not a valid theme");
assert!(err.to_string().contains("Invalid tui.toml theme"));
}
+24 -19
View File
@@ -1599,25 +1599,31 @@ async fn run_event_loop(
app.status_message = None;
}
// Language picker hotkeys: 1-5 select + persist (#566).
//
// Note: this used to be a single match-guard with `&& let`,
// but `if_let_guard` is a nightly-only feature on Rust
// before 1.94. Rewriting as a plain guard + nested `if let`
// keeps `cargo install` working on stable.
KeyCode::Char(c)
if app.onboarding == OnboardingState::Language
&& c.is_ascii_digit()
&& let Some((_, tag, _, _)) =
onboarding::language::LANGUAGE_OPTIONS
.iter()
.find(|(hotkey, _, _, _)| *hotkey == c) =>
if app.onboarding == OnboardingState::Language && c.is_ascii_digit() =>
{
match app.set_locale_from_onboarding(tag) {
Ok(()) => {
app.push_status_toast(
format!("Language set to {tag}"),
StatusToastLevel::Info,
Some(2_500),
);
advance_after_language(app);
}
Err(err) => {
app.status_message = Some(format!("Failed to save locale: {err}"));
if let Some((_, tag, _, _)) = onboarding::language::LANGUAGE_OPTIONS
.iter()
.find(|(hotkey, _, _, _)| *hotkey == c)
{
match app.set_locale_from_onboarding(tag) {
Ok(()) => {
app.push_status_toast(
format!("Language set to {tag}"),
StatusToastLevel::Info,
Some(2_500),
);
advance_after_language(app);
}
Err(err) => {
app.status_message =
Some(format!("Failed to save locale: {err}"));
}
}
}
}
@@ -4719,8 +4725,7 @@ fn render(f: &mut Frame, app: &mut App) {
// background before any sub-widgets render, so cells that end up
// uncovered by layout splits (e.g. after file-tree toggle or
// resize) don't retain stale content from a previous frame.
Block::default()
.render(chunks[1], f.buffer_mut());
Block::default().render(chunks[1], f.buffer_mut());
let mut sidebar_area = None;