| import type { TerminalNotification } from '../ink/useTerminalNotification.js' |
| import { getGlobalConfig } from '../utils/config.js' |
| import { env } from '../utils/env.js' |
| import { execFileNoThrow } from '../utils/execFileNoThrow.js' |
| import { executeNotificationHooks } from '../utils/hooks.js' |
| import { logError } from '../utils/log.js' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from './analytics/index.js' |
|
|
| export type NotificationOptions = { |
| message: string |
| title?: string |
| notificationType: string |
| } |
|
|
| export async function sendNotification( |
| notif: NotificationOptions, |
| terminal: TerminalNotification, |
| ): Promise<void> { |
| const config = getGlobalConfig() |
| const channel = config.preferredNotifChannel |
|
|
| await executeNotificationHooks(notif) |
|
|
| const methodUsed = await sendToChannel(channel, notif, terminal) |
|
|
| logEvent('tengu_notification_method_used', { |
| configured_channel: |
| channel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| method_used: |
| methodUsed as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| term: env.terminal as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| } |
|
|
| const DEFAULT_TITLE = 'Claude Code' |
|
|
| async function sendToChannel( |
| channel: string, |
| opts: NotificationOptions, |
| terminal: TerminalNotification, |
| ): Promise<string> { |
| const title = opts.title || DEFAULT_TITLE |
|
|
| try { |
| switch (channel) { |
| case 'auto': |
| return sendAuto(opts, terminal) |
| case 'iterm2': |
| terminal.notifyITerm2(opts) |
| return 'iterm2' |
| case 'iterm2_with_bell': |
| terminal.notifyITerm2(opts) |
| terminal.notifyBell() |
| return 'iterm2_with_bell' |
| case 'kitty': |
| terminal.notifyKitty({ ...opts, title, id: generateKittyId() }) |
| return 'kitty' |
| case 'ghostty': |
| terminal.notifyGhostty({ ...opts, title }) |
| return 'ghostty' |
| case 'terminal_bell': |
| terminal.notifyBell() |
| return 'terminal_bell' |
| case 'notifications_disabled': |
| return 'disabled' |
| default: |
| return 'none' |
| } |
| } catch { |
| return 'error' |
| } |
| } |
|
|
| async function sendAuto( |
| opts: NotificationOptions, |
| terminal: TerminalNotification, |
| ): Promise<string> { |
| const title = opts.title || DEFAULT_TITLE |
|
|
| switch (env.terminal) { |
| case 'Apple_Terminal': { |
| const bellDisabled = await isAppleTerminalBellDisabled() |
| if (bellDisabled) { |
| terminal.notifyBell() |
| return 'terminal_bell' |
| } |
| return 'no_method_available' |
| } |
| case 'iTerm.app': |
| terminal.notifyITerm2(opts) |
| return 'iterm2' |
| case 'kitty': |
| terminal.notifyKitty({ ...opts, title, id: generateKittyId() }) |
| return 'kitty' |
| case 'ghostty': |
| terminal.notifyGhostty({ ...opts, title }) |
| return 'ghostty' |
| default: |
| return 'no_method_available' |
| } |
| } |
|
|
| function generateKittyId(): number { |
| return Math.floor(Math.random() * 10000) |
| } |
|
|
| async function isAppleTerminalBellDisabled(): Promise<boolean> { |
| try { |
| if (env.terminal !== 'Apple_Terminal') { |
| return false |
| } |
|
|
| const osascriptResult = await execFileNoThrow('osascript', [ |
| '-e', |
| 'tell application "Terminal" to name of current settings of front window', |
| ]) |
| const currentProfile = osascriptResult.stdout.trim() |
|
|
| if (!currentProfile) { |
| return false |
| } |
|
|
| const defaultsOutput = await execFileNoThrow('defaults', [ |
| 'export', |
| 'com.apple.Terminal', |
| '-', |
| ]) |
|
|
| if (defaultsOutput.code !== 0) { |
| return false |
| } |
|
|
| |
| |
| const plist = await import('plist') |
| const parsed: Record<string, unknown> = plist.parse(defaultsOutput.stdout) |
| const windowSettings = parsed?.['Window Settings'] as |
| | Record<string, unknown> |
| | undefined |
| const profileSettings = windowSettings?.[currentProfile] as |
| | Record<string, unknown> |
| | undefined |
|
|
| if (!profileSettings) { |
| return false |
| } |
|
|
| return profileSettings.Bell === false |
| } catch (error) { |
| logError(error) |
| return false |
| } |
| } |
|
|