diff --git a/CLAUDE.md b/CLAUDE.md
index 4545a21..71a9404 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -42,6 +42,10 @@ r -e 'tinypkgr::check()'
- Text files in `this_week/` remain the source of truth. `tasks()` is a read-through projection, not a cache.
- Internal callers that need the full schema (sections, period, paths, ordering) should keep using `parse_todo()`.
+## Preview mode
+
+Every mutator (`roll_day`, `run_monday`, `fix_parents`, `next_day`, `sync_from_daily`, `instantiate_todo`) accepts `preview = TRUE` and returns a `hacer_preview` describing the would-be change without writing. Set `HACER_PREVIEW=1` to flip the default — useful for one-shot agent invocations that should be inspectable before they touch the user's todo repo. Internals live in `R/preview.R`; each mutator builds a `targets` list of `path -> new_lines` and dispatches via `.write_or_preview()`.
+
## Task File Format
```
diff --git a/DESCRIPTION b/DESCRIPTION
index f7e8d76..4c5002d 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,6 +1,6 @@
Package: hacer
Title: Plain-Text Nested ToDo Planning
-Version: 0.1.5
+Version: 0.1.6
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
email = "troy@cornball.ai",
diff --git a/NAMESPACE b/NAMESPACE
index a8568d1..e348d72 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -19,4 +19,6 @@ export(tasks)
export(todo_config)
export(use_repo)
+S3method(print,hacer_preview)
+
importFrom(yaml,read_yaml)
diff --git a/R/cli.R b/R/cli.R
index 02bc83d..26df0b5 100644
--- a/R/cli.R
+++ b/R/cli.R
@@ -2,60 +2,66 @@
#' Generate the new week's files (run on Mondays)
#' @param date A Date. Defaults to `Sys.Date()`.
#' @param cfg A config list from `todo_config()`.
+#' @param preview If `TRUE`, return a `hacer_preview` instead of writing.
+#' Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.
#' @export
-run_monday <- function(date = Sys.Date(), cfg = todo_config()) {
- dir.create(cfg$live_dir, recursive = TRUE, showWarnings = FALSE)
- dir.create(cfg$archive_dir, recursive = TRUE, showWarnings = FALSE)
-
+run_monday <- function(date = Sys.Date(), cfg = todo_config(),
+ preview = .preview_default()) {
+ if (!preview) {
+ dir.create(cfg$live_dir, recursive = TRUE, showWarnings = FALSE)
+ dir.create(cfg$archive_dir, recursive = TRUE, showWarnings = FALSE)
+ }
+
this_mon <- .monday_of(date)
- # find previous Monday by subtracting 7 days
prev_mon <- this_mon - 7L
-
- # locate previous set in archive or live
+
prev <- paths_for(prev_mon, cfg)
if (!all(file.exists(prev$live)) && !all(file.exists(prev$archive))) {
stop("Previous week's files not found in live or archive.")
}
src <- if (all(file.exists(prev$live))) prev$live else prev$archive
-
- # read previous
+
daily <- parse_todo(src[.period_types == "Daily"], "Daily")
week <- parse_todo(src[.period_types == "Week"], "Week")
month <- parse_todo(src[.period_types == "Month"], "Month")
quarter <- parse_todo(src[.period_types == "Quarter"], "Quarter")
-
- # advance and build new tables
+
nxt <- advance_period(daily, week, month, quarter, prev_mon, this_mon)
-
- # write to live (Syncthing)
+
dst <- paths_for(this_mon, cfg)
+ targets <- list()
for (p in .period_types) {
df <- nxt[[p]]
- write_todo_txt(df, dst$live[.period_types==p], p, cfg)
+ txt_path <- dst$live[.period_types == p]
+ targets[[txt_path]] <- build_todo_txt_lines(df, txt_path, p, cfg)
if (isTRUE(cfg$render_markdown)) {
- write_markdown(df, sub("\\.txt$", ".md", dst$live[.period_types==p]), p, cfg)
+ md_path <- sub("\\.txt$", ".md", txt_path)
+ targets[[md_path]] <- build_markdown_lines(df, md_path, p, cfg)
}
if (isTRUE(cfg$render_html)) {
- write_simple_html(df, sub("\\.txt$", ".html", dst$live[.period_types==p]), p)
+ html_path <- sub("\\.txt$", ".html", txt_path)
+ targets[[html_path]] <- build_simple_html_lines(df, html_path, p)
}
}
-
+
+ result <- .write_or_preview(targets, preview)
+
# archive: copy previous live into archive, then git add/commit if the folder is a repo
- if (all(file.exists(prev$live))) {
+ # This side effect only happens in non-preview mode.
+ if (!preview && all(file.exists(prev$live))) {
file.copy(from = prev$live, to = prev$archive, overwrite = TRUE)
- # best-effort git (no dependency)
old_wd <- getwd(); on.exit(setwd(old_wd), add = TRUE)
setwd(cfg$archive_dir)
if (file.exists(file.path(cfg$archive_dir, ".git"))) {
system2("git", c("add", "."))
msg <- paste("Archive ToDos:", format(prev_mon))
system2("git", c("commit", "-m", shQuote(msg)), stdout = FALSE, stderr = FALSE)
- # optional push:
system2("git", c("push"), stdout = FALSE, stderr = FALSE)
}
}
-
- invisible(dst$live)
+
+ if (!preview) return(invisible(dst$live))
+ result
}
#' Infer a period name from a ToDo filename
@@ -69,33 +75,42 @@ infer_period_from_filename <- function(f){
#' Roll up parent statuses in a single file
#' @param file_name Path to a ToDo `.txt` file.
+#' @param preview If `TRUE`, return a `hacer_preview` instead of writing.
+#' Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.
#' @export
-fix_parents <- function(file_name){
+fix_parents <- function(file_name, preview = .preview_default()){
per <- infer_period_from_filename(file_name)
df <- parse_todo(file_name, per)
df <- inherit_recur_to_parents(df)
df <- rollup_status(df)
- write_todo_txt(df, file_name, ifelse(is.na(per), "Daily", per))
- invisible(file_name)
+ per_eff <- ifelse(is.na(per), "Daily", per)
+ new_lines <- build_todo_txt_lines(df, file_name, per_eff)
+ targets <- list()
+ targets[[file_name]] <- new_lines
+ result <- .write_or_preview(targets, preview)
+ if (!preview) return(invisible(file_name))
+ result
}
# R/cli.R
#' Sync new items added in Daily up to Week/Month/Quarter
#' @param date A Date. Defaults to `Sys.Date()`.
#' @param cfg A config list from `todo_config()`.
+#' @param preview If `TRUE`, return a `hacer_preview` instead of writing.
+#' Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.
#' @export
-sync_from_daily <- function(date = Sys.Date(), cfg = todo_config()){
+sync_from_daily <- function(date = Sys.Date(), cfg = todo_config(),
+ preview = .preview_default()){
p <- paths_for(date, cfg)
d <- parse_todo(p$live[grepl("_Daily", p$live)], "Daily")
W <- parse_todo(p$live[grepl("_Week", p$live)], "Week")
M <- parse_todo(p$live[grepl("_Month", p$live)], "Month")
Q <- parse_todo(p$live[grepl("_Quarter",p$live)], "Quarter")
-
+
add_missing <- function(src, tgt){
if (!nrow(src)) return(tgt)
need <- setdiff(src$path, tgt$path)
if (!length(need)) return(tgt)
- # append missing paths (and any missing ancestors)
append_path <- function(tgt, fullpath, section){
parts <- strsplit(fullpath, " > ", fixed=TRUE)[[1]]
cur <- character()
@@ -125,14 +140,22 @@ sync_from_daily <- function(date = Sys.Date(), cfg = todo_config()){
}
tgt
}
-
+
W <- add_missing(d, W); M <- add_missing(d, M); Q <- add_missing(d, Q)
W <- inherit_recur_to_parents(W); M <- inherit_recur_to_parents(M); Q <- inherit_recur_to_parents(Q)
W <- rollup_status(W); M <- rollup_status(M); Q <- rollup_status(Q)
- write_todo_txt(W, p$live[grepl("_Week", p$live)], "Week", cfg)
- write_todo_txt(M, p$live[grepl("_Month", p$live)], "Month", cfg)
- write_todo_txt(Q, p$live[grepl("_Quarter", p$live)], "Quarter", cfg)
- invisible(TRUE)
+
+ w_path <- p$live[grepl("_Week", p$live)]
+ m_path <- p$live[grepl("_Month", p$live)]
+ q_path <- p$live[grepl("_Quarter", p$live)]
+ targets <- list()
+ targets[[w_path]] <- build_todo_txt_lines(W, w_path, "Week", cfg)
+ targets[[m_path]] <- build_todo_txt_lines(M, m_path, "Month", cfg)
+ targets[[q_path]] <- build_todo_txt_lines(Q, q_path, "Quarter", cfg)
+
+ result <- .write_or_preview(targets, preview)
+ if (!preview) return(invisible(TRUE))
+ result
}
#' Advance tasks from today to tomorrow within the Daily file
@@ -144,27 +167,28 @@ sync_from_daily <- function(date = Sys.Date(), cfg = todo_config()){
#'
#' @param date A Date. Defaults to `Sys.Date()`.
#' @param cfg A config list from `todo_config()`.
+#' @param preview If `TRUE`, return a `hacer_preview` instead of writing.
+#' Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.
#' @export
-next_day <- function(date = Sys.Date(), cfg = todo_config()) {
+next_day <- function(date = Sys.Date(), cfg = todo_config(),
+ preview = .preview_default()) {
p <- paths_for(date, cfg)
daily_file <- p$live[grepl("_Daily", p$live)]
if (!file.exists(daily_file)) stop("Daily file not found: ", daily_file)
lines <- readLines(daily_file, warn = FALSE)
- days <- cfg$daily_sections # e.g., c("Monday", "Tuesday", ...)
-
- # Determine today's weekday name
+ days <- cfg$daily_sections
today_name <- weekdays(date)
today_idx <- match(today_name, days)
if (is.na(today_idx)) stop("Today (", today_name, ") not in daily_sections")
if (today_idx >= length(days)) {
message("Already at ", today_name, " (last day). Nothing to advance.")
+ if (preview) return(.new_preview())
return(invisible(daily_file))
}
tomorrow_name <- days[today_idx + 1L]
- # Find section boundaries
section_starts <- grep("^#\\s+\\w", lines)
section_names <- sub("^#\\s+", "", lines[section_starts])
@@ -174,84 +198,75 @@ next_day <- function(date = Sys.Date(), cfg = todo_config()) {
stop("Could not find sections for ", today_name, " and ", tomorrow_name)
}
- # Find end of today's section (line before tomorrow or next section)
today_end <- tomorrow_start - 1L
while (today_end > today_start && grepl("^\\s*$|^#", lines[today_end])) {
today_end <- today_end - 1L
}
- # Extract today's task lines
if (today_end < today_start) {
message("No tasks in ", today_name, " section.")
+ if (preview) return(.new_preview())
return(invisible(daily_file))
}
today_lines <- lines[(today_start + 1L):today_end]
- # Filter for copying to tomorrow:
- # - Skip [x] completed items
- # - Skip Email/ToDo if [/] (daily recurring that got done)
- # - Keep blank lines between top-level tasks
copy_lines <- character()
last_was_task <- FALSE
for (ln in today_lines) {
- # Keep blank lines (they separate top-level task groups)
if (grepl("^\\s*$", ln)) {
if (last_was_task) copy_lines <- c(copy_lines, "")
last_was_task <- FALSE
next
}
- if (!grepl("^\\s*\\[", ln)) next # not a task line
+ if (!grepl("^\\s*\\[", ln)) next
status <- substr(sub("^\\s*", "", ln), 2, 2)
- if (status == "x") next # skip completed
+ if (status == "x") next
- # Check if it's Email or ToDo with [/]
is_daily_recur <- grepl("-\\s*\\*?(Email|ToDo)\\s*$", ln, ignore.case = TRUE)
- if (is_daily_recur && status == "/") next # skip done daily recurring
+ if (is_daily_recur && status == "/") next
- # Reset status to blank for tomorrow
ln_reset <- sub("\\[/\\]", "[ ]", ln)
- ln_reset <- sub("\\[x\\]", "[ ]", ln_reset) # just in case
+ ln_reset <- sub("\\[x\\]", "[ ]", ln_reset)
copy_lines <- c(copy_lines, ln_reset)
last_was_task <- TRUE
}
- # Filter today's section: keep only [/] and [x], remove [ ]
keep_today <- character()
for (ln in today_lines) {
if (!grepl("^\\s*\\[", ln)) {
- # Keep non-task lines (blanks, etc.)
keep_today <- c(keep_today, ln)
} else {
status <- substr(sub("^\\s*", "", ln), 2, 2)
if (status %in% c("/", "x", "!")) {
keep_today <- c(keep_today, ln)
}
- # Drop [ ] unchecked items
}
}
- # Find the separator line before tomorrow (e.g., #######################################)
separator_start <- tomorrow_start - 1L
while (separator_start > today_end && grepl("^\\s*$", lines[separator_start])) {
separator_start <- separator_start - 1L
}
- # separator_start now points to the ### line (or today_end if none)
- # Build new file
new_lines <- c(
- lines[1:today_start], # everything up to and including today header
- keep_today, # filtered today tasks
- "", # blank line
- lines[separator_start:tomorrow_start], # separator + blank + tomorrow header
- "", # blank after header
- copy_lines, # copied tasks
- "", # blank line
+ lines[1:today_start],
+ keep_today,
+ "",
+ lines[separator_start:tomorrow_start],
+ "",
+ copy_lines,
+ "",
if (tomorrow_start < length(lines)) lines[(tomorrow_start + 1L):length(lines)] else character()
)
- # Remove excess blank lines
- writeLines(new_lines, daily_file)
- message("Advanced from ", today_name, " to ", tomorrow_name)
- invisible(daily_file)
+ targets <- list()
+ targets[[daily_file]] <- new_lines
+ result <- .write_or_preview(targets, preview)
+
+ if (!preview) {
+ message("Advanced from ", today_name, " to ", tomorrow_name)
+ return(invisible(daily_file))
+ }
+ result
}
diff --git a/R/instantiate.R b/R/instantiate.R
index d6c9844..edd177e 100644
--- a/R/instantiate.R
+++ b/R/instantiate.R
@@ -2,29 +2,34 @@
#' @param repo_dir Path to your ToDo repo directory.
#' @param syncthing_live_dir Optional path to a Syncthing-shared live directory.
#' @param overwrite Overwrite existing files? Default `FALSE`.
+#' @param preview If `TRUE`, return a `hacer_preview` instead of writing.
+#' Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.
#' @export
instantiate_todo <- function(repo_dir,
syncthing_live_dir = NULL,
- overwrite = FALSE) {
+ overwrite = FALSE,
+ preview = .preview_default()) {
repo_dir <- normalizePath(path.expand(repo_dir), mustWork = FALSE)
- if (!dir.exists(repo_dir)) dir.create(repo_dir, recursive = TRUE, showWarnings = FALSE)
-
- # sanity: warn if not a git repo (but don’t require)
+ if (!preview && !dir.exists(repo_dir)) {
+ dir.create(repo_dir, recursive = TRUE, showWarnings = FALSE)
+ }
+
has_git <- file.exists(file.path(repo_dir, ".git"))
-
+
live_dir <- if (is.null(syncthing_live_dir)) file.path(repo_dir, "this_week")
else normalizePath(path.expand(syncthing_live_dir), mustWork = FALSE)
archive_dir <- file.path(repo_dir, "archive")
-
- dir.create(live_dir, recursive = TRUE, showWarnings = FALSE)
- dir.create(archive_dir, recursive = TRUE, showWarnings = FALSE)
-
- # 1) write local config file
+
+ if (!preview) {
+ dir.create(live_dir, recursive = TRUE, showWarnings = FALSE)
+ dir.create(archive_dir, recursive = TRUE, showWarnings = FALSE)
+ }
+
cfg_path <- file.path(repo_dir, "hacer_config.R")
if (file.exists(cfg_path) && !overwrite) {
stop("Config already exists at ", cfg_path, ". Set overwrite=TRUE to replace.")
}
-
+
cfg_lines <- c(
"## Local configuration for hacer",
paste0("# ", repo_dir, "/hacer_config.R"),
@@ -39,18 +44,15 @@ instantiate_todo <- function(repo_dir,
" render_html = TRUE",
")"
)
- writeLines(cfg_lines, cfg_path)
- # 2) seed initial week in live_dir (based on current Monday)
mon <- .monday_of(Sys.Date())
files <- .build_names_for(mon)
paths <- file.path(live_dir, files)
-
+
if (any(file.exists(paths)) && !overwrite) {
stop("Initial ToDo files already exist in live_dir; set overwrite=TRUE to replace.")
}
-
- # very small, neutral starters (you can expand later)
+
starter_daily <- function(fname) {
secs <- c("Monday","Tuesday","Wednesday","Thursday","Friday")
lines <- c(paste0("# ", basename(fname)))
@@ -66,7 +68,7 @@ instantiate_todo <- function(repo_dir,
}
lines
}
-
+
starter_week <- function(fname) {
c(paste0("# ", basename(fname)),
"", "#######################################", "",
@@ -79,7 +81,7 @@ instantiate_todo <- function(repo_dir,
" [ ] - Fix leaks",
" [ ] - Master Window")
}
-
+
starter_month <- function(fname) {
c(paste0("# ", basename(fname)),
"", "#######################################", "",
@@ -87,7 +89,7 @@ instantiate_todo <- function(repo_dir,
" [ ] - Major Task A",
" [ ] - Major Task B")
}
-
+
starter_quarter <- function(fname) {
c(paste0("# ", basename(fname)),
"", "#######################################", "",
@@ -95,16 +97,18 @@ instantiate_todo <- function(repo_dir,
" [ ] - Theme 1",
" [ ] - Theme 2")
}
-
+
starters <- list(starter_daily, starter_month, starter_quarter, starter_week)
+
+ targets <- list()
+ targets[[cfg_path]] <- cfg_lines
for (i in seq_along(paths)) {
- writeLines(starters[[i]](paths[i]), paths[i])
+ targets[[paths[i]]] <- starters[[i]](paths[i])
}
-
- # 3) basic README to guide the user (optional)
+
readme_path <- file.path(repo_dir, "README_HACER.md")
if (!file.exists(readme_path) || overwrite) {
- readme <- c(
+ targets[[readme_path]] <- c(
"# ToDo Project",
"",
"- Edit `hacer_config.R` to tweak paths and options.",
@@ -113,18 +117,22 @@ instantiate_todo <- function(repo_dir,
"- Run `hacer::run_monday()` each Monday (or set a cron job).",
"- Edit `.txt` files directly in RStudio; Markdown/HTML mirrors are optional."
)
- writeLines(readme, readme_path)
}
-
- if (!has_git) {
- message("Note: '", repo_dir, "' does not look like a Git repo (no .git). ",
- "You can run `git init` there before committing archives.")
+
+ result <- .write_or_preview(targets, preview)
+
+ if (!preview) {
+ if (!has_git) {
+ message("Note: '", repo_dir, "' does not look like a Git repo (no .git). ",
+ "You can run `git init` there before committing archives.")
+ }
+ message("Initialized hacer project at: ", repo_dir,
+ "\n- Config: ", cfg_path,
+ "\n- Live dir: ", live_dir,
+ "\n- Archive dir: ", archive_dir,
+ "\n- Seeded files: \n - ", paste(basename(paths), collapse = "\n - "))
+ return(invisible(list(config = cfg_path, live_files = paths,
+ archive_dir = archive_dir)))
}
-
- message("Initialized hacer project at: ", repo_dir,
- "\n- Config: ", cfg_path,
- "\n- Live dir: ", live_dir,
- "\n- Archive dir: ", archive_dir,
- "\n- Seeded files: \n - ", paste(basename(paths), collapse = "\n - "))
- invisible(list(config = cfg_path, live_files = paths, archive_dir = archive_dir))
+ result
}
diff --git a/R/io.R b/R/io.R
index 516f358..633b184 100644
--- a/R/io.R
+++ b/R/io.R
@@ -1,10 +1,8 @@
# R/io.R
-write_todo_txt <- function(df, file, period, cfg = todo_config()) {
- # rebuild nested text with sections and indent
+build_todo_txt_lines <- function(df, file, period, cfg = todo_config()) {
lines <- character()
lines <- c(lines, paste0("# ", basename(file)))
-
- # for Daily, keep day sections; otherwise ignore sections
+
if (period == "Daily") {
secs <- unique(df$section)
secs <- secs[!is.na(secs)]
@@ -17,7 +15,11 @@ write_todo_txt <- function(df, file, period, cfg = todo_config()) {
lines <- c(lines, "", "#######################################", "")
lines <- c(lines, .df_to_lines(df, cfg$indent))
}
- writeLines(lines, file)
+ lines
+}
+
+write_todo_txt <- function(df, file, period, cfg = todo_config()) {
+ writeLines(build_todo_txt_lines(df, file, period, cfg), file)
}
.df_to_lines <- function(df, indent) {
@@ -36,10 +38,10 @@ write_todo_txt <- function(df, file, period, cfg = todo_config()) {
}
# optional mirrors:
-write_markdown <- function(df, file_md, period, cfg = todo_config()) {
+build_markdown_lines <- function(df, file_md, period, cfg = todo_config()) {
lines <- character()
lines <- c(lines, paste0("# ", sub("\\.md$", "", basename(file_md))))
-
+
one <- function(df) {
if (!nrow(df)) return(character())
df <- df[order(df$order), , drop=FALSE]
@@ -52,7 +54,7 @@ write_markdown <- function(df, file_md, period, cfg = todo_config()) {
}
out
}
-
+
if (period == "Daily") {
secs <- unique(df$section); secs <- secs[!is.na(secs)]
for (s in secs) {
@@ -63,16 +65,19 @@ write_markdown <- function(df, file_md, period, cfg = todo_config()) {
lines <- c(lines, "", "## Tasks", "")
lines <- c(lines, one(df))
}
- writeLines(lines, file_md)
+ lines
}
-write_simple_html <- function(df, file_html, period) {
- # dead-simple static HTML: no deps
+write_markdown <- function(df, file_md, period, cfg = todo_config()) {
+ writeLines(build_markdown_lines(df, file_md, period, cfg), file_md)
+}
+
+build_simple_html_lines <- function(df, file_html, period) {
esc <- function(x) { x <- gsub("&","&",x, fixed=TRUE); x <- gsub("<","<",x,fixed=TRUE); gsub(">",">",x,fixed=TRUE) }
lines <- c("","",
"",
paste0("
", esc(period), "
"))
-
+
to_ul <- function(df) {
if (!nrow(df)) return(character())
df <- df[order(df$order), , drop=FALSE]
@@ -86,7 +91,7 @@ write_simple_html <- function(df, file_html, period) {
}
out
}
-
+
if (period == "Daily") {
secs <- unique(df$section); secs <- secs[!is.na(secs)]
for (s in secs) {
@@ -96,5 +101,9 @@ write_simple_html <- function(df, file_html, period) {
} else {
lines <- c(lines, to_ul(df))
}
- writeLines(lines, file_html)
+ lines
+}
+
+write_simple_html <- function(df, file_html, period) {
+ writeLines(build_simple_html_lines(df, file_html, period), file_html)
}
diff --git a/R/preview.R b/R/preview.R
new file mode 100644
index 0000000..e1e3f90
--- /dev/null
+++ b/R/preview.R
@@ -0,0 +1,177 @@
+# R/preview.R
+# Preview-mode plumbing: every mutator can return a `hacer_preview` object
+# describing what would change instead of writing to disk. Set the env var
+# HACER_PREVIEW=1 to flip the default for the whole session / one-shot CLI.
+
+.preview_default <- function() {
+ v <- Sys.getenv("HACER_PREVIEW", unset = "")
+ v %in% c("1", "true", "TRUE", "yes", "YES")
+}
+
+.empty_diff_df <- function() {
+ data.frame(file = character(), line = integer(), text = character(),
+ stringsAsFactors = FALSE)
+}
+
+.new_preview <- function() {
+ obj <- list(
+ files_created = character(),
+ files_modified = character(),
+ lines_added = .empty_diff_df(),
+ lines_removed = .empty_diff_df(),
+ done_log_appends = character()
+ )
+ attr(obj, "targets") <- list() # named list: path -> new lines
+ attr(obj, "done_log_path") <- NA_character_
+ class(obj) <- "hacer_preview"
+ obj
+}
+
+# LCS-based line diff. Returns added/removed data.frames keyed to `file`.
+.line_diff <- function(old, new, file) {
+ added <- list(); removed <- list()
+ if (!length(old) && !length(new)) {
+ return(list(added = .empty_diff_df(), removed = .empty_diff_df()))
+ }
+ if (!length(old)) {
+ return(list(
+ added = data.frame(file = file, line = seq_along(new), text = new,
+ stringsAsFactors = FALSE),
+ removed = .empty_diff_df()
+ ))
+ }
+ if (!length(new)) {
+ return(list(
+ added = .empty_diff_df(),
+ removed = data.frame(file = file, line = seq_along(old), text = old,
+ stringsAsFactors = FALSE)
+ ))
+ }
+ m <- length(old); n <- length(new)
+ L <- matrix(0L, m + 1L, n + 1L)
+ for (i in seq_len(m)) {
+ ai <- old[i]
+ for (j in seq_len(n)) {
+ if (identical(ai, new[j])) L[i + 1L, j + 1L] <- L[i, j] + 1L
+ else L[i + 1L, j + 1L] <- max(L[i, j + 1L], L[i + 1L, j])
+ }
+ }
+ i <- m; j <- n
+ while (i > 0L || j > 0L) {
+ if (i > 0L && j > 0L && identical(old[i], new[j])) {
+ i <- i - 1L; j <- j - 1L
+ } else if (j > 0L && (i == 0L || L[i + 1L, j] >= L[i, j + 1L])) {
+ added[[length(added) + 1L]] <- data.frame(
+ file = file, line = j, text = new[j], stringsAsFactors = FALSE)
+ j <- j - 1L
+ } else {
+ removed[[length(removed) + 1L]] <- data.frame(
+ file = file, line = i, text = old[i], stringsAsFactors = FALSE)
+ i <- i - 1L
+ }
+ }
+ list(
+ added = if (length(added))
+ do.call(rbind, rev(added)) else .empty_diff_df(),
+ removed = if (length(removed))
+ do.call(rbind, rev(removed)) else .empty_diff_df()
+ )
+}
+
+# Take a list of (path -> new lines) plus optional done.log appends and
+# either write everything to disk (preview = FALSE) or return a hacer_preview.
+.write_or_preview <- function(targets,
+ preview,
+ done_log_path = NA_character_,
+ done_log_appends = character()) {
+ if (!preview) {
+ for (path in names(targets)) {
+ dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE)
+ writeLines(targets[[path]], path)
+ }
+ if (length(done_log_appends) && !is.na(done_log_path)) {
+ if (file.exists(done_log_path)) {
+ cat(paste0(done_log_appends, "\n"),
+ file = done_log_path, append = TRUE, sep = "")
+ } else {
+ writeLines(done_log_appends, done_log_path)
+ }
+ }
+ return(invisible(names(targets)))
+ }
+
+ pv <- .new_preview()
+ for (path in names(targets)) {
+ new_lines <- targets[[path]]
+ if (!file.exists(path)) {
+ pv$files_created <- c(pv$files_created, path)
+ if (length(new_lines)) {
+ pv$lines_added <- rbind(pv$lines_added, data.frame(
+ file = path, line = seq_along(new_lines), text = new_lines,
+ stringsAsFactors = FALSE))
+ }
+ } else {
+ old_lines <- readLines(path, warn = FALSE)
+ if (!identical(old_lines, new_lines)) {
+ pv$files_modified <- c(pv$files_modified, path)
+ d <- .line_diff(old_lines, new_lines, path)
+ pv$lines_added <- rbind(pv$lines_added, d$added)
+ pv$lines_removed <- rbind(pv$lines_removed, d$removed)
+ }
+ }
+ }
+ pv$done_log_appends <- done_log_appends
+ attr(pv, "targets") <- targets
+ attr(pv, "done_log_path") <- done_log_path
+ pv
+}
+
+# Apply a preview to disk. Mainly used by tests to verify that a preview
+# round-trips to the same end state as a non-preview call. Internal.
+.apply_preview <- function(pv) {
+ targets <- attr(pv, "targets")
+ for (path in names(targets)) {
+ dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE)
+ writeLines(targets[[path]], path)
+ }
+ done_log_path <- attr(pv, "done_log_path")
+ if (length(pv$done_log_appends) && !is.na(done_log_path)) {
+ if (file.exists(done_log_path)) {
+ cat(paste0(pv$done_log_appends, "\n"),
+ file = done_log_path, append = TRUE, sep = "")
+ } else {
+ writeLines(pv$done_log_appends, done_log_path)
+ }
+ }
+ invisible(names(targets))
+}
+
+#' Print a hacer_preview summary
+#'
+#' @param x A `hacer_preview` object.
+#' @param ... Unused.
+#' @export
+print.hacer_preview <- function(x, ...) {
+ cat("hacer preview\n")
+ if (length(x$files_created)) {
+ cat(" created (", length(x$files_created), "):\n", sep = "")
+ for (f in x$files_created) cat(" + ", f, "\n", sep = "")
+ }
+ if (length(x$files_modified)) {
+ cat(" modified (", length(x$files_modified), "):\n", sep = "")
+ for (f in x$files_modified) {
+ n_add <- sum(x$lines_added$file == f)
+ n_rm <- sum(x$lines_removed$file == f)
+ cat(" ~ ", f, " (+", n_add, "/-", n_rm, ")\n", sep = "")
+ }
+ }
+ if (length(x$done_log_appends)) {
+ cat(" done.log appends (", length(x$done_log_appends), "):\n", sep = "")
+ for (ln in x$done_log_appends) cat(" > ", ln, "\n", sep = "")
+ }
+ if (!length(x$files_created) && !length(x$files_modified) &&
+ !length(x$done_log_appends)) {
+ cat(" (no changes)\n")
+ }
+ invisible(x)
+}
diff --git a/R/roll_day.R b/R/roll_day.R
index e6d278b..058a5a9 100644
--- a/R/roll_day.R
+++ b/R/roll_day.R
@@ -8,15 +8,22 @@
#'
#' @param date A Date. Defaults to \code{Sys.Date()}.
#' @param cfg A config list from \code{todo_config()}.
+#' @param preview If \code{TRUE}, return a \code{hacer_preview} describing the
+#' change and write nothing. Defaults to \code{HACER_PREVIEW=1} env var or
+#' \code{FALSE}.
#' @export
-roll_day <- function(date = Sys.Date(), cfg = todo_config()) {
- dir.create(cfg$live_dir, recursive = TRUE, showWarnings = FALSE)
+roll_day <- function(date = Sys.Date(), cfg = todo_config(),
+ preview = .preview_default()) {
+ if (!preview) {
+ dir.create(cfg$live_dir, recursive = TRUE, showWarnings = FALSE)
+ }
types <- c("Daily", "Week", "Month", "Quarter")
live_files <- list.files(cfg$live_dir, pattern = "^ToDo_\\d{6}_.+\\.txt$")
if (!length(live_files)) {
message("No prior files found in ", cfg$live_dir, ". Nothing to roll.")
+ if (preview) return(.new_preview())
return(invisible(character()))
}
@@ -25,6 +32,7 @@ roll_day <- function(date = Sys.Date(), cfg = todo_config()) {
valid <- vapply(m, length, integer(1L)) == 3L
if (!any(valid)) {
message("No valid ToDo files found in ", cfg$live_dir, ". Nothing to roll.")
+ if (preview) return(.new_preview())
return(invisible(character()))
}
@@ -33,8 +41,8 @@ roll_day <- function(date = Sys.Date(), cfg = todo_config()) {
file_types <- vapply(m, `[`, character(1L), 3L)
today_str <- format(date, "%y%m%d")
- created <- character()
done_log <- character()
+ targets <- list()
repo_dir <- dirname(cfg$live_dir)
done_log_path <- file.path(repo_dir, "done.log")
@@ -86,24 +94,19 @@ roll_day <- function(date = Sys.Date(), cfg = todo_config()) {
out_lines <- c(out_lines, ln)
}
- writeLines(out_lines, dst_file)
- created <- c(created, dst_file)
+ targets[[dst_file]] <- out_lines
}
- if (length(done_log)) {
- if (file.exists(done_log_path)) {
- cat(paste0(done_log, "\n"), file = done_log_path, append = TRUE)
+ result <- .write_or_preview(targets, preview, done_log_path, done_log)
+
+ if (!preview) {
+ if (length(targets)) {
+ message("Rolled to ", format(date), ": ",
+ paste(basename(names(targets)), collapse = ", "))
} else {
- writeLines(done_log, done_log_path)
+ message("Nothing to roll for ", format(date))
}
+ return(invisible(names(targets)))
}
-
- if (length(created)) {
- message("Rolled to ", format(date), ": ",
- paste(basename(created), collapse = ", "))
- } else {
- message("Nothing to roll for ", format(date))
- }
-
- invisible(created)
+ result
}
diff --git a/README.md b/README.md
index a11d8ff..1d7dbb0 100644
--- a/README.md
+++ b/README.md
@@ -100,6 +100,30 @@ Resolution order for the repo path is: `repo_dir` argument → `options("hacer.r
If you're running [corteza](https://github.com/cornball-ai/corteza)'s MCP server, hacer's exports register automatically as `hacer::*` tools (on the corteza `hacer` branch) so any MCP-capable agent can call them.
+### Look before you leap: preview mode
+
+Every function that writes to disk takes `preview = TRUE` and returns a `hacer_preview` object describing what would change without touching the filesystem:
+
+```r
+pv <- hacer::roll_day(preview = TRUE)
+print(pv)
+#> hacer preview
+#> created (1):
+#> + ~/todo/this_week/ToDo_250916_Daily.txt
+#> done.log appends (2):
+#> > 2025-09-16 [x] - Some finished task
+#> > 2025-09-16 [x] - A nested done item
+```
+
+Set `HACER_PREVIEW=1` to flip the default for a one-shot CLI agent so it never accidentally writes:
+
+```bash
+HACER_REPO=~/todo HACER_PREVIEW=1 r -e 'hacer::run_monday()'
+HACER_REPO=~/todo HACER_PREVIEW=1 r -e 'hacer::roll_day()'
+```
+
+Covers `roll_day()`, `run_monday()`, `fix_parents()`, `next_day()`, `sync_from_daily()`, and `instantiate_todo()`. The preview lists `files_created`, `files_modified`, line-level diffs, and any `done.log` lines that would be appended.
+
### Reading: `tasks()` is the structured API
`hacer::tasks()` is the read interface for agents. It returns the entire task set across `this_week/` as a `data.frame` so you can filter and reason without re-parsing text:
diff --git a/inst/tinytest/test_preview.R b/inst/tinytest/test_preview.R
new file mode 100644
index 0000000..274de97
--- /dev/null
+++ b/inst/tinytest/test_preview.R
@@ -0,0 +1,267 @@
+# test_preview.R - Tests for preview mode across all mutators
+
+library(hacer)
+
+tmp_repo <- function() {
+ d <- tempfile()
+ dir.create(file.path(d, "this_week"), recursive = TRUE, showWarnings = FALSE)
+ dir.create(file.path(d, "archive"), recursive = TRUE, showWarnings = FALSE)
+ d
+}
+
+mtimes_in <- function(dir) {
+ fs <- list.files(dir, recursive = TRUE, full.names = TRUE)
+ setNames(file.info(fs)$mtime, fs)
+}
+
+# ---- Helper: roll_day round-trip preview vs apply ----
+cfg_for <- function(repo, indent = 2L) {
+ list(live_dir = file.path(repo, "this_week"), indent = indent)
+}
+
+# ---- Case 1: roll_day(preview=TRUE) writes nothing ----
+repo1 <- tmp_repo()
+cfg1 <- cfg_for(repo1)
+f1 <- file.path(cfg1$live_dir, "ToDo_250915_Daily.txt")
+writeLines(c(
+ "# ToDo_250915_Daily.txt",
+ "",
+ "[ ] - Pending",
+ "[x] - Done"
+), f1)
+
+before_mtimes <- mtimes_in(repo1)
+pv1 <- hacer::roll_day(date = as.Date("2025-09-16"), cfg = cfg1, preview = TRUE)
+after_mtimes <- mtimes_in(repo1)
+
+expect_inherits(pv1, "hacer_preview",
+ info = "preview=TRUE returns a hacer_preview")
+expect_identical(before_mtimes, after_mtimes,
+ info = "preview=TRUE leaves on-disk mtimes untouched")
+expect_false(file.exists(file.path(cfg1$live_dir, "ToDo_250916_Daily.txt")),
+ info = "preview=TRUE does not create new files")
+expect_false(file.exists(file.path(repo1, "done.log")),
+ info = "preview=TRUE does not create done.log")
+
+# Preview reports the would-be changes
+expect_equal(length(pv1$files_created), 1L,
+ info = "Preview lists one new file")
+expect_true(grepl("ToDo_250916_Daily\\.txt$", pv1$files_created[1]),
+ info = "Preview names the new daily file")
+expect_equal(length(pv1$done_log_appends), 1L,
+ info = "Preview lists one done.log append")
+expect_true(grepl("Done", pv1$done_log_appends[1]),
+ info = "done.log append captures the dropped task text")
+
+unlink(repo1, recursive = TRUE)
+
+# ---- Case 2: roll_day preview + apply == roll_day non-preview ----
+make_repo <- function() {
+ r <- tmp_repo()
+ f <- file.path(r, "this_week", "ToDo_250915_Daily.txt")
+ writeLines(c(
+ "# ToDo_250915_Daily.txt",
+ "",
+ "#######################################",
+ "",
+ "# Monday",
+ "",
+ "[ ] - House",
+ " [/] - Drywall",
+ " [x] - Trim",
+ "[ ] -*Exercise",
+ "[x] - One-time"
+ ), f)
+ r
+}
+
+repo_a <- make_repo()
+hacer::roll_day(date = as.Date("2025-09-16"),
+ cfg = cfg_for(repo_a),
+ preview = FALSE)
+state_a <- list(
+ daily = readLines(file.path(repo_a, "this_week", "ToDo_250916_Daily.txt"),
+ warn = FALSE),
+ done = readLines(file.path(repo_a, "done.log"), warn = FALSE)
+)
+
+repo_b <- make_repo()
+pv2 <- hacer::roll_day(date = as.Date("2025-09-16"),
+ cfg = cfg_for(repo_b),
+ preview = TRUE)
+hacer:::.apply_preview(pv2)
+state_b <- list(
+ daily = readLines(file.path(repo_b, "this_week", "ToDo_250916_Daily.txt"),
+ warn = FALSE),
+ done = readLines(file.path(repo_b, "done.log"), warn = FALSE)
+)
+
+expect_identical(state_a$daily, state_b$daily,
+ info = "preview + apply produces same Daily file as direct roll")
+expect_identical(state_a$done, state_b$done,
+ info = "preview + apply produces same done.log as direct roll")
+
+unlink(repo_a, recursive = TRUE)
+unlink(repo_b, recursive = TRUE)
+
+# ---- Case 3: HACER_PREVIEW env var flips the default ----
+repo3 <- tmp_repo()
+cfg3 <- cfg_for(repo3)
+f3 <- file.path(cfg3$live_dir, "ToDo_250915_Daily.txt")
+writeLines(c("# ToDo_250915_Daily.txt", "", "[ ] - Task"), f3)
+
+old_env <- Sys.getenv("HACER_PREVIEW", unset = NA)
+Sys.setenv(HACER_PREVIEW = "1")
+pv3 <- hacer::roll_day(date = as.Date("2025-09-16"), cfg = cfg3)
+expect_inherits(pv3, "hacer_preview",
+ info = "HACER_PREVIEW=1 flips the default to preview mode")
+expect_false(file.exists(file.path(cfg3$live_dir, "ToDo_250916_Daily.txt")),
+ info = "HACER_PREVIEW=1 still writes nothing")
+
+Sys.setenv(HACER_PREVIEW = "")
+out3 <- hacer::roll_day(date = as.Date("2025-09-16"), cfg = cfg3)
+expect_false(inherits(out3, "hacer_preview"),
+ info = "Empty HACER_PREVIEW restores the write default")
+expect_true(file.exists(file.path(cfg3$live_dir, "ToDo_250916_Daily.txt")),
+ info = "Empty HACER_PREVIEW writes as before")
+
+if (is.na(old_env)) Sys.unsetenv("HACER_PREVIEW") else Sys.setenv(HACER_PREVIEW = old_env)
+unlink(repo3, recursive = TRUE)
+
+# ---- Case 4: print.hacer_preview renders a sensible summary ----
+repo4 <- tmp_repo()
+cfg4 <- cfg_for(repo4)
+f4 <- file.path(cfg4$live_dir, "ToDo_250915_Daily.txt")
+writeLines(c("# ToDo_250915_Daily.txt", "", "[ ] - Task", "[x] - Done"), f4)
+
+pv4 <- hacer::roll_day(date = as.Date("2025-09-16"), cfg = cfg4, preview = TRUE)
+out4 <- capture.output(print(pv4))
+combined4 <- paste(out4, collapse = "\n")
+expect_true(grepl("hacer preview", combined4),
+ info = "Print method labels itself")
+expect_true(grepl("created", combined4),
+ info = "Print mentions created files")
+expect_true(grepl("done\\.log", combined4),
+ info = "Print mentions done.log appends")
+unlink(repo4, recursive = TRUE)
+
+# ---- Case 5: fix_parents preview vs apply round trip ----
+make_fp_repo <- function() {
+ r <- tmp_repo()
+ f <- file.path(r, "this_week", "ToDo_250915_Daily.txt")
+ writeLines(c(
+ "# ToDo_250915_Daily.txt",
+ "",
+ "#######################################",
+ "",
+ "# Monday",
+ "",
+ "[ ] - House",
+ " [/] - Drywall",
+ " [x] - Trim"
+ ), f)
+ list(repo = r,
+ file = f)
+}
+
+fp_a <- make_fp_repo()
+hacer::fix_parents(fp_a$file, preview = FALSE)
+state_fp_a <- readLines(fp_a$file, warn = FALSE)
+
+fp_b <- make_fp_repo()
+mtime_before <- file.info(fp_b$file)$mtime
+pv5 <- hacer::fix_parents(fp_b$file, preview = TRUE)
+mtime_after <- file.info(fp_b$file)$mtime
+expect_identical(mtime_before, mtime_after,
+ info = "fix_parents preview leaves mtime untouched")
+expect_inherits(pv5, "hacer_preview")
+expect_equal(length(pv5$files_modified), 1L,
+ info = "fix_parents preview reports one modified file")
+
+hacer:::.apply_preview(pv5)
+state_fp_b <- readLines(fp_b$file, warn = FALSE)
+expect_identical(state_fp_a, state_fp_b,
+ info = "fix_parents preview + apply matches non-preview")
+
+unlink(fp_a$repo, recursive = TRUE)
+unlink(fp_b$repo, recursive = TRUE)
+
+# ---- Case 6: instantiate_todo preview lists files but creates none ----
+repo6 <- tempfile()
+pv6 <- hacer::instantiate_todo(repo6, preview = TRUE)
+expect_inherits(pv6, "hacer_preview")
+expect_false(dir.exists(repo6),
+ info = "instantiate_todo preview does not create the repo dir")
+expect_true(length(pv6$files_created) >= 5L,
+ info = "Preview lists at least the seed + config files (>= 5)")
+
+# Apply the preview and verify everything materialized
+hacer:::.apply_preview(pv6)
+expect_true(dir.exists(repo6),
+ info = "Applying the preview creates the directory")
+expect_true(file.exists(file.path(repo6, "hacer_config.R")),
+ info = "Config file exists after apply")
+expect_true(file.exists(file.path(repo6, "README_HACER.md")),
+ info = "README exists after apply")
+unlink(repo6, recursive = TRUE)
+
+# ---- Case 7: roll_day on empty repo returns empty preview, not error ----
+repo7 <- tmp_repo()
+pv7 <- hacer::roll_day(date = as.Date("2025-09-16"),
+ cfg = cfg_for(repo7),
+ preview = TRUE)
+expect_inherits(pv7, "hacer_preview")
+expect_equal(length(pv7$files_created), 0L,
+ info = "Empty repo preview has no created files")
+expect_equal(length(pv7$files_modified), 0L,
+ info = "Empty repo preview has no modified files")
+unlink(repo7, recursive = TRUE)
+
+# ---- Case 8: sync_from_daily preview round-trip ----
+make_sync_repo <- function() {
+ r <- tmp_repo()
+ mon <- "250915"
+ for (p in c("Daily", "Week", "Month", "Quarter")) {
+ f <- file.path(r, "this_week", sprintf("ToDo_%s_%s.txt", mon, p))
+ writeLines(c(
+ paste0("# ToDo_", mon, "_", p, ".txt"),
+ "",
+ "#######################################",
+ "",
+ "[ ] - Existing"
+ ), f)
+ }
+ # Add a brand-new task only to Daily
+ daily <- file.path(r, "this_week", paste0("ToDo_", mon, "_Daily.txt"))
+ writeLines(c(
+ paste0("# ToDo_", mon, "_Daily.txt"),
+ "",
+ "#######################################",
+ "",
+ "[ ] - Existing",
+ "[ ] - Brand new task"
+ ), daily)
+ r
+}
+
+sync_a <- make_sync_repo()
+hacer::sync_from_daily(date = as.Date("2025-09-15"),
+ cfg = cfg_for(sync_a), preview = FALSE)
+state_sync_a <- lapply(c("Week","Month","Quarter"), function(p) {
+ readLines(file.path(sync_a, "this_week", paste0("ToDo_250915_", p, ".txt")),
+ warn = FALSE)
+})
+
+sync_b <- make_sync_repo()
+pv8 <- hacer::sync_from_daily(date = as.Date("2025-09-15"),
+ cfg = cfg_for(sync_b), preview = TRUE)
+expect_inherits(pv8, "hacer_preview")
+hacer:::.apply_preview(pv8)
+state_sync_b <- lapply(c("Week","Month","Quarter"), function(p) {
+ readLines(file.path(sync_b, "this_week", paste0("ToDo_250915_", p, ".txt")),
+ warn = FALSE)
+})
+expect_identical(state_sync_a, state_sync_b,
+ info = "sync_from_daily preview + apply matches non-preview")
+unlink(sync_a, recursive = TRUE)
+unlink(sync_b, recursive = TRUE)
diff --git a/man/fix_parents.Rd b/man/fix_parents.Rd
index 5e50450..695815b 100644
--- a/man/fix_parents.Rd
+++ b/man/fix_parents.Rd
@@ -3,10 +3,13 @@
\alias{fix_parents}
\title{Roll up parent statuses in a single file}
\usage{
-fix_parents(file_name)
+fix_parents(file_name, preview = .preview_default())
}
\arguments{
\item{file_name}{Path to a ToDo `.txt` file.}
+
+\item{preview}{If `TRUE`, return a `hacer_preview` instead of writing.
+Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.}
}
\description{
Roll up parent statuses in a single file
diff --git a/man/instantiate_todo.Rd b/man/instantiate_todo.Rd
index 2758173..7a59368 100644
--- a/man/instantiate_todo.Rd
+++ b/man/instantiate_todo.Rd
@@ -3,7 +3,8 @@
\alias{instantiate_todo}
\title{Create a hacer project layout in a Git repo.}
\usage{
-instantiate_todo(repo_dir, syncthing_live_dir = NULL, overwrite = FALSE)
+instantiate_todo(repo_dir, syncthing_live_dir = NULL, overwrite = FALSE,
+ preview = .preview_default())
}
\arguments{
\item{repo_dir}{Path to your ToDo repo directory.}
@@ -11,6 +12,9 @@ instantiate_todo(repo_dir, syncthing_live_dir = NULL, overwrite = FALSE)
\item{syncthing_live_dir}{Optional path to a Syncthing-shared live directory.}
\item{overwrite}{Overwrite existing files? Default `FALSE`.}
+
+\item{preview}{If `TRUE`, return a `hacer_preview` instead of writing.
+Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.}
}
\description{
Create a hacer project layout in a Git repo.
diff --git a/man/next_day.Rd b/man/next_day.Rd
index 2f2b1f0..94f253d 100644
--- a/man/next_day.Rd
+++ b/man/next_day.Rd
@@ -3,12 +3,15 @@
\alias{next_day}
\title{Advance tasks from today to tomorrow within the Daily file}
\usage{
-next_day(date = Sys.Date(), cfg = todo_config())
+next_day(date = Sys.Date(), cfg = todo_config(), preview = .preview_default())
}
\arguments{
\item{date}{A Date. Defaults to `Sys.Date()`.}
\item{cfg}{A config list from `todo_config()`.}
+
+\item{preview}{If `TRUE`, return a `hacer_preview` instead of writing.
+Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.}
}
\description{
Copies tasks from today's section to tomorrow's section (except completed
diff --git a/man/print.hacer_preview.Rd b/man/print.hacer_preview.Rd
new file mode 100644
index 0000000..0ab21b1
--- /dev/null
+++ b/man/print.hacer_preview.Rd
@@ -0,0 +1,15 @@
+% tinyrox says don't edit this manually, but it can't stop you!
+\name{print.hacer_preview}
+\alias{print.hacer_preview}
+\title{Print a hacer_preview summary}
+\usage{
+\method{print}{hacer_preview}(x, ...)
+}
+\arguments{
+\item{x}{A `hacer_preview` object.}
+
+\item{...}{Unused.}
+}
+\description{
+Print a hacer_preview summary
+}
diff --git a/man/roll_day.Rd b/man/roll_day.Rd
index 7713dc0..528df78 100644
--- a/man/roll_day.Rd
+++ b/man/roll_day.Rd
@@ -3,12 +3,16 @@
\alias{roll_day}
\title{Roll the most recent ToDo files forward to today}
\usage{
-roll_day(date = Sys.Date(), cfg = todo_config())
+roll_day(date = Sys.Date(), cfg = todo_config(), preview = .preview_default())
}
\arguments{
\item{date}{A Date. Defaults to \code{Sys.Date()}.}
\item{cfg}{A config list from \code{todo_config()}.}
+
+\item{preview}{If \code{TRUE}, return a \code{hacer_preview} describing the
+change and write nothing. Defaults to \code{HACER_PREVIEW=1} env var or
+\code{FALSE}.}
}
\description{
Finds the most recent file of each cadence in \code{this_week/}, copies it
diff --git a/man/run_monday.Rd b/man/run_monday.Rd
index adf5c85..c04d108 100644
--- a/man/run_monday.Rd
+++ b/man/run_monday.Rd
@@ -3,12 +3,15 @@
\alias{run_monday}
\title{Generate the new week's files (run on Mondays)}
\usage{
-run_monday(date = Sys.Date(), cfg = todo_config())
+run_monday(date = Sys.Date(), cfg = todo_config(), preview = .preview_default())
}
\arguments{
\item{date}{A Date. Defaults to `Sys.Date()`.}
\item{cfg}{A config list from `todo_config()`.}
+
+\item{preview}{If `TRUE`, return a `hacer_preview` instead of writing.
+Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.}
}
\description{
Generate the new week's files (run on Mondays)
diff --git a/man/sync_from_daily.Rd b/man/sync_from_daily.Rd
index 8ebc357..5943cab 100644
--- a/man/sync_from_daily.Rd
+++ b/man/sync_from_daily.Rd
@@ -3,12 +3,16 @@
\alias{sync_from_daily}
\title{Sync new items added in Daily up to Week/Month/Quarter}
\usage{
-sync_from_daily(date = Sys.Date(), cfg = todo_config())
+sync_from_daily(date = Sys.Date(), cfg = todo_config(),
+ preview = .preview_default())
}
\arguments{
\item{date}{A Date. Defaults to `Sys.Date()`.}
\item{cfg}{A config list from `todo_config()`.}
+
+\item{preview}{If `TRUE`, return a `hacer_preview` instead of writing.
+Defaults to the `HACER_PREVIEW=1` env var or `FALSE`.}
}
\description{
Sync new items added in Daily up to Week/Month/Quarter