Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Compiles widgets/Center/WidgetCenter.swift into a static library, so the
//! app can ask WidgetKit to reload the desktop widgets.

use std::path::PathBuf;
use std::process::Command;

fn main() {
let source = "widgets/Center/WidgetCenter.swift";
println!("cargo:rerun-if-changed={source}");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") {
return;
}
let arch = match std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() {
Ok("aarch64") => "arm64",
Ok("x86_64") => "x86_64",
Ok(other) => panic!("no Swift target for {other}"),
Err(_) => panic!("CARGO_CFG_TARGET_ARCH is not set"),
};
let out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is not set"));
let status = Command::new("xcrun")
.args([
"swiftc",
"-emit-library",
"-static",
"-parse-as-library",
"-O",
])
.args(["-module-name", "TidegaugeWidgetCenter"])
.args(["-target", &format!("{arch}-apple-macos11.0")])
.arg(source)
.arg("-o")
.arg(out.join("libtidegauge_widget_center.a"))
.status()
.expect("xcrun swiftc could not run; install the Xcode command line tools");
assert!(status.success(), "swiftc failed on {source}");

// The Swift runtime ships with macOS; the compatibility shims the
// library may pull in ship with the toolchain.
let toolchain = Command::new("xcrun")
.args(["--find", "swiftc"])
.output()
.expect("xcrun --find swiftc could not run");
let swiftc = PathBuf::from(String::from_utf8_lossy(&toolchain.stdout).trim());
let shims = swiftc
.parent()
.and_then(|bin| bin.parent())
.map(|usr| usr.join("lib/swift/macosx"));
println!("cargo:rustc-link-search=native={}", out.display());
println!("cargo:rustc-link-lib=static=tidegauge_widget_center");
println!("cargo:rustc-link-search=native=/usr/lib/swift");
if let Some(shims) = shims {
println!("cargo:rustc-link-search=native={}", shims.display());
}
println!("cargo:rustc-link-lib=framework=WidgetKit");
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
}
29 changes: 23 additions & 6 deletions src/widget_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,22 +296,23 @@ fn attempt(dir: &Path, snap: &Snapshot, now: SystemTime) -> String {
return "sample is stale; not written".into();
}
match write_atomic(dir, &build(snap, seconds)) {
Ok(()) => "wrote the snapshot".into(),
Ok(()) => WROTE.into(),
Err(error) => ["write failed: ", &error.to_string()].concat(),
}
}

/// Appends `outcome` to `~/Library/Logs/Tidegauge/widget-export.log` when it
/// differs from the last one, so a steady state is logged once.
fn note(dir: &Path, outcome: &str) {
/// differs from the last one, so a steady state is logged once. Returns
/// whether it differed.
fn note(dir: &Path, outcome: &str) -> bool {
static LAST: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
let mut last = LAST.lock().unwrap_or_else(|e| e.into_inner());
if *last == outcome {
return;
return false;
}
*last = outcome.to_string();
let Some(home) = directories::BaseDirs::new().map(|d| d.home_dir().to_path_buf()) else {
return;
return true;
};
let folder = home.join("Library/Logs/Tidegauge");
let file = folder.join("widget-export.log");
Expand All @@ -323,6 +324,7 @@ fn note(dir: &Path, outcome: &str) {
let at = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
let _ = writeln!(out, "{at} {} :: {outcome}", dir.display());
}
true
}

/// Writes the readings for the widgets, if one is on the desktop. Cheap when
Expand All @@ -332,7 +334,22 @@ pub fn publish(snap: &Snapshot) {
note(Path::new(GROUP_ID), "no group folder yet; waiting for a widget or access");
return;
};
note(&dir, &attempt(&dir, snap, SystemTime::now()));
let outcome = attempt(&dir, snap, SystemTime::now());
// The first write after launch or sleep: the widgets may be showing
// "Open Tidegauge" and macOS may not ask them again for hours.
if note(&dir, &outcome) && outcome == WROTE {
reload_widgets();
}
}

const WROTE: &str = "wrote the snapshot";

fn reload_widgets() {
extern "C" {
fn tidegauge_reload_widgets();
}
// SAFETY: a Swift function with no arguments, built by build.rs.
unsafe { tidegauge_reload_widgets() }
}

#[cfg(test)]
Expand Down
9 changes: 9 additions & 0 deletions widgets/Center/WidgetCenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Linked into the tidegauge binary by build.rs. WidgetKit takes a reload
// request only from the main executable of the app that holds the widget,
// so a helper program cannot do this.
import WidgetKit

@_cdecl("tidegauge_reload_widgets")
public func tidegaugeReloadWidgets() {
WidgetCenter.shared.reloadAllTimelines()
}
Loading