Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 82 additions & 41 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,92 @@

import PackageDescription

var products: [Product] = []
var targets: [Target] = [
.target(
name: "TimelineCore",
dependencies: [],
path: "TimelineCore/",
sources: [
"Alerts.swift",
"Logic/FilteredAppsStorage.swift",
"Logic/PureAlignedTimer.swift",
"Logic/PureCounter.swift",
"Logic/Tracker.swift",
"Storage/Model.swift",
"Storage/Storage.swift"
]),
.target(
name: "testing_utils",
dependencies: ["TimelineCore"],
path: "testing-utils/"),
.testTarget(
name: "TimelineCoreTests",
dependencies: ["TimelineCore", "testing_utils"],
path: "TimelineCoreTests/"),
]

var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/stephencelis/CSQLite.git", from: "0.0.3"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", "0.13.3"..<"0.14.0"),
]

targets.append(
.target(
name: "SQLiteStorage",
dependencies: [
"TimelineCore",
.product(name: "SQLite", package: "SQLite.swift"),
.product(name: "CSQLite", package: "CSQLite"),
],
path: "SQLiteStorage/")
)

#if os(macOS)
products.append(.executable(name: "Timeline-macOS", targets: ["TimelineCocoa"]))
targets.append(
.executableTarget(
name: "TimelineCocoa",
dependencies: ["TimelineCore", "SQLiteStorage"],
path: "macOS/",
sources: [
"AppDelegate.swift",
"CocoaApps.swift",
"CocoaTime.swift",
"Date+Extensions.swift",
"main.swift",
"StatisticsView.swift",
"CocoaAlerter.swift"
],
resources: [
.copy("macOS.entitlements"),
.copy("timeline--macOS--Info.plist"),
])
)
#endif

#if os(Linux)
dependencies.append(.package(url: "https://github.com/aestesis/X11.git", branch: "master"))
products.append(.executable(name: "Timeline-linux", targets: ["TimelineLinux"]))
targets.append(
.executableTarget(
name: "TimelineLinux",
dependencies: ["TimelineCore", "SQLiteStorage", "X11"],
path: "linux/",
sources: [
"main.swift",
"X11Apps.swift",
"LinuxAlerter.swift"
])
)
#endif

let package = Package(
name: "timeline",
platforms: [
.macOS(.v12)
],
products: [
.executable(name: "Timeline-macOS", targets: ["TimelineCocoa"]),
.executable(name: "Timeline-linux", targets: ["TimelineLinux"]),
],
dependencies: [
.package(url: "https://github.com/stephencelis/SQLite.swift.git", "0.13.3"..<"0.14.0"),
.package(url: "https://github.com/aestesis/X11.git", branch: "master"),
],
targets: [
.target(
name: "TimelineCore",
dependencies: [],
path: "TimelineCore/"),
.target(
name: "SQLiteStorage",
dependencies: [
"TimelineCore",
.product(name: "SQLite", package: "SQLite.swift"),
],
path: "SQLiteStorage/"),
.target(
name: "testing_utils",
dependencies: ["TimelineCore"],
path: "testing-utils/"),
.testTarget(
name: "TimelineCoreTests",
dependencies: ["TimelineCore", "testing_utils"],
path: "TimelineCoreTests/"),
.executableTarget(
name: "TimelineCocoa",
dependencies: ["TimelineCore", "SQLiteStorage"],
path: "macOS/",
resources: [
.copy("macOS/macOS.entitlements"),
.copy("macOS/timeline--macOS--Info.plist"),
]),
.executableTarget(
name: "TimelineLinux",
dependencies: ["TimelineCore", "SQLiteStorage", "X11"],
path: "linux/"),
]
products: products,
dependencies: dependencies,
targets: targets
)
143 changes: 89 additions & 54 deletions SQLiteStorage/SQLiteStorage.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import SQLite
import CSQLite
import TimelineCore

public class SQLiteStorage: Storage {
Expand All @@ -22,79 +23,113 @@ public class SQLiteStorage: Storage {
let appId = Expression<String>("appId")
let activityName = Expression<String>("activityName")
let duration = Expression<TimeInterval>("duration")

private static func mapStorageError(_ error: Error, operation: String) -> StorageError {
if case let Result.error(message, code, _) = error, code == SQLITE_FULL {
return .diskFull(reason: message)
}
let nsError = error as NSError
if nsError.domain == NSPOSIXErrorDomain && nsError.code == ENOSPC {
return .diskFull(reason: nsError.localizedDescription)
}
if nsError.domain == NSCocoaErrorDomain && nsError.code == NSFileWriteOutOfSpaceError {
return .diskFull(reason: nsError.localizedDescription)
}
return .cantWrite(reason: "\(operation): \(error.localizedDescription)")
}

public init(filepath: String) throws {
db = try Connection(filepath)
try db.run(appsTable.create(ifNotExists: true) {
$0.column(idColumn, primaryKey: true)
$0.column(trackingMode)
})
try db.run(appsTable.createIndex(idColumn, unique: true, ifNotExists: true))
try db.run(timelinesTable.create(ifNotExists: true) {
$0.column(idColumn, primaryKey: true)
$0.column(deviceName)
$0.column(deviceSystem)
$0.column(timezoneName)
$0.column(timezoneShift)
$0.column(dateStart)
})
try db.run(timelinesTable.createIndex(idColumn, unique: true, ifNotExists: true))
try db.run(logsTable.create(ifNotExists: true) {
$0.column(timelineId)
$0.column(timeslotStart)
$0.column(appId)
$0.column(activityName)
$0.column(duration)
$0.foreignKey(timelineId, references: timelinesTable, idColumn)
$0.foreignKey(appId, references: appsTable, idColumn)
})
try db.run(logsTable.createIndex(timeslotStart, unique: false, ifNotExists: true))
do {
db = try Connection(filepath)
try db.run(appsTable.create(ifNotExists: true) {
$0.column(idColumn, primaryKey: true)
$0.column(trackingMode)
})
try db.run(appsTable.createIndex(idColumn, unique: true, ifNotExists: true))
try db.run(timelinesTable.create(ifNotExists: true) {
$0.column(idColumn, primaryKey: true)
$0.column(deviceName)
$0.column(deviceSystem)
$0.column(timezoneName)
$0.column(timezoneShift)
$0.column(dateStart)
})
try db.run(timelinesTable.createIndex(idColumn, unique: true, ifNotExists: true))
try db.run(logsTable.create(ifNotExists: true) {
$0.column(timelineId)
$0.column(timeslotStart)
$0.column(appId)
$0.column(activityName)
$0.column(duration)
$0.foreignKey(timelineId, references: timelinesTable, idColumn)
$0.foreignKey(appId, references: appsTable, idColumn)
})
try db.run(logsTable.createIndex(timeslotStart, unique: false, ifNotExists: true))
} catch {
let mapped = Self.mapStorageError(error, operation: "open sqlite")
if case .diskFull = mapped {
throw mapped
}
throw StorageError.cantOpen(reason: error.localizedDescription)
}
}

public func store(log: Log) {
try! db.run(logsTable.insert(
timelineId <- log.timelineId,
timeslotStart <- log.timeslotStart,
appId <- log.appId,
activityName <- log.activityName,
duration <- log.duration
))
public func store(log: Log) throws {
do {
try db.run(logsTable.insert(
timelineId <- log.timelineId,
timeslotStart <- log.timeslotStart,
appId <- log.appId,
activityName <- log.activityName,
duration <- log.duration
))
} catch {
throw Self.mapStorageError(error, operation: "store log")
}
}

public func store(app: App) {
try! db.run(appsTable.upsert(
idColumn <- app.id,
trackingMode <- app.trackingMode,
onConflictOf: idColumn))
public func store(app: App) throws {
do {
try db.run(appsTable.upsert(
idColumn <- app.id,
trackingMode <- app.trackingMode,
onConflictOf: idColumn))
} catch {
throw Self.mapStorageError(error, operation: "store app")
}
}

public func store(timeline: Timeline) {
try! db.run(timelinesTable.upsert(
idColumn <- timeline.id,
deviceName <- timeline.deviceName,
deviceSystem <- timeline.deviceSystem,
timezoneName <- timeline.timezoneName,
timezoneShift <- timeline.timezoneShift,
dateStart <- timeline.dateStart,
onConflictOf: idColumn))
public func store(timeline: Timeline) throws {
do {
try db.run(timelinesTable.upsert(
idColumn <- timeline.id,
deviceName <- timeline.deviceName,
deviceSystem <- timeline.deviceSystem,
timezoneName <- timeline.timezoneName,
timezoneShift <- timeline.timezoneShift,
dateStart <- timeline.dateStart,
onConflictOf: idColumn))
} catch {
throw Self.mapStorageError(error, operation: "store timeline")
}
}

public func fetchLogs(since: Date, till: Date) -> [Log] {
let logs: [LogStruct] = try! db.prepare(logsTable.filter(timeslotStart >= since && timeslotStart < till))
let logs: [LogStruct]? = try? db.prepare(logsTable.filter(timeslotStart >= since && timeslotStart < till))
.map { try $0.decode() }
return logs
return logs ?? []
}

public func fetchApps() -> [String : App] {
let apps: [AppStruct] = try! db.prepare(appsTable).map { row -> AppStruct in
return AppStruct(id: try! row.get(idColumn), trackingMode: try! row.get(trackingMode))
let apps: [AppStruct]? = try? db.prepare(appsTable).map { row -> AppStruct in
return AppStruct(id: try row.get(idColumn), trackingMode: try row.get(trackingMode))
}
return Dictionary(grouping: apps) { $0.id } .mapValues { $0[0] }
return Dictionary(grouping: apps ?? []) { $0.id } .mapValues { $0[0] }
}

public func fetchTimeline(id: String) -> Timeline? {
let timelines: [TimelineStruct] = try! db.prepare(timelinesTable.filter(idColumn == id).limit(1)).map { try $0.decode() }
return timelines.first
let timelines: [TimelineStruct]? = try? db.prepare(timelinesTable.filter(idColumn == id).limit(1)).map { try $0.decode() }
return timelines?.first
}
}

Expand Down
5 changes: 5 additions & 0 deletions TimelineCore/Alerts.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public protocol Alerter {
func showAlert(title: String, message: String)
}
12 changes: 6 additions & 6 deletions TimelineCore/Logic/FilteredAppsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ public class FilteredAppsStorage: Storage {
self.innerStorage = storage
}

public func store(log: Log) {
innerStorage.store(log: log)
public func store(log: Log) throws {
try innerStorage.store(log: log)
}

public func store(app: App) {
innerStorage.store(app: app)
public func store(app: App) throws {
try innerStorage.store(app: app)
}

public func store(timeline: Timeline) {
innerStorage.store(timeline: timeline)
public func store(timeline: Timeline) throws {
try innerStorage.store(timeline: timeline)
}

public func fetchLogs(since: Date, till: Date) -> [Log] {
Expand Down
Loading