198 lines
5.8 KiB
Bash
198 lines
5.8 KiB
Bash
#!/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<AnyCancellable>()
|
||
_ = 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
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>Label</key>
|
||
<string>cn.org.cirrus.tesutoalarmdec2025</string>
|
||
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>${script_dir}/tesuto</string>
|
||
</array>
|
||
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
|
||
<key>KeepAlive</key>
|
||
<true/>
|
||
|
||
<key>StandardOutPath</key>
|
||
<string>${script_dir}/tesuto.log</string>
|
||
|
||
<key>StandardErrorPath</key>
|
||
<string>${script_dir}/tesuto.error.log</string>
|
||
</dict>
|
||
</plist>
|
||
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即可。" |