Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions PadIO/ControllerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -695,11 +695,19 @@ final class ControllerManager: ObservableObject {
return nil
}

/// Returns the sorted union of profile mode names and shared mode names.
/// Returns the sorted union of profile mode names and shared mode names, minus any
/// modes hidden via `hidden_modes` (per-profile or top-level). This is the single
/// source feeding the mode picker and prev/next cycling — the manual switchers.
/// Hidden modes stay reachable via setMode, external context, and default_mode.
///
/// Degenerate guard: if hiding would leave nothing, fall back to the full set so the
/// picker is never an empty, inescapable panel.
private func allModeNames(profile: ProfileConfig, config: MappingConfig) -> [String] {
var names = Set(profile.modes.keys)
if let shared = config.sharedModes { names.formUnion(shared.keys) }
return names.sorted()
let hidden = Set(profile.hiddenModes).union(config.hiddenModes)
let visible = names.subtracting(hidden)
return (visible.isEmpty ? names : visible).sorted()
}

private func switchMode(_ modeName: String, profileName: String) {
Expand Down
14 changes: 13 additions & 1 deletion PadIO/MappingConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ struct MappingConfig: Codable, Sendable {
var aliases: [String: ActionConfig]?
/// Modes shared across profiles — any profile can reference these by name.
var sharedModes: [String: ModeConfig]?
/// Mode names hidden from the mode picker across all profiles (typically shared modes
/// that are context-driven). Hidden modes stay reachable via setMode, external context,
/// and default_mode; only the picker listing is filtered.
var hiddenModes: [String]

enum CodingKeys: String, CodingKey {
case triggerThreshold = "trigger_threshold"
Expand All @@ -107,9 +111,10 @@ struct MappingConfig: Codable, Sendable {
case haptics
case aliases
case sharedModes = "shared_modes"
case hiddenModes = "hidden_modes"
}

init(triggerThreshold: Double?, debugOverlay: Bool?, global: [String: ActionConfig], profiles: [String: ProfileConfig], menus: [String: MenuConfig], haptics: HapticsConfig? = nil, aliases: [String: ActionConfig]? = nil, sharedModes: [String: ModeConfig]? = nil) {
init(triggerThreshold: Double?, debugOverlay: Bool?, global: [String: ActionConfig], profiles: [String: ProfileConfig], menus: [String: MenuConfig], haptics: HapticsConfig? = nil, aliases: [String: ActionConfig]? = nil, sharedModes: [String: ModeConfig]? = nil, hiddenModes: [String] = []) {
self.triggerThreshold = triggerThreshold
self.debugOverlay = debugOverlay
self.global = global
Expand All @@ -118,6 +123,7 @@ struct MappingConfig: Codable, Sendable {
self.haptics = haptics
self.aliases = aliases
self.sharedModes = sharedModes
self.hiddenModes = hiddenModes
}

init(from decoder: Decoder) throws {
Expand All @@ -130,6 +136,7 @@ struct MappingConfig: Codable, Sendable {
haptics = try container.decodeIfPresent(HapticsConfig.self, forKey: .haptics)
aliases = try container.decodeIfPresent([String: ActionConfig].self, forKey: .aliases)
sharedModes = try container.decodeIfPresent([String: ModeConfig].self, forKey: .sharedModes)
hiddenModes = try container.decodeIfPresent([String].self, forKey: .hiddenModes) ?? []
}

static let empty = MappingConfig(triggerThreshold: nil, debugOverlay: nil, global: [:], profiles: [:], menus: [:])
Expand All @@ -148,13 +155,17 @@ struct ProfileConfig: Codable, Sendable {
/// Maps an external context token (e.g. a process name from the herdr bridge)
/// to a mode name. Applied when the token changes; see ControllerManager.
let contextModes: [String: String]
/// Mode names hidden from the mode picker in this profile. Hidden modes stay reachable
/// via setMode, external context, and default_mode; only the picker listing is filtered.
let hiddenModes: [String]

enum CodingKeys: String, CodingKey {
case apps
case defaultMode = "default_mode"
case global
case modes
case contextModes = "context_modes"
case hiddenModes = "hidden_modes"
}

init(from decoder: Decoder) throws {
Expand All @@ -164,6 +175,7 @@ struct ProfileConfig: Codable, Sendable {
global = try container.decodeIfPresent([String: ActionConfig].self, forKey: .global) ?? [:]
modes = try container.decodeIfPresent([String: ModeConfig].self, forKey: .modes) ?? [:]
contextModes = try container.decodeIfPresent([String: String].self, forKey: .contextModes) ?? [:]
hiddenModes = try container.decodeIfPresent([String].self, forKey: .hiddenModes) ?? []
}
}

Expand Down