Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/IPC/DevLog/
AppDataPrefix.rs

1//! Resolve the Tauri app-data prefix for THIS profile so logs
2//! and aliasing pick the right `~/Library/Application Support/
3//! land.editor.*.mountain` directory. The detection walks the
4//! Application Support tree, prefers a strict suffix match
5//! against the binary signature, falls back to the first
6//! `*.mountain` candidate so a mismatch still produces a
7//! usable path.
8
9use std::sync::{Mutex, OnceLock};
10
11// Two-phase resolution:
12// • `RESOLVED` is set permanently once we find a real prefix.
13// • `FAILED_ONCE` records whether the first attempt returned None so
14//   subsequent writes can retry after Tauri has created the directory,
15//   rather than caching None forever and routing all logs to /tmp.
16static RESOLVED:OnceLock<String> = OnceLock::new();
17
18static RETRY:Mutex<bool> = Mutex::new(true);
19
20pub fn Fn() -> Option<&'static str> {
21	if let Some(S) = RESOLVED.get() {
22		return Some(S.as_str());
23	}
24
25	// Fast-path: guard without taking the mutex if we already have a result.
26	let Ok(mut Guard) = RETRY.try_lock() else {
27		return None;
28	};
29
30	if !*Guard {
31		return None;
32	}
33
34	if let Some(Prefix) = DetectAppDataPrefix() {
35		// RESOLVED may already be set by a concurrent caller - that's fine.
36		let _ = RESOLVED.set(Prefix);
37
38		*Guard = false;
39		return RESOLVED.get().map(String::as_str);
40	}
41
42	// Not found yet - leave RETRY=true so the next write retries.
43	None
44}
45
46fn BinarySignature() -> String {
47	let PackageName = env!("CARGO_PKG_NAME");
48
49	let Segments:Vec<&str> = PackageName.split('_').collect();
50
51	let Take = Segments.len().min(4);
52
53	let Start = Segments.len().saturating_sub(Take);
54
55	Segments[Start..]
56		.iter()
57		.flat_map(|Segment| SplitPascalCaseIntoWords(Segment))
58		.collect::<Vec<String>>()
59		.join(".")
60		.to_ascii_lowercase()
61}
62
63fn SplitPascalCaseIntoWords(Segment:&str) -> Vec<String> {
64	let mut Words:Vec<String> = Vec::new();
65
66	let mut Current = String::new();
67
68	let mut PrevWasUpper = false;
69
70	let mut PrevWasDigit = false;
71
72	for Ch in Segment.chars() {
73		let IsUpper = Ch.is_ascii_uppercase();
74
75		let IsDigit = Ch.is_ascii_digit();
76
77		let NeedBreak =
78			!Current.is_empty() && ((IsUpper && !PrevWasUpper) || (IsDigit != PrevWasDigit && !Current.is_empty()));
79
80		if NeedBreak {
81			Words.push(std::mem::take(&mut Current));
82		}
83
84		Current.push(Ch);
85
86		PrevWasUpper = IsUpper;
87
88		PrevWasDigit = IsDigit;
89	}
90
91	if !Current.is_empty() {
92		Words.push(Current);
93	}
94
95	Words.into_iter().filter(|Word| !Word.is_empty()).collect()
96}
97
98fn DetectAppDataPrefix() -> Option<String> {
99	let Home = std::env::var("HOME").ok()?;
100
101	let Base = format!("{}/Library/Application Support", Home);
102
103	let Signature = BinarySignature();
104
105	let mut FirstMatchingMountain:Option<String> = None;
106
107	if let Ok(Entries) = std::fs::read_dir(&Base) {
108		for Entry in Entries.flatten() {
109			let Name = Entry.file_name();
110
111			let Name = Name.to_string_lossy().into_owned();
112
113			if !Name.starts_with("land.editor.") || !Name.contains("mountain") {
114				continue;
115			}
116
117			if Name.ends_with(&Signature) {
118				return Some(format!("{}/{}", Base, Name));
119			}
120
121			if FirstMatchingMountain.is_none() {
122				FirstMatchingMountain = Some(format!("{}/{}", Base, Name));
123			}
124		}
125	}
126
127	FirstMatchingMountain
128}