aboutsummaryrefslogtreecommitdiff
path: root/QSThingsPlugin
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-09-10 11:32:28 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-09-10 11:32:28 +0200
commitc791858db3de3568f0ea2839c79b360a0dfb15ab (patch)
tree7cf50c93cdf03075d10c14c8ad1f7a646b2a05c7 /QSThingsPlugin
Initial commit
Diffstat (limited to 'QSThingsPlugin')
-rw-r--r--QSThingsPlugin/Actions/SearchThingsAction.h11
-rw-r--r--QSThingsPlugin/Actions/SearchThingsAction.m24
-rw-r--r--QSThingsPlugin/Actions/ShowInThingsAction.h11
-rw-r--r--QSThingsPlugin/Actions/ShowInThingsAction.m33
-rw-r--r--QSThingsPlugin/Helpers/SQLHelper.h16
-rw-r--r--QSThingsPlugin/Helpers/SQLHelper.m84
-rw-r--r--QSThingsPlugin/Parsers/QSThingsAreasParser.h4
-rw-r--r--QSThingsPlugin/Parsers/QSThingsAreasParser.m33
-rw-r--r--QSThingsPlugin/Parsers/QSThingsProjectsParser.h4
-rw-r--r--QSThingsPlugin/Parsers/QSThingsProjectsParser.m42
-rw-r--r--QSThingsPlugin/Parsers/QSThingsTasksParser.h4
-rw-r--r--QSThingsPlugin/Parsers/QSThingsTasksParser.m43
-rw-r--r--QSThingsPlugin/QSThingsPluginSource.h10
-rw-r--r--QSThingsPlugin/QSThingsPluginSource.m59
-rw-r--r--QSThingsPlugin/QSThingsPluginSource.xib50
-rw-r--r--QSThingsPlugin/Types/Constants.h7
-rw-r--r--QSThingsPlugin/Types/Constants.m7
-rw-r--r--QSThingsPlugin/en.lproj/Localizable.strings1
18 files changed, 443 insertions, 0 deletions
diff --git a/QSThingsPlugin/Actions/SearchThingsAction.h b/QSThingsPlugin/Actions/SearchThingsAction.h
new file mode 100644
index 0000000..5f0ed48
--- /dev/null
+++ b/QSThingsPlugin/Actions/SearchThingsAction.h
@@ -0,0 +1,11 @@
+#import "Constants.h"
+#import <Foundation/Foundation.h>
+#import <QSCore/QSCore.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SearchThingsAction : QSActionProvider
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QSThingsPlugin/Actions/SearchThingsAction.m b/QSThingsPlugin/Actions/SearchThingsAction.m
new file mode 100644
index 0000000..9b042b3
--- /dev/null
+++ b/QSThingsPlugin/Actions/SearchThingsAction.m
@@ -0,0 +1,24 @@
+#import "SearchThingsAction.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation SearchThingsAction
+
+- (QSObject *)searchFor:(QSObject *)directObject
+{
+
+ NSString *contents = [directObject displayName];
+ if (contents) {
+ NSString *urlString = [NSString stringWithFormat:@"things:///search?query=%@", contents];
+ NSURL *url = [NSURL URLWithString:urlString];
+
+ [[NSWorkspace sharedWorkspace] openURL:url];
+ } else {
+ NSLog(@"Could not get the text of the Things object.");
+ }
+ return directObject;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QSThingsPlugin/Actions/ShowInThingsAction.h b/QSThingsPlugin/Actions/ShowInThingsAction.h
new file mode 100644
index 0000000..8d7790c
--- /dev/null
+++ b/QSThingsPlugin/Actions/ShowInThingsAction.h
@@ -0,0 +1,11 @@
+#import "Constants.h"
+#import <Foundation/Foundation.h>
+#import <QSCore/QSCore.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface ShowInThingsAction : QSActionProvider
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QSThingsPlugin/Actions/ShowInThingsAction.m b/QSThingsPlugin/Actions/ShowInThingsAction.m
new file mode 100644
index 0000000..91fe3ec
--- /dev/null
+++ b/QSThingsPlugin/Actions/ShowInThingsAction.m
@@ -0,0 +1,33 @@
+#import "ShowInThingsAction.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation ShowInThingsAction
+
+- (QSObject *)showObject:(QSObject *)directObject
+{
+ NSSet *validTypes = [NSSet setWithObjects:kAppSectionType, kTagType, kAreaType, kProjectType, kTaskType, nil];
+
+
+ if ([validTypes containsObject:directObject.primaryType]) {
+ NSDictionary *contents = [directObject objectForType:directObject.primaryType];
+ if (contents) {
+ NSString *uuid = [contents objectForKey:@"uuid"];
+ if (uuid) {
+ NSString *urlString = [NSString stringWithFormat:@"things:///show?id=%@", uuid];
+ NSURL *url = [NSURL URLWithString:urlString];
+
+ [[NSWorkspace sharedWorkspace] openURL:url];
+ } else {
+ NSLog(@"Things object did not have a uuid.");
+ }
+ } else {
+ NSLog(@"Could not get the contents of the Things object.");
+ }
+ }
+ return directObject;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QSThingsPlugin/Helpers/SQLHelper.h b/QSThingsPlugin/Helpers/SQLHelper.h
new file mode 100644
index 0000000..fc63650
--- /dev/null
+++ b/QSThingsPlugin/Helpers/SQLHelper.h
@@ -0,0 +1,16 @@
+/**
+ Helper class to access the things sqlite database.
+ */
+@interface SQLHelper : NSObject
+{
+}
+
+/**
+ Execute an SQL query on the things database
+ @param query the SQL query. Must contain output fieldnames "url" and "title".
+ @param path file path to the database
+ @returns an array of Task objects. Will return empty if an error occurs.
+ */
++ (NSArray *) executeSql:(NSString *)query onFile:(NSString *)path;
+
+@end
diff --git a/QSThingsPlugin/Helpers/SQLHelper.m b/QSThingsPlugin/Helpers/SQLHelper.m
new file mode 100644
index 0000000..6d94bb5
--- /dev/null
+++ b/QSThingsPlugin/Helpers/SQLHelper.m
@@ -0,0 +1,84 @@
+#import "SQLHelper.h"
+#import <QSFoundation/FMDatabase.h>
+
+@implementation SQLHelper
+
++ (NSArray *) executeSql:(NSString *)query onFile:(NSString *)path {
+
+ NSMutableArray *objects = [NSMutableArray arrayWithCapacity:0];
+ NSFileManager *manager = [NSFileManager defaultManager];
+
+ FMDatabase *database = [FMDatabase databaseWithPath:path];
+ if (![database openWithFlags:0x00000001]) { // 1 = SQLITE_OPEN_READONLY
+ NSLog(@"Could not open Firefox's places.sqlite DB.");
+ return @[];
+ }
+ [database goodConnection];
+
+ // execute SQL query
+ FMResultSet *resultSet = [database executeQuery:query];
+ if ([database hadError]) {
+ NSLog(@"Error while reading Things database. (%d): %@", [database lastErrorCode], [database lastErrorMessage]);
+ return @[];
+ }
+
+ while ([resultSet next]) {
+ // Create a completely independent dictionary with string copies
+ NSMutableDictionary *resultDictionary = [NSMutableDictionary dictionary];
+
+ // Manually copy each field to ensure we have independent string objects
+ NSDictionary *originalDict = resultSet.resultDictionary;
+ for (NSString *key in originalDict.allKeys) {
+ id value = [originalDict objectForKey:key];
+ if (value && value != [NSNull null]) {
+ // Make sure we have independent copies of strings
+ if ([value isKindOfClass:[NSString class]]) {
+ resultDictionary[key] = [NSString stringWithString:value];
+ } else {
+ resultDictionary[key] = value;
+ }
+ }
+ }
+
+ [objects addObject:[resultDictionary copy]]; // Make immutable copy
+ }
+
+ [resultSet close];
+ [database close];
+
+ return objects;
+}
+
++ (BOOL)appendString:(NSString *)string toFile:(NSString *)filePath {
+ // Create file manager
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+
+ // Check if file exists, create it if it doesn't
+ if (![fileManager fileExistsAtPath:filePath]) {
+ // Create empty file
+ [@"" writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
+ }
+
+ // Create file handle for appending
+ NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
+
+ if (!fileHandle) {
+ NSLog(@"Failed to open file for writing: %@", filePath);
+ return NO;
+ }
+
+ // Move to end of file
+ [fileHandle seekToEndOfFile];
+
+ // Convert string to data and write
+ NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
+ [fileHandle writeData:stringData];
+
+ // Close file handle
+ [fileHandle closeFile];
+
+ NSLog(@"Successfully appended string to: %@", filePath);
+ return YES;
+}
+
+@end
diff --git a/QSThingsPlugin/Parsers/QSThingsAreasParser.h b/QSThingsPlugin/Parsers/QSThingsAreasParser.h
new file mode 100644
index 0000000..74f829b
--- /dev/null
+++ b/QSThingsPlugin/Parsers/QSThingsAreasParser.h
@@ -0,0 +1,4 @@
+@interface QSThingsAreasParser : QSParser
+{
+}
+@end
diff --git a/QSThingsPlugin/Parsers/QSThingsAreasParser.m b/QSThingsPlugin/Parsers/QSThingsAreasParser.m
new file mode 100644
index 0000000..d4f9b5f
--- /dev/null
+++ b/QSThingsPlugin/Parsers/QSThingsAreasParser.m
@@ -0,0 +1,33 @@
+#import "Constants.h"
+#import "QSThingsAreasParser.h"
+#import "SQLHelper.h"
+
+@implementation QSThingsAreasParser
+- (BOOL)validParserForPath:(NSString *)path {
+ return [[path lastPathComponent] isEqualToString:@"main.sqlite"];
+}
+
+- (NSArray *)objectsFromPath:(NSString *)path withSettings:(NSDictionary *)settings {
+ NSString *query = @"SELECT "
+ "uuid, "
+ "title "
+ "FROM TMArea "
+ "ORDER BY title;";
+
+ NSArray *results = [SQLHelper executeSql:query onFile:path];
+
+ NSMutableArray *objects = [NSMutableArray array];
+ [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) {
+
+ QSObject *newObject = [QSObject
+ makeObjectWithIdentifier:[dictionary objectForKey:@"uuid"]];
+ [newObject setObject:dictionary forType:kProjectType];
+ [newObject setName:[dictionary objectForKey:@"title"]];
+ [newObject setPrimaryType:kProjectType];
+
+ [objects addObject:newObject];
+ }];
+
+ return objects;
+}
+@end
diff --git a/QSThingsPlugin/Parsers/QSThingsProjectsParser.h b/QSThingsPlugin/Parsers/QSThingsProjectsParser.h
new file mode 100644
index 0000000..cee939d
--- /dev/null
+++ b/QSThingsPlugin/Parsers/QSThingsProjectsParser.h
@@ -0,0 +1,4 @@
+@interface QSThingsProjectsParser : QSParser
+{
+}
+@end
diff --git a/QSThingsPlugin/Parsers/QSThingsProjectsParser.m b/QSThingsPlugin/Parsers/QSThingsProjectsParser.m
new file mode 100644
index 0000000..1a7956e
--- /dev/null
+++ b/QSThingsPlugin/Parsers/QSThingsProjectsParser.m
@@ -0,0 +1,42 @@
+#import "Constants.h"
+#import "QSThingsProjectsParser.h"
+#import "SQLHelper.h"
+
+@implementation QSThingsProjectsParser
+- (BOOL)validParserForPath:(NSString *)path {
+ return [[path lastPathComponent] isEqualToString:@"main.sqlite"];
+}
+
+- (NSArray *)objectsFromPath:(NSString *)path withSettings:(NSDictionary *)settings {
+ NSString *query = @"SELECT "
+ "uuid, "
+ "creationDate, "
+ "status, "
+ "title, "
+ "notes, "
+ "area, "
+ "project "
+ "FROM TMTask "
+ "WHERE trashed = 0 "
+ "AND type = 1 "
+ "AND status < 3 "
+ "ORDER BY title;";
+
+ NSArray *results = [SQLHelper executeSql:query onFile:path];
+
+ NSMutableArray *objects = [NSMutableArray array];
+ [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) {
+
+ QSObject *newObject = [QSObject
+ makeObjectWithIdentifier:[dictionary objectForKey:@"uuid"]];
+ [newObject setObject:dictionary forType:kProjectType];
+ [newObject setName:[dictionary objectForKey:@"title"]];
+ [newObject setDetails:[dictionary objectForKey:@"notes"]];
+ [newObject setPrimaryType:kProjectType];
+
+ [objects addObject:newObject];
+ }];
+
+ return objects;
+}
+@end
diff --git a/QSThingsPlugin/Parsers/QSThingsTasksParser.h b/QSThingsPlugin/Parsers/QSThingsTasksParser.h
new file mode 100644
index 0000000..7e3f1f2
--- /dev/null
+++ b/QSThingsPlugin/Parsers/QSThingsTasksParser.h
@@ -0,0 +1,4 @@
+@interface QSThingsTasksParser : QSParser
+{
+}
+@end
diff --git a/QSThingsPlugin/Parsers/QSThingsTasksParser.m b/QSThingsPlugin/Parsers/QSThingsTasksParser.m
new file mode 100644
index 0000000..b29d011
--- /dev/null
+++ b/QSThingsPlugin/Parsers/QSThingsTasksParser.m
@@ -0,0 +1,43 @@
+#import "Constants.h"
+#import "QSThingsTasksParser.h"
+#import "SQLHelper.h"
+#import <os/log.h>
+
+@implementation QSThingsTasksParser
+- (BOOL)validParserForPath:(NSString *)path {
+ return [[path lastPathComponent] isEqualToString:@"main.sqlite"];
+}
+
+- (NSArray *)objectsFromPath:(NSString *)path withSettings:(NSDictionary *)settings {
+ NSString *query = @"SELECT "
+ "uuid, "
+ "creationDate, "
+ "status, "
+ "title, "
+ "notes, "
+ "area, "
+ "project "
+ "FROM TMTask "
+ "WHERE trashed = 0 "
+ "AND type = 0 "
+ "AND status < 3 "
+ "ORDER BY title;";
+
+ NSArray *results = [SQLHelper executeSql:query onFile:path];
+
+ NSMutableArray *objects = [NSMutableArray array];
+ [results enumerateObjectsUsingBlock:^(NSDictionary *dictionary, NSUInteger i, BOOL *stop) {
+
+ QSObject *newObject = [QSObject
+ makeObjectWithIdentifier:[dictionary objectForKey:@"uuid"]];
+ [newObject setObject:dictionary forType:kTaskType];
+ [newObject setName:[dictionary objectForKey:@"title"]];
+ [newObject setDetails:[dictionary objectForKey:@"notes"]];
+ [newObject setPrimaryType:kTaskType];
+
+ [objects addObject:newObject];
+ }];
+
+ return objects;
+}
+@end
diff --git a/QSThingsPlugin/QSThingsPluginSource.h b/QSThingsPlugin/QSThingsPluginSource.h
new file mode 100644
index 0000000..95daae8
--- /dev/null
+++ b/QSThingsPlugin/QSThingsPluginSource.h
@@ -0,0 +1,10 @@
+#import <Foundation/Foundation.h>
+#import <QSCore/QSCore.h>
+
+@interface QSThingsPluginSource : QSObjectSource
+@end
+
+@interface QSCatalogEntry (OldStyleSourceSupport)
+@property NSMutableDictionary *info;
+- (id)objectForKey:(NSString *)key;
+@end
diff --git a/QSThingsPlugin/QSThingsPluginSource.m b/QSThingsPlugin/QSThingsPluginSource.m
new file mode 100644
index 0000000..3f5f06e
--- /dev/null
+++ b/QSThingsPlugin/QSThingsPluginSource.m
@@ -0,0 +1,59 @@
+#import "QSThingsPluginSource.h"
+#import "Constants.h"
+#import <QSCore/QSCore.h>
+
+@implementation QSThingsPluginSource
+
+#pragma mark - Lifecycle
+
+// This method will get called whenever we change which
+// active entry is selected.
+- (void)setSelectedEntry:(id)selectedEntry {
+}
+
+#pragma mark - Quicksilver Source Methods
+
+- (BOOL)indexIsValidFromDate:(NSDate *)indexDate
+ forEntry:(NSDictionary *)theEntry {
+ return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60;
+}
+
+- (BOOL)isVisibleSource {
+ return YES;
+}
+
+- (NSImage *)iconForEntry:(NSDictionary *)dict {
+ return [[NSBundle bundleForClass:[self class]] imageNamed:@"things"];
+}
+
+- (NSView *)settingsView {
+ if (![super settingsView]) {
+ [[NSBundle bundleForClass:[self class]]
+ loadNibNamed:NSStringFromClass([self class])
+ owner:self
+ topLevelObjects:NULL];
+ }
+ return [super settingsView];
+}
+
+#pragma mark - Objects For Entry
+
+- (NSArray *)objectsForEntry:(QSCatalogEntry *)theEntry {
+ return @[];
+}
+
+#pragma mark - Object Handler Methods
+
+- (void)setQuickIconForObject:(QSObject *)object {
+ // Not yet
+}
+
+- (BOOL)objectHasChildren:(QSObject *)object {
+ return YES;
+}
+
+- (BOOL)loadChildrenForObject:(QSObject *)object {
+ return YES;
+}
+
+@end
diff --git a/QSThingsPlugin/QSThingsPluginSource.xib b/QSThingsPlugin/QSThingsPluginSource.xib
new file mode 100644
index 0000000..ac6254e
--- /dev/null
+++ b/QSThingsPlugin/QSThingsPluginSource.xib
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
+ <dependencies>
+ <deployment identifier="macosx"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="QSThingsPluginSource">
+ <connections>
+ <outlet property="settingsView" destination="settings-view" id="settings-view-outlet"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+ <customView translatesAutoresizingMaskIntoConstraints="NO" id="settings-view">
+ <rect key="frame" x="0.0" y="0.0" width="249" height="252"/>
+ <subviews>
+ <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="site-label">
+ <rect key="frame" x="58" y="118" width="133" height="16"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="16" id="Eoo-3h-Uin"/>
+ </constraints>
+ <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Nothing to configure!" id="site-label-cell">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ </subviews>
+ <constraints>
+ <constraint firstItem="site-label" firstAttribute="centerX" secondItem="settings-view" secondAttribute="centerX" id="D0Z-9a-PUk"/>
+ <constraint firstItem="site-label" firstAttribute="centerY" secondItem="settings-view" secondAttribute="centerY" id="KkB-XD-j1l"/>
+ </constraints>
+ <point key="canvasLocation" x="155.5" y="180"/>
+ </customView>
+ <objectController id="JpK-62-U8F" userLabel="Settings">
+ <declaredKeys>
+ <string>info.username</string>
+ <string>info.site</string>
+ <string>info.includeTags</string>
+ <string>info.host</string>
+ </declaredKeys>
+ <connections>
+ <binding destination="-2" name="contentObject" keyPath="self.selectedEntry" id="r1i-y5-4Mi"/>
+ <outlet property="content" destination="-2" id="UlK-eW-ayD"/>
+ </connections>
+ </objectController>
+ </objects>
+</document>
diff --git a/QSThingsPlugin/Types/Constants.h b/QSThingsPlugin/Types/Constants.h
new file mode 100644
index 0000000..e365a8f
--- /dev/null
+++ b/QSThingsPlugin/Types/Constants.h
@@ -0,0 +1,7 @@
+#pragma mark - Things Object Types
+
+extern NSString *const kAppSectionType;
+extern NSString *const kProjectType;
+extern NSString *const kAreaType;
+extern NSString *const kTagType;
+extern NSString *const kTaskType;
diff --git a/QSThingsPlugin/Types/Constants.m b/QSThingsPlugin/Types/Constants.m
new file mode 100644
index 0000000..4abf9f7
--- /dev/null
+++ b/QSThingsPlugin/Types/Constants.m
@@ -0,0 +1,7 @@
+#pragma mark - Things Object Types
+
+NSString *const kAppSectionType = @"things.app-section";
+NSString *const kProjectType = @"things.project";
+NSString *const kAreaType = @"things.area";
+NSString *const kTagType = @"things.tag";
+NSString *const kTaskType = @"things.task";
diff --git a/QSThingsPlugin/en.lproj/Localizable.strings b/QSThingsPlugin/en.lproj/Localizable.strings
new file mode 100644
index 0000000..2ac3fa6
--- /dev/null
+++ b/QSThingsPlugin/en.lproj/Localizable.strings
@@ -0,0 +1 @@
+"QSThingsPluginSource" = "Things 3";