#!/bin/bash username=$(whoami) script_dir=$(dirname "$0") cd $script_dir echo "==Tesuto Alarm Dec 2025(By Cirrus)安装程序==" echo "当前用户: $username" echo "当前目录: $script_dir" echo "" echo "检测程序是否正在运行:" if launchctl list | grep -q cn.org.cirrus.tesutoalarmdec2025;then echo "在运行,先终止进程,再安装。" echo "" launchctl stop cn.org.cirrus.tesutoalarmdec2025 launchctl unload /Users/$username/Library/LaunchAgents/cn.org.cirrus.tesutoalarmdec2025.plist echo "" echo "继续安装。" else echo "不在运行,继续安装。" fi echo "" echo "提示:程序文件默认保存目录:" echo "二进制文件:${script_dir}/tesuto" echo "STDOUT目录:${script_dir}/tesuto.log" echo "STDERR目录:${script_dir}/tesuto.error.log" echo "注意:请勿重命名、移动、修改、删除。" echo "" echo "请输入声音文件的绝对路径:(直接回车表示$script_dir/sound.mp3)" read sound if [ "$sound" == "" ];then sound="${script_dir}/sound.mp3" fi echo "注意:为保证程序运行正常,请保持该文件在原目录,且未重命名。" echo "" echo "请输入闹铃JSON文件的保存文件夹目录:(以“/”结尾,请勿加上alarms.json)" echo "(直接回车表示$script_dir/)" read jsonpath if [ "$jsonpath" == "" ];then jsonpath="${script_dir}/" fi cat > ${jsonpath}alarms.json << EOF [ { "name": "Test1", "hour": 8, "minute": 45, "triggered": false }, { "name": "Test2", "hour": 9, "minute": 41, "triggered": false } ] EOF cat > ./tesuto.swift << EOF import UniformTypeIdentifiers import SwiftUI import Combine class Alarm: Codable, ObservableObject { let name: String let hour: Int let minute: Int var triggered: Bool } let app = NSApplication.shared NSApp.setActivationPolicy(.regular) let sound = NSSound(contentsOf: URL(fileURLWithPath: ("$sound" as NSString).expandingTildeInPath), byReference: false) if sound != nil { print("\(Date()): 读取声音文件成功") print("尝试播放:(2秒后停止)") fflush(stdout) sound?.play() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { sound?.stop() } } else { print("\(Date()): 读取声音文件失败") fflush(stdout) } var alarms: [Alarm] = [] func readJSON(url: URL) -> Bool { do { let json = try Data(contentsOf: url) let loadedData = try JSONDecoder().decode([Alarm].self, from: json) alarms = loadedData print("\(Date()): 读取JSON成功") print("闹钟数据:") for alarm in alarms { print("\n名称:\(alarm.name)") print("时间:\(alarm.hour):\(alarm.minute)") } fflush(stdout) return true } catch { print("\(Date()): 读取JSON失败(\(error))") fflush(stdout) return false } } func perform(for alarm: Alarm) { let window = NSAlert() window.messageText = "闹钟提醒" window.informativeText = "\(alarm.name) - \(alarm.hour):\(alarm.minute)" window.addButton(withTitle: "关闭\(alarm.name)") print("\(Date()): 触发\(alarm.name) - \(alarm.hour):\(alarm.minute)") fflush(stdout) sound?.play() Task { try await Task.sleep(nanoseconds: 1 * 60 * 1_000_000_000) sound?.stop() print("\(Date()): 超时自动关闭\(alarm.name) - \(alarm.hour):\(alarm.minute)") fflush(stdout) return } window.runModal() sound?.stop() print("\(Date()): 手动关闭\(alarm.name) - \(alarm.hour):\(alarm.minute)") fflush(stdout) } let timer = Timer.publish(every: 1, on: .main, in: .default).autoconnect() var cancellables = Set() _ = readJSON(url: URL(fileURLWithPath: ("${jsonpath}alarms.json" as NSString).expandingTildeInPath)) timer.sink { _ in let now = Date() let calendar = Calendar.current let currentSecond = calendar.component(.second, from: now) if currentSecond < 3 { let currentHour = calendar.component(.hour, from: now) let currentMinute = calendar.component(.minute, from: now) for alarm in alarms { if alarm.hour == currentHour && alarm.minute == currentMinute && !alarm.triggered { perform(for: alarm) alarm.triggered.toggle() DispatchQueue.main.asyncAfter(deadline: .now() + 5) { alarm.triggered.toggle() print("\(Date()): 清除\(alarm.name)的triggered标识") fflush(stdout) } } } } } .store(in: &cancellables) RunLoop.current.run() EOF swiftc tesuto.swift cat > ./cn.org.cirrus.tesutoalarmdec2025.plist << EOF Label cn.org.cirrus.tesutoalarmdec2025 ProgramArguments ${script_dir}/tesuto RunAtLoad KeepAlive StandardOutPath ${script_dir}/tesuto.log StandardErrorPath ${script_dir}/tesuto.error.log EOF cp ./cn.org.cirrus.tesutoalarmdec2025.plist /Users/$username/Library/LaunchAgents/cn.org.cirrus.tesutoalarmdec2025.plist launchctl load /Users/$username/Library/LaunchAgents/cn.org.cirrus.tesutoalarmdec2025.plist echo "" echo "如果听到您指定音频文件的开头2秒,则表明程序安装完成。" echo "详见cirrus.log。" echo "更改时区代码较为麻烦,故请您将就看,把每个时间的小时数+8即可。"