!> Project configuration and defaults. !! !! Reads watcher configuration from: !! - Built-in defaults (`set_watch_defaults`) !! - `fpm.toml` under `[extra.fpm-watch]` (`apply_watch_from_manifest`) !! !! Also provides helper routines used by the supervisor (`watch_restart`) for !! auto-restart defaults (delay, max restarts, self path). module watch_config use watch_types, only: watch_opts_t use fpm_strings, only: string_t use fpm_filesystem, only: exists use fpm_error, only: error_t use fpm_toml, only: read_package_file, get_list use tomlf, only: toml_table, toml_stat, get_value implicit none private public set_watch_defaults, apply_watch_from_manifest, normalize_watch_opts, push_feature, get_restart_defaults contains !> Apply built-in defaults to watcher options. !! !! This routine sets conservative defaults intended to work well for most !! projects, and ensures that list fields are always allocated (possibly !! with size zero). subroutine set_watch_defaults(w) type(watch_opts_t), intent(inout) :: w w%verbosity = 0 w%debug = .false. w%poll = 0.5 w%debounce = 0.2 w%rescan = 0.0 w%run_on_start = .true. w%print_files_once = .false. w%silent_fpm = .false. w%watch_deps = .false. w%low_cpu = .false. w%once = .false. if (.not. allocated(w%ignore)) allocate(w%ignore(0)) if (.not. allocated(w%include)) allocate(w%include(0)) if (.not. allocated(w%enabled_features)) allocate(w%enabled_features(0)) end subroutine set_watch_defaults !> Normalize watcher options and clamp to safe bounds. !! !! Clamping prevents pathological values (e.g., negative times, too-small poll !! intervals) from degrading performance or usability. subroutine normalize_watch_opts(w) type(watch_opts_t), intent(inout) :: w if (w%poll < 0.05) w%poll = 0.05 if (w%debounce < 0.0) w%debounce = 0.0 if (w%rescan < 0.0) w%rescan = 0.0 if (w%verbosity > 2) w%verbosity = 2 if (w%verbosity < -1) w%verbosity = -1 end subroutine normalize_watch_opts !> Apply configuration overrides from `fpm.toml` `[extra.fpm-watch]`. !! !! Missing keys are ignored; successfully read values override existing !! defaults. subroutine apply_watch_from_manifest(w) type(watch_opts_t), intent(inout) :: w type(toml_table), allocatable :: root type(toml_table), pointer :: wt logical :: ok call load_watch_table(root, wt, ok) if (.not. ok) return call toml_get_real(wt, "poll", w%poll) call toml_get_real(wt, "debounce", w%debounce) call toml_get_real(wt, "rescan", w%rescan) call toml_get_logical(wt, "debug", w%debug) call toml_get_logical(wt, "print-files", w%print_files_once) call toml_get_logical(wt, "silent-fpm", w%silent_fpm) call toml_get_logical(wt, "run-on-start", w%run_on_start) call toml_get_logical(wt, "deps", w%watch_deps) call toml_get_logical(wt, "low-cpu", w%low_cpu) call toml_get_logical(wt, "once", w%once) call toml_get_int(wt, "verbosity", w%verbosity) call toml_get_list_strings(wt, "ignore", w%ignore) call toml_get_list_strings(wt, "include", w%include) call toml_get_list_strings(wt, "features", w%enabled_features) call normalize_watch_opts(w) end subroutine apply_watch_from_manifest !> Read supervisor defaults from the manifest (when present). !! !! These are used by `watch_restart` and can be configured under !! `[extra.fpm-watch]`. !! !! Keys: !! - `auto-restart` (logical) !! - `restart-delay` (real, seconds) !! - `restart-max` (integer, 0 = unlimited) !! - `self` or `self-exe` (string, explicit path to `fpm-watch`) subroutine get_restart_defaults(auto_restart, restart_delay, restart_max, self_exe) logical, intent(out) :: auto_restart real, intent(out) :: restart_delay integer, intent(out) :: restart_max character(len=:), allocatable, intent(out) :: self_exe type(toml_table), allocatable :: root type(toml_table), pointer :: wt logical :: ok character(len=:), allocatable :: tmp auto_restart = .false. restart_delay = 1.0 restart_max = 0 self_exe = "" call load_watch_table(root, wt, ok) if (.not. ok) return call toml_get_logical(wt, "auto-restart", auto_restart) call toml_get_real(wt, "restart-delay", restart_delay) call toml_get_int(wt, "restart-max", restart_max) tmp = "" call toml_get_string(wt, "self", tmp) if (len_trim(tmp) == 0) call toml_get_string(wt, "self-exe", tmp) if (len_trim(tmp) > 0) self_exe = trim(tmp) if (restart_delay < 0.0) restart_delay = 0.0 if (restart_max < 0) restart_max = 0 end subroutine get_restart_defaults !> Locate and return the `[extra.fpm-watch]` table from `fpm.toml`. !! !! Returns `ok=.false.` when: !! - `fpm.toml` is missing, !! - the TOML cannot be read, !! - the `extra` table is absent, !! - the `fpm-watch` table is absent. subroutine load_watch_table(root, wt, ok) type(toml_table), allocatable, intent(out) :: root type(toml_table), pointer, intent(out) :: wt logical, intent(out) :: ok type(toml_table), pointer :: extra type(error_t), allocatable :: err integer :: stat ok = .false. wt => null() extra => null() if (.not. exists("fpm.toml")) return call read_package_file(root, "fpm.toml", err) if (allocated(err)) return call get_value(root, "extra", extra, requested=.false., stat=stat) if (stat /= toml_stat%success) return if (.not. associated(extra)) return call get_value(extra, "fpm-watch", wt, requested=.false., stat=stat) if (stat /= toml_stat%success) return if (.not. associated(wt)) return ok = .true. end subroutine load_watch_table !> Read a TOML real value if present. subroutine toml_get_real(table, key, v) type(toml_table), intent(inout) :: table character(len=*), intent(in) :: key real, intent(inout) :: v real :: tmp integer :: stat tmp = v call get_value(table, key, tmp, stat=stat) if (stat == toml_stat%success) v = tmp end subroutine toml_get_real !> Read a TOML integer value if present. subroutine toml_get_int(table, key, v) type(toml_table), intent(inout) :: table character(len=*), intent(in) :: key integer, intent(inout) :: v integer :: tmp, stat tmp = v call get_value(table, key, tmp, stat=stat) if (stat == toml_stat%success) v = tmp end subroutine toml_get_int !> Read a TOML logical value if present. subroutine toml_get_logical(table, key, v) type(toml_table), intent(inout) :: table character(len=*), intent(in) :: key logical, intent(inout) :: v logical :: tmp integer :: stat tmp = v call get_value(table, key, tmp, stat=stat) if (stat == toml_stat%success) v = tmp end subroutine toml_get_logical !> Read a TOML string value if present. subroutine toml_get_string(table, key, v) type(toml_table), intent(inout) :: table character(len=*), intent(in) :: key character(len=:), allocatable, intent(inout) :: v character(len=:), allocatable :: tmp integer :: stat call get_value(table, key, tmp, stat=stat) if (stat == toml_stat%success) then if (allocated(tmp)) then v = tmp else v = "" end if end if end subroutine toml_get_string !> Read a TOML string list into an allocatable `string_t` array. subroutine toml_get_list_strings(table, key, list) type(toml_table), intent(inout) :: table character(len=*), intent(in) :: key type(string_t), allocatable, intent(inout) :: list(:) type(error_t), allocatable :: err type(string_t), allocatable :: tmp(:) if (.not. allocated(list)) allocate(list(0)) call get_list(table, key, tmp, err) if (allocated(err)) then if (allocated(tmp)) deallocate(tmp) return end if if (.not. allocated(tmp)) return call move_alloc(tmp, list) end subroutine toml_get_list_strings !> Append a feature name to a `string_t` array if not already present. !! !! This is used both for plugin feature names and for `ignore/include` patterns. subroutine push_feature(features, name) type(string_t), allocatable, intent(inout) :: features(:) character(len=*), intent(in) :: name integer :: n, i type(string_t), allocatable :: tmp(:) if (len_trim(name) == 0) return if (.not. allocated(features)) then allocate(features(1)) features(1)%s = trim(name) return end if do i = 1, size(features) if (trim(features(i)%s) == trim(name)) return end do n = size(features) allocate(tmp(n+1)) tmp(1:n) = features tmp(n+1)%s = trim(name) call move_alloc(tmp, features) end subroutine push_feature end module watch_config