Skip to main content

Mountain/IPC/DevLog/
AppDataPrefix.rs

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