Codex/calendar (#4)

* feat(calendar): 新增菜单栏日历与黄历详情
This commit is contained in:
缘生 authored and GitHub committed 2026-09-23 09:00:05 +08:00
1 parent a0a15a69fd
commit aa8228fac8
30 files changed
+1961 -2

No files matched your search

+25
View File
@@ -167,6 +167,31 @@ an empty compact toolbar, so the traffic lights sit on the same ground colour as
line up with the 40 pt page header; the app switches to a regular activation policy while a window
is open and back to accessory when all are closed.
## Calendar
`CalendarMenuBarController` owns a separate, opt-in status item, independent of the metrics
layout and `MetricsHub`. It reuses `StatusPanel`, updates the displayed date every minute and
on day/time-zone changes or wake, and stops its timer when disabled. `CalendarPopover` uses a
six-week Gregorian grid (1900–2100) and month/year navigation. Clicking a day opens an almanac
page in the same panel; returning preserves the selected date and month. `CalendarAlmanac`
loads Tyme's daily advice, deities and hour fortunes only for the selected day and caches the
value model by civil-date ID. The twelve-hour grid uses early Zi through Hai, excluding the
next day's late Zi entry at 23:00; each cell exposes its exact time range in a tooltip. Browsing
history preserves the selection across midnight; a selection on today follows the new day.
`CalendarEngine` is the sole adapter for the pinned MIT-licensed Tyme4Swift 1.5.0 dependency.
All Tyme calls run on MainActor because upstream exposes mutable static tables; only value
models leave the adapter. Civil dates use the system time zone and calendar day arithmetic,
not 24-hour intervals. Tibetan conversion is guarded to the upstream Gregorian coverage
1951-01-08 through 2051-02-11. Holiday data currently ends in 2026; unknown years show a notice.
Plum-rain dates describe traditional calendar rules, not observed or forecast weather.
Calendar enablement, visible features and week start persist through `AppSettings` and the
optional fields in `SettingsDocument`. Older backups preserve those settings. Additional
calendars default off and appear only in selected-day details. There is no event access or
network request at runtime. Use `--snapshot <directory> --calendar-only` for deterministic
light/dark calendar screenshots without starting unrelated samplers or scans.
## AI usage statistics
The active Codex provider reads local JSONL rollouts from CODEX_HOME (default ~/.codex),
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 6tail
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5 -1
View File
@@ -16,6 +16,9 @@ let package = Package(
.library(name: "WebDAVSync", targets: ["WebDAVSync"]),
.library(name: "XStatsUI", targets: ["XStatsUI"]),
],
dependencies: [
.package(url: "https://github.com/6tail/tyme4swift.git", exact: "1.5.0"),
],
targets: [
.target(name: "Localization", resources: [.process("Resources")]),
.target(name: "AIUsage"),
@@ -25,7 +28,8 @@ let package = Package(
.target(name: "Cleaner", dependencies: ["Localization"]),
.target(name: "Updates", dependencies: ["Localization"]),
.target(name: "WebDAVSync", dependencies: ["Localization"]),
.target(name: "XStatsUI", dependencies: ["AIUsage", "Localization", "Metrics", "SMC", "HelperShared", "Cleaner", "Updates", "WebDAVSync"],
.target(name: "XStatsUI", dependencies: ["AIUsage", "Localization", "Metrics", "SMC", "HelperShared", "Cleaner", "Updates", "WebDAVSync",
.product(name: "Tyme4Swift", package: "tyme4swift")],
resources: [.copy("Resources/Flags"), .copy("Resources/Logos")]),
.testTarget(name: "MetricsTests", dependencies: ["Metrics", "SMC"]),
.testTarget(name: "CleanerTests", dependencies: ["Cleaner"]),
@@ -0,0 +1,96 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
extension Translations {
static let calendarEnglish = #"""
日历 Calendar
菜单栏日历 Menu Bar Calendar
日历设置… Calendar Settings…
独立显示日期,点击打开月历;不受指标合并布局影响 Show a separate date item; click to open the calendar, even when metrics are combined
每周开始于 First day of week
星期一 Monday
星期日 Sunday
农历 Lunar calendar
星期 Weekdays
法定节假日与调休 Holidays and makeup workdays
传统与公历节日 Festivals
二十四节气 Solar terms
干支 Sexagenary cycle
三伏天 Dog days
梅雨天 Plum-rain days
藏历 Tibetan calendar
回历 Hijri calendar
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Gregorian dates stay visible. Tibetan and Hijri dates appear in day details. Plum-rain days follow traditional calendar rules, not weather forecasts.
该日期暂无藏历数据 Tibetan calendar data is unavailable for this date
该年份暂无中国法定假日与调休数据 Chinese holiday and makeup workday data is unavailable for this year
按传统历法推算,并非天气预报 Traditional calendar calculation, not a weather forecast
年份 Year
月份 Month
上个月 Previous month
下个月 Next month
班 W
休 R
调休上班 Makeup workday
放假 Holiday
春节 Spring Festival
元宵节 Lantern Festival
龙头节 Dragon Head-Raising Festival
上巳节 Shangsi Festival
清明节 Qingming Festival
端午节 Dragon Boat Festival
七夕节 Qixi Festival
中元节 Ghost Festival
中秋节 Mid-Autumn Festival
重阳节 Double Ninth Festival
冬至节 Winter Solstice Festival
腊八节 Laba Festival
除夕 Lunar New Year's Eve
元旦 New Year's Day
劳动节 Labour Day
国庆节 National Day
国庆中秋 National Day / Mid-Autumn
抗战胜利日 Victory Day
立春 Start of Spring
雨水 Rain Water
惊蛰 Awakening of Insects
春分 Spring Equinox
清明 Pure Brightness
谷雨 Grain Rain
立夏 Start of Summer
小满 Grain Buds
芒种 Grain in Ear
夏至 Summer Solstice
小暑 Minor Heat
大暑 Major Heat
立秋 Start of Autumn
处暑 End of Heat
白露 White Dew
秋分 Autumn Equinox
寒露 Cold Dew
霜降 Frost's Descent
立冬 Start of Winter
小雪 Minor Snow
大雪 Major Snow
冬至 Winter Solstice
小寒 Minor Cold
大寒 Major Cold
返回月历 Back to calendar
控制月历格中的日干支;黄历详情始终显示年月日干支 Show the day cycle in month cells; almanac details always include year, month and day cycles
宜 Favorable
忌 Avoid
纳音 Na Yin
冲煞 Clash / Sha
冲{} 煞{} Clash: {} · Sha: {}
值神 Day deity
建除十二神 Twelve officers
吉神宜趋 Auspicious deities
今日胎神 Fetal deity
凶神宜忌 Inauspicious deities
彭祖百忌 Peng Zu taboos
二十八星宿 Lunar mansions
时辰吉凶 Hour fortunes
子时显示当日早子时;23 点起的晚子时按次日干支计算 Zi shows 00:00–00:59; late Zi from 23:00 uses the next day's cycle
吉 Lucky
凶 Unlucky
"""#
}
@@ -1247,3 +1247,92 @@ Token 活动 نشاط الرموز
每周 أسبوعي
{} 当周:{} Tokens أسبوع {}: {} Tokens
截至 {} 当周累计:{} Tokens الإجمالي حتى أسبوع {}: {} Tokens
日历 التقويم
菜单栏日历 تقويم شريط القوائم
日历设置… إعدادات التقويم…
独立显示日期,点击打开月历;不受指标合并布局影响 عرض التاريخ بشكل مستقل؛ انقر لفتح التقويم حتى عند دمج المؤشرات
每周开始于 أول أيام الأسبوع
星期一 الاثنين
星期日 الأحد
农历 التقويم القمري الصيني
星期 أيام الأسبوع
法定节假日与调休 العطلات وأيام العمل التعويضية
传统与公历节日 المناسبات التقليدية والميلادية
二十四节气 الفصول الشمسية الأربعة والعشرون
干支 الدورة الستينية
三伏天 أيام القيظ
梅雨天 أيام أمطار البرقوق التقليدية
藏历 التقويم التبتي
回历 التقويم الهجري
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 تظل التواريخ الميلادية ظاهرة. تظهر التواريخ التبتية والهجرية في تفاصيل اليوم. تُحسب أمطار البرقوق وفق القواعد التقليدية وليست توقعات جوية.
该日期暂无藏历数据 لا تتوفر بيانات التقويم التبتي لهذا التاريخ
该年份暂无中国法定假日与调休数据 لا تتوفر بيانات العطلات الصينية وأيام العمل التعويضية لهذه السنة
按传统历法推算,并非天气预报 حساب وفق التقويم التقليدي، وليس توقعًا جويًا
年份 السنة
月份 الشهر
上个月 الشهر السابق
下个月 الشهر التالي
班 ع
休 ر
调休上班 يوم عمل تعويضي
放假 عطلة
春节 عيد الربيع
元宵节 عيد الفوانيس
龙头节 مهرجان رأس التنين
上巳节 مهرجان شانغسي
清明节 عيد تشينغمينغ
端午节 عيد قوارب التنين
七夕节 عيد تشيشي
中元节 عيد الأشباح
中秋节 عيد منتصف الخريف
重阳节 عيد التسعة المزدوجة
冬至节 عيد الانقلاب الشتوي
腊八节 عيد لابا
除夕 ليلة رأس السنة القمرية
元旦 رأس السنة
劳动节 عيد العمال
国庆节 العيد الوطني
国庆中秋 العيد الوطني / منتصف الخريف
抗战胜利日 يوم النصر
立春 بداية الربيع
雨水 ماء المطر
惊蛰 استيقاظ الحشرات
春分 الاعتدال الربيعي
清明 الصفاء النقي
谷雨 مطر الحبوب
立夏 بداية الصيف
小满 الامتلاء الصغير
芒种 الحبوب في السنابل
夏至 الانقلاب الصيفي
小暑 الحر الصغير
大暑 الحر الكبير
立秋 بداية الخريف
处暑 نهاية الحر
白露 الندى الأبيض
秋分 الاعتدال الخريفي
寒露 الندى البارد
霜降 نزول الصقيع
立冬 بداية الشتاء
小雪 الثلج الصغير
大雪 الثلج الكبير
冬至 الانقلاب الشتوي
小寒 البرد الصغير
大寒 البرد الكبير
返回月历 العودة إلى التقويم
控制月历格中的日干支;黄历详情始终显示年月日干支 عرض دورة اليوم في خلايا الشهر؛ تتضمن تفاصيل التقويم التقليدي دائمًا دورات السنة والشهر واليوم
宜 مستحب
忌 يُتجنّب
纳音 نا يين
冲煞 التعارض / شا
冲{} 煞{} التعارض: {} · شا: {}
值神 حارس اليوم
建除十二神 الحراس الاثنا عشر
吉神宜趋 حراس الحظ الحسن
今日胎神 حارس الجنين
凶神宜忌 حراس النحس
彭祖百忌 محظورات بنغ تسو
二十八星宿 منازل القمر
时辰吉凶 حظ الساعات
子时显示当日早子时;23 点起的晚子时按次日干支计算 تُعرض فترة زي من 00:00 إلى 00:59؛ ومن 23:00 تُستخدم دورة اليوم التالي
吉 حسن
凶 سيئ
1 # Copyright (C) 2026 ysicing
1247 每周
1248 {} 当周:{} Tokens
1249 截至 {} 当周累计:{} Tokens
1250 日历
1251 菜单栏日历
1252 日历设置…
1253 独立显示日期,点击打开月历;不受指标合并布局影响
1254 每周开始于
1255 星期一
1256 星期日
1257 农历
1258 星期
1259 法定节假日与调休
1260 传统与公历节日
1261 二十四节气
1262 干支
1263 三伏天
1264 梅雨天
1265 藏历
1266 回历
1267 公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。
1268 该日期暂无藏历数据
1269 该年份暂无中国法定假日与调休数据
1270 按传统历法推算,并非天气预报
1271 年份
1272 月份
1273 上个月
1274 下个月
1275 班
1276 休
1277 调休上班
1278 放假
1279 春节
1280 元宵节
1281 龙头节
1282 上巳节
1283 清明节
1284 端午节
1285 七夕节
1286 中元节
1287 中秋节
1288 重阳节
1289 冬至节
1290 腊八节
1291 除夕
1292 元旦
1293 劳动节
1294 国庆节
1295 国庆中秋
1296 抗战胜利日
1297 立春
1298 雨水
1299 惊蛰
1300 春分
1301 清明
1302 谷雨
1303 立夏
1304 小满
1305 芒种
1306 夏至
1307 小暑
1308 大暑
1309 立秋
1310 处暑
1311 白露
1312 秋分
1313 寒露
1314 霜降
1315 立冬
1316 小雪
1317 大雪
1318 冬至
1319 小寒
1320 大寒
1321 返回月历
1322 控制月历格中的日干支;黄历详情始终显示年月日干支
1323 宜
1324 忌
1325 纳音
1326 冲煞
1327 冲{} 煞{}
1328 值神
1329 建除十二神
1330 吉神宜趋
1331 今日胎神
1332 凶神宜忌
1333 彭祖百忌
1334 二十八星宿
1335 时辰吉凶
1336 子时显示当日早子时;23 点起的晚子时按次日干支计算
1337 吉
1338 凶
@@ -1247,3 +1247,92 @@ Token 活动 Token-Aktivität
每周 Wöchentlich
{} 当周:{} Tokens Woche vom {}: {} Tokens
截至 {} 当周累计:{} Tokens Bis zur Woche vom {}: {} Tokens kumuliert
日历 Kalender
菜单栏日历 Menüleistenkalender
日历设置… Kalendereinstellungen…
独立显示日期,点击打开月历;不受指标合并布局影响 Separates Datum; ein Klick öffnet den Kalender, auch bei kombinierten Messwerten
每周开始于 Wochenbeginn
星期一 Montag
星期日 Sonntag
农历 Chinesischer Mondkalender
星期 Wochentage
法定节假日与调休 Feiertage und Ausgleichsarbeitstage
传统与公历节日 Traditionelle und gregorianische Feste
二十四节气 24 Sonnenperioden
干支 Sechzigjahreszyklus
三伏天 Hundstage
梅雨天 Traditionelle Pflaumenregenzeit
藏历 Tibetischer Kalender
回历 Hidschri-Kalender
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Gregorianische Daten bleiben sichtbar. Tibetische und Hidschri-Daten stehen in den Tagesdetails. Die Pflaumenregenzeit folgt traditionellen Kalenderregeln, nicht Wettervorhersagen.
该日期暂无藏历数据 Für dieses Datum sind keine tibetischen Kalenderdaten verfügbar
该年份暂无中国法定假日与调休数据 Für dieses Jahr fehlen chinesische Feiertags- und Ausgleichsarbeitstagsdaten
按传统历法推算,并非天气预报 Traditionelle Kalenderberechnung, keine Wettervorhersage
年份 Jahr
月份 Monat
上个月 Vorheriger Monat
下个月 Nächster Monat
班 A
休 F
调休上班 Ausgleichsarbeitstag
放假 Feiertag
春节 Frühlingsfest
元宵节 Laternenfest
龙头节 Longtaitou
上巳节 Shangsi-Fest
清明节 Qingming-Fest
端午节 Drachenbootfest
七夕节 Qixi-Fest
中元节 Geisterfest
中秋节 Mittherbstfest
重阳节 Doppelneunfest
冬至节 Wintersonnenwendfest
腊八节 Laba-Fest
除夕 Mondneujahrsabend
元旦 Neujahr
劳动节 Tag der Arbeit
国庆节 Nationalfeiertag
国庆中秋 Nationalfeiertag / Mittherbstfest
抗战胜利日 Tag des Sieges
立春 Frühlingsbeginn
雨水 Regenwasser
惊蛰 Erwachen der Insekten
春分 Frühlings-Tagundnachtgleiche
清明 Helle Klarheit
谷雨 Getreideregen
立夏 Sommerbeginn
小满 Kleine Fülle
芒种 Ährensaat
夏至 Sommersonnenwende
小暑 Kleine Hitze
大暑 Große Hitze
立秋 Herbstbeginn
处暑 Ende der Hitze
白露 Weißer Tau
秋分 Herbst-Tagundnachtgleiche
寒露 Kalter Tau
霜降 Reifbeginn
立冬 Winterbeginn
小雪 Kleiner Schnee
大雪 Großer Schnee
冬至 Wintersonnenwende
小寒 Kleine Kälte
大寒 Große Kälte
返回月历 Zurück zum Kalender
控制月历格中的日干支;黄历详情始终显示年月日干支 Tageszyklus in Monatszellen anzeigen; Almanachdetails enthalten immer Jahres-, Monats- und Tageszyklen
宜 Günstig
忌 Meiden
纳音 Na Yin
冲煞 Konflikt / Sha
冲{} 煞{} Konflikt: {} · Sha: {}
值神 Tagesgottheit
建除十二神 Zwölf Tagesämter
吉神宜趋 Günstige Gottheiten
今日胎神 Fötusgottheit
凶神宜忌 Ungünstige Gottheiten
彭祖百忌 Peng-Zu-Tabus
二十八星宿 Mondhäuser
时辰吉凶 Stundenglück
子时显示当日早子时;23 点起的晚子时按次日干支计算 Zi zeigt 00:00–00:59; ab 23:00 gilt der Zyklus des Folgetags
吉 Glück
凶 Unglück
1 # Copyright (C) 2026 ysicing
1247 每周
1248 {} 当周:{} Tokens
1249 截至 {} 当周累计:{} Tokens
1250 日历
1251 菜单栏日历
1252 日历设置…
1253 独立显示日期,点击打开月历;不受指标合并布局影响
1254 每周开始于
1255 星期一
1256 星期日
1257 农历
1258 星期
1259 法定节假日与调休
1260 传统与公历节日
1261 二十四节气
1262 干支
1263 三伏天
1264 梅雨天
1265 藏历
1266 回历
1267 公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。
1268 该日期暂无藏历数据
1269 该年份暂无中国法定假日与调休数据
1270 按传统历法推算,并非天气预报
1271 年份
1272 月份
1273 上个月
1274 下个月
1275 班
1276 休
1277 调休上班
1278 放假
1279 春节
1280 元宵节
1281 龙头节
1282 上巳节
1283 清明节
1284 端午节
1285 七夕节
1286 中元节
1287 中秋节
1288 重阳节
1289 冬至节
1290 腊八节
1291 除夕
1292 元旦
1293 劳动节
1294 国庆节
1295 国庆中秋
1296 抗战胜利日
1297 立春
1298 雨水
1299 惊蛰
1300 春分
1301 清明
1302 谷雨
1303 立夏
1304 小满
1305 芒种
1306 夏至
1307 小暑
1308 大暑
1309 立秋
1310 处暑
1311 白露
1312 秋分
1313 寒露
1314 霜降
1315 立冬
1316 小雪
1317 大雪
1318 冬至
1319 小寒
1320 大寒
1321 返回月历
1322 控制月历格中的日干支;黄历详情始终显示年月日干支
1323 宜
1324 忌
1325 纳音
1326 冲煞
1327 冲{} 煞{}
1328 值神
1329 建除十二神
1330 吉神宜趋
1331 今日胎神
1332 凶神宜忌
1333 彭祖百忌
1334 二十八星宿
1335 时辰吉凶
1336 子时显示当日早子时;23 点起的晚子时按次日干支计算
1337 吉
1338 凶
@@ -1247,3 +1247,92 @@ Token 活动 Actividad de tokens
每周 Semanal
{} 当周:{} Tokens Semana del {}: {} Tokens
截至 {} 当周累计:{} Tokens Acumulado hasta la semana del {}: {} Tokens
日历 Calendario
菜单栏日历 Calendario de la barra de menús
日历设置… Ajustes del calendario…
独立显示日期,点击打开月历;不受指标合并布局影响 Muestra la fecha por separado; haz clic para abrir el calendario, incluso con métricas combinadas
每周开始于 Primer día de la semana
星期一 Lunes
星期日 Domingo
农历 Calendario lunar chino
星期 Días de la semana
法定节假日与调休 Festivos y días laborales compensatorios
传统与公历节日 Fiestas tradicionales y gregorianas
二十四节气 24 términos solares
干支 Ciclo sexagenario
三伏天 Días caniculares
梅雨天 Lluvias de ciruela tradicionales
藏历 Calendario tibetano
回历 Calendario hiyri
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Las fechas gregorianas siempre se muestran. Los calendarios tibetano e hiyri aparecen en los detalles del día. Las lluvias de ciruela se calculan según reglas tradicionales, no son pronósticos.
该日期暂无藏历数据 No hay datos del calendario tibetano para esta fecha
该年份暂无中国法定假日与调休数据 No hay datos de festivos chinos ni días laborales compensatorios para este año
按传统历法推算,并非天气预报 Cálculo del calendario tradicional, no un pronóstico
年份 Año
月份 Mes
上个月 Mes anterior
下个月 Mes siguiente
班 T
休 D
调休上班 Día laboral compensatorio
放假 Festivo
春节 Fiesta de la Primavera
元宵节 Fiesta de los Faroles
龙头节 Longtaitou
上巳节 Fiesta de Shangsi
清明节 Fiesta de Qingming
端午节 Fiesta del Bote del Dragón
七夕节 Fiesta de Qixi
中元节 Fiesta de los Fantasmas
中秋节 Fiesta del Medio Otoño
重阳节 Fiesta del Doble Nueve
冬至节 Fiesta del Solsticio de Invierno
腊八节 Fiesta de Laba
除夕 Víspera del Año Nuevo Lunar
元旦 Año Nuevo
劳动节 Día del Trabajo
国庆节 Fiesta Nacional
国庆中秋 Fiesta Nacional / Medio Otoño
抗战胜利日 Día de la Victoria
立春 Inicio de la primavera
雨水 Agua de lluvia
惊蛰 Despertar de los insectos
春分 Equinoccio de primavera
清明 Claridad pura
谷雨 Lluvia para el grano
立夏 Inicio del verano
小满 Pequeña abundancia
芒种 Grano en espiga
夏至 Solsticio de verano
小暑 Calor menor
大暑 Calor mayor
立秋 Inicio del otoño
处暑 Fin del calor
白露 Rocío blanco
秋分 Equinoccio de otoño
寒露 Rocío frío
霜降 Descenso de la escarcha
立冬 Inicio del invierno
小雪 Nieve menor
大雪 Nieve mayor
冬至 Solsticio de invierno
小寒 Frío menor
大寒 Frío mayor
返回月历 Volver al calendario
控制月历格中的日干支;黄历详情始终显示年月日干支 Muestra el ciclo diario en las celdas; el almanaque siempre incluye los ciclos del año, mes y día
宜 Favorable
忌 Evitar
纳音 Na Yin
冲煞 Conflicto / Sha
冲{} 煞{} Conflicto: {} · Sha: {}
值神 Deidad del día
建除十二神 Doce oficiales
吉神宜趋 Deidades favorables
今日胎神 Deidad fetal
凶神宜忌 Deidades desfavorables
彭祖百忌 Tabúes de Peng Zu
二十八星宿 Mansiones lunares
时辰吉凶 Fortuna horaria
子时显示当日早子时;23 点起的晚子时按次日干支计算 Zi muestra 00:00–00:59; desde las 23:00 usa el ciclo del día siguiente
吉 Bueno
凶 Malo
1 # Copyright (C) 2026 ysicing
1247 每周
1248 {} 当周:{} Tokens
1249 截至 {} 当周累计:{} Tokens
1250 日历
1251 菜单栏日历
1252 日历设置…
1253 独立显示日期,点击打开月历;不受指标合并布局影响
1254 每周开始于
1255 星期一
1256 星期日
1257 农历
1258 星期
1259 法定节假日与调休
1260 传统与公历节日
1261 二十四节气
1262 干支
1263 三伏天
1264 梅雨天
1265 藏历
1266 回历
1267 公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。
1268 该日期暂无藏历数据
1269 该年份暂无中国法定假日与调休数据
1270 按传统历法推算,并非天气预报
1271 年份
1272 月份
1273 上个月
1274 下个月
1275 班
1276 休
1277 调休上班
1278 放假
1279 春节
1280 元宵节
1281 龙头节
1282 上巳节
1283 清明节
1284 端午节
1285 七夕节
1286 中元节
1287 中秋节
1288 重阳节
1289 冬至节
1290 腊八节
1291 除夕
1292 元旦
1293 劳动节
1294 国庆节
1295 国庆中秋
1296 抗战胜利日
1297 立春
1298 雨水
1299 惊蛰
1300 春分
1301 清明
1302 谷雨
1303 立夏
1304 小满
1305 芒种
1306 夏至
1307 小暑
1308 大暑
1309 立秋
1310 处暑
1311 白露
1312 秋分
1313 寒露
1314 霜降
1315 立冬
1316 小雪
1317 大雪
1318 冬至
1319 小寒
1320 大寒
1321 返回月历
1322 控制月历格中的日干支;黄历详情始终显示年月日干支
1323 宜
1324 忌
1325 纳音
1326 冲煞
1327 冲{} 煞{}
1328 值神
1329 建除十二神
1330 吉神宜趋
1331 今日胎神
1332 凶神宜忌
1333 彭祖百忌
1334 二十八星宿
1335 时辰吉凶
1336 子时显示当日早子时;23 点起的晚子时按次日干支计算
1337 吉
1338 凶
@@ -1247,3 +1247,92 @@ Token 活动 Activité des tokens
每周 Par semaine
{} 当周:{} Tokens Semaine du {} : {} Tokens
截至 {} 当周累计:{} Tokens Cumul jusqu’à la semaine du {} : {} Tokens
日历 Calendrier
菜单栏日历 Calendrier de la barre des menus
日历设置… Réglages du calendrier…
独立显示日期,点击打开月历;不受指标合并布局影响 Affiche la date séparément ; cliquez pour ouvrir le calendrier, même avec les indicateurs regroupés
每周开始于 Premier jour de la semaine
星期一 Lundi
星期日 Dimanche
农历 Calendrier lunaire chinois
星期 Jours de la semaine
法定节假日与调休 Jours fériés et jours de travail compensatoires
传统与公历节日 Fêtes traditionnelles et grégoriennes
二十四节气 24 périodes solaires
干支 Cycle sexagésimal
三伏天 Jours caniculaires
梅雨天 Pluies des prunes traditionnelles
藏历 Calendrier tibétain
回历 Calendrier hégirien
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Les dates grégoriennes restent visibles. Les dates tibétaines et hégiriennes figurent dans les détails du jour. Les pluies des prunes suivent les règles traditionnelles, sans prévision météo.
该日期暂无藏历数据 Aucune donnée du calendrier tibétain pour cette date
该年份暂无中国法定假日与调休数据 Aucune donnée sur les jours fériés chinois et les jours de travail compensatoires pour cette année
按传统历法推算,并非天气预报 Calcul du calendrier traditionnel, pas une prévision météo
年份 Année
月份 Mois
上个月 Mois précédent
下个月 Mois suivant
班 T
休 R
调休上班 Jour de travail compensatoire
放假 Jour férié
春节 Fête du Printemps
元宵节 Fête des Lanternes
龙头节 Longtaitou
上巳节 Fête de Shangsi
清明节 Fête de Qingming
端午节 Fête des Bateaux-Dragons
七夕节 Fête de Qixi
中元节 Fête des Fantômes
中秋节 Fête de la Mi-Automne
重阳节 Fête du Double Neuf
冬至节 Fête du Solstice d’Hiver
腊八节 Fête de Laba
除夕 Veille du Nouvel An lunaire
元旦 Jour de l’An
劳动节 Fête du Travail
国庆节 Fête nationale
国庆中秋 Fête nationale / Mi-Automne
抗战胜利日 Jour de la Victoire
立春 Début du printemps
雨水 Eau de pluie
惊蛰 Réveil des insectes
春分 Équinoxe de printemps
清明 Clarté pure
谷雨 Pluie des céréales
立夏 Début de l’été
小满 Petite plénitude
芒种 Grain en épi
夏至 Solstice d’été
小暑 Petite chaleur
大暑 Grande chaleur
立秋 Début de l’automne
处暑 Fin de la chaleur
白露 Rosée blanche
秋分 Équinoxe d’automne
寒露 Rosée froide
霜降 Arrivée du givre
立冬 Début de l’hiver
小雪 Petite neige
大雪 Grande neige
冬至 Solstice d’hiver
小寒 Petit froid
大寒 Grand froid
返回月历 Retour au calendrier
控制月历格中的日干支;黄历详情始终显示年月日干支 Affiche le cycle du jour dans les cases ; l’almanach inclut toujours les cycles de l’année, du mois et du jour
宜 Favorable
忌 À éviter
纳音 Na Yin
冲煞 Opposition / Sha
冲{} 煞{} Opposition : {} · Sha : {}
值神 Divinité du jour
建除十二神 Douze officiers
吉神宜趋 Divinités favorables
今日胎神 Divinité fœtale
凶神宜忌 Divinités défavorables
彭祖百忌 Tabous de Peng Zu
二十八星宿 Demeures lunaires
时辰吉凶 Fortune des heures
子时显示当日早子时;23 点起的晚子时按次日干支计算 Zi affiche 00 h–00 h 59 ; dès 23 h, le cycle du lendemain s’applique
吉 Favorable
凶 Défavorable
1 # Copyright (C) 2026 ysicing
1247 每周
1248 {} 当周:{} Tokens
1249 截至 {} 当周累计:{} Tokens
1250 日历
1251 菜单栏日历
1252 日历设置…
1253 独立显示日期,点击打开月历;不受指标合并布局影响
1254 每周开始于
1255 星期一
1256 星期日
1257 农历
1258 星期
1259 法定节假日与调休
1260 传统与公历节日
1261 二十四节气
1262 干支
1263 三伏天
1264 梅雨天
1265 藏历
1266 回历
1267 公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。
1268 该日期暂无藏历数据
1269 该年份暂无中国法定假日与调休数据
1270 按传统历法推算,并非天气预报
1271 年份
1272 月份
1273 上个月
1274 下个月
1275 班
1276 休
1277 调休上班
1278 放假
1279 春节
1280 元宵节
1281 龙头节
1282 上巳节
1283 清明节
1284 端午节
1285 七夕节
1286 中元节
1287 中秋节
1288 重阳节
1289 冬至节
1290 腊八节
1291 除夕
1292 元旦
1293 劳动节
1294 国庆节
1295 国庆中秋
1296 抗战胜利日
1297 立春
1298 雨水
1299 惊蛰
1300 春分
1301 清明
1302 谷雨
1303 立夏
1304 小满
1305 芒种
1306 夏至
1307 小暑
1308 大暑
1309 立秋
1310 处暑
1311 白露
1312 秋分
1313 寒露
1314 霜降
1315 立冬
1316 小雪
1317 大雪
1318 冬至
1319 小寒
1320 大寒
1321 返回月历
1322 控制月历格中的日干支;黄历详情始终显示年月日干支
1323 宜
1324 忌
1325 纳音
1326 冲煞
1327 冲{} 煞{}
1328 值神
1329 建除十二神
1330 吉神宜趋
1331 今日胎神
1332 凶神宜忌
1333 彭祖百忌
1334 二十八星宿
1335 时辰吉凶
1336 子时显示当日早子时;23 点起的晚子时按次日干支计算
1337 吉
1338 凶
@@ -1247,3 +1247,92 @@ Token 活动 トークン活動
每周 週別
{} 当周:{} Tokens {} の週:{} Tokens
截至 {} 当周累计:{} Tokens {} の週までの累計:{} Tokens
日历 カレンダー
菜单栏日历 メニューバーカレンダー
日历设置… カレンダー設定…
独立显示日期,点击打开月历;不受指标合并布局影响 日付を独立表示し、クリックで月表示を開きます。指標の結合設定には影響されません
每周开始于 週の開始曜日
星期一 月曜日
星期日 日曜日
农历 旧暦
星期 曜日
法定节假日与调休 祝日と振替出勤日
传统与公历节日 伝統と西暦の祝祭日
二十四节气 二十四節気
干支 干支
三伏天 三伏
梅雨天 入梅・出梅
藏历 チベット暦
回历 ヒジュラ暦
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 西暦は常に表示します。チベット暦とヒジュラ暦は日付の詳細に表示します。入梅・出梅は伝統的な暦の計算で、天気予報ではありません。
该日期暂无藏历数据 この日付のチベット暦データはありません
该年份暂无中国法定假日与调休数据 この年の中国の祝日・振替出勤日データはありません
按传统历法推算,并非天气预报 伝統的な暦の計算であり、天気予報ではありません
年份 年
月份 月
上个月 前の月
下个月 次の月
班 勤
休 休
调休上班 振替出勤
放假 休日
春节 春節
元宵节 元宵節
龙头节 龍頭節
上巳节 上巳節
清明节 清明節
端午节 端午節
七夕节 七夕節
中元节 中元節
中秋节 中秋節
重阳节 重陽節
冬至节 冬至節
腊八节 臘八節
除夕 旧暦の大晦日
元旦 元日
劳动节 労働節
国庆节 国慶節
国庆中秋 国慶節・中秋節
抗战胜利日 抗日戦争勝利記念日
立春 立春
雨水 雨水
惊蛰 啓蟄
春分 春分
清明 清明
谷雨 穀雨
立夏 立夏
小满 小満
芒种 芒種
夏至 夏至
小暑 小暑
大暑 大暑
立秋 立秋
处暑 処暑
白露 白露
秋分 秋分
寒露 寒露
霜降 霜降
立冬 立冬
小雪 小雪
大雪 大雪
冬至 冬至
小寒 小寒
大寒 大寒
返回月历 月表示に戻る
控制月历格中的日干支;黄历详情始终显示年月日干支 月表示の日干支を設定します。暦の詳細には年・月・日の干支を常に表示します
宜 宜
忌 忌
纳音 納音
冲煞 沖煞
冲{} 煞{} 沖:{} 煞:{}
值神 値神
建除十二神 建除十二直
吉神宜趋 吉神
今日胎神 今日の胎神
凶神宜忌 凶神
彭祖百忌 彭祖百忌
二十八星宿 二十八宿
时辰吉凶 時辰の吉凶
子时显示当日早子时;23 点起的晚子时按次日干支计算 子の刻は当日の0時台を表示します。23時以降は翌日の干支で計算します
吉 吉
凶 凶
1 # Copyright (C) 2026 ysicing
1247 每周
1248 {} 当周:{} Tokens
1249 截至 {} 当周累计:{} Tokens
1250 日历
1251 菜单栏日历
1252 日历设置…
1253 独立显示日期,点击打开月历;不受指标合并布局影响
1254 每周开始于
1255 星期一
1256 星期日
1257 农历
1258 星期
1259 法定节假日与调休
1260 传统与公历节日
1261 二十四节气
1262 干支
1263 三伏天
1264 梅雨天
1265 藏历
1266 回历
1267 公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。
1268 该日期暂无藏历数据
1269 该年份暂无中国法定假日与调休数据
1270 按传统历法推算,并非天气预报
1271 年份
1272 月份
1273 上个月
1274 下个月
1275 班
1276 休
1277 调休上班
1278 放假
1279 春节
1280 元宵节
1281 龙头节
1282 上巳节
1283 清明节
1284 端午节
1285 七夕节
1286 中元节
1287 中秋节
1288 重阳节
1289 冬至节
1290 腊八节
1291 除夕
1292 元旦
1293 劳动节
1294 国庆节
1295 国庆中秋
1296 抗战胜利日
1297 立春
1298 雨水
1299 惊蛰
1300 春分
1301 清明
1302 谷雨
1303 立夏
1304 小满
1305 芒种
1306 夏至
1307 小暑
1308 大暑
1309 立秋
1310 处暑
1311 白露
1312 秋分
1313 寒露
1314 霜降
1315 立冬
1316 小雪
1317 大雪
1318 冬至
1319 小寒
1320 大寒
1321 返回月历
1322 控制月历格中的日干支;黄历详情始终显示年月日干支
1323 宜
1324 忌
1325 纳音
1326 冲煞
1327 冲{} 煞{}
1328 值神
1329 建除十二神
1330 吉神宜趋
1331 今日胎神
1332 凶神宜忌
1333 彭祖百忌
1334 二十八星宿
1335 时辰吉凶
1336 子时显示当日早子时;23 点起的晚子时按次日干支计算
1337 吉
1338 凶
@@ -1247,3 +1247,92 @@ Token 活动 토큰 활동
每周 주별
{} 当周:{} Tokens {} 주: {} Tokens
截至 {} 当周累计:{} Tokens {} 주까지 누적: {} Tokens
日历 달력
菜单栏日历 메뉴 막대 달력
日历设置… 달력 설정…
独立显示日期,点击打开月历;不受指标合并布局影响 날짜를 독립적으로 표시합니다. 클릭하면 월 달력이 열리며 지표 통합 설정과 무관합니다
每周开始于 한 주의 시작
星期一 월요일
星期日 일요일
农历 음력
星期 요일
法定节假日与调休 공휴일 및 대체 근무일
传统与公历节日 전통 및 양력 기념일
二十四节气 24절기
干支 간지
三伏天 삼복
梅雨天 입매·출매
藏历 티베트력
回历 이슬람력
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 양력은 항상 표시됩니다. 티베트력과 이슬람력은 날짜 상세 정보에 표시됩니다. 입매·출매는 전통 달력 계산이며 일기 예보가 아닙니다.
该日期暂无藏历数据 이 날짜의 티베트력 데이터가 없습니다
该年份暂无中国法定假日与调休数据 이 연도의 중국 공휴일 및 대체 근무일 데이터가 없습니다
按传统历法推算,并非天气预报 전통 달력 계산이며 일기 예보가 아닙니다
年份 연도
月份 월
上个月 이전 달
下个月 다음 달
班 근
休 휴
调休上班 대체 근무
放假 휴일
春节 춘절
元宵节 원소절
龙头节 용두절
上巳节 상사절
清明节 청명절
端午节 단오절
七夕节 칠석
中元节 중원절
中秋节 중추절
重阳节 중양절
冬至节 동지절
腊八节 납팔절
除夕 음력 섣달그믐
元旦 새해 첫날
劳动节 노동절
国庆节 국경절
国庆中秋 국경절·중추절
抗战胜利日 항일 전쟁 승리 기념일
立春 입춘
雨水 우수
惊蛰 경칩
春分 춘분
清明 청명
谷雨 곡우
立夏 입하
小满 소만
芒种 망종
夏至 하지
小暑 소서
大暑 대서
立秋 입추
处暑 처서
白露 백로
秋分 추분
寒露 한로
霜降 상강
立冬 입동
小雪 소설
大雪 대설
冬至 동지
小寒 소한
大寒 대한
返回月历 월 달력으로 돌아가기
控制月历格中的日干支;黄历详情始终显示年月日干支 월 달력 칸의 일진 표시를 설정합니다. 황력 상세에는 연·월·일 간지가 항상 표시됩니다
宜 길
忌 기
纳音 납음
冲煞 충살
冲{} 煞{} 충: {} · 살: {}
值神 당일 신
建除十二神 건제십이신
吉神宜趋 길신
今日胎神 오늘의 태신
凶神宜忌 흉신
彭祖百忌 팽조백기
二十八星宿 이십팔수
时辰吉凶 시진 길흉
子时显示当日早子时;23 点起的晚子时按次日干支计算 자시는 당일 00시를 표시합니다. 23시 이후의 야자시는 다음 날 간지로 계산합니다
吉 길
凶 흉
1 # Copyright (C) 2026 ysicing
1247 每周
1248 {} 当周:{} Tokens
1249 截至 {} 当周累计:{} Tokens
1250 日历
1251 菜单栏日历
1252 日历设置…
1253 独立显示日期,点击打开月历;不受指标合并布局影响
1254 每周开始于
1255 星期一
1256 星期日
1257 农历
1258 星期
1259 法定节假日与调休
1260 传统与公历节日
1261 二十四节气
1262 干支
1263 三伏天
1264 梅雨天
1265 藏历
1266 回历
1267 公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。
1268 该日期暂无藏历数据
1269 该年份暂无中国法定假日与调休数据
1270 按传统历法推算,并非天气预报
1271 年份
1272 月份
1273 上个月
1274 下个月
1275 班
1276 休
1277 调休上班
1278 放假
1279 春节
1280 元宵节
1281 龙头节
1282 上巳节
1283 清明节
1284 端午节
1285 七夕节
1286 中元节
1287 中秋节
1288 重阳节
1289 冬至节
1290 腊八节
1291 除夕
1292 元旦
1293 劳动节
1294 国庆节
1295 国庆中秋
1296 抗战胜利日
1297 立春
1298 雨水
1299 惊蛰
1300 春分
1301 清明
1302 谷雨
1303 立夏
1304 小满
1305 芒种
1306 夏至
1307 小暑
1308 大暑
1309 立秋
1310 处暑
1311 白露
1312 秋分
1313 寒露
1314 霜降
1315 立冬
1316 小雪
1317 大雪
1318 冬至
1319 小寒
1320 大寒
1321 返回月历
1322 控制月历格中的日干支;黄历详情始终显示年月日干支
1323 宜
1324 忌
1325 纳音
1326 冲煞
1327 冲{} 煞{}
1328 值神
1329 建除十二神
1330 吉神宜趋
1331 今日胎神
1332 凶神宜忌
1333 彭祖百忌
1334 二十八星宿
1335 时辰吉凶
1336 子时显示当日早子时;23 点起的晚子时按次日干支计算
1337 吉
1338 凶
@@ -1248,3 +1248,92 @@ Token 活动 Token 活動
每周 每週
{} 当周:{} Tokens {} 當週:{} Tokens
截至 {} 当周累计:{} Tokens 截至 {} 當週累計:{} Tokens
日历 日曆
菜单栏日历 選單列日曆
日历设置… 日曆設定…
独立显示日期,点击打开月历;不受指标合并布局影响 獨立顯示日期,點擊開啟月曆;不受指標合併佈局影響
每周开始于 每週開始於
星期一 星期一
星期日 星期日
农历 農曆
星期 星期
法定节假日与调休 法定節假日與調休
传统与公历节日 傳統與公曆節日
二十四节气 二十四節氣
干支 干支
三伏天 三伏天
梅雨天 梅雨天
藏历 藏曆
回历 回曆
公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 公曆始終顯示。藏曆與回曆顯示在日期詳情中;梅雨天按傳統曆法推算,並非天氣預報。
该日期暂无藏历数据 該日期暫無藏曆資料
该年份暂无中国法定假日与调休数据 該年份暫無中國法定假日與調休資料
按传统历法推算,并非天气预报 按傳統曆法推算,並非天氣預報
年份 年份
月份 月份
上个月 上個月
下个月 下個月
班 班
休 休
调休上班 調休上班
放假 放假
春节 春節
元宵节 元宵節
龙头节 龍頭節
上巳节 上巳節
清明节 清明節
端午节 端午節
七夕节 七夕節
中元节 中元節
中秋节 中秋節
重阳节 重陽節
冬至节 冬至節
腊八节 臘八節
除夕 除夕
元旦 元旦
劳动节 勞動節
国庆节 國慶節
国庆中秋 國慶中秋
抗战胜利日 抗戰勝利日
立春 立春
雨水 雨水
惊蛰 驚蟄
春分 春分
清明 清明
谷雨 穀雨
立夏 立夏
小满 小滿
芒种 芒種
夏至 夏至
小暑 小暑
大暑 大暑
立秋 立秋
处暑 處暑
白露 白露
秋分 秋分
寒露 寒露
霜降 霜降
立冬 立冬
小雪 小雪
大雪 大雪
冬至 冬至
小寒 小寒
大寒 大寒
返回月历 返回月曆
控制月历格中的日干支;黄历详情始终显示年月日干支 控制月曆格中的日干支;黃曆詳情始終顯示年月日干支
宜 宜
忌 忌
纳音 納音
冲煞 沖煞
冲{} 煞{} 沖{} 煞{}
值神 值神
建除十二神 建除十二神
吉神宜趋 吉神宜趨
今日胎神 今日胎神
凶神宜忌 凶神宜忌
彭祖百忌 彭祖百忌
二十八星宿 二十八星宿
时辰吉凶 時辰吉凶
子时显示当日早子时;23 点起的晚子时按次日干支计算 子時顯示當日早子時;23 點起的晚子時按次日干支計算
吉 吉
凶 凶
1 # Copyright (c) 2026 GiantAccel, LLC
1248 每周
1249 {} 当周:{} Tokens
1250 截至 {} 当周累计:{} Tokens
1251 日历
1252 菜单栏日历
1253 日历设置…
1254 独立显示日期,点击打开月历;不受指标合并布局影响
1255 每周开始于
1256 星期一
1257 星期日
1258 农历
1259 星期
1260 法定节假日与调休
1261 传统与公历节日
1262 二十四节气
1263 干支
1264 三伏天
1265 梅雨天
1266 藏历
1267 回历
1268 公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。
1269 该日期暂无藏历数据
1270 该年份暂无中国法定假日与调休数据
1271 按传统历法推算,并非天气预报
1272 年份
1273 月份
1274 上个月
1275 下个月
1276 班
1277 休
1278 调休上班
1279 放假
1280 春节
1281 元宵节
1282 龙头节
1283 上巳节
1284 清明节
1285 端午节
1286 七夕节
1287 中元节
1288 中秋节
1289 重阳节
1290 冬至节
1291 腊八节
1292 除夕
1293 元旦
1294 劳动节
1295 国庆节
1296 国庆中秋
1297 抗战胜利日
1298 立春
1299 雨水
1300 惊蛰
1301 春分
1302 清明
1303 谷雨
1304 立夏
1305 小满
1306 芒种
1307 夏至
1308 小暑
1309 大暑
1310 立秋
1311 处暑
1312 白露
1313 秋分
1314 寒露
1315 霜降
1316 立冬
1317 小雪
1318 大雪
1319 冬至
1320 小寒
1321 大寒
1322 返回月历
1323 控制月历格中的日干支;黄历详情始终显示年月日干支
1324 宜
1325 忌
1326 纳音
1327 冲煞
1328 冲{} 煞{}
1329 值神
1330 建除十二神
1331 吉神宜趋
1332 今日胎神
1333 凶神宜忌
1334 彭祖百忌
1335 二十八星宿
1336 时辰吉凶
1337 子时显示当日早子时;23 点起的晚子时按次日干支计算
1338 吉
1339 凶
@@ -8,7 +8,7 @@
extension Translations {
static var tables: [String] {
[english, webDAVEnglish, languagePickerEnglish]
[english, webDAVEnglish, languagePickerEnglish, calendarEnglish]
}
static let english = #"""
@@ -12,6 +12,7 @@ import SwiftUI
public final class AppController: NSObject, NSApplicationDelegate {
private let model = AppModel()
private var menuBar: MenuBarController!
private var calendarMenuBar: CalendarMenuBarController!
private var mainWindow: MainWindowController!
private var updateWindow: UpdateWindowController!
private var speedTestWindow: SpeedTestWindowController!
@@ -31,6 +32,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
NSApp.mainMenu = MainMenu.make(target: self, settingsAction: #selector(openSettingsFromMenu),
updateAction: #selector(checkForUpdatesFromMenu))
menuBar = MenuBarController(model: model)
calendarMenuBar = CalendarMenuBarController(model: model)
mainWindow = MainWindowController(model: model)
mainWindow.onVisibilityChange = { [weak self] _ in self?.updateActivationPolicy() }
updateWindow = UpdateWindowController(model: model)
@@ -39,22 +41,27 @@ public final class AppController: NSObject, NSApplicationDelegate {
egressWindow = EgressWindowController(model: model)
egressWindow.onVisibilityChange = { [weak self] _ in self?.updateActivationPolicy() }
menuBar.update()
calendarMenuBar.start()
// 设置已并入主窗口:所有“设置…”入口都打开主窗口的通用设置
model.openSettings = { [weak self] in
self?.menuBar.dismissPopovers()
self?.calendarMenuBar.dismiss()
self?.mainWindow.show(tab: .settingsGeneral)
}
model.openMainWindow = { [weak self] tab in
self?.menuBar.dismissPopovers()
self?.calendarMenuBar.dismiss()
self?.mainWindow.show(tab: tab)
}
model.openEgressWindow = { [weak self] in
self?.menuBar.dismissPopovers()
self?.calendarMenuBar.dismiss()
self?.egressWindow.show()
}
model.openSpeedTestWindow = { [weak self] in
self?.menuBar.dismissPopovers()
self?.calendarMenuBar.dismiss()
self?.speedTestWindow.show()
}
model.quit = { NSApp.terminate(nil) }
@@ -79,6 +86,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
startUpdateChecks()
model.alerts.openTab = { [weak self] tab in
self?.menuBar.dismissPopovers()
self?.calendarMenuBar.dismiss()
self?.mainWindow.show(tab: tab)
}
model.alerts.start()
@@ -101,6 +109,13 @@ public final class AppController: NSObject, NSApplicationDelegate {
}
}
}
if arguments.contains("--show-calendar") {
model.settings.calendarEnabled = true
calendarMenuBar.update()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.calendarMenuBar.show()
}
}
if arguments.contains("--show-window") {
mainWindow.show(tab: nil)
}
@@ -139,6 +154,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
}
public func applicationWillTerminate(_ notification: Notification) {
calendarMenuBar.stop()
model.aiUsage.stop()
model.keepAwake.releaseForTermination()
if model.fans.mode != .automatic || model.keepAwake.lidClosedActive {
@@ -188,6 +204,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
private func startUpdateChecks() {
model.updates.onPrompt = { [weak self] in
self?.menuBar.dismissPopovers()
self?.calendarMenuBar.dismiss()
self?.updateWindow.show()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
@@ -0,0 +1,65 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
@preconcurrency import Tyme4Swift
/// 选中日期才计算的黄历值模型,不在生成 42 格月历时重复查询宜忌表。
struct CalendarAlmanac: Identifiable {
let id: String
struct Hour: Identifiable {
let id: Int
let stem: String
let branch: String
let isLucky: Bool
let timeRange: String
}
let ganzhiSummary: String
let recommends: [String]
let avoids: [String]
let sound: String
let opposingZodiac: String
let ominousDirection: String
let twelveStar: String
let twelveStarIsLucky: Bool
let hours: [Hour]
let duty: String
let luckyGods: [String]
let unluckyGods: [String]
let fetus: String
let pengZu: [String]
let star: String
let starIsLucky: Bool
}
extension CalendarEngine {
static func almanac(for day: CalendarDay) -> CalendarAlmanac? {
guard let solar = try? SolarDay.fromYmd(day.year, day.month, day.day) else { return nil }
let lunar = solar.getLunarDay()
let cycle = solar.getSixtyCycleDay()
let gods = cycle.gods
let star = cycle.twentyEightStar
let pengZu = cycle.sixtyCycle.pengZu
// Tyme 返回早子时、丑至亥、晚子时共 13 项。末项 23 点已用次日日柱,
// 黄历的十二格取前 12 项,避免重复子时;保留农历闰月信息,直接使用原对象。
let hours = lunar.hours.prefix(12).enumerated().map { index, hour in
CalendarAlmanac.Hour(id: index, stem: hour.sixtyCycle.heavenStem.getName(),
branch: hour.sixtyCycle.earthBranch.getName(),
isLucky: hour.twelveStar.ecliptic.luck.index == 0,
timeRange: index == 0 ? "00:00–00:59" : String(format: "%02d:00–%02d:59", index * 2 - 1, index * 2))
}
return CalendarAlmanac(
id: day.id,
ganzhiSummary: "\(cycle.year.getName())(\(cycle.year.earthBranch.getZodiac().getName()))年 \(cycle.month.getName())月 \(cycle.sixtyCycle.getName())日",
recommends: cycle.recommends.map { $0.getName() }, avoids: cycle.avoids.map { $0.getName() },
sound: cycle.sixtyCycle.sound.getName(), opposingZodiac: cycle.sixtyCycle.earthBranch.getOpposite().getZodiac().getName(),
ominousDirection: cycle.sixtyCycle.earthBranch.getOminous().getName(),
twelveStar: cycle.twelveStar.getName(), twelveStarIsLucky: cycle.twelveStar.ecliptic.luck.index == 0,
hours: hours, duty: cycle.duty.getName(),
luckyGods: gods.filter { $0.luck.index == 0 }.map { $0.getName() },
unluckyGods: gods.filter { $0.luck.index != 0 }.map { $0.getName() },
fetus: lunar.fetusDay.getName(),
pengZu: [pengZu.pengZuHeavenStem.getName(), pengZu.pengZuEarthBranch.getName()],
star: star.getName() + star.sevenStar.getName() + star.getAnimal().getName(), starIsLucky: star.luck.index == 0)
}
}
@@ -0,0 +1,212 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
import Localization
import SwiftUI
/// 传统黄历详情:以分格表呈现同一日期的数据,正文保留 Tyme 的传统名称。
struct CalendarAlmanacView: View {
let day: CalendarDay
let almanac: CalendarAlmanac
let features: Set<CalendarFeature>
private let heading = Color.dynamic(light: 0xAE743F, dark: 0xDBAC79)
private let lucky = Color.dynamic(light: 0x288B3B, dark: 0x63CC76)
var body: some View {
VStack(spacing: 16) {
VStack(spacing: 8) {
Text(dateTitle).font(.system(size: 13)).foregroundStyle(.secondary)
if features.contains(.lunar) {
Text(day.lunarSummary).font(.system(size: 21)).foregroundStyle(.red)
}
Text(almanac.ganzhiSummary).font(.system(size: 13)).foregroundStyle(.secondary)
}
VStack(alignment: .leading, spacing: 10) {
advice(tr("宜"), entries: almanac.recommends, color: lucky)
advice(tr("忌"), entries: almanac.avoids, color: .red)
}
almanacTable
supplementaryDetails
}
.padding(.bottom, 6)
.textSelection(.enabled)
}
private var dateTitle: String {
let formatter = DateFormatter()
formatter.locale = L10n.locale
formatter.calendar = CalendarEngine.gregorian()
formatter.setLocalizedDateFormatFromTemplate(features.contains(.weekdays) ? "yMMMMdEEEE" : "yMMMMd")
return formatter.string(from: day.date)
}
private func advice(_ label: String, entries: [String], color: Color) -> some View {
HStack(alignment: .top, spacing: 10) {
Text(label).font(.system(size: 12, weight: .medium)).foregroundStyle(.white)
.padding(4).background(color, in: Capsule())
Text(entries.isEmpty ? tr("无") : entries.joined(separator: " "))
.font(.system(size: 12)).foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 3)
}
}
private var almanacTable: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
tile(tr("纳音")) { Text(almanac.sound) }
verticalLine
tile(tr("冲煞")) { Text(tr("冲\(almanac.opposingZodiac) 煞\(almanac.ominousDirection)")) }
verticalLine
tile(tr("值神")) {
HStack(spacing: 4) {
Text(almanac.twelveStar)
luckLabel(almanac.twelveStarIsLucky)
}
}
}
.fixedSize(horizontal: false, vertical: true)
Divider()
hourTable
Divider()
HStack(spacing: 0) {
sideTile(tr("建除十二神"), value: almanac.duty)
verticalLine
VStack(spacing: 0) {
HStack(alignment: .top, spacing: 0) {
tile(tr("吉神宜趋")) { Text(almanac.luckyGods.joined(separator: " ")) }
verticalLine
tile(tr("今日胎神")) { Text(almanac.fetus) }
verticalLine
tile(tr("凶神宜忌")) { Text(almanac.unluckyGods.joined(separator: " ")) }
}
.fixedSize(horizontal: false, vertical: true)
Divider()
tile(tr("彭祖百忌")) { Text(almanac.pengZu.joined(separator: "\n")) }
}
verticalLine
sideTile(tr("二十八星宿"), value: almanac.star, isLucky: almanac.starIsLucky)
}
.fixedSize(horizontal: false, vertical: true)
}
.overlay { Rectangle().strokeBorder(DS.Palette.border, lineWidth: 1) }
}
private func sideTile(_ title: String, value: String, isLucky: Bool? = nil) -> some View {
Group {
if L10n.language.isChinese {
HStack(spacing: 6) {
Text(title.map(String.init).joined(separator: "\n"))
.font(.system(size: 16)).foregroundStyle(heading)
VStack(spacing: 5) {
Text(value.map(String.init).joined(separator: "\n")).foregroundStyle(.secondary)
if let isLucky { luckLabel(isLucky) }
}
.font(.system(size: 12))
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(title + " " + value + (isLucky.map { " " + tr($0 ? "吉" : "凶") } ?? ""))
} else {
VStack(spacing: 8) {
Text(title).font(.system(size: 13)).foregroundStyle(heading)
Text(value).font(.system(size: 12)).foregroundStyle(.secondary)
if let isLucky { luckLabel(isLucky) }
}
}
}
.multilineTextAlignment(.center)
.frame(width: L10n.language.isChinese ? 52 : 72)
.padding(.horizontal, 5)
.padding(.vertical, 10)
}
private var hourTable: some View {
HStack(spacing: 0) {
Text(tr("时辰吉凶"))
.font(.system(size: 15)).foregroundStyle(heading)
.multilineTextAlignment(.center)
.frame(width: L10n.language.isChinese ? 42 : 60).padding(.horizontal, 6)
.help(tr("子时显示当日早子时;23 点起的晚子时按次日干支计算"))
verticalLine
ForEach(almanac.hours) { hour in
VStack(spacing: 2) {
Text(hour.stem)
Text(hour.branch)
luckLabel(hour.isLucky, compact: true)
}
.font(.system(size: 12))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.help(hour.timeRange)
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(hour.stem)\(hour.branch) \(hour.timeRange) \(tr(hour.isLucky ? "吉" : "凶"))")
}
}
.fixedSize(horizontal: false, vertical: true)
}
private var verticalLine: some View {
Rectangle().fill(DS.Palette.border).frame(width: 1)
}
private func tile<Content: View>(_ title: String, @ViewBuilder content: () -> Content) -> some View {
VStack(spacing: 5) {
Text(title).font(.system(size: 16)).foregroundStyle(heading)
content().font(.system(size: 12)).foregroundStyle(.secondary)
}
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding(.horizontal, 6)
.padding(.vertical, 10)
.fixedSize(horizontal: false, vertical: true)
}
@ViewBuilder private func luckLabel(_ isLucky: Bool, compact: Bool = false) -> some View {
if compact && !L10n.language.isChinese {
Image(systemName: isLucky ? "checkmark" : "xmark")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(isLucky ? lucky : .red)
.accessibilityLabel(tr(isLucky ? "吉" : "凶"))
.help(tr(isLucky ? "吉" : "凶"))
} else {
Text(tr(isLucky ? "吉" : "凶")).foregroundStyle(isLucky ? lucky : .red)
}
}
@ViewBuilder private var supplementaryDetails: some View {
let rows = additionalDetails
if !rows.isEmpty {
VStack(alignment: .leading, spacing: 7) {
ForEach(rows) { row in
HStack(alignment: .firstTextBaseline, spacing: 12) {
Text(row.feature.title).foregroundStyle(.secondary)
Text(row.value).frame(maxWidth: .infinity, alignment: .trailing)
}
}
if features.contains(.plumRain), day.plumRain != nil {
Text(tr("按传统历法推算,并非天气预报")).font(.caption2).foregroundStyle(.secondary)
}
}
.font(.system(size: 12))
.padding(12)
.background(DS.Palette.textSecondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10))
}
}
private var additionalDetails: [CalendarDetail] {
var rows: [CalendarDetail] = []
if features.contains(.holidays), let holiday = day.holiday {
rows.append(.init(feature: .holidays, value: tr(holiday.name) + " · " + tr(holiday.isWork ? "调休上班" : "放假")))
}
if features.contains(.festivals), !day.festivals.isEmpty {
rows.append(.init(feature: .festivals, value: day.festivals.map { tr($0) }.joined(separator: " · ")))
}
if features.contains(.solarTerms), let value = day.solarTerm { rows.append(.init(feature: .solarTerms, value: tr(value))) }
if features.contains(.dogDays), let value = day.dogDays { rows.append(.init(feature: .dogDays, value: value)) }
if features.contains(.plumRain), let value = day.plumRain { rows.append(.init(feature: .plumRain, value: value)) }
return rows + CalendarEngine.additionalCalendars(for: day, features: features)
}
}
@@ -0,0 +1,164 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
import Localization
// Tyme 1.5 使用 Swift 5;并发兼容仅限此适配层,所有访问由 MainActor 串行化。
@preconcurrency import Tyme4Swift
/// 公历是月历的坐标;其余信息可独立隐藏,设置以稳定 rawValue 保存和同步。
public enum CalendarFeature: String, CaseIterable, Identifiable, Sendable {
case lunar, weekdays, holidays, festivals, solarTerms, ganzhi, dogDays, plumRain, tibetan, hijri
public var id: String { rawValue }
public static let defaults: Set<Self> = [.lunar, .weekdays, .holidays, .festivals, .solarTerms]
var title: String {
switch self {
case .lunar: tr("农历")
case .weekdays: tr("星期")
case .holidays: tr("法定节假日与调休")
case .festivals: tr("传统与公历节日")
case .solarTerms: tr("二十四节气")
case .ganzhi: tr("干支")
case .dogDays: tr("三伏天")
case .plumRain: tr("梅雨天")
case .tibetan: tr("藏历")
case .hijri: tr("回历")
}
}
}
struct CalendarMonth: Equatable, Hashable {
static let years = 1900...2100
var year: Int
var month: Int
func shifted(by delta: Int) -> Self? {
let index = year * 12 + month - 1 + delta
let result = Self(year: index / 12, month: index % 12 + 1)
return Self.years.contains(result.year) ? result : nil
}
}
struct CalendarDay: Identifiable, Equatable {
struct Holiday: Equatable {
let name: String
let isWork: Bool
}
let date: Date
let year: Int
let month: Int
let day: Int
/// Foundation 的星期编号:1 为周日。
let weekday: Int
let lunarLabel: String
let lunarSummary: String
let ganzhiDay: String
let ganzhiSummary: String
let festivals: [String]
let solarTerm: String?
let holiday: Holiday?
let dogDays: String?
let plumRain: String?
var id: String { String(format: "%04d-%02d-%02d", year, month, day) }
var isWeekend: Bool { weekday == 1 || weekday == 7 }
/// 每格仅显示一条主注释,所有启用的信息在选中日期详情中保留。
func subtitle(features: Set<CalendarFeature>) -> String {
if features.contains(.festivals), let festival = festivals.first { return festival }
if features.contains(.solarTerms), let solarTerm { return solarTerm }
if features.contains(.dogDays), let dogDays { return dogDays }
if features.contains(.plumRain), let plumRain { return plumRain }
return features.contains(.lunar) ? lunarLabel : ""
}
}
struct CalendarDetail: Identifiable {
let feature: CalendarFeature
let value: String
var isUnavailable = false
var id: CalendarFeature { feature }
}
/// Tyme 使用可变的静态表;统一在主线程读取,不把其引用类型暴露给界面或跨线程共享。
@MainActor
enum CalendarEngine {
static func gregorian(timeZone: TimeZone = .autoupdatingCurrent) -> Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
return calendar
}
static func today(at date: Date = Date(), timeZone: TimeZone = .autoupdatingCurrent) -> CalendarDay? {
let parts = gregorian(timeZone: timeZone).dateComponents([.year, .month, .day], from: date)
guard let year = parts.year, let month = parts.month, let day = parts.day else { return nil }
return self.day(year: year, month: month, day: day, timeZone: timeZone)
}
/// 保留同月的公历选择,并用当前时区重建 Date;不能复用旧时区的绝对时间。
static func selection(_ previous: CalendarDay?, in month: CalendarMonth,
timeZone: TimeZone = .autoupdatingCurrent) -> CalendarDay? {
let day = previous?.year == month.year && previous?.month == month.month ? (previous?.day ?? 1) : 1
return self.day(year: month.year, month: month.month, day: day, timeZone: timeZone)
}
static func month(year: Int, month: Int, firstWeekday: Int,
timeZone: TimeZone = .autoupdatingCurrent) -> [CalendarDay] {
guard CalendarMonth.years.contains(year), (1...12).contains(month),
let first = day(year: year, month: month, day: 1, timeZone: timeZone) else { return [] }
let calendar = gregorian(timeZone: timeZone)
let weekStart = firstWeekday == 1 ? 1 : 2
let offset = (first.weekday - weekStart + 7) % 7
// 用日历加天而非 86400 秒;夏令时切换不能导致重复日期或漏掉日期。
return (0..<42).compactMap { index in
guard let date = calendar.date(byAdding: .day, value: index - offset, to: first.date) else { return nil }
return today(at: date, timeZone: timeZone)
}
}
static func day(year: Int, month: Int, day: Int,
timeZone: TimeZone = .autoupdatingCurrent) -> CalendarDay? {
// 包含边缘月份的相邻日期;界面浏览限制在 1900–2100 年,避开历史历法切换歧义。
guard (1899...2101).contains(year), (1...12).contains(month),
let solar = try? SolarDay.fromYmd(year, month, day) else { return nil }
let calendar = gregorian(timeZone: timeZone)
guard let date = calendar.date(from: DateComponents(year: year, month: month, day: day, hour: 12)) else { return nil }
let lunar = solar.getLunarDay()
let cycle = solar.getSixtyCycleDay()
let term = solar.termDay
let festivals = [lunar.festival?.getName(), solar.festival?.getName()].compactMap { $0 }
return CalendarDay(date: date, year: year, month: month, day: day,
weekday: calendar.component(.weekday, from: date),
lunarLabel: lunar.day == 1 ? lunar.lunarMonth.getName() : lunar.getName(),
lunarSummary: lunar.description, ganzhiDay: cycle.sixtyCycle.getName(),
ganzhiSummary: "\(cycle.year.getName())年 \(cycle.month.getName())月 \(cycle.sixtyCycle.getName())日",
festivals: festivals, solarTerm: term.dayIndex == 0 ? term.solarTerm.getName() : nil,
holiday: solar.legalHoliday.map { .init(name: $0.name, isWork: $0.isWork) },
dogDays: solar.dogDay?.description, plumRain: solar.plumRainDay?.description)
}
/// 年份覆盖来自固定版本的数据本身;未收录年份不能把 nil 解读为“无需调休”。
static func hasHolidayData(year: Int) -> Bool { holidayYears.contains(year) }
private static let holidayYears: Set<Int> = {
let data = Array(LegalHoliday.DATA.utf8)
return Set(stride(from: 0, to: data.count, by: 13).compactMap { index in
guard index + 4 <= data.count else { return nil }
return Int(String(decoding: data[index..<(index + 4)], as: UTF8.self))
})
}()
static func additionalCalendars(for day: CalendarDay, features: Set<CalendarFeature>) -> [CalendarDetail] {
guard let solar = try? SolarDay.fromYmd(day.year, day.month, day.day) else { return [] }
var details: [CalendarDetail] = []
if features.contains(.tibetan) {
// 上游从固定起点向前扫描,范围外部分路径使用 try!;先检查完整的公历覆盖范围。
let supported = day.id >= "1951-01-08" && day.id <= "2051-02-11"
let value = supported ? (try? solar.getRabByungDay().description) : nil
details.append(.init(feature: .tibetan, value: value ?? tr("该日期暂无藏历数据"), isUnavailable: value == nil))
}
if features.contains(.hijri) {
details.append(.init(feature: .hijri, value: solar.getHijriDay().description))
}
return details
}
}
@@ -0,0 +1,136 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import AppKit
import Localization
import SwiftUI
/// 独立日历入口:只按分钟刷新日期文本,关闭后移除定时器,不参与 MetricsHub 的秒级采样。
@MainActor
final class CalendarMenuBarController: NSObject {
private let model: AppModel
private var item: NSStatusItem?
private var panel: StatusPanel?
private var timer: Timer?
private var observers: [(NotificationCenter, NSObjectProtocol)] = []
init(model: AppModel) {
self.model = model
super.init()
}
func start() {
update()
observeSettings()
for (center, name) in [
(NotificationCenter.default, Notification.Name.NSCalendarDayChanged),
(NotificationCenter.default, Notification.Name.NSSystemTimeZoneDidChange),
(NSWorkspace.shared.notificationCenter, NSWorkspace.didWakeNotification),
] {
let token = center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.refreshTitle() }
}
observers.append((center, token))
}
}
func stop() {
timer?.invalidate()
timer = nil
for (center, token) in observers { center.removeObserver(token) }
observers.removeAll()
dismiss()
if let item { NSStatusBar.system.removeStatusItem(item) }
item = nil
}
private func observeSettings() {
withObservationTracking {
_ = model.settings.calendarEnabled
_ = model.settings.calendarFeatures
_ = model.settings.calendarFirstWeekday
_ = model.settings.language
_ = model.settings.appearance
} onChange: { [weak self] in
Task { @MainActor in
self?.update()
self?.observeSettings()
}
}
}
func update() {
guard model.settings.calendarEnabled else {
dismiss()
if let item { NSStatusBar.system.removeStatusItem(item) }
item = nil
timer?.invalidate()
timer = nil
return
}
if item == nil {
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
item.autosaveName = "XStats.calendar"
item.button?.target = self
item.button?.action = #selector(clicked(_:))
item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
item.button?.image = NSImage(systemSymbolName: "calendar", accessibilityDescription: tr("日历"))
item.button?.imagePosition = .imageLeading
item.button?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
self.item = item
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
MainActor.assumeIsolated { self?.refreshTitle() }
}
timer?.tolerance = 2
}
refreshTitle()
panel?.refreshHeight()
}
private func refreshTitle() {
guard let button = item?.button else { return }
let formatter = DateFormatter()
formatter.locale = L10n.locale
formatter.calendar = CalendarEngine.gregorian()
formatter.timeZone = .autoupdatingCurrent
formatter.setLocalizedDateFormatFromTemplate(model.settings.calendarFeatures.contains(.weekdays) ? "MdEEE" : "Md")
button.title = " " + formatter.string(from: Date())
formatter.dateStyle = .full
button.toolTip = formatter.string(from: Date())
}
@objc private func clicked(_ sender: NSStatusBarButton) {
if NSApp.currentEvent?.type == .rightMouseUp, let item {
let menu = NSMenu()
menu.addItem(withTitle: tr("日历设置…"), action: #selector(openSettings), keyEquivalent: "").target = self
menu.addItem(withTitle: tr("退出 XStats"), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
item.menu = menu
sender.performClick(nil)
item.menu = nil
} else {
show()
}
}
func show() {
guard let button = item?.button, let window = button.window else { return }
if panel == nil {
let model = self.model
let panel = StatusPanel(width: CalendarPopover.width, minHeight: 380,
content: { CalendarPopover().environment(model) },
measuring: { CalendarPopover().environment(model).environment(\.isSnapshot, true) })
panel.onVisibilityChange = { [weak self] visible in self?.item?.button?.highlight(visible) }
self.panel = panel
}
refreshTitle()
let anchor = window.convertToScreen(button.convert(button.bounds, to: nil))
panel?.toggle(below: anchor, on: window.screen)
}
func dismiss() { panel?.dismiss() }
@objc private func openSettings() {
dismiss()
model.openMainWindow(.settingsMenuBar)
}
}
@@ -0,0 +1,250 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
import Localization
import SwiftUI
/// 月历与黄历详情在同一个面板内切换;返回时保留浏览月份和选中的日期。
struct CalendarPopover: View {
static let width: CGFloat = 560
@Environment(AppModel.self) private var model
@Environment(\.isSnapshot) private var isSnapshot
@State private var month: CalendarMonth
@State private var selected: CalendarDay?
@State private var days: [CalendarDay]
@State private var lastTodayID: String?
@State private var showsDayDetails: Bool
@State private var almanac: CalendarAlmanac?
private let referenceDate: Date?
init(referenceDate: Date? = nil, showsDayDetails: Bool = false) {
self.referenceDate = referenceDate
let today = CalendarEngine.today(at: referenceDate ?? Date())
let month = CalendarMonth(year: today?.year ?? 2026, month: today?.month ?? 1)
_month = State(initialValue: month)
_selected = State(initialValue: today)
_showsDayDetails = State(initialValue: showsDayDetails)
_almanac = State(initialValue: showsDayDetails ? today.flatMap { CalendarEngine.almanac(for: $0) } : nil)
_lastTodayID = State(initialValue: today?.id)
_days = State(initialValue: CalendarEngine.month(year: month.year, month: month.month, firstWeekday: 2))
}
var body: some View {
TimelineView(.periodic(from: .now, by: 60)) { context in
let today = CalendarEngine.today(at: referenceDate ?? context.date)
VStack(spacing: 0) {
Group {
if showsDayDetails { detailHeader(today: today) }
else { header(today: today) }
}
.padding(18)
PageScroll {
VStack(spacing: 16) {
if showsDayDetails, let selected, let almanac {
CalendarAlmanacView(day: selected, almanac: almanac, features: model.settings.calendarFeatures)
} else {
calendarGrid(today: today)
}
if model.settings.calendarFeatures.contains(.holidays), !CalendarEngine.hasHolidayData(year: month.year) {
Text(tr("该年份暂无中国法定假日与调休数据"))
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.horizontal, 8)
.padding(.bottom, 8)
}
.environment(\.isPopover, true)
}
.onChange(of: today?.id) { _, newID in
// 正在看今天时跨日自动跟进;浏览历史日期时保留用户选择。
if selected?.id == lastTodayID, let today {
selected = today
month = CalendarMonth(year: today.year, month: today.month)
}
lastTodayID = newID
}
}
.frame(width: Self.width)
.frame(maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
.fixedSize(horizontal: false, vertical: isSnapshot)
.background(DS.Palette.background, in: RoundedRectangle(cornerRadius: DS.Radius.xl))
.appLanguageEnvironment()
.overlay { RoundedRectangle(cornerRadius: DS.Radius.xl).strokeBorder(DS.Palette.border) }
.onAppear { reload() }
.onChange(of: month) { _, _ in reload() }
.onChange(of: selected?.id) { _, _ in refreshAlmanac() }
.onChange(of: model.settings.calendarFirstWeekday) { _, _ in reload() }
.onReceive(NotificationCenter.default.publisher(for: .NSSystemTimeZoneDidChange)) { _ in reload() }
.onReceive(NotificationCenter.default.publisher(for: .NSCalendarDayChanged)) { _ in
guard referenceDate == nil, let today = CalendarEngine.today() else { return }
if selected?.id == lastTodayID { goToToday(today) }
lastTodayID = today.id
}
}
private func header(today: CalendarDay?) -> some View {
HStack(spacing: 10) {
Image(systemName: "calendar").foregroundStyle(Color.accentColor)
Picker(tr("年份"), selection: $month.year) {
ForEach(CalendarMonth.years, id: \.self) { Text(String($0)).tag($0) }
}
.labelsHidden().fixedSize()
Picker(tr("月份"), selection: $month.month) {
ForEach(1...12, id: \.self) { value in Text(monthName(value)).tag(value) }
}
.labelsHidden().fixedSize()
Spacer(minLength: 0)
Button { shift(-1) } label: { Image(systemName: "chevron.left") }
.help(tr("上个月")).accessibilityLabel(tr("上个月"))
.disabled(month.shifted(by: -1) == nil)
Button(tr("今天")) { if let today { goToToday(today) } }
Button { shift(1) } label: { Image(systemName: "chevron.right") }
.help(tr("下个月")).accessibilityLabel(tr("下个月"))
.disabled(month.shifted(by: 1) == nil)
Button { model.openMainWindow(.settingsMenuBar) } label: { Image(systemName: "gearshape") }
.help(tr("日历设置…")).accessibilityLabel(tr("日历设置…"))
}
.buttonStyle(.borderless)
}
private func calendarGrid(today: CalendarDay?) -> some View {
let features = model.settings.calendarFeatures
let firstWeekday = model.settings.calendarFirstWeekday
return VStack(spacing: 8) {
if features.contains(.weekdays) {
HStack(spacing: 4) {
ForEach(0..<7) { offset in
let weekday = (firstWeekday - 1 + offset) % 7 + 1
Text(weekdayName(weekday))
.font(.system(size: 12, weight: .medium))
.foregroundStyle(weekday == 1 || weekday == 7 ? Color.red : .secondary)
.frame(maxWidth: .infinity)
}
}
.padding(.bottom, 4)
}
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 4), count: 7), spacing: 5) {
ForEach(days) { day in
CalendarDayCell(day: day, features: features, isCurrentMonth: day.month == month.month,
isToday: day.id == today?.id, isSelected: day.id == selected?.id) {
selected = day
if day.month != month.month, CalendarMonth.years.contains(day.year) {
month = CalendarMonth(year: day.year, month: day.month)
}
showsDayDetails = true
refreshAlmanac()
}
}
}
}
}
private func detailHeader(today: CalendarDay?) -> some View {
HStack {
Button {
showsDayDetails = false
} label: {
Label(tr("返回月历"), systemImage: "chevron.backward")
}
Spacer()
Button(tr("今天")) {
if let today { goToToday(today) }
}
Button { model.openMainWindow(.settingsMenuBar) } label: { Image(systemName: "gearshape") }
.help(tr("日历设置…")).accessibilityLabel(tr("日历设置…"))
}
.buttonStyle(.borderless)
}
private func refreshAlmanac() {
guard showsDayDetails, let selected, almanac?.id != selected.id else { return }
almanac = CalendarEngine.almanac(for: selected)
}
private func reload() {
selected = CalendarEngine.selection(selected, in: month)
days = CalendarEngine.month(year: month.year, month: month.month, firstWeekday: model.settings.calendarFirstWeekday)
}
private func shift(_ delta: Int) {
guard let next = month.shifted(by: delta) else { return }
month = next
selected = CalendarEngine.day(year: next.year, month: next.month, day: 1)
}
private func goToToday(_ today: CalendarDay) {
month = CalendarMonth(year: today.year, month: today.month)
selected = today
}
private func monthName(_ month: Int) -> String {
let formatter = DateFormatter()
formatter.locale = L10n.locale
formatter.calendar = CalendarEngine.gregorian()
return formatter.monthSymbols[month - 1]
}
private func weekdayName(_ weekday: Int) -> String {
let formatter = DateFormatter()
formatter.locale = L10n.locale
formatter.calendar = CalendarEngine.gregorian()
return formatter.shortWeekdaySymbols[weekday - 1]
}
}
private struct CalendarDayCell: View {
let day: CalendarDay
let features: Set<CalendarFeature>
let isCurrentMonth: Bool
let isToday: Bool
let isSelected: Bool
let action: () -> Void
private var holiday: CalendarDay.Holiday? { features.contains(.holidays) ? day.holiday : nil }
private var isRest: Bool { holiday.map { !$0.isWork } ?? day.isWeekend }
private var foreground: Color { isSelected ? .white : isRest ? .red : DS.Palette.textPrimary }
var body: some View {
Button(action: action) {
VStack(spacing: 3) {
Text(String(day.day)).font(.system(size: 21, weight: isToday ? .semibold : .regular, design: .rounded))
Text(tr(day.subtitle(features: features)))
.font(.system(size: 10))
.lineLimit(1)
.minimumScaleFactor(0.75)
if features.contains(.ganzhi) {
Text(day.ganzhiDay).font(.system(size: 9)).opacity(0.75)
}
}
.foregroundStyle(foreground)
.frame(maxWidth: .infinity)
.frame(height: 66)
.background(isSelected ? Color.accentColor : holiday != nil ? foreground.opacity(0.05) : .clear,
in: RoundedRectangle(cornerRadius: 10))
.overlay {
if isToday && !isSelected { RoundedRectangle(cornerRadius: 10).strokeBorder(Color.accentColor, lineWidth: 1.5) }
}
.overlay(alignment: .topTrailing) {
if let holiday {
Text(tr(holiday.isWork ? "班" : "休"))
.font(.system(size: 9, weight: .medium))
.foregroundStyle(.white)
.padding(2)
.background(holiday.isWork ? Color.secondary : .red, in: RoundedRectangle(cornerRadius: 3))
.padding(2)
} else if isToday {
Circle().fill(isSelected ? .white : Color.accentColor).frame(width: 5, height: 5).padding(5)
}
}
.opacity(isCurrentMonth ? 1 : 0.35)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel([day.id, features.contains(.lunar) ? day.lunarSummary : "", tr(day.subtitle(features: features)),
holiday.map { tr($0.isWork ? "调休上班" : "放假") } ?? "", isToday ? tr("今天") : ""]
.filter { !$0.isEmpty }.joined(separator: ", "))
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}
@@ -0,0 +1,48 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Localization
import SwiftUI
struct CalendarSettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@Bindable var settings = model.settings
SettingsGroup(caption: tr("日历")) {
GroupRow(showsDivider: false) {
SettingRow(title: tr("菜单栏日历"), subtitle: tr("独立显示日期,点击打开月历;不受指标合并布局影响"), icon: "calendar") {
DSToggle(isOn: $settings.calendarEnabled, label: tr("菜单栏日历"))
}
}
if settings.calendarEnabled {
GroupRow {
SettingRow(title: tr("每周开始于")) {
Picker(tr("每周开始于"), selection: $settings.calendarFirstWeekday) {
Text(tr("星期一")).tag(2)
Text(tr("星期日")).tag(1)
}
.labelsHidden()
.fixedSize()
}
}
ForEach(CalendarFeature.allCases) { feature in
GroupRow {
SettingRow(title: feature.title, subtitle: feature == .ganzhi ? tr("控制月历格中的日干支;黄历详情始终显示年月日干支") : nil) {
DSToggle(isOn: Binding(get: { settings.calendarFeatures.contains(feature) }, set: { enabled in
if enabled { settings.calendarFeatures.insert(feature) }
else { settings.calendarFeatures.remove(feature) }
}), label: feature.title)
}
}
}
GroupRow {
Text(tr("公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。"))
.dsFont(.xs)
.foregroundStyle(DS.Palette.textSecondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
}
@@ -285,6 +285,7 @@ struct MenuBarSettings: View {
@Bindable var settings = model.settings
MenuBarPreview()
CalendarSettings()
SettingsGroup(caption: tr("布局")) {
GroupRow(showsDivider: false) {
@@ -17,6 +17,12 @@ enum SnapshotRenderer {
static func run(outputDirectory: URL) async {
try? FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
// 日历快照只渲染固定日期,不启动系统采样、AI 扫描或磁盘扫描。
if CommandLine.arguments.contains("--calendar-only") {
renderCalendar(outputDirectory: outputDirectory)
return
}
let defaults = UserDefaults(suiteName: "XStats.snapshot") ?? .standard
let settings = AppSettings(defaults: defaults)
settings.language = L10n.language
@@ -116,6 +122,29 @@ enum SnapshotRenderer {
print(tr("截图已输出到 \(outputDirectory.path)"))
}
private static func renderCalendar(outputDirectory: URL) {
let name = "XStats.calendarSnapshot.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: name) else { return }
defer { defaults.removePersistentDomain(forName: name) }
let settings = AppSettings(defaults: defaults)
settings.calendarEnabled = true
let model = AppModel(settings: settings, historyURL: nil, aiUsageProviders: [])
guard let september = CalendarEngine.day(year: 2026, month: 9, day: 23)?.date,
let summer = CalendarEngine.day(year: 2024, month: 6, day: 11)?.date else { return }
for (name, suffix) in [(NSAppearance.Name.aqua, "light"), (.darkAqua, "dark")] {
guard let appearance = NSAppearance(named: name) else { continue }
NSApp.appearance = appearance
settings.calendarFeatures = CalendarFeature.defaults
write(CalendarPopover(referenceDate: september), model: model, appearance: appearance,
to: outputDirectory.appendingPathComponent("calendar-\(suffix).png"))
write(CalendarPopover(referenceDate: september, showsDayDetails: true), model: model, appearance: appearance,
to: outputDirectory.appendingPathComponent("calendar-almanac-\(suffix).png"))
settings.calendarFeatures = Set(CalendarFeature.allCases)
write(CalendarPopover(referenceDate: summer, showsDayDetails: true), model: model, appearance: appearance,
to: outputDirectory.appendingPathComponent("calendar-details-\(suffix).png"))
}
}
/// 用真实的 NSHostingView 放进离屏窗口截图,渲染路径与 App 内一致
private static func write<V: View>(_ view: V, model: AppModel, appearance: NSAppearance, to url: URL) {
let hosting = NSHostingView(rootView: view
@@ -393,6 +393,17 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
public final class AppSettings {
@ObservationIgnored private let defaults: UserDefaults
/// 日历独立于性能指标,即使指标合并也保留单独入口;旧用户默认关闭。
public var calendarEnabled: Bool {
didSet { defaults.set(calendarEnabled, forKey: Keys.calendarEnabled) }
}
public var calendarFeatures: Set<CalendarFeature> {
didSet { defaults.set(calendarFeatures.map(\.rawValue).sorted(), forKey: Keys.calendarFeatures) }
}
public var calendarFirstWeekday: Int {
didSet { defaults.set(calendarFirstWeekday, forKey: Keys.calendarFirstWeekday) }
}
public var menuBarItems: Set<MenuBarItem> {
didSet { defaults.set(menuBarItems.map(\.rawValue).sorted(), forKey: Keys.menuBarItems) }
}
@@ -545,6 +556,10 @@ public final class AppSettings {
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
calendarEnabled = defaults.bool(forKey: Keys.calendarEnabled)
calendarFeatures = defaults.stringArray(forKey: Keys.calendarFeatures)
.map { Set($0.compactMap(CalendarFeature.init(rawValue:))) } ?? CalendarFeature.defaults
calendarFirstWeekday = defaults.integer(forKey: Keys.calendarFirstWeekday) == 1 ? 1 : 2
let items = defaults.stringArray(forKey: Keys.menuBarItems)?.compactMap(MenuBarItem.init(rawValue:))
menuBarItems = Set(items ?? [.cpu, .memory, .network])
menuBarStyle = defaults.string(forKey: Keys.menuBarStyle).flatMap(MenuBarStyle.init(rawValue:)) ?? .stacked
@@ -638,6 +653,9 @@ public final class AppSettings {
}
private enum Keys {
static let calendarEnabled = "calendarEnabled"
static let calendarFeatures = "calendarFeatures"
static let calendarFirstWeekday = "calendarFirstWeekday"
static let menuBarItems = "menuBarItems"
static let menuBarStyle = "menuBarStyle"
static let networkStyle = "networkStyle"
@@ -15,6 +15,9 @@ import WebDAVSync
/// 应用时逐项校验,不认识的值跳过
public struct SettingsDocument: Codable, Equatable, Sendable {
public var schema: Int? = 1
public var calendarEnabled: Bool?
public var calendarFeatures: [String]?
public var calendarFirstWeekday: Int?
public var menuBarItems: [String]?
public var menuBarStyle: String?
public var networkStyle: String?
@@ -49,6 +52,9 @@ extension AppSettings {
/// 显式列出可同步的偏好;WebDAV 地址、用户名和密码不会进入备份。
public func exportDocument() -> SettingsDocument {
var doc = SettingsDocument()
doc.calendarEnabled = calendarEnabled
doc.calendarFeatures = calendarFeatures.map(\.rawValue).sorted()
doc.calendarFirstWeekday = calendarFirstWeekday
doc.menuBarItems = menuBarItems.map(\.rawValue).sorted()
doc.menuBarStyle = menuBarStyle.rawValue
doc.networkStyle = networkStyle.rawValue
@@ -88,6 +94,9 @@ extension AppSettings {
value.flatMap { options.contains($0) ? $0 : nil }
}
assign(\.calendarEnabled, doc.calendarEnabled)
assign(\.calendarFeatures, doc.calendarFeatures.map { Set($0.compactMap(CalendarFeature.init(rawValue:))) })
assign(\.calendarFirstWeekday, option(doc.calendarFirstWeekday, in: [1, 2]))
assign(\.menuBarItems, doc.menuBarItems.map { Set($0.compactMap(MenuBarItem.init(rawValue:))) })
assign(\.menuBarStyle, doc.menuBarStyle.flatMap(MenuBarStyle.init(rawValue:)))
assign(\.networkStyle, doc.networkStyle.flatMap(NetworkMenuStyle.init(rawValue:)))
@@ -0,0 +1,52 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
import Testing
@testable import XStatsUI
@MainActor
struct CalendarAlmanacTests {
@Test func referenceDateMatchesAlmanacFields() throws {
let day = try #require(CalendarEngine.day(year: 2026, month: 9, day: 23))
let detail = try #require(CalendarEngine.almanac(for: day))
#expect(detail.sound == "壁上土")
#expect(detail.opposingZodiac == "马")
#expect(detail.ominousDirection == "南")
#expect(detail.twelveStar == "司命")
#expect(detail.twelveStarIsLucky)
#expect(detail.duty == "平")
#expect(detail.fetus == "占碓磨 房内南")
#expect(detail.pengZu == ["庚不经络织机虚张", "子不问卜自惹祸殃"])
#expect(detail.star == "箕水豹")
#expect(detail.starIsLucky)
#expect(detail.ganzhiSummary == "丙午(马)年 丁酉月 庚子日")
}
@Test func recommendationsAndAvoidancesComeFromTyme() throws {
let day = try #require(CalendarEngine.day(year: 2024, month: 6, day: 26))
let detail = try #require(CalendarEngine.almanac(for: day))
#expect(detail.recommends == ["嫁娶", "祭祀", "理发", "作灶", "修饰垣墙", "平治道涂", "整手足甲", "沐浴", "冠笄"])
#expect(detail.avoids == ["破土", "出行", "栽种"])
#expect(Set(detail.luckyGods).isDisjoint(with: Set(detail.unluckyGods)))
}
@Test func twelveHoursDoNotIncludeNextDaysLateZi() throws {
let day = try #require(CalendarEngine.day(year: 2026, month: 9, day: 23))
let detail = try #require(CalendarEngine.almanac(for: day))
#expect(detail.hours.count == 12)
#expect(detail.hours.map(\.branch) == ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"])
#expect(detail.hours.map(\.stem) == ["丙", "丁", "戊", "己", "庚", "辛", "壬", "癸", "甲", "乙", "丙", "丁"])
#expect(detail.hours.map(\.isLucky) == [true, true, false, true, false, false, true, false, true, true, false, false])
#expect(Set(detail.hours.map(\.id)).count == 12)
}
@Test(arguments: [(1900, 1, 1), (2023, 3, 22), (2100, 12, 31)])
func supportedBoundariesAndLunarLeapMonth(_ date: (Int, Int, Int)) throws {
let day = try #require(CalendarEngine.day(year: date.0, month: date.1, day: date.2))
let detail = try #require(CalendarEngine.almanac(for: day))
#expect(detail.hours.count == 12)
#expect(detail.sound.isEmpty == false)
#expect(detail.pengZu.count == 2)
}
}
@@ -0,0 +1,129 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
import Testing
@testable import XStatsUI
@MainActor
struct CalendarTests {
let zone = TimeZone(identifier: "Asia/Shanghai")!
@Test func monthGridIncludesLeapDayAndAdjacentMonths() throws {
let days = CalendarEngine.month(year: 2024, month: 2, firstWeekday: 2, timeZone: zone)
#expect(days.count == 42)
#expect(days.first?.id == "2024-01-29")
#expect(days.last?.id == "2024-03-10")
#expect(days.filter { $0.month == 2 }.count == 29)
let sunday = CalendarEngine.month(year: 2024, month: 2, firstWeekday: 1, timeZone: zone)
#expect(sunday.first?.id == "2024-01-28")
}
@Test func lunarLeapMonthAndFestivalPriority() throws {
let leap = try #require(CalendarEngine.day(year: 2023, month: 3, day: 22, timeZone: zone))
#expect(leap.lunarLabel == "闰二月")
let newYear = try #require(CalendarEngine.day(year: 2026, month: 2, day: 17, timeZone: zone))
#expect(newYear.festivals.contains("春节"))
#expect(newYear.subtitle(features: [.lunar, .festivals]) == "春节")
#expect(newYear.subtitle(features: [.lunar]) == "正月")
#expect(newYear.subtitle(features: []) == "")
}
@Test func restAndMakeupWorkAreDistinct() throws {
let rest = try #require(CalendarEngine.day(year: 2026, month: 9, day: 25, timeZone: zone))
let work = try #require(CalendarEngine.day(year: 2026, month: 9, day: 20, timeZone: zone))
#expect(rest.holiday?.isWork == false)
#expect(work.holiday?.isWork == true)
#expect(CalendarEngine.hasHolidayData(year: 2026))
#expect(CalendarEngine.hasHolidayData(year: 2099) == false)
}
@Test func termsAndSeasonalBoundaries() throws {
#expect(CalendarEngine.day(year: 2026, month: 9, day: 23, timeZone: zone)?.solarTerm == "秋分")
#expect(CalendarEngine.day(year: 2026, month: 9, day: 24, timeZone: zone)?.solarTerm == nil)
#expect(CalendarEngine.day(year: 2024, month: 6, day: 10, timeZone: zone)?.plumRain == nil)
#expect(CalendarEngine.day(year: 2024, month: 6, day: 11, timeZone: zone)?.plumRain == "入梅第1天")
#expect(CalendarEngine.day(year: 2024, month: 7, day: 6, timeZone: zone)?.plumRain == "出梅")
#expect(CalendarEngine.day(year: 2024, month: 7, day: 7, timeZone: zone)?.plumRain == nil)
#expect(CalendarEngine.day(year: 2012, month: 8, day: 8, timeZone: zone)?.dogDays == "末伏第2天")
}
@Test func optionalCalendarsHandleUnsupportedDates() throws {
let date = try #require(CalendarEngine.day(year: 2026, month: 5, day: 13, timeZone: zone))
let details = CalendarEngine.additionalCalendars(for: date, features: [.hijri, .tibetan])
#expect(details.first { $0.feature == .hijri }?.value == "1447年都尔喀尔德月26日")
#expect(details.first { $0.feature == .tibetan }?.value != nil)
let early = try #require(CalendarEngine.day(year: 1900, month: 1, day: 1, timeZone: zone))
#expect(CalendarEngine.additionalCalendars(for: early, features: [.tibetan]).first?.isUnavailable == true)
let late = try #require(CalendarEngine.day(year: 2100, month: 1, day: 1, timeZone: zone))
#expect(CalendarEngine.additionalCalendars(for: late, features: [.tibetan]).first?.isUnavailable == true)
#expect(CalendarEngine.additionalCalendars(for: date, features: []).isEmpty)
}
@Test(arguments: [(1951, 1, 7, true), (1951, 1, 8, false), (2051, 2, 11, false), (2051, 2, 12, true)])
func tibetanCoverageBoundaries(_ sample: (Int, Int, Int, Bool)) throws {
let day = try #require(CalendarEngine.day(year: sample.0, month: sample.1, day: sample.2, timeZone: zone))
let detail = try #require(CalendarEngine.additionalCalendars(for: day, features: [.tibetan]).first)
#expect(detail.isUnavailable == sample.3)
}
@Test func invalidDatesAndNavigationLimits() {
#expect(CalendarEngine.day(year: 2026, month: 2, day: 30, timeZone: zone) == nil)
#expect(CalendarEngine.month(year: 2026, month: 13, firstWeekday: 2, timeZone: zone).isEmpty)
#expect(CalendarMonth(year: 2026, month: 12).shifted(by: 1) == CalendarMonth(year: 2027, month: 1))
#expect(CalendarMonth(year: 1900, month: 1).shifted(by: -1) == nil)
#expect(CalendarMonth(year: 2100, month: 12).shifted(by: 1) == nil)
}
@Test func localDayChangesAcrossMidnightAndTimeZones() throws {
let instant = try #require(ISO8601DateFormatter().date(from: "2026-09-23T16:00:00Z"))
#expect(CalendarEngine.today(at: instant, timeZone: zone)?.id == "2026-09-24")
#expect(CalendarEngine.today(at: instant, timeZone: TimeZone(secondsFromGMT: 0)!)?.id == "2026-09-23")
let losAngeles = TimeZone(identifier: "America/Los_Angeles")!
let days = CalendarEngine.month(year: 2026, month: 3, firstWeekday: 2, timeZone: losAngeles)
#expect(Set(days.map(\.id)).count == 42)
}
@Test func historicalSelectionKeepsCivilDateAfterTimeZoneChange() throws {
let old = try #require(CalendarEngine.day(year: 2024, month: 3, day: 22, timeZone: zone))
let newZone = try #require(TimeZone(identifier: "America/Los_Angeles"))
let month = CalendarMonth(year: 2024, month: 3)
let selected = try #require(CalendarEngine.selection(old, in: month, timeZone: newZone))
#expect(selected.id == old.id)
#expect(selected.lunarSummary == old.lunarSummary)
#expect(selected.date != old.date)
let parts = CalendarEngine.gregorian(timeZone: newZone).dateComponents([.day, .hour], from: selected.date)
#expect(parts.day == 22)
#expect(parts.hour == 12)
#expect(CalendarEngine.selection(old, in: CalendarMonth(year: 2024, month: 4), timeZone: newZone)?.id == "2024-04-01")
}
@Test func preferencesPersistAndRoundTripWhileOldBackupsPreserveThem() throws {
let name = "CalendarTests.\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: name))
defer { defaults.removePersistentDomain(forName: name) }
let settings = AppSettings(defaults: defaults)
#expect(settings.calendarEnabled == false)
#expect(settings.calendarFeatures == CalendarFeature.defaults)
settings.calendarEnabled = true
settings.calendarFeatures = [.hijri, .ganzhi]
settings.calendarFirstWeekday = 1
let restored = AppSettings(defaults: defaults)
#expect(restored.calendarEnabled)
#expect(restored.calendarFeatures == [.hijri, .ganzhi])
#expect(restored.calendarFirstWeekday == 1)
let doc = try JSONDecoder().decode(SettingsDocument.self, from: JSONEncoder().encode(settings.exportDocument()))
restored.calendarFeatures = []
restored.apply(doc)
#expect(restored.calendarFeatures == [.hijri, .ganzhi])
restored.apply(SettingsDocument())
#expect(restored.calendarEnabled)
#expect(restored.calendarFeatures == [.hijri, .ganzhi])
var invalid = SettingsDocument()
invalid.calendarFeatures = ["hijri", "future-feature"]
invalid.calendarFirstWeekday = 99
restored.apply(invalid)
#expect(restored.calendarFeatures == [.hijri])
#expect(restored.calendarFirstWeekday == 1)
}
}
+15
View File
@@ -29,6 +29,21 @@ All system-monitoring metrics are read on your own Mac and are never uploaded. N
</div>
## Calendar
Enable the independent date item in **Settings → Menu Bar → Calendar**. Browse months and years,
return to today, and select a day for details. Gregorian dates stay visible; lunar dates, weekdays,
festivals, solar terms, Chinese holidays and makeup workdays, sexagenary cycles, dog days and
traditional plum-rain days each have a display switch. Tibetan and Hijri details are opt-in.
The item stays separate when metrics are combined; calendar preferences are included in WebDAV backups.
Calculations run locally using [Tyme4Swift 1.5.0](https://github.com/6tail/tyme4swift), licensed under MIT
(see [third-party notices](ThirdPartyNotices.md)). Browse 1900–2100; Tibetan data covers Gregorian
1951-01-08 through 2051-02-11. Chinese holiday data currently ends in 2026; unsupported dates show a notice.
Plum-rain days follow traditional calendar rules and are not weather forecasts.
Click a day for its traditional almanac: favorable and avoided activities, Na Yin, clashes, day deities, twelve-hour fortunes, twelve officers, fetal deity, Peng Zu taboos and lunar mansions. Returning to the month preserves the selection.
---
## Install
+13
View File
@@ -25,6 +25,19 @@ XStats は、Mac の各 CPU コア、GPU、メモリ負荷、通信速度、デ
</div>
## カレンダー
「設定 → メニューバー → カレンダー」で独立した日付表示を有効にできます。月・年の切り替え、
今日への移動、日付の詳細表示に対応します。西暦を常に表示し、旧暦、曜日、祝祭日、中国の振替出勤日、
二十四節気、干支、三伏、入梅・出梅は個別に切り替えられます。チベット暦とヒジュラ暦は初期設定では無効です。
設定は WebDAV バックアップに含まれます。
[Tyme4Swift 1.5.0](https://github.com/6tail/tyme4swift)(MIT、[第三者ライセンス](ThirdPartyNotices.md))
でローカル計算します。閲覧範囲は 1900〜2100 年、チベット暦のデータ範囲は西暦 1951-01-08〜2051-02-11 です。
中国の法定休日データは現在 2026 年までで、範囲外はその旨を表示します。入梅・出梅は伝統的な暦の計算で、天気予報ではありません。
日付をクリックすると、宜忌、納音、沖煞、値神、時辰の吉凶、建除十二直、吉神・凶神、胎神、彭祖百忌、二十八宿の詳細を表示します。月表示に戻っても選択日は保持されます。
## インストール
[GitHub Releases](https://github.com/ysicing/xstats/releases) から Apple Silicon 版をダウンロードしてください。
+13
View File
@@ -25,6 +25,19 @@ XStats는 CPU 코어별 사용량, GPU, 메모리 압력, 네트워크 속도,
</div>
## 달력
「설정 → 메뉴 막대 → 달력」에서 독립 날짜 항목을 켤 수 있습니다. 월·연도 이동, 오늘로 돌아가기,
날짜별 상세 정보를 지원합니다. 양력은 항상 표시하며 음력, 요일, 명절, 중국 공휴일과 대체 근무일,
절기, 간지, 삼복, 전통 입매·출매 날짜는 개별적으로 켜고 끌 수 있습니다. 티베트력과 이슬람력은 기본적으로 꺼져 있습니다.
달력 설정은 WebDAV 백업에 포함됩니다.
[Tyme4Swift 1.5.0](https://github.com/6tail/tyme4swift)(MIT, [타사 라이선스](ThirdPartyNotices.md))로
기기에서 계산합니다. 탐색 범위는 1900–2100년이며 티베트력 데이터는 양력 1951-01-08부터 2051-02-11까지입니다.
중국 공휴일 데이터는 현재 2026년까지 제공하며 범위 밖에서는 안내를 표시합니다. 전통 입매·출매 날짜는 일기 예보가 아닙니다.
날짜를 클릭하면 길한 활동과 피할 활동, 납음, 충살, 당일 신, 열두 시진의 길흉, 건제십이신, 길신·흉신, 태신, 팽조백기, 이십팔수를 표시합니다. 월 달력으로 돌아가도 선택한 날짜는 유지됩니다.
## 설치
[GitHub Releases](https://github.com/ysicing/xstats/releases)에서 Apple Silicon 버전을 다운로드하세요.
+12
View File
@@ -27,6 +27,18 @@ XStats 是一款 macOS 菜单栏应用,实时显示 Mac 正在做什么:各
</div>
## 日历
在「设置 → 菜单栏 → 日历」开启独立日期入口,点击查看月历。支持切换月份、年份、
返回今天和选择日期详情;公历始终显示,农历、星期、节日、节气、调休、干支、三伏天和梅雨天可分别开关。
藏历和回历默认关闭,开启后显示在日期详情中。日历不受指标合并布局影响,设置可随 WebDAV 备份。
历法由 [Tyme4Swift 1.5.0](https://github.com/6tail/tyme4swift) 在本机计算,采用 MIT 许可,
见[第三方声明](ThirdPartyNotices.md)。月历可浏览 1900–2100 年;藏历数据覆盖公历 1951-01-08 至 2051-02-11,
中国法定假日与调休数据当前收录到 2026 年,超出覆盖范围会提示。梅雨天是传统历法推算,并非天气预报。
点击日期可进入黄历详情,查看宜忌、纳音、冲煞、值神、十二时辰吉凶、建除十二神、吉凶神煞、胎神、彭祖百忌和二十八星宿;返回月历后保留原日期选择。
---
## 安装
+7
View File
@@ -87,3 +87,10 @@ XStats 未引入这些项目的可执行程序、Sparkle 更新模块或浏览
完整许可证文本见 [LICENSES/AI-Usage-and-OpenUsage-MIT.txt](LICENSES/AI-Usage-and-OpenUsage-MIT.txt)
与 [LICENSES/usage-bar-BSD-2-Clause.txt](LICENSES/usage-bar-BSD-2-Clause.txt)。
## Tyme4Swift
日历功能通过 Swift Package Manager 使用 [6tail/tyme4swift](https://github.com/6tail/tyme4swift)
1.5.0(MIT License,Copyright (c) 2026 6tail),提供公历、农历、藏历、回历、干支、
节气、节日、调休、三伏与传统梅雨日期计算。未修改上游源码。
完整许可见 [LICENSES/Tyme4Swift-MIT.txt](LICENSES/Tyme4Swift-MIT.txt)。