162 lines
6.2 KiB
JavaScript
162 lines
6.2 KiB
JavaScript
/**
|
|
* Desktop Notification Channel
|
|
* Sends notifications to the local desktop
|
|
*/
|
|
|
|
const NotificationChannel = require('../base/channel');
|
|
const { execSync, spawn } = require('child_process');
|
|
const path = require('path');
|
|
|
|
class DesktopChannel extends NotificationChannel {
|
|
constructor(config = {}) {
|
|
super('desktop', config);
|
|
this.platform = process.platform;
|
|
this.soundsDir = path.join(__dirname, '../../assets/sounds');
|
|
}
|
|
|
|
async _sendImpl(notification) {
|
|
const { title, message } = notification;
|
|
const sound = this._getSoundForType(notification.type);
|
|
|
|
switch (this.platform) {
|
|
case 'darwin':
|
|
return this._sendMacOS(title, message, sound);
|
|
case 'linux':
|
|
return this._sendLinux(title, message, sound);
|
|
case 'win32':
|
|
return this._sendWindows(title, message, sound);
|
|
default:
|
|
this.logger.warn(`Platform ${this.platform} not supported`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_getSoundForType(type) {
|
|
const soundMap = {
|
|
completed: this.config.completedSound || 'Glass',
|
|
waiting: this.config.waitingSound || 'Tink'
|
|
};
|
|
return soundMap[type] || 'Glass';
|
|
}
|
|
|
|
_sendMacOS(title, message, sound) {
|
|
try {
|
|
// Try terminal-notifier first
|
|
try {
|
|
const cmd = `terminal-notifier -title "${title}" -message "${message}" -sound "${sound}" -group "claude-code-remote"`;
|
|
execSync(cmd, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 });
|
|
return true;
|
|
} catch (e) {
|
|
// Fallback to osascript
|
|
const script = `display notification "${message}" with title "${title}"`;
|
|
execSync(`osascript -e '${script}'`, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 });
|
|
|
|
// Play sound separately
|
|
this._playSound(sound);
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
this.logger.error('macOS notification failed:', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_sendLinux(title, message, sound) {
|
|
try {
|
|
const notificationTimeout = parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000;
|
|
const displayTime = parseInt(process.env.NOTIFICATION_DISPLAY_TIME) || 10000;
|
|
execSync(`notify-send "${title}" "${message}" -t ${displayTime}`, { timeout: notificationTimeout });
|
|
this._playSound(sound);
|
|
return true;
|
|
} catch (error) {
|
|
this.logger.error('Linux notification failed:', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_sendWindows(title, message, sound) {
|
|
try {
|
|
const script = `
|
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
|
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
|
$xml = [xml] $template.GetXml()
|
|
$xml.toast.visual.binding.text[0].AppendChild($xml.CreateTextNode("${title}")) > $null
|
|
$xml.toast.visual.binding.text[1].AppendChild($xml.CreateTextNode("${message}")) > $null
|
|
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Claude-Code-Remote").Show($toast)
|
|
`;
|
|
|
|
execSync(`powershell -Command "${script}"`, { timeout: 5000 });
|
|
this._playSound(sound);
|
|
return true;
|
|
} catch (error) {
|
|
this.logger.error('Windows notification failed:', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_playSound(soundName) {
|
|
if (!soundName || soundName === 'default') return;
|
|
|
|
try {
|
|
if (this.platform === 'darwin') {
|
|
const soundPath = `/System/Library/Sounds/${soundName}.aiff`;
|
|
const audioProcess = spawn('afplay', [soundPath], {
|
|
detached: true,
|
|
stdio: 'ignore'
|
|
});
|
|
audioProcess.unref();
|
|
} else if (this.platform === 'linux') {
|
|
const soundPath = `/usr/share/sounds/freedesktop/stereo/${soundName.toLowerCase()}.oga`;
|
|
const audioProcess = spawn('paplay', [soundPath], {
|
|
detached: true,
|
|
stdio: 'ignore'
|
|
});
|
|
audioProcess.unref();
|
|
} else if (this.platform === 'win32') {
|
|
const audioProcess = spawn('powershell', ['-c', `[console]::beep(800,300)`], {
|
|
detached: true,
|
|
stdio: 'ignore'
|
|
});
|
|
audioProcess.unref();
|
|
}
|
|
} catch (error) {
|
|
this.logger.debug('Sound playback failed:', error.message);
|
|
}
|
|
}
|
|
|
|
validateConfig() {
|
|
// Desktop notifications don't require configuration
|
|
return true;
|
|
}
|
|
|
|
getAvailableSounds() {
|
|
const sounds = {
|
|
'System Sounds': ['Glass', 'Tink', 'Ping', 'Pop', 'Basso', 'Blow', 'Bottle',
|
|
'Frog', 'Funk', 'Hero', 'Morse', 'Purr', 'Sosumi', 'Submarine'],
|
|
'Alert Sounds': ['Beep', 'Boop', 'Sosumi', 'Tink', 'Glass'],
|
|
'Nature Sounds': ['Frog', 'Submarine'],
|
|
'Musical Sounds': ['Funk', 'Hero', 'Morse', 'Sosumi']
|
|
};
|
|
|
|
// Add custom sounds from assets directory
|
|
try {
|
|
const fs = require('fs');
|
|
if (fs.existsSync(this.soundsDir)) {
|
|
const customSounds = fs.readdirSync(this.soundsDir)
|
|
.filter(file => /\.(wav|mp3|m4a|aiff|ogg)$/i.test(file))
|
|
.map(file => path.basename(file, path.extname(file)));
|
|
|
|
if (customSounds.length > 0) {
|
|
sounds['Custom Sounds'] = customSounds;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.logger.debug('Failed to load custom sounds:', error.message);
|
|
}
|
|
|
|
return sounds;
|
|
}
|
|
}
|
|
|
|
module.exports = DesktopChannel; |