source dump of claude code
at main 156 lines 4.3 kB view raw
1import type { TerminalNotification } from '../ink/useTerminalNotification.js' 2import { getGlobalConfig } from '../utils/config.js' 3import { env } from '../utils/env.js' 4import { execFileNoThrow } from '../utils/execFileNoThrow.js' 5import { executeNotificationHooks } from '../utils/hooks.js' 6import { logError } from '../utils/log.js' 7import { 8 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 9 logEvent, 10} from './analytics/index.js' 11 12export type NotificationOptions = { 13 message: string 14 title?: string 15 notificationType: string 16} 17 18export async function sendNotification( 19 notif: NotificationOptions, 20 terminal: TerminalNotification, 21): Promise<void> { 22 const config = getGlobalConfig() 23 const channel = config.preferredNotifChannel 24 25 await executeNotificationHooks(notif) 26 27 const methodUsed = await sendToChannel(channel, notif, terminal) 28 29 logEvent('tengu_notification_method_used', { 30 configured_channel: 31 channel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 32 method_used: 33 methodUsed as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 34 term: env.terminal as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 35 }) 36} 37 38const DEFAULT_TITLE = 'Claude Code' 39 40async function sendToChannel( 41 channel: string, 42 opts: NotificationOptions, 43 terminal: TerminalNotification, 44): Promise<string> { 45 const title = opts.title || DEFAULT_TITLE 46 47 try { 48 switch (channel) { 49 case 'auto': 50 return sendAuto(opts, terminal) 51 case 'iterm2': 52 terminal.notifyITerm2(opts) 53 return 'iterm2' 54 case 'iterm2_with_bell': 55 terminal.notifyITerm2(opts) 56 terminal.notifyBell() 57 return 'iterm2_with_bell' 58 case 'kitty': 59 terminal.notifyKitty({ ...opts, title, id: generateKittyId() }) 60 return 'kitty' 61 case 'ghostty': 62 terminal.notifyGhostty({ ...opts, title }) 63 return 'ghostty' 64 case 'terminal_bell': 65 terminal.notifyBell() 66 return 'terminal_bell' 67 case 'notifications_disabled': 68 return 'disabled' 69 default: 70 return 'none' 71 } 72 } catch { 73 return 'error' 74 } 75} 76 77async function sendAuto( 78 opts: NotificationOptions, 79 terminal: TerminalNotification, 80): Promise<string> { 81 const title = opts.title || DEFAULT_TITLE 82 83 switch (env.terminal) { 84 case 'Apple_Terminal': { 85 const bellDisabled = await isAppleTerminalBellDisabled() 86 if (bellDisabled) { 87 terminal.notifyBell() 88 return 'terminal_bell' 89 } 90 return 'no_method_available' 91 } 92 case 'iTerm.app': 93 terminal.notifyITerm2(opts) 94 return 'iterm2' 95 case 'kitty': 96 terminal.notifyKitty({ ...opts, title, id: generateKittyId() }) 97 return 'kitty' 98 case 'ghostty': 99 terminal.notifyGhostty({ ...opts, title }) 100 return 'ghostty' 101 default: 102 return 'no_method_available' 103 } 104} 105 106function generateKittyId(): number { 107 return Math.floor(Math.random() * 10000) 108} 109 110async function isAppleTerminalBellDisabled(): Promise<boolean> { 111 try { 112 if (env.terminal !== 'Apple_Terminal') { 113 return false 114 } 115 116 const osascriptResult = await execFileNoThrow('osascript', [ 117 '-e', 118 'tell application "Terminal" to name of current settings of front window', 119 ]) 120 const currentProfile = osascriptResult.stdout.trim() 121 122 if (!currentProfile) { 123 return false 124 } 125 126 const defaultsOutput = await execFileNoThrow('defaults', [ 127 'export', 128 'com.apple.Terminal', 129 '-', 130 ]) 131 132 if (defaultsOutput.code !== 0) { 133 return false 134 } 135 136 // Lazy-load plist (~280KB with xmlbuilder+@xmldom) — only hit on 137 // Apple_Terminal with auto-channel, which is a small fraction of users. 138 const plist = await import('plist') 139 const parsed: Record<string, unknown> = plist.parse(defaultsOutput.stdout) 140 const windowSettings = parsed?.['Window Settings'] as 141 | Record<string, unknown> 142 | undefined 143 const profileSettings = windowSettings?.[currentProfile] as 144 | Record<string, unknown> 145 | undefined 146 147 if (!profileSettings) { 148 return false 149 } 150 151 return profileSettings.Bell === false 152 } catch (error) { 153 logError(error) 154 return false 155 } 156}