claude-code-remote-remake/src/channels/local/desktop.js

162 lines
6.2 KiB
JavaScript
Raw Normal View History

/**
* 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;