diff --git a/KeyStats.Windows/KeyStats/Models/AppSettings.cs b/KeyStats.Windows/KeyStats/Models/AppSettings.cs
index 37d45f6..65e2f34 100644
--- a/KeyStats.Windows/KeyStats/Models/AppSettings.cs
+++ b/KeyStats.Windows/KeyStats/Models/AppSettings.cs
@@ -16,6 +16,9 @@ public class AppSettings
[JsonPropertyName("clickNotifyThreshold")]
public int ClickNotifyThreshold { get; set; } = 1000;
+ [JsonPropertyName("keyFatigueNotifyEnabled")]
+ public bool KeyFatigueNotifyEnabled { get; set; }
+
[JsonPropertyName("launchAtStartup")]
public bool LaunchAtStartup { get; set; }
diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.cs b/KeyStats.Windows/KeyStats/Properties/Strings.cs
index ef22dee..f5dd025 100644
--- a/KeyStats.Windows/KeyStats/Properties/Strings.cs
+++ b/KeyStats.Windows/KeyStats/Properties/Strings.cs
@@ -27,6 +27,9 @@ public static class Strings
public static string Notif_ThresholdTitle => Get(nameof(Notif_ThresholdTitle));
public static string Notif_KeyThresholdReachedFormat => Get(nameof(Notif_KeyThresholdReachedFormat));
public static string Notif_ClickThresholdReachedFormat => Get(nameof(Notif_ClickThresholdReachedFormat));
+ public static string Notif_KeyFatigueBodyFormat => Get(nameof(Notif_KeyFatigueBodyFormat));
+ public static string NotifSettings_FatigueTitle => Get(nameof(NotifSettings_FatigueTitle));
+ public static string NotifSettings_FatigueHint => Get(nameof(NotifSettings_FatigueHint));
public static string Tray_OpenMainWindow => Get(nameof(Tray_OpenMainWindow));
public static string Tray_Settings => Get(nameof(Tray_Settings));
diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.resx b/KeyStats.Windows/KeyStats/Properties/Strings.resx
index 6be7731..b713e22 100644
--- a/KeyStats.Windows/KeyStats/Properties/Strings.resx
+++ b/KeyStats.Windows/KeyStats/Properties/Strings.resx
@@ -66,6 +66,9 @@
KeyStats Reminder
Today's key presses reached {0:N0}.
Today's clicks reached {0:N0}.
+ Key {0} today has exceeded 1/3 of its 7-day average ({1:N0} presses).
+ Key Fatigue Reminder (Top 4 Keys)
+ Ranks keys by total presses over the last 7 days. If any of the top 4 keys exceeds 1/3 of its own daily average today, you'll be reminded once.
Open Main Window
Settings
diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx
index cbd3005..3fb4b8a 100644
--- a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx
+++ b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx
@@ -66,6 +66,9 @@
统计提醒
今日按键数已达到 {0:N0} 次
今日点击数已达到 {0:N0} 次
+ 按键 {0} 今日已达 {1:N0} 次,超过其近 7 日均值的 1/3,注意用指疲劳
+ 按键疲劳提醒(Top 4 按键)
+ 按近 7 天按键总数排名取前 4 个按键,任一键当日次数超过其自身日均值的 1/3 时,当天提醒一次。
打开主界面
设置
diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx
index d6fc94b..176e565 100644
--- a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx
+++ b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx
@@ -66,6 +66,9 @@
KeyStats 提醒
今日按鍵數已達 {0:N0} 次。
今日點擊數已達 {0:N0} 次。
+ 按鍵 {0} 今日已達 {1:N0} 次,超過其近 7 日均值的 1/3,注意用指疲勞
+ 按鍵疲勞提醒(Top 4 按鍵)
+ 按近 7 天按鍵總數排名取前 4 個按鍵,任一鍵當日次數超過其自身日均值的 1/3 時,當天提醒一次。
開啟主視窗
設定
diff --git a/KeyStats.Windows/KeyStats/Services/NotificationService.cs b/KeyStats.Windows/KeyStats/Services/NotificationService.cs
index e3625f8..a88427c 100644
--- a/KeyStats.Windows/KeyStats/Services/NotificationService.cs
+++ b/KeyStats.Windows/KeyStats/Services/NotificationService.cs
@@ -38,6 +38,23 @@ public void SendThresholdNotification(Metric metric, int count)
}
}
+ public void SendKeyFatigueNotification(string keyName, int threshold)
+ {
+ var body = string.Format(KeyStats.Properties.Strings.Notif_KeyFatigueBodyFormat, keyName, threshold);
+
+ try
+ {
+ new ToastContentBuilder()
+ .AddText(KeyStats.Properties.Strings.Notif_ThresholdTitle)
+ .AddText(body)
+ .Show();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error showing notification: {ex.Message}");
+ }
+ }
+
public void ClearNotifications()
{
try
diff --git a/KeyStats.Windows/KeyStats/Services/StatsManager.cs b/KeyStats.Windows/KeyStats/Services/StatsManager.cs
index 55c64e3..fac74d1 100644
--- a/KeyStats.Windows/KeyStats/Services/StatsManager.cs
+++ b/KeyStats.Windows/KeyStats/Services/StatsManager.cs
@@ -57,6 +57,12 @@ public enum StatsUpdateKind
private int _lastNotifiedKeyPresses;
private int _lastNotifiedClicks;
+ private const int KeyFatigueTopCount = 4;
+ private const double KeyFatigueRatio = 1.0 / 3.0;
+ private static readonly HashSet KeyFatigueExcludedKeys = new(StringComparer.Ordinal) { "Enter", "Space" }; // 排除回车/空格,避免干扰疲劳判断
+ private readonly Dictionary _fatigueThresholds = new(StringComparer.Ordinal); // Top4 键 -> 阈值
+ private readonly HashSet _fatigueNotifiedKeys = new(StringComparer.Ordinal); // 当日已提醒的键
+
public DailyStats CurrentStats { get; private set; }
public AppSettings Settings { get; private set; }
public Dictionary History { get; private set; } = new();
@@ -197,6 +203,7 @@ private void OnKeyPressed(string keyName, string appName, string displayName)
RecordKeyForPeakKPS();
NotifyStatsUpdate();
NotifyKeyPressThresholdIfNeeded();
+ NotifyKeyFatigueIfNeeded(keyName);
ScheduleSave();
}
@@ -1238,6 +1245,7 @@ private void UpdateNotificationBaselines()
{
_lastNotifiedKeyPresses = NormalizedBaseline(CurrentStats.KeyPresses, Settings.KeyPressNotifyThreshold);
_lastNotifiedClicks = NormalizedBaseline(CurrentStats.TotalClicks, Settings.ClickNotifyThreshold);
+ UpdateKeyFatigueBaseline();
}
private int NormalizedBaseline(int count, int threshold)
@@ -1282,6 +1290,56 @@ private void NotifyClickThresholdIfNeeded()
}
}
+ private void UpdateKeyFatigueBaseline()
+ {
+ lock (_lock)
+ {
+ var today = DateTime.Today;
+ var totals = new Dictionary(StringComparer.Ordinal);
+ for (var date = today.AddDays(-7); date < today; date = date.AddDays(1))
+ {
+ MergeKeyCounts(GetDailyStats(date).KeyPressCounts, totals);
+ }
+
+ var topKeys = totals
+ .Where(x => x.Value > 0 && !KeyFatigueExcludedKeys.Contains(x.Key))
+ .OrderByDescending(x => x.Value)
+ .ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
+ .Take(KeyFatigueTopCount);
+
+ _fatigueThresholds.Clear();
+ foreach (var pair in topKeys)
+ {
+ var dailyAvg = pair.Value / 7.0;
+ var threshold = (int)Math.Floor(dailyAvg * KeyFatigueRatio);
+ _fatigueThresholds[pair.Key] = Math.Max(1, threshold);
+ }
+
+ _fatigueNotifiedKeys.Clear();
+ }
+ }
+
+ private void NotifyKeyFatigueIfNeeded(string keyName)
+ {
+ if (!Settings.KeyFatigueNotifyEnabled) return;
+ if (string.IsNullOrWhiteSpace(keyName)) return;
+ if (_fatigueNotifiedKeys.Contains(keyName)) return;
+
+ int threshold;
+ lock (_lock)
+ {
+ if (_fatigueNotifiedKeys.Contains(keyName)) return;
+ if (!_fatigueThresholds.TryGetValue(keyName, out threshold)) return;
+ var todayCount = CurrentStats.KeyPressCounts.TryGetValue(keyName, out var count) ? count : 0;
+ if (todayCount <= threshold) return;
+ _fatigueNotifiedKeys.Add(keyName);
+
+ Console.WriteLine($"Key fatigue reminder: key '{keyName}' exceeded threshold today ({todayCount} presses > {threshold}), notification sent.");
+ }
+
+ NotificationService.Instance.SendKeyFatigueNotification(keyName, threshold);
+ }
+
#endregion
#region Formatting
diff --git a/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml b/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml
index e1a2ee6..38ceaa9 100644
--- a/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml
+++ b/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml
@@ -63,6 +63,20 @@
+
+
+
+
+
+
+
+