@@ -233,16 +233,19 @@ impl ManagedInstallation {
233233 fs:: create_dir_all ( & xdg_bin_home)
234234 . with_context ( || format ! ( "failed to create {}" , xdg_bin_home. display( ) ) ) ?;
235235
236+ let desktop_launcher = xdg_bin_home. join ( "taskers-desktop-launch" ) ;
237+ write_executable ( & desktop_launcher, & desktop_launch_wrapper_contents ( & launcher) ) ?;
238+
239+ let desktop_entry_path = applications_dir. join ( "dev.taskers.app.desktop" ) ;
236240 let desktop_entry = include_str ! ( concat!(
237241 env!( "CARGO_MANIFEST_DIR" ) ,
238242 "/assets/taskers.desktop.in"
239243 ) )
240- . replace ( "{{EXEC}}" , & desktop_exec ( & launcher) ) ;
241- fs:: write (
242- applications_dir. join ( "dev.taskers.app.desktop" ) ,
243- desktop_entry,
244- )
245- . with_context ( || format ! ( "failed to write {}" , applications_dir. display( ) ) ) ?;
244+ . replace ( "{{EXEC}}" , & desktop_exec ( & desktop_launcher) ) ;
245+ if should_update_desktop_entry ( & desktop_entry_path, & desktop_launcher) ? {
246+ fs:: write ( & desktop_entry_path, desktop_entry)
247+ . with_context ( || format ! ( "failed to write {}" , applications_dir. display( ) ) ) ?;
248+ }
246249 fs:: write (
247250 icons_dir. join ( "taskers.svg" ) ,
248251 include_bytes ! ( concat!( env!( "CARGO_MANIFEST_DIR" ) , "/assets/taskers.svg" ) ) ,
@@ -298,6 +301,7 @@ fn validate_bundle_layout(bundle_root: &Path) -> bool {
298301 . join ( "lib" )
299302 . join ( "libtaskers_ghostty_bridge.so" )
300303 . is_file ( )
304+ && bundle_root. join ( "ghostty" ) . join ( "themes" ) . is_dir ( )
301305 && bundle_root. join ( "terminfo" ) . is_dir ( )
302306}
303307
@@ -449,6 +453,32 @@ fn desktop_exec(path: &Path) -> String {
449453 raw. replace ( '\\' , "\\ \\ " ) . replace ( ' ' , "\\ " )
450454}
451455
456+ fn shell_single_quote ( path : & Path ) -> String {
457+ format ! ( "'{}'" , path. display( ) . to_string( ) . replace( '\'' , "'\" '\" '" ) )
458+ }
459+
460+ fn desktop_launch_wrapper_contents ( target : & Path ) -> String {
461+ format ! (
462+ "#!/bin/sh\n set -eu\n log_dir=\" ${{XDG_CACHE_HOME:-$HOME/.cache}}/taskers\" \n mkdir -p \" $log_dir\" \n exec /usr/bin/setsid -f {target} --diagnostic-log \" $log_dir/desktop-launch-diagnostics.log\" >>\" $log_dir/desktop-launch.log\" 2>&1\n " ,
463+ target = shell_single_quote( target) ,
464+ )
465+ }
466+
467+ fn should_update_desktop_entry ( path : & Path , desktop_launcher : & Path ) -> Result < bool > {
468+ let Ok ( existing) = fs:: read_to_string ( path) else {
469+ return Ok ( true ) ;
470+ } ;
471+ let Some ( existing_exec) = existing
472+ . lines ( )
473+ . find_map ( |line| line. strip_prefix ( "Exec=" ) )
474+ . map ( str:: trim)
475+ else {
476+ return Ok ( true ) ;
477+ } ;
478+
479+ Ok ( existing_exec == desktop_exec ( desktop_launcher) )
480+ }
481+
452482fn write_executable ( path : & Path , contents : & str ) -> Result < ( ) > {
453483 if let Some ( parent) = path. parent ( ) {
454484 fs:: create_dir_all ( parent)
@@ -558,12 +588,13 @@ where
558588mod tests {
559589 use super :: {
560590 ArtifactKind , ManagedInstallation , ReleaseArtifact , ReleaseManifest , bundle_root,
561- current_target_triple, default_manifest_url, launcher_path_looks_installed,
562- path_taskers_executable, sha256_path,
591+ current_target_triple, default_manifest_url, desktop_exec,
592+ desktop_launch_wrapper_contents, launcher_path_looks_installed, path_taskers_executable,
593+ sha256_path, should_update_desktop_entry,
563594 } ;
564595 #[ cfg( unix) ]
565596 use std:: os:: unix:: fs:: symlink;
566- use std:: { collections:: BTreeMap , ffi:: OsString , fs, path:: PathBuf } ;
597+ use std:: { collections:: BTreeMap , ffi:: OsString , fs, path:: { Path , PathBuf } } ;
567598 use tar:: Builder ;
568599 use tempfile:: tempdir;
569600 use xz2:: write:: XzEncoder ;
@@ -594,6 +625,7 @@ mod tests {
594625 let bundle_dir = temp. path ( ) . join ( "bundle" ) ;
595626 fs:: create_dir_all ( bundle_dir. join ( "bin" ) ) . expect ( "bin dir" ) ;
596627 fs:: create_dir_all ( bundle_dir. join ( "ghostty" ) . join ( "lib" ) ) . expect ( "ghostty dir" ) ;
628+ fs:: create_dir_all ( bundle_dir. join ( "ghostty" ) . join ( "themes" ) ) . expect ( "themes dir" ) ;
597629 fs:: create_dir_all ( bundle_dir. join ( "terminfo" ) . join ( "g" ) ) . expect ( "terminfo dir" ) ;
598630 fs:: write (
599631 bundle_dir. join ( "bin" ) . join ( "taskers" ) ,
@@ -618,6 +650,14 @@ mod tests {
618650 "bridge" ,
619651 )
620652 . expect ( "bridge" ) ;
653+ fs:: write (
654+ bundle_dir
655+ . join ( "ghostty" )
656+ . join ( "themes" )
657+ . join ( "Catppuccin Mocha" ) ,
658+ "palette = 0=#1e1e2e\n " ,
659+ )
660+ . expect ( "theme" ) ;
621661 fs:: write (
622662 bundle_dir. join ( "terminfo" ) . join ( "g" ) . join ( "ghostty" ) ,
623663 "ghostty" ,
@@ -695,4 +735,43 @@ mod tests {
695735 let repo_binary = PathBuf :: from ( "/home/notes/Projects/taskers/target/debug/taskers" ) ;
696736 assert ! ( !launcher_path_looks_installed( & repo_binary) ) ;
697737 }
738+
739+ #[ test]
740+ fn preserves_non_launcher_desktop_entry ( ) {
741+ let temp = tempdir ( ) . expect ( "tempdir" ) ;
742+ let desktop_entry = temp. path ( ) . join ( "dev.taskers.app.desktop" ) ;
743+ fs:: write (
744+ & desktop_entry,
745+ "[Desktop Entry]\n Exec=/home/notes/.cargo/bin/taskers-gtk\n " ,
746+ )
747+ . expect ( "desktop entry" ) ;
748+
749+ let launcher = PathBuf :: from ( "/home/notes/.local/bin/taskers" ) ;
750+ assert ! (
751+ !should_update_desktop_entry( & desktop_entry, & launcher) . expect( "decision" ) ,
752+ ) ;
753+ }
754+
755+ #[ test]
756+ fn updates_matching_launcher_desktop_entry ( ) {
757+ let temp = tempdir ( ) . expect ( "tempdir" ) ;
758+ let desktop_entry = temp. path ( ) . join ( "dev.taskers.app.desktop" ) ;
759+ let launcher = PathBuf :: from ( "/home/notes/.local/bin/taskers-desktop-launch" ) ;
760+ fs:: write (
761+ & desktop_entry,
762+ format ! ( "[Desktop Entry]\n Exec={}\n " , desktop_exec( & launcher) ) ,
763+ )
764+ . expect ( "desktop entry" ) ;
765+
766+ assert ! ( should_update_desktop_entry( & desktop_entry, & launcher) . expect( "decision" ) ) ;
767+ }
768+
769+ #[ test]
770+ fn desktop_launch_wrapper_targets_binary_with_log_redirection ( ) {
771+ let contents = desktop_launch_wrapper_contents ( Path :: new ( "/home/notes/.local/bin/taskers" ) ) ;
772+ assert ! ( contents. contains( "desktop-launch-diagnostics.log" ) ) ;
773+ assert ! ( contents. contains( "desktop-launch.log" ) ) ;
774+ assert ! ( contents. contains( "/usr/bin/setsid -f" ) ) ;
775+ assert ! ( contents. contains( "'/home/notes/.local/bin/taskers'" ) ) ;
776+ }
698777}
0 commit comments