Skip to content

Commit

Permalink
Merge pull request #104 from wakatime/main
Browse files Browse the repository at this point in the history
Release v1.2.0
  • Loading branch information
alanhamlett authored Jun 17, 2023
2 parents 87caadf + 8695741 commit d56bce1
Show file tree
Hide file tree
Showing 16 changed files with 330 additions and 220 deletions.
7 changes: 0 additions & 7 deletions WakaTime/Controls/WKTextField.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// WKTextField.swift
// WakaTime
//
// Created by Tobias Lensing on 07.06.23.
//

import AppKit

class WKTextField: NSTextField {
Expand Down
File renamed without changes.
7 changes: 7 additions & 0 deletions WakaTime/Extensions/OptionalExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

extension Optional where Wrapped: Collection {
var isEmpty: Bool {
self?.isEmpty ?? true
}
}
16 changes: 16 additions & 0 deletions WakaTime/Extensions/ProcessExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

extension Process {
// Runs process.launch() prior to macOS 13 or process.run() on macOS 13 or newer.
// Adds Swift exception handling to process.launch().
func execute() throws {
if #available(macOS 13.0, *) {
// Use Process.run() on macOS 13 or newer. Process.run() throws Swift exceptions.
try self.run()
} else {
// Note: Process.launch() can throw ObjC exceptions. For further reference, see
// https://developer.apple.com/documentation/foundation/process/1414189-launch?changes=_3
try ObjC.catchException { self.launch() }
}
}
}
10 changes: 10 additions & 0 deletions WakaTime/Extensions/URLExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

extension URL {
// Implements foward-compat for macOS 13's URL.formatted
func formatted() -> String {
let components = URLComponents(url: self, resolvingAgainstBaseURL: true)
let path = components?.path ?? ""
return path.replacingOccurrences(of: " ", with: "\\ ")
}
}
12 changes: 12 additions & 0 deletions WakaTime/Helpers/Accessibility.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import AppKit

class Accessibility {
public static func requestA11yPermission() {
let prompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
let options: NSDictionary = [prompt: true]
let appHasPermission = AXIsProcessTrustedWithOptions(options)
if appHasPermission {
// print("has a11y permission")
}
}
}
171 changes: 171 additions & 0 deletions WakaTime/Helpers/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import Foundation

// swiftlint:disable force_unwrapping
// swiftlint:disable force_try
class Dependencies {
public static func installDependencies() {
Task {
if !(await isCLILatest()) {
downloadCLI()
}
}
}

private static func getLatestVersion() async throws -> String? {
struct Release: Decodable {
let tagName: String
private enum CodingKeys: String, CodingKey {
case tagName = "tag_name"
}
}

let apiUrl = "https://api.github.com/repos/wakatime/wakatime-cli/releases/latest"
var request = URLRequest(url: URL(string: apiUrl)!, cachePolicy: .reloadIgnoringCacheData)
let lastModified = ConfigFile.getSetting(section: "internal", key: "cli_version_last_modified", internalConfig: true)
let currentVersion = ConfigFile.getSetting(section: "internal", key: "cli_version", internalConfig: true)
if let lastModified, currentVersion != nil {
request.setValue(lastModified, forHTTPHeaderField: "If-Modified-Since")
}
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else { return nil }

if httpResponse.statusCode == 304 {
// Current version is still the latest version available
return currentVersion
} else if let lastModified = httpResponse.value(forHTTPHeaderField: "Last-Modified"),
let release = try? JSONDecoder().decode(Release.self, from: data) {
// Remote version successfully decoded
ConfigFile.setSetting(section: "internal", key: "cli_version_last_modified", val: lastModified, internalConfig: true)
ConfigFile.setSetting(section: "internal", key: "cli_version", val: release.tagName, internalConfig: true)
return release.tagName
} else {
// Unexpected response
return nil
}
}

private static func isCLILatest() async -> Bool {
let cli = NSString.path(
withComponents: ConfigFile.resourcesFolder + ["wakatime-cli"]
)
guard FileManager.default.fileExists(atPath: cli) else { return false }

let outputPipe = Pipe()
let process = Process()
process.launchPath = cli
process.arguments = ["--version"]
process.standardOutput = outputPipe
process.standardError = FileHandle.nullDevice
do {
try process.run()
} catch {
// Error running CLI process
return false
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(decoding: outputData, as: UTF8.self)
let version: String?
if let regex = try? NSRegularExpression(pattern: "([0-9]+\\.[0-9]+\\.[0-9]+)"),
let match = regex.firstMatch(in: output, range: NSRange(output.startIndex..., in: output)),
let range = Range(match.range, in: output) {
version = String(output[range])
} else {
version = nil
}
let remoteVersion = try? await getLatestVersion()
guard let remoteVersion else {
// Could not retrieve remote version
return true
}
if let version, "v" + version == remoteVersion {
// Local version up to date
return true
} else {
// Newer version available
return false
}
}

private static func downloadCLI() {
let dir = NSString.path(withComponents: ConfigFile.resourcesFolder)
if !FileManager.default.fileExists(atPath: dir) {
do {
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil)
} catch {
print(error.localizedDescription)
}
}

let url = "https://github.com/wakatime/wakatime-cli/releases/latest/download/wakatime-cli-darwin-\(architecture()).zip"
let zipFile = NSString.path(withComponents: ConfigFile.resourcesFolder + ["wakatime-cli.zip"])
let cli = NSString.path(withComponents: ConfigFile.resourcesFolder + ["wakatime-cli"])
let cliReal = NSString.path(withComponents: ConfigFile.resourcesFolder + ["wakatime-cli-darwin-\(architecture())"])

if FileManager.default.fileExists(atPath: zipFile) {
do {
try FileManager.default.removeItem(atPath: zipFile)
} catch {
print(error.localizedDescription)
return
}
}

URLSession.shared.downloadTask(with: URLRequest(url: URL(string: url)!)) { fileUrl, _, _ in
guard let fileUrl else { return }

do {
// download wakatime-cli.zip
try FileManager.default.moveItem(at: fileUrl, to: URL(fileURLWithPath: zipFile))

if FileManager.default.fileExists(atPath: cliReal) {
do {
try FileManager.default.removeItem(atPath: cliReal)
} catch {
print(error.localizedDescription)
return
}
}

// unzip wakatime-cli.zip
let process = Process()
process.launchPath = "/usr/bin/unzip"
process.arguments = [zipFile, "-d", dir]
process.standardOutput = FileHandle.nullDevice
process.standardError = FileHandle.nullDevice
process.launch()
process.waitUntilExit()

// cleanup wakatime-cli.zip
try! FileManager.default.removeItem(atPath: zipFile)

// create ~/.wakatime/wakatime-cli symlink
do {
try FileManager.default.removeItem(atPath: cli)
} catch { }
try! FileManager.default.createSymbolicLink(atPath: cli, withDestinationPath: cliReal)

} catch {
print(error.localizedDescription)
}
}.resume()
}

private static func architecture() -> String {
var systeminfo = utsname()
uname(&systeminfo)
let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr -> String in
let data = Data(bufPtr)
if let lastIndex = data.lastIndex(where: { $0 != 0 }) {
return String(data: data[0...lastIndex], encoding: .isoLatin1)!
} else {
return String(data: data, encoding: .isoLatin1)!
}
}
if machine == "x86_64" {
return "amd64"
}
return "arm64"
}
}
// swiftlint:enable force_unwrapping
// swiftlint:enable force_try
12 changes: 12 additions & 0 deletions WakaTime/Helpers/SettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ class SettingsManager {
}
}

static func shouldRegisterAsLoginItem() -> Bool {
guard
!loginItemRegistered(),
PropertiesManager.shouldLaunchOnLogin
else { return false }
#if DEBUG
return false
#else
return true
#endif
}

static func registerAsLoginItem() {
PropertiesManager.shouldLaunchOnLogin = true

Expand Down
28 changes: 28 additions & 0 deletions WakaTime/Utils/Atomic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

@propertyWrapper
struct Atomic<Value> {
private var value: Value
private let lock = NSLock()

init(wrappedValue value: Value) {
self.value = value
}

var wrappedValue: Value {
get { getValue() }
set { setValue(newValue) }
}

func getValue() -> Value {
lock.lock()
defer { lock.unlock() }
return value
}

mutating func setValue(_ newValue: Value) {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
12 changes: 12 additions & 0 deletions WakaTime/Utils/ObjC.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#ifndef ObjC_h
#define ObjC_h

#import <Foundation/Foundation.h>

@interface ObjC : NSObject

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;

@end

#endif /* ObjC_h */
18 changes: 18 additions & 0 deletions WakaTime/Utils/ObjC.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import <Foundation/Foundation.h>

#import "ObjC.h"

@implementation ObjC

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (NSException *exception) {
*error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
return NO;
}
}

@end
1 change: 0 additions & 1 deletion WakaTime/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ class SettingsView: NSView, NSTextFieldDelegate {

lazy var versionLabel: NSTextField = {
let versionString = "Version: \(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "")"
+ "(\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""))"
let versionLabel = NSTextField(labelWithString: versionString)
versionLabel.translatesAutoresizingMaskIntoConstraints = false
return versionLabel
Expand Down
5 changes: 5 additions & 0 deletions WakaTime/WakaTime-Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "Utils/ObjC.h"
Loading

0 comments on commit d56bce1

Please sign in to comment.