Drop the modules/threads tabs; show both as collapsible sidebar groups mirroring the hooks UX: a handful of top picks plus a browse popover for the rest. Default threads to collapsed. Revert the eager re-analysis of persisted modules on attach; keep disassembly analysis lazy and add a per-module Analyze action and status indicator instead. Share one sidebar-row and one browser-popover component across hooks, modules, and threads in both the SwiftUI and GTK frontends.
161 lines
5.2 KiB
Swift
161 lines
5.2 KiB
Swift
public struct ThreadDelta: Sendable {
|
|
public let added: [ProcessThread]
|
|
public let removed: [UInt]
|
|
public let renamed: [Rename]
|
|
|
|
public init(added: [ProcessThread] = [], removed: [UInt] = [], renamed: [Rename] = []) {
|
|
self.added = added
|
|
self.removed = removed
|
|
self.renamed = renamed
|
|
}
|
|
|
|
public var isEmpty: Bool { added.isEmpty && removed.isEmpty && renamed.isEmpty }
|
|
|
|
public struct Rename: Sendable, Hashable {
|
|
public let id: UInt
|
|
public let name: String?
|
|
|
|
public init(id: UInt, name: String?) {
|
|
self.id = id
|
|
self.name = name
|
|
}
|
|
}
|
|
|
|
public func applied(to base: [ProcessThread]?) -> [ProcessThread] {
|
|
var result = base ?? []
|
|
let removedSet = Set(removed)
|
|
if !removedSet.isEmpty {
|
|
result.removeAll { removedSet.contains($0.id) }
|
|
}
|
|
result.append(contentsOf: added)
|
|
for r in renamed {
|
|
if let i = result.firstIndex(where: { $0.id == r.id }) {
|
|
result[i].name = r.name
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
extension Array where Element == ProcessThread {
|
|
public func sidebarHighlights(
|
|
selectedID: ProcessThread.ID?,
|
|
limit: Int = SidebarHighlights.defaultLimit
|
|
) -> [ProcessThread] {
|
|
guard let main = first else { return [] }
|
|
let rest = dropFirst()
|
|
let named = rest.filter { $0.name != nil }
|
|
let unnamed = rest.filter { $0.name == nil }
|
|
let peers = (named + unnamed).prefix(Swift.max(0, limit - 1))
|
|
let featured = [main] + peers
|
|
return featured.withSelected(selectedID, from: self, limit: limit)
|
|
}
|
|
}
|
|
|
|
public struct ProcessThread: Hashable, Identifiable, Codable, Sendable {
|
|
public var id: UInt
|
|
public var name: String?
|
|
public var entrypoint: Entrypoint?
|
|
|
|
public init(id: UInt, name: String? = nil, entrypoint: Entrypoint? = nil) {
|
|
self.id = id
|
|
self.name = name
|
|
self.entrypoint = entrypoint
|
|
}
|
|
|
|
public struct Entrypoint: Hashable, Codable, Sendable {
|
|
public let routine: UInt64
|
|
public let parameter: UInt64?
|
|
|
|
public init(routine: UInt64, parameter: UInt64? = nil) {
|
|
self.routine = routine
|
|
self.parameter = parameter
|
|
}
|
|
}
|
|
|
|
public func toJSON() -> [String: Any] {
|
|
var obj: [String: Any] = ["id": Int(id)]
|
|
if let name { obj["name"] = name }
|
|
if let entrypoint {
|
|
var ep: [String: Any] = ["routine": String(format: "0x%llx", entrypoint.routine)]
|
|
if let parameter = entrypoint.parameter {
|
|
ep["parameter"] = String(format: "0x%llx", parameter)
|
|
}
|
|
obj["entrypoint"] = ep
|
|
}
|
|
return obj
|
|
}
|
|
|
|
public static func fromJSON(_ obj: [String: Any]) -> ProcessThread? {
|
|
guard let rawID = obj["id"] as? Int else { return nil }
|
|
let name = obj["name"] as? String
|
|
var entry: Entrypoint?
|
|
if let ep = obj["entrypoint"] as? [String: Any],
|
|
let routineStr = ep["routine"] as? String,
|
|
let routine = parseHexAddress(routineStr)
|
|
{
|
|
let param = (ep["parameter"] as? String).flatMap(parseHexAddress)
|
|
entry = Entrypoint(routine: routine, parameter: param)
|
|
}
|
|
return ProcessThread(id: UInt(rawID), name: name, entrypoint: entry)
|
|
}
|
|
}
|
|
|
|
private func parseHexAddress(_ s: String) -> UInt64? {
|
|
let trimmed = s.hasPrefix("0x") ? String(s.dropFirst(2)) : s
|
|
return UInt64(trimmed, radix: 16)
|
|
}
|
|
|
|
public struct ThreadSnapshot: Sendable {
|
|
public var id: UInt
|
|
public var name: String?
|
|
public var state: String
|
|
public var registers: [Register]
|
|
|
|
public struct Register: Sendable, Hashable, Identifiable {
|
|
public var id: String { name }
|
|
public let name: String
|
|
public let rawValue: String
|
|
|
|
public init(name: String, rawValue: String) {
|
|
self.name = name
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
public var pointerValue: UInt64? {
|
|
let trimmed = rawValue.hasPrefix("0x") ? String(rawValue.dropFirst(2)) : rawValue
|
|
return UInt64(trimmed, radix: 16)
|
|
}
|
|
}
|
|
|
|
public init(id: UInt, name: String?, state: String, registers: [Register]) {
|
|
self.id = id
|
|
self.name = name
|
|
self.state = state
|
|
self.registers = registers
|
|
}
|
|
|
|
public static func fromJSON(_ obj: [String: Any]) -> ThreadSnapshot? {
|
|
guard let rawID = obj["id"] as? Int,
|
|
let state = obj["state"] as? String,
|
|
let regsAny = obj["registers"] as? [[Any]]
|
|
else { return nil }
|
|
|
|
let name = obj["name"] as? String
|
|
|
|
var regs: [Register] = []
|
|
regs.reserveCapacity(regsAny.count)
|
|
for entry in regsAny {
|
|
guard entry.count == 2,
|
|
let key = entry[0] as? String,
|
|
let value = entry[1] as? String
|
|
else { continue }
|
|
regs.append(Register(name: key, rawValue: value))
|
|
}
|
|
|
|
let order = CpuRegisterLayout.ordered(regs.map(\.name))
|
|
let byName = Dictionary(uniqueKeysWithValues: regs.map { ($0.name, $0) })
|
|
return ThreadSnapshot(id: UInt(rawID), name: name, state: state, registers: order.map { byName[$0]! })
|
|
}
|
|
}
|