fix(appimage): tell running desktops when the icon is installed
- On Plasma under Wayland the AppImage's window went from the generic icon to a blank one. The desktop entry and the icon were both installed, and a new KDE process resolved and drew the icon, but a shell reads the icon theme's directories when it starts, before hicolor/scalable/apps existed under the user's data directory, so it found the entry and no icon. - After writing the icon the window now sends org.kde.KIconLoader.iconChanged on the session bus through dbus-send, the signal KDE's own programs send to have running programs reload their icons, and sets the theme directory's modification time for loaders that compare times. - Both happen only when the icon was written, so an AppImage whose icon is already installed sends nothing. A desktop without dbus-send shows the icon from the next login, as before.
This commit is contained in:
parent
43f7b8916b
commit
f250678dd6
2 changed files with 82 additions and 19 deletions
|
|
@ -11,6 +11,9 @@ use std::path::{Path, PathBuf};
|
||||||
/// The desktop entry's name without its extension, which is the window's application ID.
|
/// The desktop entry's name without its extension, which is the window's application ID.
|
||||||
const ID: &str = "com.mrgeckosmedia.MidiHarbor";
|
const ID: &str = "com.mrgeckosmedia.MidiHarbor";
|
||||||
|
|
||||||
|
/// The icon theme every desktop falls back to, under a data directory.
|
||||||
|
const THEME: &str = "icons/hicolor";
|
||||||
|
|
||||||
/// Installs this AppImage's desktop entry and icon for the user, when the program runs from one.
|
/// Installs this AppImage's desktop entry and icon for the user, when the program runs from one.
|
||||||
///
|
///
|
||||||
/// A failure costs the icon and nothing else, so it is logged and the window opens regardless.
|
/// A failure costs the icon and nothing else, so it is logged and the window opens regardless.
|
||||||
|
|
@ -22,13 +25,28 @@ pub fn integrate() {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match install(&appimage, &appdir, &data_home, &data_dirs()) {
|
match install(&appimage, &appdir, &data_home, &data_dirs()) {
|
||||||
Ok(true) => tracing::info!(appimage = %appimage.display(), "installed the desktop entry"),
|
Ok(Written { entry, icon }) => {
|
||||||
Ok(false) => {}
|
if icon {
|
||||||
|
announce_icon(&data_home.join(THEME));
|
||||||
|
}
|
||||||
|
if entry || icon {
|
||||||
|
tracing::info!(appimage = %appimage.display(), "installed the desktop entry");
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(error) => tracing::debug!(error = %error, "could not install the desktop entry"),
|
Err(error) => tracing::debug!(error = %error, "could not install the desktop entry"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Installs the entry and icon under `data_home`, and reports whether anything was written.
|
/// Which of the two files an installation wrote.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
struct Written {
|
||||||
|
/// The desktop entry.
|
||||||
|
entry: bool,
|
||||||
|
/// The icon.
|
||||||
|
icon: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs the entry and icon under `data_home`, and reports which were written.
|
||||||
///
|
///
|
||||||
/// Nothing is installed where a package already provides the entry in `data_dirs`: one under
|
/// Nothing is installed where a package already provides the entry in `data_dirs`: one under
|
||||||
/// the user's directory takes precedence, and would point the package's menu item at the
|
/// the user's directory takes precedence, and would point the package's menu item at the
|
||||||
|
|
@ -38,17 +56,17 @@ fn install(
|
||||||
appdir: &Path,
|
appdir: &Path,
|
||||||
data_home: &Path,
|
data_home: &Path,
|
||||||
data_dirs: &[PathBuf],
|
data_dirs: &[PathBuf],
|
||||||
) -> std::io::Result<bool> {
|
) -> std::io::Result<Written> {
|
||||||
let entry_name = format!("{ID}.desktop");
|
let entry_name = format!("{ID}.desktop");
|
||||||
let icon_name = format!("{ID}.svg");
|
let icon_name = format!("{ID}.svg");
|
||||||
if data_dirs
|
if data_dirs
|
||||||
.iter()
|
.iter()
|
||||||
.any(|dir| dir.join("applications").join(&entry_name).exists())
|
.any(|dir| dir.join("applications").join(&entry_name).exists())
|
||||||
{
|
{
|
||||||
return Ok(false);
|
return Ok(Written::default());
|
||||||
}
|
}
|
||||||
let Some(target) = appimage.to_str() else {
|
let Some(target) = appimage.to_str() else {
|
||||||
return Ok(false);
|
return Ok(Written::default());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write each only when it differs, so an AppImage that has not moved touches nothing.
|
// Write each only when it differs, so an AppImage that has not moved touches nothing.
|
||||||
|
|
@ -63,12 +81,39 @@ fn install(
|
||||||
entry.as_bytes(),
|
entry.as_bytes(),
|
||||||
)?;
|
)?;
|
||||||
let wrote_icon = write_if_changed(
|
let wrote_icon = write_if_changed(
|
||||||
&data_home
|
&data_home.join(THEME).join("scalable/apps").join(&icon_name),
|
||||||
.join("icons/hicolor/scalable/apps")
|
|
||||||
.join(&icon_name),
|
|
||||||
&icon,
|
&icon,
|
||||||
)?;
|
)?;
|
||||||
Ok(wrote_entry || wrote_icon)
|
Ok(Written {
|
||||||
|
entry: wrote_entry,
|
||||||
|
icon: wrote_icon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tells desktops already running that the icon theme under `theme` has a new icon.
|
||||||
|
///
|
||||||
|
/// A shell reads the theme's directories when it starts and does not look again. Plasma, running
|
||||||
|
/// since before the icon's directory existed, found the desktop entry and drew a blank where
|
||||||
|
/// the icon belonged. KDE reloads its icons on the signal sent here, and loaders that compare
|
||||||
|
/// times reload when the theme's directory is newer than what they read. Neither is needed for
|
||||||
|
/// the icon to be there at the next login, so a failure is not reported.
|
||||||
|
fn announce_icon(theme: &Path) {
|
||||||
|
if let Ok(directory) = std::fs::File::open(theme) {
|
||||||
|
let _ = directory.set_modified(std::time::SystemTime::now());
|
||||||
|
}
|
||||||
|
// The signal KDE's own programs send after changing icons. Group 0 is the desktop's.
|
||||||
|
let _ = std::process::Command::new("dbus-send")
|
||||||
|
.args([
|
||||||
|
"--session",
|
||||||
|
"--type=signal",
|
||||||
|
"/KIconLoader",
|
||||||
|
"org.kde.KIconLoader.iconChanged",
|
||||||
|
"int32:0",
|
||||||
|
])
|
||||||
|
.stdin(std::process::Stdio::null())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.status();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes `contents` to `path` unless the file already holds them, and reports whether it wrote.
|
/// Writes `contents` to `path` unless the file already holds them, and reports whether it wrote.
|
||||||
|
|
@ -218,9 +263,13 @@ mod tests {
|
||||||
let home = root.join("home");
|
let home = root.join("home");
|
||||||
let installed = home.join("applications").join(&entry);
|
let installed = home.join("applications").join(&entry);
|
||||||
|
|
||||||
assert!(
|
assert_eq!(
|
||||||
install(&appimage, &appdir, &home, &[]).unwrap(),
|
install(&appimage, &appdir, &home, &[]).unwrap(),
|
||||||
"the first run must install the entry"
|
Written {
|
||||||
|
entry: true,
|
||||||
|
icon: true
|
||||||
|
},
|
||||||
|
"the first run must install the entry and the icon"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
std::fs::read_to_string(&installed)
|
std::fs::read_to_string(&installed)
|
||||||
|
|
@ -234,8 +283,9 @@ mod tests {
|
||||||
.exists(),
|
.exists(),
|
||||||
"the icon the entry names must be installed with it"
|
"the icon the entry names must be installed with it"
|
||||||
);
|
);
|
||||||
assert!(
|
assert_eq!(
|
||||||
!install(&appimage, &appdir, &home, &[]).unwrap(),
|
install(&appimage, &appdir, &home, &[]).unwrap(),
|
||||||
|
Written::default(),
|
||||||
"a second run of the same AppImage must write nothing"
|
"a second run of the same AppImage must write nothing"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -244,8 +294,9 @@ mod tests {
|
||||||
std::fs::create_dir_all(system.join("applications")).unwrap();
|
std::fs::create_dir_all(system.join("applications")).unwrap();
|
||||||
std::fs::write(system.join("applications").join(&entry), "").unwrap();
|
std::fs::write(system.join("applications").join(&entry), "").unwrap();
|
||||||
let other_home = root.join("other-home");
|
let other_home = root.join("other-home");
|
||||||
assert!(
|
assert_eq!(
|
||||||
!install(&appimage, &appdir, &other_home, &[system]).unwrap(),
|
install(&appimage, &appdir, &other_home, &[system]).unwrap(),
|
||||||
|
Written::default(),
|
||||||
"an entry a package provides must not be shadowed"
|
"an entry a package provides must not be shadowed"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,8 @@ from its next start.
|
||||||
|
|
||||||
## R-109: The AppImage's window showed the generic Wayland icon
|
## R-109: The AppImage's window showed the generic Wayland icon
|
||||||
|
|
||||||
**Status**: **FIXED** (2026-10-02), on X11. Built as T254. Not yet seen on a Wayland desktop.
|
**Status**: **FIXED** (2026-10-02), on X11; on Plasma under Wayland the entry is found and the
|
||||||
|
icon's redraw in a running shell is not yet confirmed. Built as T254.
|
||||||
|
|
||||||
The owner reported that the window of the AppImage showed the Wayland icon instead of Midi
|
The owner reported that the window of the AppImage showed the Wayland icon instead of Midi
|
||||||
Harbor's. A Wayland desktop does not take an icon from the window. It takes the window's
|
Harbor's. A Wayland desktop does not take an icon from the window. It takes the window's
|
||||||
|
|
@ -155,5 +156,16 @@ shown the window with no icon, showed Midi Harbor's. On the first run the icon w
|
||||||
because that machine's `icons/hicolor` directory belonged to root from an earlier test; the entry
|
because that machine's `icons/hicolor` directory belonged to root from an earlier test; the entry
|
||||||
was installed without it, and the icon followed once the directory was the user's.
|
was installed without it, and the icon followed once the directory was the user's.
|
||||||
|
|
||||||
**Not covered:** a Wayland desktop, which is where it was reported, and a real AppImage built by
|
**On Plasma, a blank icon.** With the released AppImage on Plasma 6 under Wayland the generic
|
||||||
the release.
|
icon was gone and a blank stood in its place. The entry and the icon were both installed, a new
|
||||||
|
KDE process resolved the icon's name to the file (`kiconfinder6`), and KDE's renderer drew it
|
||||||
|
correctly (`ksvgtopng`). The shell itself had been running for two weeks and had read the icon
|
||||||
|
theme's directories when it started, before `hicolor/scalable/apps` existed under the user's data
|
||||||
|
directory, so it found the entry and no icon. After writing the icon the window now sends
|
||||||
|
`org.kde.KIconLoader.iconChanged` on the session bus, which is what KDE's own programs send to
|
||||||
|
have every running program reload its icons, and sets the theme directory's modification time
|
||||||
|
for loaders that compare times. Both are done only when the icon was written.
|
||||||
|
|
||||||
|
**Not covered:** whether the shell redraws the icon on that signal without the window being
|
||||||
|
opened again, which only the owner's desktop can show, and a real AppImage carrying this last
|
||||||
|
change.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue