AvengeMedia / AvengeMedia/DankMaterialShell

Launcher starts a new instance of an app that is already running

Open
#3,268 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
QML
Stars
8.1k
Forks
515
Avg merge
1d 8h
Merged PRs (30d)
102

Description

### Feature Description

Picking an already-running app in the launcher runs its desktop entry's `Exec` unconditionally. For most apps nothing visible happens; it seems like the app's own single-instance handling discards the duplicate so the launcher appears to do nothing while the app sits on another workspace. For apps without single-instance handling, though, it tries to start a second copy that fails.

The Dock already seems to do the right thing, but the Launcher does something different.

`Modules/Dock/DockAppButton.qml:288:`
```qml
case "grouped":
if (appData.windowCount === 0) {
// ... launchDesktopEntry
} else {
groupedToplevel.activate();
}
case "window":
windowToplevel.activate();
```

`Modals/DankLauncherV2/Controller.qml:2080:`
```qml
function launchApp(app) {
var entry = _resolveDesktopEntry(app);
if (!entry)
return;
SessionService.launchDesktopEntry(entry); // unconditional
AppUsageHistoryData.addAppUsage(entry);
}
```

To reproduce:
1. Open an app that keeps a single main window (Slack, Signal, Todoist)
2. Switch to another workspace
3. Use the Launcher to "launch" that app

I'd expect the focus to shift to the running window, as clicking it in the Dock does. Instead nothing happens.

Seen on DMS 1.5.3, niri, Wayland.

### Use Case

Every other way of asking for an app already activates: clicking it in the Dock, or a compositor keybind if one is wired. Only the launcher hard wires the request as "start this app." Other desktops that I've used activate here too; picking a running app in macOS's Launchpad or GNOME's app grid brings you to its window. For those of us who don't run a dock, the launcher is the only graphical, quick access entry point, so the one surface that misses a running app is the one we use for everything that doesn't warrant hogging a keybinding.

The failure is worse for apps that destroy their window on close and have no single-instance handling. Seafile is a concrete example: the second copy loses the race for its data-directory lock and shows "Seafile exited unexpectedly", which reads as a crash in an app that is running fine.

```
seaf-daemon.c(385): Failed to lock pidfile .../seaf-daemon.pid: Resource temporarily unavailable
seaf-daemon.c(555): The seafile data directory ... is already used by another Seafile client instance.
```

### Compositor(s)

All compositors

### If Other, please specify

_No response_

### Proposed Solution

`launchApp()` leveraging existing info:

1. If a matching `toplevel` exists → `activate()` it. `ToplevelManager`, per my read of the Dock.
2. If no `toplevel` exists, but there's a tray item for the app → activate that item. The shell already exposes this via the tray IPC target (DMSShellIPC.qml, findTrayItem/activate). For an app that destroys its window on close, that item is the only way to get back to it, AFAIK.
3. Else → `launchDesktopEntry()`, i.e. the current behavior.

I suspect that order matters for step 2: a number of apps implement `Activate` as a toggle, so asking while a window is on screen might hide it. Reaching step 2 only after step 1 finds nothing keeps that case unreachable; it can only ever show a window.

One implementation note that I bumped into while poking around this: Identity probably needs resolving rather than comparing, because window app-ids don't always match desktop entry ids exactly. Measured on just my machine, I have three of five apps that mismatch:

```
┌──────────┬─────────────────────┬────────────────────────────┬──────────────┐
│ App │ Desktop entry ID │ Window app_id │ Exact match? │
├──────────┼─────────────────────┼────────────────────────────┼──────────────┤
│ Signal │ org.signal.Signal │ org.signal.Signal │ yes │
├──────────┼─────────────────────┼────────────────────────────┼──────────────┤
│ Slack │ com.slack.Slack │ com.slack.Slack │ yes │
├──────────┼─────────────────────┼────────────────────────────┼──────────────┤
│ Todoist │ com.todoist.Todoist │ todoist │ no │
├──────────┼─────────────────────┼────────────────────────────┼──────────────┤
│ Seafile │ com.seafile.Client │ com.seafile.seafile-applet │ no │
├──────────┼─────────────────────┼────────────────────────────┼──────────────┤
│ ZenNotes │ zennotes │ ZenNotes │ no (case) │
└──────────┴─────────────────────┴────────────────────────────┴──────────────┘
```

The Dock handles this well, so it's probably already in place.

### Alternatives/Existing Solutions

As mentioned, I think the Dock in DMS itself might be the :easybutton:, and macOS and GNOME both activate from their app grids.

Some users may want the launcher to open a second window (a new browser window, a new terminal). From what I've seen, apps supporting this behavior generally state it explicitly via `Actions=new-window` in the desktop entry which the launcher already surfaces as separate action items. So "activate the app, and use the explicit action for a new window" remains a thing. If that's contentious, a setting would work, though the Dock's behavior suggests activation is already the project's intended default.

For now, I'm working around it using a wrapper script plus a `desktop-entry` override for each app, which won't scale well. :-)

Contributor guide

Open the contributing guide

Research direction

Start in Modals/DankLauncherV2/Controller.qml at launchApp() and compare its behavior with Modules/Dock/DockAppButton.qml:288. Read the referenced ToplevelManager and DMSShellIPC.qml tray lookup paths, then verify that running windows or tray items are activated before falling back to launchDesktopEntry; test the Slack, Signal, Todoist, and Seafile scenarios described in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
linux
Domain
desktop
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.