| Crates.io | alfrusco |
| lib.rs | alfrusco |
| version | 0.1.9 |
| created_at | 2024-05-27 05:09:30.463312+00 |
| updated_at | 2025-06-10 20:43:30.083971+00 |
| description | Utilities for building Alfred workflows with Rust. |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1253035 |
| size | 282,343 |
A powerful, ergonomic Rust library for building Alfred workflows with ease. Alfrusco handles the complexity of Alfred's JSON protocol, provides rich item building capabilities, and includes advanced features like background jobs, clipboard operations, and comprehensive logging.
Add alfrusco to your Cargo.toml:
[dependencies]
alfrusco = "0.1"
# For async workflows
tokio = { version = "1", features = ["full"] }
# For command-line argument parsing (recommended)
clap = { version = "4", features = ["derive", "env"] }
use alfrusco::{execute, Item, Runnable, Workflow};
use alfrusco::config::AlfredEnvProvider;
use clap::Parser;
#[derive(Parser)]
struct MyWorkflow {
query: Vec<String>,
}
impl Runnable for MyWorkflow {
type Error = alfrusco::Error;
fn run(self, workflow: &mut Workflow) -> Result<(), Self::Error> {
let query = self.query.join(" ");
workflow.append_item(
Item::new(format!("Hello, {}!", query))
.subtitle("This is a basic Alfred workflow")
.arg(&query)
.valid(true)
);
Ok(())
}
}
fn main() {
// Initialize logging (optional but recommended)
let _ = alfrusco::init_logging(&AlfredEnvProvider);
// Parse command line arguments and execute workflow
let command = MyWorkflow::parse();
execute(&AlfredEnvProvider, command, &mut std::io::stdout());
}
use alfrusco::{execute_async, AsyncRunnable, Item, Workflow, WorkflowError};
use alfrusco::config::AlfredEnvProvider;
use clap::Parser;
use serde::Deserialize;
#[derive(Parser)]
struct ApiWorkflow {
query: Vec<String>,
}
#[derive(Deserialize)]
struct ApiResponse {
results: Vec<ApiResult>,
}
#[derive(Deserialize)]
struct ApiResult {
title: String,
description: String,
url: String,
}
#[async_trait::async_trait]
impl AsyncRunnable for ApiWorkflow {
type Error = Box<dyn WorkflowError>;
async fn run_async(self, workflow: &mut Workflow) -> Result<(), Self::Error> {
let query = self.query.join(" ");
workflow.set_filter_keyword(query.clone());
let url = format!("https://api.example.com/search?q={}", query);
let response: ApiResponse = reqwest::get(&url)
.await?
.json()
.await?;
let items: Vec<Item> = response.results
.into_iter()
.map(|result| {
Item::new(&result.title)
.subtitle(&result.description)
.arg(&result.url)
.quicklook_url(&result.url)
.valid(true)
})
.collect();
workflow.append_items(items);
Ok(())
}
}
#[tokio::main]
async fn main() {
let _ = alfrusco::init_logging(&AlfredEnvProvider);
let command = ApiWorkflow::parse();
execute_async(&AlfredEnvProvider, command, &mut std::io::stdout()).await;
}
Items are the building blocks of Alfred workflows. Each item represents a choice in the Alfred selection UI:
use alfrusco::Item;
let item = Item::new("My Title")
.subtitle("Additional information")
.arg("argument-passed-to-action")
.uid("unique-identifier")
.valid(true)
.icon_from_image("/path/to/icon.png")
.copy_text("Text copied with โC")
.large_type_text("Text shown in large type with โL")
.quicklook_url("https://example.com")
.var("CUSTOM_VAR", "value")
.autocomplete("text for tab completion");
Alfrusco automatically handles Alfred's environment variables through configuration providers:
use alfrusco::config::{AlfredEnvProvider, TestingProvider};
// For production (reads from Alfred environment variables)
let provider = AlfredEnvProvider;
// For testing (uses temporary directories)
let temp_dir = tempfile::tempdir().unwrap();
let provider = TestingProvider(temp_dir.path().to_path_buf());
Implement custom error types that work seamlessly with Alfred:
use alfrusco::{WorkflowError, Item};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyWorkflowError {
#[error("Network request failed: {0}")]
Network(#[from] reqwest::Error),
#[error("Invalid input: {0}")]
InvalidInput(String),
}
impl WorkflowError for MyWorkflowError {}
// Errors automatically become Alfred items
impl Runnable for MyWorkflow {
type Error = MyWorkflowError;
fn run(self, workflow: &mut Workflow) -> Result<(), Self::Error> {
// If this returns an error, Alfred will show it as an item
Err(MyWorkflowError::InvalidInput("Missing required field".to_string()))
}
}
Run long-running tasks without blocking Alfred's UI with enhanced status tracking and automatic retry logic:
use std::process::Command;
use std::time::Duration;
impl Runnable for MyWorkflow {
type Error = alfrusco::Error;
fn run(self, workflow: &mut Workflow) -> Result<(), Self::Error> {
// Set up a command to run in the background
let mut cmd = Command::new("curl");
cmd.arg("-s")
.arg("https://api.github.com/repos/rust-lang/rust/releases/latest");
// Run the command in the background, refresh every 30 seconds
workflow.run_in_background(
"github-releases",
Duration::from_secs(30),
cmd
);
// Show immediate results while background job runs
workflow.append_item(
Item::new("Fetching latest Rust release...")
.subtitle("Background job in progress")
.valid(false)
);
Ok(())
}
}
Enhanced Background Job Features:
Background Job Status Messages:
When a background job is running, Alfred will display informative status items:
Background Job 'github-releases'
Last succeeded 2 minutes ago (14:32:15), running for 3s
This gives users clear visibility into:
### URL Items with Rich Clipboard Support
Create URL items with automatic clipboard integration:
```rust
use alfrusco::URLItem;
let url_item = URLItem::new("Rust Documentation", "https://doc.rust-lang.org/")
.subtitle("The Rust Programming Language Documentation")
.short_title("Rust Docs") // Used in Cmd+Shift modifier
.long_title("The Rust Programming Language Official Documentation") // Used in Cmd+Ctrl modifier
.icon_for_filetype("public.html")
.copy_text("doc.rust-lang.org");
// Convert to regular Item (happens automatically when added to workflow)
let item: Item = url_item.into();
URL items automatically include modifiers for copying links:
[title](url)Enable automatic fuzzy search and sorting of results:
impl Runnable for SearchWorkflow {
type Error = alfrusco::Error;
fn run(self, workflow: &mut Workflow) -> Result<(), Self::Error> {
let query = self.query.join(" ");
// Enable filtering - results will be automatically filtered and sorted
workflow.set_filter_keyword(query);
// Add items - they'll be filtered based on the query
workflow.append_items(vec![
Item::new("Apple").subtitle("Fruit"),
Item::new("Banana").subtitle("Yellow fruit"),
Item::new("Carrot").subtitle("Orange vegetable"),
]);
Ok(())
}
}
Access workflow-specific data and cache directories:
impl Runnable for MyWorkflow {
type Error = alfrusco::Error;
fn run(self, workflow: &mut Workflow) -> Result<(), Self::Error> {
// Access workflow data directory (persistent storage)
let data_dir = workflow.data_dir();
let config_file = data_dir.join("config.json");
// Access workflow cache directory (temporary storage)
let cache_dir = workflow.cache_dir();
let temp_file = cache_dir.join("temp_data.json");
// Use directories for file operations
std::fs::write(config_file, "{\"setting\": \"value\"}")?;
Ok(())
}
}
Control Alfred's caching behavior and automatic refresh:
use std::time::Duration;
impl Runnable for MyWorkflow {
type Error = alfrusco::Error;
fn run(self, workflow: &mut Workflow) -> Result<(), Self::Error> {
// Cache results for 5 minutes, allow loose reload
workflow.response.cache(Duration::from_secs(300), true);
// Automatically rerun every 30 seconds
workflow.response.rerun(Duration::from_secs(30));
// Skip Alfred's knowledge base integration
workflow.response.skip_knowledge(true);
workflow.append_item(Item::new("Cached result"));
Ok(())
}
}
Alfrusco provides comprehensive testing support with shared utilities and organized test structure:
#[cfg(test)]
mod tests {
use super::*;
use alfrusco::config::TestingProvider;
use tempfile::tempdir;
#[test]
fn test_my_workflow() {
let workflow = MyWorkflow {
query: vec!["test".to_string()],
};
// Use TestingProvider for isolated testing
let temp_dir = tempdir().unwrap();
let provider = TestingProvider(temp_dir.path().to_path_buf());
let mut buffer = Vec::new();
alfrusco::execute(&provider, workflow, &mut buffer);
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("Hello, test!"));
}
#[tokio::test]
async fn test_async_workflow() {
let workflow = AsyncWorkflow {
query: vec!["async".to_string()],
};
let temp_dir = tempdir().unwrap();
let provider = TestingProvider(temp_dir.path().to_path_buf());
let mut buffer = Vec::new();
alfrusco::execute_async(&provider, workflow, &mut buffer).await;
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("async"));
}
}
Alfrusco maintains a comprehensive test suite with 112 tests across organized test files:
background_job_integration_tests.rs - Complete background job lifecycle testing (6 tests)clipboard_tests.rs - Clipboard functionality testing (4 tests)config_tests.rs - Configuration and environment testing (8 tests)error_injection_tests.rs - Error handling and edge cases (2 tests)error_tests.rs - Error type behavior (7 tests)logging_tests.rs - Logging functionality (1 test)runnable_tests.rs - Trait implementation testing (4 tests)tests/common/mod.rs - Shared test utilities and helpersThe tests/common/mod.rs module provides reusable testing utilities that eliminate code duplication and ensure consistent test setup across the entire test suite. This includes helper functions for creating test workflows, managing temporary directories, and common test operations.
## ๐ Examples
The `examples/` directory contains complete, runnable examples. Since these examples use `AlfredEnvProvider`, they require Alfred environment variables to be set. We provide a convenient script to run them with mock environment variables:
### Running Examples
**Option 1: Using the run script (recommended)**
```bash
# Basic static output
./run-example.sh static_output
# Success workflow with custom message
./run-example.sh success --message "Custom message"
# Async API example
./run-example.sh random_user search_term
# URL items demonstration
./run-example.sh url_items
# Background job example
./run-example.sh sleep --duration-in-seconds 10
# Error handling example
./run-example.sh error --file-path nonexistent.txt
Option 2: Using Make targets
# List all available examples
make examples-help
# Run specific examples
make example-static_output
make example-success
make example-url_items
Option 3: Manual environment setup
# Set required Alfred environment variables
export alfred_workflow_bundleid="com.example.test"
export alfred_workflow_cache="/tmp/cache"
export alfred_workflow_data="/tmp/data"
export alfred_version="5.0"
export alfred_version_build="2058"
export alfred_workflow_name="Test Workflow"
# Then run normally
cargo run --example static_output
ItemThe primary building block for Alfred workflow results.
Key Methods:
new(title) - Create a new item with a titlesubtitle(text) - Set subtitle textarg(value) / args(values) - Set arguments passed to actionsvalid(bool) - Set whether the item is actionableuid(id) - Set unique identifier for Alfred's learningicon_from_image(path) / icon_for_filetype(type) - Set item iconscopy_text(text) / large_type_text(text) - Set text operationsquicklook_url(url) - Enable Quick Look previewvar(key, value) - Set workflow variablesautocomplete(text) - Set tab completion textmodifier(modifier) - Add keyboard modifier actionsURLItemSpecialized item type for URLs with automatic clipboard integration.
Key Methods:
new(title, url) - Create a URL itemsubtitle(text) - Override default URL subtitleshort_title(text) / long_title(text) - Set alternative titles for modifiersdisplay_title(text) - Override displayed title while preserving link titlecopy_text(text) - Set custom copy texticon_from_image(path) / icon_for_filetype(type) - Set iconsWorkflowMain workflow execution context.
Key Methods:
append_item(item) / append_items(items) - Add items to resultsprepend_item(item) / prepend_items(items) - Add items to beginningset_filter_keyword(query) - Enable fuzzy filteringdata_dir() / cache_dir() - Access workflow directoriesrun_in_background(name, max_age, command) - Execute background jobsResponseControls Alfred's response behavior.
Key Methods:
cache(duration, loose_reload) - Set caching behaviorrerun(interval) - Set automatic refresh intervalskip_knowledge(bool) - Control Alfred's knowledge integrationRunnableFor synchronous workflows.
trait Runnable {
type Error: WorkflowError;
fn run(self, workflow: &mut Workflow) -> Result<(), Self::Error>;
}
AsyncRunnableFor asynchronous workflows.
#[async_trait]
trait AsyncRunnable {
type Error: WorkflowError;
async fn run_async(self, workflow: &mut Workflow) -> Result<(), Self::Error>;
}
WorkflowErrorFor custom error types that integrate with Alfred.
trait WorkflowError: std::error::Error {
fn error_item(&self) -> Item { /* default implementation */ }
}
AlfredEnvProviderProduction configuration provider that reads from Alfred environment variables.
TestingProviderTesting configuration provider that uses temporary directories.
execute(provider, runnable, writer) - Execute synchronous workflowexecute_async(provider, runnable, writer) - Execute asynchronous workflowinit_logging(provider) - Initialize structured logging## ๐ ๏ธ Developmentgit clone https://github.com/adlio/alfrusco.git
cd alfrusco
cargo build
# Run all tests
cargo test
# Run tests with nextest (recommended)
cargo nextest run
# Run tests serially (for debugging flaky tests)
make test-serial
# Run with coverage
cargo tarpaulin --out html
Examples require Alfred environment variables. Use the provided script:
# Basic static output
./run-example.sh static_output
# Success workflow with custom message
./run-example.sh success --message "Hello World"
# Async API example
./run-example.sh random_user john
# URL items demonstration
./run-example.sh url_items
# Background job example
./run-example.sh sleep --duration-in-seconds 5
# Error handling example
./run-example.sh error --file-path /nonexistent/file.txt
# Or use Make targets
make example-static_output
make examples-help # See all available examples
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
git checkout -b feature/amazing-feature)cargo nextest run)cargo clippy)cargo fmt)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)cargo fmt)cargo clippy)This project is licensed under the MIT License - see the LICENSE file for details.
Made with โค๏ธ for the Alfred and Rust communities.