Skip to content

Commit 8da5196

Browse files
authored
Merge pull request #1679 from ksylvan/0807-desktop-notification
Add cross-platform desktop notifications to Fabric CLI
2 parents 3584f83 + 30d23f1 commit 8da5196

File tree

14 files changed

+790
-43
lines changed

14 files changed

+790
-43
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"Laomedeia",
8181
"ldflags",
8282
"libexec",
83+
"libnotify",
8384
"listcontexts",
8485
"listextensions",
8586
"listmodels",
@@ -93,6 +94,7 @@
9394
"matplotlib",
9495
"mattn",
9596
"mbed",
97+
"metacharacters",
9698
"Miessler",
9799
"nometa",
98100
"numpy",
@@ -102,6 +104,7 @@
102104
"opencode",
103105
"openrouter",
104106
"Orus",
107+
"osascript",
105108
"otiai",
106109
"pdflatex",
107110
"pipx",

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ Application Options:
551551
--voice= TTS voice name for supported models (e.g., Kore, Charon, Puck)
552552
(default: Kore)
553553
--list-gemini-voices List all available Gemini TTS voices
554+
--notification Send desktop notification when command completes
555+
--notification-command= Custom command to run for notifications (overrides built-in
556+
notifications)
554557
555558
Help Options:
556559
-h, --help Show this help message
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### PR [#1679](https://github.com/danielmiessler/Fabric/pull/1679) by [ksylvan](https://github.com/ksylvan): Add cross-platform desktop notifications to Fabric CLI
2+
3+
- Add cross-platform desktop notifications with secure custom commands
4+
- Integrate notification sending into chat processing workflow
5+
- Add --notification and --notification-command CLI flags and help
6+
- Provide cross-platform providers: macOS, Linux, Windows with fallbacks
7+
- Escape shell metacharacters to prevent injection vulnerabilities

completions/_fabric

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ _fabric() {
117117
'(--think-start-tag)--think-start-tag[Start tag for thinking sections (default: <think>)]:start tag:' \
118118
'(--think-end-tag)--think-end-tag[End tag for thinking sections (default: </think>)]:end tag:' \
119119
'(--disable-responses-api)--disable-responses-api[Disable OpenAI Responses API (default: false)]' \
120+
'(--notification)--notification[Send desktop notification when command completes]' \
121+
'(--notification-command)--notification-command[Custom command to run for notifications]:notification command:' \
120122
'(-h --help)'{-h,--help}'[Show this help message]' \
121123
'*:arguments:'
122124
}

completions/fabric.bash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ _fabric() {
1313
_get_comp_words_by_ref -n : cur prev words cword
1414

1515
# Define all possible options/flags
16-
local opts="--pattern -p --variable -v --context -C --session --attachment -a --setup -S --temperature -t --topp -T --stream -s --presencepenalty -P --raw -r --frequencypenalty -F --listpatterns -l --listmodels -L --listcontexts -x --listsessions -X --updatepatterns -U --copy -c --model -m --modelContextLength --output -o --output-session --latest -n --changeDefaultModel -d --youtube -y --playlist --transcript --transcript-with-timestamps --comments --metadata --language -g --scrape_url -u --scrape_question -q --seed -e --wipecontext -w --wipesession -W --printcontext --printsession --readability --input-has-vars --dry-run --serve --serveOllama --address --api-key --config --search --search-location --image-file --image-size --image-quality --image-compression --image-background --suppress-think --think-start-tag --think-end-tag --disable-responses-api --voice --list-gemini-voices --version --listextensions --addextension --rmextension --strategy --liststrategies --listvendors --shell-complete-list --help -h"
16+
local opts="--pattern -p --variable -v --context -C --session --attachment -a --setup -S --temperature -t --topp -T --stream -s --presencepenalty -P --raw -r --frequencypenalty -F --listpatterns -l --listmodels -L --listcontexts -x --listsessions -X --updatepatterns -U --copy -c --model -m --modelContextLength --output -o --output-session --latest -n --changeDefaultModel -d --youtube -y --playlist --transcript --transcript-with-timestamps --comments --metadata --language -g --scrape_url -u --scrape_question -q --seed -e --wipecontext -w --wipesession -W --printcontext --printsession --readability --input-has-vars --dry-run --serve --serveOllama --address --api-key --config --search --search-location --image-file --image-size --image-quality --image-compression --image-background --suppress-think --think-start-tag --think-end-tag --disable-responses-api --voice --list-gemini-voices --notification --notification-command --version --listextensions --addextension --rmextension --strategy --liststrategies --listvendors --shell-complete-list --help -h"
1717

1818
# Helper function for dynamic completions
1919
_fabric_get_list() {
@@ -85,7 +85,7 @@ _fabric() {
8585
return 0
8686
;;
8787
# Options requiring simple arguments (no specific completion logic here)
88-
-v | --variable | -t | --temperature | -T | --topp | -P | --presencepenalty | -F | --frequencypenalty | --modelContextLength | -n | --latest | -y | --youtube | -g | --language | -u | --scrape_url | -q | --scrape_question | -e | --seed | --address | --api-key | --search-location | --image-compression | --think-start-tag | --think-end-tag)
88+
-v | --variable | -t | --temperature | -T | --topp | -P | --presencepenalty | -F | --frequencypenalty | --modelContextLength | -n | --latest | -y | --youtube | -g | --language | -u | --scrape_url | -q | --scrape_question | -e | --seed | --address | --api-key | --search-location | --image-compression | --think-start-tag | --think-end-tag | --notification-command)
8989
# No specific completion suggestions, user types the value
9090
return 0
9191
;;

completions/fabric.fish

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ complete -c fabric -l strategy -d "Choose a strategy from the available strategi
7676
complete -c fabric -l think-start-tag -d "Start tag for thinking sections (default: <think>)"
7777
complete -c fabric -l think-end-tag -d "End tag for thinking sections (default: </think>)"
7878
complete -c fabric -l voice -d "TTS voice name for supported models (e.g., Kore, Charon, Puck)" -a "(__fabric_get_gemini_voices)"
79+
complete -c fabric -l notification-command -d "Custom command to run for notifications (overrides built-in notifications)"
7980

8081
# Boolean flags (no arguments)
8182
complete -c fabric -s S -l setup -d "Run setup for all reconfigurable parts of fabric"
@@ -108,4 +109,5 @@ complete -c fabric -l list-gemini-voices -d "List all available Gemini TTS voice
108109
complete -c fabric -l shell-complete-list -d "Output raw list without headers/formatting (for shell completion)"
109110
complete -c fabric -l suppress-think -d "Suppress text enclosed in thinking tags"
110111
complete -c fabric -l disable-responses-api -d "Disable OpenAI Responses API (default: false)"
112+
complete -c fabric -l notification -d "Send desktop notification when command completes"
111113
complete -c fabric -s h -l help -d "Show this help message"

docs/Desktop-Notifications.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Desktop Notifications
2+
3+
Fabric supports desktop notifications to alert you when commands complete, which is especially useful for long-running tasks or when you're multitasking.
4+
5+
## Quick Start
6+
7+
Enable notifications with the `--notification` flag:
8+
9+
```bash
10+
fabric --pattern summarize --notification < article.txt
11+
```
12+
13+
## Configuration
14+
15+
### Command Line Options
16+
17+
- `--notification`: Enable desktop notifications when command completes
18+
- `--notification-command`: Use a custom notification command instead of built-in notifications
19+
20+
### YAML Configuration
21+
22+
Add notification settings to your `~/.config/fabric/config.yaml`:
23+
24+
```yaml
25+
# Enable notifications by default
26+
notification: true
27+
28+
# Optional: Custom notification command
29+
notificationCommand: 'notify-send --urgency=normal "$1" "$2"'
30+
```
31+
32+
## Platform Support
33+
34+
### macOS
35+
36+
- **Default**: Uses `osascript` (built into macOS)
37+
- **Enhanced**: Install `terminal-notifier` for better notifications:
38+
39+
```bash
40+
brew install terminal-notifier
41+
```
42+
43+
### Linux
44+
45+
- **Requirement**: Install `notify-send`:
46+
47+
```bash
48+
# Ubuntu/Debian
49+
sudo apt install libnotify-bin
50+
51+
# Fedora
52+
sudo dnf install libnotify
53+
```
54+
55+
### Windows
56+
57+
- **Default**: Uses PowerShell message boxes (built-in)
58+
59+
## Custom Notification Commands
60+
61+
The `--notification-command` flag allows you to use custom notification scripts or commands. The command receives the title as `$1` and message as `$2` as shell positional arguments.
62+
63+
**Security Note**: The title and message content are properly escaped to prevent command injection attacks from AI-generated output containing shell metacharacters.
64+
65+
### Examples
66+
67+
**macOS with custom sound:**
68+
69+
```bash
70+
fabric --pattern analyze_claims --notification-command 'osascript -e "display notification \"$2\" with title \"$1\" sound name \"Ping\""' < document.txt
71+
```
72+
73+
**Linux with urgency levels:**
74+
75+
```bash
76+
fabric --pattern extract_wisdom --notification-command 'notify-send --urgency=critical "$1" "$2"' < video-transcript.txt
77+
```
78+
79+
**Custom script:**
80+
81+
```bash
82+
fabric --pattern summarize --notification-command '/path/to/my-notification-script.sh "$1" "$2"' < report.pdf
83+
```
84+
85+
**Testing your custom command:**
86+
87+
```bash
88+
# Test that $1 and $2 are passed correctly
89+
fabric --pattern raw_query --notification-command 'echo "Title: $1, Message: $2"' "test input"
90+
```
91+
92+
## Notification Content
93+
94+
Notifications include:
95+
96+
- **Title**: "Fabric Command Complete" or "Fabric: [pattern] Complete"
97+
- **Message**: Brief summary of the output (first 100 characters)
98+
99+
For long outputs, the message is truncated with "..." to fit notification display limits.
100+
101+
## Use Cases
102+
103+
### Long-Running Tasks
104+
105+
```bash
106+
# Process large document with notifications
107+
fabric --pattern analyze_paper --notification < research-paper.pdf
108+
109+
# Extract wisdom from long video with alerts
110+
fabric -y "https://youtube.com/watch?v=..." --pattern extract_wisdom --notification
111+
```
112+
113+
### Background Processing
114+
115+
```bash
116+
# Process multiple files and get notified when each completes
117+
for file in *.txt; do
118+
fabric --pattern summarize --notification < "$file" &
119+
done
120+
```
121+
122+
### Integration with Other Tools
123+
124+
```bash
125+
# Combine with other commands
126+
curl -s "https://api.example.com/data" | \
127+
fabric --pattern analyze_data --notification --output results.md
128+
```
129+
130+
## Troubleshooting
131+
132+
### No Notifications Appearing
133+
134+
1. **Check system notifications are enabled** for Terminal/your shell
135+
2. **Verify notification tools are installed**:
136+
- macOS: `which osascript` (should exist)
137+
- Linux: `which notify-send`
138+
- Windows: `where.exe powershell`
139+
140+
3. **Test with simple command**:
141+
142+
```bash
143+
echo "test" | fabric --pattern raw_query --notification --dry-run
144+
```
145+
146+
### Notification Permission Issues
147+
148+
On some systems, you may need to grant notification permissions to your terminal application:
149+
150+
- **macOS**: System Preferences → Security & Privacy → Privacy → Notifications → Enable for Terminal
151+
- **Linux**: Depends on desktop environment; usually automatic
152+
- **Windows**: Usually works by default
153+
154+
### Custom Commands Not Working
155+
156+
- Ensure your custom notification command is executable
157+
- Test the command manually with sample arguments
158+
- Check that all required dependencies are installed
159+
160+
## Advanced Configuration
161+
162+
### Environment-Specific Settings
163+
164+
Create different configuration files for different environments:
165+
166+
```bash
167+
# Work computer (quieter notifications)
168+
fabric --config ~/.config/fabric/work-config.yaml --notification
169+
170+
# Personal computer (with sound)
171+
fabric --config ~/.config/fabric/personal-config.yaml --notification
172+
```
173+
174+
### Integration with Task Management
175+
176+
```bash
177+
# Custom script that also logs to task management system
178+
notificationCommand: '/usr/local/bin/fabric-notify-and-log.sh "$1" "$2"'
179+
```
180+
181+
## Examples
182+
183+
See `docs/notification-config.yaml` for a complete configuration example with various notification command options.

docs/notification-config.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Example Fabric configuration with notification support
2+
# Save this to ~/.config/fabric/config.yaml to use as defaults
3+
4+
# Enable notifications by default for all commands
5+
notification: true
6+
7+
# Optional: Use a custom notification command
8+
# Examples:
9+
# macOS with custom sound:
10+
# notificationCommand: 'osascript -e "display notification \"$2\" with title \"$1\" sound name \"Ping\""'
11+
#
12+
# Linux with custom urgency:
13+
# notificationCommand: 'notify-send --urgency=normal "$1" "$2"'
14+
#
15+
# Custom script:
16+
# notificationCommand: '/path/to/custom-notification-script.sh "$1" "$2"'
17+
18+
# Other common settings
19+
model: "gpt-4o"
20+
temperature: 0.7
21+
stream: true

internal/cli/chat.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package cli
33
import (
44
"fmt"
55
"os"
6+
"os/exec"
67
"path/filepath"
78
"strings"
89

910
"github.com/danielmiessler/fabric/internal/core"
1011
"github.com/danielmiessler/fabric/internal/domain"
1112
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
13+
"github.com/danielmiessler/fabric/internal/tools/notifications"
1214
)
1315

1416
// handleChatProcessing handles the main chat processing logic
@@ -115,9 +117,65 @@ func handleChatProcessing(currentFlags *Flags, registry *core.PluginRegistry, me
115117
}
116118
}
117119
}
120+
121+
// Send notification if requested
122+
if chatOptions.Notification {
123+
if err = sendNotification(chatOptions, chatReq.PatternName, result); err != nil {
124+
// Log notification error but don't fail the main command
125+
fmt.Fprintf(os.Stderr, "Failed to send notification: %v\n", err)
126+
}
127+
}
128+
118129
return
119130
}
120131

132+
// sendNotification sends a desktop notification about command completion.
133+
//
134+
// When truncating the result for notification display, this function counts Unicode code points,
135+
// not grapheme clusters. As a result, complex emoji or accented characters with multiple combining
136+
// characters may be truncated improperly. This is a limitation of the current implementation.
137+
func sendNotification(options *domain.ChatOptions, patternName, result string) error {
138+
title := "Fabric Command Complete"
139+
if patternName != "" {
140+
title = fmt.Sprintf("Fabric: %s Complete", patternName)
141+
}
142+
143+
// Limit message length for notification display (counts Unicode code points)
144+
message := "Command completed successfully"
145+
if result != "" {
146+
maxLength := 100
147+
runes := []rune(result)
148+
if len(runes) > maxLength {
149+
message = fmt.Sprintf("Output: %s...", string(runes[:maxLength]))
150+
} else {
151+
message = fmt.Sprintf("Output: %s", result)
152+
}
153+
// Clean up newlines for notification display
154+
message = strings.ReplaceAll(message, "\n", " ")
155+
}
156+
157+
// Use custom notification command if provided
158+
if options.NotificationCommand != "" {
159+
// SECURITY: Pass title and message as proper shell positional arguments $1 and $2
160+
// This matches the documented interface where custom commands receive title and message as shell variables
161+
cmd := exec.Command("sh", "-c", options.NotificationCommand+" \"$1\" \"$2\"", "--", title, message)
162+
163+
// For debugging: capture and display output from custom commands
164+
cmd.Stdout = os.Stdout
165+
cmd.Stderr = os.Stderr
166+
167+
return cmd.Run()
168+
}
169+
170+
// Use built-in notification system
171+
notificationManager := notifications.NewNotificationManager()
172+
if !notificationManager.IsAvailable() {
173+
return fmt.Errorf("no notification system available")
174+
}
175+
176+
return notificationManager.Send(title, message)
177+
}
178+
121179
// isTTSModel checks if the model is a text-to-speech model
122180
func isTTSModel(modelName string) bool {
123181
lowerModel := strings.ToLower(modelName)

0 commit comments

Comments
 (0)