8 Commits

Author SHA1 Message Date
Your Name
6b584efb40 Bump version to 0.1.1 2026-02-07 11:16:29 +02:00
Your Name
b01dbd6c0b Tighten TimeSpinner spacing and fix build warnings
Reduce gap between numbers and h/m unit labels in the workday
schedule spinners. Remove unused parse_hour function and fix
Svelte state_referenced_locally warning in BreakScreen.
2026-02-07 11:16:29 +02:00
Your Name
d93d231a45 Enable custom-protocol for embedded frontend assets
Without this feature, Tauri falls back to devUrl even in release
builds. The tauri CLI adds it automatically but direct cargo
builds need it in Cargo.toml.
2026-02-07 11:00:45 +02:00
Your Name
4f4599c4c9 Fix WebView2 detection - use loader API instead of registry
Registry check gave false negatives on systems where WebView2
is installed through Edge rather than EdgeUpdate. Now calls
GetAvailableCoreWebView2BrowserVersionString (statically linked)
which detects all installation methods.
2026-02-07 10:51:49 +02:00
Your Name
e9021e51e5 Statically link WebView2Loader - single exe, no DLL needed
build.rs swaps the dynamic import library with the static archive
from webview2-com-sys, so the WebView2 loader code is baked into
the exe. msvc_compat.rs provides the MSVC CRT symbols (security
cookie, thread-safe init, C++ operators) that the MSVC-compiled
static library expects.
2026-02-07 02:32:54 +02:00
Your Name
37d0d638d5 Detect missing WebView2 at startup, show download link
Checks Windows registry for WebView2 Runtime before Tauri
initializes. If missing, shows a native MessageBox explaining
the requirement and opens the Microsoft download page.
2026-02-07 02:08:51 +02:00
Your Name
6bba2835bb Fix fullscreen break screen - centering, progress bar, ripple sizing
- Center break content on fullscreen (was top-left aligned)
- Move fullscreen progress bar to screen bottom (was overlapping buttons)
- Scale ripple circles to match ring size per mode (140/160/200px)
- Increase ripple expansion for more dramatic spread
2026-02-07 01:56:10 +02:00
Your Name
3aeb83f69b Add screenshots, move philosophy section above features 2026-02-07 01:36:43 +02:00
16 changed files with 325 additions and 53 deletions

View File

@@ -41,6 +41,62 @@ Core Cooldown is a single portable `.exe`. No installer, no account, no telemetr
--- ---
## 💡 Philosophy
Core Cooldown exists because rest is not a luxury. It is a fundamental need that no productivity framework, no employer, and no software platform should gatekeep behind a paywall or a subscription.
This application:
- **Collects nothing.** No analytics, no telemetry, no usage tracking, no crash reports phoned home. Your break habits are your own business.
- **Costs nothing.** Not "free tier with limitations." Not "free for personal use." Free, unconditionally, for everyone.
- **Requires nothing.** No account, no email, no app store, no internet connection after download. It runs on your machine and answers to you alone.
- **Owns nothing.** Released under CC0 - the most complete relinquishment of rights possible under law. There is no owner. There are no restrictions. The code belongs to the commons.
Tools for human wellbeing should never be enclosed, never be scarce, and never serve a master other than the person using them.
<br />
---
## 🖼️ Screenshots
<p align="center">
<img src="screenshots/01-dashboard.png" alt="Dashboard - Main Timer" width="420" /><br />
<sub><strong>Dashboard</strong> - Focus timer with countdown ring, status pill, and quick controls</sub>
</p>
<br />
<p align="center">
<img src="screenshots/02-stats.png" alt="Statistics" width="420" /><br />
<sub><strong>Statistics</strong> - Daily summary, compliance rate, streaks, and 7-day history chart</sub>
</p>
<br />
<p align="center">
<img src="screenshots/03-settings.png" alt="Settings" width="420" /><br />
<sub><strong>Settings</strong> - Grouped configuration cards with live preview</sub>
</p>
<br />
<p align="center">
<img src="screenshots/04-break.png" alt="Break Screen" width="420" /><br />
<sub><strong>Break Screen</strong> - Always-on-top break overlay with activity suggestions</sub>
</p>
<br />
<p align="center">
<img src="screenshots/05-mini.png" alt="Mini Mode" width="300" /><br />
<sub><strong>Mini Mode</strong> - Compact floating timer, click-through until hovered</sub>
</p>
<br />
---
## Features ## Features
### ⏱️ Timer & Breaks ### ⏱️ Timer & Breaks
@@ -465,23 +521,6 @@ The best software is built through mutual aid - people helping people because it
--- ---
## 💡 Philosophy
Core Cooldown exists because rest is not a luxury. It is a fundamental need that no productivity framework, no employer, and no software platform should gatekeep behind a paywall or a subscription.
This application:
- **Collects nothing.** No analytics, no telemetry, no usage tracking, no crash reports phoned home. Your break habits are your own business.
- **Costs nothing.** Not "free tier with limitations." Not "free for personal use." Free, unconditionally, for everyone.
- **Requires nothing.** No account, no email, no app store, no internet connection after download. It runs on your machine and answers to you alone.
- **Owns nothing.** Released under CC0 - the most complete relinquishment of rights possible under law. There is no owner. There are no restrictions. The code belongs to the commons.
Tools for human wellbeing should never be enclosed, never be scarce, and never serve a master other than the person using them.
<br />
---
## 📄 License ## 📄 License
<p align="center"> <p align="center">

View File

@@ -1,7 +1,7 @@
{ {
"name": "core-cooldown", "name": "core-cooldown",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
screenshots/02-stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
screenshots/03-settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
screenshots/04-break.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
screenshots/05-mini.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "core-cooldown" name = "core-cooldown"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
[lib] [lib]
@@ -11,7 +11,7 @@ crate-type = ["lib", "staticlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = ["tray-icon"] } tauri = { version = "2", features = ["tray-icon", "custom-protocol"] }
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
tauri-plugin-global-shortcut = "2" tauri-plugin-global-shortcut = "2"
@@ -22,4 +22,4 @@ chrono = "0.4"
anyhow = "1" anyhow = "1"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser", "sysinfoapi", "windef"] } winapi = { version = "0.3", features = ["winuser", "sysinfoapi", "windef", "shellapi"] }

View File

@@ -7,5 +7,75 @@ fn main() {
std::env::set_var("PATH", mingw_bin); std::env::set_var("PATH", mingw_bin);
} }
// On GNU targets, replace the WebView2Loader import library with the static
// library so the loader is baked into the exe — no DLL to ship.
#[cfg(target_env = "gnu")]
swap_webview2_to_static();
tauri_build::build() tauri_build::build()
} }
/// Replace `WebView2Loader.dll.lib` (dynamic import lib) with the contents of
/// `WebView2LoaderStatic.lib` (static archive) in the webview2-com-sys build
/// output. The linker then statically links the WebView2 loader code, removing
/// the runtime dependency on WebView2Loader.dll.
#[cfg(target_env = "gnu")]
fn swap_webview2_to_static() {
use std::fs;
use std::path::PathBuf;
let out_dir = std::env::var("OUT_DIR").unwrap_or_default();
// OUT_DIR = target/.../build/core-cooldown-HASH/out
// We need: target/.../build/ (two levels up)
let build_dir = PathBuf::from(&out_dir)
.parent() // core-cooldown-HASH
.and_then(|p| p.parent()) // build/
.map(|p| p.to_path_buf());
let build_dir = match build_dir {
Some(d) => d,
None => return,
};
let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH")
.unwrap_or_default()
.as_str()
{
"x86_64" => "x64",
"x86" => "x86",
"aarch64" => "arm64",
_ => return,
};
let entries = match fs::read_dir(&build_dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with("webview2-com-sys-") {
continue;
}
let lib_dir = entry.path().join("out").join(target_arch);
let import_lib = lib_dir.join("WebView2Loader.dll.lib");
let static_lib = lib_dir.join("WebView2LoaderStatic.lib");
if static_lib.exists() && import_lib.exists() {
if let Ok(static_bytes) = fs::read(&static_lib) {
match fs::write(&import_lib, &static_bytes) {
Ok(_) => println!(
"cargo:warning=Swapped WebView2Loader to static linking ({})",
lib_dir.display()
),
Err(e) => println!(
"cargo:warning=Failed to swap WebView2Loader lib: {}",
e
),
}
}
}
}
}

View File

@@ -1,4 +1,6 @@
mod config; mod config;
#[cfg(all(windows, target_env = "gnu"))]
mod msvc_compat;
mod stats; mod stats;
mod timer; mod timer;

View File

@@ -2,5 +2,66 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
#[cfg(windows)]
if !webview2_check::is_webview2_installed() {
webview2_check::show_missing_dialog();
return;
}
core_cooldown_lib::run() core_cooldown_lib::run()
} }
#[cfg(windows)]
mod webview2_check {
use std::ptr;
use winapi::um::winuser::{MessageBoxW, MB_OK, MB_ICONWARNING};
use winapi::um::shellapi::ShellExecuteW;
// Statically linked from WebView2LoaderStatic.lib via msvc_compat shims
extern "system" {
fn GetAvailableCoreWebView2BrowserVersionString(
browser_executable_folder: *const u16,
version_info: *mut *mut u16,
) -> i32;
}
extern "system" {
fn CoTaskMemFree(pv: *mut std::ffi::c_void);
}
fn to_wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
pub fn is_webview2_installed() -> bool {
unsafe {
let mut version_info: *mut u16 = ptr::null_mut();
let hr = GetAvailableCoreWebView2BrowserVersionString(
ptr::null(),
&mut version_info,
);
let installed = hr == 0 && !version_info.is_null();
if !version_info.is_null() {
CoTaskMemFree(version_info as *mut _);
}
installed
}
}
pub fn show_missing_dialog() {
let title = to_wide("Core Cooldown - WebView2 Required");
let message = to_wide(
"Microsoft WebView2 Runtime is required to run Core Cooldown, \
but it was not found on this system.\n\n\
Click OK to open the download page in your browser.\n\n\
After installing WebView2, restart Core Cooldown."
);
let url = to_wide("https://developer.microsoft.com/en-us/microsoft-edge/webview2/consumer");
let open = to_wide("open");
unsafe {
MessageBoxW(ptr::null_mut(), message.as_ptr(), title.as_ptr(), MB_OK | MB_ICONWARNING);
ShellExecuteW(ptr::null_mut(), open.as_ptr(), url.as_ptr(), ptr::null(), ptr::null(), 1);
}
}
}

View File

@@ -0,0 +1,98 @@
//! Compatibility shims for MSVC CRT symbols required by WebView2LoaderStatic.lib.
//!
//! When statically linking the WebView2 loader on GNU/MinGW, the MSVC-compiled
//! object code references these symbols. We provide minimal implementations so
//! the linker can resolve them.
use std::sync::atomic::{AtomicI32, Ordering};
// ── MSVC Buffer Security Check (/GS) ────────────────────────────────────────
//
// MSVC's /GS flag instruments functions with stack canaries. These two symbols
// implement the canary check. The cookie value is arbitrary — real MSVC CRT
// randomises it at startup, but for a statically-linked helper library this
// fixed sentinel is sufficient.
#[no_mangle]
pub static __security_cookie: u64 = 0x00002B992DDFA232;
#[no_mangle]
pub unsafe extern "C" fn __security_check_cookie(cookie: u64) {
if cookie != __security_cookie {
std::process::abort();
}
}
// ── MSVC Thread-Safe Static Initialisation ───────────────────────────────────
//
// C++11 guarantees that function-local statics are initialised exactly once,
// even under concurrent access. MSVC implements this with an epoch counter and
// a set of helper functions. The WebView2 loader uses a few statics internally.
//
// Simplified implementation: uses an atomic spin for the guard. This is safe
// because WebView2 initialisation runs on the main thread in practice.
#[no_mangle]
pub static _Init_thread_epoch: AtomicI32 = AtomicI32::new(0);
#[no_mangle]
pub unsafe extern "C" fn _Init_thread_header(guard: *mut i32) {
if guard.is_null() {
return;
}
// Spin until we can claim the guard (-1 = uninitialized, 0 = done, 1 = in progress)
loop {
let val = guard.read_volatile();
if val == 0 {
// Already initialised — tell caller to skip
return;
}
if val == -1 {
// Not yet initialised — try to claim it
guard.write_volatile(1);
return;
}
// val == 1: another thread is initialising — yield and retry
std::thread::yield_now();
}
}
#[no_mangle]
pub unsafe extern "C" fn _Init_thread_footer(guard: *mut i32) {
if !guard.is_null() {
guard.write_volatile(0); // Mark initialisation complete
_Init_thread_epoch.fetch_add(1, Ordering::Release);
}
}
// ── MSVC C++ Runtime Operators (mangled names) ───────────────────────────────
//
// The static library is compiled with MSVC, which uses its own C++ name mangling.
// MinGW's libstdc++ exports the same operators but with GCC/Itanium mangling,
// so the linker can't match them. We provide the MSVC-mangled versions here.
/// `std::nothrow` — MSVC-mangled `?nothrow@std@@3Unothrow_t@1@B`
/// An empty struct constant used as a tag for nothrow `new`.
#[export_name = "?nothrow@std@@3Unothrow_t@1@B"]
pub static MSVC_STD_NOTHROW: u8 = 0;
/// `operator new(size_t, const std::nothrow_t&)` — nothrow allocation
/// MSVC-mangled: `??2@YAPEAX_KAEBUnothrow_t@std@@@Z`
#[export_name = "??2@YAPEAX_KAEBUnothrow_t@std@@@Z"]
pub unsafe extern "C" fn msvc_operator_new_nothrow(size: usize, _nothrow: *const u8) -> *mut u8 {
let size = if size == 0 { 1 } else { size };
let layout = std::alloc::Layout::from_size_align_unchecked(size, 8);
let ptr = std::alloc::alloc(layout);
ptr // null on failure — nothrow semantics
}
/// `operator delete(void*, size_t)` — sized deallocation
/// MSVC-mangled: `??3@YAXPEAX_K@Z`
#[export_name = "??3@YAXPEAX_K@Z"]
pub unsafe extern "C" fn msvc_operator_delete_sized(ptr: *mut u8, size: usize) {
if !ptr.is_null() {
let size = if size == 0 { 1 } else { size };
let layout = std::alloc::Layout::from_size_align_unchecked(size, 8);
std::alloc::dealloc(ptr, layout);
}
}

View File

@@ -462,14 +462,6 @@ pub enum IdleCheckResult {
NaturalBreakDetected(u64), // duration in seconds NaturalBreakDetected(u64), // duration in seconds
} }
fn parse_hour(time_str: &str) -> u32 {
time_str
.split(':')
.next()
.and_then(|h| h.parse().ok())
.unwrap_or(9)
}
/// Returns the number of seconds since last user input (mouse/keyboard). /// Returns the number of seconds since last user input (mouse/keyboard).
#[cfg(windows)] #[cfg(windows)]
pub fn get_idle_seconds() -> u64 { pub fn get_idle_seconds() -> u64 {

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json", "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "Core Cooldown", "productName": "Core Cooldown",
"version": "0.1.0", "version": "0.1.1",
"identifier": "com.corecooldown.app", "identifier": "com.corecooldown.app",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",

View File

@@ -13,7 +13,7 @@
let { standalone = false }: Props = $props(); let { standalone = false }: Props = $props();
const appWindow = standalone ? getCurrentWebviewWindow() : null; const appWindow = $derived(standalone ? getCurrentWebviewWindow() : null);
let currentActivity = $state<BreakActivity>(pickRandomActivity()); let currentActivity = $state<BreakActivity>(pickRandomActivity());
let activityCycleTimer: ReturnType<typeof setInterval> | null = null; let activityCycleTimer: ReturnType<typeof setInterval> | null = null;
@@ -168,23 +168,19 @@
{:else} {:else}
<!-- ── In-app break screen: full-screen fill OR modal with backdrop ── --> <!-- ── In-app break screen: full-screen fill OR modal with backdrop ── -->
<div <div
class="relative h-full" class="relative h-full flex items-center justify-center"
class:flex={isModal} style="background: #000;"
class:items-center={isModal}
class:justify-center={isModal}
style={isModal ? `background: #000;` : ""}
> >
<div <div
class="relative flex flex-col" class="relative flex flex-col items-center"
class:h-full={!isModal}
class:break-modal={isModal} class:break-modal={isModal}
> >
<!-- Break ring with breathing pulse + ripples --> <!-- Break ring with breathing pulse + ripples -->
<div class="mb-6 relative" use:scaleIn={{ duration: 0.6, delay: 0.1 }}> <div class="mb-6 relative" use:scaleIn={{ duration: 0.6, delay: 0.1 }}>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none"> <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div class="break-ripple ripple-1" style="--ripple-color: {$config.break_color}"></div> <div class="break-ripple ripple-1" style="--ripple-color: {$config.break_color}; --ripple-size: {isModal ? 160 : 200}px"></div>
<div class="break-ripple ripple-2" style="--ripple-color: {$config.break_color}"></div> <div class="break-ripple ripple-2" style="--ripple-color: {$config.break_color}; --ripple-size: {isModal ? 160 : 200}px"></div>
<div class="break-ripple ripple-3" style="--ripple-color: {$config.break_color}"></div> <div class="break-ripple ripple-3" style="--ripple-color: {$config.break_color}; --ripple-size: {isModal ? 160 : 200}px"></div>
</div> </div>
<div class="break-breathe relative"> <div class="break-breathe relative">
@@ -267,16 +263,30 @@
{/if} {/if}
{/if} {/if}
<!-- Bottom progress bar for modal - uses clip-path to respect border radius --> <!-- Bottom progress bar for modal -->
<div class="break-modal-progress-container"> {#if isModal}
<div class="break-modal-progress-track"> <div class="break-modal-progress-container">
<div class="break-modal-progress-track">
<div
class="h-full transition-[width] duration-1000 ease-linear"
style="width: {$timer.breakProgress * 100}%; background: {barGradient};"
></div>
</div>
</div>
{/if}
</div>
<!-- Fullscreen progress bar - anchored to bottom of screen -->
{#if !isModal}
<div class="absolute bottom-8 left-12 right-12 h-[3px] rounded-full overflow-hidden">
<div class="w-full h-full" style="background: rgba(255,255,255,0.03);">
<div <div
class="h-full transition-[width] duration-1000 ease-linear" class="h-full transition-[width] duration-1000 ease-linear"
style="width: {$timer.breakProgress * 100}%; background: {barGradient};" style="width: {$timer.breakProgress * 100}%; background: {barGradient};"
></div> ></div>
</div> </div>
</div> </div>
</div> {/if}
</div> </div>
{/if} {/if}
@@ -393,8 +403,8 @@
/* ── Ripple circles ── */ /* ── Ripple circles ── */
.break-ripple { .break-ripple {
position: absolute; position: absolute;
width: 140px; width: var(--ripple-size, 140px);
height: 140px; height: var(--ripple-size, 140px);
border-radius: 50%; border-radius: 50%;
border: 1.5px solid var(--ripple-color); border: 1.5px solid var(--ripple-color);
opacity: 0; opacity: 0;
@@ -407,10 +417,10 @@
@keyframes ripple-expand { @keyframes ripple-expand {
0% { 0% {
transform: scale(1); transform: scale(1);
opacity: 0.3; opacity: 0.25;
} }
100% { 100% {
transform: scale(2.2); transform: scale(2.5);
opacity: 0; opacity: 0;
} }
} }

View File

@@ -324,14 +324,14 @@
.time-spinner { .time-spinner {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 2px;
user-select: none; user-select: none;
touch-action: none; touch-action: none;
} }
.wheel-field { .wheel-field {
position: relative; position: relative;
width: 50px; width: 44px;
height: 36px; height: 36px;
border-radius: 8px; border-radius: 8px;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
@@ -376,13 +376,13 @@
line-height: 1; line-height: 1;
backface-visibility: hidden; backface-visibility: hidden;
pointer-events: none; pointer-events: none;
padding-right: 12px; padding-right: 6px;
} }
/* Unit label pinned to the right of the field */ /* Unit label pinned to the right of the field */
.unit-badge { .unit-badge {
position: absolute; position: absolute;
right: 5px; right: 3px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
font-size: 10px; font-size: 10px;