Last active
August 10, 2022 13:39
-
-
Save seakintruth/89943bd52e706f8ce6ae673d414e7ab2 to your computer and use it in GitHub Desktop.
This R countdown timer draws from three different discussions and code solutions from arround the web.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env Rscript | |
if (!require("pacman")) install.packages("pacman") | |
pacman::p_load(tcltk) | |
# From: https://gist.githubusercontent.com/jbryer/3342915/raw/07cede2640889d82944e604be6a4840a964a1a58/varEntryDialog.r | |
#' Creates a dialog box using tcl/tk to get input from the user. | |
#' | |
#' This function will create a tcl/tk dialog box to get user input. It has been | |
#' written to be extensible so the R programmer can easily create a dialog with | |
#' any number of varaibles with custom labels and data conversion of the user | |
#' entered data. The function will return a list where the element names are | |
#' \code{vars} and the value is the user input. By default, all entry will be | |
#' converted using the \code{as.character} function. However, this can easily be | |
#' altered using the \code{fun} parameter. For example, if integers are required, | |
#' use \code{fun=c(as.integer, ...)}. It is also possible to write custom | |
#' functions that can serve as a data validation. See the examples. | |
#' | |
#' Adopted from Kay Cichini: | |
#' \url{http://thebiobucket.blogspot.com/2012/08/tcltk-gui-example-with-variable-input.html} | |
#' See also: | |
#' \url{http://bioinf.wehi.edu.au/~wettenhall/RTclTkExamples/OKCancelDialog.html} | |
#' | |
#' @param vars character list of variable names. These will be the element names | |
#' within the returned list. | |
#' @param labels the labels the user will see for each variable entry. | |
#' @param fun list of functions that converts the user input. | |
#' @param title the title of the dialog box. | |
#' @param prompt the prompt the user will see on the dialog box. | |
#' @return a \code{\link{list}} of named values entered by the user. | |
varEntryDialog <- function(vars, | |
labels = vars, | |
fun = rep(list(as.character), length(vars)), | |
title = 'Variable Entry', | |
prompt = NULL) { | |
require(tcltk) | |
stopifnot(length(vars) == length(labels), length(labels) == length(fun)) | |
# Create a variable to keep track of the state of the dialog window: | |
# done = 0; If the window is active | |
# done = 1; If the window has been closed using the OK button | |
# done = 2; If the window has been closed using the Cancel button or destroyed | |
done <- tclVar(0) | |
tt <- tktoplevel() | |
tkwm.title(tt, title) | |
entries <- list() | |
tclvars <- list() | |
# Capture the event "Destroy" (e.g. Alt-F4 in Windows) and when this happens, | |
# assign 2 to done. | |
tkbind(tt,"<Destroy>",function() tclvalue(done)<-2) | |
for(i in seq_along(vars)) { | |
tclvars[[i]] <- tclVar("") | |
entries[[i]] <- tkentry(tt, textvariable=tclvars[[i]]) | |
} | |
doneVal <- as.integer(tclvalue(done)) | |
results <- list() | |
reset <- function() { | |
for(i in seq_along(entries)) { | |
tclvalue(tclvars[[i]]) <<- "" | |
} | |
} | |
reset.but <- tkbutton(tt, text="Reset", command=reset) | |
cancel <- function() { | |
tclvalue(done) <- 2 | |
} | |
cancel.but <- tkbutton(tt, text='Cancel', command=cancel) | |
submit <- function() { | |
for(i in seq_along(vars)) { | |
tryCatch( { | |
results[[vars[[i]]]] <<- fun[[i]](tclvalue(tclvars[[i]])) | |
tclvalue(done) <- 1 | |
}, | |
error = function(e) { tkmessageBox(message=geterrmessage()) }, | |
finally = { } | |
) | |
} | |
} | |
submit.but <- tkbutton(tt, text="Submit", command=submit) | |
if(!is.null(prompt)) { | |
tkgrid(tklabel(tt,text=prompt), columnspan=3, pady=10) | |
} | |
for(i in seq_along(vars)) { | |
tkgrid(tklabel(tt, text=labels[i]), entries[[i]], pady=10, padx=10, columnspan=4) | |
} | |
tkgrid(submit.but, cancel.but, reset.but, pady=10, padx=10, columnspan=3) | |
tkfocus(tt) | |
# Do not proceed with the following code until the variable done is non-zero. | |
# (But other processes can still run, i.e. the system is not frozen.) | |
tkwait.variable(done) | |
if(tclvalue(done) != 1) { | |
results <- NULL | |
} | |
tkdestroy(tt) | |
return(results) | |
} | |
################# | |
# function .get_pretty_timestamp_diff is from: | |
# https://raw.githubusercontent.com/seakintruth/podping-stats/master/mastodon-toot-bot-hive/visualize-data.R | |
# Display stuff # | |
# There's likely a much more elegant way to do this...# | |
################# | |
.get_pretty_timestamp_diff <- function( | |
start_timestamp, | |
end_timestamp, | |
seconds_decimal=0, | |
round_simple=FALSE | |
){ | |
# Set defaults | |
.days_decimal <- 0 | |
.days <- 0 | |
.hours_decimal <- 0 | |
.hours <- 0 | |
.minutes_decimal <- 0 | |
.minutes <- 0 | |
.seconds_display <- 0 | |
.seconds <- (end_timestamp-start_timestamp) | |
if (round_simple) { | |
.years <- as.integer(.seconds / (365.24*24*60*60)) | |
if (.years > 0) { | |
.years <-round(.seconds / (365.24*24*60*60),0) | |
.seconds <- 0 | |
} else { | |
.days <- as.integer((.seconds / (365.24*24*60*60)-.years)*365.24) | |
if (.days > 0 ) { | |
.days <-round((.seconds / (365.24*24*60*60)-.years)*365.24,0) | |
.seconds <- 0 | |
} else { | |
.days_decimal <-(.seconds / (365.24*24*60*60)-.years)*365.24-.days | |
.hours <- as.integer(.days_decimal*24) | |
if (.hours > 0) { | |
.hours <- round(.days_decimal*24,0) | |
}else{ | |
.hours_decimal <- .days_decimal*24 - .hours | |
.minutes <- as.integer(.hours_decimal*60) | |
.minutes_decimal <- .hours_decimal*60 - .minutes | |
if (.minutes > 0) { | |
.minutes <- round(.hours_decimal*60,0) | |
.hours_decimal <- 0 | |
.seconds_display <- 0 | |
} else { | |
.seconds_display <-1 # round(.seconds,seconds_decimal) | |
} | |
} | |
} | |
} | |
} else { | |
.years <- as.integer(.seconds / (365.24*24*60*60)) | |
.days <- as.integer((.seconds / (365.24*24*60*60)-.years)*365.24) | |
.days_decimal <-(.seconds / (365.24*24*60*60)-.years)*365.24-.days | |
.hours <- as.integer(.days_decimal*24) | |
.hours_decimal <- .days_decimal*24 - .hours | |
.minutes <- as.integer(.hours_decimal*60) | |
.minutes_decimal <- .hours_decimal*60 - .minutes | |
.seconds_display <- round(.minutes_decimal*60,seconds_decimal) | |
} | |
.time_statement_list <- c( | |
ifelse(as.integer(.years), | |
ifelse((.years == 1)," year ",paste0(.years," years ")), | |
NA | |
), | |
ifelse(as.integer(.days), | |
ifelse((.days == 1)," day ",paste0(.days," days ")), | |
NA | |
), | |
ifelse(as.integer(.hours), | |
ifelse((.hours == 1)," hour ",paste0(.hours," hours ")), | |
NA | |
), | |
ifelse(as.integer(.minutes), | |
ifelse((.minutes == 1)," minute ",paste0(.minutes," minutes ")), | |
NA | |
), | |
ifelse(as.integer(.seconds_display), | |
ifelse( | |
(.seconds_display == 1), | |
" second ", | |
paste0(.seconds_display," seconds ") | |
), | |
NA | |
) | |
) | |
.time_statement_list <- na.omit(.time_statement_list) | |
ifelse( | |
(length(.time_statement_list) <= 1), | |
.time_statement_list[1], | |
paste0( | |
paste0( | |
.time_statement_list[1:(length(.time_statement_list)-1)], | |
collapse="" | |
), | |
"and ", | |
.time_statement_list[length(.time_statement_list)] | |
) | |
) | |
} | |
###################### | |
# Begin Timer Script # | |
###################### | |
run_countdown <- function(){ | |
timer <- varEntryDialog( | |
vars=c('Var1'), | |
labels=c('Enter the number of minutes between 0 and 1200:'), | |
fun=c( | |
function(x) { | |
x <- as.double(x) | |
if(x > 0 & x <= 1200) { | |
return(x) | |
} else { | |
stop( | |
"User input error: Why didn't you follow instruction!\nPlease enter a number between 1 and 1200." | |
) | |
} | |
} | |
) | |
) | |
# Convert from minutes to seconds | |
timer <- as.double(timer[1]) * 60 | |
# the remaining is from | |
# https://stackoverflow.com/a/56857971/1146659 | |
pb <- tkProgressBar("Timer") | |
start = Sys.time() | |
# Should handle error message differently if timer message box is closed, currently | |
# Error in structure(.External(.C_dotTclObjv, objv), class = "tclObj") : | |
# [tcl] invalid command name ".2.1". | |
# Calls: setTkProgressBar ... <Anonymous> -> tkconfigure -> tcl -> .Tcl.objv -> structure | |
# Execution halted | |
# set remaining so we can test it... | |
remaining <-1 | |
while(remaining > 0){ | |
elapsed = as.numeric(difftime(Sys.time(), start, units = 'secs')) | |
remaining = timer - elapsed | |
Sys.sleep(0.1) | |
attempt <- try( | |
setTkProgressBar( | |
pb, remaining/timer, | |
label = .get_pretty_timestamp_diff( | |
as.double( | |
as.POSIXlt(Sys.time()) | |
)-remaining, | |
as.double( | |
as.POSIXlt(Sys.time()) | |
) | |
) | |
) | |
, silent=TRUE) | |
if(class(attempt) == "try-error") { | |
remaining <- 0 | |
} | |
} | |
Sys.sleep(1) | |
close(pb) | |
} | |
run_countdown() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
.get_pretty_timestamp_diff function doesn't display 1 minutes and 1 seconds properly, currently looks like:
5 minutes 2 seconds
5 minutes second
5 minutes
4 minutes 59 seconds
...
2 minutes
minute and 59 seconds
For a count down timer, should move to HH:MM:SS format.