Skip to main content

Mountain/Binary/Build/
WindowBuild.rs

1//! # Window Build Module
2//!
3//! Creates and configures the main application window.
4
5use tauri::{App, WebviewUrl, WebviewWindow, WebviewWindowBuilder, Wry};
6
7use crate::IPC::WindServiceHandlers::Utilities::RecentlyOpened::Read::Fn as ReadRecentlyOpened;
8
9/// Creates and configures the main application window.
10///
11/// # Arguments
12///
13/// * `Application` - The Tauri application instance
14/// * `LocalhostUrl` - The localhost URL for the webview content
15///
16/// # Returns
17///
18/// A configured `WebviewWindow<Wry>` instance.
19///
20/// # Platform-Specific Behavior
21///
22/// - **macOS**: `TitleBarStyle::Overlay` + `hidden_title(true)` keeps the
23///   traffic-light buttons at the top-left but hides the native title bar
24///   strip, so VS Code's custom titlebar (which has `-webkit-app-region: drag`
25///   baked into its CSS) lights up the entire top row as a drag region. The
26///   previous `maximized(true)` was the direct cause of "can't drag the editor
27///   around" - maximized macOS windows are pinned to the screen and refuse all
28///   drag events, regardless of `app-region` CSS.
29/// - **Windows / Linux**: `decorations(false)` keeps the window chrome-less so
30///   the workbench draws its own. We still start `resizable(true)` so the
31///   window can be moved by the drag region.
32/// - **Debug builds**: DevTools auto-open.
33pub fn WindowBuild(Application:&mut App, LocalhostUrl:String) -> tauri::WebviewWindow<Wry> {
34	// Restore the most-recently-opened folder so the webview boots
35	// directly into the workspace. Without this, every launch lands
36	// on the Welcome tab, the user clicks "Open Folder", and the
37	// `pickFolderAndOpen` handler fires `Window.navigate()` - a hard
38	// reload that wipes workbench state mid-initialisation and is
39	// the direct cause of the purple splash flash, stuttering paint,
40	// empty `@builtin` sidebar, and broken keybindings (every layer
41	// has to boot twice and the second pass often loses references
42	// to the first). Using `?folder=...` in the initial URL skips
43	// that destructive round-trip.
44	let InitialUrl = BuildInitialUrl(&LocalhostUrl);
45
46	let WindowUrl = WebviewUrl::External(InitialUrl.parse().expect("FATAL: Failed to parse initial webview URL"));
47
48	// Configure window builder with base settings.
49	//
50	// `visible(false)` is the hidden-until-ready pattern. Tauri's default
51	// is to show the window the instant it's built, which paints the native
52	// chrome + Base.astro's `#1e1e1e` inline background + VS Code theme CSS
53	// + workbench DOM in four separate repaints over the first ~200 ms -
54	// observed as the "purple/dark flash" and panel-pop flicker.
55	//
56	// Mountain shows the window explicitly when the frontend's
57	// `lifecycle:advancePhase(3)` (Restored) arrives, which fires after
58	// `.monaco-workbench` is attached and the first frame is ready. A 3 s
59	// safety timer in `AppLifecycle` guarantees the window appears even if
60	// Sky crashes before signalling phase 3.
61	// Diagnostic initialization script. Runs at `document_start`, BEFORE any
62	// page <script> tag executes. Captures the state of Tauri's IPC bridge
63	// at the earliest possible point so a missing injection (forked Tauri
64	// runtime, mis-scoped capability, race condition) is detectable from
65	// the captured DevTools console + `window.__MOUNTAIN_TAURI_DIAG`
66	// snapshot - without depending on the rest of the bundle loading.
67	let TauriDiagnosticScript = r#"(function() {
68		if (window.__MOUNTAIN_TAURI_DIAG) { return; }
69
70		const Stamp = (Reason) => ({
71			at: Date.now(),
72			reason: Reason,
73			hasTAURI_INTERNALS: typeof window.__TAURI_INTERNALS__ === 'object' && window.__TAURI_INTERNALS__ !== null,
74			invokeOnInternals: typeof window.__TAURI_INTERNALS__?.invoke === 'function',
75			hasTAURI: typeof window.__TAURI__ === 'object' && window.__TAURI__ !== null,
76			invokeOnTauriCore: typeof window.__TAURI__?.core?.invoke === 'function',
77			invokeOnTauriDirect: typeof window.__TAURI__?.invoke === 'function',
78			hasIPCPostMessage: typeof window.ipc?.postMessage === 'function',
79			origin: window.location.origin,
80			url: window.location.href,
81		});
82
83		window.__MOUNTAIN_TAURI_DIAG = { initial: Stamp('initialization_script') };
84
85		try {
86
87			window.addEventListener('DOMContentLoaded', () => {
88				window.__MOUNTAIN_TAURI_DIAG.dom_content_loaded = Stamp('DOMContentLoaded');
89			});
90
91			window.addEventListener('load', () => {
92				window.__MOUNTAIN_TAURI_DIAG.load = Stamp('load');
93			});
94		} catch {}
95	})();"#;
96
97	// Capture-phase keydown listener that fires BEFORE WKWebView dispatches
98	// undo:/redo: via doCommandBySelector:. Even without a native menu entry
99	// WKWebView binds Cmd+Z to NSUndoManager through its internal responder
100	// chain. preventDefault() at the capture phase stops that dispatch so
101	// Monaco's own keydown handler handles the undo stack exclusively.
102	// Cmd+Y (redo) is included for completeness. Monaco's handlers still fire
103	// and process the keystroke normally - only the native NSUndoManager path
104	// is blocked.
105	let WkUndoSuppressScript = r#"(function() {
106		document.addEventListener('keydown', function(e) {
107			if (e.metaKey && !e.ctrlKey && !e.altKey) {
108				if (e.key === 'z' || e.key === 'Z') { e.preventDefault(); }
109
110				if (e.key === 'y' || e.key === 'Y') { e.preventDefault(); }
111			}
112		}, true);
113	})();"#;
114
115	let mut WindowBuilder = WebviewWindowBuilder::new(Application, "main", WindowUrl)
116		.use_https_scheme(false)
117		.initialization_script(TauriDiagnosticScript)
118		.initialization_script(WkUndoSuppressScript)
119		.zoom_hotkeys_enabled(true)
120		.browser_extensions_enabled(false)
121
122		// macOS first-responder: by default WKWebView swallows the
123		// first click on an unfocused window as a "make me key"
124		// no-op and the click never reaches the inner content. With
125		// `Inspect=1` running DevTools alongside the main window
126		// every switch-back to the editor needed two clicks - first
127		// to focus the NSWindow, second to actually focus the
128		// Monaco textarea - which the user reports as "I clicked,
129		// I'm typing, nothing's happening". `accept_first_mouse`
130		// flips the responder chain so the first click already
131		// reaches WKWebView's content and the textarea picks up
132		// keyboard input immediately.
133		.accept_first_mouse(true)
134		.title("Mountain")
135		.resizable(true)
136		.inner_size(1400.0, 900.0)
137		.shadow(true)
138		.visible(false);
139
140	#[cfg(target_os = "macos")]
141	{
142		// Overlay style lets VS Code's custom titlebar paint behind the
143		// traffic-light buttons. `hidden_title(true)` suppresses the OS
144		// title text so it doesn't collide with the workbench menubar.
145		// `decorations(true)` is REQUIRED for the traffic lights to
146		// render - turning decorations off also removes the buttons and
147		// breaks the native drag + resize handles entirely on macOS.
148		// `content_protected(true)` tells macOS to reserve a safe zone
149		// around the traffic-light cluster so WKWebView content doesn't
150		// render underneath them, preventing click-through and visual
151		// overlap with the workbench titlebar.
152		WindowBuilder = WindowBuilder
153			.title_bar_style(tauri::TitleBarStyle::Overlay)
154			.hidden_title(true)
155			.decorations(true)
156			.content_protected(true);
157	}
158
159	#[cfg(any(target_os = "windows", target_os = "linux"))]
160	{
161		WindowBuilder = WindowBuilder.decorations(false);
162	}
163
164	// Enable WKWebView inspection when InDebug mode + Inspect=1.
165	// This sets WKWebView.isInspectable via Wry's devtools flag so that
166	// external inspectors (Safari/Web Inspector) can attach.
167	#[cfg(debug_assertions)]
168	{
169		let enable_debugtools = std::env::var("Inspect").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
170
171		if enable_debugtools {
172			WindowBuilder = WindowBuilder.devtools(true);
173		}
174	}
175
176	#[cfg(debug_assertions)]
177	{
178		let enable_debug_server = std::env::var("DebugServer").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
179
180		if enable_debug_server {
181			WindowBuilder = WindowBuilder.on_page_load(|window, _payload| {
182				let _ = window.eval(
183					r#"(function() {
184					if (!window.__MOUNTAIN_DEBUG_CONSOLE) {
185						window.__MOUNTAIN_DEBUG_CONSOLE = [];
186
187						const origLog = console.log;
188
189						const origError = console.error;
190
191						const origWarn = console.warn;
192
193						const origInfo = console.info;
194
195						const origDebug = console.debug;
196
197						function pushLog(level, args) {
198							const argStrings = args.map(arg => {
199								if (typeof arg === 'object') {
200									try { return JSON.stringify(arg); } catch { return String(arg); }
201								} else {
202									return String(arg);
203								}
204							});
205
206							window.__MOUNTAIN_DEBUG_CONSOLE.push({ level, messages: argStrings, timestamp: Date.now() });
207
208							if (window.__MOUNTAIN_DEBUG_CONSOLE.length > 1000) {
209								window.__MOUNTAIN_DEBUG_CONSOLE = window.__MOUNTAIN_DEBUG_CONSOLE.slice(-1000);
210							}
211						}
212
213						console.log = function(...args) { pushLog('log', args); origLog.apply(console, args); };
214
215						console.error = function(...args) { pushLog('error', args); origError.apply(console, args); };
216
217						console.warn = function(...args) { pushLog('warn', args); origWarn.apply(console, args); };
218
219						console.info = function(...args) { pushLog('info', args); origInfo.apply(console, args); };
220
221						console.debug = function(...args) { pushLog('debug', args); origDebug.apply(console, args); };
222					}
223				})()"#,
224				);
225			});
226		}
227	}
228
229	// Build the main window
230	let MainWindow = WindowBuilder.build().expect("FATAL: Main window build failed");
231
232	// DevTools auto-open lives in `Binary/Main/AppLifecycle.rs:174`
233	// (gated on `cfg(debug_assertions)`, with a `[Window] Debug build:
234	// opening DevTools.` log line). Calling `open_devtools()` here as
235	// well opened a SECOND DevTools window on every debug launch -
236	// reported as "two DevTools" after the last rebuild. Single-source
237	// the call to AppLifecycle so the log line and the window match.
238
239	MainWindow
240}
241
242/// Build the initial webview URL, optionally appending `?folder=<path>`
243/// when `~/.fiddee/workspaces/RecentlyOpened.json` has an entry for the
244/// previous session's workspace. Falls back to plain `index.html` if
245/// the file is missing, malformed, or has no resolvable path.
246///
247/// The returned string is already URL-encoded and safe to feed to
248/// `WebviewUrl::External`.
249fn BuildInitialUrl(LocalhostUrl:&str) -> String {
250	let Base = format!("{}/index.html", LocalhostUrl);
251
252	let Recent = match ReadRecentlyOpened() {
253		Ok(Value) => Value,
254
255		Err(_) => return Base,
256	};
257
258	let Workspaces = match Recent.get("workspaces").and_then(|V| V.as_array()) {
259		Some(Array) if !Array.is_empty() => Array,
260
261		_ => return Base,
262	};
263
264	// VS Code's Recently-Opened record can store the folder under a few
265	// different shapes depending on whether the entry came from the
266	// extension host, the workbench, or a `$deltaWorkspaceFolders`
267	// broadcast. Probe them in the same priority order the workbench
268	// itself uses in `getRecentlyOpenedWorkspaces`.
269	let Probe = |Entry:&serde_json::Value| -> Option<String> {
270		// Mountain's own writer emits `{ uri: "file://…", label }` (see
271		// `RecentlyOpened.json` on a freshly closed window). VS Code's
272		// historical `folderUri` / `workspace.configPath` shapes are kept
273		// as fallbacks so imported profiles and third-party writers keep
274		// working.
275		if let Some(Uri) = Entry.get("uri").and_then(|V| V.as_str()) {
276			return Some(Uri.to_string());
277		}
278
279		if let Some(Uri) = Entry.get("folderUri").and_then(|V| V.as_str()) {
280			return Some(Uri.to_string());
281		}
282
283		if let Some(Path) = Entry.get("folderUri").and_then(|V| V.get("path")).and_then(|V| V.as_str()) {
284			return Some(Path.to_string());
285		}
286
287		if let Some(Path) = Entry
288			.get("workspace")
289			.and_then(|V| V.get("configPath"))
290			.and_then(|V| V.get("path"))
291			.and_then(|V| V.as_str())
292		{
293			return Some(Path.to_string());
294		}
295
296		None
297	};
298
299	let FolderPath = match Workspaces.iter().find_map(Probe) {
300		Some(Path) => Path,
301
302		None => return Base,
303	};
304
305	// Strip any `file://` scheme so the query param is a plain path
306	// the workbench will stringify into a `file:` URI itself; leaving
307	// the scheme in doubles up and breaks the URL-decode on the other
308	// side (observed as the second `?folder=` boot path appearing as
309	// `file:/Volumes/...` in `wb:boot`).
310	let WithoutScheme = FolderPath.strip_prefix("file://").unwrap_or(FolderPath.as_str()).to_string();
311
312	// RecentlyOpened.json stores workspace URIs with a trailing slash
313	// (`file:///Volumes/.../Mountain/`). Drop it before encoding into
314	// the URL so the workbench-side `URI.revive({ scheme: "file",
315	// path: <param> })` produces a folder URI that matches the
316	// workbench's own `URI.from(<file>)` results - which never carry
317	// a trailing slash on the parent directory. The mismatch caused
318	// `IUriIdentityService.extUri.relativePath` to return absolute
319	// paths and breadcrumbs / quick-pick / Problems-panel labels to
320	// render absolute `/Volumes/<vol>/...` paths instead of the workspace-relative
321	// short form. Preserve `/` itself when the path IS root (vanishing
322	// edge case but cheap to guard).
323	let TrailingTrimmed = WithoutScheme.trim_end_matches('/');
324
325	let Normalised = if TrailingTrimmed.is_empty() {
326		"/".to_string()
327	} else {
328		TrailingTrimmed.to_string()
329	};
330
331	if !std::path::Path::new(&Normalised).is_dir() {
332		return Base;
333	}
334
335	let Encoded = url::form_urlencoded::Serializer::new(String::new())
336		.append_pair("folder", &Normalised)
337		.finish();
338
339	format!("{}/?{}", LocalhostUrl, Encoded)
340}