Plugin Development
Plugin Examples
Real-world plugin implementations and use cases
Plugin Examples
Learn from real examples
These production-ready plugin implementations demonstrate best practices and common patterns.
Calculator Plugin
A simple calculator plugin that evaluates mathematical expressions:
import {
Plugin,
PluginContext,
PluginResult,
PluginResultType,
} from "@/types/plugin";
import { evaluate } from "mathjs";
class CalculatorPlugin implements Plugin {
id = "calculator";
name = "Calculator";
description = "Evaluate mathematical expressions";
enabled = true;
canHandle(context: PluginContext): boolean {
// Check if query looks like a math expression
const mathRegex = /^[\d\s+\-*/().^%]+$/;
return mathRegex.test(context.query.trim());
}
async match(context: PluginContext): Promise<PluginResult[]> {
try {
const result = evaluate(context.query);
return [
{
id: "1",
title: String(result),
subtitle: context.query,
icon: "🔢",
type: PluginResultType.Calculator,
action: () => {
navigator.clipboard.writeText(String(result));
},
},
];
} catch (error) {
return [];
}
}
async execute(result: PluginResult): Promise<void> {
if (result.action) {
await result.action();
}
}
}
export default CalculatorPlugin;Web Search Plugin
Search the web directly from Volt:
import {
Plugin,
PluginContext,
PluginResult,
PluginResultType,
} from "@/types/plugin";
class WebSearchPlugin implements Plugin {
id = "web-search";
name = "Web Search";
description = "Search the web with various search engines";
enabled = true;
private searchEngines = {
google: "https://google.com/search?q=",
bing: "https://bing.com/search?q=",
duckduckgo: "https://duckduckgo.com/?q=",
};
canHandle(context: PluginContext): boolean {
return context.query.startsWith("search ") || context.query.startsWith("?");
}
async match(context: PluginContext): Promise<PluginResult[]> {
const query = context.query.replace(/^(search |\?)/, "").trim();
if (!query) return [];
return Object.entries(this.searchEngines).map(([engine, url], index) => ({
id: String(index),
title: `Search "${query}" on ${engine}`,
subtitle: `Open ${engine} in browser`,
icon: "🔍",
type: PluginResultType.WebSearch,
metadata: { url: url + encodeURIComponent(query) },
action: () => {
window.open(url + encodeURIComponent(query), "_blank");
},
}));
}
async execute(result: PluginResult): Promise<void> {
if (result.action) {
await result.action();
}
}
}
export default WebSearchPlugin;System Monitor Plugin
Display system information:
import {
Plugin,
PluginContext,
PluginResult,
PluginResultType,
} from "@/types/plugin";
import { invoke } from "@tauri-apps/api/core";
class SystemMonitorPlugin implements Plugin {
id = "system-monitor";
name = "System Monitor";
description = "Display system information";
enabled = true;
canHandle(context: PluginContext): boolean {
const triggers = ["system", "cpu", "memory", "disk"];
return triggers.some((trigger) =>
context.query.toLowerCase().includes(trigger)
);
}
async match(context: PluginContext): Promise<PluginResult[]> {
try {
const systemInfo = await invoke("get_system_info");
return [
{
id: "cpu",
title: `CPU: ${systemInfo.cpu_usage}%`,
subtitle: `${systemInfo.cpu_name}`,
icon: "🖥️",
type: PluginResultType.SystemMonitor,
},
{
id: "memory",
title: `Memory: ${systemInfo.memory_used}GB / ${systemInfo.memory_total}GB`,
subtitle: `${systemInfo.memory_percent}% used`,
icon: "💾",
type: PluginResultType.SystemMonitor,
},
{
id: "disk",
title: `Disk: ${systemInfo.disk_used}GB / ${systemInfo.disk_total}GB`,
subtitle: `${systemInfo.disk_percent}% used`,
icon: "💿",
type: PluginResultType.SystemMonitor,
},
];
} catch (error) {
console.error("Failed to get system info:", error);
return [];
}
}
async execute(result: PluginResult): Promise<void> {
// System monitor results are read-only
}
}
export default SystemMonitorPlugin;Timer Plugin
Create timers and countdowns:
import {
Plugin,
PluginContext,
PluginResult,
PluginResultType,
} from "@/types/plugin";
class TimerPlugin implements Plugin {
id = "timer";
name = "Timer";
description = "Create timers and countdowns";
enabled = true;
canHandle(context: PluginContext): boolean {
return /^timer\s+\d+[smh]?$/i.test(context.query);
}
async match(context: PluginContext): Promise<PluginResult[]> {
const match = context.query.match(/^timer\s+(\d+)([smh])?$/i);
if (!match) return [];
const [, amount, unit = "s"] = match;
const seconds = this.convertToSeconds(parseInt(amount), unit);
return [
{
id: "1",
title: `Set ${amount}${unit} timer`,
subtitle: `Timer for ${this.formatDuration(seconds)}`,
icon: "⏱️",
type: PluginResultType.Timer,
metadata: { seconds },
action: () => {
this.startTimer(seconds);
},
},
];
}
async execute(result: PluginResult): Promise<void> {
if (result.action) {
await result.action();
}
}
private convertToSeconds(amount: number, unit: string): number {
switch (unit.toLowerCase()) {
case "m":
return amount * 60;
case "h":
return amount * 3600;
default:
return amount;
}
}
private formatDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
}
private startTimer(seconds: number): void {
const endTime = Date.now() + seconds * 1000;
const interval = setInterval(() => {
const remaining = Math.max(0, Math.floor((endTime - Date.now()) / 1000));
if (remaining === 0) {
clearInterval(interval);
new Notification("Timer Complete!", {
body: "Your timer has finished",
});
}
}, 1000);
}
}
export default TimerPlugin;Clipboard History Plugin
Access clipboard history:
import {
Plugin,
PluginContext,
PluginResult,
PluginResultType,
} from "@/types/plugin";
import { invoke } from "@tauri-apps/api/core";
class ClipboardPlugin implements Plugin {
id = "clipboard";
name = "Clipboard History";
description = "Access clipboard history";
enabled = true;
canHandle(context: PluginContext): boolean {
return context.query.toLowerCase().startsWith("clip");
}
async match(context: PluginContext): Promise<PluginResult[]> {
try {
const history = await invoke<string[]>("get_clipboard_history");
return history.slice(0, 10).map((item, index) => ({
id: String(index),
title: item.substring(0, 50),
subtitle: `${item.length} characters`,
icon: "📋",
type: PluginResultType.Clipboard,
metadata: { content: item },
action: () => {
navigator.clipboard.writeText(item);
},
}));
} catch (error) {
console.error("Failed to get clipboard history:", error);
return [];
}
}
async execute(result: PluginResult): Promise<void> {
if (result.action) {
await result.action();
}
}
}
export default ClipboardPlugin;Backend Plugin Example (Rust)
Steam game scanner plugin:
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
pub struct GameInfo {
pub id: String,
pub name: String,
pub path: PathBuf,
pub icon: Option<String>,
}
pub struct SteamPlugin {
steam_path: PathBuf,
}
impl SteamPlugin {
pub fn new() -> Self {
let steam_path = Self::find_steam_path();
Self { steam_path }
}
fn find_steam_path() -> PathBuf {
#[cfg(target_os = "windows")]
{
PathBuf::from("C:\\Program Files (x86)\\Steam")
}
#[cfg(target_os = "macos")]
{
PathBuf::from("/Users/Shared/Steam")
}
#[cfg(target_os = "linux")]
{
PathBuf::from("~/.steam/steam")
}
}
pub fn scan_games(&self) -> Vec<GameInfo> {
let library_path = self.steam_path.join("steamapps");
let mut games = Vec::new();
if let Ok(entries) = std::fs::read_dir(&library_path) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.starts_with("appmanifest_") && name.ends_with(".acf") {
if let Ok(game_info) = self.parse_manifest(&entry.path()) {
games.push(game_info);
}
}
}
}
}
games
}
fn parse_manifest(&self, path: &PathBuf) -> Result<GameInfo, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
// Simple ACF parsing
let id = Self::extract_value(&content, "appid")?;
let name = Self::extract_value(&content, "name")?;
let install_dir = Self::extract_value(&content, "installdir")?;
let game_path = self.steam_path
.join("steamapps")
.join("common")
.join(&install_dir);
Ok(GameInfo {
id,
name,
path: game_path,
icon: None,
})
}
fn extract_value(content: &str, key: &str) -> Result<String, Box<dyn std::error::Error>> {
let pattern = format!("\"{}\"\\s+\"([^\"]+)\"", key);
let re = regex::Regex::new(&pattern)?;
if let Some(captures) = re.captures(content) {
Ok(captures[1].to_string())
} else {
Err("Key not found".into())
}
}
}
#[tauri::command]
pub async fn scan_steam_games() -> Result<Vec<GameInfo>, String> {
let plugin = SteamPlugin::new();
Ok(plugin.scan_games())
}For more information, check the API Reference or learn how to publish your plugin.