Skip to content

Commit c17a6c8

Browse files
aarondfrancisclaude
andcommitted
Add exit code integration
- Add -e/--exit-code flag to pass previous command's exit code - Auto-set urgency to critical for non-zero exit codes - Add --if-failed flag to only notify on failure - Add tests for exit code functionality - Update README with exit code documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent c3e94b6 commit c17a6c8

File tree

3 files changed

+167
-9
lines changed

3 files changed

+167
-9
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ tnotify --bell # Just beep
5454

5555
# Show capabilities
5656
tnotify --capabilities
57+
58+
# Exit code integration (auto-sets urgency)
59+
make build; tnotify -e $? "Build finished" # critical if failed
60+
make test; tnotify -e $? --if-failed "Tests failed!" # only notify on failure
5761
```
5862

5963
## Terminal Support
@@ -68,6 +72,28 @@ tnotify --capabilities
6872
| foot | OSC 777 ||||
6973
| Others | Native fallback ||||
7074

75+
## Exit Code Integration
76+
77+
Use `-e` / `--exit-code` to pass the previous command's exit code:
78+
79+
```bash
80+
# Notify with auto-urgency (critical if non-zero)
81+
long-running-task; tnotify -e $? "Task complete"
82+
83+
# Only notify on failure
84+
make test; tnotify -e $? --if-failed "Tests failed!"
85+
86+
# Combine with title
87+
./deploy.sh; tnotify -e $? -t "Deploy" "Finished"
88+
```
89+
90+
| Exit Code | Urgency |
91+
|-----------|---------|
92+
| 0 | normal |
93+
| non-zero | critical |
94+
95+
The `--if-failed` flag skips the notification entirely if the exit code is 0.
96+
7197
## Native Fallbacks
7298

7399
When OSC notifications aren't supported, tnotify falls back to:

main.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ var (
2020
)
2121

2222
var (
23-
title string
24-
urgency string
25-
id string
26-
closeID string
27-
forceOSC bool
23+
title string
24+
urgency string
25+
id string
26+
closeID string
27+
forceOSC bool
2828
forceNative bool
29-
forceBell bool
30-
showCaps bool
29+
forceBell bool
30+
showCaps bool
31+
exitCode int
32+
useExitCode bool
3133
)
3234

3335
func main() {
@@ -48,7 +50,9 @@ OSC sequences work over SSH and inside tmux/screen!`,
4850
tnotify -u critical "Server down!"
4951
tnotify -i progress "Building... 50%"
5052
tnotify --close progress
51-
echo "Done" | tnotify -t "Results"`,
53+
echo "Done" | tnotify -t "Results"
54+
make build; tnotify -e $? "Build finished"
55+
make test; tnotify -e $? --if-failed "Tests failed!"`,
5256
Version: fmt.Sprintf("%s (commit: %s, built: %s)", version, commit, date),
5357
Args: cobra.MaximumNArgs(1),
5458
RunE: run,
@@ -62,6 +66,8 @@ OSC sequences work over SSH and inside tmux/screen!`,
6266
rootCmd.Flags().BoolVar(&forceNative, "native", false, "Force native notifications only")
6367
rootCmd.Flags().BoolVar(&forceBell, "bell", false, "Send terminal bell only")
6468
rootCmd.Flags().BoolVar(&showCaps, "capabilities", false, "Show terminal capabilities as JSON")
69+
rootCmd.Flags().IntVarP(&exitCode, "exit-code", "e", 0, "Previous command's exit code (sets urgency automatically)")
70+
rootCmd.Flags().BoolVar(&useExitCode, "if-failed", false, "Only notify if exit code is non-zero (use with -e)")
6571

6672
if err := rootCmd.Execute(); err != nil {
6773
os.Exit(1)
@@ -85,6 +91,11 @@ func run(cmd *cobra.Command, args []string) error {
8591
return nil
8692
}
8793

94+
// Handle --if-failed: skip notification if exit code is 0
95+
if useExitCode && exitCode == 0 {
96+
return nil
97+
}
98+
8899
// Get message from args or stdin
89100
message := ""
90101
if len(args) > 0 {
@@ -106,8 +117,11 @@ func run(cmd *cobra.Command, args []string) error {
106117
return fmt.Errorf("no message provided")
107118
}
108119

109-
// Parse urgency
120+
// Parse urgency (exit code overrides if provided via -e flag)
110121
urgencyLevel := parseUrgency(urgency)
122+
if cmd.Flags().Changed("exit-code") {
123+
urgencyLevel = urgencyFromExitCode(exitCode)
124+
}
111125

112126
// Send notification
113127
if forceNative {
@@ -133,6 +147,13 @@ func parseUrgency(u string) int {
133147
}
134148
}
135149

150+
func urgencyFromExitCode(code int) int {
151+
if code == 0 {
152+
return osc.UrgencyNormal
153+
}
154+
return osc.UrgencyCritical
155+
}
156+
136157
func sendOSC(message, title string, urgency int, id string) error {
137158
terminal := detect.DetectTerminal()
138159
protocol := detect.SelectProtocol(terminal)

main_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ func newTestCommand() *cobra.Command {
2121
forceNative = false
2222
forceBell = false
2323
showCaps = false
24+
exitCode = 0
25+
useExitCode = false
2426

2527
cmd := &cobra.Command{
2628
Use: "tnotify [message]",
@@ -38,6 +40,8 @@ func newTestCommand() *cobra.Command {
3840
cmd.Flags().BoolVar(&forceNative, "native", false, "Force native notifications only")
3941
cmd.Flags().BoolVar(&forceBell, "bell", false, "Send terminal bell only")
4042
cmd.Flags().BoolVar(&showCaps, "capabilities", false, "Show terminal capabilities as JSON")
43+
cmd.Flags().IntVarP(&exitCode, "exit-code", "e", 0, "Previous command's exit code")
44+
cmd.Flags().BoolVar(&useExitCode, "if-failed", false, "Only notify if exit code is non-zero")
4145

4246
return cmd
4347
}
@@ -263,3 +267,110 @@ func TestFlagMutualExclusion(t *testing.T) {
263267
})
264268
}
265269
}
270+
271+
func TestUrgencyFromExitCode(t *testing.T) {
272+
tests := []struct {
273+
name string
274+
exitCode int
275+
want int
276+
}{
277+
{"exit 0 is normal", 0, osc.UrgencyNormal},
278+
{"exit 1 is critical", 1, osc.UrgencyCritical},
279+
{"exit 2 is critical", 2, osc.UrgencyCritical},
280+
{"exit 127 is critical", 127, osc.UrgencyCritical},
281+
{"exit 255 is critical", 255, osc.UrgencyCritical},
282+
{"negative exit code is critical", -1, osc.UrgencyCritical},
283+
}
284+
285+
for _, tt := range tests {
286+
t.Run(tt.name, func(t *testing.T) {
287+
got := urgencyFromExitCode(tt.exitCode)
288+
if got != tt.want {
289+
t.Errorf("urgencyFromExitCode(%d) = %d, want %d", tt.exitCode, got, tt.want)
290+
}
291+
})
292+
}
293+
}
294+
295+
func TestExitCodeFlag(t *testing.T) {
296+
tests := []struct {
297+
name string
298+
args []string
299+
}{
300+
{"short flag", []string{"-e", "0", "msg"}},
301+
{"long flag", []string{"--exit-code", "1", "msg"}},
302+
{"with if-failed", []string{"-e", "1", "--if-failed", "msg"}},
303+
}
304+
305+
for _, tt := range tests {
306+
t.Run(tt.name, func(t *testing.T) {
307+
cmd := newTestCommand()
308+
cmd.SetArgs(tt.args)
309+
310+
err := cmd.ParseFlags(tt.args)
311+
if err != nil {
312+
t.Errorf("Failed to parse flags: %v", err)
313+
}
314+
})
315+
}
316+
}
317+
318+
func TestIfFailedSkipsOnSuccess(t *testing.T) {
319+
cmd := newTestCommand()
320+
buf := new(bytes.Buffer)
321+
cmd.SetOut(buf)
322+
cmd.SetErr(buf)
323+
cmd.SetArgs([]string{"-e", "0", "--if-failed", "This should not notify"})
324+
325+
// Capture stdout
326+
oldStdout := os.Stdout
327+
r, w, _ := os.Pipe()
328+
os.Stdout = w
329+
330+
err := cmd.Execute()
331+
332+
w.Close()
333+
os.Stdout = oldStdout
334+
335+
if err != nil {
336+
t.Fatalf("--if-failed with exit 0 returned error: %v", err)
337+
}
338+
339+
var stdout bytes.Buffer
340+
stdout.ReadFrom(r)
341+
342+
// Should produce no output (notification skipped)
343+
if stdout.String() != "" {
344+
t.Errorf("--if-failed with exit 0 should produce no output, got: %q", stdout.String())
345+
}
346+
}
347+
348+
func TestIfFailedNotifiesOnFailure(t *testing.T) {
349+
cmd := newTestCommand()
350+
buf := new(bytes.Buffer)
351+
cmd.SetOut(buf)
352+
cmd.SetErr(buf)
353+
cmd.SetArgs([]string{"-e", "1", "--if-failed", "--bell", "Build failed"})
354+
355+
// Capture stdout
356+
oldStdout := os.Stdout
357+
r, w, _ := os.Pipe()
358+
os.Stdout = w
359+
360+
err := cmd.Execute()
361+
362+
w.Close()
363+
os.Stdout = oldStdout
364+
365+
if err != nil {
366+
t.Fatalf("--if-failed with exit 1 returned error: %v", err)
367+
}
368+
369+
var stdout bytes.Buffer
370+
stdout.ReadFrom(r)
371+
372+
// Should produce bell output (notification sent)
373+
if stdout.String() != "\x07" {
374+
t.Errorf("--if-failed with exit 1 should produce bell, got: %q", stdout.String())
375+
}
376+
}

0 commit comments

Comments
 (0)