Files
voice-cat/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift

1420 lines
60 KiB
Swift
Raw Normal View History

import AppKit
import VoiceCatCore
// MARK: - Channel tree node
private final class ChannelNode: NSObject {
let channel: Channel
var children: [ChannelNode]
init(_ channel: Channel, children: [ChannelNode] = []) {
self.channel = channel
self.children = children
}
}
// MARK: - Window controller
final class MainWindowController: NSWindowController, NSWindowDelegate {
// MARK: - Owned client
private let client: VoiceCatClient
private let selfUserId: UInt32
private let nickname: String
// MARK: - State
private var currentChannelId: UInt32 = 0
private var channels: [Channel] = []
private var users: [UInt32: User] = [:]
private var talkingUsers: Set<UInt32> = []
private var ownPermissions = Permissions(canCreateTempChannel: false, canKick: false, canBan: false,
canMoveUsers: false, canAdminAccounts: false, isAdmin: false)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
internal var micStreamId: UInt32 = 0
private var screenStreamId: UInt32 = 0
private var screenCapture: ScreenAudioCapture?
// Last app/exclusion choice from the share picker; reused as the default next time.
private var screenAudioSelection: ScreenAudioSelection = .default
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
internal var pttKeyCode: UInt16 = 0x60 // F8
private var pttMonitor: Any?
private var serverMuted = false
private var serverDeafened = false
private var channelTree: [ChannelNode] = []
private var displayedUsers: [User] = []
private var displayedAccounts: [Account] = []
private var adminMenuItem: NSMenuItem?
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
private var voiceMenuItem: NSMenuItem?
private var messagesMenuItem: NSMenuItem?
private var settingsMenuItem: NSMenuItem?
private var pmWindows: [UInt32: PrivateMessageWindowController] = [:]
private var settingsWindowController: SettingsWindowController?
// MARK: - Audio settings state (source of truth read/written by SettingsWindowController)
internal var selectedInputMode: VoiceCatInputMode = .voiceActivation
internal var vadThresholdValue: Float = 0.05
internal var selectedInputDeviceId: String?
// MARK: - UI components
private let channelOutlineView = NSOutlineView()
private let userTableView = NSTableView()
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
private let logTextView: NSTextView = {
let tv = NSTextView()
tv.isEditable = false
tv.isSelectable = true
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
tv.isAutomaticQuoteSubstitutionEnabled = false
tv.textContainerInset = NSSize(width: 4, height: 4)
return tv
}()
private let statusLabel = NSTextField(labelWithString: "")
private let composeField = NSTextField()
private let sendButton = NSButton()
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// MARK: - Toolbar controls (kept as fields so we can update their state)
private var joinVoiceButton: NSButton?
private var joinVoiceItem: NSToolbarItem?
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
private var shareScreenButton: NSButton?
private var shareScreenItem: NSToolbarItem?
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
private var muteButton: NSButton?
private var deafenButton: NSButton?
private var outputVolumeSlider: NSSlider?
// MARK: - Init
init(client: VoiceCatClient, selfUserId: UInt32, nickname: String) {
self.client = client
self.selfUserId = selfUserId
self.nickname = nickname
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 900, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.title = "VoiceCat — \(nickname)"
window.minSize = NSSize(width: 640, height: 480)
window.center()
super.init(window: window)
window.delegate = self
buildUI()
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
buildToolbar()
wireEvents()
bootstrap()
}
required init?(coder: NSCoder) { fatalError() }
deinit {
NSLog("[VoiceCatMac] MainWindowController deinit — client and event handlers are gone")
}
// MARK: - UI construction
private func buildUI() {
guard let contentView = window?.contentView else { return }
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// Outer split: left panel (channels+users) | right panel (unified log)
let outerSplit = NSSplitView()
outerSplit.isVertical = true
outerSplit.dividerStyle = .thin
outerSplit.translatesAutoresizingMaskIntoConstraints = false
// Left panel: channel outline + user table stacked vertically
let innerSplit = NSSplitView()
innerSplit.isVertical = false
innerSplit.dividerStyle = .thin
innerSplit.translatesAutoresizingMaskIntoConstraints = false
let channelPanel = buildChannelPanel()
let userPanel = buildUserPanel()
innerSplit.addArrangedSubview(channelPanel)
innerSplit.addArrangedSubview(userPanel)
innerSplit.setHoldingPriority(.defaultLow, forSubviewAt: 0)
innerSplit.setHoldingPriority(.defaultLow + 1, forSubviewAt: 1)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// Right panel: unified log (chat + activity in one text view)
let logPanel = buildLogPanel()
outerSplit.addArrangedSubview(innerSplit)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
outerSplit.addArrangedSubview(logPanel)
outerSplit.setHoldingPriority(.defaultLow, forSubviewAt: 0)
outerSplit.setHoldingPriority(.defaultLow + 1, forSubviewAt: 1)
let statusBar = buildStatusBar()
let composeBar = buildComposeBar()
contentView.addSubview(outerSplit)
contentView.addSubview(statusBar)
contentView.addSubview(composeBar)
NSLayoutConstraint.activate([
outerSplit.topAnchor.constraint(equalTo: contentView.topAnchor),
outerSplit.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
outerSplit.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
outerSplit.bottomAnchor.constraint(equalTo: statusBar.topAnchor),
statusBar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
statusBar.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
statusBar.bottomAnchor.constraint(equalTo: composeBar.topAnchor),
statusBar.heightAnchor.constraint(equalToConstant: 24),
composeBar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
composeBar.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
composeBar.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
composeBar.heightAnchor.constraint(equalToConstant: 36),
])
// Set initial divider positions so panels get reasonable space instead of
// collapsing to zero. These are applied after the window is on screen.
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
DispatchQueue.main.async { [weak self, weak outerSplit, weak innerSplit] in
// outerSplit is vertical (left|right): divide at 40% from left
outerSplit?.setPosition(360, ofDividerAt: 0)
// innerSplit is horizontal (channels on top, users below): divide at 60% from top
innerSplit?.setPosition(260, ofDividerAt: 0)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
_ = self
}
}
private func buildChannelPanel() -> NSView {
let col = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("channel"))
channelOutlineView.addTableColumn(col)
channelOutlineView.headerView = nil
channelOutlineView.outlineTableColumn = col
channelOutlineView.dataSource = self
channelOutlineView.delegate = self
channelOutlineView.doubleAction = #selector(channelDoubleClicked)
channelOutlineView.target = self
channelOutlineView.setAccessibilityLabel("Channel list")
let sv = NSScrollView()
sv.documentView = channelOutlineView
sv.hasVerticalScroller = true
sv.borderType = .noBorder
sv.translatesAutoresizingMaskIntoConstraints = false
let menu = NSMenu()
menu.delegate = self
channelOutlineView.menu = menu
return sv
}
private func buildUserPanel() -> NSView {
let col = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("user"))
userTableView.addTableColumn(col)
userTableView.headerView = nil
userTableView.dataSource = self
userTableView.delegate = self
userTableView.doubleAction = #selector(userDoubleClicked)
userTableView.target = self
userTableView.setAccessibilityLabel("Users in current channel")
let menu = NSMenu()
menu.delegate = self
userTableView.menu = menu
let sv = NSScrollView()
sv.documentView = userTableView
sv.hasVerticalScroller = true
sv.borderType = .noBorder
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
private func buildLogPanel() -> NSView {
logTextView.setAccessibilityLabel("Chat and activity log")
logTextView.setAccessibilityHelp("Combined history of chat messages and activity events")
let sv = NSScrollView()
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
sv.documentView = logTextView
sv.hasVerticalScroller = true
sv.borderType = .noBorder
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}
private func buildStatusBar() -> NSView {
let bar = NSView()
bar.translatesAutoresizingMaskIntoConstraints = false
statusLabel.translatesAutoresizingMaskIntoConstraints = false
statusLabel.textColor = .secondaryLabelColor
statusLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize)
bar.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.leadingAnchor.constraint(equalTo: bar.leadingAnchor, constant: 8),
statusLabel.trailingAnchor.constraint(equalTo: bar.trailingAnchor, constant: -8),
statusLabel.centerYAnchor.constraint(equalTo: bar.centerYAnchor),
])
let sep = NSBox()
sep.boxType = .separator
sep.translatesAutoresizingMaskIntoConstraints = false
bar.addSubview(sep)
NSLayoutConstraint.activate([
sep.topAnchor.constraint(equalTo: bar.topAnchor),
sep.leadingAnchor.constraint(equalTo: bar.leadingAnchor),
sep.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
sep.heightAnchor.constraint(equalToConstant: 1),
])
return bar
}
private func buildComposeBar() -> NSView {
let bar = NSView()
bar.translatesAutoresizingMaskIntoConstraints = false
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
composeField.placeholderString = "Type a message to the current channel…"
composeField.setAccessibilityLabel("Compose channel message")
composeField.target = self; composeField.action = #selector(sendClicked)
sendButton.title = "Send"
sendButton.bezelStyle = .rounded
sendButton.target = self; sendButton.action = #selector(sendClicked)
sendButton.setAccessibilityLabel("Send message")
sendButton.keyEquivalent = "\r"
let sep = NSBox(); sep.boxType = .separator; sep.translatesAutoresizingMaskIntoConstraints = false
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
[sep, composeField, sendButton].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
bar.addSubview(v)
}
NSLayoutConstraint.activate([
sep.topAnchor.constraint(equalTo: bar.topAnchor),
sep.leadingAnchor.constraint(equalTo: bar.leadingAnchor),
sep.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
sep.heightAnchor.constraint(equalToConstant: 1),
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
composeField.leadingAnchor.constraint(equalTo: bar.leadingAnchor, constant: 8),
composeField.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -6),
composeField.centerYAnchor.constraint(equalTo: bar.centerYAnchor),
sendButton.trailingAnchor.constraint(equalTo: bar.trailingAnchor, constant: -8),
sendButton.centerYAnchor.constraint(equalTo: bar.centerYAnchor),
sendButton.widthAnchor.constraint(equalToConstant: 60),
])
return bar
}
private func hstack(_ views: [NSView]) -> NSStackView {
let s = NSStackView(views: views)
s.orientation = .horizontal
s.spacing = 8
s.alignment = .centerY
return s
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// MARK: - Toolbar
private enum ToolbarID {
static let joinVoice = NSToolbarItem.Identifier("vc.joinVoice")
static let shareScreen = NSToolbarItem.Identifier("vc.shareScreen")
static let mute = NSToolbarItem.Identifier("vc.mute")
static let deafen = NSToolbarItem.Identifier("vc.deafen")
static let outputVolume = NSToolbarItem.Identifier("vc.outputVolume")
}
private func buildToolbar() {
let toolbar = NSToolbar(identifier: "VoiceCatMainToolbar")
toolbar.delegate = self
toolbar.displayMode = .iconAndLabel
toolbar.showsBaselineSeparator = true
window?.toolbar = toolbar
}
private func makeToolbarButton(symbol: String, label: String, action: Selector,
accessibilityLabel: String) -> NSButton {
let btn = NSButton()
btn.bezelStyle = .inline
btn.image = NSImage(systemSymbolName: symbol, accessibilityDescription: accessibilityLabel)
btn.imagePosition = .imageLeading
btn.title = label
btn.font = .systemFont(ofSize: NSFont.smallSystemFontSize)
btn.target = self
btn.action = action
btn.setAccessibilityLabel(accessibilityLabel)
return btn
}
// MARK: - Event wiring
private func wireEvents() {
client.onEvent = { [weak self] event in self?.handleEvent(event) }
client.onLevel = { [weak self] streamId, rms in self?.handleLevel(streamId, rms) }
pttMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp]) { [weak self] event in
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
guard let self, self.selectedInputMode == .pushToTalk,
self.micStreamId != 0, event.keyCode == self.pttKeyCode else { return event }
self.client.setPushToTalk(event.type == .keyDown)
return nil
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// Observe PM window close so we can drop the controller from pmWindows.
NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification, object: nil, queue: .main
) { [weak self] note in
guard let self,
let window = note.object as? NSWindow,
let wc = window.windowController as? PrivateMessageWindowController else { return }
self.pmWindows.removeValue(forKey: wc.otherUserId)
}
}
// MARK: - Bootstrap
private func bootstrap() {
channels = client.listChannels()
let allUsers = client.listUsers()
users.removeAll()
for u in allUsers {
users[u.id] = u
if u.id == selfUserId { currentChannelId = u.channelId }
}
ownPermissions = client.getPermissions()
NSLog("[VoiceCatMac] bootstrap: channels=%d users=%d perms{admin=%d kick=%d} currentChannelId=%u",
channels.count, allUsers.count,
ownPermissions.isAdmin, ownPermissions.canKick, currentChannelId)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// Apply initial output volume (default 80% matches Windows client)
client.setOutputVolume(0.8)
refreshChannelTree()
refreshUserList()
updateStatusLabel()
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
buildSettingsMenuItem()
buildVoiceMenu()
buildMessagesMenu()
buildAdminMenu()
addActivity("Connected to server as \(nickname)")
}
// MARK: - Event handling
private func handleEvent(_ event: VoiceCatEvent) {
NSLog("[VoiceCatMac] event type=%d result=%d userId=%u channelId=%u streamId=%u text=%@",
event.type.rawValue, event.result.rawValue, event.userId, event.channelId,
event.streamId, event.text ?? "(nil)")
switch event.type {
case .channelList:
channels = client.listChannels()
let allUsers = client.listUsers()
users.removeAll()
for u in allUsers {
users[u.id] = u
if u.id == selfUserId { currentChannelId = u.channelId }
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
refreshChannelTree(); refreshUserList()
case .userJoined:
let u = User(id: event.userId,
nickname: event.text ?? "User#\(event.userId)",
isGuest: true,
channelId: event.channelId,
selfMicMuted: false, selfDeafened: false,
serverMuted: false, serverDeafened: false)
users[event.userId] = u
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
refreshChannelTree(); refreshUserList()
if event.channelId == currentChannelId && event.userId != selfUserId {
addActivity("\(u.nickname) joined the channel")
}
case .userLeft:
let nick = users[event.userId]?.nickname ?? "User#\(event.userId)"
let wasHere = users[event.userId]?.channelId == currentChannelId && event.userId != selfUserId
users.removeValue(forKey: event.userId)
talkingUsers.remove(event.userId)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
refreshChannelTree(); refreshUserList()
if wasHere { addActivity("\(nick) left the channel") }
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
if let pmWin = pmWindows[event.userId] {
pmWin.appendActivity("\(nick) disconnected from server")
}
case .userUpdated:
let allUsers = client.listUsers()
users.removeAll()
for u in allUsers {
users[u.id] = u
if u.id == selfUserId {
currentChannelId = u.channelId
applyServerMuteState(muted: u.serverMuted, deafened: u.serverDeafened)
}
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
refreshChannelTree(); refreshUserList()
case .joinResult:
if event.result == .ok {
currentChannelId = event.channelId
if let self_ = users[selfUserId] {
users[selfUserId] = User(id: self_.id, nickname: self_.nickname, isGuest: self_.isGuest,
channelId: event.channelId,
selfMicMuted: self_.selfMicMuted, selfDeafened: self_.selfDeafened,
serverMuted: self_.serverMuted, serverDeafened: self_.serverDeafened)
}
refreshChannelTree(); refreshUserList(); updateStatusLabel()
let name = channels.first(where: { $0.id == event.channelId })?.name ?? "Channel #\(event.channelId)"
addActivity("Joined \(name)")
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
NSAccessibility.post(element: logTextView, notification: .announcementRequested,
userInfo: [.announcement: "Joined \(name)", .priority: NSAccessibilityPriorityLevel.medium])
} else {
addActivity("Could not join channel: \(event.text ?? "\(event.result)")")
}
case .genericResult:
let prefix = event.result == .ok ? "Success" : "Failed"
let detail = event.text.map { ": \($0)" } ?? ""
addActivity("\(prefix)\(detail) (\(event.result))")
case .accountList:
addActivity("Account list updated")
case .textMessage:
appendChatMessage(event)
case .talkState:
let talking = event.u32a == 1
if talking { talkingUsers.insert(event.userId) } else { talkingUsers.remove(event.userId) }
refreshUserList()
if talking && event.userId != selfUserId,
let u = users[event.userId], u.channelId == currentChannelId {
addActivity("\(u.nickname) started talking")
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
NSAccessibility.post(element: logTextView, notification: .announcementRequested,
userInfo: [.announcement: "\(u.nickname) started talking",
.priority: NSAccessibilityPriorityLevel.medium])
}
case .streamStarted:
// Our own SCREEN_AUDIO stream is now live on the server begin capture.
if event.userId == selfUserId && event.streamId == screenStreamId {
startScreenCapture()
}
guard let u = users[event.userId], u.channelId == currentChannelId else { break }
let streams = client.listUserStreams(event.userId)
let kind = streams.first(where: { $0.id == event.streamId })?.kind
let kindStr = kind == .screenAudio ? "screen audio" : kind == .auxDevice ? "aux device" : "microphone"
addActivity("\(u.nickname) started \(kindStr) stream")
case .streamStopped:
if let u = users[event.userId], u.channelId == currentChannelId {
addActivity("\(u.nickname) stopped a stream")
}
case .disconnected:
handleDisconnected(event)
default:
break
}
}
private func handleLevel(_ streamId: UInt32, _ rms: Float) {
guard streamId == micStreamId else { return }
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
settingsWindowController?.updateLevel(rms: rms)
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// MARK: - Chat / unified log
private func appendChatMessage(_ event: VoiceCatEvent) {
let time: String
if event.timestampUnixMs > 0 {
let date = Date(timeIntervalSince1970: Double(event.timestampUnixMs) / 1000.0)
time = DateFormatter.localizedString(from: date, dateStyle: .none, timeStyle: .short)
} else {
time = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .short)
}
let sender = nickname(for: event.userId)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
if event.textScope == .private {
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// Route PMs to per-conversation windows. For our own outgoing PM, ev.channelId
// carries the recipient user ID; for incoming, ev.userId is the sender.
let otherId = event.userId == selfUserId ? event.channelId : event.userId
let win = getOrOpenPmWindow(otherId)
win.appendMessage(time: time, isSelf: event.userId == selfUserId, sender: sender,
text: event.text ?? "")
if event.userId != selfUserId {
addActivity("Private message from \(sender)")
}
} else {
let line = "[\(time)] \(sender): \(event.text ?? "")\n"
logTextView.textStorage?.append(NSAttributedString(string: line))
logTextView.scrollToEndOfDocument(nil)
}
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// MARK: - Activity log (appended to the unified log in gray)
private func addActivity(_ text: String) {
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
let time = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .short)
let line = "[\(time)] \(text)\n"
let attributed = NSAttributedString(string: line, attributes: [
.foregroundColor: NSColor.secondaryLabelColor,
])
logTextView.textStorage?.append(attributed)
logTextView.scrollToEndOfDocument(nil)
}
// MARK: - UI refresh
private func refreshChannelTree() {
channelTree = buildChannelTree(channels)
channelOutlineView.reloadData()
channelOutlineView.expandItem(nil, expandChildren: true)
}
private func buildChannelTree(_ channels: [Channel]) -> [ChannelNode] {
var nodeMap: [UInt32: ChannelNode] = [:]
for ch in channels { nodeMap[ch.id] = ChannelNode(ch) }
var roots: [ChannelNode] = []
for ch in channels.sorted(by: { $0.name < $1.name }) {
if ch.parentId == 0 {
roots.append(nodeMap[ch.id]!)
} else if let parent = nodeMap[ch.parentId] {
parent.children.append(nodeMap[ch.id]!)
}
}
return roots.sorted { $0.channel.name < $1.channel.name }
}
private func refreshUserList() {
displayedUsers = users.values
.filter { $0.channelId == currentChannelId }
.sorted { $0.nickname < $1.nickname }
userTableView.reloadData()
updateStatusLabel()
}
private func updateStatusLabel() {
var suffix = ""
if serverMuted { suffix += " [server muted]" }
if serverDeafened { suffix += " [server deafened]" }
if currentChannelId == 0 {
statusLabel.stringValue = "Connected as \(nickname)\(suffix) — not in a channel."
return
}
let chanName = channels.first(where: { $0.id == currentChannelId })?.name ?? "Channel #\(currentChannelId)"
let count = users.values.filter { $0.channelId == currentChannelId }.count
statusLabel.stringValue = "Connected as \(nickname)\(suffix)\(chanName) (\(count) user\(count == 1 ? "" : "s"))"
}
private func applyServerMuteState(muted: Bool, deafened: Bool) {
if muted && !serverMuted { addActivity("You have been server-muted") }
if deafened && !serverDeafened { addActivity("You have been server-deafened") }
if !muted && serverMuted { addActivity("Server mute cleared") }
if !deafened && serverDeafened { addActivity("Server deafen cleared") }
serverMuted = muted; serverDeafened = deafened
updateStatusLabel()
}
private func handleDisconnected(_ event: VoiceCatEvent) {
let msg = event.text.map { "Disconnected: \($0)" } ?? "Disconnected from server."
statusLabel.stringValue = msg
addActivity(msg)
channelTree = []; channelOutlineView.reloadData()
displayedUsers = []; userTableView.reloadData()
users.removeAll(); talkingUsers.removeAll()
stopScreenCapture()
currentChannelId = 0; micStreamId = 0; screenStreamId = 0
composeField.isEnabled = false; sendButton.isEnabled = false
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
joinVoiceButton?.isEnabled = false
shareScreenButton?.isEnabled = false
muteButton?.isEnabled = false
deafenButton?.isEnabled = false
// Close all PM windows
for (_, pmWin) in pmWindows { pmWin.close() }
pmWindows.removeAll()
}
// MARK: - Admin menu
private func buildAdminMenu() {
guard ownPermissions.canAdminAccounts || ownPermissions.isAdmin else { return }
let adminMenu = NSMenu(title: "Admin")
let accountsItem = NSMenuItem(title: "Server accounts…", action: #selector(openAccounts), keyEquivalent: "")
accountsItem.target = self
adminMenu.addItem(accountsItem)
let topItem = NSMenuItem(title: "Admin", action: nil, keyEquivalent: "")
topItem.submenu = adminMenu
adminMenuItem = topItem
NSApp.mainMenu?.addItem(topItem)
}
@objc private func openAccounts() {
presentSheet(AccountsSheet(client: client))
}
// MARK: - Channel actions
@objc private func channelDoubleClicked() {
guard let node = channelOutlineView.item(atRow: channelOutlineView.clickedRow) as? ChannelNode else { return }
joinChannelRequest(node.channel.id)
}
private func joinChannelRequest(_ channelId: UInt32) {
guard channelId != currentChannelId else { return }
let channel = channels.first(where: { $0.id == channelId })
if channel?.passwordProtected == true {
let sheet = PasswordPromptSheet(prompt: "Password for channel \"\(channel!.name)\":")
sheet.onComplete = { [weak self] pw in
self?.client.joinChannel(channelId, password: pw)
}
presentSheet(sheet)
} else {
client.joinChannel(channelId, password: nil)
}
}
// MARK: - User actions
@objc private func userDoubleClicked() {
openUserTuning(row: userTableView.clickedRow)
}
private func openUserTuning(row: Int) {
guard row >= 0, row < displayedUsers.count else { return }
let user = displayedUsers[row]
presentSheet(PerUserTuningSheet(client: client, userId: user.id, nickname: user.nickname))
}
// MARK: - Voice controls
@objc private func micToggleClicked() {
if micStreamId == 0 {
let (result, streamId) = client.startStream(StreamDescriptor(kind: .mic, deviceId: nil, label: "Microphone"))
if result == .ok {
micStreamId = streamId
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
if let devId = selectedInputDeviceId {
client.setInputDevice(streamId: streamId, deviceId: devId)
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
client.setInputMode(selectedInputMode)
if selectedInputMode == .voiceActivation {
client.setVadThreshold(vadThresholdValue)
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
setVoiceJoinedState(true)
addActivity("Joined voice — microphone active")
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
NSAccessibility.post(element: logTextView, notification: .announcementRequested,
userInfo: [.announcement: "Joined voice", .priority: NSAccessibilityPriorityLevel.medium])
} else {
addActivity("Failed to start microphone: \(result)")
}
} else {
client.setPushToTalk(false)
client.stopStream(micStreamId)
micStreamId = 0
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
settingsWindowController?.resetLevel()
setVoiceJoinedState(false)
addActivity("Left voice")
}
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
/// Update toolbar button labels/states to reflect whether voice is active. Mirrors the
/// Windows client's `SetVoiceJoinedState`.
private func setVoiceJoinedState(_ joined: Bool) {
joinVoiceButton?.title = joined ? "Leave Voice" : "Join Voice"
joinVoiceButton?.image = NSImage(systemSymbolName: joined ? "mic.fill" : "mic",
accessibilityDescription: joined ? "Leave Voice" : "Join Voice")
// The text shown beneath a custom-view toolbar item comes from the NSToolbarItem's
// label, not the inner button's title update it too or the label stays "Join Voice".
joinVoiceItem?.label = joined ? "Leave Voice" : "Join Voice"
joinVoiceItem?.paletteLabel = joined ? "Leave Voice" : "Join Voice"
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
joinVoiceButton?.setAccessibilityLabel(joined ? "Leave Voice — stop sending microphone audio"
: "Join Voice — start sending microphone audio")
muteButton?.isEnabled = joined
deafenButton?.isEnabled = joined
}
@objc private func screenAudioClicked() {
if screenStreamId == 0 {
// Let the user pick what to share (apps to include/exclude, screen-reader audio)
// before we announce anything. The picker remembers the previous choice.
let sheet = ScreenSharePickerSheet(selection: screenAudioSelection)
sheet.onComplete = { [weak self] selection in
guard let self, let selection else { return } // nil = cancelled
self.screenAudioSelection = selection
self.beginScreenAudioShare()
}
presentSheet(sheet)
} else {
stopScreenCapture()
client.stopStream(screenStreamId)
screenStreamId = 0
setShareScreenButton(active: false)
addActivity("Stopped sharing screen audio")
}
}
/// Announce the SCREEN_AUDIO stream with the chosen selection in hand. ScreenCaptureKit
/// capture starts once the server's StreamAnnounceResult lands (the .streamStarted event),
/// when the effective audio config and thus the channel count is known. See
/// startScreenCapture.
private func beginScreenAudioShare() {
let (result, streamId) = client.startStream(StreamDescriptor(kind: .screenAudio, deviceId: nil, label: "Desktop audio"))
if result == .ok {
screenStreamId = streamId
setShareScreenButton(active: true)
addActivity("Starting screen audio share…")
} else {
addActivity("Failed to start screen audio: \(result)")
}
}
private func setShareScreenButton(active: Bool) {
shareScreenButton?.title = active ? "Stop Screen Audio" : "Share Screen Audio"
shareScreenButton?.image = NSImage(
systemSymbolName: active ? "rectangle.on.rectangle.angled.fill" : "rectangle.on.rectangle.angled",
accessibilityDescription: active ? "Stop Screen Audio" : "Share Screen Audio")
// Update the toolbar item's label too (see setVoiceJoinedState) the inner button's
// title alone doesn't change the text shown beneath a custom-view toolbar item.
shareScreenItem?.label = active ? "Stop Screen Audio" : "Share Screen Audio"
shareScreenItem?.paletteLabel = active ? "Stop Screen Audio" : "Share Screen Audio"
}
/// Start ScreenCaptureKit capture for our own SCREEN_AUDIO stream. Called from the
/// `.streamStarted` event handler, where the effective audio config is available. Captures
/// in the channel's mode (stereo when the channel is stereo) and feeds 20 ms PCM frames.
private func startScreenCapture() {
guard screenStreamId != 0, screenCapture == nil else { return }
let (cfgResult, cfg) = client.getStreamAudioConfig(userId: selfUserId, streamId: screenStreamId)
let channels: UInt32 = (cfgResult == .ok && cfg?.stereo == true) ? 2 : 1
let streamId = screenStreamId
let capture = ScreenAudioCapture(channels: channels, selection: screenAudioSelection) { [weak self] pcm, samples, ch in
self?.client.feedPcm(streamId: streamId, pcm: pcm, samplesPerChannel: samples, channels: ch)
}
screenCapture = capture
Task { @MainActor in
do {
try await capture.start()
addActivity("Started sharing screen audio (\(channels == 2 ? "stereo" : "mono"))"
+ "\(Self.scopeSuffix(for: screenAudioSelection))")
} catch {
// Most commonly: Screen Recording permission denied. Roll back the stream.
screenCapture = nil
if screenStreamId != 0 {
client.stopStream(screenStreamId)
screenStreamId = 0
setShareScreenButton(active: false)
}
addActivity("Screen audio capture failed — grant Screen Recording in System "
+ "Settings ▸ Privacy & Security, then try again. (\(error))")
}
}
}
private func stopScreenCapture() {
screenCapture?.stop()
screenCapture = nil
}
/// A short human-readable description of the share scope for the activity log.
private static func scopeSuffix(for selection: ScreenAudioSelection) -> String {
var parts: [String] = []
switch selection.scope {
case .entireDesktop: break
case .onlyApps(let ids): if !ids.isEmpty { parts.append("only \(ids.count) app(s)") }
case .allExcept(let ids): if !ids.isEmpty { parts.append("excluding \(ids.count) app(s)") }
}
// For .onlyApps the screen reader is already excluded, so don't claim it twice.
if selection.excludeScreenReader {
if case .onlyApps = selection.scope {} else { parts.append("no screen reader") }
}
return parts.isEmpty ? "" : "" + parts.joined(separator: ", ")
}
@objc private func muteChanged() {
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
let muted = muteButton?.state == .on
let deafened = deafenButton?.state == .on
client.setSelfMute(micMuted: muted, deafened: deafened)
muteButton?.image = NSImage(systemSymbolName: muted ? "mic.slash.fill" : "mic.fill",
accessibilityDescription: muted ? "Unmute microphone" : "Mute microphone")
}
@objc private func deafenChanged() {
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
let muted = muteButton?.state == .on
let deafened = deafenButton?.state == .on
client.setSelfMute(micMuted: muted, deafened: deafened)
deafenButton?.image = NSImage(systemSymbolName: deafened ? "speaker.slash.fill" : "speaker.wave.2.fill",
accessibilityDescription: deafened ? "Undeafen" : "Deafen")
}
// MARK: - Text send
@objc private func sendClicked() {
let msg = composeField.stringValue.trimmingCharacters(in: .whitespaces)
guard !msg.isEmpty else { return }
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
guard currentChannelId != 0 else { return }
client.sendText(scope: .channel, targetId: currentChannelId, text: msg)
composeField.stringValue = ""
}
// MARK: - M5: Moderation helpers
private func moveUser(_ user: User) {
let sheet = MoveUserSheet(channels: channels, currentChannelId: user.channelId)
sheet.onComplete = { [weak self] channelId in
guard let channelId else { return }
self?.client.moveUser(user.id, toChannel: channelId)
}
presentSheet(sheet)
}
private func kickUser(_ user: User) {
let sheet = InputSheet(title: "Kick user", prompt: "Reason:", defaultValue: "Kicked by admin")
sheet.onComplete = { [weak self] reason in
self?.client.kickUser(user.id, reason: reason)
}
presentSheet(sheet)
}
private func banUser(_ user: User) {
let sheet = BanUserSheet(nickname: user.nickname)
sheet.onComplete = { [weak self] (reason, expiresMs) in
self?.client.banUser(user.id, reason: reason, expiresUnixMs: expiresMs)
}
presentSheet(sheet)
}
private func setPermissions(_ user: User) {
let sheet = PermissionsSheet(nickname: user.nickname, current: Permissions(
canCreateTempChannel: false, canKick: false, canBan: false,
canMoveUsers: false, canAdminAccounts: false, isAdmin: false))
sheet.onComplete = { [weak self] perms in
guard let perms else { return }
self?.client.setPermission(user.id, perms: perms)
}
presentSheet(sheet)
}
private func toggleServerMute(_ user: User) {
client.setServerMute(user.id, muted: !user.serverMuted, deafened: user.serverDeafened)
}
private func toggleServerDeafen(_ user: User) {
client.setServerMute(user.id, muted: user.serverMuted, deafened: !user.serverDeafened)
}
// MARK: - Helpers
private func nickname(for userId: UInt32) -> String {
if userId == selfUserId { return nickname }
return users[userId]?.nickname ?? "User#\(userId)"
}
private func presentSheet(_ vc: NSViewController) {
if let cvc = window?.contentViewController {
cvc.presentAsSheet(vc)
} else {
let cvc = NSViewController()
cvc.view = window!.contentView!
window?.contentViewController = cvc
cvc.presentAsSheet(vc)
}
}
private func selectedUserInTable() -> User? {
let row = userTableView.selectedRow
guard row >= 0, row < displayedUsers.count else { return nil }
return displayedUsers[row]
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// MARK: - Voice menu (V / S / M / D)
private func buildVoiceMenu() {
let menu = NSMenu(title: "Voice")
let joinItem = NSMenuItem(title: "Join Voice", action: #selector(micToggleClicked),
keyEquivalent: "v")
joinItem.keyEquivalentModifierMask = [.command, .shift]
joinItem.target = self
menu.addItem(joinItem)
let screenItem = NSMenuItem(title: "Share Screen Audio", action: #selector(screenAudioClicked),
keyEquivalent: "s")
screenItem.keyEquivalentModifierMask = [.command, .shift]
screenItem.target = self
menu.addItem(screenItem)
menu.addItem(.separator())
let muteItem = NSMenuItem(title: "Mute", action: #selector(muteClicked),
keyEquivalent: "m")
muteItem.keyEquivalentModifierMask = [.command, .shift]
muteItem.target = self
menu.addItem(muteItem)
let deafenItem = NSMenuItem(title: "Deafen", action: #selector(deafenClicked),
keyEquivalent: "d")
deafenItem.keyEquivalentModifierMask = [.command, .shift]
deafenItem.target = self
menu.addItem(deafenItem)
let topItem = NSMenuItem(title: "Voice", action: nil, keyEquivalent: "")
topItem.submenu = menu
voiceMenuItem = topItem
NSApp.mainMenu?.addItem(topItem)
}
/// Toolbar/menu toggle for self-mute. Flips the toolbar button state and applies.
@objc private func muteClicked() {
muteButton?.state = (muteButton?.state == .on) ? .off : .on
muteChanged()
}
/// Toolbar/menu toggle for self-deafen. Flips the toolbar button state and applies.
@objc private func deafenClicked() {
deafenButton?.state = (deafenButton?.state == .on) ? .off : .on
deafenChanged()
}
// MARK: - Messages menu (N for New Private Message)
private func buildMessagesMenu() {
let menu = NSMenu(title: "Messages")
let newPmItem = NSMenuItem(title: "New Private Message…", action: #selector(openNewPmDialog),
keyEquivalent: "n")
newPmItem.keyEquivalentModifierMask = [.command, .shift]
newPmItem.target = self
menu.addItem(newPmItem)
let topItem = NSMenuItem(title: "Messages", action: nil, keyEquivalent: "")
topItem.submenu = menu
messagesMenuItem = topItem
NSApp.mainMenu?.addItem(topItem)
}
// MARK: - Private messaging
/// Get or open a PM window for the given user. If one already exists, bring it to front.
@discardableResult
private func getOrOpenPmWindow(_ userId: UInt32) -> PrivateMessageWindowController {
if let existing = pmWindows[userId], existing.window != nil {
existing.window?.makeKeyAndOrderFront(nil)
return existing
}
let nick = nickname(for: userId)
let wc = PrivateMessageWindowController(client: client, otherUserId: userId,
nickname: nick, selfUserId: selfUserId)
pmWindows[userId] = wc
wc.showWindow(nil)
return wc
}
/// Open the PM picker dialog (Messages New Private Message). Lists all server users.
@objc private func openNewPmDialog() {
let others = users.values
.filter { $0.id != selfUserId }
.sorted { $0.nickname < $1.nickname }
if others.isEmpty {
let alert = NSAlert()
alert.messageText = "No other users are connected to the server."
alert.addButton(withTitle: "OK")
guard let window else { return }
alert.beginSheetModal(for: window) { _ in }
return
}
let sheet = UserPickerSheet(users: others)
sheet.onComplete = { [weak self] userId in
guard let userId else { return }
self?.getOrOpenPmWindow(userId)
}
presentSheet(sheet)
}
/// Open a PM window from the user context menu.
private func openPmWindow(_ user: User) {
getOrOpenPmWindow(user.id)
}
// MARK: - Output volume
@objc private func outputVolumeSliderChanged() {
let gain = Float(outputVolumeSlider?.doubleValue ?? 80) / 100.0
client.setOutputVolume(gain)
}
// MARK: - Settings window (,)
/// Add "Settings" to the app menu (the first menu, named after the app). Mac convention:
/// Settings lives in the app menu with , key equivalent.
private func buildSettingsMenuItem() {
guard let appMenu = NSApp.mainMenu?.item(at: 0)?.submenu else { return }
let settingsItem = NSMenuItem(title: "Settings…", action: #selector(openSettings),
keyEquivalent: ",")
settingsItem.target = self
// Insert before the Quit item (or at the end if Quit isn't found)
if let quitIndex = appMenu.items.firstIndex(where: { $0.keyEquivalent == "q" }) {
appMenu.insertItem(settingsItem, at: quitIndex)
appMenu.insertItem(NSMenuItem.separator(), at: quitIndex)
} else {
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(settingsItem)
}
settingsMenuItem = settingsItem
}
@objc private func openSettings() {
if let existing = settingsWindowController, existing.window != nil {
existing.window?.makeKeyAndOrderFront(nil)
return
}
let wc = SettingsWindowController(client: client, mainController: self)
settingsWindowController = wc
wc.showWindow(nil)
}
// MARK: - NSWindowDelegate
func windowWillClose(_ notification: Notification) {
if let mon = pttMonitor { NSEvent.removeMonitor(mon) }
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
NotificationCenter.default.removeObserver(self)
// Close all PM windows
for (_, pmWin) in pmWindows { pmWin.close() }
pmWindows.removeAll()
// Close settings window
settingsWindowController?.close()
settingsWindowController = nil
// Remove app menus we added
if let item = voiceMenuItem { NSApp.mainMenu?.removeItem(item) }
if let item = messagesMenuItem { NSApp.mainMenu?.removeItem(item) }
if let item = adminMenuItem { NSApp.mainMenu?.removeItem(item) }
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// Remove Settings menu item + separator from app menu
if let appMenu = NSApp.mainMenu?.item(at: 0)?.submenu {
if let item = settingsMenuItem { appMenu.removeItem(item) }
// Remove the separator we inserted before Quit
for mi in appMenu.items where mi.isSeparatorItem {
if let quitIndex = appMenu.items.firstIndex(where: { $0.keyEquivalent == "q" }),
let sepIndex = appMenu.items.firstIndex(of: mi),
sepIndex == quitIndex - 1 {
appMenu.removeItem(at: sepIndex)
break
}
}
}
client.setPushToTalk(false)
stopScreenCapture()
if screenStreamId != 0 { client.stopStream(screenStreamId) }
if micStreamId != 0 { client.stopStream(micStreamId) }
client.onLevel = nil
client.onEvent = nil
client.disconnect()
}
}
// MARK: - NSOutlineView DataSource / Delegate
extension MainWindowController: NSOutlineViewDataSource, NSOutlineViewDelegate {
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if item == nil { return channelTree.count }
return (item as! ChannelNode).children.count
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if item == nil { return channelTree[index] }
return (item as! ChannelNode).children[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
!(item as! ChannelNode).children.isEmpty
}
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
let node = item as! ChannelNode
let ch = node.channel
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
let userCount = users.values.filter { $0.channelId == ch.id }.count
var label = "\(ch.name) (\(userCount))"
if ch.passwordProtected { label += " [pw]" }
if ch.id == currentChannelId { label += "" }
let id = NSUserInterfaceItemIdentifier("channelCell")
let cell = outlineView.makeView(withIdentifier: id, owner: nil) as? NSTableCellView
?? makeTextCellView(identifier: id)
cell.textField?.stringValue = label
cell.setAccessibilityLabel(label)
return cell
}
func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool { true }
}
// MARK: - NSTableView DataSource / Delegate
extension MainWindowController: NSTableViewDataSource, NSTableViewDelegate {
func numberOfRows(in tableView: NSTableView) -> Int {
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
displayedUsers.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let id = NSUserInterfaceItemIdentifier("cell")
let cell = tableView.makeView(withIdentifier: id, owner: nil) as? NSTableCellView
?? makeTextCellView(identifier: id)
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
let user = displayedUsers[row]
var label = user.nickname
if user.id == selfUserId { label += " (you)" }
if talkingUsers.contains(user.id) { label += " (talking)" }
if user.selfMicMuted || user.serverMuted { label += " (muted)" }
if user.selfDeafened || user.serverDeafened { label += " (deafened)" }
cell.textField?.stringValue = label
cell.setAccessibilityLabel(label)
return cell
}
private func makeTextCellView(identifier: NSUserInterfaceItemIdentifier) -> NSTableCellView {
let cell = NSTableCellView()
cell.identifier = identifier
let tf = NSTextField(labelWithString: "")
tf.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(tf); cell.textField = tf
NSLayoutConstraint.activate([
tf.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 4),
tf.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4),
tf.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
])
return cell
}
}
// MARK: - NSMenuDelegate (context menus)
extension MainWindowController: NSMenuDelegate {
func menuNeedsUpdate(_ menu: NSMenu) {
menu.removeAllItems()
if menu === channelOutlineView.menu {
buildChannelContextMenu(menu)
} else if menu === userTableView.menu {
buildUserContextMenu(menu)
}
}
private func buildChannelContextMenu(_ menu: NSMenu) {
let row = channelOutlineView.clickedRow
if row >= 0, let node = channelOutlineView.item(atRow: row) as? ChannelNode {
menu.addItem(withTitle: "Join", action: #selector(channelContextJoin), keyEquivalent: "").target = self
menu.addItem(.separator())
if ownPermissions.isAdmin {
let edit = menu.addItem(withTitle: "Edit channel…", action: #selector(channelContextEdit), keyEquivalent: "")
edit.target = self; edit.representedObject = node
let del = menu.addItem(withTitle: "Delete channel…", action: #selector(channelContextDelete), keyEquivalent: "")
del.target = self; del.representedObject = node
menu.addItem(.separator())
}
}
if ownPermissions.canCreateTempChannel || ownPermissions.isAdmin {
menu.addItem(withTitle: "Create channel…", action: #selector(channelContextCreate), keyEquivalent: "").target = self
}
}
private func buildUserContextMenu(_ menu: NSMenu) {
let row = userTableView.clickedRow
guard row >= 0, row < displayedUsers.count else { return }
let user = displayedUsers[row]
let isSelf = user.id == selfUserId
menu.addItem(withTitle: "Adjust volume and noise settings…",
action: #selector(userContextTune), keyEquivalent: "").target = self
if !isSelf {
menu.addItem(.separator())
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
menu.addItem(withTitle: "Send Private Message…",
action: #selector(userContextPm), keyEquivalent: "").target = self
menu.addItem(.separator())
if ownPermissions.canMoveUsers || ownPermissions.isAdmin {
menu.addItem(withTitle: "Move to channel…", action: #selector(userContextMove), keyEquivalent: "").target = self
}
if ownPermissions.canKick || ownPermissions.isAdmin {
menu.addItem(withTitle: "Kick…", action: #selector(userContextKick), keyEquivalent: "").target = self
}
if ownPermissions.canBan || ownPermissions.isAdmin {
menu.addItem(withTitle: "Ban…", action: #selector(userContextBan), keyEquivalent: "").target = self
}
if ownPermissions.isAdmin {
menu.addItem(.separator())
menu.addItem(withTitle: user.serverMuted ? "Server unmute" : "Server mute",
action: #selector(userContextServerMute), keyEquivalent: "").target = self
menu.addItem(withTitle: user.serverDeafened ? "Server undeafen" : "Server deafen",
action: #selector(userContextServerDeafen), keyEquivalent: "").target = self
menu.addItem(withTitle: "Set permissions…", action: #selector(userContextPermissions), keyEquivalent: "").target = self
}
}
}
@objc private func channelContextJoin() {
guard let node = channelOutlineView.item(atRow: channelOutlineView.clickedRow) as? ChannelNode else { return }
joinChannelRequest(node.channel.id)
}
@objc private func channelContextCreate() {
let sheet = ChannelEditSheet(channels: channels, editing: nil)
sheet.onComplete = { [weak self] info in
guard let info else { return }
self?.client.createChannel(info)
}
presentSheet(sheet)
}
@objc private func channelContextEdit(_ sender: NSMenuItem) {
guard let node = sender.representedObject as? ChannelNode else { return }
let ch = node.channel
let info = ChannelEdit(id: ch.id, parentId: ch.parentId, name: ch.name,
topic: ch.topic, passwordProtected: ch.passwordProtected,
password: nil, maxUsers: ch.maxUsers,
sortOrder: 0, audio: AudioConfig())
let sheet = ChannelEditSheet(channels: channels, editing: info)
sheet.onComplete = { [weak self] edited in
guard let edited else { return }
self?.client.editChannel(edited)
}
presentSheet(sheet)
}
@objc private func channelContextDelete(_ sender: NSMenuItem) {
guard let node = sender.representedObject as? ChannelNode else { return }
let alert = NSAlert()
alert.messageText = "Delete channel?"
alert.informativeText = "Delete \"\(node.channel.name)\"? This cannot be undone."
alert.addButton(withTitle: "Delete"); alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
guard let window else { return }
alert.beginSheetModal(for: window) { [weak self] response in
if response == .alertFirstButtonReturn {
self?.client.deleteChannel(node.channel.id)
}
}
}
@objc private func userContextTune() { openUserTuning(row: userTableView.clickedRow) }
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
@objc private func userContextPm() {
guard let u = selectedUserAt(userTableView.clickedRow) else { return }
openPmWindow(u)
}
@objc private func userContextMove() {
guard let u = selectedUserAt(userTableView.clickedRow) else { return }
moveUser(u)
}
@objc private func userContextKick() {
guard let u = selectedUserAt(userTableView.clickedRow) else { return }
kickUser(u)
}
@objc private func userContextBan() {
guard let u = selectedUserAt(userTableView.clickedRow) else { return }
banUser(u)
}
@objc private func userContextServerMute() {
guard let u = selectedUserAt(userTableView.clickedRow) else { return }
toggleServerMute(u)
}
@objc private func userContextServerDeafen() {
guard let u = selectedUserAt(userTableView.clickedRow) else { return }
toggleServerDeafen(u)
}
@objc private func userContextPermissions() {
guard let u = selectedUserAt(userTableView.clickedRow) else { return }
setPermissions(u)
}
private func selectedUserAt(_ row: Int) -> User? {
guard row >= 0, row < displayedUsers.count else { return nil }
return displayedUsers[row]
}
}
// MARK: - PTT key name helper
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
func keyCodeName(_ keyCode: UInt16) -> String {
let map: [UInt16: String] = [
0x60: "F5", 0x61: "F6", 0x62: "F7", 0x63: "F3", 0x64: "F8", 0x65: "F9",
0x67: "F11", 0x69: "F13", 0x6A: "F16", 0x6B: "F14", 0x6D: "F10", 0x6F: "F12",
0x71: "F15", 0x72: "Help", 0x73: "Home", 0x74: "PgUp", 0x75: "Del",
0x76: "F4", 0x77: "End", 0x78: "F2", 0x79: "PgDn", 0x7A: "F1",
]
return map[keyCode] ?? "Key\(keyCode)"
}
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
// MARK: - NSToolbarDelegate
extension MainWindowController: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[ToolbarID.joinVoice, ToolbarID.shareScreen, .space,
ToolbarID.mute, ToolbarID.deafen, .flexibleSpace,
ToolbarID.outputVolume]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarDefaultItemIdentifiers(toolbar)
}
func toolbar(_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
switch itemIdentifier {
case ToolbarID.joinVoice:
let btn = makeToolbarButton(symbol: "mic", label: "Join Voice",
action: #selector(micToggleClicked),
accessibilityLabel: "Join Voice — start sending microphone audio")
item.view = btn
item.label = "Join Voice"
item.paletteLabel = "Join Voice"
item.toolTip = "Join or leave voice (⌘⇧V)"
joinVoiceButton = btn
joinVoiceItem = item
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
case ToolbarID.shareScreen:
let btn = makeToolbarButton(symbol: "rectangle.on.rectangle.angled", label: "Share Screen Audio",
action: #selector(screenAudioClicked),
accessibilityLabel: "Share Screen Audio")
item.view = btn
item.label = "Share Screen Audio"
item.paletteLabel = "Share Screen Audio"
item.toolTip = "Share screen audio (⌘⇧S)"
shareScreenButton = btn
shareScreenItem = item
feat(macos): UI overhaul -- toolbar, unified log, PM windows, settings window, hotkeys Mirrors the Windows client's UI overhaul (commit 97fa659 + 540ec13) adapted to Mac-native conventions. The main window is now just toolbar + channels + users + chat; audio device settings moved to a modeless Settings window. - NSToolbar: Join Voice, Share Screen Audio, Mute, Deafen (SF Symbol toggle buttons) + Output Volume slider (NSSlider 0-100, default 80). Voice actions, mute/deafen, and output volume moved out of the bottom panel into the toolbar - Audio device settings (input mode, VAD sensitivity, PTT key, device picker, level meter) moved to a new SettingsWindowController -- a modeless window opened via the app menu's "Settings..." (Cmd+,) item. Source-of-truth for audio state lives in MainWindowController so voice start applies settings even before the window has been opened; SettingsWindowController reads from / writes back to those properties and applies changes live when voice is active. Level meter forwarded from handleLevel -> updateLevel(rms:) - Unified log: chat NSTextView + activity NSTableView collapsed into a single NSTextView -- activity events in secondaryLabelColor (gray), chat in default - Private messaging: scope dropdown removed; compose always sends to the current channel. Each PM conversation opens in its own modeless PrivateMessageWindowController. Incoming .textMessage with .private scope routed to the right window; outgoing PMs echoed by server arrive through the same path. "Send Private Message..." added to user context menu. - Messages menu: "New Private Message..." (Cmd+Shift+N) opens a UserPickerSheet listing all server users so you can PM anyone on the server - Channel tree now shows live user counts, e.g. "General (3)"; refreshChannelTree called on .userJoined/.userLeft (was missing) - Voice menu: Join Voice (Cmd+Shift+V), Share Screen Audio (Cmd+Shift+S), Mute (Cmd+Shift+M), Deafen (Cmd+Shift+D) -- NSMenuItem key equivalents with [.command, .shift] mask, dispatched by the responder chain - setOutputVolume(_:) wrapper added to VoiceCatClient.swift (was missing -- the C ABI + C# wrapper shipped in commit 97fa659 but the Swift wrapper was never added); wired end-to-end: toolbar slider -> client.setOutputVolume(gain) Part A -- fixed and verified the previously-uncompiled Swift from the external PCM feed/tap commit (615d2a8): - Rebuilt the macOS xcframework slice (regenerated the module map from current voicecat.h, exposing vc_pcm_sink_cb / vc_stream_feed_pcm / vc_set_pcm_sink) - Fixed feedPcm type bug: size_t imports as Int in Swift not UInt; the original UInt(samplesPerChannel) was wrong - Added VoiceCatPcmSinkCallback typealias -- a Swift-idiomatic public alias for the C vc_pcm_sink_cb so consumers (tests, the macOS app) can declare a sink callback without directly importing the VoiceCatC C module. Mirrors the C# VcPcmSinkCallback delegate - keyCodeName helper deduplicated (was in PttKeyCaptureSheet.swift + MainWindowController.swift -- now shared) Platform-specific adaptations (vs. Windows): NSToolbar instead of ToolStrip; global menu bar + NSMenuItem key equivalents (Cmd not Ctrl, responder-chain dispatched, no custom key monitor needed); PM windows as modeless NSWindows; picker as Mac sheet; gray = secondaryLabelColor; SF Symbols for toolbar icons. swift test 10/10 (4 ExternalPcmTests + 6 VoiceCatClientSmokeTests against a live server); xcodebuild Debug + Release BUILD SUCCEEDED with 0 Swift warnings.
2026-06-20 23:30:52 +02:00
case ToolbarID.mute:
let btn = makeToolbarButton(symbol: "mic.fill", label: "Mute",
action: #selector(muteClicked),
accessibilityLabel: "Mute microphone")
item.view = btn
item.label = "Mute"
item.paletteLabel = "Mute"
item.toolTip = "Mute/unmute mic (⌘⇧M)"
btn.isEnabled = false
muteButton = btn
case ToolbarID.deafen:
let btn = makeToolbarButton(symbol: "speaker.wave.2.fill", label: "Deafen",
action: #selector(deafenClicked),
accessibilityLabel: "Deafen — mute all incoming audio")
item.view = btn
item.label = "Deafen"
item.paletteLabel = "Deafen"
item.toolTip = "Deafen/undeafen (⌘⇧D)"
btn.isEnabled = false
deafenButton = btn
case ToolbarID.outputVolume:
let slider = NSSlider(value: 80, minValue: 0, maxValue: 100, target: self,
action: #selector(outputVolumeSliderChanged))
slider.numberOfTickMarks = 0
slider.setAccessibilityLabel("Output volume")
slider.setAccessibilityHelp("Global playback volume for all incoming audio")
let container = NSView(frame: NSRect(x: 0, y: 0, width: 140, height: 24))
slider.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(slider)
NSLayoutConstraint.activate([
slider.leadingAnchor.constraint(equalTo: container.leadingAnchor),
slider.trailingAnchor.constraint(equalTo: container.trailingAnchor),
slider.centerYAnchor.constraint(equalTo: container.centerYAnchor),
])
item.view = container
item.label = "Output Volume"
item.paletteLabel = "Output Volume"
item.toolTip = "Global output volume"
outputVolumeSlider = slider
default:
return nil
}
return item
}
}