;; ;;; stash.scm --- A Guile script for moving directories and creating symlinks with conflict resolution ;; ;;; ;; ;;; Author: Glenn Thompson ;; ;;; Version: 0.1.0-alpha.1 ;; ;;; Created: 2024-12-03 ;; ;;; Compatibility: Guile 3.0.9 ;; ;;; Keywords: symlink, file management, conflict resolution, backup ;; ;;; ;; ;;; Commentary: ;; ;;; ;; ;;; Stash is a command-line utility written in Guile Scheme designed to facilitate the movement of directories and ;; ;;; creation of symbolic links (symlinks) for files and directories. It allows users to specify a source directory ;; ;;; and a target directory where the symlink should be created. The utility handles potential conflicts with existing ;; ;;; files or directories at the target location, offering users the choice to overwrite, back up, or skip the creation of new symlinks. ;; ;;; ;; ;;; Main Features: ;; ;;; - Command-line argument parsing for specifying source and target paths. ;; ;;; - Conflict detection with interactive user resolution options (overwrite, backup, skip, or cancel). ;; ;;; - Moving directories and creating symlinks. ;; ;;; - Simple and interactive user interface for easy use. ;; ;;; ;; ;;; Usage: ;; ;;; ;; ;;; guile -L . stash.scm --target= --source= ;; ;;; ;; ;;; Replace with the directory where you want the symlink to be created, ;; ;;; and with the path to the source directory. ;; ;;; ;; ;;; License: ;; ;;; ;; ;;; This project is licensed under the GNU General Public License v3. ;; ;;; ;;; Code: ;; Import necessary modules ;; (use-modules (ice-9 getopt-long) ;; (ice-9 popen) ;; (ice-9 rdelim) ;; (ice-9 format) ;; (stash-help) ;; Import the stash-help module for help functionality ;; (srfi srfi-1) ;; (srfi srfi-19)) ;; Use for list handling and date-time formatting ;; ;;; ANSI Color Codes for terminal output ;; (define red-text "\x1b[31m") ;; (define yellow-text "\x1b[33m") ;; (define green-text "\x1b[32m") ;; (define reset-text "\x1b[0m") ;; ;;; Function to wrap messages in color ;; (define (color-message message color) ;; (string-append color message reset-text)) ;; ;;; Helper function to get a formatted current time string ;; (define (current-timestamp) ;; "Return the current date and time as a formatted string." ;; (let* ((time (current-time)) ;; (seconds (time-second time))) ;; (strftime "%Y-%m-%d-%H-%M-%S" (localtime seconds)))) ;; ;;; Improved logging function with timestamp ;; (define (log-action message) ;; "Log an action with a timestamp to the stash.log file." ;; (let ((log-port (open-file "stash.log" "a"))) ;; Open file in append mode ;; (display (string-append "[" (current-timestamp) "] " message) log-port) ;; (newline log-port) ;; (close-port log-port))) ;; ;;; Version function ;; (define (display-version) ;; "Display the current version of the program." ;; (display (format #f "Stash version ~a\n" "0.1.0-alpha.1")) ;; (exit 0)) ;; ;;; Helper function to check for version flag and display version ;; (define (check-for-version args) ;; "Check if --version or -v is in the arguments list." ;; (if (or (member "--version" args) ;; (member "-v" args)) ;; (display-version))) ;; ;;; Expand ~ in paths to the full home directory path ;; (define (expand-home path) ;; "Expand ~ to the user's home directory." ;; (if (string-prefix? "~" path) ;; (string-append (getenv "HOME") (substring path 1)) ;; path)) ;; ;;; Helper function to concatenate paths safely ;; (define (concat-path base path) ;; "Concatenate two paths, ensuring there are no double slashes." ;; (if (string-suffix? "/" base) ;; (string-append (string-drop-right base 1) "/" path) ;; (string-append base "/" path))) ;; ;;; Conflict resolution handler with user options ;; (define (prompt-user-for-action) ;; "Prompt the user to decide how to handle a conflict: overwrite (o), skip (s), or cancel (c)." ;; (display (color-message "A conflict was detected. Choose action - Overwrite (o), Skip (s), or Cancel (c): " yellow-text)) ;; (let ((response (read-line))) ;; (cond ;; ((string-ci=? response "o") 'overwrite) ;; ((string-ci=? response "s") 'skip) ;; ((string-ci=? response "c") 'cancel) ;; (else ;; (display (color-message "Invalid input. Please try again.\n" red-text)) ;; (prompt-user-for-action))))) ;; ;;; Helper function to delete a directory recursively ;; (define (delete-directory path) ;; "Delete a directory recursively." ;; (if (file-is-directory? path) ;; (begin ;; (system (string-append "rm -rf " (shell-quote-argument path))) ;; (log-action (format #f "Deleted directory: ~a" path))) ;; (display (color-message "Error: Path is not a directory.\n" red-text)))) ;; ;;; Helper function to handle conflicts ;; (define (handle-conflict target-source-dir source-dir) ;; "Handle conflicts when the target directory already exists." ;; (let ((choice (prompt-user-for-action))) ;; (cond ;; ((eq? choice 'overwrite) ;; (delete-directory target-source-dir) ;; (rename-file source-dir target-source-dir) ;; (display (color-message (format #f "Overwriting directory ~a.\n" target-source-dir) green-text)) ;; (log-action (format #f "Overwritten directory: ~a" target-source-dir))) ;; ((eq? choice 'skip) ;; (display (color-message "Skipping move operation.\n" green-text)) ;; (log-action (format #f "Skipped moving directory: ~a" target-source-dir))) ;; ((eq? choice 'cancel) ;; (display (color-message "Operation cancelled by user.\n" yellow-text)) ;; (log-action "Operation cancelled by user.") ;; (exit 0))))) ;; ;;; Helper function to ensure target has .config appended, avoiding double slashes ;; (define (ensure-config-path target-dir) ;; "Ensure that the target directory has .config appended, avoiding double slashes." ;; (let ((target-dir (expand-home target-dir))) ;; (if (string-suffix? "/" target-dir) ;; (set! target-dir (string-drop-right target-dir 1))) ;; Remove trailing slash if it exists ;; (if (not (string-suffix? "/.config" target-dir)) ;; (string-append target-dir "/.config") ;; target-dir))) ;; ;;; Helper function to move source to target ;; (define (move-source-to-target source-dir target-dir) ;; "Move the entire source directory to the target directory, ensuring .config in the target path." ;; (let* ((target-dir (ensure-config-path target-dir)) ;; (source-dir (expand-home source-dir)) ;; (source-name (basename source-dir)) ;; (target-source-dir (concat-path target-dir source-name))) ;; ;; Ensure that the .config directory exists in the target ;; (if (not (file-exists? target-dir)) ;; (mkdir target-dir #o755)) ;; ;; Check if the target directory already exists ;; (if (file-exists? target-source-dir) ;; (handle-conflict target-source-dir source-dir) ;; ;; If the target directory doesn't exist, proceed with the move ;; (begin ;; (rename-file source-dir target-source-dir) ;; (display (color-message (format #f "Moved ~a to ~a\n" source-dir target-source-dir) green-text)) ;; (log-action (format #f "Moved ~a to ~a" source-dir target-source-dir)))) ;; target-source-dir)) ;; Return the path of the moved source directory ;; ;;; Helper function to create a symlink in the parent directory of the original source ;; (define (create-symlink-in-parent source-dir target-dir) ;; "Create a symlink in the parent directory of source-dir." ;; (let* ((parent-dir (expand-home (dirname source-dir))) ;; Ensure the parent path is expanded ;; (symlink-target (concat-path parent-dir (basename source-dir))) ;; (target-dir (expand-home target-dir))) ;; Ensure the target path is expanded ;; ;; Check if symlink already exists ;; (if (file-exists? symlink-target) ;; (let ((choice (prompt-user-for-action))) ;; (cond ;; ((eq? choice 'overwrite) ;; (delete-file symlink-target) ;; (symlink target-dir symlink-target) ;; (display (color-message (format #f "Overwriting symlink ~a pointing to ~a\n" symlink-target target-dir) green-text)) ;; (log-action (format #f "Overwritten symlink: ~a pointing to ~a" symlink-target target-dir))) ;; ((eq? choice 'skip) ;; (display (color-message "Skipping symlink creation.\n" green-text)) ;; (log-action (format #f "Skipped symlink creation: ~a" symlink-target))) ;; ((eq? choice 'cancel) ;; (display (color-message "Operation cancelled by user.\n" yellow-text)) ;; (log-action "Operation cancelled by user.") ;; (exit 0)))) ;; ;; Create the symlink if no conflict ;; (begin ;; (symlink target-dir symlink-target) ;; (display (color-message (format #f "Symlink created at ~a pointing to ~a\n" symlink-target target-dir) green-text)) ;; (log-action (format #f "Symlink created at ~a pointing to ~a" symlink-target target-dir)))))) ;; ;;; Main function to handle source and target operations ;; (define (handle-source-and-target source-dir target-dir) ;; "Move the source directory to the target and create a symlink in the parent directory." ;; (let ((target-source-dir (move-source-to-target source-dir target-dir))) ;; Move the directory ;; (create-symlink-in-parent source-dir target-source-dir))) ;; Create the symlink ;; ;;; Function to handle command-line arguments ;; (define (parse-arguments args) ;; "Parse command-line arguments." ;; (getopt-long args ;; '((target (value #t) (single-char #\t)) ;; Support both --target and -t ;; (source (value #t) (single-char #\s)) ;; Support both --source and -s ;; (help (value #f) (single-char #\h)) ;; Support -h for help ;; (version (value #f) (single-char #\v))))) ;; Support -v for version ;; ;;; Main entry point ;; (define (main args) ;; "Main function to parse arguments and execute the program." ;; (setenv "GUILE_AUTO_COMPILE" "0") ;; ;; Check if --help or -h was passed ;; (if (or (member "--help" args) (member "-h" args)) ;; (display-help)) ;; ;; Check if --version or -v was passed ;; (check-for-version args) ;; ;; Parse remaining arguments ;; (let* ((options (parse-arguments args)) ;; (target-dir (assoc-ref options 'target)) ;; (source-dir (assoc-ref options 'source))) ;; (if (and target-dir source-dir) ;; (handle-source-and-target source-dir target-dir) ;; (begin ;; (display (color-message "Error: Missing required arguments.\n" red-text)) ;; (display (color-message "Usage: stash.scm --target= --source=\n" yellow-text)) ;; (exit 1))))) ;; ;; Entry point for stash ;; (main (command-line)) ;; stash.scm --- Main script for moving directories and creating symlinks with conflict resolution (use-modules (ice-9 getopt-long) (stash help) ;; Help module (stash colors) ;; ANSI colors (stash log) ;; Logging module (stash paths) ;; Path handling module (stash conflict) ;; Conflict resolution module (stash file-ops) ;; File and symlink operations module (srfi srfi-1) (srfi srfi-19)) ;;; Version function (define (display-version) "Display the current version of the program." (newline) (display "Stash version 0.1.0-alpha.1\n") (exit 0)) ;;; Helper function to check for version flag and display version (define (check-for-version args) "Check if --version or -v is in the arguments list." (if (or (member "--version" args) (member "-v" args)) (display-version))) ;;; Main function to handle source and target operations (define (handle-source-and-target source-dir target-dir) "Move the source directory to the target and create a symlink in the parent directory." (let ((target-source-dir (move-source-to-target source-dir target-dir))) ;; Only pass source-dir and target-dir (create-symlink-in-parent source-dir target-source-dir))) ;; Create the symlink if no conflict ;;; Function to handle command-line arguments (define (parse-arguments args) "Parse command-line arguments." (getopt-long args '((target (value #t) (single-char #\t)) ;; Support both --target and -t (source (value #t) (single-char #\s)) ;; Support both --source and -s (help (value #f) (single-char #\h)) ;; Support -h for help (version (value #f) (single-char #\v))))) ;; Support -v for version ;;; Main entry point (define (main args) "Main function to parse arguments and execute the program." (setenv "GUILE_AUTO_COMPILE" "0") ;; Disable auto-compilation for performance ;; Check if --help or -h was passed (if (or (member "--help" args) (member "-h" args)) (display-help)) ;; Check if --version or -v was passed (check-for-version args) ;;; Parse remaining arguments (let* ((options (parse-arguments args)) ;; Parse the command-line options (target-dir (assoc-ref options 'target)) ;; Extract --target or -t argument (source-dir (assoc-ref options 'source))) ;; Extract --source or -s argument (if (and target-dir source-dir) ;; Ensure both arguments are provided (handle-source-and-target source-dir target-dir) ;; Proceed with moving the directory and creating the symlink (begin (display (color-message "Error: Missing required arguments.\n" red-text)) ;; Show error in red text (display (color-message "Usage: stash.scm --target --source \n" yellow-text)) ;; Show updated usage message (exit 1))))) ;; Exit with error status ;; ;; Parse remaining arguments ;; (let* ((options (parse-arguments args)) ;; (target-dir (assoc-ref options 'target)) ;; (source-dir (assoc-ref options 'source))) ;; (if (and target-dir source-dir) ;; (handle-source-and-target source-dir target-dir) ;; Handle the source move and symlink creation ;; (begin ;; (display "Error: Missing required arguments.\n") ;; (display "Usage: stash.scm --target= --source=\n") ;; (exit 1))))) ;; Entry point for stash (main (command-line))