返回 DeepSeek-TUI-2026
features.rs
根目录 / crates / tui / src / features.rs
1 #![allow(dead_code)]
2
3 //! Feature flags and metadata for DeepSeek TUI.
4
5 use std::collections::{BTreeMap, BTreeSet};
6 use std::fmt::{self, Write as _};
7
8 use serde::{Deserialize, Serialize};
9
10 /// Lifecycle stage for a feature flag.
11 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12 pub enum Stage {
13 Experimental,
14 Beta,
15 Stable,
16 Deprecated,
17 Removed,
18 }
19
20 impl Stage {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Self::Experimental => "experimental",
24 Self::Beta => "beta",
25 Self::Stable => "stable",
26 Self::Deprecated => "deprecated",
27 Self::Removed => "removed",
28 }
29 }
30 }
31
32 /// Unique features toggled via configuration.
33 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34 pub enum Feature {
35 /// Enable the default shell tool.
36 ShellTool,
37 /// Enable background sub-agent tooling.
38 Subagents,
39 /// Enable web search tool.
40 WebSearch,
41 /// Enable apply_patch tool.
42 ApplyPatch,
43 /// Enable MCP tools.
44 Mcp,
45 /// Enable execpolicy integration/tooling.
46 ExecPolicy,
47 }
48
49 impl fmt::Display for Stage {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(self.as_str())
52 }
53 }
54
55 impl Feature {
56 pub fn key(self) -> &'static str {
57 self.info().key
58 }
59
60 pub fn stage(self) -> Stage {
61 self.info().stage
62 }
63
64 pub fn default_enabled(self) -> bool {
65 self.info().default_enabled
66 }
67
68 fn info(self) -> &'static FeatureSpec {
69 FEATURES
70 .iter()
71 .find(|spec| spec.id == self)
72 .unwrap_or_else(|| unreachable!("missing FeatureSpec for {:?}", self))
73 }
74 }
75
76 /// Holds the effective set of enabled features.
77 #[derive(Debug, Clone, Default, PartialEq)]
78 pub struct Features {
79 enabled: BTreeSet<Feature>,
80 }
81
82 impl Features {
83 /// Starts with built-in defaults.
84 pub fn with_defaults() -> Self {
85 let mut set = BTreeSet::new();
86 for spec in FEATURES {
87 if spec.default_enabled {
88 set.insert(spec.id);
89 }
90 }
91 Self { enabled: set }
92 }
93
94 pub fn enabled(&self, feature: Feature) -> bool {
95 self.enabled.contains(&feature)
96 }
97
98 pub fn enable(&mut self, feature: Feature) -> &mut Self {
99 self.enabled.insert(feature);
100 self
101 }
102
103 pub fn disable(&mut self, feature: Feature) -> &mut Self {
104 self.enabled.remove(&feature);
105 self
106 }
107
108 pub fn apply_map(&mut self, entries: &BTreeMap<String, bool>) {
109 for (key, enabled) in entries {
110 if let Some(feature) = feature_from_key(key) {
111 if *enabled {
112 self.enable(feature);
113 } else {
114 self.disable(feature);
115 }
116 }
117 }
118 }
119
120 pub fn enabled_features(&self) -> Vec<Feature> {
121 let mut list: Vec<_> = self.enabled.iter().copied().collect();
122 list.sort();
123 list
124 }
125 }
126
127 /// Keys accepted in `[features]` tables.
128 pub fn is_known_feature_key(key: &str) -> bool {
129 FEATURES.iter().any(|spec| spec.key == key)
130 }
131
132 pub fn feature_from_key(key: &str) -> Option<Feature> {
133 FEATURES
134 .iter()
135 .find(|spec| spec.key == key)
136 .map(|spec| spec.id)
137 }
138
139 pub fn feature_spec_by_key(key: &str) -> Option<&'static FeatureSpec> {
140 FEATURES.iter().find(|spec| spec.key == key)
141 }
142
143 pub fn render_feature_table(features: &Features) -> String {
144 let mut output = String::from("feature\tstage\tenabled\n");
145 for spec in FEATURES {
146 let _ = writeln!(
147 output,
148 "{}\t{}\t{}",
149 spec.key,
150 spec.stage,
151 features.enabled(spec.id)
152 );
153 }
154 output
155 }
156
157 /// Deserializable features table for TOML.
158 #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
159 pub struct FeaturesToml {
160 #[serde(flatten)]
161 pub entries: BTreeMap<String, bool>,
162 }
163
164 /// Single registry of all feature definitions.
165 #[derive(Debug, Clone, Copy)]
166 pub struct FeatureSpec {
167 pub id: Feature,
168 pub key: &'static str,
169 pub stage: Stage,
170 pub default_enabled: bool,
171 }
172
173 pub const FEATURES: &[FeatureSpec] = &[
174 FeatureSpec {
175 id: Feature::ShellTool,
176 key: "shell_tool",
177 stage: Stage::Stable,
178 default_enabled: true,
179 },
180 FeatureSpec {
181 id: Feature::Subagents,
182 key: "subagents",
183 stage: Stage::Experimental,
184 default_enabled: true,
185 },
186 FeatureSpec {
187 id: Feature::WebSearch,
188 key: "web_search",
189 stage: Stage::Experimental,
190 default_enabled: true,
191 },
192 FeatureSpec {
193 id: Feature::ApplyPatch,
194 key: "apply_patch",
195 stage: Stage::Experimental,
196 default_enabled: true,
197 },
198 FeatureSpec {
199 id: Feature::Mcp,
200 key: "mcp",
201 stage: Stage::Experimental,
202 default_enabled: true,
203 },
204 FeatureSpec {
205 id: Feature::ExecPolicy,
206 key: "exec_policy",
207 stage: Stage::Experimental,
208 default_enabled: true,
209 },
210 ];
211
212 #[cfg(test)]
213 mod tests {
214 use super::*;
215
216 #[test]
217 fn apply_map_toggles_known_features_and_ignores_unknown_keys() {
218 let mut features = Features::with_defaults();
219 let entries = BTreeMap::from([
220 ("mcp".to_string(), false),
221 ("shell_tool".to_string(), false),
222 ("not_real".to_string(), false),
223 ]);
224
225 features.apply_map(&entries);
226
227 assert!(!features.enabled(Feature::Mcp));
228 assert!(!features.enabled(Feature::ShellTool));
229 assert_eq!(feature_from_key("not_real"), None);
230 }
231
232 #[test]
233 fn render_feature_table_uses_registry_order_and_effective_state() {
234 let mut features = Features::with_defaults();
235 features.disable(Feature::Mcp);
236
237 let table = render_feature_table(&features);
238 let lines = table.lines().collect::<Vec<_>>();
239
240 assert_eq!(lines.first(), Some(&"feature\tstage\tenabled"));
241 assert!(lines.contains(&"shell_tool\tstable\ttrue"));
242 assert!(lines.contains(&"mcp\texperimental\tfalse"));
243 }
244 }
245
245 lines RUST