11//! A version of `tauri::AppHandle::path()` for use outside `tauri`.
2+ #[ cfg( target_os = "linux" ) ]
3+ use std:: process:: Command ;
24use std:: { env, path:: PathBuf } ;
35
4- use anyhow:: { Context , bail } ;
6+ use anyhow:: Context ;
57
68/// The directory to store application-wide data in, like logs, **one per channel**.
79///
@@ -134,6 +136,9 @@ impl AppChannel {
134136 /// Open the GitButler GUI application for `possibly_project_dir`.
135137 ///
136138 /// This uses the deeplink URL scheme registered for the specific channel.
139+ ///
140+ /// Note: On Linux, we don't have a good way of distinguishing between channels in the installed
141+ /// binaries, so we always just resolve `gitbutler-tauri`.
137142 pub fn open ( & self , possibly_project_dir : & std:: path:: Path ) -> anyhow:: Result < ( ) > {
138143 let scheme = match self {
139144 AppChannel :: Nightly => "but-nightly" ,
@@ -155,40 +160,87 @@ impl AppChannel {
155160 timestamp
156161 ) ;
157162
158- let mut cmd_errors = Vec :: new ( ) ;
159-
160- for mut cmd in open:: commands ( url) {
161- let cleaned_vars = clean_env_vars ( & [
162- "APPDIR" ,
163- "GDK_PIXBUF_MODULE_FILE" ,
164- "GIO_EXTRA_MODULES" ,
165- "GIO_EXTRA_MODULES" ,
166- "GSETTINGS_SCHEMA_DIR" ,
167- "GST_PLUGIN_SYSTEM_PATH" ,
168- "GST_PLUGIN_SYSTEM_PATH_1_0" ,
169- "GTK_DATA_PREFIX" ,
170- "GTK_EXE_PREFIX" ,
171- "GTK_IM_MODULE_FILE" ,
172- "GTK_PATH" ,
173- "LD_LIBRARY_PATH" ,
174- "PATH" ,
175- "PERLLIB" ,
176- "PYTHONHOME" ,
177- "PYTHONPATH" ,
178- "QT_PLUGIN_PATH" ,
179- "XDG_DATA_DIRS" ,
180- ] ) ;
181-
182- cmd. envs ( cleaned_vars) ;
163+ let cleaned_vars: Vec < ( & str , String ) > = clean_env_vars ( & [
164+ "APPDIR" ,
165+ "GDK_PIXBUF_MODULE_FILE" ,
166+ "GIO_EXTRA_MODULES" ,
167+ "GSETTINGS_SCHEMA_DIR" ,
168+ "GST_PLUGIN_SYSTEM_PATH" ,
169+ "GST_PLUGIN_SYSTEM_PATH_1_0" ,
170+ "GTK_DATA_PREFIX" ,
171+ "GTK_EXE_PREFIX" ,
172+ "GTK_IM_MODULE_FILE" ,
173+ "GTK_PATH" ,
174+ "LD_LIBRARY_PATH" ,
175+ "PATH" ,
176+ "PERLLIB" ,
177+ "PYTHONHOME" ,
178+ "PYTHONPATH" ,
179+ "QT_PLUGIN_PATH" ,
180+ "XDG_DATA_DIRS" ,
181+ ] )
182+ . collect ( ) ;
183+
184+ #[ cfg( target_os = "linux" ) ]
185+ {
186+ // On Linux, we don't currently want to rely on the scheme being properly registered
187+ // with the Tauri app. The mechanism by which the scheme is registered relies on the
188+ // bundled Desktop entry, and different desktop environments have pretty wildly
189+ // different handling of such entries.
190+ //
191+ // Adding insult to injury, even if that desktop entry is resolved, it in turn just has
192+ // an exec line with the name `gitbutler-tauri` and the URL is provided as a command
193+ // line argument. Therefore, after the roundabout trip to the desktop entry via the
194+ // custom scheme, we _still_ just resolve `gitbutler-tauri` from PATH.
195+ //
196+ // Even more annoying is that the desktop entry does not have a placeholder for the
197+ // command line argument, causing some stricter environments such as KDE to just error
198+ // out, while some more lenient environments simply append the URL to the exec line.
199+ //
200+ // For these reasons, it's way more reliable and simpler to just try to call
201+ // `gitbutler-tauri` directly, completely circumventing any issues with scheme
202+ // registration.
203+ //
204+ // As the binary is always called `gitbutler-tauri`, there's currently no way to
205+ // distinguish between release, nightly and dev. We'll just have to try to launch
206+ // whatever we find. This can be fixed by giving the binaries different names, but as
207+ // so few users use nightly builds, it's just not worth the effort.
208+ let mut cmd = Command :: new ( "gitbutler-tauri" ) ;
209+ cmd. arg ( & url) ;
183210 cmd. current_dir ( env:: temp_dir ( ) ) ;
184- if cmd. status ( ) . is_ok ( ) {
185- return Ok ( ( ) ) ;
186- } else {
187- cmd_errors. push ( anyhow:: anyhow!( "Failed to execute command {cmd:?}" ) ) ;
211+ cmd. envs ( cleaned_vars. clone ( ) ) ;
212+
213+ // Unset all io to not pollute the terminal with output.
214+ cmd. stdin ( std:: process:: Stdio :: null ( ) ) ;
215+ cmd. stdout ( std:: process:: Stdio :: null ( ) ) ;
216+ cmd. stderr ( std:: process:: Stdio :: null ( ) ) ;
217+
218+ // We spawn this fire-and-forget style. The process will be re-parented to init when
219+ // the caller exits (and that caller is typically the `but` CLI). This allows you to
220+ // e.g. run `but gui` in a terminal, and then keep using that terminal.
221+ //
222+ // This is only necessary on cold start, i.e. when the GUI isn't already running, as
223+ // then this process becomes the GUI process. If the GUI is already running, this
224+ // process effectively just sends the deep link to the already running GUI and then
225+ // exits.
226+ cmd. spawn ( ) ?;
227+ } ;
228+
229+ #[ cfg( not( target_os = "linux" ) ) ]
230+ {
231+ let mut cmd_errors = Vec :: new ( ) ;
232+ for mut cmd in open:: commands ( & url) {
233+ cmd. envs ( cleaned_vars. clone ( ) ) ;
234+ cmd. current_dir ( env:: temp_dir ( ) ) ;
235+ if cmd. status ( ) . is_ok ( ) {
236+ return Ok ( ( ) ) ;
237+ } else {
238+ cmd_errors. push ( anyhow:: anyhow!( "Failed to execute command {cmd:?}" ) ) ;
239+ }
240+ }
241+ if !cmd_errors. is_empty ( ) {
242+ anyhow:: bail!( "Errors occurred: {cmd_errors:?}" ) ;
188243 }
189- }
190- if !cmd_errors. is_empty ( ) {
191- bail ! ( "Errors occurred: {cmd_errors:?}" ) ;
192244 }
193245 Ok ( ( ) )
194246 }
0 commit comments