Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ This means you can:
- **meeting_detect** Detects if the microphone is currently being used and temporarily suppresses the audio only until the microphone is no longer in use. Notification still appears.
- **notification_position** (string, default: `"top-center"`): Where overlay notifications appear on screen. Options: `"top-left"`, `"top-center"`, `"top-right"`, `"bottom-left"`, `"bottom-center"`, `"bottom-right"`.
- **notification_dismiss_seconds** (number, default: `4`): Auto-dismiss overlay notifications after N seconds. Set to `0` for persistent notifications that require a click to dismiss.
- **notification_all_screens** (boolean, default: `true`): Show overlay notifications on all screens (`true`) or only the main screen (`false`). Themed overlays (`glass`, `jarvis`, `sakura`) previously only showed on one screen — existing configs with those themes are migrated to `false` automatically. macOS only.
- **`CLAUDE_SESSION_NAME` env var**: Set before launching `claude` to give a session a custom name. Shows in both desktop notification titles and terminal tab titles. Priority over all config-based naming. Example: `CLAUDE_SESSION_NAME="Auth Refactor" claude` or `export CLAUDE_SESSION_NAME="Feature: Auth"` then `claude`. Each terminal gets its own title automatically since peon-ping runs as a child of that Claude instance.
- **notification_title_override** (string, default: `""`): Override the project name shown in notification titles. When empty, the project name is auto-detected from `/peon-ping-rename` > `CLAUDE_SESSION_NAME` > `.peon-label` > `notification_title_script` > `project_name_map` > git repo name > folder name.
- **notification_title_script** (string, default: `""`): Shell command run at event time to compute the project name dynamically. Receives env vars: `PEON_SESSION_ID`, `PEON_CWD`, `PEON_HOOK_EVENT`, `PEON_SESSION_NAME`. Use stdout (trimmed, max 50 chars); non-zero exit falls through to the next tier. Example: `"basename $PEON_CWD"`.
Expand Down
1 change: 1 addition & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"suppress_sound_when_tab_focused": false,
"notification_position": "top-center",
"notification_dismiss_seconds": 4,
"notification_all_screens": true,
"notification_title_override": "",
"notification_title_script": "",
"project_name_map": {},
Expand Down
10 changes: 10 additions & 0 deletions peon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ send_notification() {
export PEON_NOTIF_STYLE="${NOTIF_STYLE:-overlay}"
export PEON_NOTIF_POSITION="${NOTIF_POSITION:-top-center}"
export PEON_NOTIF_DISMISS="${NOTIF_DISMISS:-4}"
export PEON_NOTIF_ALL_SCREENS="${NOTIF_ALL_SCREENS:-true}"
export PEON_DIR
export PEON_SYNC="0"
[ "${PEON_TEST:-0}" = "1" ] && export PEON_SYNC="1"
Expand Down Expand Up @@ -1360,10 +1361,12 @@ dn = cfg.get('desktop_notifications', True)
ns = cfg.get('notification_style', 'overlay')
np = cfg.get('notification_position', 'top-center')
nd = cfg.get('notification_dismiss_seconds', 4)
na = cfg.get('notification_all_screens', True)
print('_NOTIF_ENABLED=' + ('true' if dn else 'false'))
print('NOTIF_STYLE=' + q(ns))
print('NOTIF_POSITION=' + q(np))
print('NOTIF_DISMISS=' + q(str(nd)))
print('NOTIF_ALL_SCREENS=' + ('true' if na else 'false'))
")"
safe_eval_python "$_py_out" || true
if [ "$_NOTIF_ENABLED" != "true" ]; then
Expand Down Expand Up @@ -2714,6 +2717,12 @@ if 'debug_retention_days' not in cfg:
cfg['debug_retention_days'] = 7
changed = True
migrations.append('debug_retention_days')
if 'notification_all_screens' not in cfg:
_theme = cfg.get('overlay_theme', '')
# Default overlay always showed on all screens; themed overlays (glass/jarvis/sakura) only showed on the focused screen
cfg['notification_all_screens'] = _theme not in ('glass', 'jarvis', 'sakura')
changed = True
migrations.append('notification_all_screens')
if changed:
json.dump(cfg, open(config_path, 'w'), indent=2)
print('peon-ping: config keys updated (' + ', '.join(migrations) + ')')
Expand Down Expand Up @@ -4116,6 +4125,7 @@ print('DESKTOP_NOTIF=' + ('true' if desktop_notif else 'false'))
print('NOTIF_STYLE=' + q(cfg.get('notification_style', 'overlay')))
print('NOTIF_POSITION=' + q(cfg.get('notification_position', 'top-center')))
print('NOTIF_DISMISS=' + q(str(cfg.get('notification_dismiss_seconds', 4))))
print('NOTIF_ALL_SCREENS=' + ('true' if cfg.get('notification_all_screens', True) else 'false'))
print('USE_SOUND_EFFECTS_DEVICE=' + q(str(use_sound_effects_device).lower()))
print('LINUX_AUDIO_PLAYER=' + q(linux_audio_player))
print('PEON_SSH_AUDIO_MODE=' + q(str(cfg.get('ssh_audio_mode', 'relay'))))
Expand Down
60 changes: 49 additions & 11 deletions scripts/mac-overlay-glass.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function run(argv) {
var subtitle = argv[8] || ''; // Context subtitle (e.g. tool info, last message)
var position = argv[9] || 'top-center';
var notifType = argv[10] || ''; // Semantic type: complete|permission|limit|idle|question
var allScreens = argv[11] === 'true';
var screenIdx = (argv[12] !== undefined && argv[12] !== '') ? parseInt(argv[12], 10) : -1;

// ── Type text ──
var typeText;
Expand All @@ -44,20 +46,30 @@ function run(argv) {
$.NSApplication.sharedApplication;
$.NSApp.setActivationPolicy($.NSApplicationActivationPolicyAccessory);

// ── Screen detection: find screen where mouse cursor is ──
var mouseLocation = $.NSEvent.mouseLocation;
// Generate unique notification ID for all sibling overlays (all-screens mode)
var slotForNotification = parseInt(argv[3], 10) || 0;
var dismissNotificationName = 'com.peonping.dismiss.glass.' + slotForNotification;

// ── Screen detection ──
var screens = $.NSScreen.screens;
var focusedScreen = screens.objectAtIndex(0);
for (var s = 0; s < screens.count; s++) {
var scr = screens.objectAtIndex(s);
var sf = scr.frame;
if (mouseLocation.x >= sf.origin.x && mouseLocation.x <= sf.origin.x + sf.size.width &&
mouseLocation.y >= sf.origin.y && mouseLocation.y <= sf.origin.y + sf.size.height) {
focusedScreen = scr; break;
var targetScreen;
if (screenIdx >= 0 && screenIdx < screens.count) {
targetScreen = screens.objectAtIndex(screenIdx);
} else {
// Find screen where mouse cursor is
var mouseLocation = $.NSEvent.mouseLocation;
targetScreen = screens.objectAtIndex(0);
for (var s = 0; s < screens.count; s++) {
var scr = screens.objectAtIndex(s);
var sf = scr.frame;
if (mouseLocation.x >= sf.origin.x && mouseLocation.x <= sf.origin.x + sf.size.width &&
mouseLocation.y >= sf.origin.y && mouseLocation.y <= sf.origin.y + sf.size.height) {
targetScreen = scr; break;
}
}
}

var vf = focusedScreen.visibleFrame;
var vf = targetScreen.visibleFrame;
var margin = 10;
var slotStep = winH + 8;
var ySlotOffset = margin + slot * slotStep;
Expand Down Expand Up @@ -289,7 +301,12 @@ function run(argv) {
} catch(e) {}
}
}
$.NSApp.terminate(null);
// Signal ALL sibling overlays to dismiss (event-driven, no polling!)
$.NSDistributedNotificationCenter.defaultCenter.postNotificationNameObject($(dismissNotificationName), $.NSString.string);
// Small delay to ensure notification is delivered before we terminate
$.NSTimer.scheduledTimerWithTimeIntervalTargetSelectorUserInfoRepeats(
0.05, $.NSApp, 'terminate:', null, false
);
}}}
});
var dh = $.GlassDismissHandler.alloc.init;
Expand Down Expand Up @@ -341,5 +358,26 @@ function run(argv) {
dismiss + 0.3, $.NSApp, 'terminate:', null, false);
}

// Event-driven dismissal: observe distributed notifications from sibling overlays
ObjC.registerSubclass({
name: 'GlassDismissObserver',
superclass: 'NSObject',
methods: {
'handleDismiss:': {
types: ['void', ['id']],
implementation: function(notification) {
$.NSApp.terminate(null);
}
}
}
});
var glassObserver = $.GlassDismissObserver.alloc.init;
$.NSDistributedNotificationCenter.defaultCenter.addObserverSelectorNameObject(
glassObserver,
'handleDismiss:',
$(dismissNotificationName),
$.NSString.string
);

$.NSApp.run;
}
59 changes: 49 additions & 10 deletions scripts/mac-overlay-jarvis.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function run(argv) {
var subtitle = argv[8] || ''; // Context subtitle (e.g. tool info, last message)
var position = argv[9] || 'top-center';
var notifType = argv[10] || ''; // Semantic type: complete|permission|limit|idle|question
var allScreens = argv[11] === 'true';
var screenIdx = (argv[12] !== undefined && argv[12] !== '') ? parseInt(argv[12], 10) : -1;

var accentR = 0.0, accentG = 0.75, accentB = 1.0;
switch (color) {
Expand Down Expand Up @@ -51,19 +53,30 @@ function run(argv) {
$.NSApplication.sharedApplication;
$.NSApp.setActivationPolicy($.NSApplicationActivationPolicyAccessory);

var mouseLocation = $.NSEvent.mouseLocation;
// Generate unique notification ID for all sibling overlays (all-screens mode)
var slotForNotification = parseInt(argv[3], 10) || 0;
var dismissNotificationName = 'com.peonping.dismiss.jarvis.' + slotForNotification;

// ── Screen detection ──
var screens = $.NSScreen.screens;
var focusedScreen = screens.objectAtIndex(0);
for (var s = 0; s < screens.count; s++) {
var scr = screens.objectAtIndex(s);
var sf = scr.frame;
if (mouseLocation.x >= sf.origin.x && mouseLocation.x <= sf.origin.x + sf.size.width &&
mouseLocation.y >= sf.origin.y && mouseLocation.y <= sf.origin.y + sf.size.height) {
focusedScreen = scr; break;
var targetScreen;
if (screenIdx >= 0 && screenIdx < screens.count) {
targetScreen = screens.objectAtIndex(screenIdx);
} else {
// Find screen where mouse cursor is
var mouseLocation = $.NSEvent.mouseLocation;
targetScreen = screens.objectAtIndex(0);
for (var s = 0; s < screens.count; s++) {
var scr = screens.objectAtIndex(s);
var sf = scr.frame;
if (mouseLocation.x >= sf.origin.x && mouseLocation.x <= sf.origin.x + sf.size.width &&
mouseLocation.y >= sf.origin.y && mouseLocation.y <= sf.origin.y + sf.size.height) {
targetScreen = scr; break;
}
}
}

var vf = focusedScreen.visibleFrame;
var vf = targetScreen.visibleFrame;
var margin = 10;
var slotStep = winSize + 5;
var ySlotOffset = margin + slot * slotStep;
Expand Down Expand Up @@ -532,7 +545,12 @@ function run(argv) {
} catch(e) {}
}
}
$.NSApp.terminate(null);
// Signal ALL sibling overlays to dismiss (event-driven, no polling!)
$.NSDistributedNotificationCenter.defaultCenter.postNotificationNameObject($(dismissNotificationName), $.NSString.string);
// Small delay to ensure notification is delivered before we terminate
$.NSTimer.scheduledTimerWithTimeIntervalTargetSelectorUserInfoRepeats(
0.05, $.NSApp, 'terminate:', null, false
);
}}}
});
var dh=$.JarvisDismissHandler.alloc.init;
Expand Down Expand Up @@ -596,5 +614,26 @@ function run(argv) {
dismiss + 0.3, $.NSApp, 'terminate:', null, false);
}

// Event-driven dismissal: observe distributed notifications from sibling overlays
ObjC.registerSubclass({
name: 'JarvisDismissObserver',
superclass: 'NSObject',
methods: {
'handleDismiss:': {
types: ['void', ['id']],
implementation: function(notification) {
$.NSApp.terminate(null);
}
}
}
});
var jarvisObserver = $.JarvisDismissObserver.alloc.init;
$.NSDistributedNotificationCenter.defaultCenter.addObserverSelectorNameObject(
jarvisObserver,
'handleDismiss:',
$(dismissNotificationName),
$.NSString.string
);

$.NSApp.run;
}
60 changes: 49 additions & 11 deletions scripts/mac-overlay-sakura.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function run(argv) {
var subtitle = argv[8] || ''; // Context subtitle (e.g. tool info, last message)
var position = argv[9] || 'top-center';
var notifType = argv[10] || ''; // Semantic type: complete|permission|limit|idle|question
var allScreens = argv[11] === 'true';
var screenIdx = (argv[12] !== undefined && argv[12] !== '') ? parseInt(argv[12], 10) : -1;

var PI = Math.PI, TAU = 2 * PI;

Expand Down Expand Up @@ -46,20 +48,30 @@ function run(argv) {
$.NSApplication.sharedApplication;
$.NSApp.setActivationPolicy($.NSApplicationActivationPolicyAccessory);

// ── Screen detection: find screen where mouse cursor is ──
var mouseLocation = $.NSEvent.mouseLocation;
// Generate unique notification ID for all sibling overlays (all-screens mode)
var slotForNotification = parseInt(argv[3], 10) || 0;
var dismissNotificationName = 'com.peonping.dismiss.sakura.' + slotForNotification;

// ── Screen detection ──
var screens = $.NSScreen.screens;
var focusedScreen = screens.objectAtIndex(0);
for (var s = 0; s < screens.count; s++) {
var scr = screens.objectAtIndex(s);
var sf = scr.frame;
if (mouseLocation.x >= sf.origin.x && mouseLocation.x <= sf.origin.x + sf.size.width &&
mouseLocation.y >= sf.origin.y && mouseLocation.y <= sf.origin.y + sf.size.height) {
focusedScreen = scr; break;
var targetScreen;
if (screenIdx >= 0 && screenIdx < screens.count) {
targetScreen = screens.objectAtIndex(screenIdx);
} else {
// Find screen where mouse cursor is
var mouseLocation = $.NSEvent.mouseLocation;
targetScreen = screens.objectAtIndex(0);
for (var s = 0; s < screens.count; s++) {
var scr = screens.objectAtIndex(s);
var sf = scr.frame;
if (mouseLocation.x >= sf.origin.x && mouseLocation.x <= sf.origin.x + sf.size.width &&
mouseLocation.y >= sf.origin.y && mouseLocation.y <= sf.origin.y + sf.size.height) {
targetScreen = scr; break;
}
}
}

var vf = focusedScreen.visibleFrame;
var vf = targetScreen.visibleFrame;
var margin = 10;
var slotStep = winH + 8;
var ySlotOffset = margin + slot * slotStep;
Expand Down Expand Up @@ -474,7 +486,12 @@ function run(argv) {
} catch(e) {}
}
}
$.NSApp.terminate(null);
// Signal ALL sibling overlays to dismiss (event-driven, no polling!)
$.NSDistributedNotificationCenter.defaultCenter.postNotificationNameObject($(dismissNotificationName), $.NSString.string);
// Small delay to ensure notification is delivered before we terminate
$.NSTimer.scheduledTimerWithTimeIntervalTargetSelectorUserInfoRepeats(
0.05, $.NSApp, 'terminate:', null, false
);
}}}
});
var dh = $.SakuraDismissHandler.alloc.init;
Expand Down Expand Up @@ -539,5 +556,26 @@ function run(argv) {
dismiss + 0.3, $.NSApp, 'terminate:', null, false);
}

// Event-driven dismissal: observe distributed notifications from sibling overlays
ObjC.registerSubclass({
name: 'SakuraDismissObserver',
superclass: 'NSObject',
methods: {
'handleDismiss:': {
types: ['void', ['id']],
implementation: function(notification) {
$.NSApp.terminate(null);
}
}
}
});
var sakuraObserver = $.SakuraDismissObserver.alloc.init;
$.NSDistributedNotificationCenter.defaultCenter.addObserverSelectorNameObject(
sakuraObserver,
'handleDismiss:',
$(dismissNotificationName),
$.NSString.string
);

$.NSApp.run;
}
Loading
Loading