1use std::collections::{HashMap, VecDeque};
4use std::sync::{Arc, RwLock};
5use std::time::Duration;
6
7use crate::config::{ConfigLayer, DEFAULT_SESSION_CACHE_MAX_RESULTS};
8use crate::core::row::Row;
9use crate::repl::HistoryShellContext;
10
11use super::command_output::CliCommandResult;
12use super::runtime::{
13 AppClients, AppRuntime, AuthState, ConfigState, LaunchContext, RuntimeContext, UiState,
14};
15use super::timing::TimingSummary;
16
17#[derive(Debug, Clone, Copy, Default)]
18pub struct DebugTimingBadge {
19 pub level: u8,
20 pub(crate) summary: TimingSummary,
21}
22
23#[derive(Clone, Default, Debug)]
24pub struct DebugTimingState {
25 inner: Arc<RwLock<Option<DebugTimingBadge>>>,
26}
27
28impl DebugTimingState {
29 pub fn set(&self, badge: DebugTimingBadge) {
30 if let Ok(mut guard) = self.inner.write() {
31 *guard = Some(badge);
32 }
33 }
34
35 pub fn clear(&self) {
36 if let Ok(mut guard) = self.inner.write() {
37 *guard = None;
38 }
39 }
40
41 pub fn badge(&self) -> Option<DebugTimingBadge> {
42 self.inner.read().map(|value| *value).unwrap_or(None)
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ReplScopeFrame {
48 command: String,
49}
50
51impl ReplScopeFrame {
52 pub fn new(command: impl Into<String>) -> Self {
53 Self {
54 command: command.into(),
55 }
56 }
57
58 pub fn command(&self) -> &str {
59 self.command.as_str()
60 }
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct ReplScopeStack {
65 frames: Vec<ReplScopeFrame>,
66}
67
68impl ReplScopeStack {
69 pub fn is_root(&self) -> bool {
70 self.frames.is_empty()
71 }
72
73 pub fn enter(&mut self, command: impl Into<String>) {
74 self.frames.push(ReplScopeFrame::new(command));
75 }
76
77 pub fn leave(&mut self) -> Option<ReplScopeFrame> {
78 self.frames.pop()
79 }
80
81 pub fn commands(&self) -> Vec<String> {
82 self.frames
83 .iter()
84 .map(|frame| frame.command.clone())
85 .collect()
86 }
87
88 pub fn contains_command(&self, command: &str) -> bool {
89 self.frames
90 .iter()
91 .any(|frame| frame.command.eq_ignore_ascii_case(command))
92 }
93
94 pub fn display_label(&self) -> Option<String> {
95 if self.is_root() {
96 None
97 } else {
98 Some(
99 self.frames
100 .iter()
101 .map(|frame| frame.command.as_str())
102 .collect::<Vec<_>>()
103 .join(" / "),
104 )
105 }
106 }
107
108 pub fn history_prefix(&self) -> String {
109 if self.is_root() {
110 String::new()
111 } else {
112 format!(
113 "{} ",
114 self.frames
115 .iter()
116 .map(|frame| frame.command.as_str())
117 .collect::<Vec<_>>()
118 .join(" ")
119 )
120 }
121 }
122
123 pub fn prefixed_tokens(&self, tokens: &[String]) -> Vec<String> {
124 let prefix = self.commands();
125 if prefix.is_empty() || tokens.starts_with(&prefix) {
126 return tokens.to_vec();
127 }
128 let mut full = prefix;
129 full.extend_from_slice(tokens);
130 full
131 }
132
133 pub fn help_tokens(&self) -> Vec<String> {
134 let mut tokens = self.commands();
135 if !tokens.is_empty() {
136 tokens.push("--help".to_string());
137 }
138 tokens
139 }
140}
141
142pub struct AppSession {
143 pub prompt_prefix: String,
144 pub history_enabled: bool,
145 pub history_shell: HistoryShellContext,
146 pub prompt_timing: DebugTimingState,
147 pub scope: ReplScopeStack,
148 pub last_rows: Vec<Row>,
149 pub last_failure: Option<LastFailure>,
150 pub result_cache: HashMap<String, Vec<Row>>,
151 pub cache_order: VecDeque<String>,
152 pub(crate) command_cache: HashMap<String, CliCommandResult>,
153 pub(crate) command_cache_order: VecDeque<String>,
154 pub max_cached_results: usize,
155 pub config_overrides: ConfigLayer,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct LastFailure {
160 pub command_line: String,
161 pub summary: String,
162 pub detail: String,
163}
164
165impl AppSession {
166 pub fn with_cache_limit(max_cached_results: usize) -> Self {
167 let bounded = max_cached_results.max(1);
168 Self {
169 prompt_prefix: "osp".to_string(),
170 history_enabled: true,
171 history_shell: HistoryShellContext::default(),
172 prompt_timing: DebugTimingState::default(),
173 scope: ReplScopeStack::default(),
174 last_rows: Vec::new(),
175 last_failure: None,
176 result_cache: HashMap::new(),
177 cache_order: VecDeque::new(),
178 command_cache: HashMap::new(),
179 command_cache_order: VecDeque::new(),
180 max_cached_results: bounded,
181 config_overrides: ConfigLayer::default(),
182 }
183 }
184
185 pub fn record_result(&mut self, command_line: &str, rows: Vec<Row>) {
186 let key = command_line.trim().to_string();
187 if key.is_empty() {
188 return;
189 }
190
191 self.last_rows = rows.clone();
192 if !self.result_cache.contains_key(&key)
193 && self.result_cache.len() >= self.max_cached_results
194 && let Some(evict_key) = self.cache_order.pop_front()
195 {
196 self.result_cache.remove(&evict_key);
197 }
198
199 self.cache_order.retain(|item| item != &key);
200 self.cache_order.push_back(key.clone());
201 self.result_cache.insert(key, rows);
202 }
203
204 pub fn record_failure(
205 &mut self,
206 command_line: &str,
207 summary: impl Into<String>,
208 detail: impl Into<String>,
209 ) {
210 let command_line = command_line.trim().to_string();
211 if command_line.is_empty() {
212 return;
213 }
214 self.last_failure = Some(LastFailure {
215 command_line,
216 summary: summary.into(),
217 detail: detail.into(),
218 });
219 }
220
221 pub fn cached_rows(&self, command_line: &str) -> Option<&[Row]> {
222 self.result_cache
223 .get(command_line.trim())
224 .map(|rows| rows.as_slice())
225 }
226
227 pub(crate) fn record_cached_command(&mut self, cache_key: &str, result: &CliCommandResult) {
228 let cache_key = cache_key.trim().to_string();
229 if cache_key.is_empty() {
230 return;
231 }
232
233 if !self.command_cache.contains_key(&cache_key)
234 && self.command_cache.len() >= self.max_cached_results
235 && let Some(evict_key) = self.command_cache_order.pop_front()
236 {
237 self.command_cache.remove(&evict_key);
238 }
239
240 self.command_cache_order.retain(|item| item != &cache_key);
241 self.command_cache_order.push_back(cache_key.clone());
242 self.command_cache.insert(cache_key, result.clone());
243 }
244
245 pub(crate) fn cached_command(&self, cache_key: &str) -> Option<CliCommandResult> {
246 self.command_cache.get(cache_key.trim()).cloned()
247 }
248
249 pub fn record_prompt_timing(
250 &self,
251 level: u8,
252 total: Duration,
253 parse: Option<Duration>,
254 execute: Option<Duration>,
255 render: Option<Duration>,
256 ) {
257 if level == 0 {
258 self.prompt_timing.clear();
259 return;
260 }
261
262 self.prompt_timing.set(DebugTimingBadge {
263 level,
264 summary: TimingSummary {
265 total,
266 parse,
267 execute,
268 render,
269 },
270 });
271 }
272
273 pub fn sync_history_shell_context(&self) {
274 self.history_shell.set_prefix(self.scope.history_prefix());
275 }
276}
277
278pub(crate) struct AppStateInit {
279 pub context: RuntimeContext,
280 pub config: crate::config::ResolvedConfig,
281 pub render_settings: crate::ui::RenderSettings,
282 pub message_verbosity: crate::ui::messages::MessageLevel,
283 pub debug_verbosity: u8,
284 pub plugins: crate::plugin::PluginManager,
285 pub themes: crate::ui::theme_loader::ThemeCatalog,
286 pub launch: LaunchContext,
287}
288
289pub struct AppState {
290 pub runtime: AppRuntime,
291 pub session: AppSession,
292 pub clients: AppClients,
293}
294
295impl AppState {
296 pub(crate) fn new(init: AppStateInit) -> Self {
297 let config_state = ConfigState::new(init.config);
298 let auth_state = AuthState::from_resolved(config_state.resolved());
299 let session_cache_max_results = crate::app::host::config_usize(
300 config_state.resolved(),
301 "session.cache.max_results",
302 DEFAULT_SESSION_CACHE_MAX_RESULTS as usize,
303 );
304
305 Self {
306 runtime: AppRuntime {
307 context: init.context,
308 config: config_state,
309 ui: UiState {
310 render_settings: init.render_settings,
311 message_verbosity: init.message_verbosity,
312 debug_verbosity: init.debug_verbosity,
313 },
314 auth: auth_state,
315 themes: init.themes,
316 launch: init.launch,
317 },
318 session: AppSession::with_cache_limit(session_cache_max_results),
319 clients: AppClients::new(init.plugins),
320 }
321 }
322
323 pub fn prompt_prefix(&self) -> String {
324 self.session.prompt_prefix.clone()
325 }
326
327 pub fn sync_history_shell_context(&self) {
328 self.session.sync_history_shell_context();
329 }
330
331 pub fn record_repl_rows(&mut self, command_line: &str, rows: Vec<Row>) {
332 self.session.record_result(command_line, rows);
333 }
334
335 pub fn record_repl_failure(
336 &mut self,
337 command_line: &str,
338 summary: impl Into<String>,
339 detail: impl Into<String>,
340 ) {
341 self.session.record_failure(command_line, summary, detail);
342 }
343
344 pub fn last_repl_rows(&self) -> Vec<Row> {
345 self.session.last_rows.clone()
346 }
347
348 pub fn last_repl_failure(&self) -> Option<LastFailure> {
349 self.session.last_failure.clone()
350 }
351
352 pub fn cached_repl_rows(&self, command_line: &str) -> Option<Vec<Row>> {
353 self.session
354 .cached_rows(command_line)
355 .map(ToOwned::to_owned)
356 }
357
358 pub fn repl_cache_size(&self) -> usize {
359 self.session.result_cache.len()
360 }
361}