semantic_query/
config.rs

1use std::env;
2use std::io::{self, Write};
3use std::time::Duration;
4use std::thread;
5use std::fs::OpenOptions;
6use crossterm::{
7    event::{self, Event, KeyCode, KeyEvent},
8    terminal,
9};
10
11
12/// Trait for types that can retrieve their configuration key from environment variables
13pub trait KeyFromEnv {
14    /// The environment variable name for this client's API key
15    const KEY_NAME: &'static str;
16    
17    /// Find the API key by checking environment variables first, then .env file
18    fn find_key() -> Option<String> {
19        // First try to load .env file (silently fail if not found)
20        let _ = dotenvy::dotenv();
21        
22        // Try to get from environment
23        env::var(Self::KEY_NAME).ok()
24    }
25    
26    /// Find the API key with user fallback - waits 15 seconds for user input then panics
27    fn find_key_with_user() -> String {
28        if let Some(key) = Self::find_key() {
29            return key;
30        }
31        
32        // Prompt user for input with timeout
33        print!("Environment variable {} not found. Please enter the API key (15 second timeout): ", Self::KEY_NAME);
34        io::stdout().flush().unwrap();
35        
36        // Create a channel for communication between threads
37        let (sender, receiver) = std::sync::mpsc::channel();
38        
39        // Spawn thread to read user input
40        thread::spawn(move || {
41            let mut input = String::new();
42            if io::stdin().read_line(&mut input).is_ok() {
43                let _ = sender.send(input.trim().to_string());
44            }
45        });
46        
47        // Wait for input with timeout
48        let api_key = match receiver.recv_timeout(Duration::from_secs(15)) {
49            Ok(input) if !input.is_empty() => input,
50            _ => panic!("Timeout waiting for {} input after 15 seconds", Self::KEY_NAME),
51        };
52        
53        // Ask if user wants to save to .env file
54        if Self::prompt_save_to_env() {
55            if let Err(e) = Self::save_to_env_file(&api_key) {
56                eprintln!("Warning: Failed to save to .env file: {}", e);
57            } else {
58                println!("API key saved to .env file");
59            }
60        }
61        
62        api_key
63    }
64    
65    /// Prompt user if they want to save the API key to .env file
66    /// Uses single keystroke detection with fallback to Enter
67    fn prompt_save_to_env() -> bool {
68        print!("Add {} to .env file? (y/N): ", Self::KEY_NAME);
69        io::stdout().flush().unwrap();
70        
71        // Try single keystroke detection first
72        if let Ok(response) = Self::read_single_key() {
73            println!("{}", response); // Echo the choice
74            return response.to_lowercase() == "y";
75        }
76        
77        // Fallback to readline
78        let mut input = String::new();
79        if io::stdin().read_line(&mut input).is_ok() {
80            input.trim().to_lowercase() == "y"
81        } else {
82            false
83        }
84    }
85    
86    /// Attempt to read a single keystroke
87    fn read_single_key() -> Result<String, Box<dyn std::error::Error>> {
88        // Enable raw mode temporarily
89        terminal::enable_raw_mode()?;
90        
91        let result = if event::poll(Duration::from_secs(30))? {
92            if let Event::Key(KeyEvent { code, .. }) = event::read()? {
93                match code {
94                    KeyCode::Char('y') | KeyCode::Char('Y') => Ok("y".to_string()),
95                    KeyCode::Char('n') | KeyCode::Char('N') => Ok("n".to_string()),
96                    KeyCode::Enter => Ok("n".to_string()), // Default to no
97                    _ => Ok("n".to_string()), // Any other key defaults to no
98                }
99            } else {
100                Ok("n".to_string())
101            }
102        } else {
103            Ok("n".to_string()) // Timeout defaults to no
104        };
105        
106        terminal::disable_raw_mode()?;
107        result
108    }
109    
110    /// Save the API key to .env file
111    fn save_to_env_file(api_key: &str) -> Result<(), Box<dyn std::error::Error>> {
112        let env_line = format!("{}={}\n", Self::KEY_NAME, api_key);
113        
114        // Check if .env file exists and if the key is already there
115        if let Ok(content) = std::fs::read_to_string(".env") {
116            if content.contains(&format!("{}=", Self::KEY_NAME)) {
117                // Key already exists, don't duplicate
118                return Ok(());
119            }
120        }
121        
122        // Append to .env file
123        let mut file = OpenOptions::new()
124            .create(true)
125            .append(true)
126            .open(".env")?;
127            
128        use std::io::Write;
129        file.write_all(env_line.as_bytes())?;
130        
131        Ok(())
132    }
133}