text:
CMAKE_PROGRESS_1 = 1
CMAKE_PROGRESS_2 = 2

repo_name: nogibjj/rusty-deploy

repo_language: Makefile

mime_type: text/plain


text:
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 3.18

# Delete rule output on recipe failure.
.DELETE_ON_ERROR:

#=============================================================================
# Special targets provided by cmake.

# Disable implicit rules so canonical targets will work.
.SUFFIXES:

# Disable VCS-based implicit rules.
% : %,v

# Disable VCS-based implicit rules.
% : RCS/%

# Disable VCS-based implicit rules.
% : RCS/%,v

# Disable VCS-based implicit rules.
% : SCCS/s.%

# Disable VCS-based implicit rules.
% : s.%

.SUFFIXES: .hpux_make_needs_suffix_list

# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s

#Suppress display of executed commands.
$(VERBOSE).SILENT:

# A target that is always out of date.
cmake_force:

.PHONY : cmake_force

#=============================================================================
# Set environment variables for the build.

# The shell in which to execute make rules.
SHELL = /bin/sh

# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake

# The command to remove a file.
RM = /usr/bin/cmake -E rm -f

# Escaping for special characters.
EQUALS = =

# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /workspaces/rusty-deploy/rtorchdist

# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /workspaces/rusty-deploy/rtorchdist/build

# Include any dependencies generated for this target.
include CMakeFiles/check_pytorch.dir/depend.make

# Include the progress variables for this target.
include CMakeFiles/check_pytorch.dir/progress.make

# Include the compile flags for this target's objects.
include CMakeFiles/check_pytorch.dir/flags.make

CMakeFiles/check_pytorch.dir/check_pytorch.cpp.o: CMakeFiles/check_pytorch.dir/flags.make
CMakeFiles/check_pytorch.dir/check_pytorch.cpp.o: ../check_pytorch.cpp
	@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green --progress-dir=/workspaces/rusty-deploy/rtorchdist/build/CMakeFiles --progress-num=$(CMAKE_PROGRESS_1) "Building CXX object CMakeFiles/check_pytorch.dir/check_pytorch.cpp.o"
	/usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o CMakeFiles/check_pytorch.dir/check_pytorch.cpp.o -c /workspaces/rusty-deploy/rtorchdist/check_pytorch.cpp

CMakeFiles/check_pytorch.dir/check_pytorch.cpp.i: cmake_force
	@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green "Preprocessing CXX source to CMakeFiles/check_pytorch.dir/check_pytorch.cpp.i"
	/usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /workspaces/rusty-deploy/rtorchdist/check_pytorch.cpp > CMakeFiles/check_pytorch.dir/check_pytorch.cpp.i

CMakeFiles/check_pytorch.dir/check_pytorch.cpp.s: cmake_force
	@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green "Compiling CXX source to assembly CMakeFiles/check_pytorch.dir/check_pytorch.cpp.s"
	/usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /workspaces/rusty-deploy/rtorchdist/check_pytorch.cpp -o CMakeFiles/check_pytorch.dir/check_pytorch.cpp.s

# Object files for target check_pytorch
check_pytorch_OBJECTS = \
"CMakeFiles/check_pytorch.dir/check_pytorch.cpp.o"

# External object files for target check_pytorch
check_pytorch_EXTERNAL_OBJECTS =

check_pytorch: CMakeFiles/check_pytorch.dir/check_pytorch.cpp.o
check_pytorch: CMakeFiles/check_pytorch.dir/build.make
check_pytorch: /usr/local/lib/libtorch/lib/libtorch.so
check_pytorch: /usr/local/lib/libtorch/lib/libc10.so
check_pytorch: /usr/local/lib/libtorch/lib/libkineto.a
check_pytorch: /usr/local/lib/libtorch/lib/libc10.so
check_pytorch: CMakeFiles/check_pytorch.dir/link.txt
	@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green --bold --progress-dir=/workspaces/rusty-deploy/rtorchdist/build/CMakeFiles --progress-num=$(CMAKE_PROGRESS_2) "Linking CXX executable check_pytorch"
	$(CMAKE_COMMAND) -E cmake_link_script CMakeFiles/check_pytorch.dir/link.txt --verbose=$(VERBOSE)

# Rule to build all files generated by this target.
CMakeFiles/check_pytorch.dir/build: check_pytorch

.PHONY : CMakeFiles/check_pytorch.dir/build

CMakeFiles/check_pytorch.dir/clean:
	$(CMAKE_COMMAND) -P CMakeFiles/check_pytorch.dir/cmake_clean.cmake
.PHONY : CMakeFiles/check_pytorch.dir/clean

CMakeFiles/check_pytorch.dir/depend:
	cd /workspaces/rusty-deploy/rtorchdist/build && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /workspaces/rusty-deploy/rtorchdist /workspaces/rusty-deploy/rtorchdist /workspaces/rusty-deploy/rtorchdist/build /workspaces/rusty-deploy/rtorchdist/build /workspaces/rusty-deploy/rtorchdist/build/CMakeFiles/check_pytorch.dir/DependInfo.cmake --color=$(COLOR)
.PHONY : CMakeFiles/check_pytorch.dir/depend


text:
use actix_web::web::Bytes;
use futures::stream::Stream;
use log::info;
use rtorchdist::logic::self_check_predict;
use rtorchdist::logic::tensor_device_cpu;
use std::pin::Pin;
use std::task::{Context, Poll};
use test_log::test;

// Helper struct to implement Stream for testing
pub struct TestStream {
    data: Vec<u8>,
    read: bool,
}

impl Stream for TestStream {
    type Item = Result<Bytes, actix_web::error::PayloadError>;

    fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
        if self.read {
            Poll::Ready(None)
        } else {
            self.read = true;
            let bytes = Bytes::from(self.data.clone());
            Poll::Ready(Some(Ok(bytes)))
        }
    }
}

#[derive(Debug)]
struct ModelInfo {
    model_path: &'static str,
}

fn get_models() -> Vec<ModelInfo> {
    vec![
        ModelInfo {
            model_path: "model/alexnet.ot",
        },
        ModelInfo {
            model_path: "model/densenet121.ot",
        },
        ModelInfo {
            model_path: "model/inception-v3.ot",
        },
        ModelInfo {
            model_path: "model/mobilenet-v2.ot",
        },
        ModelInfo {
            model_path: "model/resnet18.ot",
        },
        ModelInfo {
            model_path: "model/resnet34.ot",
        },
        ModelInfo {
            model_path: "model/squeezenet1_0.ot",
        },
        ModelInfo {
            model_path: "model/squeezenet1_1.ot",
        },
        ModelInfo {
            model_path: "model/vgg13.ot",
        },
        ModelInfo {
            model_path: "model/vgg16.ot",
        },
        ModelInfo {
            model_path: "model/vgg19.ot",
        },
    ]
}

fn load_model(model_info: &ModelInfo) -> Result<tch::CModule, Box<dyn std::error::Error>> {
    let model = tch::CModule::load(model_info.model_path)?;
    Ok(model)
}

// test that we can load all models
#[test]
fn test_load_models() -> Result<(), Box<dyn std::error::Error>> {
    let models = get_models();
    for model_info in models {
        let model = load_model(&model_info)?;
        //log it
        info!("TEST: Loaded model: {:?}", model);
    }
    Ok(())
}

//test tensor_device_cpu
#[test]
fn test_tensor_device_cpu() -> Result<(), Box<dyn std::error::Error>> {
    let size_str = tensor_device_cpu();
    info!("TEST: Tensor size: {:?}", size_str);
    assert_eq!(size_str, "3");
    Ok(())
}

//tests self_check_predict()
#[test]
fn test_self_check_predict() {
    let prediction = match self_check_predict() {
        Ok(p) => p,
        Err(e) => {
            println!("Error: {:?}", e);
            return;
        }
    };
    println!("TEST: Self check prediction: {:?}", prediction);
    //assert Prediction { probabilities: [0.05008329823613167], classes: ["tarantula"] }
    assert_eq!(prediction.probabilities[0], 0.05008329823613167);
    assert_eq!(prediction.classes[0], "tarantula");
}

/*
pub fn verify_image_contents(image_path: String, fixture_image_path: String) -> Result<(), Box<dyn std::error::Error>> {
    log::info!("Verifying image contents of file: {:?}", image_path);

    let mut image_file = File::open(image_path.clone())?;
    let mut image_contents = Vec::new();
    image_file.read_to_end(&mut image_contents)?;

    let mut fixture_file = File::open(fixture_image_path.clone())?;
    let mut fixture_contents = Vec::new();
    fixture_file.read_to_end(&mut fixture_contents)?;

    let image_checksum = md5::compute(image_contents);
    let fixture_checksum = md5::compute(fixture_contents);
    log::info!("Image checksum: {:?}", image_checksum);
    log::info!("Fixture checksum: {:?}", fixture_checksum);

    if image_checksum != fixture_checksum {
        log::error!("Image contents of file {:?} do not match fixture {:?}. Image checksum: {:?}, Fixture checksum: {:?}", image_path, fixture_image_path, image_checksum, fixture_checksum);
        return Err(format!("Image contents of file {:?} do not match fixture {:?}. Image checksum: {:?}, Fixture checksum: {:?}", image_path, fixture_image_path, image_checksum, fixture_checksum).into());
    }

    log::info!("Image contents of file {:?} match fixture {:?}. Image checksum: {:?}, Fixture checksum: {:?}", image_path, fixture_image_path, image_checksum, fixture_checksum);
    Ok(())
}

 */


text:
/*
Taken from here to ensure I have proper install
https://github.com/LaurentMazare/tch-rs/blob/main/tests/vision_tests.rs
 */
use tch::{nn, vision, Tensor};

#[test]
fn mobilenet() {
    let vs = nn::VarStore::new(tch::Device::Cpu);
    let net = vision::mobilenet::v2(&vs.root(), 1000);
    let img = Tensor::zeros(&[1, 3, 224, 224], tch::kind::FLOAT_CPU);
    let logits = img.apply_t(&net, false);
    assert_eq!(logits.size(), [1, 1000]);
}

#[test]
fn resize() {
    // Check that resizing returns a tensor with the appropriate dimensions.
    let img = Tensor::zeros(&[3, 8, 12], tch::kind::FLOAT_CPU);
    let resized_img = vision::image::resize(&img, 16, 16).unwrap();
    assert_eq!(resized_img.size(), [3, 16, 16]);
    let resized_img = vision::image::resize(&img, 4, 16).unwrap();
    assert_eq!(resized_img.size(), [3, 16, 4]);
    let resized_img = vision::image::resize(&img, 32, 8).unwrap();
    assert_eq!(resized_img.size(), [3, 8, 32]);
}

#[test]
fn resize_preserve_aspect_ratio() {
    // Check that resizing returns a tensor with the appropriate dimensions.
    let img = Tensor::zeros(&[3, 8, 12], tch::kind::FLOAT_CPU);
    let resized_img = vision::image::resize_preserve_aspect_ratio(&img, 16, 16).unwrap();
    assert_eq!(resized_img.size(), [3, 16, 16]);
    let resized_img = vision::image::resize(&img, 4, 16).unwrap();
    assert_eq!(resized_img.size(), [3, 16, 4]);
    let resized_img = vision::image::resize(&img, 32, 8).unwrap();
    assert_eq!(resized_img.size(), [3, 8, 32]);
}


text:
use actix_web::{http::StatusCode, test, App};
use rtorchdist::routes::index;

#[actix_rt::test]
async fn test_index() {
    let mut app = test::init_service(App::new().service(index)).await;
    let req = test::TestRequest::get().uri("/").to_request();
    let resp = test::call_service(&mut app, req).await;

    assert_eq!(resp.status(), StatusCode::OK);

    let response_body = test::read_body(resp).await;
    let expected_body = "Send an image payload using curl with the following command:\ncurl -X POST -H \"Content-Type: multipart/form-data\" -F \"image=@/path/to/your/image.jpg\" http://127.0.0.1:8080/predict";
    assert_eq!(response_body, expected_body);
}


text:
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>

repo_name: nahive/spotify-notify

repo_language: Swift

mime_type: text/xml


text:
# Spotify Notify
![Build Version](https://img.shields.io/github/release/nahive/spotify-notify.svg)

##### *Inspired by [Spotify Notifications](https://github.com/citruspi/Spotify-Notifications) written in Swift*

## [Download latest release here](https://github.com/nahive/spotify-notify/releases)

## Install via Homebrew

```
brew tap nahive/spotify-notify
brew install --cask spotify-notify --no-quarantine
```

## Features

- [x] Notification sounds (toggle)
- [x] Notification on play/pause (toggle)
- [x] Customizable status bar icon (default, dark and none)
- [x] Launch on login (toggle)
- [x] Album artwork (toggle)
- [x] Toggle disabling of notifications when Spotify is focused (toggle)
- [x] Shortcuts for showing notification (customizable) ✨
- [x] Show progress of a song in notification ✨
- [x] Ability to skip song from notification ✨
- [x] Options to control Spotify from menu bar ✨
- [ ] Customizable options in notification
- [ ] Add localizations

## Notes

When running the app on macOS Mojave (10.14) or higher, the first time you attempt to control Spotify you will be presented with the following dialog box:

You must grant permission for the app to work. Choosing Don't Allow will make the app unable to retrieve any information about the currently playing track.

If you click Don't Allow by accident, you can reset the permissions dialogue using

```
$ tccutil reset AppleEvents
```

Additionally, it is now required to sign the app using a paid developer account. To do this, simply set the signing team and certificate in the General settings of the SpotifyNotify target:

## Building

```
$ git clone https://github.com/nahive/spotify-notify.git
$ cd spotify-notify
$ open SpotifyNotify.xcodeproj
```

## Contributing

If you found a **bug**, open an issue.

If you have a **feature** request, open an issue.

If you want to **contribute**, submit a pull request.

## License

The source code is dedicated to the public domain. See the `LICENCE.md` file for
more information.

The Spotify artwork and icon is owned by Spotify AB Inc.


text:
//
//  SpotifyNotifyApp.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import SwiftUI
import UserNotifications
import AppKit

@main
struct SpotifyNotifyApp: App {
    @StateObject private var spotifyInteractor = SpotifyInteractor()
    @StateObject private var defaultsInteractor = DefaultsInteractor()
    @StateObject private var notificationsInteractor = NotificationsInteractor()
    @StateObject private var permissionsInteractor = PermissionsInteractor.shared

    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        MenuBarExtra("SpotifyNotify", image: defaultsInteractor.isMenuIconColored ? "IconStatusBarColor" : "IconStatusBarMonochrome") {
            MenuView()
                .environmentObject(spotifyInteractor)
                .environmentObject(notificationsInteractor)
        }
        Settings {
            SettingsView()
                .environmentObject(defaultsInteractor)
                .environmentObject(permissionsInteractor)
        }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        PermissionsInteractor.shared.registerForNotifications()
        PermissionsInteractor.shared.registerForControl()
    }

    func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
        guard !flag else { return true }

        return true
    }
}


text:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.automation.apple-events</key>
	<true/>
</dict>
</plist>


text:
//
//  String+Ext.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation

extension String {
    var asURL: URL? {
        .init(string: self)
    }

    var withLeadingZeroes: String {
        guard let int = Int(self) else { return self }
        return String(format: "%02d", int)
    }
}


text:
//
//  MenuView.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import SwiftUI
import AppKit

struct MenuView: View {

    @EnvironmentObject var spotifyInteractor: SpotifyInteractor
    @EnvironmentObject var notificationsInteractor: NotificationsInteractor

    var body: some View {
        VStack {
            Button("Status: \(spotifyInteractor.currentState.localized)") {}
                .disabled(true)
            Divider()
            Group {
                Button("Show notification") {
                    notificationsInteractor.showNotification()
                }
                Button("Previous Song") {
                    spotifyInteractor.previousTrack()
                }
                Button("Play/Pause") {
                    spotifyInteractor.playPause()
                }
                Button("Next Song") {
                    spotifyInteractor.nextTrack()
                }
            }
            Divider()
            Button("Open Spotify") {
                AppOpener.openSpotify()
            }
            Divider()
            if #available(macOS 14.0, *) {
                SettingsLink()
            } else {
                Button("Settings...") {
                    NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
                    NSApp.activate(ignoringOtherApps: true)
                }
            }
            Divider()
            Button("Quit") {
                NSApp.terminate(nil)
            }
        }
    }
}

private extension SpotifyPlayerState {
    var localized: String {
        switch self {
        case .paused:
            "Paused"
        case .stopped:
            "Stopped"
        case .playing:
            "Playing"
        case .unknown:
            "Unknown"
        }
    }
}


text:
//
//  SettingsView.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import SwiftUI
import KeyHolder
import Magnet
import LaunchAtLogin

struct SettingsView: View {

    private enum Const {
        static let homepage = "https://nahive.github.io".asURL!
        static let repo = "https://github.com/nahive/spotify-notify".asURL!
    }

    @EnvironmentObject var defaultsInteractor: DefaultsInteractor
    @EnvironmentObject var permissionsInteractor: PermissionsInteractor

    var body: some View {
        VStack {
            Image(.iconSettings)
                .resizable()
                .frame(width: 150.0, height: 150.0)
                .fixedSize()
                .padding()
            VStack(alignment: .leading) {
                LaunchAtLogin.Toggle("Launch on startup")
                Toggle(isOn: $defaultsInteractor.isMenuIconColored) {
                    Text("Show colored menu bar icon")
                }
                Divider()
                    .padding()
                Toggle(isOn: $defaultsInteractor.areNotificationsEnabled) {
                    Text("Enable notifications")
                }
                VStack(alignment: .leading) {
                    Toggle(isOn: $defaultsInteractor.shouldKeepNotificationsOnScreen) {
                        Text("Keep notifications on screen (swipe right to hide)")
                    }
                    Toggle(isOn: $defaultsInteractor.shouldShowNotificationOnPlayPause) {
                        Text("Notify on play/pause")
                    }
                    Toggle(isOn: $defaultsInteractor.shouldPlayNotificationsSound) {
                        Text("Play notification sound")
                    }
                    Toggle(isOn: $defaultsInteractor.shouldDisableNotificationsOnFocus) {
                        Text("Disable notifications when Spotify in focused")
                    }
                    Toggle(isOn: $defaultsInteractor.shouldShowAlbumArt) {
                        Text("Include album art")
                    }
                    Toggle(isOn: $defaultsInteractor.shouldRoundAlbumArt) {
                        Text("Round album art")
                    }
                    .padding(.leading)
                    .disabled(!defaultsInteractor.shouldShowAlbumArt)
                    Toggle(isOn: $defaultsInteractor.shouldShowSongProgress) {
                        Text("Show song progress")
                    }
                }
                .padding(.leading)
                .disabled(!defaultsInteractor.areNotificationsEnabled)
                Divider()
                    .padding()
                VStack(alignment: .leading){
                    Text("Show notification on shortcut")
                    ShortcutView(keyCombo: $defaultsInteractor.shortcut)
                        .frame(minHeight: 30, maxHeight: 30)
                }.padding(.horizontal)
            }
            .padding()
            HStack {
                VStack {
                    HStack(alignment: .center) {
                        Circle()
                            .foregroundStyle(permissionsInteractor.notificationPermissionEnabled ? Color.green : Color.red)
                            .frame(width: 8)
                        Text("Notification permisions")
                    }
                    if !permissionsInteractor.notificationPermissionEnabled {
                        Button("Notification settings") {
                            permissionsInteractor.openNotificationsSettings()
                        }
                    }
                }

                VStack {
                    HStack(alignment: .center) {
                        Circle()
                            .foregroundStyle(permissionsInteractor.automationPermissionEnabled ? Color.green : Color.red)
                            .frame(width: 8)
                        Text("Automation permisions")
                    }
                    if !permissionsInteractor.automationPermissionEnabled {
                        Button("Automation settings") {
                            permissionsInteractor.openAutomationSettings()
                        }
                    }
                }
            }

            HStack {
                Button("Source") {
                    NSWorkspace.shared.open(Const.repo)
                }
                Button("Home") {
                    NSWorkspace.shared.open(Const.homepage)
                }
                Button("Quit") {
                    NSApp.terminate(nil)
                }
            }
            .padding()
            Text("Created with ❤ by nahive")
                .padding(.bottom)
                .foregroundStyle(Color.gray)
                .font(.system(size: 10))
        }
        .frame(minWidth: 400, maxWidth: 400)
    }
}

struct ShortcutView: NSViewRepresentable {
    typealias NSViewType = RecordView

    @Binding private var keyCombo: KeyCombo?

    init(keyCombo: Binding<KeyCombo?>) {
        self._keyCombo = keyCombo
    }

    func makeNSView(context: Context) -> RecordView {
        let view = RecordView(frame: .zero)
        view.cornerRadius = 5
        view.delegate = context.coordinator
        return view
    }

    func updateNSView(_ recordView: RecordView, context: Context) {
        recordView.keyCombo = keyCombo
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: NSObject, RecordViewDelegate {
        var parent: ShortcutView

        init(parent: ShortcutView) {
            self.parent = parent
        }

        func recordView(_ recordView: RecordView, didChangeKeyCombo keyCombo: KeyCombo?) {
            self.parent.keyCombo = keyCombo
        }

        func recordViewShouldBeginRecording(_ recordView: RecordView) -> Bool {
            true
        }

        func recordView(_ recordView: RecordView, canRecordKeyCombo keyCombo: KeyCombo) -> Bool {
            true
        }

        func recordViewDidEndRecording(_ recordView: RecordView) {
            // nothing
        }
    }
}

struct Settings_Preview: PreviewProvider {
    @StateObject private static var defaultsInteractor = DefaultsInteractor()
    @StateObject private static var permissionsIteractor = PermissionsInteractor.shared

    static var previews: some View {
        SettingsView()
            .environmentObject(defaultsInteractor)
            .environmentObject(permissionsIteractor)
    }
}


text:
//
//  NotificationsInteractor.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation
import Combine
import UserNotifications
import AppKit

final class NotificationsInteractor: ObservableObject {
    let defaultsInteractor: DefaultsInteractor = .init()
    let spotifyInteractor: SpotifyInteractor = .init()

    private var previousTrack: Track?
    private var currentTrack: Track?

    private var cancellables = Set<AnyCancellable>()

    init() {
        spotifyInteractor.$currentState
            .dropFirst()
            .sink(receiveValue: { [weak self] _ in
                guard let self else { return }
                self.showNotification()
            })
            .store(in: &cancellables)
    }

    func showNotification(force: Bool = false) {
        System.logger.info("Starting notification flow")

        if force, let currentTrack = spotifyInteractor.currentTrack {
            createNotification(model: .init(track: currentTrack,
                                            showSongProgress: defaultsInteractor.shouldShowSongProgress,
                                            songProgress: spotifyInteractor.currentProgress))
        }

        // return if notifications are disabled
        guard defaultsInteractor.areNotificationsEnabled else {
            System.logger.warning("⚠ notification disabled")
            return
        }

        // return if notifications are disabled when in focus
        if spotifyInteractor.isFrontmost, defaultsInteractor.shouldDisableNotificationsOnFocus {
            System.logger.info("⚠ spotify is frontmost")
            return
        }

        previousTrack = currentTrack
        currentTrack  = spotifyInteractor.currentTrack

        // return if previous track is same as previous => play/pause and if it's disabled
        guard currentTrack != previousTrack || defaultsInteractor.shouldShowNotificationOnPlayPause else {
            System.logger.info("⚠ spotify is changing from play/pause")
            return
        }

        guard spotifyInteractor.currentState == .playing else {
            System.logger.info("⚠ spotify is not playing")
            return
        }

        // return if current track is nil
        guard let currentTrack = currentTrack else {
            System.logger.info("⚠ spotify has no track available")
            return
        }

        // Create and deliver notifications
        createNotification(model: .init(track: currentTrack,
                                        showSongProgress: defaultsInteractor.shouldShowSongProgress,
                                        songProgress: spotifyInteractor.currentProgress))
    }

    private func createNotification(model: SpotifyNotification) {
        System.logger.info("Creating notification")

        let notification = UNMutableNotificationContent()

        notification.title = model.title
        notification.subtitle = model.subtitle
        notification.body = model.body
        notification.categoryIdentifier = NotificationIdentifier.category

        // decide whether to add sound
        if defaultsInteractor.shouldPlayNotificationsSound {
            notification.sound = .default
        }

        if defaultsInteractor.shouldShowAlbumArt {
            System.logger.info("Creating notification with artwork")
            deliverNotificationWithArtwork(notification: notification, model: model)
        } else {
            System.logger.info("Creating notification without artwork")
            DispatchQueue.main.async { [weak self] in
                self?.deliverNotification(identifier: model.identifier, notification: notification)
            }
        }
    }

    private func deliverNotificationWithArtwork(notification: UNMutableNotificationContent, model: SpotifyNotification) {
        guard let url = model.artworkURL else {
            System.logger.info("Creating notification with artwork failed, resolving to normal")
            DispatchQueue.main.async { [weak self] in
                self?.deliverNotification(identifier: model.identifier, notification: notification)
            }
            return
        }

        Task {
            var artwork = try await url.asyncImage

            if defaultsInteractor.shouldRoundAlbumArt {
                artwork = artwork.withCircularMask
            }

            guard let url = artwork.saveToTemporaryDirectory(withName: "artwork") else { return }

            do {
                let attachment = try UNNotificationAttachment(identifier: "artwork", url: url)
                notification.attachments = [attachment]
            } catch {
                print("Error creating attachment: " + error.localizedDescription)
            }

            DispatchQueue.main.async { [weak self] in
                self?.deliverNotification(identifier: model.identifier, notification: notification)
            }
        }

    }

    private func deliverNotification(identifier: String, notification: UNMutableNotificationContent) {
        // Create a request
        let request = UNNotificationRequest(identifier: identifier, content: notification, trigger: nil)

        let notificationCenter = UNUserNotificationCenter.current()

        // Deliver current notification
        notificationCenter.add(request)

        // remove after userset number of seconds if not taken action
        if !defaultsInteractor.shouldKeepNotificationsOnScreen {
            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(defaultsInteractor.notificationLength)) {
                notificationCenter.removeAllDeliveredNotifications()
            }
        }
    }
}

enum ImageError: Error {
    case notAnImage
}

private extension URL {
    var image: NSImage? {
        guard let data = try? Data(contentsOf: self) else { return nil }
        return NSImage(data: data)
    }

    var asyncImage: NSImage {
        get async throws {
            let (data, _) = try await URLSession.shared.data(from: self)
            guard let image = NSImage(data: data) else {
                throw ImageError.notAnImage
            }
            return image
        }
    }
}

private extension NSImage {
    enum Const {
        static let bundleId = "io.nahive.SpotifyNotify"
    }

    /// Save an NSImage to a temporary directory
    ///
    /// - Parameter name: The file name, to use
    /// - Returns: A URL if saving is successful, or nil if there was an error
    func saveToTemporaryDirectory(withName name: String) -> URL? {
        guard let data = tiffRepresentation else { return nil }

        let fileManager = FileManager.default
        let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
        let bundleURL = tempURL.appendingPathComponent(Const.bundleId, isDirectory: true)

        do {
            try fileManager.createDirectory(at: bundleURL, withIntermediateDirectories: true)
            let fileURL = bundleURL.appendingPathComponent(name + ".png")

            try NSBitmapImageRep(data: data)?
                .representation(using: .png, properties: [:])?
                .write(to: fileURL)

            return fileURL
        } catch {
            print("Error: " + error.localizedDescription)
        }

        return nil
    }

    // Apply circular mask
    var withCircularMask: NSImage {
        let image = NSImage(size: size)
        image.lockFocus()

        NSGraphicsContext.current?.imageInterpolation = .high
        let frame = NSRect(origin: .zero, size: size)
        NSBezierPath(ovalIn: frame).addClip()
        draw(at: .zero, from: frame, operation: .sourceOver, fraction: 1)

        image.unlockFocus()
        return image
    }
}


text:
//
//  PermissionsInteractor.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/12.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation
import UserNotifications
import AppKit

final class PermissionsInteractor: NSObject, ObservableObject {

    static let shared = PermissionsInteractor()

    @Published var notificationPermissionEnabled = false
    @Published var automationPermissionEnabled = false

    private override init() {}

    func registerForNotifications() {
        let notificationCenter = UNUserNotificationCenter.current()
        notificationCenter.delegate = self

        // Check notification authorisation first
        notificationCenter.requestAuthorization(options: [.alert, .sound]) { [weak self] (granted, error) in
            guard let self else { return }

            DispatchQueue.main.async {
                self.notificationPermissionEnabled = granted
            }

            if let error = error {
                System.logger.warning("Notification authorisation was denied: \(error)")
                DispatchQueue.main.async {
                    self.showAlert(message: "Missing notification permissions", onSettingsTap: self.openNotificationsSettings)
                }
            } else {
                System.logger.info("Notification authorisation was granted: \(granted)")
            }
        }

        let skip = UNNotificationAction(identifier: NotificationIdentifier.skip, title: "Skip")

        let category = UNNotificationCategory(identifier: NotificationIdentifier.category,
                                              actions: [skip],
                                              intentIdentifiers: [],
                                              options: [])

        UNUserNotificationCenter.current().setNotificationCategories([category])
    }

    func registerForControl() {
        let targetAEDescriptor = NSAppleEventDescriptor(bundleIdentifier: SpotifyInteractor.Const.spotifyBundleId)
        let status = AEDeterminePermissionToAutomateTarget(targetAEDescriptor.aeDesc, typeWildCard, typeWildCard, true)

        switch status {
        case noErr:
            automationPermissionEnabled = true
            System.logger.info("Automation authorisation was granted")
        case OSStatus(errAEEventNotPermitted):
            automationPermissionEnabled = false
            showAlert(message: "Missing required automation permissions", onSettingsTap: openAutomationSettings)
            System.logger.warning("Automation authorisation was denied")
        case OSStatus(procNotFound), _:
            System.logger.info("Spotify is not running")
        }
    }

    private func showAlert(message: String, onSettingsTap: () -> Void) {
        let alert = NSAlert()
        alert.alertStyle = .warning
        alert.messageText = message
        alert.addButton(withTitle: "Go to Settings")
        alert.addButton(withTitle: "Cancel")

        let response = alert.runModal()
        switch response {
        case .alertFirstButtonReturn:
            onSettingsTap()
        default:
            break
        }
    }

    func openNotificationsSettings() {
        NSWorkspace.shared.open("x-apple.systempreferences:com.apple.preference.notifications".asURL!)
    }

    func openAutomationSettings() {
        NSWorkspace.shared.open("x-apple.systempreferences:com.apple.preference.security?Privacy_Automation".asURL!)
    }
}

extension PermissionsInteractor: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        // Force notifications to be shown, even if the SpotifyNotify is in the foreground
        completionHandler([.banner, .sound])
    }

    /// Handle the action buttons
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        switch response.actionIdentifier {
        case NotificationIdentifier.skip:
            SpotifyInteractor().nextTrack()
        default:
            AppOpener.openSpotify()
        }
    }
}


text:
//
//  SpotifyInteractor.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation
import ScriptingBridge
import AppKit
import Combine

enum SpotifyPlayerState {
    case stopped, paused, playing, unknown
}

extension SpotifyEPlS {
    var asPlayerState: SpotifyPlayerState {
        switch self {
        case .paused:
            .paused
        case .playing:
            .playing
        case .stopped:
            .stopped
        default:
            .unknown
        }
    }
}

final class SpotifyInteractor: ObservableObject {
    enum Const {
        static let spotifyAppName = "Spotify"
        static let spotifyBundleId = "com.spotify.client"
        static let notificationPlaybackChange = spotifyBundleId + ".PlaybackStateChanged"
    }

    private let spotifyBridge: SpotifyApplication? = SBApplication(bundleIdentifier: Const.spotifyBundleId)

    var currentTrack: Track? {
        spotifyBridge?.currentTrack?.asTrack
    }

    @Published var currentState: SpotifyPlayerState

    var currentProgress: Double {
        spotifyBridge?.playerPosition ?? 0
    }

    var isFrontmost: Bool {
        NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Const.spotifyBundleId
    }

    private var cancellables = Set<AnyCancellable>()

    init() {
        currentState = spotifyBridge?.playerState?.asPlayerState ?? .unknown

        DistributedNotificationCenter.default().publisher(for: .init(Const.notificationPlaybackChange))
            .sink(receiveValue: { [weak self] _ in
                guard let self else { return }
                System.logger.info("Received Spotify state change")
                self.currentState = self.spotifyBridge?.playerState?.asPlayerState ?? .unknown
            })
            .store(in: &cancellables)
    }

    func nextTrack() {
        System.logger.info("Next track")
        spotifyBridge?.nextTrack?()
    }

    func previousTrack() {
        System.logger.info("Prev track")
        spotifyBridge?.previousTrack?()
    }

    func playPause() {
        System.logger.info("Play/pause")
        spotifyBridge?.playpause?()
    }

    func play() {
        System.logger.info("Play")
        spotifyBridge?.play?()
    }

    func pause() {
        System.logger.info("Pause")
        spotifyBridge?.pause?()
    }
}


text:
//
//  DefaultsInteractor.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation
import SwiftUI
import Magnet
import LaunchAtLogin

final class DefaultsInteractor: ObservableObject {
    enum Key {
        static let notificationsEnabled = "notifications.enabled.key"
        static let notificationsKeep = "notifications.keep.key"
        static let notificationsPlayPause = "notifications.playpause.key"
        static let notificationsSound = "notifications.sound.key"
        static let notificationsDisableOnFocus = "notifications.focus.key"
        static let notificationsLength = "notifications.length.key"

        static let startOnLogin = "startonlogin.key"
        static let showAlbumArt = "showalbumart.key"
        static let roundAlbumArt = "roundalbumart.key"
        static let showSongProgress = "songprogress.key"

        static let showMenuIcon = "menuiconshow.key"
        static let menuIconColored = "menuiconcolored.key"
        static let shortcutKeyCode = "shortcut.keycode.key"
        static let shortcutModifiers = "shortcut.modifiers.key"
    }

    @AppStorage(Key.notificationsEnabled) var areNotificationsEnabled = true
    @AppStorage(Key.notificationsKeep) var shouldKeepNotificationsOnScreen = false
    @AppStorage(Key.notificationsPlayPause) var shouldShowNotificationOnPlayPause = true
    @AppStorage(Key.notificationsSound) var shouldPlayNotificationsSound = false
    @AppStorage(Key.notificationsDisableOnFocus) var shouldDisableNotificationsOnFocus = true
    @AppStorage(Key.notificationsLength) var notificationLength = 5

    @AppStorage(Key.showAlbumArt) var shouldShowAlbumArt = true
    @AppStorage(Key.roundAlbumArt) var shouldRoundAlbumArt = false
    @AppStorage(Key.showSongProgress) var shouldShowSongProgress = false

    @AppStorage(Key.showMenuIcon) var shouldShowMenuIcon = true
    @AppStorage(Key.menuIconColored) var isMenuIconColored = false
    @AppStorage(Key.shortcutKeyCode) private var shortcutKeyCode = 0
    @AppStorage(Key.shortcutModifiers) private var shortcutModifier = 0

    var shortcut: KeyCombo? {
        get {
            guard shortcutKeyCode != 0, shortcutModifier != 0 else { return nil }
            return KeyCombo(QWERTYKeyCode: shortcutKeyCode, carbonModifiers: shortcutModifier)
        }
        set {
            shortcutKeyCode = newValue?.QWERTYKeyCode ?? 0
            shortcutModifier = newValue?.modifiers ?? 0

            if let keyCombo = newValue {
                let hotKey = HotKey(identifier: "showKey", keyCombo: keyCombo) { key in
                    NotificationsInteractor().showNotification(force: true)
                }
                HotKeyCenter.shared.register(with: hotKey)
            } else {
                HotKeyCenter.shared.unregisterAll()
            }
        }
    }
}


text:
//
//  Logger.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/12.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation
import os

struct System {
    static let logger = Logger()
}


text:
//
//  AppOpener.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation
import AppKit

struct AppOpener {
    static func openSpotify() {
        guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.spotify.client") else { return }

        let path = "/bin"
        let configuration = NSWorkspace.OpenConfiguration()
        configuration.arguments = [path]
        NSWorkspace.shared.openApplication(at: url,
                                           configuration: configuration,
                                           completionHandler: nil)
    }
}


text:
// from https://gist.github.com/gf3/d622d927496d50c6108fd6ea36619bdf

import AppKit
import ScriptingBridge

@objc public protocol SBObjectProtocol: NSObjectProtocol {
    func get() -> Any!
}

@objc public protocol SBApplicationProtocol: SBObjectProtocol {
    func activate()
    var delegate: SBApplicationDelegate? { get set }
    var isRunning: Bool { @objc(isRunning) get }
}

// MARK: SpotifyEPlS
@objc public enum SpotifyEPlS : AEKeyword {
    case unknown = 0 /* this happens when permissions for reading macos interactions are disabled */
    case stopped = 0x6b505353 /* 'kPSS' */
    case playing = 0x6b505350 /* 'kPSP' */
    case paused = 0x6b505370 /* 'kPSp' */
}

// MARK: SpotifyApplication
@objc public protocol SpotifyApplication: SBApplicationProtocol {
    @objc optional var currentTrack: SpotifyTrack { get } // The current playing track.
    @objc optional var soundVolume: Int { get } // The sound output volume (0 = minimum, 100 = maximum)
    @objc optional var playerState: SpotifyEPlS { get } // Is Spotify stopped, paused, or playing?
    @objc optional var playerPosition: Double { get } // The player’s position within the currently playing track in seconds.
    @objc optional var repeatingEnabled: Bool { get } // Is repeating enabled in the current playback context?
    @objc optional var repeating: Bool { get } // Is repeating on or off?
    @objc optional var shufflingEnabled: Bool { get } // Is shuffling enabled in the current playback context?
    @objc optional var shuffling: Bool { get } // Is shuffling on or off?
    @objc optional func nextTrack() // Skip to the next track.
    @objc optional func previousTrack() // Skip to the previous track.
    @objc optional func playpause() // Toggle play/pause.
    @objc optional func pause() // Pause playback.
    @objc optional func play() // Resume playback.
    @objc optional func playTrack(_ x: String!, inContext: String!) // Start playback of a track in the given context.
    @objc optional func setSoundVolume(_ soundVolume: Int) // The sound output volume (0 = minimum, 100 = maximum)
    @objc optional func setPlayerPosition(_ playerPosition: Double) // The player’s position within the currently playing track in seconds.
    @objc optional func setRepeating(_ repeating: Bool) // Is repeating on or off?
    @objc optional func setShuffling(_ shuffling: Bool) // Is shuffling on or off?
    @objc optional var name: String { get } // The name of the application.
    @objc optional var frontmost: Bool { get } // Is this the frontmost (active) application?
    @objc optional var version: String { get } // The version of the application.
}
extension SBApplication: SpotifyApplication {}

// MARK: SpotifyTrack
@objc public protocol SpotifyTrack: SBObjectProtocol {
    @objc optional var artist: String { get } // The artist of the track.
    @objc optional var album: String { get } // The album of the track.
    @objc optional var discNumber: Int { get } // The disc number of the track.
    @objc optional var duration: Int { get } // The length of the track in seconds.
    @objc optional var playedCount: Int { get } // The number of times this track has been played.
    @objc optional var trackNumber: Int { get } // The index of the track in its album.
    @objc optional var starred: Bool { get } // Is the track starred?
    @objc optional var popularity: Int { get } // How popular is this track? 0-100
    @objc optional func id() -> String // The ID of the item.
    @objc optional var name: String { get } // The name of the track.
    @objc optional var artworkUrl: String { get } // The URL of the track%apos;s album cover.
    @objc optional var artwork: NSImage { get } // The property is deprecated and will never be set. Use the 'artwork url' instead.
    @objc optional var albumArtist: String { get } // That album artist of the track.
    @objc optional var spotifyUrl: String { get } // The URL of the track.
    @objc optional func setSpotifyUrl(_ spotifyUrl: String!) // The URL of the track.
}
extension SBObject: SpotifyTrack {}


text:
//
//  Track.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation

struct Track {
    let id: String
    let name: String
    let album: String?
    let artist: String
    let artworkURL: URL?
    let duration: Int?

    init?(id: String?, name: String?, album: String?, artist: String?, artworkURL: URL?, duration: Int?) {
        guard let id, let name, let artist else {
            System.logger.error("not a valid track")
            return nil
        }
        self.id = id
        self.name = name
        self.album = album
        self.artist = artist
        self.artworkURL = artworkURL
        self.duration = duration
    }
}

extension Track: Equatable {
    static func ==(lhs: Track, rhs: Track) -> Bool {
        return lhs.id == rhs.id
    }
}

extension SpotifyTrack {
    var asTrack: Track? {
        .init(id: id?(), name: name, album: album, artist: artist, artworkURL: artworkUrl?.asURL, duration: duration)
    }
}


text:
//
//  Notification.swift
//  SpotifyNotify
//
//  Created by Szymon Maślanka on 2023/06/11.
//  Copyright © 2023 Szymon Maślanka. All rights reserved.
//

import Foundation

enum NotificationIdentifier {
    static let skip = "notification.skip"
    static let category = "notification.category"
}

/// A notification view model for setting up a notification
struct SpotifyNotification {
    let identifier = NSUUID().uuidString
    let title: String
    let subtitle: String
    let body: String
    let artworkURL: URL?

    /// Defaults to show if, for any reason, Spotify returns nil
    private let unknownAlbum = "Unknown Album"

    init(track: Track, showSongProgress: Bool, songProgress: Double?) {

        func progress(for track: Track) -> String {
            guard let songProgress = songProgress, let duration = track.duration else {
                    return "00:00/00:00"
            }

            let percentage = songProgress / (Double(duration) / 1000.0)

            let progressMax = 14
            let currentProgress = Int(Double(progressMax) * percentage)

            let progressString = "⁃".repeated(currentProgress) +  "-".repeated(progressMax - currentProgress)

            let now = Int(songProgress).minutesSeconds
            let length = (duration / 1000).minutesSeconds

            let nowS = "\(now.minutes)".withLeadingZeroes + ":" + "\(now.seconds)".withLeadingZeroes
            let lengthS = "\(length.minutes)".withLeadingZeroes + ":" + "\(length.seconds)".withLeadingZeroes

            return "\(nowS)  \(progressString)  \(lengthS)"
        }

        let name = track.name
        let artist = track.artist
        let album = track.album ?? unknownAlbum

        title = name
        subtitle = showSongProgress ? "\(artist) - \(album)" : artist
        body = showSongProgress ? progress(for: track) : album
        artworkURL = track.artworkURL
    }
}

private extension Int {
    var minutesSeconds: (minutes: Int, seconds: Int) {
        ((self % 3600) / 60, (self % 3600) % 60)
    }
}

private extension String {
    func repeated(_ count: Int) -> String {
        String(repeating: self, count: count)
    }
}


text:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleDisplayName</key>
	<string>Notify</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIconFile</key>
	<string></string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>0.4.4</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>510</string>
	<key>LSApplicationCategoryType</key>
	<string>public.app-category.utilities</string>
	<key>LSMinimumSystemVersion</key>
	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
	<key>LSUIElement</key>
	<true/>
	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
	</dict>
	<key>NSAppleEventsUsageDescription</key>
	<string>Access is required in order to retrieve current track information, and to allow control of Spotify from the menu bar.</string>
	<key>NSHumanReadableCopyright</key>
	<string>Copyright © 2022 Szymon Maślanka. All rights reserved.</string>
	<key>NSMainNibFile</key>
	<string>MainMenu</string>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
	<key>NSUserNotificationAlertStyle</key>
	<string>alert</string>
</dict>
</plist>


text:
// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 54;
	objects = {

/* Begin PBXBuildFile section */
		4C3340EE2A3712AF0045B06B /* PermissionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3340ED2A3712AF0045B06B /* PermissionsInteractor.swift */; };
		4C5764C91C982DC900364AFE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C5764C81C982DC900364AFE /* Assets.xcassets */; };
		4C858885203F218800ED1692 /* SpotifyBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C858884203F218800ED1692 /* SpotifyBridge.swift */; };
		4C926D4C288043F40008809E /* KeyHolder in Frameworks */ = {isa = PBXBuildFile; productRef = 4C926D4B288043F40008809E /* KeyHolder */; };
		4C926D4F288044010008809E /* Magnet in Frameworks */ = {isa = PBXBuildFile; productRef = 4C926D4E288044010008809E /* Magnet */; };
		4CAF91122A3620B700C6A19B /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAF91112A3620B700C6A19B /* Logger.swift */; };
		4CDE08A62A35AB9B009609E7 /* SpotifyNotifyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08A52A35AB9B009609E7 /* SpotifyNotifyApp.swift */; };
		4CDE08AA2A35AEF6009609E7 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08A92A35AEF6009609E7 /* MenuView.swift */; };
		4CDE08AD2A35B34D009609E7 /* SpotifyInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08AC2A35B34D009609E7 /* SpotifyInteractor.swift */; };
		4CDE08B02A35B39D009609E7 /* Track.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08AF2A35B39D009609E7 /* Track.swift */; };
		4CDE08B32A35B3CD009609E7 /* String+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08B22A35B3CD009609E7 /* String+Ext.swift */; };
		4CDE08B52A35BC9B009609E7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08B42A35BC9B009609E7 /* SettingsView.swift */; };
		4CDE08B92A35C238009609E7 /* DefaultsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08B82A35C238009609E7 /* DefaultsInteractor.swift */; };
		4CDE08BD2A35CAAE009609E7 /* NotificationsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08BC2A35CAAE009609E7 /* NotificationsInteractor.swift */; };
		4CDE08BF2A35CC68009609E7 /* SpotifyNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08BE2A35CC68009609E7 /* SpotifyNotification.swift */; };
		4CDE08C42A35F53B009609E7 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 4CDE08C32A35F53B009609E7 /* LaunchAtLogin */; };
		4CDE08C62A35F664009609E7 /* AppOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE08C52A35F664009609E7 /* AppOpener.swift */; };
		FB400A90203EFA3800F2D63C /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB400A8F203EFA3800F2D63C /* ScriptingBridge.framework */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
		4C3340EC2A370EE90045B06B /* SpotifyNotify.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SpotifyNotify.entitlements; sourceTree = "<group>"; };
		4C3340ED2A3712AF0045B06B /* PermissionsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsInteractor.swift; sourceTree = "<group>"; };
		4C5764C31C982DC800364AFE /* Notify.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Notify.app; sourceTree = BUILT_PRODUCTS_DIR; };
		4C5764C81C982DC900364AFE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
		4C5764CD1C982DC900364AFE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
		4C858884203F218800ED1692 /* SpotifyBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyBridge.swift; sourceTree = "<group>"; };
		4CAF91112A3620B700C6A19B /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
		4CDE08A52A35AB9B009609E7 /* SpotifyNotifyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyNotifyApp.swift; sourceTree = "<group>"; };
		4CDE08A92A35AEF6009609E7 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = "<group>"; };
		4CDE08AC2A35B34D009609E7 /* SpotifyInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyInteractor.swift; sourceTree = "<group>"; };
		4CDE08AF2A35B39D009609E7 /* Track.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Track.swift; sourceTree = "<group>"; };
		4CDE08B22A35B3CD009609E7 /* String+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Ext.swift"; sourceTree = "<group>"; };
		4CDE08B42A35BC9B009609E7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
		4CDE08B82A35C238009609E7 /* DefaultsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsInteractor.swift; sourceTree = "<group>"; };
		4CDE08BC2A35CAAE009609E7 /* NotificationsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsInteractor.swift; sourceTree = "<group>"; };
		4CDE08BE2A35CC68009609E7 /* SpotifyNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyNotification.swift; sourceTree = "<group>"; };
		4CDE08C52A35F664009609E7 /* AppOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOpener.swift; sourceTree = "<group>"; };
		FB400A8F203EFA3800F2D63C /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
		4C5764C01C982DC800364AFE /* Frameworks */ = {
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
				4C926D4F288044010008809E /* Magnet in Frameworks */,
				4C926D4C288043F40008809E /* KeyHolder in Frameworks */,
				4CDE08C42A35F53B009609E7 /* LaunchAtLogin in Frameworks */,
				FB400A90203EFA3800F2D63C /* ScriptingBridge.framework in Frameworks */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
		4C5764BA1C982DC800364AFE = {
			isa = PBXGroup;
			children = (
				4C5764C51C982DC800364AFE /* SpotifyNotify */,
				FB400A8E203EFA3800F2D63C /* Frameworks */,
				4C5764C41C982DC800364AFE /* Products */,
			);
			sourceTree = "<group>";
		};
		4C5764C41C982DC800364AFE /* Products */ = {
			isa = PBXGroup;
			children = (
				4C5764C31C982DC800364AFE /* Notify.app */,
			);
			name = Products;
			sourceTree = "<group>";
		};
		4C5764C51C982DC800364AFE /* SpotifyNotify */ = {
			isa = PBXGroup;
			children = (
				4C3340EC2A370EE90045B06B /* SpotifyNotify.entitlements */,
				4CDE08A52A35AB9B009609E7 /* SpotifyNotifyApp.swift */,
				4CDE08B12A35B3C3009609E7 /* Extensions */,
				4CDE08AE2A35B393009609E7 /* Models */,
				4CDE08AB2A35B344009609E7 /* Interactors */,
				4CDE08A82A35AEEE009609E7 /* Views */,
				4C5764E41C982E8900364AFE /* Helpers */,
				FB6B9D0F203EC29D0062702B /* Resources */,
				FB6B9D0E203EC28E0062702B /* Other */,
			);
			path = SpotifyNotify;
			sourceTree = "<group>";
		};
		4C5764E41C982E8900364AFE /* Helpers */ = {
			isa = PBXGroup;
			children = (
				4C858884203F218800ED1692 /* SpotifyBridge.swift */,
				4CDE08C52A35F664009609E7 /* AppOpener.swift */,
				4CAF91112A3620B700C6A19B /* Logger.swift */,
			);
			path = Helpers;
			sourceTree = "<group>";
		};
		4CDE08A82A35AEEE009609E7 /* Views */ = {
			isa = PBXGroup;
			children = (
				4CDE08A92A35AEF6009609E7 /* MenuView.swift */,
				4CDE08B42A35BC9B009609E7 /* SettingsView.swift */,
			);
			path = Views;
			sourceTree = "<group>";
		};
		4CDE08AB2A35B344009609E7 /* Interactors */ = {
			isa = PBXGroup;
			children = (
				4CDE08AC2A35B34D009609E7 /* SpotifyInteractor.swift */,
				4CDE08B82A35C238009609E7 /* DefaultsInteractor.swift */,
				4CDE08BC2A35CAAE009609E7 /* NotificationsInteractor.swift */,
				4C3340ED2A3712AF0045B06B /* PermissionsInteractor.swift */,
			);
			path = Interactors;
			sourceTree = "<group>";
		};
		4CDE08AE2A35B393009609E7 /* Models */ = {
			isa = PBXGroup;
			children = (
				4CDE08AF2A35B39D009609E7 /* Track.swift */,
				4CDE08BE2A35CC68009609E7 /* SpotifyNotification.swift */,
			);
			path = Models;
			sourceTree = "<group>";
		};
		4CDE08B12A35B3C3009609E7 /* Extensions */ = {
			isa = PBXGroup;
			children = (
				4CDE08B22A35B3CD009609E7 /* String+Ext.swift */,
			);
			path = Extensions;
			sourceTree = "<group>";
		};
		FB400A8E203EFA3800F2D63C /* Frameworks */ = {
			isa = PBXGroup;
			children = (
				FB400A8F203EFA3800F2D63C /* ScriptingBridge.framework */,
			);
			name = Frameworks;
			sourceTree = "<group>";
		};
		FB6B9D0E203EC28E0062702B /* Other */ = {
			isa = PBXGroup;
			children = (
				4C5764CD1C982DC900364AFE /* Info.plist */,
			);
			path = Other;
			sourceTree = "<group>";
		};
		FB6B9D0F203EC29D0062702B /* Resources */ = {
			isa = PBXGroup;
			children = (
				4C5764C81C982DC900364AFE /* Assets.xcassets */,
			);
			path = Resources;
			sourceTree = "<group>";
		};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
		4C5764C21C982DC800364AFE /* SpotifyNotify */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = 4C5764DB1C982DC900364AFE /* Build configuration list for PBXNativeTarget "SpotifyNotify" */;
			buildPhases = (
				4C5764BF1C982DC800364AFE /* Sources */,
				4C5764C01C982DC800364AFE /* Frameworks */,
				4C5764C11C982DC800364AFE /* Resources */,
			);
			buildRules = (
			);
			dependencies = (
			);
			name = SpotifyNotify;
			packageProductDependencies = (
				4C926D4B288043F40008809E /* KeyHolder */,
				4C926D4E288044010008809E /* Magnet */,
				4CDE08C32A35F53B009609E7 /* LaunchAtLogin */,
			);
			productName = NotifySpotify;
			productReference = 4C5764C31C982DC800364AFE /* Notify.app */;
			productType = "com.apple.product-type.application";
		};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
		4C5764BB1C982DC800364AFE /* Project object */ = {
			isa = PBXProject;
			attributes = {
				BuildIndependentTargetsInParallel = YES;
				LastSwiftUpdateCheck = 0930;
				LastUpgradeCheck = 1500;
				ORGANIZATIONNAME = "Szymon Maślanka";
				TargetAttributes = {
					4C5764C21C982DC800364AFE = {
						CreatedOnToolsVersion = 7.3;
						LastSwiftMigration = "";
						SystemCapabilities = {
							com.apple.Sandbox = {
								enabled = 0;
							};
						};
					};
				};
			};
			buildConfigurationList = 4C5764BE1C982DC800364AFE /* Build configuration list for PBXProject "SpotifyNotify" */;
			compatibilityVersion = "Xcode 3.2";
			developmentRegion = en;
			hasScannedForEncodings = 0;
			knownRegions = (
				en,
				Base,
			);
			mainGroup = 4C5764BA1C982DC800364AFE;
			packageReferences = (
				4C926D4A288043F40008809E /* XCRemoteSwiftPackageReference "KeyHolder" */,
				4C926D4D288044010008809E /* XCRemoteSwiftPackageReference "Magnet" */,
				4CDE08C22A35F53B009609E7 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */,
			);
			productRefGroup = 4C5764C41C982DC800364AFE /* Products */;
			projectDirPath = "";
			projectRoot = "";
			targets = (
				4C5764C21C982DC800364AFE /* SpotifyNotify */,
			);
		};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
		4C5764C11C982DC800364AFE /* Resources */ = {
			isa = PBXResourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				4C5764C91C982DC900364AFE /* Assets.xcassets in Resources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXResourcesBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
		4C5764BF1C982DC800364AFE /* Sources */ = {
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				4CDE08B32A35B3CD009609E7 /* String+Ext.swift in Sources */,
				4C858885203F218800ED1692 /* SpotifyBridge.swift in Sources */,
				4CDE08C62A35F664009609E7 /* AppOpener.swift in Sources */,
				4CDE08B02A35B39D009609E7 /* Track.swift in Sources */,
				4CDE08A62A35AB9B009609E7 /* SpotifyNotifyApp.swift in Sources */,
				4CDE08AA2A35AEF6009609E7 /* MenuView.swift in Sources */,
				4CDE08BD2A35CAAE009609E7 /* NotificationsInteractor.swift in Sources */,
				4CAF91122A3620B700C6A19B /* Logger.swift in Sources */,
				4CDE08BF2A35CC68009609E7 /* SpotifyNotification.swift in Sources */,
				4CDE08AD2A35B34D009609E7 /* SpotifyInteractor.swift in Sources */,
				4CDE08B92A35C238009609E7 /* DefaultsInteractor.swift in Sources */,
				4CDE08B52A35BC9B009609E7 /* SettingsView.swift in Sources */,
				4C3340EE2A3712AF0045B06B /* PermissionsInteractor.swift in Sources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXSourcesBuildPhase section */

/* Begin XCBuildConfiguration section */
		4C5764D91C982DC900364AFE /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
				CLANG_CXX_LIBRARY = "libc++";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				CODE_SIGN_IDENTITY = "-";
				COPY_PHASE_STRIP = NO;
				DEAD_CODE_STRIPPING = YES;
				DEBUG_INFORMATION_FORMAT = dwarf;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_TESTABILITY = YES;
				ENABLE_USER_SCRIPT_SANDBOXING = YES;
				GCC_C_LANGUAGE_STANDARD = gnu99;
				GCC_DYNAMIC_NO_PIC = NO;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_OPTIMIZATION_LEVEL = 0;
				GCC_PREPROCESSOR_DEFINITIONS = (
					"DEBUG=1",
					"$(inherited)",
				);
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 13.0;
				MTL_ENABLE_DEBUG_INFO = YES;
				ONLY_ACTIVE_ARCH = YES;
				SAMPLE_CODE_DISAMBIGUATOR = "${DEVELOPMENT_TEAM}";
				SDKROOT = macosx;
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
				SWIFT_VERSION = 4.0;
			};
			name = Debug;
		};
		4C5764DA1C982DC900364AFE /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
				CLANG_CXX_LIBRARY = "libc++";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				CODE_SIGN_IDENTITY = "-";
				COPY_PHASE_STRIP = NO;
				DEAD_CODE_STRIPPING = YES;
				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
				ENABLE_NS_ASSERTIONS = NO;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_USER_SCRIPT_SANDBOXING = YES;
				GCC_C_LANGUAGE_STANDARD = gnu99;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 13.0;
				MTL_ENABLE_DEBUG_INFO = NO;
				SAMPLE_CODE_DISAMBIGUATOR = "${DEVELOPMENT_TEAM}";
				SDKROOT = macosx;
				SWIFT_COMPILATION_MODE = wholemodule;
				SWIFT_OPTIMIZATION_LEVEL = "-O";
				SWIFT_VERSION = 4.0;
			};
			name = Release;
		};
		4C5764DC1C982DC900364AFE /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CODE_SIGN_ENTITLEMENTS = SpotifyNotify/SpotifyNotify.entitlements;
				CODE_SIGN_IDENTITY = "Apple Development";
				CODE_SIGN_STYLE = Automatic;
				COMBINE_HIDPI_IMAGES = YES;
				DEAD_CODE_STRIPPING = YES;
				DEVELOPMENT_TEAM = LK98J2HBA6;
				ENABLE_HARDENED_RUNTIME = YES;
				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
				INFOPLIST_FILE = SpotifyNotify/Other/Info.plist;
				INFOPLIST_KEY_CFBundleDisplayName = "Spotify Notify";
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/../Frameworks",
				);
				MACOSX_DEPLOYMENT_TARGET = 13.0;
				PRODUCT_BUNDLE_IDENTIFIER = io.nahive.SpotifyNotify;
				PRODUCT_NAME = Notify;
				PROVISIONING_PROFILE_SPECIFIER = "";
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
				SWIFT_VERSION = 5.0;
			};
			name = Debug;
		};
		4C5764DD1C982DC900364AFE /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CODE_SIGN_ENTITLEMENTS = SpotifyNotify/SpotifyNotify.entitlements;
				CODE_SIGN_IDENTITY = "Apple Development";
				CODE_SIGN_STYLE = Automatic;
				COMBINE_HIDPI_IMAGES = YES;
				DEAD_CODE_STRIPPING = YES;
				DEVELOPMENT_TEAM = LK98J2HBA6;
				ENABLE_HARDENED_RUNTIME = YES;
				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
				INFOPLIST_FILE = SpotifyNotify/Other/Info.plist;
				INFOPLIST_KEY_CFBundleDisplayName = "Spotify Notify";
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/../Frameworks",
				);
				MACOSX_DEPLOYMENT_TARGET = 13.0;
				PRODUCT_BUNDLE_IDENTIFIER = io.nahive.SpotifyNotify;
				PRODUCT_NAME = Notify;
				PROVISIONING_PROFILE_SPECIFIER = "";
				SWIFT_VERSION = 5.0;
			};
			name = Release;
		};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
		4C5764BE1C982DC800364AFE /* Build configuration list for PBXProject "SpotifyNotify" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				4C5764D91C982DC900364AFE /* Debug */,
				4C5764DA1C982DC900364AFE /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
		4C5764DB1C982DC900364AFE /* Build configuration list for PBXNativeTarget "SpotifyNotify" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				4C5764DC1C982DC900364AFE /* Debug */,
				4C5764DD1C982DC900364AFE /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
		4C926D4A288043F40008809E /* XCRemoteSwiftPackageReference "KeyHolder" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/Clipy/KeyHolder";
			requirement = {
				branch = master;
				kind = branch;
			};
		};
		4C926D4D288044010008809E /* XCRemoteSwiftPackageReference "Magnet" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/Clipy/Magnet";
			requirement = {
				branch = master;
				kind = branch;
			};
		};
		4CDE08C22A35F53B009609E7 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern";
			requirement = {
				branch = main;
				kind = branch;
			};
		};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
		4C926D4B288043F40008809E /* KeyHolder */ = {
			isa = XCSwiftPackageProductDependency;
			package = 4C926D4A288043F40008809E /* XCRemoteSwiftPackageReference "KeyHolder" */;
			productName = KeyHolder;
		};
		4C926D4E288044010008809E /* Magnet */ = {
			isa = XCSwiftPackageProductDependency;
			package = 4C926D4D288044010008809E /* XCRemoteSwiftPackageReference "Magnet" */;
			productName = Magnet;
		};
		4CDE08C32A35F53B009609E7 /* LaunchAtLogin */ = {
			isa = XCSwiftPackageProductDependency;
			package = 4CDE08C22A35F53B009609E7 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */;
			productName = LaunchAtLogin;
		};
/* End XCSwiftPackageProductDependency section */
	};
	rootObject = 4C5764BB1C982DC800364AFE /* Project object */;
}


text:
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
   LastUpgradeVersion = "1500"
   version = "1.3">
   <BuildAction
      parallelizeBuildables = "YES"
      buildImplicitDependencies = "YES">
      <BuildActionEntries>
         <BuildActionEntry
            buildForTesting = "YES"
            buildForRunning = "YES"
            buildForProfiling = "YES"
            buildForArchiving = "YES"
            buildForAnalyzing = "YES">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "4C5764C21C982DC800364AFE"
               BuildableName = "Notify.app"
               BlueprintName = "SpotifyNotify"
               ReferencedContainer = "container:SpotifyNotify.xcodeproj">
            </BuildableReference>
         </BuildActionEntry>
      </BuildActionEntries>
   </BuildAction>
   <TestAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      shouldUseLaunchSchemeArgsEnv = "YES">
      <MacroExpansion>
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "4C5764C21C982DC800364AFE"
            BuildableName = "Notify.app"
            BlueprintName = "SpotifyNotify"
            ReferencedContainer = "container:SpotifyNotify.xcodeproj">
         </BuildableReference>
      </MacroExpansion>
      <Testables>
         <TestableReference
            skipped = "NO">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "4C5764D11C982DC900364AFE"
               BuildableName = "SpotifyNotify.xctest"
               BlueprintName = "SpotifyNotifyTests"
               ReferencedContainer = "container:SpotifyNotify.xcodeproj">
            </BuildableReference>
         </TestableReference>
      </Testables>
   </TestAction>
   <LaunchAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      launchStyle = "0"
      useCustomWorkingDirectory = "NO"
      ignoresPersistentStateOnLaunch = "NO"
      debugDocumentVersioning = "YES"
      debugServiceExtension = "internal"
      allowLocationSimulation = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "4C5764C21C982DC800364AFE"
            BuildableName = "Notify.app"
            BlueprintName = "SpotifyNotify"
            ReferencedContainer = "container:SpotifyNotify.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </LaunchAction>
   <ProfileAction
      buildConfiguration = "Release"
      shouldUseLaunchSchemeArgsEnv = "YES"
      savedToolIdentifier = ""
      useCustomWorkingDirectory = "NO"
      debugDocumentVersioning = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "4C5764C21C982DC800364AFE"
            BuildableName = "Notify.app"
            BlueprintName = "SpotifyNotify"
            ReferencedContainer = "container:SpotifyNotify.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </ProfileAction>
   <AnalyzeAction
      buildConfiguration = "Debug">
   </AnalyzeAction>
   <ArchiveAction
      buildConfiguration = "Release"
      revealArchiveInOrganizer = "YES">
   </ArchiveAction>
</Scheme>


text:
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:">
   </FileRef>
</Workspace>


text:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


text:
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or
  advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
  address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].

Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].

For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].

[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

repo_language: TypeScript


text:
# Contributing Guidelines

Please send a Pull Request if you have any additions or changes.

Pull requests will be accepted based on the following:

1. The pull request adheres to the repository's [Code of Conduct](/CODE_OF_CONDUCT.md)
1. If the company offer work in any of the Canary Islands
1. If you add the company in alphabetical order in the list
1. If you submit the company with all their attributes (name, website, social networks...)
1. If the company is not fully-remote. If it's full-remote, a great part of the team is in the Canary Islands.
1. The README.md is generated. Update only the [companies.ts](https://github.com/DanielRamosAcosta/tech-companies-canarias/blob/main/src/companies.ts) file.

Please adhere to the [Contributor Code of Conduct](/CODE_OF_CONDUCT.md).

Thanks for contributing!


text: export * from "https://deno.land/x/expect/mod.ts";


text:
.PHONY: lint clean format test

lint:
	deno run src/linter/lint.ts

format:
	deno fmt

test:
	deno test

README.md: src/build.ts src/companies.ts
	deno run src/build.ts > README.md


text:
name: Deno
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Setup repo
        uses: actions/checkout@v2
      - name: Setup Deno
        uses: denoland/setup-deno@main
        with:
          deno-version: v1.x
      - name: Cache Dependencies
        run: deno cache deps.ts
      - name: Run Tests
        run: deno test
      - name: Run Linter
        run: deno run src/linter/lint.ts
  build:
    runs-on: ubuntu-latest
    if: ${{ github.ref == 'refs/heads/main' }}
    steps:
      - uses: actions/checkout@master
      - name: Setup Deno
        uses: denoland/setup-deno@main
        with:
          deno-version: v1.x
      - name: Cache Dependencies
        run: deno cache deps.ts
      - name: Create local changes
        run: deno run src/build.ts > README.md
      - name: Commit & Push changes
        uses: actions-js/push@master
        with:
          github_token: ${{ secrets.PUSH_BUILD_TOKEN }}
          branch: main


text:
export const enum Island {
  EL_HIERRO = "El Hierro",
  LA_PALMA = "La Palma",
  LA_GOMERA = "La Gomera",
  TENERIFE = "Tenerife",
  GRAN_CANARIA = "Gran Canaria",
  FUERTEVENTURA = "Fuerteventura",
  LANZAROTE = "Lanzarote",
  LA_GRACIOSA = "La Graciosa",
}

export enum SocialNetworkType {
  FACEBOOK = "Facebook",
  GITHUB = "Github",
  INSTAGRAM = "Instagram",
  LINKEDIN = "Linkedin",
  TWITTER = "Twitter",
  YOUTUBE = "YouTube",
}

export const enum CompanyType {
  SOFTWARE_DEVELOPMENT,
}

export type SocialNetwork = { type: SocialNetworkType; link: string };

export type SocialNetworks = SocialNetwork[];

export type Islands = Island[];

export type Company = {
  name: string;
  type: CompanyType;
  website: string;
  hiringFrom: Islands;
  socialNetworks: SocialNetworks;
  jobPage: string;
};

export type Companies = Company[];


text:
import {
  Company,
  SocialNetwork,
  SocialNetworks,
  SocialNetworkType,
} from "./models.ts";
import { companies } from "./companies.ts";

const markdownHeader = `<!-- THIS FILE IS GENERATED, DO NOT TOUCH -->
# Tech Companies Canarias

Tech Companies Canarias is a list of tech companies that are offer tech work from the Canary Islands 🇮🇨.

Contributions are very welcome! Please [submit a pull request on GitHub](https://github.com/DanielRamosAcosta/tech-companies-canarias/blob/main/CONTRIBUTING.md).

| Company | Hiring from | Social Networks | Job Page |
| ------- | ----------- | --------------- | -------- |
`;

const iconsBaseUrl =
  "https://raw.githubusercontent.com/DanielRamosAcosta/tech-companies-canarias/main/icons";

const createSocialNetworkImageLink = (socialNetworkType: SocialNetworkType) =>
  `${iconsBaseUrl}/${socialNetworkType.toLowerCase()}.png`;

const createImage = (alt: string, url: string) => `![${alt}](${url})`;

const createLink = (content: string, link: string) => `[${content}](${link})`;

const createSocialNetworkEnabledIcon = ({
  type,
  link,
}: SocialNetwork) =>
  createLink(createImage(type, createSocialNetworkImageLink(type)), link);

const createSocialNetworkDisabledIcon = (type: SocialNetworkType) =>
  createLink(createImage(type, `${iconsBaseUrl}/empty.png`), "#");

const createSocialIcons = (socialNetworks: SocialNetworks) => {
  return Object.values(SocialNetworkType).map(
    (socialNetworkType) => {
      const socialNetwork = socialNetworks.find((socialNetwork) =>
        socialNetwork.type === socialNetworkType
      );

      return socialNetwork
        ? createSocialNetworkEnabledIcon(socialNetwork)
        : createSocialNetworkDisabledIcon(socialNetworkType);
    },
  ).join(" ");
};

const toColumns = ({
  name,
  website,
  hiringFrom,
  socialNetworks,
  jobPage,
}: Company) => [
  createLink(name, website),
  hiringFrom.join(", "),
  createSocialIcons(socialNetworks),
  createLink("💼", jobPage),
];

const toMarkdownColumns = (columns: string[]) => `| ${columns.join(" | ")} |`;

const companyTable = companies
  .map(toColumns)
  .map(toMarkdownColumns)
  .join("\n");

console.log(markdownHeader.concat(companyTable));