Private
Public Access
1
0

initial commit

This commit is contained in:
2025-08-24 16:24:21 -07:00
parent fc62f0533d
commit b5a2f318b4
13 changed files with 380 additions and 43 deletions

View File

@@ -250,7 +250,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = kordophone2/kordophone2.entitlements; CODE_SIGN_ENTITLEMENTS = "kordophone2/Supporting Files/kordophone2.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@@ -277,7 +277,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = kordophone2/kordophone2.entitlements; CODE_SIGN_ENTITLEMENTS = "kordophone2/Supporting Files/kordophone2.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;

44
kordophone2/App.swift Normal file
View File

@@ -0,0 +1,44 @@
//
// kordophone2App.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import SwiftUI
@main
struct KordophoneApp: App
{
@State var conversationListModel = ConversationListView.ViewModel()
private let xpcClient = XPCClient()
var body: some Scene {
WindowGroup {
NavigationSplitView {
ConversationListView(model: $conversationListModel)
.frame(minWidth: 330.0)
.task {
await refreshConversations()
}
} detail: {
// Detail
}
}
}
private func refreshConversations() async {
do {
let conversations = try await xpcClient.getConversations()
conversationListModel.conversations = conversations.map { Display.Conversation(from: $0) }
} catch {
reportError(error)
}
}
private func reportError(_ e: Error) {
// Just printing for now.
print("Error: \(e.localizedDescription)")
}
}

View File

@@ -0,0 +1,7 @@
//
// ChatTranscriptView.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//

View File

@@ -1,24 +0,0 @@
//
// ContentView.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}

View File

@@ -0,0 +1,49 @@
//
// ConversationListView.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import SwiftUI
struct ConversationListView: View
{
@Binding var model: ViewModel
var body: some View {
List($model.conversations, selection: $model.selectedConversations) { conv in
VStack(alignment: .leading) {
Text(conv.wrappedValue.displayName)
.bold()
Text(conv.wrappedValue.messagePreview)
}
.padding(8.0)
}
.listStyle(.sidebar)
}
// MARK: - Types
@Observable
class ViewModel
{
var conversations: [Display.Conversation]
var selectedConversations: Set<Display.Conversation.ID>
public init(conversations: [Display.Conversation] = []) {
self.conversations = conversations
self.selectedConversations = Set()
}
}
}
#Preview {
@Previewable @State var viewModel = ConversationListView.ViewModel(conversations: [
.init(id: "asdf", name: "Cool", participants: ["me"], messagePreview: "Hello there"),
.init(id: "gjkl", name: "Nice", participants: ["me"], messagePreview: "How are you"),
])
ConversationListView(model: $viewModel)
}

73
kordophone2/Models.swift Normal file
View File

@@ -0,0 +1,73 @@
//
// Models.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import Foundation
import XPC
enum Display
{
struct Conversation: Identifiable
{
let id: String
let name: String?
let participants: [String]
let messagePreview: String
var displayName: String {
if let name, name.count > 0 { return name }
else { return participants.joined(separator: ", ") }
}
init(from c: Serialized.Conversation) {
self.id = c.guid
self.name = c.displayName
self.participants = c.participants
self.messagePreview = c.lastMessagePreview ?? ""
}
init(id: String = UUID().uuidString, name: String? = nil, participants: [String], messagePreview: String) {
self.id = id
self.name = name
self.participants = participants
self.messagePreview = messagePreview
}
}
}
enum Serialized
{
struct Conversation: Decodable
{
let guid: String
let displayName: String?
let participants: [String]
let lastMessagePreview: String?
let unreadCount: Int
let date: Date
init?(xpc dict: xpc_object_t)
{
guard let g: String = dict["guid"] else { return nil }
let dn: String? = dict["display_name"]
let lmp: String? = dict["last_message_preview"]
let names: [String] = dict["participants"] ?? []
let unread: Int = dict["unread_count"] ?? 0
let dt: Date = dict["date"] ?? Date(timeIntervalSince1970: 0)
self.guid = g
self.displayName = dn
self.participants = names
self.lastMessagePreview = lmp
self.unreadCount = unread
self.date = dt
}
}
}

View File

@@ -6,5 +6,9 @@
<true/> <true/>
<key>com.apple.security.files.user-selected.read-only</key> <key>com.apple.security.files.user-selected.read-only</key>
<true/> <true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>net.buzzert.kordophonecd</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,107 @@
//
// XPCClient.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import Foundation
import XPC
private let serviceName = "net.buzzert.kordophonecd"
final class XPCClient
{
private let connection: xpc_connection_t
init() {
self.connection = xpc_connection_create_mach_service(serviceName, nil, 0)
let handler: xpc_handler_t = { _ in }
xpc_connection_set_event_handler(connection, handler)
xpc_connection_resume(connection)
}
public func getVersion() async throws -> String {
let req = makeRequest(method: "GetVersion")
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError }
if let cstr = xpc_dictionary_get_string(reply, "version") {
return String(cString: cstr)
}
throw Error.typeError
}
public func getConversations(limit: Int = 100, offset: Int = 0) async throws -> [Serialized.Conversation] {
var args: [String: xpc_object_t] = [:]
args["limit"] = xpcString(String(limit))
args["offset"] = xpcString(String(offset))
let req = makeRequest(method: "GetConversations", arguments: args)
guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { return [] }
guard let items = xpc_dictionary_get_value(reply, "conversations"), xpc_get_type(items) == XPC_TYPE_ARRAY else { return [] }
var results: [Serialized.Conversation] = []
xpc_array_apply(items) { _, element in
if xpc_get_type(element) == XPC_TYPE_DICTIONARY, let conv = Serialized.Conversation(xpc: element) {
results.append(conv)
}
return true
}
return results
}
// MARK: - Types
enum Error: Swift.Error
{
case typeError
case encodingError
}
}
extension XPCClient
{
private func makeRequest(method: String, arguments: [String: xpc_object_t]? = nil) -> xpc_object_t {
let dict = xpc_dictionary_create(nil, nil, 0)
xpc_dictionary_set_string(dict, "method", method)
if let args = arguments {
let argsDict = xpc_dictionary_create(nil, nil, 0)
for (k, v) in args {
k.withCString { cKey in
xpc_dictionary_set_value(argsDict, cKey, v)
}
}
xpc_dictionary_set_value(dict, "arguments", argsDict)
}
return dict
}
private func xpcString(_ s: String) -> xpc_object_t {
return s.withCString { ptr in xpc_string_create(ptr) }
}
private func sendSync(_ request: xpc_object_t) async throws -> xpc_object_t? {
try await withCheckedThrowingContinuation { continuation in
xpc_connection_send_message_with_reply(connection, request, DispatchQueue.global(qos: .userInitiated)) { r in
switch xpc_get_type(r) {
case XPC_TYPE_ERROR:
let error = xpc_dictionary_get_value(r, "error")
if let error = error, let errorString = xpc_string_get_string_ptr(error) {
print("XPC error: \(String(cString: errorString))")
}
continuation.resume(throwing: Error.typeError)
case XPC_TYPE_DICTIONARY:
continuation.resume(returning: r)
default:
continuation.resume(throwing: Error.typeError)
}
}
}
}
}

View File

@@ -0,0 +1,94 @@
//
// XPCConvertible.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import Foundation
import XPC
protocol XPCConvertible
{
static func fromXPC(_ value: xpc_object_t) -> Self?
}
extension String: XPCConvertible
{
static func fromXPC(_ value: xpc_object_t) -> String? {
guard xpc_get_type(value) == XPC_TYPE_STRING, let cstr = xpc_string_get_string_ptr(value) else {
return nil
}
return String(cString: cstr)
}
}
extension Int: XPCConvertible
{
static func fromXPC(_ value: xpc_object_t) -> Int? {
switch xpc_get_type(value) {
case XPC_TYPE_INT64:
return Int(xpc_int64_get_value(value))
case XPC_TYPE_UINT64:
return Int(xpc_uint64_get_value(value))
case XPC_TYPE_STRING:
if let cstr = xpc_string_get_string_ptr(value) {
return Int(String(cString: cstr))
}
default:
return nil
}
return nil
}
}
extension Date: XPCConvertible
{
static func fromXPC(_ value: xpc_object_t) -> Date? {
// Accept seconds since epoch as int/uint or string
if let seconds: Int = Int.fromXPC(value) {
return Date(timeIntervalSince1970: TimeInterval(seconds))
}
return nil
}
}
extension Array: XPCConvertible where Element: XPCConvertible
{
static func fromXPC(_ value: xpc_object_t) -> [Element]? {
guard xpc_get_type(value) == XPC_TYPE_ARRAY else {
return nil
}
var result: [Element] = []
xpc_array_apply(value) { _, item in
if let element = Element.fromXPC(item) {
result.append(element)
}
return true
}
return result
}
}
extension xpc_object_t
{
func get<T: XPCConvertible>(_ key: String) -> T? {
var raw: xpc_object_t?
key.withCString { cKey in
raw = xpc_dictionary_get_value(self, cKey)
}
guard let value = raw else { return nil }
return T.fromXPC(value)
}
subscript<T: XPCConvertible>(key: String) -> T? {
return get(key)
}
}

View File

@@ -1,17 +0,0 @@
//
// kordophone2App.swift
// kordophone2
//
// Created by James Magahern on 8/24/25.
//
import SwiftUI
@main
struct kordophone2App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}