Hi, I'm Terence. This article shares how I built a desktop wallpaper app themed around Black Myth: Wukong in roughly half a day.
First, here is what it looks like:
Black Myth: Wukong became extremely popular, and its visual style is beautiful. That naturally led to the thought: could those images be turned into desktop wallpapers more conveniently?
There were already images on the web, including on the WeGame site, but downloading them one by one was tedious. Installing a larger client app just to set wallpapers also felt excessive, especially on a work machine.
So the simplest answer was: build a small desktop app for it.
Before starting, make sure these steps are done:
The core flow is actually simple:
There is also a tray-based shortcut flow for quickly switching to the previous or next wallpaper, which is slightly different in surface behavior but shares the same underlying logic:
The two hardest parts are:
#[command]
async fn download_and_set_wallpaper(url: String, file_name: String) -> Result<(), String> {
let mut image_path = dirs::home_dir().unwrap_or(PathBuf::from("."));
let file_name_with_extension = format!("{}.jpg", file_name);
image_path.push(file_name_with_extension);
let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
let mut file = File::create(&image_path).map_err(|e| e.to_string())?;
file.write_all(&bytes).map_err(|e| e.to_string())?;
change_wallpaper(image_path.to_str().unwrap().to_string())
}This async function:
reqwest.jpgchange_wallpaper()fn change_wallpaper(image_path: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
let c_image_path = CString::new(image_path).map_err(|e| e.to_string())?;
unsafe {
if SystemParametersInfoA(SPI_SETDESKWALLPAPER, 0, c_image_path.as_ptr() as *mut _, 0)
== 0
{
return Err("Failed to set wallpaper".into());
}
}
}
#[cfg(target_os = "macos")]
{
let script = format!(
r#"tell application "System Events"
set desktopCount to count of desktops
repeat with desktopNumber from 1 to desktopCount
tell desktop desktopNumber
set picture to POSIX file "{}"
end tell
end repeat
end tell"#,
image_path
);
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()
.map_err(|e| e.to_string())?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
}
Ok(())
}On Windows, the implementation uses SystemParametersInfoA.
On macOS, it uses AppleScript through osascript to update the wallpaper across desktops.
There are other possible approaches, but these are relatively simple and practical.
Tauri communication here relies mainly on two methods:
invoke: send a message from frontend to backendlisten: receive a message from backend on the frontendThe communication can be broken into four pieces:
const handleChangeWallpaper = async (item: any, index: number, showLoading = true) => {
setTip("Setting wallpaper...");
currentIndex = index;
if (showLoading) {
setLoading(true);
}
try {
await invoke("download_and_set_wallpaper", {
url: item.url,
fileName: item.file_id,
wallpapers: wallPapers,
});
if (showLoading) {
message.success("Wallpaper changed successfully!");
}
} catch (error) {
if (showLoading) {
message.error(`Failed to change wallpaper: ${error}`);
}
} finally {
setLoading(false);
}
};#[command]
async fn download_and_set_wallpaper(url: String, file_name: String) -> Result<(), String> {}SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"next" => {
app.emit_all("change_wallpaper", Payload { message: "next_wallpaper".into() }).unwrap();
}
"previous" => {
app.emit_all("change_wallpaper", Payload { message: "previous_wallpaper".into() }).unwrap();
}
_ => {}
},useEffect(() => {
const changeWallpaperListen = listen("change_wallpaper", (data: any) => {
const currentIndex = Number(localStorage.getItem("currentIndex")) || 0;
const { message } = data.payload || {};
if (message === "next_wallpaper") {
let indexData = currentIndex + 1;
if (indexData > wallPapers.length - 1) {
indexData = 0;
}
if (wallPapers[indexData]) {
handleChangeWallpaper(wallPapers[indexData], indexData, false);
} else {
handleChangeWallpaper(WALLPAPERS[0], 0, false);
}
}
if (message === "previous_wallpaper") {
let indexData = currentIndex - 1;
if (indexData < 0) {
indexData = wallPapers.length - 1;
}
if (wallPapers[indexData]) {
handleChangeWallpaper(wallPapers[indexData], indexData, false);
} else {
handleChangeWallpaper(WALLPAPERS[0], 0, false);
}
}
});
return () => {
changeWallpaperListen.then((unlisten) => unlisten());
};
}, [wallPapers]);At that point, the main technical loop is complete. The rest is testing and polishing.
That is expected because the app is unsigned.
https://github.com/Xutaotaotao/wukong-wallpaper
https://github.com/Xutaotaotao/wukong-wallpaper/releases
This app is basically a personal toy project, and the source images come from the WeGame site. But it is a good example of how quickly a focused desktop utility can be built with Tauri.
Making small, fun tools in spare time is a good way to practice engineering, solve personal problems, and stay curious.