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 if (e.key === 'y' || e.key === 'Y') { e.preventDefault(); }
110 }
111 }, true);
112 })();"#;
113
114 let mut WindowBuilder = WebviewWindowBuilder::new(Application, "main", WindowUrl)
115 .use_https_scheme(false)
116 .initialization_script(TauriDiagnosticScript)
117 .initialization_script(WkUndoSuppressScript)
118 .zoom_hotkeys_enabled(true)
119 .browser_extensions_enabled(false)
120
121 // macOS first-responder: by default WKWebView swallows the
122 // first click on an unfocused window as a "make me key"
123 // no-op and the click never reaches the inner content. With
124 // `Inspect=1` running DevTools alongside the main window
125 // every switch-back to the editor needed two clicks - first
126 // to focus the NSWindow, second to actually focus the
127 // Monaco textarea - which the user reports as "I clicked,
128 // I'm typing, nothing's happening". `accept_first_mouse`
129 // flips the responder chain so the first click already
130 // reaches WKWebView's content and the textarea picks up
131 // keyboard input immediately.
132 .accept_first_mouse(true)
133 .title("Mountain")
134 .resizable(true)
135 .inner_size(1400.0, 900.0)
136 .shadow(true)
137 .visible(false);
138
139 #[cfg(target_os = "macos")]
140 {
141 // Overlay style lets VS Code's custom titlebar paint behind the
142 // traffic-light buttons. `hidden_title(true)` suppresses the OS
143 // title text so it doesn't collide with the workbench menubar.
144 // `decorations(true)` is REQUIRED for the traffic lights to
145 // render - turning decorations off also removes the buttons and
146 // breaks the native drag + resize handles entirely on macOS.
147 // `content_protected(true)` tells macOS to reserve a safe zone
148 // around the traffic-light cluster so WKWebView content doesn't
149 // render underneath them, preventing click-through and visual
150 // overlap with the workbench titlebar.
151 WindowBuilder = WindowBuilder
152 .title_bar_style(tauri::TitleBarStyle::Overlay)
153 .hidden_title(true)
154 .decorations(true)
155 .content_protected(true);
156 }
157
158 #[cfg(any(target_os = "windows", target_os = "linux"))]
159 {
160 WindowBuilder = WindowBuilder.decorations(false);
161 }
162
163 // Enable WKWebView inspection when InDebug mode + Inspect=1.
164 // This sets WKWebView.isInspectable via Wry's devtools flag so that
165 // external inspectors (Safari/Web Inspector) can attach.
166 #[cfg(debug_assertions)]
167 {
168 let enable_debugtools = std::env::var("Inspect").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
169
170 if enable_debugtools {
171 WindowBuilder = WindowBuilder.devtools(true);
172 }
173 }
174
175 #[cfg(debug_assertions)]
176 {
177 let enable_debug_server = std::env::var("DebugServer").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
178
179 if enable_debug_server {
180 WindowBuilder = WindowBuilder.on_page_load(|window, _payload| {
181 let _ = window.eval(
182 r#"(function() {
183 if (!window.__MOUNTAIN_DEBUG_CONSOLE) {
184 window.__MOUNTAIN_DEBUG_CONSOLE = [];
185 const origLog = console.log;
186 const origError = console.error;
187 const origWarn = console.warn;
188 const origInfo = console.info;
189 const origDebug = console.debug;
190 function pushLog(level, args) {
191 const argStrings = args.map(arg => {
192 if (typeof arg === 'object') {
193 try { return JSON.stringify(arg); } catch { return String(arg); }
194 } else {
195 return String(arg);
196 }
197 });
198 window.__MOUNTAIN_DEBUG_CONSOLE.push({ level, messages: argStrings, timestamp: Date.now() });
199 if (window.__MOUNTAIN_DEBUG_CONSOLE.length > 1000) {
200 window.__MOUNTAIN_DEBUG_CONSOLE = window.__MOUNTAIN_DEBUG_CONSOLE.slice(-1000);
201 }
202 }
203 console.log = function(...args) { pushLog('log', args); origLog.apply(console, args); };
204 console.error = function(...args) { pushLog('error', args); origError.apply(console, args); };
205 console.warn = function(...args) { pushLog('warn', args); origWarn.apply(console, args); };
206 console.info = function(...args) { pushLog('info', args); origInfo.apply(console, args); };
207 console.debug = function(...args) { pushLog('debug', args); origDebug.apply(console, args); };
208 }
209 })()"#,
210 );
211 });
212 }
213 }
214
215 // Build the main window
216 let MainWindow = WindowBuilder.build().expect("FATAL: Main window build failed");
217
218 // DevTools auto-open lives in `Binary/Main/AppLifecycle.rs:174`
219 // (gated on `cfg(debug_assertions)`, with a `[Window] Debug build:
220 // opening DevTools.` log line). Calling `open_devtools()` here as
221 // well opened a SECOND DevTools window on every debug launch -
222 // reported as "two DevTools" after the last rebuild. Single-source
223 // the call to AppLifecycle so the log line and the window match.
224
225 MainWindow
226}
227
228/// Build the initial webview URL, optionally appending `?folder=<path>`
229/// when `~/.fiddee/workspaces/RecentlyOpened.json` has an entry for the
230/// previous session's workspace. Falls back to plain `index.html` if
231/// the file is missing, malformed, or has no resolvable path.
232///
233/// The returned string is already URL-encoded and safe to feed to
234/// `WebviewUrl::External`.
235fn BuildInitialUrl(LocalhostUrl:&str) -> String {
236 let Base = format!("{}/index.html", LocalhostUrl);
237
238 let Recent = match ReadRecentlyOpened() {
239 Ok(Value) => Value,
240
241 Err(_) => return Base,
242 };
243
244 let Workspaces = match Recent.get("workspaces").and_then(|V| V.as_array()) {
245 Some(Array) if !Array.is_empty() => Array,
246
247 _ => return Base,
248 };
249
250 // VS Code's Recently-Opened record can store the folder under a few
251 // different shapes depending on whether the entry came from the
252 // extension host, the workbench, or a `$deltaWorkspaceFolders`
253 // broadcast. Probe them in the same priority order the workbench
254 // itself uses in `getRecentlyOpenedWorkspaces`.
255 let Probe = |Entry:&serde_json::Value| -> Option<String> {
256 // Mountain's own writer emits `{ uri: "file://…", label }` (see
257 // `RecentlyOpened.json` on a freshly closed window). VS Code's
258 // historical `folderUri` / `workspace.configPath` shapes are kept
259 // as fallbacks so imported profiles and third-party writers keep
260 // working.
261 if let Some(Uri) = Entry.get("uri").and_then(|V| V.as_str()) {
262 return Some(Uri.to_string());
263 }
264
265 if let Some(Uri) = Entry.get("folderUri").and_then(|V| V.as_str()) {
266 return Some(Uri.to_string());
267 }
268
269 if let Some(Path) = Entry.get("folderUri").and_then(|V| V.get("path")).and_then(|V| V.as_str()) {
270 return Some(Path.to_string());
271 }
272
273 if let Some(Path) = Entry
274 .get("workspace")
275 .and_then(|V| V.get("configPath"))
276 .and_then(|V| V.get("path"))
277 .and_then(|V| V.as_str())
278 {
279 return Some(Path.to_string());
280 }
281
282 None
283 };
284
285 let FolderPath = match Workspaces.iter().find_map(Probe) {
286 Some(Path) => Path,
287
288 None => return Base,
289 };
290
291 // Strip any `file://` scheme so the query param is a plain path
292 // the workbench will stringify into a `file:` URI itself; leaving
293 // the scheme in doubles up and breaks the URL-decode on the other
294 // side (observed as the second `?folder=` boot path appearing as
295 // `file:/Volumes/...` in `wb:boot`).
296 let WithoutScheme = FolderPath.strip_prefix("file://").unwrap_or(FolderPath.as_str()).to_string();
297
298 // RecentlyOpened.json stores workspace URIs with a trailing slash
299 // (`file:///Volumes/.../Mountain/`). Drop it before encoding into
300 // the URL so the workbench-side `URI.revive({ scheme: "file",
301 // path: <param> })` produces a folder URI that matches the
302 // workbench's own `URI.from(<file>)` results - which never carry
303 // a trailing slash on the parent directory. The mismatch caused
304 // `IUriIdentityService.extUri.relativePath` to return absolute
305 // paths and breadcrumbs / quick-pick / Problems-panel labels to
306 // render absolute `/Volumes/<vol>/...` paths instead of the workspace-relative
307 // short form. Preserve `/` itself when the path IS root (vanishing
308 // edge case but cheap to guard).
309 let TrailingTrimmed = WithoutScheme.trim_end_matches('/');
310
311 let Normalised = if TrailingTrimmed.is_empty() {
312 "/".to_string()
313 } else {
314 TrailingTrimmed.to_string()
315 };
316
317 if !std::path::Path::new(&Normalised).is_dir() {
318 return Base;
319 }
320
321 let Encoded = url::form_urlencoded::Serializer::new(String::new())
322 .append_pair("folder", &Normalised)
323 .finish();
324
325 format!("{}/?{}", LocalhostUrl, Encoded)
326}