Merge pull request #442 from lugoues/feature/configurable-media-key-steps

Add configurable step sizes for volume and brightness keys
This commit is contained in:
Hariharan Mudaliar 2026-05-18 21:45:48 +05:30 • committed by GitHub
commit 756b6b8a34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 192 additions and 57 deletions

View file

@ -9051,6 +9051,9 @@
} }
} }
} }
},
"Brightness fine step" : {
}, },
"Brightness HUD" : { "Brightness HUD" : {
"localizations" : { "localizations" : {
@ -9127,6 +9130,9 @@
} }
} }
} }
},
"Brightness step" : {
}, },
"Built-in" : { "Built-in" : {
"comment" : "A label indicating that an animation is a built-in one.", "comment" : "A label indicating that an animation is a built-in one.",
@ -16870,6 +16876,9 @@
} }
} }
} }
},
"Disabled while external display brightness integration is active." : {
}, },
"Disabled while external display integration is active — brightness keys are handled by the external app." : { "Disabled while external display integration is active — brightness keys are handled by the external app." : {
"localizations" : { "localizations" : {
@ -16898,6 +16907,9 @@
} }
} }
} }
},
"Disabled while external display volume integration is active." : {
}, },
"Discrete transitions snap between Green (0–60%), Yellow (60–85%), and Red (85–100%)." : { "Discrete transitions snap between Green (0–60%), Yellow (60–85%), and Red (85–100%)." : {
"comment" : "A description of how the color-coded progress bars work when not using smooth transitions.", "comment" : "A description of how the color-coded progress bars work when not using smooth transitions.",
@ -17175,6 +17187,9 @@
} }
} }
} }
},
"Display Overlay" : {
}, },
"Display Style" : { "Display Style" : {
"localizations" : { "localizations" : {
@ -35276,6 +35291,9 @@
} }
} }
} }
},
"Percent change per key press. Fine step applies when holding Shift+Option." : {
}, },
"Percentage" : { "Percentage" : {
"localizations" : { "localizations" : {
@ -46124,6 +46142,9 @@
} }
} }
} }
},
"Step size" : {
}, },
"Stop" : { "Stop" : {
"localizations" : { "localizations" : {
@ -51018,6 +51039,9 @@
} }
} }
} }
},
"Volume fine step" : {
}, },
"Volume HUD" : { "Volume HUD" : {
"localizations" : { "localizations" : {
@ -51094,6 +51118,9 @@
} }
} }
} }
},
"Volume step" : {
}, },
"W " : { "W " : {
"comment" : "Label for the write data point in the disk usage graph.", "comment" : "Label for the write data point in the disk usage graph.",

View file

@ -56,6 +56,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case timer case timer
case calendar case calendar
case hudAndOSD case hudAndOSD
case systemControls
case battery case battery
case stats case stats
case clipboard case clipboard
@ -75,7 +76,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
switch self { switch self {
case .general, .appearance: return .core case .general, .appearance: return .core
case .media, .liveActivities, .lockScreen, .devices: return .mediaAndDisplay case .media, .liveActivities, .lockScreen, .devices: return .mediaAndDisplay
case .hudAndOSD, .battery: return .system case .systemControls, .hudAndOSD, .battery: return .system
case .timer, .calendar, .notes: return .productivity case .timer, .calendar, .notes: return .productivity
case .clipboard, .screenAssistant, .colorPicker, .shelf, case .clipboard, .screenAssistant, .colorPicker, .shelf,
.downloads, .shortcuts: return .utilities .downloads, .shortcuts: return .utilities
@ -96,7 +97,8 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .extensions: return String(localized: "Extensions") case .extensions: return String(localized: "Extensions")
case .timer: return String(localized: "Timer") case .timer: return String(localized: "Timer")
case .calendar: return String(localized: "Calendar") case .calendar: return String(localized: "Calendar")
case .hudAndOSD: return String(localized: "Controls") case .hudAndOSD: return String(localized: "Display Overlay")
case .systemControls: return String(localized: "Controls")
case .battery: return String(localized: "Battery") case .battery: return String(localized: "Battery")
case .stats: return String(localized: "Stats") case .stats: return String(localized: "Stats")
case .clipboard: return String(localized: "Clipboard") case .clipboard: return String(localized: "Clipboard")
@ -113,27 +115,28 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
var systemImage: String { var systemImage: String {
switch self { switch self {
case .general: return String(localized: "gear") case .general: return "gear"
case .liveActivities: return String(localized: "waveform.path.ecg") case .liveActivities: return "waveform.path.ecg"
case .appearance: return String(localized: "paintpalette") case .appearance: return "paintpalette"
case .lockScreen: return String(localized: "lock.laptopcomputer") case .lockScreen: return "lock.laptopcomputer"
case .media: return String(localized: "play.laptopcomputer") case .media: return "play.laptopcomputer"
case .devices: return String(localized: "headphones") case .devices: return "headphones"
case .extensions: return String(localized: "puzzlepiece.extension") case .extensions: return "puzzlepiece.extension"
case .timer: return String(localized: "timer") case .timer: return "timer"
case .calendar: return String(localized: "calendar") case .calendar: return "calendar"
case .hudAndOSD: return String(localized: "dial.medium.fill") case .hudAndOSD: return "rectangle.inset.filled.on.rectangle"
case .battery: return String(localized: "battery.100.bolt") case .systemControls: return "dial.medium.fill"
case .stats: return String(localized: "chart.xyaxis.line") case .battery: return "battery.100.bolt"
case .clipboard: return String(localized: "clipboard") case .stats: return "chart.xyaxis.line"
case .screenAssistant: return String(localized: "brain.head.profile") case .clipboard: return "clipboard"
case .colorPicker: return String(localized: "eyedropper") case .screenAssistant: return "brain.head.profile"
case .downloads: return String(localized: "square.and.arrow.down") case .colorPicker: return "eyedropper"
case .shelf: return String(localized: "books.vertical") case .downloads: return "square.and.arrow.down"
case .shortcuts: return String(localized: "keyboard") case .shelf: return "books.vertical"
case .notes: return String(localized: "note.text") case .shortcuts: return "keyboard"
case .terminal: return String(localized: "apple.terminal") case .notes: return "note.text"
case .about: return String(localized: "info.circle") case .terminal: return "apple.terminal"
case .about: return "info.circle"
} }
} }
@ -148,7 +151,8 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .extensions: return Color(red: 0.557, green: 0.353, blue: 0.957) case .extensions: return Color(red: 0.557, green: 0.353, blue: 0.957)
case .timer: return .red case .timer: return .red
case .calendar: return .cyan case .calendar: return .cyan
case .hudAndOSD: return .indigo case .hudAndOSD: return .orange
case .systemControls: return .indigo
case .battery: return Color(red: 0.202, green: 0.783, blue: 0.348, opacity: 1.000) case .battery: return Color(red: 0.202, green: 0.783, blue: 0.348, opacity: 1.000)
case .stats: return .teal case .stats: return .teal
case .clipboard: return .mint case .clipboard: return .mint
@ -491,6 +495,7 @@ struct SettingsView: View {
.lockScreen, .lockScreen,
.devices, .devices,
// System // System
.systemControls,
.hudAndOSD, .hudAndOSD,
.battery, .battery,
// Productivity // Productivity
@ -745,6 +750,12 @@ struct SettingsView: View {
SettingsSearchEntry(tab: .battery, title: "Full battery style", keywords: ["battery", "style", "compact", "standard"], highlightID: SettingsTab.battery.highlightID(for: "Full battery style")), SettingsSearchEntry(tab: .battery, title: "Full battery style", keywords: ["battery", "style", "compact", "standard"], highlightID: SettingsTab.battery.highlightID(for: "Full battery style")),
SettingsSearchEntry(tab: .battery, title: "Full charge threshold", keywords: ["battery", "threshold", "full"], highlightID: SettingsTab.battery.highlightID(for: "Full charge threshold")), SettingsSearchEntry(tab: .battery, title: "Full charge threshold", keywords: ["battery", "threshold", "full"], highlightID: SettingsTab.battery.highlightID(for: "Full charge threshold")),
// System Controls
SettingsSearchEntry(tab: .systemControls, title: "Step size", keywords: ["brightness", "volume", "step",], highlightID: SettingsTab.systemControls.highlightID(for: "Step size")),
SettingsSearchEntry(tab: .systemControls, title: "Third-party DDC app integration", keywords: ["ddc", "third party", "external", "display", "betterdisplay", "lunar"], highlightID: SettingsTab.systemControls.highlightID(for: "Third-party DDC app integration")),
SettingsSearchEntry(tab: .systemControls, title: "Third-party DDC provider", keywords: ["provider", "betterdisplay", "lunar", "integration", "refresh detection"], highlightID: SettingsTab.systemControls.highlightID(for: "Third-party DDC provider")),
SettingsSearchEntry(tab: .systemControls, title: "Enable external volume control listener", keywords: ["external volume", "ddc volume", "betterdisplay volume", "lunar volume", "disable native volume"], highlightID: SettingsTab.systemControls.highlightID(for: "Enable external volume control listener")),
// HUDs // HUDs
SettingsSearchEntry(tab: .devices, title: "Show Bluetooth device connections", keywords: ["bluetooth", "hud"], highlightID: SettingsTab.devices.highlightID(for: "Show Bluetooth device connections")), SettingsSearchEntry(tab: .devices, title: "Show Bluetooth device connections", keywords: ["bluetooth", "hud"], highlightID: SettingsTab.devices.highlightID(for: "Show Bluetooth device connections")),
SettingsSearchEntry(tab: .devices, title: "Use circular battery indicator", keywords: ["battery", "circular"], highlightID: SettingsTab.devices.highlightID(for: "Use circular battery indicator")), SettingsSearchEntry(tab: .devices, title: "Use circular battery indicator", keywords: ["battery", "circular"], highlightID: SettingsTab.devices.highlightID(for: "Use circular battery indicator")),
@ -767,9 +778,6 @@ struct SettingsView: View {
SettingsSearchEntry(tab: .hudAndOSD, title: "Keyboard Backlight OSD", keywords: ["keyboard", "backlight", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Keyboard Backlight OSD")), SettingsSearchEntry(tab: .hudAndOSD, title: "Keyboard Backlight OSD", keywords: ["keyboard", "backlight", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Keyboard Backlight OSD")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Material", keywords: ["material", "frosted", "liquid", "glass", "solid", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Material")), SettingsSearchEntry(tab: .hudAndOSD, title: "Material", keywords: ["material", "frosted", "liquid", "glass", "solid", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Material")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Icon & Progress Color", keywords: ["color", "icon", "white", "black", "gray", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Icon & Progress Color")), SettingsSearchEntry(tab: .hudAndOSD, title: "Icon & Progress Color", keywords: ["color", "icon", "white", "black", "gray", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Icon & Progress Color")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC app integration", keywords: ["ddc", "third party", "external", "display", "betterdisplay", "lunar"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC app integration")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC provider", keywords: ["provider", "betterdisplay", "lunar", "integration", "refresh detection"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC provider")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Enable external volume control listener", keywords: ["external volume", "ddc volume", "betterdisplay volume", "lunar volume", "disable native volume"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Enable external volume control listener")),
// Media // Media
SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"], highlightID: SettingsTab.media.highlightID(for: "Music Source")), SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"], highlightID: SettingsTab.media.highlightID(for: "Music Source")),
@ -976,6 +984,10 @@ struct SettingsView: View {
SettingsForm(tab: .hudAndOSD) { SettingsForm(tab: .hudAndOSD) {
HUDAndOSDSettingsView() HUDAndOSDSettingsView()
} }
case .systemControls:
SettingsForm(tab: .systemControls) {
SystemControlsView()
}
case .battery: case .battery:
SettingsForm(tab: .battery) { SettingsForm(tab: .battery) {
Charge() Charge()
@ -2192,28 +2204,29 @@ private struct HUDAndOSDSettingsView: View {
} }
} }
} }
// Third-party display integrations (shared across all HUD variants)
ExternalDisplayIntegrationsSection()
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(paneBackgroundColor)
.navigationTitle("Controls")
.onAppear { .onAppear {
if #unavailable(macOS 26.0), verticalHUDMaterial == .liquid { if #unavailable(macOS 26.0), verticalHUDMaterial == .liquid {
verticalHUDMaterial = .frosted verticalHUDMaterial = .frosted
verticalHUDLiquidGlassCustomizationMode = .standard verticalHUDLiquidGlassCustomizationMode = .standard
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.navigationTitle("Display Overlay")
} }
} }
// MARK: - External Display Integrations Settings Section // MARK: - External Display Integrations Settings Section
private struct ExternalDisplayIntegrationsSection: View { private struct SystemControlsView: View {
@Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration
@Default(.thirdPartyDDCProvider) var thirdPartyDDCProvider @Default(.thirdPartyDDCProvider) var thirdPartyDDCProvider
@Default(.enableExternalVolumeControlListener) var enableExternalVolumeControlListener @Default(.enableExternalVolumeControlListener) var enableExternalVolumeControlListener
@Default(.volumeStepPercent) var volumeStepPercent
@Default(.volumeFineStepPercent) var volumeFineStepPercent
@Default(.brightnessStepPercent) var brightnessStepPercent
@Default(.brightnessFineStepPercent) var brightnessFineStepPercent
@ObservedObject private var betterDisplayManager = BetterDisplayManager.shared @ObservedObject private var betterDisplayManager = BetterDisplayManager.shared
@ObservedObject private var lunarManager = LunarManager.shared @ObservedObject private var lunarManager = LunarManager.shared
@ -2284,9 +2297,79 @@ private struct ExternalDisplayIntegrationsSection: View {
var body: some View { var body: some View {
Form { Form {
Section {
Stepper(value: $volumeStepPercent, in: 1...25) {
HStack {
Text("Volume step")
Spacer()
Text("\(volumeStepPercent)%")
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
.settingsHighlight(id: highlightID("Volume step"))
.disabled(enableExternalVolumeControlListener)
Stepper(value: $volumeFineStepPercent, in: 1...25) {
HStack {
Text("Volume fine step")
Spacer()
Text("\(volumeFineStepPercent)%")
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
.settingsHighlight(id: highlightID("Volume fine step"))
.disabled(enableExternalVolumeControlListener)
if enableExternalVolumeControlListener {
Text("Disabled while external display volume integration is active.")
.font(.caption)
.foregroundStyle(.secondary)
}
Stepper(value: $brightnessStepPercent, in: 1...25) {
HStack {
Text("Brightness step")
Spacer()
Text("\(brightnessStepPercent)%")
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
.settingsHighlight(id: highlightID("Brightness step"))
.disabled(enableThirdPartyDDCIntegration)
Stepper(value: $brightnessFineStepPercent, in: 1...25) {
HStack {
Text("Brightness fine step")
Spacer()
Text("\(brightnessFineStepPercent)%")
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
.settingsHighlight(id: highlightID("Brightness fine step"))
.disabled(enableThirdPartyDDCIntegration)
if enableThirdPartyDDCIntegration {
Text("Disabled while external display brightness integration is active.")
.font(.caption)
.foregroundStyle(.secondary)
}
} header: {
Text("Step size")
} footer: {
Text("Percent change per key press. Fine step applies when holding Shift+Option.")
.foregroundStyle(.secondary)
.font(.caption)
}
Section { Section {
Toggle("Enable third-party DDC app integration", isOn: $enableThirdPartyDDCIntegration) Toggle("Enable third-party DDC app integration", isOn: $enableThirdPartyDDCIntegration)
.settingsHighlight(id: highlightID("Third-party DDC app integration")) .settingsHighlight(id: highlightID("Third-party DDC app integration"))
.onChange(of: enableThirdPartyDDCIntegration) { _, _ in
enableExternalVolumeControlListener = false
}
if enableThirdPartyDDCIntegration { if enableThirdPartyDDCIntegration {
Picker("Provider", selection: $thirdPartyDDCProvider) { Picker("Provider", selection: $thirdPartyDDCProvider) {
@ -2347,6 +2430,8 @@ private struct ExternalDisplayIntegrationsSection: View {
} }
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.navigationTitle("Controls")
} }
} }
@ -2502,6 +2587,7 @@ struct HUD: View {
@Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD
@Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD
@Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration @Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration
@Default(.enableExternalVolumeControlListener) var enableExternalVolumeControlListener
@Default(.systemHUDSensitivity) var systemHUDSensitivity @Default(.systemHUDSensitivity) var systemHUDSensitivity
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared @ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared
@ -2639,7 +2725,6 @@ struct HUD: View {
} }
} }
} }
.navigationTitle("Controls")
.onAppear { .onAppear {
accessibilityPermission.refreshStatus() accessibilityPermission.refreshStatus()
} }
@ -7872,7 +7957,6 @@ struct CustomOSDSettings: View {
} }
} }
} }
.navigationTitle("Custom OSD")
.onAppear { .onAppear {
accessibilityPermission.refreshStatus() accessibilityPermission.refreshStatus()
if #unavailable(macOS 26.0), osdMaterial == .liquid { if #unavailable(macOS 26.0), osdMaterial == .liquid {

View file

@ -38,9 +38,18 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
"headphones" "headphones"
] ]
private let standardVolumeStep: Float = 1.0 / 16.0 private var standardVolumeStep: Float {
private let standardBrightnessStep: Float = 1.0 / 16.0 Float(Defaults[.volumeStepPercent]) / 100.0
private let fineStepDivisor: Float = 4.0 }
private var fineVolumeStep: Float {
Float(Defaults[.volumeFineStepPercent]) / 100.0
}
private var standardBrightnessStep: Float {
Float(Defaults[.brightnessStepPercent]) / 100.0
}
private var fineBrightnessStep: Float {
Float(Defaults[.brightnessFineStepPercent]) / 100.0
}
private var volumeEnabled = false private var volumeEnabled = false
private var brightnessEnabled = false private var brightnessEnabled = false
@ -157,7 +166,7 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
} }
} }
let baseStep = stepSize(for: step, base: standardVolumeStep) let baseStep = volumeStep(for: step)
let delta = direction == .up ? baseStep : -baseStep let delta = direction == .up ? baseStep : -baseStep
volumeController.adjust(by: delta) volumeController.adjust(by: delta)
volumeFeedbackPlayer.playIfNeeded() volumeFeedbackPlayer.playIfNeeded()
@ -185,7 +194,7 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
} }
} }
let baseStep = stepSize(for: step, base: standardBrightnessStep) let baseStep = brightnessStep(for: step)
let delta = direction == .up ? baseStep : -baseStep let delta = direction == .up ? baseStep : -baseStep
if modifiers.contains(.command) && keyboardBacklightEnabled { if modifiers.contains(.command) && keyboardBacklightEnabled {
keyboardBacklightController.adjust(by: delta) keyboardBacklightController.adjust(by: delta)
@ -336,12 +345,21 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
} }
private extension SystemChangesObserver { private extension SystemChangesObserver {
func stepSize(for step: MediaKeyStep, base: Float) -> Float { func volumeStep(for step: MediaKeyStep) -> Float {
switch step { switch step {
case .standard: case .standard:
return base return standardVolumeStep
case .fine: case .fine:
return base / fineStepDivisor return fineVolumeStep
}
}
func brightnessStep(for step: MediaKeyStep) -> Float {
switch step {
case .standard:
return standardBrightnessStep
case .fine:
return fineBrightnessStep
} }
} }
} }

View file

@ -1134,6 +1134,12 @@ extension Defaults.Keys {
static let systemHUDSensitivity = Key<Int>("systemHUDSensitivity", default: 5) static let systemHUDSensitivity = Key<Int>("systemHUDSensitivity", default: 5)
static let playVolumeChangeFeedback = Key<Bool>("playVolumeChangeFeedback", default: false) static let playVolumeChangeFeedback = Key<Bool>("playVolumeChangeFeedback", default: false)
// Step sizes for hardware media keys (percent of full range, 1-25)
static let volumeStepPercent = Key<Int>("volumeStepPercent", default: 6)
static let volumeFineStepPercent = Key<Int>("volumeFineStepPercent", default: 2)
static let brightnessStepPercent = Key<Int>("brightnessStepPercent", default: 6)
static let brightnessFineStepPercent = Key<Int>("brightnessFineStepPercent", default: 2)
// MARK: Custom OSD Window Feature // MARK: Custom OSD Window Feature
static let enableCustomOSD = Key<Bool>("enableCustomOSD", default: false) static let enableCustomOSD = Key<Bool>("enableCustomOSD", default: false)
static let enableVerticalHUD = Key<Bool>("enableVerticalHUD", default: false) static let enableVerticalHUD = Key<Bool>("enableVerticalHUD", default: false)