Skip to content

Instantly share code, notes, and snippets.

@seakintruth
Last active August 10, 2022 13:39
Show Gist options
  • Save seakintruth/89943bd52e706f8ce6ae673d414e7ab2 to your computer and use it in GitHub Desktop.
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.
#!/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()
@seakintruth
Copy link
Author

.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment