Skip to content

Commit a962651

Browse files
committed
Remove NotLogi, and a clean-up of older installs. About > Remove NotLogi lists what goes and takes it away: the agent and its login service, start at login, the launcher and icons, then the program itself (the .deb through one password prompt, the AppImage file; on macOS the bundle to the Trash and its permissions reset, on Windows the setup's uninstaller); settings, profiles and backups stay unless the switch says otherwise. The plan is a list made before anything is touched (uninstall-plan.js, no Electron, tested), carried out by uninstall.js; packaging/uninstall.sh does the same from a terminal, shipped as logimx-uninstall. Linux: leftovers of older installs go at start and on package install: a second launcher and icons from a checkout or an AppImage beside the .deb (two NotLogi in the app grid), a launcher whose program is gone, a user unit shadowing the package's, an old rule and binaries under /etc and /usr/local; a checkout or AppImage started beside the installed .deb leaves launcher and autostart to the package. Start agent at login drives the package's own unit.
1 parent 97fec7b commit a962651

36 files changed

Lines changed: 774 additions & 32 deletions

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Every release of NotLogi (formerly LogiMX), newest first. Downloads are on each
55

66
## Unreleased
77

8+
- Remove NotLogi: About → Remove NotLogi lists what goes and takes it away: the agent and its login service, start at login, the launcher and icons, then the program itself (the .deb through one password prompt, the AppImage file; on macOS the bundle goes to the Trash and its permissions are reset, on Windows the setup's uninstaller runs). Settings, profiles and backups stay unless the switch says otherwise. On Linux `logimx-uninstall [--all]` does the same from a terminal.
9+
- Linux: leftovers of older installs are cleaned up when the app starts and when the package is installed: a second launcher and icons from a checkout or an AppImage beside the .deb (two NotLogi in the app grid), a launcher whose program is gone, a user service shadowing the package's, an old rule and binaries under /etc and /usr/local. Start agent at login now drives the package's own service.
10+
811
## [0.12.5](https://github.com/aabdelghani/notlogi/releases/tag/v0.12.5) - 2026-10-16
912

1013
- Linux on Wayland: 0.12.4 did not open at all on Ubuntu 24.04 and later (#16). The restart onto XWayland went through Electron's relauncher, which leaves the new process unable to use the setuid sandbox, the only sandbox those systems allow, so it died at once and silently. The app now starts itself as a plain child process.

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ Download from the [latest release](https://github.com/aabdelghani/notlogi/releas
4343

4444
Quit Logi Options+, Solaar or logiops while NotLogi runs: they drive the same buttons.
4545

46+
To remove NotLogi: **About → Remove NotLogi** in the app lists what goes and takes it away (settings
47+
stay unless you say so). On Linux, `logimx-uninstall [--all]` does the same from a terminal (installed
48+
by the .deb, and in the repository at `packaging/uninstall.sh`).
49+
4650
## Supported devices
4751

4852
| Device | State |

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ The app follows **MVVM**: Model, view models, views.
3636
- `media.js`: volume and brightness.
3737
- `battery.js`: battery notices.
3838
- `settings.js`: the window's own settings file.
39-
- `autostart.js`, `system.js`: autostart, app icons, updates and the problem report.
39+
- `autostart.js`, `system.js`: autostart, the clean-up of older installs, app icons, updates and the problem report.
40+
- `uninstall.js`, `uninstall-plan.js`: Remove NotLogi. The plan (what goes, by kind of install) has no Electron in it and is tested; `packaging/uninstall.sh` does the same from a terminal.
4041
- `state.js`: what the parts share and change: devices, settings, windows.
4142
- `../flow.js`: Flow. NotLogi on each computer finds the others on the local network (UDP 24871),
4243
pairs with a shared key, and when the pointer is pushed against the edge facing another computer,

‎packaging/deb/build.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mkdir -p "$PKG/DEBIAN" "$PKG/opt/logimx" "$PKG/usr/bin" "$PKG/usr/lib/udev/rules
2727
cp -a "$UNPACKED/." "$PKG/opt/logimx/"
2828
install -m755 "$ROOT/agent/build/logimx-agent" "$PKG/usr/bin/logimx-agent"
2929
install -m755 "$ROOT/agent/build/logimxctl" "$PKG/usr/bin/logimxctl"
30+
install -m755 "$ROOT/packaging/uninstall.sh" "$PKG/usr/bin/logimx-uninstall"
3031
install -m644 "$ROOT/udev/60-logimx.rules" "$PKG/usr/lib/udev/rules.d/60-logimx.rules"
3132
install -m644 "$ROOT/udev/logimx-i2c.conf" "$PKG/usr/lib/modules-load.d/logimx-i2c.conf"
3233
sed 's|^ExecStart=.*|ExecStart=/usr/bin/logimx-agent|' "$ROOT/systemd/logimx.service" \
@@ -74,6 +75,9 @@ cat > "$PKG/DEBIAN/postinst" <<'POST'
7475
#!/bin/sh
7576
set -e
7677
chmod 4755 /opt/logimx/chrome-sandbox 2>/dev/null || true
78+
# what installs made from a checkout, or the app's Install rule button, left outside home: the
79+
# package has its own copies, and a second one only shadows the first
80+
rm -f /etc/udev/rules.d/60-logimx.rules /usr/local/bin/logimx-agent /usr/local/bin/logimxctl /usr/local/lib/systemd/user/logimx.service 2>/dev/null || true
7781
if command -v udevadm >/dev/null 2>&1; then
7882
udevadm control --reload || true
7983
udevadm trigger --subsystem-match=hidraw --action=add || true
@@ -101,6 +105,8 @@ PRERM
101105
cat > "$PKG/DEBIAN/postrm" <<'POSTRM'
102106
#!/bin/sh
103107
set -e
108+
# purge: the rule the app's Install rule button may have placed before the package was installed
109+
[ "$1" = purge ] && rm -f /etc/udev/rules.d/60-logimx.rules 2>/dev/null || true
104110
command -v udevadm >/dev/null 2>&1 && udevadm control --reload || true
105111
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database -q /usr/share/applications || true
106112
exit 0

‎packaging/uninstall.sh‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/bin/sh
2+
# logimx-uninstall: remove NotLogi from this computer, for when the app itself will not open.
3+
# Removes the agent's service, autostart, launcher and icons for this user, then the package or the
4+
# AppImage. Settings, profiles and backups stay unless --all is given.
5+
# logimx-uninstall [--all] [path/to/NotLogi.AppImage]
6+
set -u
7+
ALL=no; APPIMAGE=""
8+
for a in "$@"; do case "$a" in --all) ALL=yes ;; -h|--help) sed -n '2,5p' "$0"; exit 0 ;; *) APPIMAGE="$a" ;; esac; done
9+
H="${HOME:?}"; CFG="${XDG_CONFIG_HOME:-$H/.config}"; SHARE="${XDG_DATA_HOME:-$H/.local/share}"
10+
11+
echo "== stopping NotLogi"
12+
# stopped before it is disabled: a unit with Restart= would start the agent again after a plain kill
13+
systemctl --user daemon-reload >/dev/null 2>&1 || true
14+
systemctl --user stop logimx.service openoptions.service >/dev/null 2>&1 || true
15+
systemctl --user disable logimx.service openoptions.service >/dev/null 2>&1 || true
16+
pkill -x logimx 2>/dev/null; pkill -x logimx-agent 2>/dev/null; sleep 1
17+
pkill -9 -x logimx-agent 2>/dev/null || true
18+
19+
echo "== this user's files"
20+
rm -f "$CFG/systemd/user/logimx.service" "$CFG/systemd/user/openoptions.service" \
21+
"$CFG/autostart/logimx.desktop" "$CFG/autostart/openoptions.desktop" \
22+
"$SHARE/applications/logimx.desktop" "$H/.local/bin/logimx-agent" "$H/.local/bin/logimxctl" \
23+
"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/logimx.sock"
24+
find "$SHARE/icons/hicolor" -path '*/apps/*' \( -name 'logimx*.png' -o -name 'notlogi*.png' -o -iname '*logimx*.png' \) -delete 2>/dev/null
25+
systemctl --user daemon-reload >/dev/null 2>&1 || true
26+
update-desktop-database -q "$SHARE/applications" 2>/dev/null || true
27+
gtk-update-icon-cache -q -f -t "$SHARE/icons/hicolor" 2>/dev/null || true
28+
if [ "$ALL" = yes ]; then
29+
echo "== settings, profiles and backups"
30+
rm -rf "$CFG/logimx" "${XDG_STATE_HOME:-$H/.local/state}/logimx" "$CFG/LogiMX" "${XDG_CACHE_HOME:-$H/.cache}/LogiMX"
31+
fi
32+
33+
[ -n "$APPIMAGE" ] && [ -f "$APPIMAGE" ] && { echo "== $APPIMAGE"; rm -f "$APPIMAGE"; }
34+
35+
# the package, and what was placed outside home by hand or by the Install rule button
36+
SYS="/etc/udev/rules.d/60-logimx.rules /usr/local/bin/logimx-agent /usr/local/bin/logimxctl /usr/local/lib/systemd/user/logimx.service"
37+
NEED=no; for f in $SYS; do [ -e "$f" ] && NEED=yes; done
38+
dpkg -s logimx >/dev/null 2>&1 && NEED=yes
39+
if [ "$NEED" = yes ]; then
40+
echo "== system files (asks for your password)"
41+
if [ "$(id -u)" = 0 ]; then SUDO=""; elif command -v sudo >/dev/null 2>&1 && [ -t 0 ]; then SUDO=sudo; else SUDO=pkexec; fi
42+
$SUDO sh -c "if dpkg -s logimx >/dev/null 2>&1; then apt-get purge -y logimx 2>/dev/null || dpkg -P logimx; fi; rm -f $SYS; udevadm control --reload 2>/dev/null; update-desktop-database -q /usr/share/applications 2>/dev/null; true"
43+
fi
44+
echo "NotLogi removed.$([ "$ALL" = yes ] || echo ' Settings kept in ~/.config/logimx and ~/.config/LogiMX; add --all to remove them.')"

‎ui/main.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ app.setPath('userData', require('path').join(app.getPath('appData'), 'LogiMX'));
2727
if (plat.IS_WIN) app.setAppUserModelId('io.github.aabdelghani.logimx');
2828
// The main process in parts (main/), loaded once the app's name and settings folder are set;
2929
// each one gets what it uses from the others.
30-
const parts = ['env', 'settings', 'window', 'tray', 'agent', 'osd', 'battery', 'emoji', 'ring', 'media', 'bluetooth', 'system', 'autostart', 'i18n'].map(n => require('./main/' + n));
30+
const parts = ['env', 'settings', 'window', 'tray', 'agent', 'osd', 'battery', 'emoji', 'ring', 'media', 'bluetooth', 'system', 'autostart', 'uninstall', 'i18n'].map(n => require('./main/' + n));
3131
const state = require('./main/state');
3232
const ctx = Object.assign({}, ...parts.map(p => p.provide));
3333
parts.forEach(p => p.link(ctx));

‎ui/main/autostart.js‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ const fs = require('fs');
55
const { execFile } = require('child_process');
66
const plat = require('../platform');
77
const ROOT = require('path').join(__dirname, '..'); // the app's own folder
8+
const { isOurIcon } = require('./uninstall-plan');
9+
const list = d => { try { return fs.readdirSync(d); } catch (e) { return []; } };
810

911
// from the other parts of the main process, filled in by link()
10-
let APPIMAGE, PACKAGED, WM_CLASS, launchCmd, loadUi, resPath;
11-
exports.link = ctx => { ({ APPIMAGE, PACKAGED, WM_CLASS, launchCmd, loadUi, resPath } = ctx); };
12+
let APPIMAGE, PACKAGED, WM_CLASS, debInstalled, launchCmd, loadUi, resPath;
13+
exports.link = ctx => { ({ APPIMAGE, PACKAGED, WM_CLASS, debInstalled, launchCmd, loadUi, resPath } = ctx); };
1214

1315
// Artefacts left by an older name of the project: a stale enabled unit points at a binary
1416
// that no longer exists and quietly fails at login.
@@ -27,11 +29,41 @@ function cleanupLegacyAutostart() {
2729
}
2830
}
2931

32+
// Leftovers of other installs of NotLogi in this user's account: a launcher and icons written by a
33+
// checkout or an AppImage beside the package's own (two NotLogi in the app grid), a launcher whose
34+
// program is gone, a user unit saying the same as the package's. An install that still works, and
35+
// is not this one, is left as it is.
36+
const SYSTEM_UNIT = '/usr/lib/systemd/user/logimx.service';
37+
function cleanupLeftovers() {
38+
const home = os.homedir();
39+
const launcher = path.join(home, '.local', 'share', 'applications', 'logimx.desktop');
40+
const iconDir = path.join(home, '.local', 'share', 'icons', 'hicolor');
41+
// the package's launcher serves, whether this is the package or something started beside it
42+
const deb = (PACKAGED && !APPIMAGE && fs.existsSync('/usr/share/applications/logimx.desktop')) || debInstalled();
43+
let drop = deb;
44+
if (!drop) { try { const m = /^Exec=(?:"([^"]+)"|(\S+))/m.exec(fs.readFileSync(launcher, 'utf8')); drop = !!m && !fs.existsSync(m[1] || m[2]); } catch (e) {} }
45+
if (drop) {
46+
try { fs.unlinkSync(launcher); execFile('update-desktop-database', [path.dirname(launcher)], () => {}); } catch (e) {}
47+
if (deb) {
48+
for (const size of list(iconDir)) for (const f of list(path.join(iconDir, size, 'apps'))) if (isOurIcon(f)) { try { fs.unlinkSync(path.join(iconDir, size, 'apps', f)); } catch (e) {} }
49+
execFile('gtk-update-icon-cache', ['-q', '-f', '-t', iconDir], () => {});
50+
}
51+
}
52+
const unit = path.join(home, '.config', 'systemd', 'user', 'logimx.service');
53+
try {
54+
if (fs.existsSync(SYSTEM_UNIT) && /^ExecStart=\/usr\/bin\/logimx-agent\s*$/m.test(fs.readFileSync(unit, 'utf8'))) {
55+
fs.unlinkSync(unit);
56+
execFile('systemctl', ['--user', 'daemon-reload'], () => execFile('systemctl', ['--user', 'enable', 'logimx.service'], () => {}));
57+
}
58+
} catch (e) {}
59+
}
60+
3061
// Re-apply the login setting every time the app starts, so a rename, a moved checkout or a
3162
// hand-edited unit cannot leave 'start at login' switched on but broken.
3263
function ensureAutostart() {
3364
if (!plat.IS_LINUX) { try { if (loadUi().autostart) plat.setLoginItem(true); } catch (e) {} return; }
3465
cleanupLegacyAutostart();
66+
cleanupLeftovers();
3567
// a unit written by an AppImage points into a mount that no longer exists: rewrite or drop it
3668
try {
3769
const unitPath = path.join(os.homedir(), '.config', 'systemd', 'user', 'logimx.service');
@@ -78,13 +110,20 @@ function setAutostart(on) {
78110
const unitDir = path.join(os.homedir(), '.config', 'systemd', 'user');
79111
const unitPath = path.join(unitDir, 'logimx.service');
80112
const agentBin = on ? stableAgentBin() : null;
81-
if (agentBin) {
113+
if (agentBin === '/usr/bin/logimx-agent' && fs.existsSync(SYSTEM_UNIT)) {
114+
// the package's own unit: enabled as it is, and a copy of this user's saying the same goes
115+
try { fs.unlinkSync(unitPath); } catch (e) {}
116+
execFile('systemctl', ['--user', 'daemon-reload'], () => execFile('systemctl', ['--user', 'enable', 'logimx.service'], () => {}));
117+
} else if (agentBin) {
82118
const unit = `[Unit]\nDescription=NotLogi agent for MX Master and MX Keys devices\nAfter=graphical-session.target\nPartOf=graphical-session.target\n\n[Service]\nType=simple\nExecStart=${agentBin}\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=graphical-session.target\n`;
83119
try {
84120
fs.mkdirSync(unitDir, { recursive: true });
85121
fs.writeFileSync(unitPath, unit);
86122
execFile('systemctl', ['--user', 'daemon-reload'], () => execFile('systemctl', ['--user', 'enable', 'logimx.service'], () => {}));
87123
} catch (e) {}
124+
} else if (!on && fs.existsSync(SYSTEM_UNIT)) {
125+
execFile('systemctl', ['--user', 'disable', 'logimx.service'], () => {}); // the package's unit: off at login, still running now
126+
if (unitPointsAtMissingBinary(unitPath)) removeAgentUnit(unitPath);
88127
} else if (unitPointsAtMissingBinary(unitPath)) {
89128
// nothing durable to point a unit at, and the one on disk is already broken. The desktop
90129
// entry starts the app, which starts its own agent, so drop the unit rather than let it
@@ -102,6 +141,7 @@ function iconName() {
102141
function ensureDesktopEntry() {
103142
if (!plat.IS_LINUX) return; // the installer made the shortcuts
104143
if (PACKAGED && !APPIMAGE) return; // the .deb installs its own entry
144+
if (debInstalled()) return; // the .deb is installed beside this: its entry serves (cleanupLeftovers drops ours)
105145
try {
106146
const iconDir = path.join(os.homedir(), '.local', 'share', 'icons', 'hicolor', '256x256', 'apps');
107147
const appDir = path.join(os.homedir(), '.local', 'share', 'applications');
@@ -120,4 +160,4 @@ function ensureDesktopEntry() {
120160
} catch (e) {}
121161
}
122162

123-
exports.provide = { LEGACY_NAMES, cleanupLegacyAutostart, ensureAutostart, stableAgentBin, unitPointsAtMissingBinary, removeAgentUnit, setAutostart, ensureDesktopEntry };
163+
exports.provide = { LEGACY_NAMES, cleanupLegacyAutostart, cleanupLeftovers, ensureAutostart, stableAgentBin, unitPointsAtMissingBinary, removeAgentUnit, setAutostart, ensureDesktopEntry };

‎ui/main/env.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Where NotLogi runs from: the platform, the app's own folder, how it is launched, the agent's socket.
22
const { app } = require('electron');
33
const path = require('path');
4+
const fs = require('fs');
45
const plat = require('../platform');
56
const ROOT = require('path').join(__dirname, '..'); // the app's own folder
67

@@ -13,9 +14,14 @@ const APPIMAGE = process.env.APPIMAGE || '';
1314
const WM_CLASS = 'notlogi';
1415
// files shipped next to the app: repo root in development, resources/ in a package
1516
const resPath = (...p) => PACKAGED ? path.join(process.resourcesPath, ...p) : path.join(ROOT, '..', ...p);
17+
// The .deb's app, when it is installed: an AppImage or a checkout started beside it leaves the
18+
// launcher and autostart to the package, or the app grid shows two NotLogi and the wrong one
19+
// starts at login (one install at a time; the package wins).
20+
const DEB_APP = '/opt/logimx/logimx';
21+
const debInstalled = () => process.execPath !== DEB_APP && fs.existsSync(DEB_APP) && fs.existsSync('/usr/share/applications/logimx.desktop');
1622
// how to launch this very app again (autostart, desktop entry)
17-
const launchCmd = () => APPIMAGE ? `"${APPIMAGE}"` : PACKAGED ? process.execPath : `${process.execPath} ${ROOT} --no-sandbox --class=${WM_CLASS}`;
23+
const launchCmd = () => debInstalled() ? DEB_APP : APPIMAGE ? `"${APPIMAGE}"` : PACKAGED ? process.execPath : `${process.execPath} ${ROOT} --no-sandbox --class=${WM_CLASS}`;
1824

1925
const SOCKET = plat.agentEndpoint();
2026

21-
exports.provide = { PACKAGED, APPIMAGE, WM_CLASS, resPath, launchCmd, SOCKET };
27+
exports.provide = { PACKAGED, APPIMAGE, WM_CLASS, resPath, launchCmd, debInstalled, SOCKET };

0 commit comments

Comments
 (0)