From 47791df7df104116aa95e0022425952ecc181480 Mon Sep 17 00:00:00 2001 From: bthos Date: Sun, 8 Feb 2026 01:48:57 +0100 Subject: [PATCH 1/5] Update plugin template to match current TimeTracker plugin system - Update Plugin trait to use time-tracker-plugin-sdk - Change entry_point from plugin_init to _plugin_create - Add _plugin_destroy FFI export - Update plugin.toml format to match current implementation - Add Extension API documentation and examples - Update README with current Plugin API information - Update Cargo.toml to use plugin SDK dependency Co-authored-by: Cursor --- Cargo.toml | 12 +- README.md | 465 ++++++++++++++++++++++++++++++++++++-------------- plugin.toml | 6 +- src/lib.rs | 126 ++------------ src/plugin.rs | 164 +++++++++++++----- 5 files changed, 490 insertions(+), 283 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23d519c..60adf97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,17 +12,17 @@ name = "example_plugin_backend" crate-type = ["cdylib"] # Dynamic library for plugin loading [dependencies] +# Plugin SDK - this provides the Plugin trait and API interfaces +# Note: In a real plugin, you would add this as a dependency pointing to the SDK +# For the template, we'll use a placeholder that matches the actual SDK structure +# time-tracker-plugin-sdk = { path = "../plugin-sdk" } # Uncomment and adjust path when using +time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.0", features = ["full"] } - -# Plugin API dependencies (adjust versions based on TimeTracker core) -# These will be provided by the TimeTracker app at runtime -# Include them here for compilation, but they should match the core app versions [dev-dependencies] [profile.release] opt-level = 3 lto = true -codegen-units = 1 +codegen-units = 1 \ No newline at end of file diff --git a/README.md b/README.md index f8ebcba..4be08d6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This template provides a complete foundation for building TimeTracker plugins, i - **React Frontend**: UI components that can be embedded in TimeTracker's interface - **Build System**: GitHub Actions workflow for automated cross-platform builds - **Plugin Manifest**: TOML configuration file defining plugin metadata +- **Extension API**: Extend Core entities (activities, manual_entries, categories) with custom fields ## Quick Start @@ -18,7 +19,7 @@ This template provides a complete foundation for building TimeTracker plugins, i Click "Use this template" on GitHub to create your own plugin repository, or: ```bash -git clone https://github.com/your-org/time-tracker-plugin-template.git my-plugin +git clone https://github.com/bthos/time-tracker-plugin-template.git my-plugin cd my-plugin rm -rf .git git init @@ -38,8 +39,7 @@ description = "Description of your plugin" repository = "https://github.com/your-username/my-plugin" license = "MIT" api_version = "1.0" -min_core_version = "0.3.0" -max_core_version = "1.0.0" +min_core_version = "0.2.7" # Minimum required TimeTracker version ``` ### 3. Update Cargo.toml @@ -54,27 +54,91 @@ authors = ["Your Name "] description = "Description of your plugin" ``` -### 4. Implement Your Plugin +**Important**: Add the plugin SDK dependency. You can either: -Edit `src/plugin.rs` to implement your plugin logic: +1. Use the SDK from the main repository (recommended for templates): +```toml +time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk" } +``` + +2. Or use a local path if developing alongside the main app: +```toml +time-tracker-plugin-sdk = { path = "../time-tracker-app/plugin-sdk" } +``` + +### 4. Update plugin.toml Backend Section + +Make sure the backend section matches your Cargo.toml: + +```toml +[backend] +crate_name = "my-plugin" # Must match Cargo.toml [package].name +library_name = "my_plugin_backend" # Must match Cargo.toml [lib].name +entry_point = "_plugin_create" # Must match exported function in lib.rs +``` + +### 5. Implement Your Plugin + +Edit `src/plugin.rs` to implement your plugin logic. The plugin must implement the `Plugin` trait from `time-tracker-plugin-sdk`: ```rust +use time_tracker_plugin_sdk::{Plugin, PluginInfo, PluginAPIInterface}; + +pub struct MyPlugin { + info: PluginInfo, +} + impl Plugin for MyPlugin { - fn on_init(&mut self, api: &PluginAPI) -> Result<()> { - // Initialize your plugin + fn info(&self) -> &PluginInfo { + &self.info + } + + fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> { + // Register schema extensions, model extensions, etc. Ok(()) } - fn on_activity_recorded(&mut self, activity: &Activity) -> Result<()> { - // React to new activities + fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result { + // Handle plugin commands + Ok(serde_json::json!({})) + } + + fn shutdown(&self) -> Result<(), String> { + // Clean up resources Ok(()) } - // ... implement other trait methods + fn get_schema_extensions(&self) -> Vec { + vec![] + } + + fn get_frontend_bundle(&self) -> Option> { + None + } } ``` -### 5. Build Your Plugin +### 6. Export FFI Functions + +In `src/lib.rs`, export the required FFI functions: + +```rust +use time_tracker_plugin_sdk::Plugin; + +#[no_mangle] +pub extern "C" fn _plugin_create() -> *mut dyn Plugin { + Box::into_raw(Box::new(plugin::MyPlugin::new())) +} + +#[no_mangle] +pub extern "C" fn _plugin_destroy(plugin: *mut dyn Plugin) { + unsafe { + let _ = Box::from_raw(plugin); + } +} +``` + +### 7. Build Your Plugin #### Local Development @@ -82,7 +146,7 @@ impl Plugin for MyPlugin { # Build Rust backend cargo build --release -# Build frontend +# Build frontend (if you have frontend components) cd frontend npm install npm run build @@ -104,7 +168,7 @@ time-tracker-plugin-template/ │ └── workflows/ │ └── build.yml # CI/CD workflow for building plugins ├── src/ -│ ├── lib.rs # Plugin trait and API definitions +│ ├── lib.rs # FFI exports (_plugin_create, _plugin_destroy) │ └── plugin.rs # Your plugin implementation ├── frontend/ │ ├── src/ @@ -113,7 +177,7 @@ time-tracker-plugin-template/ │ ├── vite.config.ts │ └── tsconfig.json ├── migrations/ -│ └── 001_initial.sql # Database migrations +│ └── 001_initial.sql # Database migrations (optional) ├── plugin.toml # Plugin manifest ├── Cargo.toml # Rust dependencies ├── .gitignore @@ -127,23 +191,22 @@ The `plugin.toml` file defines your plugin's metadata and configuration: ### [plugin] Section - `name`: Unique identifier for your plugin (used in URLs and internal references) -- `display_name`: Human-readable name shown in the UI +- `display_name`: Human-readable name shown in the UI (optional) - `version`: Semantic version (e.g., "1.0.0") - `author`: Plugin author name - `description`: Brief description of what your plugin does - `repository`: GitHub repository URL - `license`: License identifier (MIT, Apache-2.0, etc.) -- `api_version`: Plugin API version your plugin targets -- `min_core_version`: Minimum TimeTracker version required -- `max_core_version`: Maximum TimeTracker version supported +- `api_version`: Plugin API version your plugin targets (currently "1.0") +- `min_core_version`: Minimum TimeTracker version required (e.g., "0.2.7") ### [backend] Section -- `crate_name`: Rust crate name (must match Cargo.toml) -- `library_name`: Name of the compiled library (without lib prefix) -- `entry_point`: Function name exported from lib.rs (usually `plugin_init`) +- `crate_name`: Rust crate name (must match `Cargo.toml` `[package].name`) +- `library_name`: Name of the compiled library (must match `Cargo.toml` `[lib].name`) +- `entry_point`: Function name exported from lib.rs (must be `_plugin_create`) -### [frontend] Section +### [frontend] Section (Optional) - `entry`: Path to compiled JavaScript bundle - `components`: List of React component names to export @@ -156,41 +219,148 @@ The `plugin.toml` file defines your plugin's metadata and configuration: ### Plugin Trait -All plugins must implement the `Plugin` trait: +All plugins must implement the `Plugin` trait from `time-tracker-plugin-sdk`: ```rust pub trait Plugin: Send + Sync { - fn name(&self) -> &str; - fn version(&self) -> &str; - fn on_init(&mut self, api: &PluginAPI) -> Result<()>; - fn on_start(&mut self) -> Result<()>; - fn on_stop(&mut self) -> Result<()>; - fn on_activity_recorded(&mut self, activity: &Activity) -> Result<()>; - fn on_category_created(&mut self, category: &Category) -> Result<()>; - fn migrations(&self) -> Vec; - fn commands(&self) -> Vec; + /// Get plugin metadata + fn info(&self) -> &PluginInfo; + + /// Initialize the plugin + fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String>; + + /// Invoke a command on the plugin + fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result; + + /// Shutdown the plugin + fn shutdown(&self) -> Result<(), String>; + + /// Get schema extensions that this plugin requires + fn get_schema_extensions(&self) -> Vec; + + /// Get frontend bundle bytes (if plugin provides UI) + fn get_frontend_bundle(&self) -> Option>; } ``` -### Plugin API +### Plugin API Interface -The `PluginAPI` provides access to TimeTracker functionality: +The `PluginAPIInterface` provides access to TimeTracker functionality: ```rust -impl PluginAPI { - pub fn get_activities(&self, start: i64, end: i64) -> Result>; - pub fn create_activity(&self, activity: Activity) -> Result; - pub fn subscribe(&self, event: EventType, callback: Box) -> SubscriptionId; +pub trait PluginAPIInterface: Send + Sync { + /// Register a database schema extension + fn register_schema_extension( + &self, + entity_type: EntityType, + schema_changes: Vec, + ) -> Result<(), String>; + + /// Register a model extension + fn register_model_extension( + &self, + entity_type: EntityType, + model_fields: Vec, + ) -> Result<(), String>; + + /// Register query filters + fn register_query_filters( + &self, + entity_type: EntityType, + query_filters: Vec, + ) -> Result<(), String>; + + /// Call a database method by name with JSON parameters + fn call_db_method(&self, method: &str, params: serde_json::Value) -> Result; } ``` -### Lifecycle Hooks +### Extension API + +Plugins can extend Core entities (activities, manual_entries, categories) using the Extension API: + +#### 1. Database Schema Extensions + +Add columns to Core tables or create new tables: + +```rust +api.register_schema_extension( + EntityType::Activity, + vec![ + // Create a new table + SchemaChange::CreateTable { + table: "my_plugin_data".to_string(), + columns: vec![ + TableColumn { + name: "id".to_string(), + column_type: "INTEGER".to_string(), + primary_key: true, + nullable: false, + default: None, + foreign_key: None, + }, + // ... more columns + ], + }, + // Add a column to activities table + SchemaChange::AddColumn { + table: "activities".to_string(), + column: "custom_field".to_string(), + column_type: "TEXT".to_string(), + default: None, + foreign_key: None, + }, + // Add an index + SchemaChange::AddIndex { + table: "activities".to_string(), + index: "idx_activities_custom_field".to_string(), + columns: vec!["custom_field".to_string()], + }, + ], +)?; +``` + +#### 2. Model Extensions -- **`on_init`**: Called when the plugin is first loaded. Use this to set up your plugin. -- **`on_start`**: Called when the plugin is enabled/started. -- **`on_stop`**: Called when the plugin is disabled/stopped. -- **`on_activity_recorded`**: Called whenever a new activity is recorded. -- **`on_category_created`**: Called when a new category is created. +Add fields to Core data structures: + +```rust +api.register_model_extension( + EntityType::Activity, + vec![ + ModelField { + name: "custom_field".to_string(), + type_: "Option".to_string(), + optional: true, + }, + ], +)?; +``` + +#### 3. Query Filters + +Add custom query filters for activities: + +```rust +api.register_query_filters( + EntityType::Activity, + vec![ + QueryFilter { + name: "by_custom_field".to_string(), + filter_fn: Box::new(|activities, params| { + // Filter logic + Ok(activities) + }), + }, + ], +)?; +``` + +### Lifecycle + +- **`initialize`**: Called when the plugin is first loaded. Use this to register extensions and set up your plugin. +- **`invoke_command`**: Called when a command is invoked on your plugin. Handle your plugin's commands here. +- **`shutdown`**: Called when the plugin is unloaded. Clean up any resources here. ## Frontend Components @@ -213,43 +383,27 @@ List exported components in `plugin.toml`: components = ["MySettings"] ``` -## Database Migrations - -Create SQL migration files in the `migrations/` directory: +## Database Operations -``` -migrations/ -├── 001_initial.sql -├── 002_add_user_preferences.sql -└── 003_add_analytics_table.sql -``` - -Migrations are executed automatically when the plugin is installed or updated. - -Example migration: - -```sql -CREATE TABLE IF NOT EXISTS my_plugin_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key TEXT NOT NULL UNIQUE, - value TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -); -``` - -Return migrations from your plugin: +Plugins can interact with the database through the `call_db_method` API: ```rust -fn migrations(&self) -> Vec { - vec![ - Migration { - version: 1, - sql: include_str!("../migrations/001_initial.sql").to_string(), - }, - ] +fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result { + match command { + "get_my_data" => { + // Call a database method + api.call_db_method("get_my_data", params) + } + "create_my_data" => { + api.call_db_method("create_my_data", params) + } + _ => Err(format!("Unknown command: {}", command)), + } } ``` +**Note**: Database methods must be implemented in the core app's `database.rs`. For custom tables created by your plugin, you'll need to add corresponding methods to the core app or use raw SQL through the API. + ## Building and Distribution ### Manual Build @@ -295,7 +449,7 @@ cargo build --release --target x86_64-unknown-linux-gnu - Windows: `%APPDATA%\timetracker\plugins\your-plugin-name\` - macOS: `~/Library/Application Support/timetracker/plugins/your-plugin-name/` - Linux: `~/.local/share/timetracker/plugins/your-plugin-name/` -3. Copy `plugin.toml` and frontend build to the same directory +3. Copy `plugin.toml` and frontend build (if any) to the same directory 4. Restart TimeTracker 5. Enable your plugin in Settings → Plugins @@ -312,63 +466,84 @@ time-tracker-app.exe RUST_LOG=debug ./time-tracker-app ``` -## Best Practices - -### Versioning - -- Use [Semantic Versioning](https://semver.org/) -- Increment major version for breaking API changes -- Increment minor version for new features -- Increment patch version for bug fixes - -### Error Handling +## Examples -Always return proper errors from trait methods: +### Example: Extending Activities with Custom Fields ```rust -fn on_init(&mut self, api: &PluginAPI) -> Result<()> { - // Handle errors gracefully - api.get_activities(0, 0)?; +fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> { + // Add a custom field to activities + api.register_schema_extension( + EntityType::Activity, + vec![ + SchemaChange::AddColumn { + table: "activities".to_string(), + column: "priority".to_string(), + column_type: "INTEGER".to_string(), + default: Some("0".to_string()), + foreign_key: None, + }, + ], + )?; + + api.register_model_extension( + EntityType::Activity, + vec![ + ModelField { + name: "priority".to_string(), + type_: "Option".to_string(), + optional: true, + }, + ], + )?; + Ok(()) } ``` -### Resource Management - -- Clean up resources in `on_stop` -- Unsubscribe from events when stopping -- Close database connections if opened - -### Security - -- Validate all user input -- Don't expose sensitive data in frontend components -- Use parameterized queries for database operations - -## Examples - -### Example: Activity Logger Plugin +### Example: Creating a Custom Table ```rust -pub struct ActivityLogger { - log_file: PathBuf, -} - -impl Plugin for ActivityLogger { - fn on_activity_recorded(&mut self, activity: &Activity) -> Result<()> { - let log_entry = format!( - "{}: {} - {}\n", - activity.started_at, - activity.app_name, - activity.window_title - ); - std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&self.log_file)? - .write_all(log_entry.as_bytes())?; - Ok(()) - } +fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> { + api.register_schema_extension( + EntityType::Activity, + vec![ + SchemaChange::CreateTable { + table: "notes".to_string(), + columns: vec![ + TableColumn { + name: "id".to_string(), + column_type: "INTEGER".to_string(), + primary_key: true, + nullable: false, + default: None, + foreign_key: None, + }, + TableColumn { + name: "activity_id".to_string(), + column_type: "INTEGER".to_string(), + primary_key: false, + nullable: false, + default: None, + foreign_key: Some(ForeignKey { + table: "activities".to_string(), + column: "id".to_string(), + }), + }, + TableColumn { + name: "content".to_string(), + column_type: "TEXT".to_string(), + primary_key: false, + nullable: false, + default: None, + foreign_key: None, + }, + ], + }, + ], + )?; + + Ok(()) } ``` @@ -394,6 +569,46 @@ export const MyPluginSettings: React.FC = () => { }; ``` +## Best Practices + +### Versioning + +- Use [Semantic Versioning](https://semver.org/) +- Increment major version for breaking API changes +- Increment minor version for new features +- Increment patch version for bug fixes + +### Error Handling + +Always return proper errors from trait methods: + +```rust +fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> { + api.register_schema_extension(...) + .map_err(|e| format!("Failed to register schema: {}", e))?; + Ok(()) +} +``` + +### Resource Management + +- Clean up resources in `shutdown` +- Don't hold references to API after shutdown +- Use proper error handling for all operations + +### Security + +- Validate all user input +- Don't expose sensitive data in frontend components +- Use parameterized queries for database operations (handled by core app) + +### Extension API Best Practices + +- Always register schema extensions before model extensions +- Use foreign keys for referential integrity +- Add indexes for frequently queried columns +- Keep extension fields optional when possible for compatibility + ## Contributing When contributing to this template: @@ -419,4 +634,4 @@ This template is licensed under the MIT License. See LICENSE file for details. For issues and questions: - Open an issue in this repository for template-related questions -- Open an issue in [TimeTracker](https://github.com/bthos/time-tracker-app) for plugin API questions +- Open an issue in [TimeTracker](https://github.com/bthos/time-tracker-app) for plugin API questions \ No newline at end of file diff --git a/plugin.toml b/plugin.toml index 7b1bf90..2b996fd 100644 --- a/plugin.toml +++ b/plugin.toml @@ -11,9 +11,9 @@ min_core_version = "0.3.0" # Minimum required TimeTracker version max_core_version = "1.0.0" # Maximum supported TimeTracker version [backend] -crate_name = "example_plugin" # Rust crate name -library_name = "example_plugin_backend" # Library name (without lib prefix) -entry_point = "plugin_init" # Entry point function name +crate_name = "example-plugin" # Rust crate name (must match Cargo.toml package.name) +library_name = "example_plugin_backend" # Library name (without lib prefix, must match Cargo.toml [lib].name) +entry_point = "_plugin_create" # Entry point function name (must match exported function in lib.rs) [frontend] entry = "frontend/dist/index.js" # Path to compiled JavaScript bundle diff --git a/src/lib.rs b/src/lib.rs index bf6c45c..280bd5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,125 +6,31 @@ //! 1. Update plugin.toml with your plugin information //! 2. Rename the crate in Cargo.toml //! 3. Implement the Plugin trait in src/plugin.rs -//! 4. Export the plugin_init function from this file +//! 4. Export the _plugin_create and _plugin_destroy functions from this file -use std::sync::Arc; - -// Plugin API types (these should match TimeTracker core) -// In a real implementation, these would be provided by a plugin-sdk crate -pub type Result = std::result::Result>; - -/// Plugin trait that all TimeTracker plugins must implement -pub trait Plugin: Send + Sync { - /// Returns the plugin name - fn name(&self) -> &str; - - /// Returns the plugin version - fn version(&self) -> &str; - - /// Called when the plugin is initialized - /// Provides access to the PluginAPI for interacting with TimeTracker - fn on_init(&mut self, api: &PluginAPI) -> Result<()>; - - /// Called when the plugin is started - fn on_start(&mut self) -> Result<()>; - - /// Called when the plugin is stopped - fn on_stop(&mut self) -> Result<()>; - - /// Called when a new activity is recorded - fn on_activity_recorded(&mut self, activity: &Activity) -> Result<()>; - - /// Called when a category is created - fn on_category_created(&mut self, category: &Category) -> Result<()>; - - /// Returns database migrations for this plugin - fn migrations(&self) -> Vec; - - /// Returns Tauri commands exposed by this plugin - fn commands(&self) -> Vec; -} - -/// Plugin API for accessing TimeTracker functionality -pub struct PluginAPI { - // Database connection (provided by TimeTracker core) - // pub db: Arc, - - // Event emitter for subscribing to events - // pub events: EventEmitter, -} - -impl PluginAPI { - /// Get activities within a time range - pub fn get_activities(&self, _start: i64, _end: i64) -> Result> { - // Implementation provided by TimeTracker core - Ok(vec![]) - } - - /// Create a new activity - pub fn create_activity(&self, _activity: Activity) -> Result { - // Implementation provided by TimeTracker core - Ok(0) - } - - /// Subscribe to events - pub fn subscribe(&self, _event: EventType, _callback: Box) -> SubscriptionId { - // Implementation provided by TimeTracker core - SubscriptionId(0) - } -} - -/// Activity record -#[derive(Debug, Clone)] -pub struct Activity { - pub id: Option, - pub app_name: String, - pub window_title: String, - pub started_at: i64, - pub ended_at: Option, - pub category_id: Option, -} - -/// Category record -#[derive(Debug, Clone)] -pub struct Category { - pub id: Option, - pub name: String, - pub color: String, -} - -/// Database migration -pub struct Migration { - pub version: i32, - pub sql: String, -} - -/// Event types -pub enum EventType { - ActivityRecorded, - CategoryCreated, - ProjectCreated, -} - -/// Event -pub struct Event { - pub event_type: EventType, - pub data: serde_json::Value, -} - -/// Subscription ID -#[derive(Debug, Clone, Copy)] -pub struct SubscriptionId(pub i64); +use time_tracker_plugin_sdk::{Plugin, PluginInfo}; // Import the plugin implementation mod plugin; -/// Plugin initialization function +/// Plugin creation function /// This function is called by TimeTracker when loading the plugin /// /// # Safety /// This function must be exported with the exact name specified in plugin.toml #[no_mangle] -pub extern "C" fn plugin_init() -> *mut dyn Plugin { +pub extern "C" fn _plugin_create() -> *mut dyn Plugin { Box::into_raw(Box::new(plugin::ExamplePlugin::new())) } + +/// Plugin destruction function +/// This function is called by TimeTracker when unloading the plugin +/// +/// # Safety +/// This function must be exported with the exact name specified in plugin.toml +#[no_mangle] +pub extern "C" fn _plugin_destroy(plugin: *mut dyn Plugin) { + unsafe { + let _ = Box::from_raw(plugin); + } +} \ No newline at end of file diff --git a/src/plugin.rs b/src/plugin.rs index 5bf4e6e..9934c4e 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -3,70 +3,156 @@ //! This is a simple example plugin that demonstrates the Plugin trait. //! Replace this with your own plugin logic. -use super::*; +use time_tracker_plugin_sdk::{ + Plugin, + PluginInfo, + PluginAPIInterface, + EntityType, + SchemaChange, + ModelField, + ForeignKey, + SchemaExtension, +}; +use serde_json; /// Example plugin implementation pub struct ExamplePlugin { - name: String, - version: String, - initialized: bool, + info: PluginInfo, } impl ExamplePlugin { /// Create a new instance of the example plugin pub fn new() -> Self { Self { - name: "example-plugin".to_string(), - version: "1.0.0".to_string(), - initialized: false, + info: PluginInfo { + id: "example-plugin".to_string(), + name: "Example Plugin".to_string(), + version: "1.0.0".to_string(), + description: Some("An example plugin template for TimeTracker".to_string()), + is_builtin: false, + }, } } } impl Plugin for ExamplePlugin { - fn name(&self) -> &str { - &self.name + fn info(&self) -> &PluginInfo { + &self.info } - fn version(&self) -> &str { - &self.version - } - - fn on_init(&mut self, _api: &PluginAPI) -> Result<()> { - self.initialized = true; - println!("ExamplePlugin: Initialized"); - Ok(()) - } - - fn on_start(&mut self) -> Result<()> { - println!("ExamplePlugin: Started"); - Ok(()) - } - - fn on_stop(&mut self) -> Result<()> { - println!("ExamplePlugin: Stopped"); + fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> { + // Register schema extensions if your plugin needs to create tables + // or extend Core entities (activities, manual_entries, categories) + + // Example: Create a custom table for your plugin + api.register_schema_extension( + EntityType::Activity, // This doesn't affect the entity, just groups the changes + vec![ + SchemaChange::CreateTable { + table: "example_plugin_data".to_string(), + columns: vec![ + time_tracker_plugin_sdk::TableColumn { + name: "id".to_string(), + column_type: "INTEGER".to_string(), + primary_key: true, + nullable: false, + default: None, + foreign_key: None, + }, + time_tracker_plugin_sdk::TableColumn { + name: "key".to_string(), + column_type: "TEXT".to_string(), + primary_key: false, + nullable: false, + default: None, + foreign_key: None, + }, + time_tracker_plugin_sdk::TableColumn { + name: "value".to_string(), + column_type: "TEXT".to_string(), + primary_key: false, + nullable: true, + default: None, + foreign_key: None, + }, + time_tracker_plugin_sdk::TableColumn { + name: "created_at".to_string(), + column_type: "INTEGER".to_string(), + primary_key: false, + nullable: false, + default: None, + foreign_key: None, + }, + ], + }, + SchemaChange::AddIndex { + table: "example_plugin_data".to_string(), + index: "idx_example_plugin_data_key".to_string(), + columns: vec!["key".to_string()], + }, + ], + )?; + + // Example: Extend activities with a custom field + // Uncomment and modify if you need to extend Core entities: + /* + api.register_schema_extension( + EntityType::Activity, + vec![ + SchemaChange::AddColumn { + table: "activities".to_string(), + column: "custom_field".to_string(), + column_type: "TEXT".to_string(), + default: None, + foreign_key: None, + }, + ], + )?; + + api.register_model_extension( + EntityType::Activity, + vec![ + ModelField { + name: "custom_field".to_string(), + type_: "Option".to_string(), + optional: true, + }, + ], + )?; + */ + Ok(()) } - fn on_activity_recorded(&mut self, activity: &Activity) -> Result<()> { - println!("ExamplePlugin: Activity recorded - {} - {}", activity.app_name, activity.window_title); - Ok(()) + fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result { + match command { + "get_example_data" => { + // Example: Call a database method + // Note: You need to implement these methods in the core app's database.rs + // For now, this is just an example of how to structure commands + api.call_db_method("get_example_data", params) + } + "set_example_data" => { + api.call_db_method("set_example_data", params) + } + _ => Err(format!("Unknown command: {}", command)), + } } - fn on_category_created(&mut self, category: &Category) -> Result<()> { - println!("ExamplePlugin: Category created - {}", category.name); + fn shutdown(&self) -> Result<(), String> { + // Clean up any resources here Ok(()) } - fn migrations(&self) -> Vec { - // Return any database migrations your plugin needs - // See migrations/001_initial.sql for an example + fn get_schema_extensions(&self) -> Vec { + // Schema extensions are registered in initialize() + // This method can return pre-defined extensions if needed vec![] } - fn commands(&self) -> Vec { - // Return list of Tauri command names exposed by this plugin - // These commands will be registered with Tauri - vec![] + fn get_frontend_bundle(&self) -> Option> { + // Return frontend bundle bytes if your plugin provides UI + // The bundle should be loaded from the compiled frontend build + None } -} +} \ No newline at end of file From 17821ef36c0a5835eb7508d96ecce96c0ae4afcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:00:14 +0000 Subject: [PATCH 2/5] Initial plan From 1f6035a41567a5bf9497ddd473747906007f2fef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:02:36 +0000 Subject: [PATCH 3/5] Remove unused imports and add null pointer check Co-authored-by: bthos <13679349+bthos@users.noreply.github.com> --- src/lib.rs | 5 ++++- src/plugin.rs | 12 +++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 280bd5e..314fe71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ //! 3. Implement the Plugin trait in src/plugin.rs //! 4. Export the _plugin_create and _plugin_destroy functions from this file -use time_tracker_plugin_sdk::{Plugin, PluginInfo}; +use time_tracker_plugin_sdk::Plugin; // Import the plugin implementation mod plugin; @@ -30,6 +30,9 @@ pub extern "C" fn _plugin_create() -> *mut dyn Plugin { /// This function must be exported with the exact name specified in plugin.toml #[no_mangle] pub extern "C" fn _plugin_destroy(plugin: *mut dyn Plugin) { + if plugin.is_null() { + return; + } unsafe { let _ = Box::from_raw(plugin); } diff --git a/src/plugin.rs b/src/plugin.rs index 9934c4e..48709d3 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -9,9 +9,7 @@ use time_tracker_plugin_sdk::{ PluginAPIInterface, EntityType, SchemaChange, - ModelField, - ForeignKey, - SchemaExtension, + extensions::{SchemaExtension, TableColumn}, }; use serde_json; @@ -51,7 +49,7 @@ impl Plugin for ExamplePlugin { SchemaChange::CreateTable { table: "example_plugin_data".to_string(), columns: vec![ - time_tracker_plugin_sdk::TableColumn { + TableColumn { name: "id".to_string(), column_type: "INTEGER".to_string(), primary_key: true, @@ -59,7 +57,7 @@ impl Plugin for ExamplePlugin { default: None, foreign_key: None, }, - time_tracker_plugin_sdk::TableColumn { + TableColumn { name: "key".to_string(), column_type: "TEXT".to_string(), primary_key: false, @@ -67,7 +65,7 @@ impl Plugin for ExamplePlugin { default: None, foreign_key: None, }, - time_tracker_plugin_sdk::TableColumn { + TableColumn { name: "value".to_string(), column_type: "TEXT".to_string(), primary_key: false, @@ -75,7 +73,7 @@ impl Plugin for ExamplePlugin { default: None, foreign_key: None, }, - time_tracker_plugin_sdk::TableColumn { + TableColumn { name: "created_at".to_string(), column_type: "INTEGER".to_string(), primary_key: false, From 0f48dccf155efe9b8cafa7290a52ce1a14013697 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:03:49 +0000 Subject: [PATCH 4/5] Pin SDK dependency and align version numbers Co-authored-by: bthos <13679349+bthos@users.noreply.github.com> --- Cargo.toml | 2 +- README.md | 8 +++++--- plugin.toml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60adf97..21b2891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ crate-type = ["cdylib"] # Dynamic library for plugin loading # Note: In a real plugin, you would add this as a dependency pointing to the SDK # For the template, we'll use a placeholder that matches the actual SDK structure # time-tracker-plugin-sdk = { path = "../plugin-sdk" } # Uncomment and adjust path when using -time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk" } +time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk", rev = "003d4143c50283016206b893ce57e2cf547355d0" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/README.md b/README.md index 4be08d6..8849c0f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ description = "Description of your plugin" repository = "https://github.com/your-username/my-plugin" license = "MIT" api_version = "1.0" -min_core_version = "0.2.7" # Minimum required TimeTracker version +min_core_version = "0.2.8" # Minimum required TimeTracker version ``` ### 3. Update Cargo.toml @@ -56,9 +56,9 @@ description = "Description of your plugin" **Important**: Add the plugin SDK dependency. You can either: -1. Use the SDK from the main repository (recommended for templates): +1. Use the SDK from the main repository pinned to a specific version (recommended for production plugins): ```toml -time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk" } +time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk", rev = "003d4143" } ``` 2. Or use a local path if developing alongside the main app: @@ -66,6 +66,8 @@ time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", p time-tracker-plugin-sdk = { path = "../time-tracker-app/plugin-sdk" } ``` +**Note**: Always pin the SDK to a specific `rev` (commit hash) in production to ensure reproducible builds and avoid breaking changes from upstream updates. + ### 4. Update plugin.toml Backend Section Make sure the backend section matches your Cargo.toml: diff --git a/plugin.toml b/plugin.toml index 2b996fd..c1c09f9 100644 --- a/plugin.toml +++ b/plugin.toml @@ -7,7 +7,7 @@ description = "An example plugin template for TimeTracker" repository = "https://github.com/your-username/your-plugin-name" license = "MIT" api_version = "1.0" # Plugin API version -min_core_version = "0.3.0" # Minimum required TimeTracker version +min_core_version = "0.2.8" # Minimum required TimeTracker version max_core_version = "1.0.0" # Maximum supported TimeTracker version [backend] From fe871461a744319bc891c9e8e4c117788eede274 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:04:24 +0000 Subject: [PATCH 5/5] Use full commit hash in README for consistency Co-authored-by: bthos <13679349+bthos@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8849c0f..ff96bb1 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ description = "Description of your plugin" 1. Use the SDK from the main repository pinned to a specific version (recommended for production plugins): ```toml -time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk", rev = "003d4143" } +time-tracker-plugin-sdk = { git = "https://github.com/bthos/time-tracker-app", package = "time-tracker-plugin-sdk", rev = "003d4143c50283016206b893ce57e2cf547355d0" } ``` 2. Or use a local path if developing alongside the main app: