Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,031 changes: 72 additions & 1,959 deletions Cargo.lock

Large diffs are not rendered by default.

18 changes: 3 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,15 @@ windows = { version = "0.62", features = [
"Win32_System_Variant",
"Win32_System_Memory",
"Win32_System_Com_StructuredStorage",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Direct3D",
"Media",
"Win32_Networking_WinHttp",
"Win32_Storage_FileSystem",
"Media_Control",
"Foundation",
] }
webview2-com = "0.39"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
image = "0.24"
winreg = "0.51"
dirs = "5.0"
log = "0.4"
env_logger = "0.10"
thiserror = "1.0"
lazy_static = "1.4"
base64 = "0.21"
reqwest = { version = "0.11", features = ["blocking", "json"] }
log = { version = "0.4", features = ["std"] }
semver = "1.0"

[build-dependencies]
Expand Down
87 changes: 56 additions & 31 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::thread;
use windows::Win32::UI::WindowsAndMessaging::*;
use windows::Win32::System::Com::*;
use windows::Win32::Foundation::*;
use windows::Win32::System::Threading::GetCurrentThreadId;
use crate::settings::Settings;
use crate::tray::TrayIcon;
use crate::events::AppEvent;
use crate::timer;
use crate::overlay::{OverlayWindow, capture, blur, webview_env};
use crate::settings_ui::SettingsWindow;

pub const WM_APP_EVENT: u32 = WM_USER + 1;
static mut MAIN_THREAD_ID: u32 = 0;

pub fn wakeup_main_thread() {
unsafe {
if MAIN_THREAD_ID != 0 {
let _ = PostThreadMessageW(MAIN_THREAD_ID, WM_APP_EVENT, WPARAM(0), LPARAM(0));
}
}
}

pub struct App {
pub settings: Arc<RwLock<Settings>>,
pub paused: Arc<AtomicBool>,
Expand All @@ -33,6 +44,8 @@ impl App {
let paused = Arc::new(AtomicBool::new(false));
let session_paused = Arc::new(AtomicBool::new(false));
let is_dark_mode = crate::system::is_dark_mode();

unsafe { MAIN_THREAD_ID = GetCurrentThreadId(); }

Self {
settings,
Expand All @@ -50,10 +63,6 @@ impl App {
}

pub fn init(&mut self) -> windows::core::Result<()> {
unsafe {
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
}

let _ = webview_env::init_global_env();
let _ = self.settings.read().unwrap().update_autostart();
crate::system::set_tray_menu_theme(self.is_dark_mode);
Expand Down Expand Up @@ -81,6 +90,25 @@ impl App {
Ok(())
}

pub fn run(&mut self) -> windows::core::Result<()> {
self.init()?;

unsafe {
let mut msg = MSG::default();
// Pulse every 1s as a secondary safety, though WM_APP_EVENT is primary
let _ = SetTimer(None, 1, 1000, None);

while GetMessageW(&mut msg, None, 0, 0).as_bool() {
if msg.message == WM_APP_EVENT || (msg.message == WM_TIMER && msg.wParam.0 == 1) {
self.drain_events();
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
Ok(())
}

pub fn handle_event(&mut self, event: AppEvent) {
match event {
AppEvent::ShowOverlay => {
Expand All @@ -94,6 +122,8 @@ impl App {
self.resume_media();
}
self.reminder_overlay = None;
blur::flush_buffers();
capture::flush_buffer();
}
AppEvent::SettingsClosed => {
self.settings_window = None;
Expand Down Expand Up @@ -131,8 +161,14 @@ impl App {
let event_tx = self.event_tx.clone();
thread::spawn(move || {
match crate::updater::check_for_updates() {
Ok(info) => { let _ = event_tx.send(AppEvent::UpdateStatus(info)); }
Err(e) => { let _ = event_tx.send(AppEvent::UpdateError(e.to_string())); }
Ok(info) => {
let _ = event_tx.send(AppEvent::UpdateStatus(info));
wakeup_main_thread();
}
Err(e) => {
let _ = event_tx.send(AppEvent::UpdateError(e.to_string()));
wakeup_main_thread();
}
}
});
}
Expand All @@ -146,6 +182,7 @@ impl App {
thread::spawn(move || {
if let Err(e) = crate::updater::download_and_install(event_tx.clone()) {
let _ = event_tx.send(AppEvent::UpdateError(e.to_string()));
wakeup_main_thread();
}
});
}
Expand Down Expand Up @@ -206,7 +243,7 @@ impl App {
}

fn show_overlay_optimized(&mut self) {
let (width, height, data) = if let Some(bg) = self.pre_captured_bg.read().unwrap().clone() {
let (blurred_width, blurred_height, data) = if let Some(bg) = self.pre_captured_bg.read().unwrap().clone() {
bg
} else {
if let Ok(captured) = capture::capture_virtual_screen() {
Expand All @@ -215,12 +252,17 @@ impl App {
} else { return; }
};

let current_settings = self.settings.read().unwrap().clone();
if let Ok(overlay) = OverlayWindow::new(self.event_tx.clone(), width, height, data, current_settings) {
overlay.update_theme(self.is_dark_mode);
crate::system::apply_immersive_dark_mode(overlay.hwnd, self.is_dark_mode);
overlay.fade_in();
self.reminder_overlay = Some(overlay);
unsafe {
let screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
let current_settings = self.settings.read().unwrap().clone();

if let Ok(overlay) = OverlayWindow::new(self.event_tx.clone(), screen_width, screen_height, blurred_width, blurred_height, data, current_settings) {
overlay.update_theme(self.is_dark_mode);
crate::system::apply_immersive_dark_mode(overlay.hwnd, self.is_dark_mode);
overlay.fade_in();
self.reminder_overlay = Some(overlay);
}
}

let mut lock = self.pre_captured_bg.write().unwrap();
Expand Down Expand Up @@ -261,12 +303,8 @@ mod internal_tests {
#[test]
fn test_app_logic_methods() {
let mut app = App::new();

// Test media logic (smoke)
app.pause_media();
app.resume_media();

// Test handle_event with variants that don't trigger real UI popups
app.handle_event(AppEvent::TogglePause);
app.handle_event(AppEvent::SettingsClosed);
app.handle_event(AppEvent::SessionLocked);
Expand All @@ -283,38 +321,25 @@ mod internal_tests {
app.handle_event(AppEvent::CheckForUpdates);
app.handle_event(AppEvent::StartUpdate);
app.handle_event(AppEvent::Quit);

// Test media logic with mock-ish calls
app.pause_media();
app.resume_media();

// Test init (smoke)
// Now safe because windows are hidden in tests
let _ = app.init();

// Test draining
app.event_tx.send(AppEvent::UserDismissed).unwrap();
app.drain_events();
}

#[test]
fn test_app_window_management_logic() {
let mut app = App::new();

// Test overlay closing logic
use windows::Win32::Foundation::HWND;
use crate::overlay::OverlayWindow;
app.reminder_overlay = Some(OverlayWindow { hwnd: HWND(1 as *mut _) });
app.handle_event(AppEvent::HideOverlay);
assert!(app.reminder_overlay.is_none());

// Test settings window closing logic
use crate::settings_ui::SettingsWindow;
app.settings_window = Some(SettingsWindow { hwnd: HWND(1 as *mut _) });
app.handle_event(AppEvent::SettingsClosed);
assert!(app.settings_window.is_none());

// Test session events with dummy windows
app.settings_window = Some(SettingsWindow { hwnd: HWND(1 as *mut _) });
app.handle_event(AppEvent::SessionLocked);
app.handle_event(AppEvent::SessionUnlocked);
Expand Down
115 changes: 34 additions & 81 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
#![windows_subsystem = "windows"]

use windows::Win32::UI::WindowsAndMessaging::*;
use windows::Win32::Foundation::*;
use pausecat::app::App;
use pausecat::settings::Settings;

struct SimpleFileLogger {
file: std::sync::Mutex<std::fs::File>,
}

impl log::Log for SimpleFileLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::Level::Info
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
if let Ok(mut file) = self.file.lock() {
use std::io::Write;
let _ = writeln!(file, "[{}] {}",
record.level(),
record.args());
}
}
}
fn flush(&self) {}
}

fn setup_logging() -> windows::core::Result<()> {
let mut path = dirs::config_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
path.push("PauseCat");
let mut path = Settings::get_config_dir();
let _ = std::fs::create_dir_all(&path);
path.push("app.log");

Expand All @@ -16,97 +35,31 @@ fn setup_logging() -> windows::core::Result<()> {
.open(&path)
.map_err(|e| windows::core::Error::from_hresult(windows::core::HRESULT(e.raw_os_error().unwrap_or(-1) as i32)))?;

let target = Box::new(file);
env_logger::Builder::from_default_env()
.target(env_logger::Target::Pipe(target))
.filter_level(log::LevelFilter::Info)
.init();
let logger = SimpleFileLogger { file: std::sync::Mutex::new(file) };
let _ = log::set_boxed_logger(Box::new(logger));
log::set_max_level(log::LevelFilter::Info);

log::info!("PauseCat started (Optimized)");
log::info!("PauseCat started (Ultra-Optimized)");
Ok(())
}

fn check_webview2() -> windows::core::Result<bool> {
use webview2_com::Microsoft::Web::WebView2::Win32::*;
unsafe {
let mut version = windows::core::PWSTR::null();
let result = GetAvailableCoreWebView2BrowserVersionString(windows::core::PCWSTR::null(), &mut version);
let exists = result.is_ok();
if !version.is_null() {
windows::Win32::System::Com::CoTaskMemFree(Some(version.0 as *const _));
}
Ok(exists)
}
}

pub fn is_settings_mode() -> bool {
std::env::args().any(|arg| arg == "--settings")
}

fn main() -> windows::core::Result<()> {
let _ = setup_logging();

unsafe {
use windows::Win32::System::Threading::CreateMutexW;
let _handle = CreateMutexW(None, true, windows::core::w!("Global\\PauseCatSingleInstanceMutex"));
if GetLastError() == ERROR_ALREADY_EXISTS {
return Ok(());
}
}

match check_webview2() {
Ok(true) => log::info!("WebView2 found."),
_ => {
unsafe {
MessageBoxW(
None,
windows::core::w!("PauseCat requires WebView2 Runtime."),
windows::core::w!("Error"),
MB_OK | MB_ICONERROR,
);
}
return Ok(());
}
}

let mut app = App::new();
if let Err(e) = app.init() {
return Err(e);
}

if is_settings_mode() {
app.handle_event(pausecat::events::AppEvent::OpenSettings);
}

unsafe {
let _ = SetTimer(None, 1, 100, None);

let mut msg = MSG::default();
while GetMessageW(&mut msg, None, 0, 0).into() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
app.drain_events();
}
let _ = windows::Win32::System::Com::CoInitializeEx(None, windows::Win32::System::Com::COINIT_APARTMENTTHREADED);
let _ = setup_logging();
let mut app = App::new();
app.run()?;
windows::Win32::System::Com::CoUninitialize();
}

Ok(())
}

#[cfg(test)]
mod internal_tests {
mod tests {
use super::*;

#[test]
fn test_main_helpers_smoke() {
let _ = check_webview2();
let _ = setup_logging();

// Mock args for is_settings_mode
let _ = is_settings_mode();
}

#[test]
fn test_mutex_logic_smoke() {
fn test_main_startup_smoke() {
unsafe {
use windows::Win32::System::Threading::CreateMutexW;
let mutex_name = windows::core::w!("Global\\PauseCatTestMutex");
Expand Down
Loading
Loading