Skip to content

Commit 6840118

Browse files
committed
stopwatch
1 parent 6af3b83 commit 6840118

File tree

2 files changed

+237
-10
lines changed

2 files changed

+237
-10
lines changed

src/main.rs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use std::sync::{Arc, Mutex};
1717
use std::{env, fs};
1818
use tempfile::{tempdir, NamedTempFile};
1919
use timers::*;
20+
use timers::{check_stopwatch, reset_stopwatch, start_stopwatch, stop_stopwatch};
2021
use tracing_appender::non_blocking::WorkerGuard;
2122
use tracing_subscriber::filter::FilterFn;
2223
use tracing_subscriber::Registry;
@@ -592,12 +593,10 @@ fn call_fn(
592593
}
593594
}
594595

595-
"get_clipboard" => {
596-
match get_clipboard_string() {
597-
Ok(text) => Some(text),
598-
Err(e) => Some(e),
599-
}
600-
}
596+
"get_clipboard" => match get_clipboard_string() {
597+
Ok(text) => Some(text),
598+
Err(e) => Some(e),
599+
},
601600

602601
"qr_to_clipboard" => {
603602
let args = match serde_json::from_str::<serde_json::Value>(fn_args) {
@@ -703,6 +702,26 @@ fn call_fn(
703702
}
704703
}
705704

705+
"start_stopwatch" => {
706+
info!("Handling start_stopwatch function call.");
707+
Some(start_stopwatch())
708+
}
709+
710+
"stop_stopwatch" => {
711+
info!("Handling stop_stopwatch function call.");
712+
Some(stop_stopwatch())
713+
}
714+
715+
"check_stopwatch" => {
716+
info!("Handling check_stopwatch function call.");
717+
Some(check_stopwatch())
718+
}
719+
720+
"reset_stopwatch" => {
721+
info!("Handling reset_stopwatch function call.");
722+
Some(reset_stopwatch())
723+
}
724+
706725
_ => {
707726
println!("Unknown function: {}", fn_name);
708727
warn!("AI called unknown function: {}", fn_name);
@@ -1008,8 +1027,8 @@ fn qr_to_clipboard(text: &str) -> Result<(), String> {
10081027
}
10091028

10101029
fn get_clipboard_string() -> Result<String, String> {
1011-
let mut clipboard: ClipboardContext = ClipboardProvider::new()
1012-
.map_err(|e| format!("Failed to initialize clipboard: {}", e))?;
1030+
let mut clipboard: ClipboardContext =
1031+
ClipboardProvider::new().map_err(|e| format!("Failed to initialize clipboard: {}", e))?;
10131032
clipboard
10141033
.get_contents()
10151034
.map_err(|e| format!("Failed to read clipboard contents: {}", e))
@@ -1731,6 +1750,46 @@ async fn main() -> Result<(), Box<dyn Error>> {
17311750
"required": ["device_name"],
17321751
}))
17331752
.build().unwrap(),
1753+
1754+
ChatCompletionFunctionsArgs::default()
1755+
.name("start_stopwatch")
1756+
.description("Starts the stopwatch. If already running, does nothing. If paused, resumes from where it left off.")
1757+
.parameters(json!({
1758+
"type": "object",
1759+
"properties": {},
1760+
"required": [],
1761+
}))
1762+
.build().unwrap(),
1763+
1764+
ChatCompletionFunctionsArgs::default()
1765+
.name("stop_stopwatch")
1766+
.description("Stops (pauses) the stopwatch and returns the total elapsed time. Can be resumed later with start_stopwatch.")
1767+
.parameters(json!({
1768+
"type": "object",
1769+
"properties": {},
1770+
"required": [],
1771+
}))
1772+
.build().unwrap(),
1773+
1774+
ChatCompletionFunctionsArgs::default()
1775+
.name("check_stopwatch")
1776+
.description("Returns the current elapsed time on the stopwatch without stopping it. Shows whether it's running or stopped.")
1777+
.parameters(json!({
1778+
"type": "object",
1779+
"properties": {},
1780+
"required": [],
1781+
}))
1782+
.build().unwrap(),
1783+
1784+
ChatCompletionFunctionsArgs::default()
1785+
.name("reset_stopwatch")
1786+
.description("Resets the stopwatch to zero and stops it if it's running.")
1787+
.parameters(json!({
1788+
"type": "object",
1789+
"properties": {},
1790+
"required": [],
1791+
}))
1792+
.build().unwrap(),
17341793
])
17351794
.build()
17361795
.unwrap();

src/timers.rs

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::{
99
LazyLock, RwLock,
1010
}, // Use LazyLock from std
1111
thread,
12+
time::Instant,
1213
};
1314
use tracing::{info, warn};
1415

@@ -26,6 +27,11 @@ static TIMERS: LazyLock<RwLock<Vec<(u64, String, DateTime<Local>)>>> = LazyLock:
2627
RwLock::new(timers)
2728
});
2829

30+
// Stopwatch state: (start_time, total_elapsed_before_pause)
31+
// When running: start_time is Some, when paused: start_time is None
32+
static STOPWATCH: LazyLock<RwLock<(Option<Instant>, std::time::Duration)>> =
33+
LazyLock::new(|| RwLock::new((None, std::time::Duration::ZERO)));
34+
2935
fn load_timers_from_disk(
3036
path: &Path,
3137
) -> Result<Vec<(u64, String, DateTime<Local>)>, anyhow::Error> {
@@ -228,12 +234,84 @@ impl AudibleTimers {
228234
}
229235
}
230236

237+
// Stopwatch API
238+
239+
/// Starts the stopwatch. If already running, this does nothing.
240+
/// If paused, it resumes from where it left off.
241+
pub fn start_stopwatch() -> String {
242+
let mut sw = STOPWATCH.write().unwrap();
243+
if sw.0.is_some() {
244+
"Stopwatch is already running.".to_string()
245+
} else {
246+
sw.0 = Some(Instant::now());
247+
if sw.1.as_secs() == 0 {
248+
"Stopwatch started.".to_string()
249+
} else {
250+
"Stopwatch resumed.".to_string()
251+
}
252+
}
253+
}
254+
255+
/// Stops (pauses) the stopwatch and returns the elapsed time.
256+
pub fn stop_stopwatch() -> String {
257+
let mut sw = STOPWATCH.write().unwrap();
258+
match sw.0 {
259+
Some(start) => {
260+
let elapsed = start.elapsed();
261+
sw.1 += elapsed;
262+
sw.0 = None;
263+
let total = sw.1;
264+
format!(
265+
"Stopwatch stopped. Total elapsed time: {}",
266+
humantime::format_duration(total)
267+
)
268+
}
269+
None => {
270+
if sw.1.as_secs() == 0 {
271+
"Stopwatch is not running and has no recorded time.".to_string()
272+
} else {
273+
format!(
274+
"Stopwatch is already stopped. Total elapsed time: {}",
275+
humantime::format_duration(sw.1)
276+
)
277+
}
278+
}
279+
}
280+
}
281+
282+
/// Returns the current elapsed time without stopping the stopwatch.
283+
pub fn check_stopwatch() -> String {
284+
let sw = STOPWATCH.read().unwrap();
285+
let total = match sw.0 {
286+
Some(start) => sw.1 + start.elapsed(),
287+
None => sw.1,
288+
};
289+
290+
if sw.0.is_none() && sw.1.as_secs() == 0 {
291+
"Stopwatch has not been started yet.".to_string()
292+
} else {
293+
let status = if sw.0.is_some() { "running" } else { "stopped" };
294+
format!(
295+
"Stopwatch is {}. Elapsed time: {}",
296+
status,
297+
humantime::format_duration(total)
298+
)
299+
}
300+
}
301+
302+
/// Resets the stopwatch to zero and stops it if running.
303+
pub fn reset_stopwatch() -> String {
304+
let mut sw = STOPWATCH.write().unwrap();
305+
*sw = (None, std::time::Duration::ZERO);
306+
"Stopwatch has been reset to zero.".to_string()
307+
}
308+
231309
#[cfg(test)]
232310
mod tests {
233311
use super::*;
234-
use tempfile::tempdir;
235-
use chrono::{Local, Duration};
312+
use chrono::{Duration, Local};
236313
use std::env;
314+
use tempfile::tempdir;
237315

238316
#[test]
239317
fn set_get_delete_timer() {
@@ -254,4 +332,94 @@ mod tests {
254332
delete_timer(timers[0].0).unwrap();
255333
assert!(get_timers().is_empty());
256334
}
335+
336+
#[test]
337+
fn test_stopwatch_basic_operations() {
338+
// Reset stopwatch to ensure clean state
339+
reset_stopwatch();
340+
341+
// Give it a moment to settle
342+
std::thread::sleep(std::time::Duration::from_millis(10));
343+
344+
// Check initial state (after reset it's zero but not started)
345+
let result = check_stopwatch();
346+
assert!(
347+
result.contains("not been started") || result.contains("Elapsed time: 0s"),
348+
"Got: {}",
349+
result
350+
);
351+
352+
// Start the stopwatch
353+
let result = start_stopwatch();
354+
assert!(
355+
result.contains("started") || result.contains("resumed"),
356+
"Got: {}",
357+
result
358+
);
359+
360+
// Starting again should indicate it's already running
361+
let result = start_stopwatch();
362+
assert!(result.contains("already running"), "Got: {}", result);
363+
364+
// Sleep for a bit
365+
std::thread::sleep(std::time::Duration::from_millis(100));
366+
367+
// Check while running
368+
let result = check_stopwatch();
369+
assert!(result.contains("running"), "Got: {}", result);
370+
371+
// Stop the stopwatch
372+
let result = stop_stopwatch();
373+
assert!(
374+
result.contains("stopped") || result.contains("Total elapsed time"),
375+
"Got: {}",
376+
result
377+
);
378+
379+
// Reset the stopwatch
380+
let result = reset_stopwatch();
381+
assert!(result.contains("reset"), "Got: {}", result);
382+
383+
// Check after reset
384+
let result = check_stopwatch();
385+
assert!(
386+
result.contains("not been started") || result.contains("Elapsed time: 0s"),
387+
"Got: {}",
388+
result
389+
);
390+
}
391+
392+
#[test]
393+
fn test_stopwatch_pause_resume() {
394+
reset_stopwatch();
395+
396+
// Start stopwatch
397+
start_stopwatch();
398+
std::thread::sleep(std::time::Duration::from_millis(50));
399+
400+
// Stop (pause)
401+
stop_stopwatch();
402+
403+
// Sleep while paused
404+
std::thread::sleep(std::time::Duration::from_millis(50));
405+
406+
// Resume (after stopping, starting again should say resumed)
407+
let result = start_stopwatch();
408+
assert!(
409+
result.contains("resumed") || result.contains("started"),
410+
"Got: {}",
411+
result
412+
);
413+
414+
// Check elapsed time
415+
let result = check_stopwatch();
416+
assert!(
417+
result.contains("running") || result.contains("Elapsed time"),
418+
"Got: {}",
419+
result
420+
);
421+
422+
// Clean up
423+
reset_stopwatch();
424+
}
257425
}

0 commit comments

Comments
 (0)