Thank you for your interest in contributing to the Stream Deck iCal Plugin! This document provides guidelines and information for developers.
- Development Environment Setup
- Project Architecture
- Building the Plugin
- Testing
- Debugging with Stream Deck
- Code Style and Conventions
- Commit Messages
- Provider-Specific Notes
- Creating Releases
- Global Settings
- Node.js 20+ (required for Stream Deck Node.js runtime)
- npm (comes with Node.js)
- Git
- Elgato Stream Deck Software 6.0+ (for testing)
- Clone the repository:
git clone https://github.com/pedrofuentes/stream-deck-ical.git
cd stream-deck-ical- Install dependencies:
npm install- Build the plugin:
npm run buildThe plugin is built using:
- TypeScript for type safety and better developer experience
- Rollup for bundling the Node.js plugin and Property Inspector
- Vitest for fast, modern testing
- @elgato/streamdeck Node.js SDK v2 for Stream Deck integration
stream-deck-ical/
├── src/ # TypeScript source code
│ ├── plugin.ts # Main plugin entry point
│ ├── actions/ # Action implementations
│ │ ├── base-action.ts # Base class for all actions
│ │ ├── next-meeting.ts # NextMeeting action
│ │ └── time-left.ts # TimeLeft action
│ ├── services/ # Business logic layer
│ │ ├── calendar-service.ts # Calendar fetching & caching
│ │ ├── ical-parser.ts # iCal parsing
│ │ ├── recurrence-expander.ts # RRULE expansion
│ │ └── timezone-service.ts # Timezone conversions
│ ├── utils/ # Utility functions
│ │ ├── time-utils.ts # Time formatting
│ │ ├── event-utils.ts # Event filtering/sorting
│ │ └── logger.ts # Debug logging
│ └── types/ # TypeScript type definitions
│ └── index.ts
├── pi/ # Property Inspector (HTML/JS)
│ ├── pi.html # Main PI interface
│ ├── pi.js # PI logic
│ ├── setup.html # Settings window
│ ├── setup.js # Settings logic
│ └── css/ # Styles
├── assets/ # Images and icons
├── __fixtures__/ # Test data (iCal files)
│ ├── google-calendar/
│ ├── outlook/
│ └── apple/
├── tests/ # Test files
│ ├── *.test.ts # Unit tests
│ └── integration/ # Integration tests
├── dist/ # Build output (gitignored)
├── manifest.json # Plugin metadata
├── tsconfig.json # TypeScript configuration
├── rollup.config.js # Build configuration
└── vitest.config.ts # Test configuration
Actions are the buttons users place on their Stream Deck. Each action:
- Extends
BaseActionclass - Uses
@actiondecorator with UUID - Implements
updateDisplay()for live updates - Handles user interactions (key press, double press)
Services handle the business logic:
- CalendarService: Fetches and caches iCal feeds, filters all-day events
- ICalParser: Parses iCal format with timezone support
- RecurrenceExpander: Expands recurring events using RRULE
- TimezoneService: Converts Windows timezones to IANA format
The PI is a web-based UI (HTML/CSS/JS) that appears in Stream Deck software when configuring the plugin. It communicates with the plugin via WebSocket.
npm run buildCreates a development build in dist/ with source maps.
npm run build:productionCreates an optimized production build in release/com.pedrofuentes.ical.sdPlugin/.
npm run watchAutomatically rebuilds on file changes (useful during development).
npm run clean
npm run buildnpm testnpm run test:watchnpm run test:coverageTests use Vitest and follow this structure:
import { describe, it, expect } from 'vitest';
import { myFunction } from '../src/utils/my-util';
describe('myFunction', () => {
it('should do something', () => {
expect(myFunction(input)).toBe(expectedOutput);
});
});iCal test fixtures are in __fixtures__/ organized by provider:
google-calendar/- Google Calendar exportsoutlook/- Microsoft Outlook/Office 365 exportsapple/- Apple Calendar/iCloud exports
When adding fixtures:
- Use realistic, anonymized data
- Include PRODID to test provider detection
- Test edge cases (all-day events, recurring, timezones)
The project includes npm scripts for the Stream Deck CLI:
# Link plugin for development (run once)
npm run streamdeck:link
# Start Stream Deck with plugin in debug mode
npm run streamdeck:dev
# Restart the plugin without restarting Stream Deck
npm run streamdeck:restart
# Package plugin for distribution
npm run streamdeck:packSet the STREAMDECK_DEBUG environment variable before building to enable detailed logging and the debug panel:
Windows (PowerShell):
$env:STREAMDECK_DEBUG = "1"
npm run build
streamdeck restart com.pedrofuentes.icalmacOS/Linux:
STREAMDECK_DEBUG=1 npm run buildDebug Mode Behavior:
| Feature | Debug Mode OFF | Debug Mode ON |
|---|---|---|
| Log Level | INFO (minimal) | TRACE (verbose) |
| Debug Panel | Hidden | Visible |
| DEBUG/TRACE logs | Suppressed | Written to log file |
| Performance | Optimized | More overhead |
When debug mode is enabled:
- Log level is set to TRACE (all DEBUG and TRACE messages logged)
- A Debug Panel appears in the Settings popup showing:
- Cache status (INIT/LOADING/OK/ERROR)
- Event count and last fetch time
- List of upcoming events (first 10)
- Recent debug logs with timestamps
- Actions log state changes for troubleshooting
When debug mode is disabled:
- Log level is set to INFO (cleaner, less verbose logs)
- Debug Panel is hidden from the Settings popup
- Only important messages (INFO, WARN, ERROR) are logged
If not using the CLI, create a symlink to the Stream Deck plugins directory:
macOS:
ln -s "$(pwd)/dist/com.pedrofuentes.ical.sdPlugin" "$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/com.pedrofuentes.ical.sdPlugin"Windows (PowerShell as Administrator):
New-Item -ItemType SymbolicLink -Path "$env:APPDATA\Elgato\StreamDeck\Plugins\com.pedrofuentes.ical.sdPlugin" -Target "$(pwd)\dist\com.pedrofuentes.ical.sdPlugin"The plugin will now appear in the Stream Deck software.
Plugin Logs (Node.js):
- The plugin uses
@elgato/streamdecklogger - Set
STREAMDECK_DEBUG=1for verbose debug logging - Logs visible in Stream Deck CLI output or log files
Stream Deck Logs Location:
- macOS:
~/Library/Logs/ElgatoStreamDeck/ - Windows:
%APPDATA%\Elgato\StreamDeck\logs\
Property Inspector can be debugged:
- Right-click on action in Stream Deck software
- Select "Inspect Property Inspector"
- Chrome DevTools opens for the PI
- Use strict mode (
strict: truein tsconfig.json) - Prefer
interfaceovertypefor object shapes - Use explicit return types for public functions
- Avoid
any- useunknownif type is truly unknown
- Files: kebab-case (
calendar-service.ts) - Classes: PascalCase (
CalendarService) - Functions/Variables: camelCase (
updateCalendar) - Constants: UPPER_SNAKE_CASE (
UPDATE_INTERVAL) - Types/Interfaces: PascalCase (
CalendarEvent)
Always use .js extension in imports (TypeScript transpiles .ts → .js):
import { myFunction } from './my-file.js'; // ✅ Correct
import { myFunction } from './my-file'; // ❌ Wrong- Log errors with context using
logger.error() - Use specific error types when possible
- Handle errors gracefully - plugin should never crash
- Use JSDoc for public APIs
- Inline comments for complex logic only
- Keep comments up-to-date with code
Follow Conventional Commits:
<type>(<scope>): <subject>
<body>
<footer>
feat: New featurefix: Bug fixdocs: Documentation changestest: Test changesrefactor: Code refactoringchore: Build/tooling changes
feat(timezone): add Windows timezone conversion
Implement conversion of Windows timezone names (e.g., "Eastern Standard Time")
to IANA format using windows-iana library.
Closes #12
fix(parser): handle escaped characters in iCal text fields
Parse \n, \,, \; correctly according to RFC 5545.
- Uses IANA timezone names (e.g.,
America/New_York) - PRODID:
-//Google Inc//Google Calendar//EN - May include
X-GOOGLE-CONFERENCEfor Meet links - Includes
DTSTAMP,CREATED,LAST-MODIFIEDfields
- Uses Windows timezone names (e.g.,
Eastern Standard Time) - Often wraps TZID in quotes:
TZID="Eastern Standard Time" - PRODID:
-//Microsoft Corporation//Outlook//EN - May include
X-MICROSOFT-CDO-*properties - Requires windows-iana conversion
- Uses IANA timezone names
- PRODID:
-//Apple Inc.//Mac OS X//ENor-//Apple Inc.//iOS//EN - May include
X-APPLE-*properties - Clean iCal format, follows spec closely
- All-day events:
VALUE=DATEparameter, no time component (filtered by default viaexcludeAllDaysetting) - Recurring events: RRULE with various frequency types
- EXDATE: Dates excluded from recurrence
- Folded lines: Long lines split with leading whitespace
- Escaped text:
\n,\,,\;in SUMMARY/DESCRIPTION - DST transitions: Events during daylight saving time changes
-
Update version numbers in
manifest.jsonandpackage.json:// manifest.json "Version": "X.Y.Z.0" // package.json "version": "X.Y.Z"
-
Build for production:
npm run build:production
This creates an optimized build in
release/com.pedrofuentes.ical.sdPlugin/. -
Create the plugin package using Stream Deck CLI:
streamdeck pack "release/com.pedrofuentes.ical.sdPlugin" --output releaseThis creates
release/com.pedrofuentes.ical.streamDeckPlugin- the official package format.Important: Do NOT use
Compress-Archiveor manual zipping. The Stream Deck CLI creates a properly formatted package that Stream Deck can install. -
Create a Git tag:
git tag -a vX.Y.Z -m "vX.Y.Z - Release description" git push origin vX.Y.Z -
Create GitHub release with the plugin package:
gh release create vX.Y.Z "release/com.pedrofuentes.ical.streamDeckPlugin" \ --title "vX.Y.Z - Release Title" \ --notes "Release notes here"
Before publishing, test the package:
-
Uninstall the development plugin:
# Stop Stream Deck Stop-Process -Name "StreamDeck" -Force # Remove plugin Remove-Item "$env:APPDATA\Elgato\StreamDeck\Plugins\com.pedrofuentes.ical.sdPlugin" -Recurse -Force # Start Stream Deck Start-Process "$env:ProgramFiles\Elgato\StreamDeck\StreamDeck.exe"
-
Install the release package: Double-click
com.pedrofuentes.ical.streamDeckPlugin -
Verify functionality: Test all actions and settings
Follow Semantic Versioning:
- MAJOR: Breaking changes or major rewrites
- MINOR: New features, backward compatible
- PATCH: Bug fixes, backward compatible
The manifest uses 4-part version (X.Y.Z.0), package.json uses 3-part (X.Y.Z).
The plugin uses these global settings stored via Stream Deck SDK:
| Setting | Type | Default | Description |
|---|---|---|---|
url |
string | '' |
iCal feed URL |
timeWindow |
number | 3 |
Time window in days (1, 3, 5, or 7) |
excludeAllDay |
boolean | true |
Filter out all-day events |
urlVersion |
number | 0 |
Incremented on force refresh to trigger update |
Settings are managed in src/plugin.ts and pi/setup.js.
- Bug Reports: GitHub Issues
- Feature Requests: GitHub Issues
- Discussions: GitHub Discussions
Thank you for contributing! 🎉