Skip to main content

journey/
app.rs

1use crate::cli::Cli;
2use crate::config::Config;
3use crate::config_manager::ConfigManager;
4use crate::errors::JourneyError;
5use crate::vault::Vault;
6use chrono::{Local, NaiveDate};
7use std::env;
8use std::process::Command;
9
10#[derive(Debug, Clone)]
11struct DailyNotesConfig {
12    enabled: bool,
13    format: String,
14    folder: String,
15    template: Option<String>,
16}
17
18#[derive(Debug, Clone)]
19struct PeriodicNotesConfig {
20    enabled: bool,
21}
22
23#[derive(Debug, Clone)]
24struct JournalsConfig {
25    name: String,
26}
27
28#[derive(Debug, Clone)]
29struct ObsidianPluginConfigs {
30    daily_notes: Option<DailyNotesConfig>,
31    periodic_notes: Option<PeriodicNotesConfig>,
32    journals: Vec<JournalsConfig>,
33}
34
35impl ObsidianPluginConfigs {
36    fn new() -> Self {
37        Self {
38            daily_notes: None,
39            periodic_notes: None,
40            journals: Vec::new(),
41        }
42    }
43}
44
45#[derive(Clone)]
46pub struct CliArgs {
47    pub vault: Option<String>,
48    pub date: Option<String>,
49    pub relative_date: Option<i64>,
50    pub time: Option<String>,
51    pub time_format: Option<String>,
52}
53
54pub struct App {
55    config_manager: ConfigManager,
56    config: Config,
57}
58
59impl App {
60    pub fn new() -> Result<Self, JourneyError> {
61        let config_manager = ConfigManager::new()?;
62        
63        // Strictly require the config file to exist; surface a clear error otherwise
64        if !config_manager.config_exists() {
65            return Err(JourneyError::Config(
66                "No configuration file found. Please run 'journey --init --path <path>' to create your first vault.".to_string()
67            ));
68        }
69        let config = config_manager.load_config()?;
70        
71        Ok(Self {
72            config_manager,
73            config,
74        })
75    }
76
77    pub fn new_for_init() -> Result<Self, JourneyError> {
78        let config_manager = ConfigManager::new()?;
79        let config = config_manager.load_config()?;
80        
81        Ok(Self {
82            config_manager,
83            config,
84        })
85    }
86
87    pub fn run(&mut self, cli: Cli) -> Result<(), JourneyError> {
88        match cli.command {
89            Some(cmd) => {
90                // Extract the parts we need before moving
91                let vault = cli.vault.clone();
92                let date = cli.date.clone();
93                let relative_date = cli.relative_date;
94                let time = cli.time.clone();
95                let time_format = cli.time_format.clone();
96                self.handle_command_with_args(cmd, vault, date, relative_date, time, time_format)
97            }
98            None => self.handle_default_behavior(&cli),
99        }
100    }
101
102    pub fn run_journeyctl(&mut self, cli: crate::journeyctl::JourneyCtlCli) -> Result<(), JourneyError> {
103        match cli.command {
104            Some(cmd) => self.run_journeyctl_command(cmd),
105            None => Err(JourneyError::Config("No command provided".to_string()))
106        }
107    }
108
109    pub fn run_journeyctl_command(&mut self, command: crate::journeyctl::Commands) -> Result<(), JourneyError> {
110        match command {
111            crate::journeyctl::Commands::Init { path, name, vault_type: _, obsidian } => {
112                self.init_vault(path, name, obsidian)
113            }
114            crate::journeyctl::Commands::List => {
115                self.list_vaults()
116            }
117            crate::journeyctl::Commands::SetDefault { vault_name } => {
118                self.set_default_vault(&vault_name)
119            }
120            crate::journeyctl::Commands::UnsetDefault => {
121                self.unset_default_vault()
122            }
123            crate::journeyctl::Commands::ShowDefault => {
124                self.show_default_vault()
125            }
126            crate::journeyctl::Commands::UnlistVault { vault_name } => {
127                self.unlist_vault(&vault_name)
128            }
129            crate::journeyctl::Commands::Today { vault, verbose } => {
130                self.show_today_file(vault, verbose)
131            }
132        }
133    }
134
135    fn handle_command_with_args(&mut self, cmd: crate::cli::Commands, vault: Option<String>, date: Option<String>, relative_date: Option<i64>, time: Option<String>, time_format: Option<String>) -> Result<(), JourneyError> {
136        match cmd {
137            crate::cli::Commands::Add { content } => {
138                let cli_args = CliArgs { vault, date, relative_date, time, time_format };
139                self.add_note(&content, &cli_args, None)
140            }
141            crate::cli::Commands::List => {
142                let cli_args = CliArgs { vault, date, relative_date, time, time_format };
143                self.list_notes(&cli_args, false, None)
144            }
145            crate::cli::Commands::Edit => {
146                let cli_args = CliArgs { vault, date, relative_date, time, time_format };
147                self.edit_notes(&cli_args)
148            }
149        }
150    }
151
152    fn handle_default_behavior(&mut self, cli: &Cli) -> Result<(), JourneyError> {
153        let cli_args = CliArgs {
154            vault: cli.vault.clone(),
155            date: cli.date.clone(),
156            relative_date: cli.relative_date,
157            time: cli.time.clone(),
158            time_format: cli.time_format.clone(),
159        };
160        
161        if cli.list {
162            self.list_notes(&cli_args, cli.header, cli.category.as_deref())
163        } else if cli.edit {
164            self.edit_notes(&cli_args)
165        } else if cli.stdin {
166            self.handle_stdin_input(&cli_args, cli.category.as_deref())
167        } else if let Some(note) = &cli.add_note {
168            self.add_note(note, &cli_args, cli.category.as_deref())
169        } else if !cli.note_content.is_empty() {
170            // Default behavior: treat note_content as note content
171            let content = cli.note_content.join(" ");
172            self.add_note(&content, &cli_args, cli.category.as_deref())
173        } else {
174            // Default behavior: list today's notes (same as --list)
175            self.list_notes(&cli_args, cli.header, cli.category.as_deref())
176        }
177    }
178
179    pub fn init_vault(&mut self, path: std::path::PathBuf, name: Option<String>, obsidian: bool) -> Result<(), JourneyError> {
180        if obsidian {
181            self.init_obsidian_vault(path, name)
182        } else {
183            self.init_regular_vault(path, name)
184        }
185    }
186
187    fn init_regular_vault(&mut self, path: std::path::PathBuf, name: Option<String>) -> Result<(), JourneyError> {
188        // Create vault directory
189        std::fs::create_dir_all(&path)?;
190
191        // Determine vault name - use provided name or path basename
192        let vault_name = if let Some(name) = name {
193            name
194        } else {
195            match path.file_name().and_then(|n| n.to_str()) {
196                Some(s) if !s.is_empty() => s.to_string(),
197                _ => return Err(JourneyError::Config("Invalid path: cannot extract basename".to_string())),
198            }
199        };
200
201        // Get system locale
202        let locale = self.get_system_locale();
203
204        // Create vault config
205        let vault_config = crate::config::VaultConfig {
206            name: vault_name.clone(),
207            path,
208            locale,
209            phrases: std::collections::HashMap::new(),
210            section_header: None,
211            section_header_work: None,
212            section_header_personal: None,
213            section_header_health: None,
214            section_header_meetings: None,
215            table_headers: None,
216            date_format: None,
217            template_file: None,
218            file_path_format: None,
219            list_type: None,
220            section_name: None,
221            weekly_format: None,
222            monthly_format: None,
223            quarterly_format: None,
224            yearly_format: None,
225            note_format: None,
226        };
227
228        // Add to config and save
229        self.config.add_vault(vault_config);
230        self.config_manager.save_config(&self.config)?;
231
232        println!("Vault '{}' initialized successfully!", vault_name);
233        Ok(())
234    }
235
236    fn init_obsidian_vault(&mut self, path: std::path::PathBuf, name: Option<String>) -> Result<(), JourneyError> {
237        // Validate that the path exists and is an Obsidian vault
238        if !path.exists() {
239            return Err(JourneyError::Config(format!("Obsidian vault path does not exist: {}", path.display())));
240        }
241
242        // Check if it's a valid Obsidian vault by looking for .obsidian directory
243        let obsidian_dir = path.join(".obsidian");
244        if !obsidian_dir.exists() {
245            return Err(JourneyError::Config(format!("Path is not a valid Obsidian vault (missing .obsidian directory): {}", path.display())));
246        }
247
248        // Determine vault name - use provided name or path basename
249        let vault_name = if let Some(name) = name {
250            name
251        } else {
252            path.file_name()
253                .and_then(|n| n.to_str())
254                .ok_or_else(|| JourneyError::Config("Invalid path: cannot extract basename".to_string()))?
255                .to_string()
256        };
257
258        // Check for required Obsidian plugins
259        let plugin_configs = self.check_obsidian_plugins(&obsidian_dir)?;
260
261        // Print plugin detection results
262        println!("🔍 Detected Obsidian plugins:");
263        if let Some(_) = &plugin_configs.daily_notes {
264            println!("  ✅ Daily Notes (core plugin) - enabled");
265        } else {
266            println!("  ❌ Daily Notes (core plugin) - not enabled");
267        }
268
269        if let Some(_) = &plugin_configs.periodic_notes {
270            println!("  ✅ Periodic Notes plugin - enabled");
271        } else {
272            println!("  ❌ Periodic Notes plugin - not enabled");
273        }
274
275        if !plugin_configs.journals.is_empty() {
276            println!("  ✅ Journals plugin - enabled ({} journal(s) configured)", plugin_configs.journals.len());
277            for journal in &plugin_configs.journals {
278                println!("    📝 Journal: '{}'", journal.name);
279            }
280        } else {
281            println!("  ❌ Journals plugin - not configured");
282        }
283
284        // Get system locale
285        let locale = self.get_system_locale();
286
287        // Create vault config with Obsidian-specific settings
288        let mut vault_config = crate::config::VaultConfig {
289            name: vault_name.clone(),
290            path,
291            locale,
292            phrases: std::collections::HashMap::new(),
293            section_header: None,
294            section_header_work: None,
295            section_header_personal: None,
296            section_header_health: None,
297            section_header_meetings: None,
298            table_headers: None,
299            date_format: None,
300            template_file: None,
301            file_path_format: None,
302            list_type: None,
303            section_name: None,
304            weekly_format: None,
305            monthly_format: None,
306            quarterly_format: None,
307            yearly_format: None,
308            note_format: None,
309        };
310
311        // Apply Obsidian plugin configurations (excluding journals for now)
312        self.apply_obsidian_config(&mut vault_config, &plugin_configs)?;
313
314        // Create vaults for each journal
315        let mut vault_count = 0;
316        for journal in &plugin_configs.journals {
317            let mut journal_vault_config = vault_config.clone();
318            
319            // Set journal-specific name
320            journal_vault_config.name = format!("{}-{}", vault_name, journal.name);
321            
322            // Apply journal-specific configuration
323            
324            // Add journal vault to config
325            self.config.add_vault(journal_vault_config);
326            vault_count += 1;
327        }
328
329        // If no journals found, create a single vault with the base configuration
330        if vault_count == 0 {
331            self.config.add_vault(vault_config);
332            vault_count = 1;
333        }
334
335        // Save configuration
336        self.config_manager.save_config(&self.config)?;
337
338        println!();
339        println!("🎉 Obsidian vault '{}' initialized successfully with {} vault(s)!", vault_name, vault_count);
340        
341        // Print important configuration reminder
342        println!();
343        println!("⚠️  IMPORTANT: Please review your configuration and add the missing essential information:");
344        println!("   • section_header: The default section header in the daily note where journey will put your notes (e.g., '## Todays notes')");
345        println!("   • list_type: The format for your notes ('bullet' or 'table')");
346        println!("   • file_path_format: template string journey uses to determine the location of the daily note");
347        println!("   • It can contain variables like {{year}}, {{month}}, {{day}}, {{weekday}}, {{weekday_short}}, {{month_name}}, {{month_short}}, etc");
348        println!("   • The variables are replaced with the actual values when the note is added");
349        println!();
350        println!();
351        println!("   You can edit the configuration file at: {}", self.config_manager.config_path.display());
352        println!();
353        
354        // Print configuration summary
355        if let Some(daily_notes) = &plugin_configs.daily_notes {
356            println!("📅 Daily Notes: format='{}', folder='{}'", daily_notes.format, daily_notes.folder);
357        }
358        
359        if let Some(periodic_notes) = &plugin_configs.periodic_notes {
360            println!("📊 Periodic Notes: enabled={}", periodic_notes.enabled);
361        }
362        
363        if !plugin_configs.journals.is_empty() {
364            println!("📚 Journals: {} configured", plugin_configs.journals.len());
365        }
366        
367        Ok(())
368    }
369
370    fn check_obsidian_plugins(&self, obsidian_dir: &std::path::Path) -> Result<ObsidianPluginConfigs, JourneyError> {
371        let mut configs = ObsidianPluginConfigs::new();
372
373        // Check for Daily Notes core plugin
374        if let Ok(daily_notes_config) = self.check_daily_notes_plugin(obsidian_dir) {
375            configs.daily_notes = Some(daily_notes_config);
376        }
377
378        // Check for Periodic Notes plugin
379        if let Ok(periodic_notes_config) = self.check_periodic_notes_plugin(obsidian_dir) {
380            configs.periodic_notes = Some(periodic_notes_config);
381        }
382
383        // Check for Journals plugin
384        if let Ok(journals_configs) = self.check_journals_plugin(obsidian_dir) {
385            configs.journals = journals_configs;
386        }
387
388        Ok(configs)
389    }
390
391    fn check_daily_notes_plugin(&self, obsidian_dir: &std::path::Path) -> Result<DailyNotesConfig, JourneyError> {
392        let app_json_path = obsidian_dir.join("app.json");
393        if !app_json_path.exists() {
394            return Err(JourneyError::Config("Obsidian app.json not found".to_string()));
395        }
396
397        let app_json_content = std::fs::read_to_string(&app_json_path)?;
398        let app_config: serde_json::Value = serde_json::from_str(&app_json_content)?;
399
400        // Check if Daily Notes is enabled
401        if let Some(daily_notes) = app_config.get("dailyNotes") {
402            if let Some(enabled) = daily_notes.get("enabled") {
403                if enabled.as_bool().unwrap_or(false) {
404                    let format = daily_notes.get("format").and_then(|v| v.as_str()).unwrap_or("YYYY-MM-DD").to_string();
405                    let folder = daily_notes.get("folder").and_then(|v| v.as_str()).unwrap_or("/").to_string();
406                    let template = daily_notes.get("template").and_then(|v| v.as_str()).map(|s| s.to_string());
407
408                    return Ok(DailyNotesConfig {
409                        enabled: true,
410                        format,
411                        folder,
412                        template,
413                    });
414                }
415            } else {
416                // If enabled field is missing, assume plugin is disabled
417                return Err(JourneyError::Config("Daily Notes plugin not enabled".to_string()));
418            }
419        }
420
421        Err(JourneyError::Config("Daily Notes plugin not enabled".to_string()))
422    }
423
424    fn check_periodic_notes_plugin(&self, obsidian_dir: &std::path::Path) -> Result<PeriodicNotesConfig, JourneyError> {
425        let plugins_dir = obsidian_dir.join("plugins");
426        let periodic_notes_dir = plugins_dir.join("periodic-notes");
427        
428        if !periodic_notes_dir.exists() {
429            return Err(JourneyError::Config("Periodic Notes plugin not installed".to_string()));
430        }
431
432        let data_json_path = periodic_notes_dir.join("data.json");
433        if !data_json_path.exists() {
434            return Err(JourneyError::Config("Periodic Notes plugin data not found".to_string()));
435        }
436
437        let data_content = std::fs::read_to_string(&data_json_path)?;
438        let plugin_data: serde_json::Value = serde_json::from_str(&data_content)?;
439
440        // Check if plugin is enabled
441        if let Some(enabled) = plugin_data.get("enabled") {
442            if !enabled.as_bool().unwrap_or(false) {
443                return Err(JourneyError::Config("Periodic Notes plugin not enabled".to_string()));
444            }
445        } else {
446            // If enabled field is missing, assume plugin is disabled
447            return Err(JourneyError::Config("Periodic Notes plugin not enabled".to_string()));
448        }
449
450        // Extract configuration
451        Ok(PeriodicNotesConfig {
452            enabled: true,
453        })
454    }
455
456    fn check_journals_plugin(&self, obsidian_dir: &std::path::Path) -> Result<Vec<JournalsConfig>, JourneyError> {
457        let plugins_dir = obsidian_dir.join("plugins");
458        let journals_dir = plugins_dir.join("journals");
459        
460        if !journals_dir.exists() {
461            return Err(JourneyError::Config("Journals plugin not installed".to_string()));
462        }
463
464        let data_json_path = journals_dir.join("data.json");
465        if !data_json_path.exists() {
466            return Err(JourneyError::Config("Journals plugin data not found".to_string()));
467        }
468
469        let data_content = std::fs::read_to_string(&data_json_path)?;
470        let plugin_data: serde_json::Value = serde_json::from_str(&data_content)?;
471
472        // Check if plugin is enabled (Journals plugin doesn't have a simple enabled field)
473        // We'll consider it enabled if it has journals configured
474        if !plugin_data.get("journals").is_some() {
475            return Err(JourneyError::Config("Journals plugin not configured".to_string()));
476        }
477
478        // Extract configuration from all journal entries
479        let journals = plugin_data.get("journals")
480            .ok_or_else(|| JourneyError::Config("No journals configured".to_string()))?;
481        
482        let mut journal_configs = Vec::new();
483        
484        if let Some(journals_obj) = journals.as_object() {
485            for (journal_name, _journal_data) in journals_obj {
486                // Extract configuration from each journal entry
487                journal_configs.push(JournalsConfig {
488                    name: journal_name.clone(),
489                });
490            }
491        } else {
492            return Err(JourneyError::Config("Invalid journals configuration".to_string()));
493        }
494
495        if journal_configs.is_empty() {
496            return Err(JourneyError::Config("No journal entries found".to_string()));
497        }
498
499        Ok(journal_configs)
500    }
501
502    fn apply_obsidian_config(&mut self, vault_config: &mut crate::config::VaultConfig, plugin_configs: &ObsidianPluginConfigs) -> Result<(), JourneyError> {
503        // Apply Daily Notes configuration
504        if let Some(daily_notes) = &plugin_configs.daily_notes {
505            if daily_notes.enabled {
506                vault_config.date_format = Some(daily_notes.format.clone());
507                vault_config.file_path_format = Some(daily_notes.folder.clone());
508                
509                if let Some(template) = &daily_notes.template {
510                    vault_config.template_file = Some(template.clone());
511                }
512            }
513        }
514
515        // Apply Periodic Notes configuration
516        if let Some(periodic_notes) = &plugin_configs.periodic_notes {
517            if periodic_notes.enabled {
518                // Periodic notes configuration is enabled
519            }
520        }
521
522        // Apply Journals configuration - this will be handled separately to create multiple vaults
523
524        Ok(())
525    }
526
527    fn add_note(&mut self, content: &str, cli: &CliArgs, category: Option<&str>) -> Result<(), JourneyError> {
528        let vault = self.get_vault(cli.vault.as_deref())?;
529        let date = self.parse_date(cli)?;
530        let time = self.parse_time(cli)?;
531        
532        let timestamp = if let Some(time) = time {
533            vault.date_handler.combine_date_time(date, time)
534        } else {
535            // Use current time with the specified date (or current date if not specified)
536            let current_time = vault.date_handler.get_current_datetime().time();
537            vault.date_handler.combine_date_time(date, current_time)
538        };
539
540        vault.add_note_with_category(content, Some(timestamp), category)?;
541        println!("Note added successfully!");
542        Ok(())
543    }
544
545    fn list_notes(&self, cli: &CliArgs, header: bool, category: Option<&str>) -> Result<(), JourneyError> {
546        let vault = self.get_vault(cli.vault.as_deref())?;
547        let date = self.parse_date(cli)?;
548        
549        let notes = vault.list_notes_with_category(date, category)?;
550        
551        if notes.is_empty() {
552            println!("No notes found for {}", vault.date_handler.format_date(date));
553        } else {
554            let has_table_format = notes.iter().any(|note| note.trim().starts_with("|"));
555
556            // Suppress "Notes for" message when in table mode
557            if !header && !has_table_format {
558                println!("Notes for {}:", vault.date_handler.format_date(date));
559            }
560
561            // If header flag is set and we have table format notes, include table headers
562            if header && has_table_format {
563                let (time_header, content_header) = vault.get_table_headers();
564                println!("| {} | {} |", time_header, content_header);
565                println!("|------|----------|");
566            }
567            
568            for note in notes {
569                println!("{}", note);
570            }
571        }
572        
573        Ok(())
574    }
575
576    fn edit_notes(&self, cli: &CliArgs) -> Result<(), JourneyError> {
577        let vault = self.get_vault(cli.vault.as_deref())?;
578        let date = self.parse_date(cli)?;
579        let note_path = vault.get_editor_path(date);
580
581        // Get editor from environment
582        let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
583        
584        // Open editor
585        let status = Command::new(&editor)
586            .arg(&note_path)
587            .status()
588            .map_err(|_| JourneyError::EditorNotFound(editor))?;
589
590        if !status.success() {
591            eprintln!("Editor exited with non-zero status");
592        }
593
594        Ok(())
595    }
596
597
598    pub fn get_vault(&self, vault_name: Option<&str>) -> Result<Vault, JourneyError> {
599        // Explicitly error if no vaults configured
600        if self.config.vaults.is_empty() {
601            return Err(JourneyError::VaultNotFound("No vaults configured. Use 'journey init' to create one.".to_string()));
602        }
603
604        let vault_config = if let Some(name) = vault_name {
605            // User explicitly specified a vault
606            self.config.get_vault(name)
607                .ok_or_else(|| JourneyError::VaultNotFound(name.to_string()))?
608        } else {
609            // No vault specified - use default vault or smart selection
610            if let Some(default_name) = &self.config.default_vault {
611                // Use the explicitly set default vault
612                self.config.get_vault(default_name)
613                    .ok_or_else(|| JourneyError::VaultNotFound(format!("Default vault '{}' not found", default_name)))?
614            } else if self.config.vaults.len() == 1 {
615                // Only one vault exists - use it automatically
616                self.config.vaults.values().next()
617                    .ok_or_else(|| JourneyError::VaultNotFound("No vaults configured".to_string()))?
618            } else if self.config.vaults.len() > 1 {
619                // Multiple vaults exist - need to specify which one or set a default
620                return Err(JourneyError::VaultNotFound(
621                    format!("Multiple vaults available: {}. Please specify --vault or set a default vault", 
622                        self.config.vaults.keys().map(|s| s.as_str()).collect::<Vec<_>>().join(", "))
623                ));
624            } else {
625                // No vaults configured
626                return Err(JourneyError::VaultNotFound("No vaults configured. Use 'journey init' to create one.".to_string()));
627            }
628        };
629
630        Ok(Vault::new(vault_config.clone()))
631    }
632
633    pub fn parse_date(&self, cli: &CliArgs) -> Result<NaiveDate, JourneyError> {
634        if let Some(date_str) = &cli.date {
635            let vault = self.get_vault(cli.vault.as_deref())?;
636            vault.date_handler.parse_date_with_format_override(date_str, vault.config.date_format.as_deref())
637        } else if let Some(days_ago) = cli.relative_date {
638            let vault = self.get_vault(cli.vault.as_deref())?;
639            Ok(vault.date_handler.parse_relative_date(days_ago))
640        } else {
641            Ok(Local::now().date_naive())
642        }
643    }
644
645    fn parse_time(&self, cli: &CliArgs) -> Result<Option<chrono::NaiveTime>, JourneyError> {
646        if let Some(time_str) = &cli.time {
647            let vault = self.get_vault(cli.vault.as_deref())?;
648            Ok(Some(vault.date_handler.parse_time_with_format_override(time_str, cli.time_format.as_deref())?))
649        } else {
650            Ok(None)
651        }
652    }
653
654    fn handle_stdin_input(&mut self, cli: &CliArgs, category: Option<&str>) -> Result<(), JourneyError> {
655        use std::io::{self, BufRead};
656        
657        let stdin = io::stdin();
658        let lines = stdin.lock().lines();
659        
660        let mut note_count = 0;
661        
662        for line in lines {
663            let line = line?;
664            let trimmed = line.trim();
665            
666            // Skip empty lines
667            if trimmed.is_empty() {
668                continue;
669            }
670            
671            // Add each line as a separate note
672            self.add_note(trimmed, cli, category)?;
673            note_count += 1;
674        }
675        
676        if note_count > 0 {
677            println!("Added {} notes from stdin", note_count);
678        } else {
679            println!("No content received from stdin");
680        }
681        
682        Ok(())
683    }
684
685    fn get_system_locale(&self) -> String {
686        // Try to get locale from environment
687        env::var("LANG")
688            .or_else(|_| env::var("LC_ALL"))
689            .unwrap_or_else(|_| "en-US".to_string())
690    }
691
692    fn set_default_vault(&mut self, vault_name: &str) -> Result<(), JourneyError> {
693        match self.config.set_default_vault(vault_name) {
694            Ok(()) => {
695                self.config_manager.save_config(&self.config)?;
696                println!("Default vault set to '{}'", vault_name);
697                Ok(())
698            }
699            Err(error) => Err(JourneyError::Config(error))
700        }
701    }
702
703    fn unset_default_vault(&mut self) -> Result<(), JourneyError> {
704        self.config.unset_default_vault();
705        self.config_manager.save_config(&self.config)?;
706        println!("Default vault unset");
707        Ok(())
708    }
709
710    fn show_default_vault(&self) -> Result<(), JourneyError> {
711        if let Some(default_name) = &self.config.default_vault {
712            if let Some(vault) = self.config.get_vault(default_name) {
713                println!("Default vault: {} ({})", default_name, vault.path.display());
714            } else {
715                println!("Default vault '{}' is set but vault not found", default_name);
716            }
717        } else {
718            println!("No default vault set");
719            if self.config.vaults.len() > 1 {
720                println!("Available vaults: {}", 
721                    self.config.vaults.keys().map(|s| s.as_str()).collect::<Vec<_>>().join(", "));
722            }
723        }
724        Ok(())
725    }
726
727    pub fn unlist_vault(&mut self, vault_name: &str) -> Result<(), JourneyError> {
728        // Check if vault exists
729        if !self.config.vaults.contains_key(vault_name) {
730            return Err(JourneyError::VaultNotFound(format!("Vault '{}' not found", vault_name)));
731        }
732
733        // Remove vault from config
734        self.config.remove_vault(vault_name)
735            .map_err(|e| JourneyError::Config(e))?;
736
737        // Save updated config
738        self.config_manager.save_config(&self.config)?;
739
740        println!("Vault '{}' unlisted successfully!", vault_name);
741        Ok(())
742    }
743
744    // Test helper methods
745    #[doc(hidden)]
746    pub fn get_config(&self) -> &Config {
747        &self.config
748    }
749
750    fn list_vaults(&self) -> Result<(), JourneyError> {
751        if self.config.vaults.is_empty() {
752            println!("No vaults configured.");
753            println!("Use 'journeyctl init --path <path> --name <name>' to create your first vault.");
754            return Ok(());
755        }
756
757        println!("Configured vaults:");
758        println!();
759
760        for (name, vault) in &self.config.vaults {
761            let is_default = self.config.default_vault.as_ref() == Some(name);
762            let default_marker = if is_default { " (default)" } else { "" };
763            
764            println!("  {}: {}{}", name, vault.path.display(), default_marker);
765            
766            // Show additional vault information
767            if let Some(section_header) = &vault.section_header {
768                println!("    Section: {}", section_header);
769            }
770            if let Some(template_file) = &vault.template_file {
771                println!("    Template: {}", template_file);
772            }
773            if let Some(file_path_format) = &vault.file_path_format {
774                println!("    Path format: {}", file_path_format);
775            }
776            if !vault.phrases.is_empty() {
777                println!("    Phrases: {} configured", vault.phrases.len());
778            }
779            println!();
780        }
781
782        if let Some(default_name) = &self.config.default_vault {
783            println!("Default vault: {}", default_name);
784        } else {
785            println!("No default vault set");
786        }
787
788        Ok(())
789    }
790
791    fn show_today_file(&self, vault_name: Option<String>, verbose: bool) -> Result<(), JourneyError> {
792        let vault = self.get_vault(vault_name.as_deref())?;
793        let today = vault.date_handler.get_current_datetime().date_naive();
794        let file_path = vault.get_note_path(today);
795        
796        if verbose {
797            println!("Today's file location: {}", file_path.display());
798            
799            if file_path.exists() {
800                println!("File exists: Yes");
801            } else {
802                println!("File exists: No");
803            }
804        } else {
805            println!("{}", file_path.display());
806        }
807        
808        Ok(())
809    }
810}