Audiomixer zeigt Programme statt Prozesse
Core Audio meldet Prozesse, nicht Programme. Angezeigt wurden deshalb Kennungen wie „com.apple.WebKit.GPU" und Dutzende Systemdienste, die theoretisch Ton ausgeben könnten, es aber nie tun. ProcessNaming fragt erst das laufende Programm, kürzt sonst bekannte Hilfsprozess-Endungen weg und schlägt das übergeordnete Programm nach — Chromes Ton kommt aus einem Helfer, angezeigt gehört Chrome. Symbole gehen denselben Weg, sonst steht neben dem Namen ein Platzhalter. ProcessVisibility zeigt, was klingt, kürzlich klang oder geregelt wird. Die Nachlaufzeit von zwei Minuten ist der eigentliche Punkt: ohne sie verschwindet Spotify beim Pausieren aus der Liste, und man kann die Lautstärke genau dann nicht mehr einstellen, wenn man es will. Geregelte Programme bleiben immer — wer etwas leiser gedreht hat, muss es auch zurückdrehen können. Die Prozentzahl wandert vom Namen zum Regler. Oben war sie genau die Breite, die den Namen abgeschnitten hat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
112
Packages/OnyxKit/Sources/AudioProvider/ProcessNaming.swift
Normal file
112
Packages/OnyxKit/Sources/AudioProvider/ProcessNaming.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
/// Macht aus Core-Audio-Prozessen etwas, das man wiedererkennt.
|
||||
///
|
||||
/// Core Audio meldet **Prozesse**, nicht Programme. Was dort ankommt, heißt
|
||||
/// `com.google.Chrome.helper` oder `com.apple.WebKit.GPU`. Technisch richtig,
|
||||
/// im Mischpult unbrauchbar: wer „Spotify leiser" sucht, sucht nach „Spotify".
|
||||
public enum ProcessNaming {
|
||||
|
||||
/// Endungen, mit denen Programme ihre Hilfsprozesse benennen.
|
||||
///
|
||||
/// Chromes Ton kommt aus einem solchen Prozess — abgegriffen wird also
|
||||
/// zurecht der Helfer, angezeigt aber das Programm dahinter.
|
||||
private static let helperSuffixes = [
|
||||
".helper", ".Helper", ".ServiceExtension", ".Renderer", ".GPU",
|
||||
".plugin", ".xpc", ".Service",
|
||||
]
|
||||
|
||||
/// Die Kennung des Programms, zu dem ein Prozess gehört.
|
||||
public static func parentBundleID(for bundleID: String) -> String {
|
||||
var result = bundleID
|
||||
// Mehrfach kürzen: `com.google.Chrome.helper.Renderer` hat zwei.
|
||||
var changed = true
|
||||
while changed {
|
||||
changed = false
|
||||
for suffix in helperSuffixes where result.hasSuffix(suffix) {
|
||||
result.removeLast(suffix.count)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return result.isEmpty ? bundleID : result
|
||||
}
|
||||
|
||||
/// Der anzuzeigende Name, wenn das Programm selbst nicht gefragt werden kann.
|
||||
public static func fallbackName(for bundleID: String) -> String {
|
||||
guard !bundleID.isEmpty else { return "Unbekannt" }
|
||||
let parent = parentBundleID(for: bundleID)
|
||||
// Der letzte Bestandteil ist bei umgekehrter Domainschreibweise der
|
||||
// Programmname — „com.spotify.client" ist die Ausnahme, die auf einen
|
||||
// generischen Begriff endet.
|
||||
let components = parent.split(separator: ".")
|
||||
// Ein einzelnes Wort ist keine umgekehrte Domain, sondern ein
|
||||
// Prozessname wie `systemsoundserverd`. Den groß zu schreiben macht ihn
|
||||
// nicht lesbarer, nur falsch.
|
||||
guard components.count > 1 else { return parent }
|
||||
guard let last = components.last else { return bundleID }
|
||||
|
||||
let generic = ["client", "app", "mac", "macos", "desktop"]
|
||||
let chosen = generic.contains(last.lowercased()) && components.count > 1
|
||||
? components[components.count - 2]
|
||||
: last
|
||||
|
||||
return chosen.prefix(1).uppercased() + chosen.dropFirst()
|
||||
}
|
||||
|
||||
/// Der beste verfügbare Name: erst das laufende Programm fragen, dann den
|
||||
/// übergeordneten Prozess, dann die Kennung zerlegen.
|
||||
@MainActor
|
||||
public static func displayName(bundleID: String, pid: pid_t) -> String {
|
||||
if let name = NSRunningApplication(processIdentifier: pid)?.localizedName {
|
||||
return name
|
||||
}
|
||||
let parent = parentBundleID(for: bundleID)
|
||||
if parent != bundleID,
|
||||
let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: parent) {
|
||||
return FileManager.default.displayName(atPath: url.path)
|
||||
.replacingOccurrences(of: ".app", with: "")
|
||||
}
|
||||
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) {
|
||||
return FileManager.default.displayName(atPath: url.path)
|
||||
.replacingOccurrences(of: ".app", with: "")
|
||||
}
|
||||
return fallbackName(for: bundleID)
|
||||
}
|
||||
|
||||
/// Das Symbol des Programms — auch für Hilfsprozesse.
|
||||
@MainActor
|
||||
public static func icon(bundleID: String, pid: pid_t) -> NSImage? {
|
||||
if let icon = NSRunningApplication(processIdentifier: pid)?.icon { return icon }
|
||||
let parent = parentBundleID(for: bundleID)
|
||||
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: parent) {
|
||||
return NSWorkspace.shared.icon(forFile: url.path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Wer im Mischpult auftaucht.
|
||||
public enum ProcessVisibility {
|
||||
|
||||
/// Wie lange ein verstummtes Programm noch sichtbar bleibt.
|
||||
///
|
||||
/// Ohne diese Nachlaufzeit verschwindet Spotify beim Pausieren aus der
|
||||
/// Liste — und man kann die Lautstärke genau dann nicht mehr einstellen,
|
||||
/// wenn man es will.
|
||||
public static let gracePeriod: TimeInterval = 120
|
||||
|
||||
/// - Parameters:
|
||||
/// - isPlaying: gibt der Prozess gerade Ton aus?
|
||||
/// - secondsSinceHeard: wie lange ist das letzte Mal her? `nil` = noch nie.
|
||||
/// - isManaged: wird seine Lautstärke von Onyx geregelt?
|
||||
public static func shouldShow(isPlaying: Bool,
|
||||
secondsSinceHeard: TimeInterval?,
|
||||
isManaged: Bool) -> Bool {
|
||||
// Geregelte Programme immer: wer eine Lautstärke eingestellt hat, muss
|
||||
// sie auch zurücknehmen können.
|
||||
if isManaged || isPlaying { return true }
|
||||
guard let seconds = secondsSinceHeard else { return false }
|
||||
return seconds <= gracePeriod
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user