Skip to content

Commit 5d877b9

Browse files
committed
fix(packaging): preserve desktop integration and Ghostty themes
1 parent 8688b37 commit 5d877b9

10 files changed

Lines changed: 149 additions & 54 deletions

File tree

assets/taskers.desktop.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ Icon=taskers
88
Terminal=false
99
Categories=Development;
1010
StartupNotify=true
11-
StartupWMClass=taskers
11+
StartupWMClass=dev.taskers.app
1212
X-GNOME-UsesNotifications=true

crates/taskers-app/src/main.rs

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ fn default_true() -> bool {
188188
true
189189
}
190190

191+
fn safe_eprintln(message: impl std::fmt::Display) {
192+
let mut stderr = io::stderr().lock();
193+
let _ = writeln!(stderr, "{message}");
194+
}
195+
191196
impl TaskersConfig {
192197
fn load() -> Result<Self> {
193198
let path = taskers_paths::default_config_path();
@@ -245,7 +250,7 @@ fn main() -> glib::ExitCode {
245250
let bootstrap = match bootstrap_runtime(None) {
246251
Ok(bootstrap) => bootstrap,
247252
Err(error) => {
248-
eprintln!("failed to bootstrap Taskers host: {error:?}");
253+
safe_eprintln(format!("failed to bootstrap Taskers host: {error:?}"));
249254
return glib::ExitCode::FAILURE;
250255
}
251256
};
@@ -282,7 +287,7 @@ fn build_ui(
282287
cli: Cli,
283288
) {
284289
if let Err(error) = build_ui_result(app, bootstrap, hold_guard, cli) {
285-
eprintln!("failed to launch Taskers host: {error:?}");
290+
safe_eprintln(format!("failed to launch Taskers host: {error:?}"));
286291
}
287292
}
288293

@@ -331,8 +336,10 @@ fn build_ui_result(
331336
.default_width(1440)
332337
.default_height(900)
333338
.build();
339+
let app_for_close = app.clone();
334340
window.connect_close_request(move |_| {
335341
drop(hold_guard.borrow_mut().take());
342+
app_for_close.quit();
336343
glib::Propagation::Proceed
337344
});
338345
let host_widget = host.borrow().widget();
@@ -362,7 +369,7 @@ fn build_ui_result(
362369
diagnostics.as_ref(),
363370
DiagnosticRecord::new(DiagnosticCategory::Startup, None, note.clone()),
364371
);
365-
eprintln!("{note}");
372+
safe_eprintln(note);
366373
}
367374
log_diagnostic(
368375
diagnostics.as_ref(),
@@ -372,7 +379,7 @@ fn build_ui_result(
372379
control_server_note.clone(),
373380
),
374381
);
375-
eprintln!("{control_server_note}");
382+
safe_eprintln(control_server_note);
376383
log_diagnostic(
377384
diagnostics.as_ref(),
378385
DiagnosticRecord::new(
@@ -381,7 +388,7 @@ fn build_ui_result(
381388
format!("shared shell listening on {shell_url}"),
382389
),
383390
);
384-
eprintln!("shared shell listening on {shell_url}");
391+
safe_eprintln(format!("shared shell listening on {shell_url}"));
385392

386393
let smoke_script = cli.smoke_script;
387394
let quit_after_ms = cli.quit_after_ms.unwrap_or(8_000);
@@ -534,7 +541,9 @@ fn process_pending_notifications(
534541
.with_pane(notification.pane_id)
535542
.with_surface(notification.surface_id),
536543
);
537-
eprintln!("taskers notification delivery update failed: {error:?}");
544+
safe_eprintln(format!(
545+
"taskers notification delivery update failed: {error:?}"
546+
));
538547
}
539548
}
540549

@@ -652,7 +661,7 @@ fn persist_settings_if_needed(
652661
format!("failed to persist config: {error:?}"),
653662
),
654663
);
655-
eprintln!("taskers config save failed: {error:?}");
664+
safe_eprintln(format!("taskers config save failed: {error:?}"));
656665
return;
657666
}
658667

@@ -1030,10 +1039,10 @@ fn run_internal_ghostty_probe(mode: GhosttyProbeMode) -> glib::ExitCode {
10301039
host
10311040
}
10321041
Err(error) => {
1033-
eprintln!(
1042+
safe_eprintln(format!(
10341043
"ghostty {} self-probe failed during host init: {error}",
10351044
mode.as_arg()
1036-
);
1045+
));
10371046
return glib::ExitCode::FAILURE;
10381047
}
10391048
};
@@ -1053,10 +1062,10 @@ fn run_internal_surface_probe(
10531062
) -> glib::ExitCode {
10541063
if !gtk::is_initialized_main_thread() {
10551064
if let Err(error) = gtk::init() {
1056-
eprintln!(
1065+
safe_eprintln(format!(
10571066
"ghostty {} self-probe failed during gtk init: {error}",
10581067
mode.as_arg()
1059-
);
1068+
));
10601069
return glib::ExitCode::FAILURE;
10611070
}
10621071
}
@@ -1084,10 +1093,10 @@ fn run_internal_surface_probe(
10841093
) {
10851094
Ok(app_state) => app_state,
10861095
Err(error) => {
1087-
eprintln!(
1096+
safe_eprintln(format!(
10881097
"ghostty {} self-probe failed during app state bootstrap: {error}",
10891098
mode.as_arg()
1090-
);
1099+
));
10911100
return glib::ExitCode::FAILURE;
10921101
}
10931102
};
@@ -1121,10 +1130,10 @@ fn run_internal_surface_probe(
11211130

11221131
spin_probe_main_context(Duration::from_millis(80));
11231132
if let Err(error) = taskers_host.sync_snapshot(&core.snapshot()) {
1124-
eprintln!(
1133+
safe_eprintln(format!(
11251134
"ghostty {} self-probe failed during snapshot sync: {error}",
11261135
mode.as_arg()
1127-
);
1136+
));
11281137
return glib::ExitCode::FAILURE;
11291138
}
11301139

@@ -1271,7 +1280,7 @@ fn sync_window(
12711280
format!("host command failed: {error}"),
12721281
),
12731282
);
1274-
eprintln!("taskers host command failed: {error}");
1283+
safe_eprintln(format!("taskers host command failed: {error}"));
12751284
}
12761285
}
12771286

@@ -1320,7 +1329,9 @@ fn sync_window(
13201329
format!("snapshot sync failed: {error}"),
13211330
),
13221331
);
1323-
eprintln!("taskers host sync failed for revision {revision}: {error}");
1332+
safe_eprintln(format!(
1333+
"taskers host sync failed for revision {revision}: {error}"
1334+
));
13241335
}
13251336
last_revision.set(revision);
13261337
}
@@ -1574,14 +1585,14 @@ fn spawn_control_server(
15741585
};
15751586
if let Err(error) = serve_with_handler(listener, handler, pending::<()>()).await
15761587
{
1577-
eprintln!("control server error: {error}");
1588+
safe_eprintln(format!("control server error: {error}"));
15781589
}
15791590
}
15801591
Err(error) => {
1581-
eprintln!(
1592+
safe_eprintln(format!(
15821593
"control server unavailable at {}: {error}",
15831594
socket_path.display()
1584-
);
1595+
));
15851596
}
15861597
}
15871598
});
@@ -1649,7 +1660,7 @@ fn launch_liveview_server(core: SharedCore) -> Result<String> {
16491660
);
16501661

16511662
if let Err(error) = axum::serve(listener, router.into_make_service()).await {
1652-
eprintln!("liveview server failed: {error}");
1663+
safe_eprintln(format!("liveview server failed: {error}"));
16531664
}
16541665
});
16551666
});
@@ -1873,7 +1884,7 @@ impl DiagnosticsWriter {
18731884
target: DiagnosticsTarget::File(Arc::new(Mutex::new(file))),
18741885
}),
18751886
Err(error) => {
1876-
eprintln!("taskers diagnostics log path failed: {error}");
1887+
safe_eprintln(format!("taskers diagnostics log path failed: {error}"));
18771888
None
18781889
}
18791890
}

crates/taskers-cli/assets/taskers.desktop.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ Icon=taskers
99
Terminal=false
1010
Categories=Development;
1111
StartupNotify=true
12-
StartupWMClass=taskers
12+
StartupWMClass=dev.taskers.app
1313
X-GNOME-UsesNotifications=true

crates/taskers-launcher/assets/taskers.desktop.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ Icon=taskers
99
Terminal=false
1010
Categories=Development;
1111
StartupNotify=true
12-
StartupWMClass=taskers
12+
StartupWMClass=dev.taskers.app
1313
X-GNOME-UsesNotifications=true

crates/taskers-launcher/src/lib.rs

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nset -eu\nlog_dir=\"${{XDG_CACHE_HOME:-$HOME/.cache}}/taskers\"\nmkdir -p \"$log_dir\"\nexec /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+
452482
fn 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
558588
mod 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]\nExec=/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]\nExec={}\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
}

docs/usage.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ Taskers still pins a small set of embedded-pane invariants:
3737

3838
- the launch command stays Taskers-owned
3939
- Ghostty shell integration stays disabled because Taskers provides its own shell wrapper
40-
- Ghostty window padding stays zero and Taskers adds its own host-side terminal gutter
4140
- Ghostty Linux cgroup settings stay disabled for embedded panes
4241

43-
That means Ghostty settings such as fonts, theme, colors, cursor behavior, and scrollback should carry over, while window-style and shell-launch behavior remains controlled by Taskers.
42+
That means Ghostty settings such as fonts, theme, colors, cursor behavior, scrollback, and window padding should carry over, while shell-launch behavior remains controlled by Taskers.
4443

4544
## A Typical Session
4645

scripts/build_ghostty_runtime_bundle.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ trap cleanup EXIT
2929
--prefix "$prefix_dir"
3030
)
3131

32-
mkdir -p "$bundle_dir/ghostty/lib" "$bundle_dir/ghostty/shell-integration" "$bundle_dir/terminfo"
32+
mkdir -p "$bundle_dir/ghostty/lib" "$bundle_dir/ghostty/shell-integration" "$bundle_dir/ghostty/themes" "$bundle_dir/terminfo"
3333
cp "$prefix_dir/lib/libtaskers_ghostty_bridge.so" "$bundle_dir/ghostty/lib/"
3434
cp -R "$prefix_dir/share/ghostty/shell-integration/." "$bundle_dir/ghostty/shell-integration/"
35+
cp -R "$prefix_dir/share/ghostty/themes/." "$bundle_dir/ghostty/themes/"
3536
cp -R "$prefix_dir/share/terminfo/." "$bundle_dir/terminfo/"
3637
printf '%s\n' "$version" > "$bundle_dir/ghostty/.taskers-runtime-version"
3738

0 commit comments

Comments
 (0)