Skip to content

Commit

Permalink
feat: add debug profile to feedback message
Browse files Browse the repository at this point in the history
  • Loading branch information
louis.pontoise authored and lwouis committed Mar 10, 2020
1 parent 671fdab commit a14f965
Show file tree
Hide file tree
Showing 27 changed files with 164 additions and 42 deletions.
8 changes: 8 additions & 0 deletions alt-tab-macos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@
D04BA84074E5FD6221720BC7 /* CollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAACB6648E7C2A4E0339D /* CollectionViewFlowLayout.swift */; };
D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */; };
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */; };
D04BA9EE5D34A2789DCB0EE2 /* Sysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA896E37EFD27CAB61DF0 /* Sysctl.swift */; };
D04BAAD43731608067734ED3 /* DispatchQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA56E285C3FCDA52ED262 /* DispatchQueues.swift */; };
D04BAB5E802C938E78839011 /* TextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8011143819B48F204C2 /* TextArea.swift */; };
D04BABF88726DA42B2CBA68B /* ThumbnailsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAF40D5E54AD1044B3FF7 /* ThumbnailsPanel.swift */; };
D04BAC0BF53A80D4F1EE22ED /* AboutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE23C37E0F3B07EEE7B1 /* AboutTab.swift */; };
D04BAC4F69FE9563BC1C5E9C /* DebugProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3915020FB9B34555D74 /* DebugProfile.swift */; };
D04BAC9C031D482119F6DEB8 /* FeedbackWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAFA84FD0B02215718F94 /* FeedbackWindow.swift */; };
D04BACD398A35D82D514A9F7 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BABC180117F8785D250E1 /* TextField.swift */; };
D04BAD5A6B2F9EEE6FD4185F /* CollectionViewItemTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA4BABBA0312E0EDBA647 /* CollectionViewItemTitle.swift */; };
Expand All @@ -63,6 +65,7 @@
D04BA2D2BCBA4C47E25315AF /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = "<group>"; };
D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA35456DA0DDA74F9687E /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
D04BA3915020FB9B34555D74 /* DebugProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugProfile.swift; sourceTree = "<group>"; };
D04BA3D65E7CA78D699EDAB0 /* LabelAndControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAndControl.swift; sourceTree = "<group>"; };
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
D04BA40A4291E4F310527DBF /* AXUIElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AXUIElement.swift; sourceTree = "<group>"; };
Expand All @@ -86,6 +89,7 @@
D04BA7ECCE728582D9ECA613 /* determine_version.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = determine_version.sh; sourceTree = "<group>"; };
D04BA8011143819B48F204C2 /* TextArea.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextArea.swift; sourceTree = "<group>"; };
D04BA82F792DF53958D92572 /* alt-tab-macos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "alt-tab-macos.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D04BA896E37EFD27CAB61DF0 /* Sysctl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sysctl.swift; sourceTree = "<group>"; };
D04BA89FAEC4A5734D892C4B /* build_release.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = build_release.sh; sourceTree = "<group>"; };
D04BA8BEE821E2062F23AA97 /* CollectionViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItem.swift; sourceTree = "<group>"; };
D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperExtensions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -196,6 +200,7 @@
D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */,
D04BAF0DFC1F44322973CE1E /* PrivateApis.swift */,
D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */,
D04BA896E37EFD27CAB61DF0 /* Sysctl.swift */,
);
path = "api-wrappers";
sourceTree = "<group>";
Expand Down Expand Up @@ -293,6 +298,7 @@
D04BAF13DFAA6930676D0492 /* Application.swift */,
D04BA66B5B4143D2238F50B9 /* Applications.swift */,
D04BA56E285C3FCDA52ED262 /* DispatchQueues.swift */,
D04BA3915020FB9B34555D74 /* DebugProfile.swift */,
);
path = logic;
sourceTree = "<group>";
Expand Down Expand Up @@ -472,6 +478,8 @@
D04BA40A1C8B02448D720EA3 /* BaseLabel.swift in Sources */,
D04BAD5A6B2F9EEE6FD4185F /* CollectionViewItemTitle.swift in Sources */,
D04BA6D9DA2A8BCD93347F0E /* CollectionViewItemFontIcon.swift in Sources */,
D04BA9EE5D34A2789DCB0EE2 /* Sysctl.swift in Sources */,
D04BAC4F69FE9563BC1C5E9C /* DebugProfile.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/api-wrappers/AXUIElement.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

extension AXUIElement {
func cgWindowId() -> CGWindowID {
Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/api-wrappers/CGWindow.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

typealias CGWindow = [CFString: Any]

Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/api-wrappers/CGWindowID.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

extension CGWindowID {
func title() -> String? {
Expand Down
2 changes: 1 addition & 1 deletion alt-tab-macos/api-wrappers/HelperExtensions.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Foundation
import Cocoa

extension CGFloat {
Expand Down Expand Up @@ -101,6 +100,7 @@ extension NSView {
}

extension NSGridView {
// set height for all rows
func setRowsHeight(_ height: CGFloat) {
for i in 0..<numberOfRows {
row(at: i).height = height
Expand Down
5 changes: 2 additions & 3 deletions alt-tab-macos/api-wrappers/PrivateApis.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

// Private APIs are APIs that we can build the app against, but they are not supported or documented by Apple
// We can see their names as symbols in the SDK (see https://github.com/lwouis/MacOSX-SDKs)
Expand Down Expand Up @@ -231,8 +230,8 @@ func SLSRequestScreenCaptureAccess() -> UInt8
//func CGSProcessAssignToAllSpaces(_ cid: CGSConnectionID, _ pid: pid_t) -> CGError
//
//enum SpaceManagementMode: Int {
// case separate = 1
// case notSeparate = 0
// case checked = 1
// case unchecked = 0
//}
//
//// returns the status of the "Displays have separate Spaces" system Preference
Expand Down
47 changes: 47 additions & 0 deletions alt-tab-macos/api-wrappers/Sysctl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Foundation

public struct Sysctl {
static func run(_ name: String) -> String {
return run(name, { $0.baseAddress.flatMap { String(validatingUTF8: $0) } }) ?? ""
}

static func run<T>(_ name: String, _ type: T.Type) -> T? {
return run(name, { $0.baseAddress?.withMemoryRebound(to: T.self, capacity: 1) { $0.pointee } })
}

private static func run<R>(_ name: String, _ fn: (UnsafeBufferPointer<Int8>) -> R?) -> R? {
return keys(name).flatMap { keys in data(keys)?.withUnsafeBufferPointer() { fn($0) } }
}

private static func data(_ keys: [Int32]) -> [Int8]? {
return keys.withUnsafeBufferPointer() { keysPointer in
var requiredSize = 0
let preFlightResult = Darwin.sysctl(UnsafeMutablePointer<Int32>(mutating: keysPointer.baseAddress), UInt32(keys.count), nil, &requiredSize, nil, 0)
if preFlightResult != 0 {
return nil
}
let data = Array<Int8>(repeating: 0, count: requiredSize)
let result = data.withUnsafeBufferPointer() { dataBuffer -> Int32 in
return Darwin.sysctl(UnsafeMutablePointer<Int32>(mutating: keysPointer.baseAddress), UInt32(keys.count), UnsafeMutableRawPointer(mutating: dataBuffer.baseAddress), &requiredSize, nil, 0)
}
if result != 0 {
return nil
}
return data
}
}

private static func keys(_ name: String) -> [Int32]? {
var keysBufferSize = Int(CTL_MAXNAME)
var keysBuffer = Array<Int32>(repeating: 0, count: keysBufferSize)
_ = keysBuffer.withUnsafeMutableBufferPointer { (lbp: inout UnsafeMutableBufferPointer<Int32>) in
name.withCString { (nbp: UnsafePointer<Int8>) in
sysctlnametomib(nbp, lbp.baseAddress, &keysBufferSize)
}
}
if keysBuffer.count > keysBufferSize {
keysBuffer.removeSubrange(keysBufferSize..<keysBuffer.count)
}
return keysBuffer
}
}
1 change: 0 additions & 1 deletion alt-tab-macos/logic/Application.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Foundation
import Cocoa

class Application: NSObject {
Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/logic/Applications.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Foundation
import Cocoa

class Applications {
Expand Down
70 changes: 70 additions & 0 deletions alt-tab-macos/logic/DebugProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Cocoa
import Darwin

class DebugProfile {
static let intraSeparator = ": "
static let interSeparator = ", "
static let bulletPoint = "* "
static let nestedSeparator = "\n " + bulletPoint

static func make() -> String {
([
// app
("App version", App.version),
("App preferences", appPreferences()),
("Applications count", String(Applications.list.count)),
("Windows", appWindows()),
// os
("OS version", ProcessInfo.processInfo.operatingSystemVersionString),
("OS architecture", Sysctl.run("hw.machine")),
("Locale", Locale.current.debugDescription),
("Spaces count", String((CGSCopyManagedDisplaySpaces(cgsMainConnectionId) as! [NSDictionary]).map { $0["Spaces"] }.count)),
// hardware
("Hardware model", Sysctl.run("hw.model")),
("Displays count", String(NSScreen.screens.count)),
("CPU model", Sysctl.run("machdep.cpu.brand_string")),
("Memory size", ByteCountFormatter.string(fromByteCount: Int64(ProcessInfo.processInfo.physicalMemory), countStyle: .file)),
// TODO: add gpu model(s)
// hardware utilization
("Active CPU count", Sysctl.run("hw.activecpu", UInt.self).flatMap { String($0) } ?? ""),
("Current CPU frequency", Sysctl.run("hw.cpufrequency", Int.self).map { String(format: "%.1f", Double($0) / Double(1_000_000_000)) + " Ghz" } ?? ""),
// TODO: CPU utilization
// TODO: Active GPU
// TODO: GPU utilization
// TODO: Memory utilization
// TODO: disk space to detect disk pressure
// TODO: thermals to check if overheating
// TODO: battery to check if low-energy mode / throttling

] as [(String, String)])
.map { bulletPoint + $0.0 + intraSeparator + $0.1 }
.joined(separator: "\n")
}

private static func appPreferences() -> String {
return nestedSeparator + Preferences.rawValues
.sorted { $0.0 < $1.0 }
.map { $0 + intraSeparator + $1 }
.joined(separator: nestedSeparator)
}

private static func appWindows() -> String {
return nestedSeparator + Windows.list
.sorted { $0.cgWindowId < $1.cgWindowId }
.map { appWindow($0) }
.joined(separator: nestedSeparator)
}

private static func appWindow(_ window: Window) -> String {
return "{" + ([
("isMinimized", String(window.isMinimized)),
("isHidden", String(window.isHidden)),
("isOnAllSpaces", String(window.isOnAllSpaces)),
("spaceId", window.spaceId.flatMap { String($0) } ?? ""),
("spaceIndex", window.spaceIndex.flatMap { String($0) } ?? ""),
] as [(String, String)])
.map { $0.0 + intraSeparator + $0.1 }
.joined(separator: interSeparator)
+ "}"
}
}
1 change: 0 additions & 1 deletion alt-tab-macos/logic/Preferences.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Foundation
import Cocoa
import Carbon.HIToolbox.Events

Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/logic/Screen.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Foundation
import Cocoa

class Screen {
Expand Down
3 changes: 1 addition & 2 deletions alt-tab-macos/logic/Spaces.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class Spaces {
static var currentSpaceId = CGSSpaceID(1)
Expand Down Expand Up @@ -31,7 +30,7 @@ class Spaces {

static func allIdsAndIndexes() -> [(CGSSpaceID, SpaceIndex)] {
return (CGSCopyManagedDisplaySpaces(cgsMainConnectionId) as! [NSDictionary])
.map { return $0["Spaces"] }.joined().enumerated()
.map { $0["Spaces"] }.joined().enumerated()
.map { (($0.element as! NSDictionary)["id64"]! as! CGSSpaceID, $0.offset + 1) }
}

Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/logic/SystemPermissions.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Foundation
import Cocoa

// macOS has some privacy restrictions. The user needs to grant certain permissions, app by app, in System Preferences > Security & Privacy
Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/logic/Window.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class Window {
var cgWindowId: CGWindowID
Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/logic/Windows.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class Windows {
// order in the array is important: most-recently-used elements are first
Expand Down
2 changes: 1 addition & 1 deletion alt-tab-macos/ui/App.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import Cocoa
import Darwin

let cgsMainConnectionId = CGSMainConnectionID()

Expand Down
50 changes: 34 additions & 16 deletions alt-tab-macos/ui/FeedbackWindow.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Cocoa
import Foundation

class FeedbackWindow: NSWindow, NSTextViewDelegate {
var body: TextArea!
var email: TextArea!
var sendButton: NSButton!
var debugProfile: NSButton!

override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
super.init(contentRect: .zero, styleMask: style, backing: backingStoreType, defer: flag)
Expand Down Expand Up @@ -47,14 +47,16 @@ class FeedbackWindow: NSWindow, NSTextViewDelegate {
body = TextArea(80, 20, "I think the app could be improved with…")
body.delegate = self
email = TextArea(80, 1.1, "Optional: email (if you want a reply)")
debugProfile = NSButton(checkboxWithTitle: "Send debug profile", target: nil, action: nil)
debugProfile.state = .on
let view = GridView.make([
[header],
[body],
[email],
[debugProfile],
[buttons],
])
view.cell(atColumnIndex: 0, rowIndex: 3).xPlacement = .trailing
view.fit()
view.cell(atColumnIndex: 0, rowIndex: 4).xPlacement = .trailing
setContentSize(view.fittingSize)
contentView = view
}
Expand All @@ -70,24 +72,40 @@ class FeedbackWindow: NSWindow, NSTextViewDelegate {

@objc
private func sendCallback(senderControl: NSControl) {
URLSession.shared.dataTask(with: prepareRequest(), completionHandler: { data, response, error in
if error != nil || response == nil || (response as! HTTPURLResponse).statusCode != 201 {
debugPrint("HTTP call failed:", response ?? "nil", error ?? "nil")
}
}).resume()
close()
}

private func prepareRequest() -> URLRequest {
var request = URLRequest(url: URL(string: "https://api.github.com/repos/lwouis/alt-tab-macos/issues")!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
// access token of the alt-tab-macos-bot github account, with scope repo > public_repo
request.addValue("token 6ab65e11bb51e47835fe1f64970b1d7df0341653", forHTTPHeaderField: "Authorization")
let preamble = "_This issue was opened by a bot after a user submitted feedback through the in-app form. Here is what they wrote:_\n\n> "
let emailNote = !email.string.isEmpty ? "\n\nAuthor's email: " + email.string : ""
let parameters: [String: Any] = [
request.addValue("token 231413d7bf0e6cc533aae851c83dca25afed86bb", forHTTPHeaderField: "Authorization")
request.httpBody = try! JSONSerialization.data(withJSONObject: [
"title": "[In-app feedback]",
"body": preamble + body.string.replacingOccurrences(of: "\n", with: "\n> ") + emailNote
]
request.httpBody = try! JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if error != nil || response == nil || (response as! HTTPURLResponse).statusCode != 201 {
debugPrint("HTTP call failed:", response, error)
}
}).resume()
close()
"body": assembleBody()
])
return request
}

private func assembleBody() -> String {
var result = ""
result += "_This issue was opened by a bot after a user submitted feedback through the in-app form._"
if !email.string.isEmpty {
result += "\n\n__From:__ " + email.string
}
result += "\n\n__Message:__"
result += "\n\n> " + body.string.replacingOccurrences(of: "\n", with: "\n> ")
if debugProfile.state == .on {
result += "\n\n__Debug profile:__"
result += "\n\n" + DebugProfile.make()
}
return result
}
}
1 change: 0 additions & 1 deletion alt-tab-macos/ui/generic-components/GridView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class GridView {
static let padding = CGFloat(20)
Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/ui/generic-components/text/TextArea.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class TextArea: NSTextView {
static let paddingX = CGFloat(5)
Expand Down
1 change: 0 additions & 1 deletion alt-tab-macos/ui/preferences-window/LabelAndControl.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class LabelAndControl: NSObject {
static var callbackTarget: PreferencesWindow!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class PreferencesWindow: NSWindow {
var windowCloseRequested = false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cocoa
import Foundation

class TabViewController: NSTabViewController {
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
Expand Down
Loading

0 comments on commit a14f965

Please sign in to comment.