Files
2026-07-13 12:26:32 +08:00

405 lines
15 KiB
C#

// Copyright 2020 Takuto Nakamura
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Microsoft.Win32;
using RunCat365.Properties;
using System.Diagnostics;
using System.Globalization;
using FormsTimer = System.Windows.Forms.Timer;
namespace RunCat365
{
internal static class Program
{
[STAThread]
static void Main()
{
#if DEBUG
var defaultCultureInfo = SupportedLanguage.English.GetDefaultCultureInfo();
#else
var defaultCultureInfo = SupportedLanguageExtension.GetCurrentLanguage().GetDefaultCultureInfo();
#endif
CultureInfo.CurrentUICulture = defaultCultureInfo;
CultureInfo.CurrentCulture = defaultCultureInfo;
using var procMutex = new Mutex(true, "_RUNCAT_MUTEX", out var result);
if (!result) return;
try
{
ApplicationConfiguration.Initialize();
Application.SetColorMode(SystemColorMode.System);
Application.Run(new RunCat365ApplicationContext());
}
finally
{
procMutex?.ReleaseMutex();
}
}
}
internal class RunCat365ApplicationContext : ApplicationContext
{
private const int FETCH_TIMER_DEFAULT_INTERVAL = 1000;
private const int FETCH_COUNTER_SIZE = 5;
private const int ANIMATE_TIMER_DEFAULT_INTERVAL = 200;
private readonly CPURepository cpuRepository;
private readonly GPURepository gpuRepository;
private readonly MemoryRepository memoryRepository;
private readonly TemperatureRepository temperatureRepository;
private readonly StorageRepository storageRepository;
private readonly NetworkRepository networkRepository;
private readonly CustomRunnerRepository customRunnerRepository;
private readonly LaunchAtStartupManager launchAtStartupManager;
private readonly ContextMenuManager contextMenuManager;
private readonly FormsTimer fetchTimer;
private readonly FormsTimer animateTimer;
private Runner runner = Runner.Cat;
private Theme manualTheme = Theme.System;
private TemperatureUnit temperatureUnit = TemperatureUnit.System;
private FPSMaxLimit fpsMaxLimit = FPSMaxLimit.FPS40;
private SpeedSource speedSource = SpeedSource.CPU;
private string? customRunnerName;
private int fetchCounter = 5;
private bool isFetching;
public RunCat365ApplicationContext()
{
UserSettings.Default.Reload();
_ = Enum.TryParse(UserSettings.Default.Runner, out runner);
_ = Enum.TryParse(UserSettings.Default.Theme, out manualTheme);
_ = Enum.TryParse(UserSettings.Default.TemperatureUnit, out temperatureUnit);
_ = Enum.TryParse(UserSettings.Default.FPSMaxLimit, out fpsMaxLimit);
_ = Enum.TryParse(UserSettings.Default.SpeedSource, out speedSource);
customRunnerName = string.IsNullOrEmpty(UserSettings.Default.CustomRunnerName)
? null
: UserSettings.Default.CustomRunnerName;
SystemEvents.UserPreferenceChanged += new UserPreferenceChangedEventHandler(UserPreferenceChanged);
cpuRepository = new CPURepository();
gpuRepository = new GPURepository();
memoryRepository = new MemoryRepository();
temperatureRepository = new TemperatureRepository();
storageRepository = new StorageRepository();
networkRepository = new NetworkRepository();
customRunnerRepository = new CustomRunnerRepository();
launchAtStartupManager = new LaunchAtStartupManager();
ResolveSpeedSource();
contextMenuManager = new ContextMenuManager(
() => runner,
r => ChangeRunner(r),
customRunnerRepository,
() => customRunnerName,
name => ApplyCustomRunner(name),
deletedName => HandleCustomRunnerDeleted(deletedName),
() => GetSystemTheme(),
() => manualTheme,
t => ChangeManualTheme(t),
() => speedSource,
s => ChangeSpeedSource(s),
s => IsSpeedSourceAvailable(s),
() => fpsMaxLimit,
f => ChangeFPSMaxLimit(f),
() => temperatureUnit,
u => ChangeTemperatureUnit(u),
() => launchAtStartupManager.GetStartup(),
s => launchAtStartupManager.ToggleStartup(s),
() => OpenProjectPage(),
() => Application.Exit()
);
if (customRunnerName is not null)
{
ApplyCustomRunner(customRunnerName);
}
animateTimer = new FormsTimer
{
Interval = ANIMATE_TIMER_DEFAULT_INTERVAL
};
animateTimer.Tick += new EventHandler(AnimationTick);
animateTimer.Start();
fetchTimer = new FormsTimer
{
Interval = FETCH_TIMER_DEFAULT_INTERVAL
};
fetchTimer.Tick += new EventHandler(FetchTick);
fetchTimer.Start();
ShowBalloonTipIfNeeded();
}
private static Theme GetSystemTheme()
{
var keyName = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
using var rKey = Registry.CurrentUser.OpenSubKey(keyName);
if (rKey is null) return Theme.Light;
var value = rKey.GetValue("SystemUsesLightTheme");
if (value is null) return Theme.Light;
return (int)value == 0 ? Theme.Dark : Theme.Light;
}
private bool IsSpeedSourceAvailable(SpeedSource speedSource)
{
return speedSource switch
{
SpeedSource.CPU => true,
SpeedSource.GPU => gpuRepository.IsAvailable,
SpeedSource.Memory => true,
_ => false,
};
}
private void ResolveSpeedSource()
{
if (!IsSpeedSourceAvailable(speedSource))
{
ChangeSpeedSource(SpeedSource.CPU);
}
}
private void ShowBalloonTipIfNeeded()
{
if (!cpuRepository.IsAvailable)
{
contextMenuManager.ShowBalloonTip(BalloonTipType.CPUInfoUnavailable);
}
else if (UserSettings.Default.FirstLaunch)
{
contextMenuManager.ShowBalloonTip(BalloonTipType.AppLaunched);
UserSettings.Default.FirstLaunch = false;
UserSettings.Default.Save();
}
}
private void UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
if (e.Category != UserPreferenceCategory.General) return;
var systemTheme = GetSystemTheme();
if (contextMenuManager.HasActiveCustomIcons)
{
contextMenuManager.RecolorActiveCustomIcons(systemTheme, manualTheme);
}
else
{
contextMenuManager.SetIcons(systemTheme, manualTheme, runner);
}
}
private static void OpenProjectPage()
{
try
{
Process.Start(new ProcessStartInfo()
{
FileName = "https://github.com/runcat-dev/RunCat365",
UseShellExecute = true
});
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.Message}");
}
}
private void ChangeRunner(Runner r)
{
runner = r;
customRunnerName = null;
UserSettings.Default.Runner = runner.ToString();
UserSettings.Default.CustomRunnerName = string.Empty;
UserSettings.Default.Save();
}
private void ApplyCustomRunner(string name)
{
var frames = customRunnerRepository.LoadFrames(name);
if (frames.Count == 0) return;
customRunnerName = name;
UserSettings.Default.CustomRunnerName = name;
UserSettings.Default.Save();
contextMenuManager.SetCustomIcons(frames, GetSystemTheme(), manualTheme);
foreach (var frame in frames) frame.Dispose();
}
private void RevertToBuiltInRunner()
{
customRunnerName = null;
UserSettings.Default.CustomRunnerName = string.Empty;
UserSettings.Default.Save();
contextMenuManager.SetIcons(GetSystemTheme(), manualTheme, runner);
}
private void HandleCustomRunnerDeleted(string deletedName)
{
if (!string.Equals(customRunnerName, deletedName, StringComparison.OrdinalIgnoreCase)) return;
RevertToBuiltInRunner();
}
private void ChangeManualTheme(Theme t)
{
manualTheme = t;
UserSettings.Default.Theme = manualTheme.ToString();
UserSettings.Default.Save();
}
private void ChangeTemperatureUnit(TemperatureUnit u)
{
temperatureUnit = u;
UserSettings.Default.TemperatureUnit = temperatureUnit.ToString();
UserSettings.Default.Save();
}
private void ChangeSpeedSource(SpeedSource s)
{
speedSource = s;
UserSettings.Default.SpeedSource = speedSource.ToString();
UserSettings.Default.Save();
}
private void ChangeFPSMaxLimit(FPSMaxLimit f)
{
fpsMaxLimit = f;
UserSettings.Default.FPSMaxLimit = fpsMaxLimit.ToString();
UserSettings.Default.Save();
}
private void AnimationTick(object? sender, EventArgs e)
{
contextMenuManager.AdvanceFrame();
}
private string GetInfoDescription(CPUInfo cpuInfo, GPUInfo? gpuInfo, MemoryInfo memoryInfo, TemperatureInfo? temperatureInfo)
{
var baseDescription = speedSource switch
{
SpeedSource.CPU => cpuInfo.GetDescription(),
SpeedSource.GPU => gpuInfo?.GetDescription() ?? "",
SpeedSource.Memory => memoryInfo.GetDescription(),
_ => "",
};
var temperatureDescription = temperatureInfo?.GetDescription(temperatureUnit) ?? "";
return string.IsNullOrEmpty(temperatureDescription) ? baseDescription : $"{baseDescription}\n{temperatureDescription}";
}
private int CalculateInterval(CPUInfo cpuInfo, GPUInfo? gpuInfo, MemoryInfo memoryInfo)
{
var load = speedSource switch
{
SpeedSource.CPU => cpuInfo.Total,
SpeedSource.GPU => gpuInfo?.Maximum ?? 0f,
SpeedSource.Memory => memoryInfo.MemoryLoad,
_ => 0f,
};
var speed = (float)Math.Max(1.0f, (load / 5.0f) * fpsMaxLimit.GetRate());
return (int)(500.0f / speed);
}
private int FetchSystemInfo()
{
var cpuInfo = cpuRepository.Get();
var gpuInfo = gpuRepository.Get();
var memoryInfo = memoryRepository.Get();
var temperatureInfo = temperatureRepository.Get();
var storageInfo = storageRepository.Get();
var networkInfo = networkRepository.Get();
contextMenuManager.SetNotifyIconText(GetInfoDescription(cpuInfo, gpuInfo, memoryInfo, temperatureInfo));
var systemInfoValues = new List<string>();
systemInfoValues.AddRange(cpuInfo.GenerateIndicator());
if (gpuInfo.HasValue)
{
systemInfoValues.AddRange(gpuInfo.Value.GenerateIndicator());
}
systemInfoValues.AddRange(memoryInfo.GenerateIndicator());
if (temperatureInfo.HasValue)
{
systemInfoValues.AddRange(temperatureInfo.Value.GenerateIndicator(temperatureUnit));
}
systemInfoValues.AddRange(storageInfo.GenerateIndicator());
if (networkInfo.HasValue)
{
systemInfoValues.AddRange(networkInfo.Value.GenerateIndicator());
}
contextMenuManager.SetSystemInfoMenuText(string.Join("\n", [.. systemInfoValues]));
return CalculateInterval(cpuInfo, gpuInfo, memoryInfo);
}
private async void FetchTick(object? sender, EventArgs e)
{
if (isFetching) return;
isFetching = true;
try
{
var doHeavySlice = await Task.Run(() =>
{
cpuRepository.Update();
gpuRepository.Update();
fetchCounter += 1;
if (fetchCounter < FETCH_COUNTER_SIZE) return false;
fetchCounter = 0;
temperatureRepository.Update();
memoryRepository.Update();
storageRepository.Update();
networkRepository.Update();
return true;
});
if (!doHeavySlice) return;
var interval = FetchSystemInfo();
animateTimer.Stop();
animateTimer.Interval = interval;
animateTimer.Start();
}
catch (Exception exception)
{
Console.WriteLine($"FetchTick failed: {exception.Message}");
}
finally
{
isFetching = false;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
SystemEvents.UserPreferenceChanged -= UserPreferenceChanged;
animateTimer?.Stop();
animateTimer?.Dispose();
fetchTimer?.Stop();
fetchTimer?.Dispose();
cpuRepository?.Close();
gpuRepository?.Close();
temperatureRepository?.Close();
contextMenuManager?.HideNotifyIcon();
contextMenuManager?.Dispose();
}
base.Dispose(disposing);
}
}
}