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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,11 @@

<key>SUAllowsAutomaticUpdates</key>
<true/>

<!-- AppleScript Support -->
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML comments use non-standard syntax for plist files. Plist files typically don't use XML comments within the dictionary. While this may work, consider using standard plist structure without inline comments, or document the AppleScript support in a separate documentation file.

Suggested change
<!-- AppleScript Support -->

Copilot uses AI. Check for mistakes.
<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>Kaset.sdef</string>
</dict>
</plist>
70 changes: 70 additions & 0 deletions App/Kaset.sdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="Kaset Terminology">

<!-- Standard Suite -->
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">

<command name="quit" code="aevtquit" description="Quit the application.">
<cocoa class="NSQuitCommand"/>
</command>

</suite>

<!-- Kaset Suite -->
<suite name="Kaset Suite" code="Kast" description="Commands and classes for controlling Kaset.">

<!-- Playback Commands -->
<command name="play" code="Kastplay" description="Start or resume playback.">
<cocoa class="KasetPlayCommand"/>
</command>

<command name="pause" code="Kastpaus" description="Pause playback.">
<cocoa class="KasetPauseCommand"/>
</command>

<command name="playpause" code="Kasttogg" description="Toggle play/pause state.">
<cocoa class="KasetPlayPauseCommand"/>
</command>

<command name="next track" code="Kastnext" description="Skip to the next track.">
<cocoa class="KasetNextTrackCommand"/>
</command>

<command name="previous track" code="Kastprev" description="Go to the previous track.">
<cocoa class="KasetPreviousTrackCommand"/>
</command>

<command name="set volume" code="Kastvolu" description="Set the playback volume.">
<cocoa class="KasetSetVolumeCommand"/>
<direct-parameter type="integer" description="Volume level (0-100)."/>
</command>

<command name="toggle shuffle" code="Kastshuf" description="Toggle shuffle mode.">
<cocoa class="KasetToggleShuffleCommand"/>
</command>

<command name="cycle repeat" code="Kastrepe" description="Cycle through repeat modes (off, all, one).">
<cocoa class="KasetCycleRepeatCommand"/>
</command>

<command name="toggle mute" code="Kastmute" description="Toggle mute state.">
<cocoa class="KasetToggleMuteCommand"/>
</command>

<command name="get player info" code="Kastinfo" description="Get current player state as JSON.">
<cocoa class="KasetGetPlayerInfoCommand"/>
<result type="text" description="JSON string with player state."/>
</command>

<command name="like track" code="Kastlike" description="Like or unlike the current track.">
<cocoa class="KasetLikeTrackCommand"/>
</command>

<command name="dislike track" code="Kastdslk" description="Dislike or undislike the current track.">
<cocoa class="KasetDislikeTrackCommand"/>
</command>

</suite>

</dictionary>
11 changes: 8 additions & 3 deletions App/KasetApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ struct KasetApp: App {
player.setYTMusicClient(client)
SongLikeStatusManager.shared.setClient(client)

// Set shared instance for AppleScript access
PlayerService.shared = player

// Create account service
let account = AccountService(ytMusicClient: client, authService: auth)

Expand Down Expand Up @@ -125,10 +128,12 @@ struct KasetApp: App {
.environment(\.searchFocusTrigger, self.$searchFocusTrigger)
.environment(\.navigationSelection, self.$navigationSelection)
.environment(\.showCommandBar, self.$showCommandBar)
.task {
// Wire up PlayerService to AppDelegate for dock menu actions
.onAppear {
// Wire up PlayerService to AppDelegate for dock menu and AppleScript actions
// This runs synchronously so AppleScript commands can access playerService immediately
self.appDelegate.playerService = self.playerService

}
Comment on lines +131 to +135
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the playerService assignment from .task to .onAppear changes when the assignment happens but doesn't fully eliminate the race condition. AppleScript commands could theoretically be invoked before onAppear runs (e.g., via automation scripts that launch the app and immediately send commands). Consider setting PlayerService.shared = player immediately after creating the PlayerService instance in the init method (line 88-92) to ensure it's available as early as possible.

Copilot uses AI. Check for mistakes.
.task {
// Check if user is already logged in from previous session
await self.authService.checkLoginStatus()

Expand Down
10 changes: 10 additions & 0 deletions Core/Services/Player/PlayerService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import os
@MainActor
@Observable
final class PlayerService: NSObject, PlayerServiceProtocol {
/// Shared instance for AppleScript access.
///
/// **Safety Invariant:** This property is set exactly once during app initialization
/// in `KasetApp.init()` before any AppleScript commands can be received, and is never
/// modified afterward. The property is `@MainActor`-isolated along with the entire class,
/// ensuring thread-safe access from AppleScript commands (which run on the main thread).
///
/// AppleScript commands should handle the `nil` case gracefully by returning an error
/// to the caller, as there's a brief window during app launch before initialization completes.
static var shared: PlayerService?
/// Current playback state.
enum PlaybackState: Equatable, Sendable {
case idle
Expand Down
Loading