;;; ---------------------------------------------------------------------------
;;; TimeLog.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; TIME SPENT, RECORDED TO A FILE
;;;
;;; PURPOSE
;;;   A stopwatch for drawing time. Start it against a project and an activity,
;;;   stop it when you move on, and each spell is written to a file with who,
;;;   what, when and how long.
;;;
;;;   For billing, for job costing, or simply for finding out where the week
;;;   actually went.
;;;
;;;   LOGFILE   choose where entries are written and open it
;;;   LOGSTART  start the clock
;;;   LOGSTOP   stop it and write the entry
;;;   LOGSHOW   how long the current spell has been running
;;;
;;;   The file is comma separated, so it opens straight into a spreadsheet.
;;;
;;; WHAT WAS FIXED
;;;   - Stopping the log ran (command "END"). END was the old name for
;;;     save-and-quit and was removed from AutoCAD after Release 12. On anything
;;;     newer it is an unknown command; on anything older it CLOSED THE DRAWING.
;;;     Either way it is not what "stop logging" should do.
;;;   - The file was opened for writing with "w", which empties it. Every
;;;     session threw away every entry from the session before. It is opened for
;;;     appending now.
;;;   - The helper functions were named CDATE and TIME - both global, and CDATE
;;;     is also the name of a system variable, which makes reading the code
;;;     unnecessarily hard.
;;;   - Eleven variables were global, including HH, MM, SS and FN.
;;;   - There was no way to see whether the clock was running, and no warning if
;;;     LOGSTART was pressed twice.
;;;   - It carried an autosave function that saved every fifteen minutes.
;;;     AutoCAD has done that itself since Release 13, controlled by SAVETIME.
;;;
;;;   LOGFILE   - choose and open the log file
;;;   LOGSTART  - start the clock
;;;   LOGSTOP   - stop it and write the entry
;;;   LOGSHOW   - report the running spell
;;; ---------------------------------------------------------------------------

(setq *Log:File*  nil     ; path being written to
      *Log:Start* nil     ; Julian date the current spell began
      *Log:Began* nil     ; and the same moment as readable text
      *Log:Who*   nil
      *Log:Job*   nil
      *Log:Task*  nil)

;;; ---------------------------------------------------------------------------
;;; TIME
;;; ---------------------------------------------------------------------------

;;; The date and time now, as a readable string. CDATE holds it as the number
;;; YYYYMMDD.HHMMSS, so the pieces are pulled out by position.
(defun Log:Now ( / s )
    (setq s (rtos (getvar "CDATE") 2 6))
    (strcat (substr s 1 4) "-" (substr s 5 2) "-" (substr s 7 2)
            " " (substr s 10 2) ":" (substr s 12 2) ":" (substr s 14 2))
)

;;; A span of Julian days as hours, minutes and seconds.
(defun Log:Span ( days / total h m s )
    (setq total (* days 86400.0)
          h     (fix (/ total 3600.0))
          m     (fix (/ (- total (* h 3600.0)) 60.0))
          s     (fix (- total (* h 3600.0) (* m 60.0))))
    (strcat (itoa h) ":"
            (if (< m 10) "0" "") (itoa m) ":"
            (if (< s 10) "0" "") (itoa s))
)

;;; Ask, offering what was used last time.
(defun Log:Ask ( prompt was )
    (setq prompt (getstring t (strcat "\n" prompt
                                      (if was (strcat " <" was ">") "") ": ")))
    (if (= "" prompt) was prompt)
)

;;; ---------------------------------------------------------------------------
;;; COMMANDS
;;; ---------------------------------------------------------------------------

(defun c:LOGFILE ( / path fh new )
    (setq path (getfiled "Log file to write to" "timelog.csv" "csv;txt" 1))

    (if (null path)
        (princ "\nCancelled.")
        (progn
            ;; Opened for APPEND. The original used "w", which empties the file
            ;; - so every session wiped the one before.
            (setq new (null (findfile path))
                  fh  (open path "a"))
            (if (null fh)
                (princ (strcat "\n** Cannot write to " path " **"))
                (progn
                    (if new
                        (write-line "Who,Project,Activity,Started,Stopped,Elapsed" fh))
                    (close fh)
                    (setq *Log:File* path)
                    (princ (strcat "\nEntries will be added to " path
                                   (if new " (new file, header written)" "")))))))
    (princ)
)

(defun c:LOGSTART ( )
    (if *Log:Start*
        (princ (strcat "\nThe clock is already running - started "
                       (Log:Now) ". Use LOGSTOP first."))
        (progn
            (setq *Log:Who*  (Log:Ask "Your name" *Log:Who*)
                  *Log:Job*  (Log:Ask "Project" *Log:Job*)
                  *Log:Task* (Log:Ask "Activity" *Log:Task*))
            (if (null *Log:File*)
                (princ "\nNo log file set - run LOGFILE, or this spell is only reported to screen."))
            ;; Both forms of the start time are kept: the number to measure
            ;; the span with, and the text to write into the entry. Reading the
            ;; clock again at the end would record the stop time twice.
            (setq *Log:Start* (getvar "DATE")
                  *Log:Began* (Log:Now))
            (princ (strcat "\nClock started " *Log:Began* "."))))
    (princ)
)

(defun c:LOGSHOW ( )
    (if (null *Log:Start*)
        (princ "\nThe clock is not running.")
        (princ (strcat "\n" (cond (*Log:Job*) (t "-")) " / "
                       (cond (*Log:Task*) (t "-")) " - running for "
                       (Log:Span (- (getvar "DATE") *Log:Start*)) ".")))
    (princ)
)

(defun c:LOGSTOP ( / span line fh )
    (if (null *Log:Start*)
        (princ "\nThe clock is not running - nothing to stop.")
        (progn
            (setq span (- (getvar "DATE") *Log:Start*)
                  line (strcat (cond (*Log:Who*) (t "")) ","
                               (cond (*Log:Job*) (t "")) ","
                               (cond (*Log:Task*) (t "")) ","
                               *Log:Began* ","
                               (Log:Now) ","
                               (Log:Span span)))

            (if *Log:File*
                (progn
                    (setq fh (open *Log:File* "a"))
                    (if fh
                        (progn (write-line line fh) (close fh)
                               (princ (strcat "\nWritten to " *Log:File* ".")))
                        (princ "\n** Could not open the log file - reporting to screen only. **"))))

            (princ (strcat "\n" (cond (*Log:Job*) (t "-")) " / "
                           (cond (*Log:Task*) (t "-")) " - "
                           (Log:Span span) " elapsed."))
            (setq *Log:Start* nil *Log:Began* nil)))
    (princ)
)

(princ)
