1use clap::Args;
29use scope::config::OutputFormat;
30use scope::error::Result;
31use serde::{Deserialize, Serialize};
32use std::fmt;
33use std::path::PathBuf;
34
35#[derive(Debug, Clone, Args)]
37#[command(after_help = "\x1b[1mExamples:\x1b[0m
38 scope interactive
39 scope shell
40 scope interactive --no-banner")]
41pub struct InteractiveArgs {
42 #[arg(long)]
44 pub no_banner: bool,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SessionContext {
50 pub chain: String,
52
53 pub format: OutputFormat,
55
56 pub last_address: Option<String>,
58
59 pub last_tx: Option<String>,
61
62 pub include_tokens: bool,
64
65 pub include_txs: bool,
67
68 pub trace: bool,
70
71 pub decode: bool,
73
74 pub limit: u32,
76}
77
78impl SessionContext {
79 pub fn is_auto_chain(&self) -> bool {
81 self.chain == "auto"
82 }
83}
84
85impl Default for SessionContext {
86 fn default() -> Self {
87 Self {
88 chain: "auto".to_string(),
89 format: OutputFormat::Table,
90 last_address: None,
91 last_tx: None,
92 include_tokens: false,
93 include_txs: false,
94 trace: false,
95 decode: false,
96 limit: 100,
97 }
98 }
99}
100
101impl fmt::Display for SessionContext {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 writeln!(f, "Current Context:")?;
104 if self.is_auto_chain() {
105 writeln!(f, " Chain: auto (inferred from input)")?;
106 } else {
107 writeln!(f, " Chain: {} (pinned)", self.chain)?;
108 }
109 writeln!(f, " Format: {:?}", self.format)?;
110 writeln!(f, " Include Tokens: {}", self.include_tokens)?;
111 writeln!(f, " Include TXs: {}", self.include_txs)?;
112 writeln!(f, " Trace: {}", self.trace)?;
113 writeln!(f, " Decode: {}", self.decode)?;
114 writeln!(f, " Limit: {}", self.limit)?;
115 if let Some(ref addr) = self.last_address {
116 writeln!(f, " Last Address: {}", addr)?;
117 }
118 if let Some(ref tx) = self.last_tx {
119 writeln!(f, " Last TX: {}", tx)?;
120 }
121 Ok(())
122 }
123}
124
125impl SessionContext {
126 fn context_path() -> Option<PathBuf> {
128 Some(scope::paths::state_dir().join("session.yaml"))
129 }
130
131 pub fn load() -> Self {
133 Self::context_path()
134 .map(|path| Self::load_from(&path))
135 .unwrap_or_default()
136 }
137
138 pub fn load_from(path: &std::path::Path) -> Self {
141 std::fs::read_to_string(path)
142 .ok()
143 .and_then(|contents| serde_saphyr::from_str(&contents).ok())
144 .unwrap_or_default()
145 }
146
147 pub fn save(&self) -> Result<()> {
149 match Self::context_path() {
150 Some(path) => self.save_to(&path),
151 None => Ok(()),
152 }
153 }
154
155 pub fn save_to(&self, path: &std::path::Path) -> Result<()> {
157 if let Some(parent) = path.parent() {
158 std::fs::create_dir_all(parent)?;
159 }
160 let contents = serde_saphyr::to_string(self)
161 .map_err(|e| scope::error::ScopeError::Export(e.to_string()))?;
162 std::fs::write(path, contents)?;
163 Ok(())
164 }
165}
166
167pub async fn run(
174 args: InteractiveArgs,
175 settings: crate::cli::tui::Settings,
176 reload: crate::cli::tui::Reload<'_>,
177) -> Result<crate::cli::tui::TuiExit> {
178 crate::cli::tui::run(args.no_banner, settings, reload).await
179}
180
181pub async fn run_monitor(
188 args: crate::cli::monitor::MonitorArgs,
189 settings: crate::cli::tui::Settings,
190 reload: crate::cli::tui::Reload<'_>,
191) -> Result<crate::cli::tui::TuiExit> {
192 let start = Some(crate::cli::Commands::Monitor(args));
193 crate::cli::tui::run_with(true, start, settings, reload).await
194}
195
196#[cfg(test)]
201mod tests {
202 use super::*;
203 #[test]
204 fn test_session_context_default() {
205 let ctx = SessionContext::default();
206 assert_eq!(ctx.chain, "auto");
207 assert_eq!(ctx.format, OutputFormat::Table);
208 assert!(!ctx.include_tokens);
209 assert!(!ctx.include_txs);
210 assert!(!ctx.trace);
211 assert!(!ctx.decode);
212 assert_eq!(ctx.limit, 100);
213 assert!(ctx.last_address.is_none());
214 assert!(ctx.last_tx.is_none());
215 }
216 #[test]
217 fn test_session_context_display() {
218 let ctx = SessionContext::default();
219 let display = format!("{}", ctx);
220 assert!(display.contains("auto"));
221 assert!(display.contains("Table"));
222 }
223 #[test]
224 fn test_interactive_args_default() {
225 let args = InteractiveArgs { no_banner: false };
226 assert!(!args.no_banner);
227 }
228 #[test]
229 fn test_session_context_serialization() {
230 let ctx = SessionContext {
231 chain: "polygon".to_string(),
232 format: OutputFormat::Json,
233 last_address: Some("0xabc".to_string()),
234 last_tx: Some("0xdef".to_string()),
235 include_tokens: true,
236 include_txs: true,
237 trace: true,
238 decode: true,
239 limit: 50,
240 };
241
242 let yaml = serde_saphyr::to_string(&ctx).unwrap();
243 let deserialized: SessionContext = serde_saphyr::from_str(&yaml).unwrap();
244 assert_eq!(deserialized.chain, "polygon");
245 assert!(!deserialized.is_auto_chain());
246 assert_eq!(deserialized.format, OutputFormat::Json);
247 assert_eq!(deserialized.last_address.as_deref(), Some("0xabc"));
248 assert_eq!(deserialized.last_tx.as_deref(), Some("0xdef"));
249 assert!(deserialized.include_tokens);
250 assert!(deserialized.include_txs);
251 assert!(deserialized.trace);
252 assert!(deserialized.decode);
253 assert_eq!(deserialized.limit, 50);
254 }
255 #[test]
256 fn test_session_context_display_with_address_and_tx() {
257 let ctx = SessionContext {
258 chain: "polygon".to_string(),
259 last_address: Some("0x1234".to_string()),
260 last_tx: Some("0xabcd".to_string()),
261 ..Default::default()
262 };
263 let display = format!("{}", ctx);
264 assert!(display.contains("0x1234"));
265 assert!(display.contains("0xabcd"));
266 assert!(display.contains("(pinned)"));
267 }
268 #[test]
269 fn test_session_context_display_auto_chain() {
270 let ctx = SessionContext::default();
271 let display = format!("{}", ctx);
272 assert!(display.contains("auto"));
273 assert!(display.contains("inferred from input"));
274 }
275 #[test]
276 fn test_session_context_serialization_roundtrip() {
277 let ctx = SessionContext {
278 chain: "solana".to_string(),
279 include_tokens: true,
280 limit: 25,
281 last_address: Some("0xtest".to_string()),
282 ..Default::default()
283 };
284
285 let yaml = serde_saphyr::to_string(&ctx).unwrap();
286 let deserialized: SessionContext = serde_saphyr::from_str(&yaml).unwrap();
287 assert_eq!(deserialized.chain, "solana");
288 assert!(deserialized.include_tokens);
289 assert_eq!(deserialized.limit, 25);
290 assert_eq!(deserialized.last_address, Some("0xtest".to_string()));
291 }
292 #[test]
293 fn test_session_context_is_auto_chain() {
294 let auto_ctx = SessionContext::default();
295 assert!(auto_ctx.is_auto_chain());
296 let pinned_ctx = SessionContext {
297 chain: "ethereum".to_string(),
298 ..Default::default()
299 };
300 assert!(!pinned_ctx.is_auto_chain());
301 }
302
303 #[test]
304 fn test_session_context_save_to_and_load_from() {
305 let dir = tempfile::tempdir().unwrap();
307 let path = dir.path().join("nested").join("session.yaml");
308 let ctx = SessionContext {
309 chain: "solana".into(),
310 include_txs: true,
311 last_address: Some("addr".into()),
312 ..Default::default()
313 };
314 ctx.save_to(&path).unwrap();
315 let back = SessionContext::load_from(&path);
316 assert_eq!(back.chain, "solana");
317 assert!(back.include_txs);
318 assert_eq!(back.last_address.as_deref(), Some("addr"));
319 }
320
321 #[test]
322 fn test_session_save_and_load_use_the_test_sandbox() {
323 let path = SessionContext::context_path().unwrap();
327 assert!(path.starts_with(std::env::temp_dir()), "{}", path.display());
328 let ctx = SessionContext {
329 chain: "base".into(),
330 limit: 7,
331 ..Default::default()
332 };
333 ctx.save().unwrap();
334 let back = SessionContext::load();
335 assert_eq!(back.chain, "base");
336 assert_eq!(back.limit, 7);
337 }
338
339 #[test]
340 fn test_session_context_load_from_missing_or_corrupt_is_default() {
341 let dir = tempfile::tempdir().unwrap();
342 assert!(SessionContext::load_from(&dir.path().join("none.yaml")).is_auto_chain());
343 let bad = dir.path().join("bad.yaml");
344 std::fs::write(&bad, "chain: [unclosed").unwrap();
345 assert!(SessionContext::load_from(&bad).is_auto_chain());
346 }
347}