Repository: fsnotify/fsnotify
Pull request #632 (2024-05-01 22:05:58)

Split out Watcher and backends
Rather than have a bunch of Watcher types guarded by build tags, have one Watcher that's always available which proxies to a backend interface.

This will allow adding a polling watcher, fanotify, fsevents, and things like that. There are no backends to select from yet, and I'm not 100% sure yet what an API for that would look like, but this sets up the scaffolding for all of it.

Backends are per-watcher; originally I prototyped something that allows selecting it per-Add() call, but the bookkeeping on that became rather complex, and this use case is probably far too rare to spend a lot of effort on. People can still use different backends by using different Watchers; they'll just have to do the bookkeeping themselves.


Patch:
diff --git a/backend_fen.go b/backend_fen.go
index a816760..c349c32 100644
--- a/backend_fen.go
+++ b/backend_fen.go
@@ -3,9 +3,6 @@
 // FEN backend for illumos (supported) and Solaris (untested, but should work).
 //
 // See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create
-//
-// Note: the documentation on the Watcher type and methods is generated from
-// mkdoc.zsh
 
 package fsnotify
 
@@ -21,112 +18,8 @@ import (
 	"golang.org/x/sys/unix"
 )
 
-// Watcher watches a set of paths, delivering events on a channel.
-//
-// A watcher should not be copied (e.g. pass it by pointer, rather than by
-// value).
-//
-// # Linux notes
-//
-// When a file is removed a Remove event won't be emitted until all file
-// descriptors are closed, and deletes will always emit a Chmod. For example:
-//
-//	fp := os.Open("file")
-//	os.Remove("file")        // Triggers Chmod
-//	fp.Close()               // Triggers Remove
-//
-// This is the event that inotify sends, so not much can be changed about this.
-//
-// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
-// for the number of watches per user, and fs.inotify.max_user_instances
-// specifies the maximum number of inotify instances per user. Every Watcher you
-// create is an "instance", and every path you add is a "watch".
-//
-// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
-// /proc/sys/fs/inotify/max_user_instances
-//
-// To increase them you can use sysctl or write the value to the /proc file:
-//
-//	# Default values on Linux 5.18
-//	sysctl fs.inotify.max_user_watches=124983
-//	sysctl fs.inotify.max_user_instances=128
-//
-// To make the changes persist on reboot edit /etc/sysctl.conf or
-// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
-// your distro's documentation):
-//
-//	fs.inotify.max_user_watches=124983
-//	fs.inotify.max_user_instances=128
-//
-// Reaching the limit will result in a "no space left on device" or "too many open
-// files" error.
-//
-// # kqueue notes (macOS, BSD)
-//
-// kqueue requires opening a file descriptor for every file that's being watched;
-// so if you're watching a directory with five files then that's six file
-// descriptors. You will run in to your system's "max open files" limit faster on
-// these platforms.
-//
-// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
-// control the maximum number of open files, as well as /etc/login.conf on BSD
-// systems.
-//
-// # Windows notes
-//
-// Paths can be added as "C:\path\to\dir", but forward slashes
-// ("C:/path/to/dir") will also work.
-//
-// When a watched directory is removed it will always send an event for the
-// directory itself, but may not send events for all files in that directory.
-// Sometimes it will send events for all times, sometimes it will send no
-// events, and often only for some files.
-//
-// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
-// value that is guaranteed to work with SMB filesystems. If you have many
-// events in quick succession this may not be enough, and you will have to use
-// [WithBufferSize] to increase the value.
-type Watcher struct {
-	// Events sends the filesystem change events.
-	//
-	// fsnotify can send the following events; a "path" here can refer to a
-	// file, directory, symbolic link, or special file like a FIFO.
-	//
-	//   fsnotify.Create    A new path was created; this may be followed by one
-	//                      or more Write events if data also gets written to a
-	//                      file.
-	//
-	//   fsnotify.Remove    A path was removed.
-	//
-	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
-	//                      old path as Event.Name, and a Create event will be
-	//                      sent with the new name. Renames are only sent for
-	//                      paths that are currently watched; e.g. moving an
-	//                      unmonitored file into a monitored directory will
-	//                      show up as just a Create. Similarly, renaming a file
-	//                      to outside a monitored directory will show up as
-	//                      only a Rename.
-	//
-	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
-	//                      also trigger a Write. A single "write action"
-	//                      initiated by the user may show up as one or multiple
-	//                      writes, depending on when the system syncs things to
-	//                      disk. For example when compiling a large Go program
-	//                      you may get hundreds of Write events, and you may
-	//                      want to wait until you've stopped receiving them
-	//                      (see the dedup example in cmd/fsnotify).
-	//
-	//                      Some systems may send Write event for directories
-	//                      when the directory content changes.
-	//
-	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
-	//                      when a file is removed (or more accurately, when a
-	//                      link to an inode is removed). On kqueue it's sent
-	//                      when a file is truncated. On Windows it's never
-	//                      sent.
+type fen struct {
 	Events chan Event
-
-	// Errors sends any errors.
 	Errors chan error
 
 	mu      sync.Mutex
@@ -136,23 +29,14 @@ type Watcher struct {
 	watches map[string]Op // Explicitly watched non-directories
 }
 
-// NewWatcher creates a new Watcher.
-func NewWatcher() (*Watcher, error) {
-	return NewBufferedWatcher(0)
+func newBackend(ev chan Event, errs chan error) (backend, error) {
+	return newBufferedBackend(0, ev, errs)
 }
 
-// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
-// channel.
-//
-// The main use case for this is situations with a very large number of events
-// where the kernel buffer size can't be increased (e.g. due to lack of
-// permissions). An unbuffered Watcher will perform better for almost all use
-// cases, and whenever possible you will be better off increasing the kernel
-// buffers instead of adding a large userspace buffer.
-func NewBufferedWatcher(sz uint) (*Watcher, error) {
-	w := &Watcher{
-		Events:  make(chan Event, sz),
-		Errors:  make(chan error),
+func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
+	w := &fen{
+		Events:  ev,
+		Errors:  errs,
 		dirs:    make(map[string]Op),
 		watches: make(map[string]Op),
 		done:    make(chan struct{}),
@@ -170,7 +54,7 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {
 
 // sendEvent attempts to send an event to the user, returning true if the event
 // was put in the channel successfully and false if the watcher has been closed.
-func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
+func (w *fen) sendEvent(name string, op Op) (sent bool) {
 	select {
 	case <-w.done:
 		return false
@@ -181,7 +65,7 @@ func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
 
 // sendError attempts to send an error to the user, returning true if the error
 // was put in the channel successfully and false if the watcher has been closed.
-func (w *Watcher) sendError(err error) (sent bool) {
+func (w *fen) sendError(err error) (sent bool) {
 	if err == nil {
 		return true
 	}
@@ -193,7 +77,7 @@ func (w *Watcher) sendError(err error) (sent bool) {
 	}
 }
 
-func (w *Watcher) isClosed() bool {
+func (w *fen) isClosed() bool {
 	select {
 	case <-w.done:
 		return true
@@ -202,8 +86,7 @@ func (w *Watcher) isClosed() bool {
 	}
 }
 
-// Close removes all watches and closes the Events channel.
-func (w *Watcher) Close() error {
+func (w *fen) Close() error {
 	// Take the lock used by associateFile to prevent lingering events from
 	// being processed after the close
 	w.mu.Lock()
@@ -215,52 +98,9 @@ func (w *Watcher) Close() error {
 	return w.port.Close()
 }
 
-// Add starts monitoring the path for changes.
-//
-// A path can only be watched once; watching it more than once is a no-op and will
-// not return an error. Paths that do not yet exist on the filesystem cannot be
-// watched.
-//
-// A watch will be automatically removed if the watched path is deleted or
-// renamed. The exception is the Windows backend, which doesn't remove the
-// watcher on renames.
-//
-// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
-// filesystems (/proc, /sys, etc.) generally don't work.
-//
-// Returns [ErrClosed] if [Watcher.Close] was called.
-//
-// See [Watcher.AddWith] for a version that allows adding options.
-//
-// # Watching directories
-//
-// All files in a directory are monitored, including new files that are created
-// after the watcher is started. Subdirectories are not watched (i.e. it's
-// non-recursive).
-//
-// # Watching files
-//
-// Watching individual files (rather than directories) is generally not
-// recommended as many programs (especially editors) update files atomically: it
-// will write to a temporary file which is then moved to to destination,
-// overwriting the original (or some variant thereof). The watcher on the
-// original file is now lost, as that no longer exists.
-//
-// The upshot of this is that a power failure or crash won't leave a
-// half-written file.
-//
-// Watch the parent directory and use Event.Name to filter out files you're not
-// interested in. There is an example of this in cmd/fsnotify/file.go.
-func (w *Watcher) Add(name string) error { return w.AddWith(name) }
+func (w *fen) Add(name string) error { return w.AddWith(name) }
 
-// AddWith is like [Watcher.Add], but allows adding options. When using Add()
-// the defaults described below are used.
-//
-// Possible options are:
-//
-//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
-//     other platforms. The default is 64K (65536 bytes).
-func (w *Watcher) AddWith(name string, opts ...addOpt) error {
+func (w *fen) AddWith(name string, opts ...addOpt) error {
 	if w.isClosed() {
 		return ErrClosed
 	}
@@ -305,15 +145,7 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
 	return nil
 }
 
-// Remove stops monitoring the path for changes.
-//
-// Directories are always removed non-recursively. For example, if you added
-// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
-//
-// Removing a path that has not yet been added returns [ErrNonExistentWatch].
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) Remove(name string) error {
+func (w *fen) Remove(name string) error {
 	if w.isClosed() {
 		return nil
 	}
@@ -356,7 +188,7 @@ func (w *Watcher) Remove(name string) error {
 }
 
 // readEvents contains the main loop that runs in a goroutine watching for events.
-func (w *Watcher) readEvents() {
+func (w *fen) readEvents() {
 	// If this function returns, the watcher has been closed and we can close
 	// these channels
 	defer func() {
@@ -404,7 +236,7 @@ func (w *Watcher) readEvents() {
 	}
 }
 
-func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
+func (w *fen) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
 	files, err := os.ReadDir(path)
 	if err != nil {
 		return err
@@ -430,7 +262,7 @@ func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, ha
 // bitmap matches more than one event type (e.g. the file was both modified and
 // had the attributes changed between when the association was created and the
 // when event was returned)
-func (w *Watcher) handleEvent(event *unix.PortEvent) error {
+func (w *fen) handleEvent(event *unix.PortEvent) error {
 	var (
 		events     = event.Events
 		path       = event.Path
@@ -549,7 +381,7 @@ func (w *Watcher) handleEvent(event *unix.PortEvent) error {
 	return nil
 }
 
-func (w *Watcher) updateDirectory(path string) error {
+func (w *fen) updateDirectory(path string) error {
 	// The directory was modified, so we must find unwatched entities and watch
 	// them. If something was removed from the directory, nothing will happen,
 	// as everything else should still be watched.
@@ -579,7 +411,7 @@ func (w *Watcher) updateDirectory(path string) error {
 	return nil
 }
 
-func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
+func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error {
 	if w.isClosed() {
 		return ErrClosed
 	}
@@ -617,18 +449,14 @@ func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) erro
 	return w.port.AssociatePath(path, stat, events, stat.Mode())
 }
 
-func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
+func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error {
 	if !w.port.PathIsWatched(path) {
 		return nil
 	}
 	return w.port.DissociatePath(path)
 }
 
-// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
-// yet removed).
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) WatchList() []string {
+func (w *fen) WatchList() []string {
 	if w.isClosed() {
 		return nil
 	}
@@ -647,11 +475,7 @@ func (w *Watcher) WatchList() []string {
 	return entries
 }
 
-// Supports reports if all the listed operations are supported by this platform.
-//
-// Create, Write, Remove, Rename, and Chmod are always supported. It can only
-// return false for an Op starting with Unportable.
-func (w *Watcher) xSupports(op Op) bool {
+func (w *fen) xSupports(op Op) bool {
 	if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
 		op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
 		return false
diff --git a/backend_inotify.go b/backend_inotify.go
index 2d41f55..e6ef288 100644
--- a/backend_inotify.go
+++ b/backend_inotify.go
@@ -1,8 +1,5 @@
 //go:build linux && !appengine
 
-// Note: the documentation on the Watcher type and methods is generated from
-// mkdoc.zsh
-
 package fsnotify
 
 import (
@@ -21,112 +18,8 @@ import (
 	"golang.org/x/sys/unix"
 )
 
-// Watcher watches a set of paths, delivering events on a channel.
-//
-// A watcher should not be copied (e.g. pass it by pointer, rather than by
-// value).
-//
-// # Linux notes
-//
-// When a file is removed a Remove event won't be emitted until all file
-// descriptors are closed, and deletes will always emit a Chmod. For example:
-//
-//	fp := os.Open("file")
-//	os.Remove("file")        // Triggers Chmod
-//	fp.Close()               // Triggers Remove
-//
-// This is the event that inotify sends, so not much can be changed about this.
-//
-// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
-// for the number of watches per user, and fs.inotify.max_user_instances
-// specifies the maximum number of inotify instances per user. Every Watcher you
-// create is an "instance", and every path you add is a "watch".
-//
-// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
-// /proc/sys/fs/inotify/max_user_instances
-//
-// To increase them you can use sysctl or write the value to the /proc file:
-//
-//	# Default values on Linux 5.18
-//	sysctl fs.inotify.max_user_watches=124983
-//	sysctl fs.inotify.max_user_instances=128
-//
-// To make the changes persist on reboot edit /etc/sysctl.conf or
-// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
-// your distro's documentation):
-//
-//	fs.inotify.max_user_watches=124983
-//	fs.inotify.max_user_instances=128
-//
-// Reaching the limit will result in a "no space left on device" or "too many open
-// files" error.
-//
-// # kqueue notes (macOS, BSD)
-//
-// kqueue requires opening a file descriptor for every file that's being watched;
-// so if you're watching a directory with five files then that's six file
-// descriptors. You will run in to your system's "max open files" limit faster on
-// these platforms.
-//
-// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
-// control the maximum number of open files, as well as /etc/login.conf on BSD
-// systems.
-//
-// # Windows notes
-//
-// Paths can be added as "C:\path\to\dir", but forward slashes
-// ("C:/path/to/dir") will also work.
-//
-// When a watched directory is removed it will always send an event for the
-// directory itself, but may not send events for all files in that directory.
-// Sometimes it will send events for all times, sometimes it will send no
-// events, and often only for some files.
-//
-// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
-// value that is guaranteed to work with SMB filesystems. If you have many
-// events in quick succession this may not be enough, and you will have to use
-// [WithBufferSize] to increase the value.
-type Watcher struct {
-	// Events sends the filesystem change events.
-	//
-	// fsnotify can send the following events; a "path" here can refer to a
-	// file, directory, symbolic link, or special file like a FIFO.
-	//
-	//   fsnotify.Create    A new path was created; this may be followed by one
-	//                      or more Write events if data also gets written to a
-	//                      file.
-	//
-	//   fsnotify.Remove    A path was removed.
-	//
-	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
-	//                      old path as Event.Name, and a Create event will be
-	//                      sent with the new name. Renames are only sent for
-	//                      paths that are currently watched; e.g. moving an
-	//                      unmonitored file into a monitored directory will
-	//                      show up as just a Create. Similarly, renaming a file
-	//                      to outside a monitored directory will show up as
-	//                      only a Rename.
-	//
-	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
-	//                      also trigger a Write. A single "write action"
-	//                      initiated by the user may show up as one or multiple
-	//                      writes, depending on when the system syncs things to
-	//                      disk. For example when compiling a large Go program
-	//                      you may get hundreds of Write events, and you may
-	//                      want to wait until you've stopped receiving them
-	//                      (see the dedup example in cmd/fsnotify).
-	//
-	//                      Some systems may send Write event for directories
-	//                      when the directory content changes.
-	//
-	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
-	//                      when a file is removed (or more accurately, when a
-	//                      link to an inode is removed). On kqueue it's sent
-	//                      when a file is truncated. On Windows it's never
-	//                      sent.
+type inotify struct {
 	Events chan Event
-
-	// Errors sends any errors.
 	Errors chan error
 
 	// Store fd here as os.File.Read() will no longer return on close after
@@ -273,20 +166,11 @@ func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error
 	return nil
 }
 
-// NewWatcher creates a new Watcher.
-func NewWatcher() (*Watcher, error) {
-	return NewBufferedWatcher(0)
+func newBackend(ev chan Event, errs chan error) (backend, error) {
+	return newBufferedBackend(0, ev, errs)
 }
 
-// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
-// channel.
-//
-// The main use case for this is situations with a very large number of events
-// where the kernel buffer size can't be increased (e.g. due to lack of
-// permissions). An unbuffered Watcher will perform better for almost all use
-// cases, and whenever possible you will be better off increasing the kernel
-// buffers instead of adding a large userspace buffer.
-func NewBufferedWatcher(sz uint) (*Watcher, error) {
+func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
 	// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
 	// I/O operations won't terminate on close.
 	fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
@@ -294,12 +178,12 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {
 		return nil, errno
 	}
 
-	w := &Watcher{
+	w := &inotify{
+		Events:      ev,
+		Errors:      errs,
 		fd:          fd,
 		inotifyFile: os.NewFile(uintptr(fd), ""),
 		watches:     newWatches(),
-		Events:      make(chan Event, sz),
-		Errors:      make(chan error),
 		done:        make(chan struct{}),
 		doneResp:    make(chan struct{}),
 	}
@@ -309,7 +193,7 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {
 }
 
 // Returns true if the event was sent, or false if watcher is closed.
-func (w *Watcher) sendEvent(e Event) bool {
+func (w *inotify) sendEvent(e Event) bool {
 	select {
 	case <-w.done:
 		return false
@@ -319,7 +203,7 @@ func (w *Watcher) sendEvent(e Event) bool {
 }
 
 // Returns true if the error was sent, or false if watcher is closed.
-func (w *Watcher) sendError(err error) bool {
+func (w *inotify) sendError(err error) bool {
 	if err == nil {
 		return true
 	}
@@ -331,7 +215,7 @@ func (w *Watcher) sendError(err error) bool {
 	}
 }
 
-func (w *Watcher) isClosed() bool {
+func (w *inotify) isClosed() bool {
 	select {
 	case <-w.done:
 		return true
@@ -340,8 +224,7 @@ func (w *Watcher) isClosed() bool {
 	}
 }
 
-// Close removes all watches and closes the Events channel.
-func (w *Watcher) Close() error {
+func (w *inotify) Close() error {
 	w.doneMu.Lock()
 	if w.isClosed() {
 		w.doneMu.Unlock()
@@ -363,52 +246,9 @@ func (w *Watcher) Close() error {
 	return nil
 }
 
-// Add starts monitoring the path for changes.
-//
-// A path can only be watched once; watching it more than once is a no-op and will
-// not return an error. Paths that do not yet exist on the filesystem cannot be
-// watched.
-//
-// A watch will be automatically removed if the watched path is deleted or
-// renamed. The exception is the Windows backend, which doesn't remove the
-// watcher on renames.
-//
-// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
-// filesystems (/proc, /sys, etc.) generally don't work.
-//
-// Returns [ErrClosed] if [Watcher.Close] was called.
-//
-// See [Watcher.AddWith] for a version that allows adding options.
-//
-// # Watching directories
-//
-// All files in a directory are monitored, including new files that are created
-// after the watcher is started. Subdirectories are not watched (i.e. it's
-// non-recursive).
-//
-// # Watching files
-//
-// Watching individual files (rather than directories) is generally not
-// recommended as many programs (especially editors) update files atomically: it
-// will write to a temporary file which is then moved to to destination,
-// overwriting the original (or some variant thereof). The watcher on the
-// original file is now lost, as that no longer exists.
-//
-// The upshot of this is that a power failure or crash won't leave a
-// half-written file.
-//
-// Watch the parent directory and use Event.Name to filter out files you're not
-// interested in. There is an example of this in cmd/fsnotify/file.go.
-func (w *Watcher) Add(name string) error { return w.AddWith(name) }
-
-// AddWith is like [Watcher.Add], but allows adding options. When using Add()
-// the defaults described below are used.
-//
-// Possible options are:
-//
-//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
-//     other platforms. The default is 64K (65536 bytes).
-func (w *Watcher) AddWith(path string, opts ...addOpt) error {
+func (w *inotify) Add(name string) error { return w.AddWith(name) }
+
+func (w *inotify) AddWith(path string, opts ...addOpt) error {
 	if w.isClosed() {
 		return ErrClosed
 	}
@@ -452,7 +292,7 @@ func (w *Watcher) AddWith(path string, opts ...addOpt) error {
 	return w.add(path, with, false)
 }
 
-func (w *Watcher) add(path string, with withOpts, recurse bool) error {
+func (w *inotify) add(path string, with withOpts, recurse bool) error {
 	var flags uint32
 	if with.noFollow {
 		flags |= unix.IN_DONT_FOLLOW
@@ -487,7 +327,7 @@ func (w *Watcher) add(path string, with withOpts, recurse bool) error {
 	return w.register(path, flags, recurse)
 }
 
-func (w *Watcher) register(path string, flags uint32, recurse bool) error {
+func (w *inotify) register(path string, flags uint32, recurse bool) error {
 	return w.watches.updatePath(path, func(existing *watch) (*watch, error) {
 		if existing != nil {
 			flags |= existing.flags | unix.IN_MASK_ADD
@@ -513,15 +353,7 @@ func (w *Watcher) register(path string, flags uint32, recurse bool) error {
 	})
 }
 
-// Remove stops monitoring the path for changes.
-//
-// Directories are always removed non-recursively. For example, if you added
-// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
-//
-// Removing a path that has not yet been added returns [ErrNonExistentWatch].
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) Remove(name string) error {
+func (w *inotify) Remove(name string) error {
 	if w.isClosed() {
 		return nil
 	}
@@ -532,7 +364,7 @@ func (w *Watcher) Remove(name string) error {
 	return w.remove(filepath.Clean(name))
 }
 
-func (w *Watcher) remove(name string) error {
+func (w *inotify) remove(name string) error {
 	wds, err := w.watches.removePath(name)
 	if err != nil {
 		return err
@@ -558,11 +390,7 @@ func (w *Watcher) remove(name string) error {
 	return nil
 }
 
-// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
-// yet removed).
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) WatchList() []string {
+func (w *inotify) WatchList() []string {
 	if w.isClosed() {
 		return nil
 	}
@@ -579,7 +407,7 @@ func (w *Watcher) WatchList() []string {
 
 // readEvents reads from the inotify file descriptor, converts the
 // received events into Event objects and sends them via the Events channel
-func (w *Watcher) readEvents() {
+func (w *inotify) readEvents() {
 	defer func() {
 		close(w.doneResp)
 		close(w.Errors)
@@ -744,7 +572,7 @@ func (w *Watcher) readEvents() {
 	}
 }
 
-func (w *Watcher) isRecursive(path string) bool {
+func (w *inotify) isRecursive(path string) bool {
 	ww := w.watches.byPath(path)
 	if ww == nil { // path could be a file, so also check the Dir.
 		ww = w.watches.byPath(filepath.Dir(path))
@@ -752,7 +580,7 @@ func (w *Watcher) isRecursive(path string) bool {
 	return ww != nil && ww.recurse
 }
 
-func (w *Watcher) newEvent(name string, mask, cookie uint32) Event {
+func (w *inotify) newEvent(name string, mask, cookie uint32) Event {
 	e := Event{Name: name}
 	if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
 		e.Op |= Create
@@ -807,15 +635,11 @@ func (w *Watcher) newEvent(name string, mask, cookie uint32) Event {
 	return e
 }
 
-// Supports reports if all the listed operations are supported by this platform.
-//
-// Create, Write, Remove, Rename, and Chmod are always supported. It can only
-// return false for an Op starting with Unportable.
-func (w *Watcher) xSupports(op Op) bool {
+func (w *inotify) xSupports(op Op) bool {
 	return true // Supports everything.
 }
 
-func (w *Watcher) state() {
+func (w *inotify) state() {
 	w.watches.mu.Lock()
 	defer w.watches.mu.Unlock()
 	for wd, ww := range w.watches.wd {
diff --git a/backend_kqueue.go b/backend_kqueue.go
index 9e6aa9d..1a640d5 100644
--- a/backend_kqueue.go
+++ b/backend_kqueue.go
@@ -1,8 +1,5 @@
 //go:build freebsd || openbsd || netbsd || dragonfly || darwin
 
-// Note: the documentation on the Watcher type and methods is generated from
-// mkdoc.zsh
-
 package fsnotify
 
 import (
@@ -18,112 +15,8 @@ import (
 	"golang.org/x/sys/unix"
 )
 
-// Watcher watches a set of paths, delivering events on a channel.
-//
-// A watcher should not be copied (e.g. pass it by pointer, rather than by
-// value).
-//
-// # Linux notes
-//
-// When a file is removed a Remove event won't be emitted until all file
-// descriptors are closed, and deletes will always emit a Chmod. For example:
-//
-//	fp := os.Open("file")
-//	os.Remove("file")        // Triggers Chmod
-//	fp.Close()               // Triggers Remove
-//
-// This is the event that inotify sends, so not much can be changed about this.
-//
-// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
-// for the number of watches per user, and fs.inotify.max_user_instances
-// specifies the maximum number of inotify instances per user. Every Watcher you
-// create is an "instance", and every path you add is a "watch".
-//
-// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
-// /proc/sys/fs/inotify/max_user_instances
-//
-// To increase them you can use sysctl or write the value to the /proc file:
-//
-//	# Default values on Linux 5.18
-//	sysctl fs.inotify.max_user_watches=124983
-//	sysctl fs.inotify.max_user_instances=128
-//
-// To make the changes persist on reboot edit /etc/sysctl.conf or
-// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
-// your distro's documentation):
-//
-//	fs.inotify.max_user_watches=124983
-//	fs.inotify.max_user_instances=128
-//
-// Reaching the limit will result in a "no space left on device" or "too many open
-// files" error.
-//
-// # kqueue notes (macOS, BSD)
-//
-// kqueue requires opening a file descriptor for every file that's being watched;
-// so if you're watching a directory with five files then that's six file
-// descriptors. You will run in to your system's "max open files" limit faster on
-// these platforms.
-//
-// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
-// control the maximum number of open files, as well as /etc/login.conf on BSD
-// systems.
-//
-// # Windows notes
-//
-// Paths can be added as "C:\path\to\dir", but forward slashes
-// ("C:/path/to/dir") will also work.
-//
-// When a watched directory is removed it will always send an event for the
-// directory itself, but may not send events for all files in that directory.
-// Sometimes it will send events for all times, sometimes it will send no
-// events, and often only for some files.
-//
-// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
-// value that is guaranteed to work with SMB filesystems. If you have many
-// events in quick succession this may not be enough, and you will have to use
-// [WithBufferSize] to increase the value.
-type Watcher struct {
-	// Events sends the filesystem change events.
-	//
-	// fsnotify can send the following events; a "path" here can refer to a
-	// file, directory, symbolic link, or special file like a FIFO.
-	//
-	//   fsnotify.Create    A new path was created; this may be followed by one
-	//                      or more Write events if data also gets written to a
-	//                      file.
-	//
-	//   fsnotify.Remove    A path was removed.
-	//
-	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
-	//                      old path as Event.Name, and a Create event will be
-	//                      sent with the new name. Renames are only sent for
-	//                      paths that are currently watched; e.g. moving an
-	//                      unmonitored file into a monitored directory will
-	//                      show up as just a Create. Similarly, renaming a file
-	//                      to outside a monitored directory will show up as
-	//                      only a Rename.
-	//
-	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
-	//                      also trigger a Write. A single "write action"
-	//                      initiated by the user may show up as one or multiple
-	//                      writes, depending on when the system syncs things to
-	//                      disk. For example when compiling a large Go program
-	//                      you may get hundreds of Write events, and you may
-	//                      want to wait until you've stopped receiving them
-	//                      (see the dedup example in cmd/fsnotify).
-	//
-	//                      Some systems may send Write event for directories
-	//                      when the directory content changes.
-	//
-	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
-	//                      when a file is removed (or more accurately, when a
-	//                      link to an inode is removed). On kqueue it's sent
-	//                      when a file is truncated. On Windows it's never
-	//                      sent.
+type kqueue struct {
 	Events chan Event
-
-	// Errors sends any errors.
 	Errors chan error
 
 	kq        int    // File descriptor (as returned by the kqueue() syscall).
@@ -286,30 +179,21 @@ func (w *watches) seenBefore(path string) bool {
 	return ok
 }
 
-// NewWatcher creates a new Watcher.
-func NewWatcher() (*Watcher, error) {
-	return NewBufferedWatcher(0)
+func newBackend(ev chan Event, errs chan error) (backend, error) {
+	return newBufferedBackend(0, ev, errs)
 }
 
-// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
-// channel.
-//
-// The main use case for this is situations with a very large number of events
-// where the kernel buffer size can't be increased (e.g. due to lack of
-// permissions). An unbuffered Watcher will perform better for almost all use
-// cases, and whenever possible you will be better off increasing the kernel
-// buffers instead of adding a large userspace buffer.
-func NewBufferedWatcher(sz uint) (*Watcher, error) {
+func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
 	kq, closepipe, err := newKqueue()
 	if err != nil {
 		return nil, err
 	}
 
-	w := &Watcher{
+	w := &kqueue{
+		Events:    ev,
+		Errors:    errs,
 		kq:        kq,
 		closepipe: closepipe,
-		Events:    make(chan Event, sz),
-		Errors:    make(chan error),
 		done:      make(chan struct{}),
 		watches:   newWatches(),
 	}
@@ -356,7 +240,7 @@ func newKqueue() (kq int, closepipe [2]int, err error) {
 }
 
 // Returns true if the event was sent, or false if watcher is closed.
-func (w *Watcher) sendEvent(e Event) bool {
+func (w *kqueue) sendEvent(e Event) bool {
 	select {
 	case <-w.done:
 		return false
@@ -366,7 +250,7 @@ func (w *Watcher) sendEvent(e Event) bool {
 }
 
 // Returns true if the error was sent, or false if watcher is closed.
-func (w *Watcher) sendError(err error) bool {
+func (w *kqueue) sendError(err error) bool {
 	if err == nil {
 		return true
 	}
@@ -378,7 +262,7 @@ func (w *Watcher) sendError(err error) bool {
 	}
 }
 
-func (w *Watcher) isClosed() bool {
+func (w *kqueue) isClosed() bool {
 	select {
 	case <-w.done:
 		return true
@@ -387,8 +271,7 @@ func (w *Watcher) isClosed() bool {
 	}
 }
 
-// Close removes all watches and closes the Events channel.
-func (w *Watcher) Close() error {
+func (w *kqueue) Close() error {
 	w.doneMu.Lock()
 	if w.isClosed() {
 		w.doneMu.Unlock()
@@ -407,52 +290,9 @@ func (w *Watcher) Close() error {
 	return nil
 }
 
-// Add starts monitoring the path for changes.
-//
-// A path can only be watched once; watching it more than once is a no-op and will
-// not return an error. Paths that do not yet exist on the filesystem cannot be
-// watched.
-//
-// A watch will be automatically removed if the watched path is deleted or
-// renamed. The exception is the Windows backend, which doesn't remove the
-// watcher on renames.
-//
-// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
-// filesystems (/proc, /sys, etc.) generally don't work.
-//
-// Returns [ErrClosed] if [Watcher.Close] was called.
-//
-// See [Watcher.AddWith] for a version that allows adding options.
-//
-// # Watching directories
-//
-// All files in a directory are monitored, including new files that are created
-// after the watcher is started. Subdirectories are not watched (i.e. it's
-// non-recursive).
-//
-// # Watching files
-//
-// Watching individual files (rather than directories) is generally not
-// recommended as many programs (especially editors) update files atomically: it
-// will write to a temporary file which is then moved to to destination,
-// overwriting the original (or some variant thereof). The watcher on the
-// original file is now lost, as that no longer exists.
-//
-// The upshot of this is that a power failure or crash won't leave a
-// half-written file.
-//
-// Watch the parent directory and use Event.Name to filter out files you're not
-// interested in. There is an example of this in cmd/fsnotify/file.go.
-func (w *Watcher) Add(name string) error { return w.AddWith(name) }
+func (w *kqueue) Add(name string) error { return w.AddWith(name) }
 
-// AddWith is like [Watcher.Add], but allows adding options. When using Add()
-// the defaults described below are used.
-//
-// Possible options are:
-//
-//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
-//     other platforms. The default is 64K (65536 bytes).
-func (w *Watcher) AddWith(name string, opts ...addOpt) error {
+func (w *kqueue) AddWith(name string, opts ...addOpt) error {
 	if debug {
 		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  AddWith(%q)\n",
 			time.Now().Format("15:04:05.000000000"), name)
@@ -468,15 +308,7 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
 	return err
 }
 
-// Remove stops monitoring the path for changes.
-//
-// Directories are always removed non-recursively. For example, if you added
-// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
-//
-// Removing a path that has not yet been added returns [ErrNonExistentWatch].
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) Remove(name string) error {
+func (w *kqueue) Remove(name string) error {
 	if debug {
 		fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s  Remove(%q)\n",
 			time.Now().Format("15:04:05.000000000"), name)
@@ -484,7 +316,7 @@ func (w *Watcher) Remove(name string) error {
 	return w.remove(name, true)
 }
 
-func (w *Watcher) remove(name string, unwatchFiles bool) error {
+func (w *kqueue) remove(name string, unwatchFiles bool) error {
 	if w.isClosed() {
 		return nil
 	}
@@ -517,11 +349,7 @@ func (w *Watcher) remove(name string, unwatchFiles bool) error {
 	return nil
 }
 
-// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
-// yet removed).
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) WatchList() []string {
+func (w *kqueue) WatchList() []string {
 	if w.isClosed() {
 		return nil
 	}
@@ -535,7 +363,7 @@ const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | un
 // described in kevent(2).
 //
 // Returns the real path to the file which was added, with symlinks resolved.
-func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
+func (w *kqueue) addWatch(name string, flags uint32) (string, error) {
 	if w.isClosed() {
 		return "", ErrClosed
 	}
@@ -626,7 +454,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
 
 // readEvents reads from kqueue and converts the received kevents into
 // Event values that it sends down the Events channel.
-func (w *Watcher) readEvents() {
+func (w *kqueue) readEvents() {
 	defer func() {
 		close(w.Events)
 		close(w.Errors)
@@ -738,7 +566,7 @@ func (w *Watcher) readEvents() {
 }
 
 // newEvent returns an platform-independent Event based on kqueue Fflags.
-func (w *Watcher) newEvent(name, linkName string, mask uint32) Event {
+func (w *kqueue) newEvent(name, linkName string, mask uint32) Event {
 	e := Event{Name: name}
 	if linkName != "" {
 		// If the user watched "/path/link" then emit events as "/path/link"
@@ -767,7 +595,7 @@ func (w *Watcher) newEvent(name, linkName string, mask uint32) Event {
 }
 
 // watchDirectoryFiles to mimic inotify when adding a watch on a directory
-func (w *Watcher) watchDirectoryFiles(dirPath string) error {
+func (w *kqueue) watchDirectoryFiles(dirPath string) error {
 	files, err := os.ReadDir(dirPath)
 	if err != nil {
 		return err
@@ -805,7 +633,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
 //
 // This functionality is to have the BSD watcher match the inotify, which sends
 // a create event for files created in a watched directory.
-func (w *Watcher) dirChange(dir string) error {
+func (w *kqueue) dirChange(dir string) error {
 	files, err := os.ReadDir(dir)
 	if err != nil {
 		// Directory no longer exists: we can ignore this safely. kqueue will
@@ -836,7 +664,7 @@ func (w *Watcher) dirChange(dir string) error {
 
 // Send a create event if the file isn't already being tracked, and start
 // watching this file.
-func (w *Watcher) sendCreateIfNew(path string, fi os.FileInfo) error {
+func (w *kqueue) sendCreateIfNew(path string, fi os.FileInfo) error {
 	if !w.watches.seenBefore(path) {
 		if !w.sendEvent(Event{Name: path, Op: Create}) {
 			return nil
@@ -852,7 +680,7 @@ func (w *Watcher) sendCreateIfNew(path string, fi os.FileInfo) error {
 	return nil
 }
 
-func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
+func (w *kqueue) internalWatch(name string, fi os.FileInfo) (string, error) {
 	if fi.IsDir() {
 		// mimic Linux providing delete events for subdirectories, but preserve
 		// the flags used if currently watching subdirectory
@@ -865,7 +693,7 @@ func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
 }
 
 // Register events with the queue.
-func (w *Watcher) register(fds []int, flags int, fflags uint32) error {
+func (w *kqueue) register(fds []int, flags int, fflags uint32) error {
 	changes := make([]unix.Kevent_t, len(fds))
 	for i, fd := range fds {
 		// SetKevent converts int to the platform-specific types.
@@ -882,7 +710,7 @@ func (w *Watcher) register(fds []int, flags int, fflags uint32) error {
 }
 
 // read retrieves pending events, or waits until an event occurs.
-func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
+func (w *kqueue) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
 	n, err := unix.Kevent(w.kq, nil, events, nil)
 	if err != nil {
 		return nil, err
@@ -890,11 +718,7 @@ func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
 	return events[0:n], nil
 }
 
-// Supports reports if all the listed operations are supported by this platform.
-//
-// Create, Write, Remove, Rename, and Chmod are always supported. It can only
-// return false for an Op starting with Unportable.
-func (w *Watcher) xSupports(op Op) bool {
+func (w *kqueue) xSupports(op Op) bool {
 	if runtime.GOOS == "freebsd" {
 		//return true // Supports everything.
 	}
diff --git a/backend_other.go b/backend_other.go
index 9cf8276..5eb5dbc 100644
--- a/backend_other.go
+++ b/backend_other.go
@@ -1,204 +1,23 @@
 //go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
 
-// Note: the documentation on the Watcher type and methods is generated from
-// mkdoc.zsh
-
 package fsnotify
 
 import "errors"
 
-// Watcher watches a set of paths, delivering events on a channel.
-//
-// A watcher should not be copied (e.g. pass it by pointer, rather than by
-// value).
-//
-// # Linux notes
-//
-// When a file is removed a Remove event won't be emitted until all file
-// descriptors are closed, and deletes will always emit a Chmod. For example:
-//
-//	fp := os.Open("file")
-//	os.Remove("file")        // Triggers Chmod
-//	fp.Close()               // Triggers Remove
-//
-// This is the event that inotify sends, so not much can be changed about this.
-//
-// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
-// for the number of watches per user, and fs.inotify.max_user_instances
-// specifies the maximum number of inotify instances per user. Every Watcher you
-// create is an "instance", and every path you add is a "watch".
-//
-// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
-// /proc/sys/fs/inotify/max_user_instances
-//
-// To increase them you can use sysctl or write the value to the /proc file:
-//
-//	# Default values on Linux 5.18
-//	sysctl fs.inotify.max_user_watches=124983
-//	sysctl fs.inotify.max_user_instances=128
-//
-// To make the changes persist on reboot edit /etc/sysctl.conf or
-// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
-// your distro's documentation):
-//
-//	fs.inotify.max_user_watches=124983
-//	fs.inotify.max_user_instances=128
-//
-// Reaching the limit will result in a "no space left on device" or "too many open
-// files" error.
-//
-// # kqueue notes (macOS, BSD)
-//
-// kqueue requires opening a file descriptor for every file that's being watched;
-// so if you're watching a directory with five files then that's six file
-// descriptors. You will run in to your system's "max open files" limit faster on
-// these platforms.
-//
-// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
-// control the maximum number of open files, as well as /etc/login.conf on BSD
-// systems.
-//
-// # Windows notes
-//
-// Paths can be added as "C:\path\to\dir", but forward slashes
-// ("C:/path/to/dir") will also work.
-//
-// When a watched directory is removed it will always send an event for the
-// directory itself, but may not send events for all files in that directory.
-// Sometimes it will send events for all times, sometimes it will send no
-// events, and often only for some files.
-//
-// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
-// value that is guaranteed to work with SMB filesystems. If you have many
-// events in quick succession this may not be enough, and you will have to use
-// [WithBufferSize] to increase the value.
-type Watcher struct {
-	// Events sends the filesystem change events.
-	//
-	// fsnotify can send the following events; a "path" here can refer to a
-	// file, directory, symbolic link, or special file like a FIFO.
-	//
-	//   fsnotify.Create    A new path was created; this may be followed by one
-	//                      or more Write events if data also gets written to a
-	//                      file.
-	//
-	//   fsnotify.Remove    A path was removed.
-	//
-	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
-	//                      old path as Event.Name, and a Create event will be
-	//                      sent with the new name. Renames are only sent for
-	//                      paths that are currently watched; e.g. moving an
-	//                      unmonitored file into a monitored directory will
-	//                      show up as just a Create. Similarly, renaming a file
-	//                      to outside a monitored directory will show up as
-	//                      only a Rename.
-	//
-	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
-	//                      also trigger a Write. A single "write action"
-	//                      initiated by the user may show up as one or multiple
-	//                      writes, depending on when the system syncs things to
-	//                      disk. For example when compiling a large Go program
-	//                      you may get hundreds of Write events, and you may
-	//                      want to wait until you've stopped receiving them
-	//                      (see the dedup example in cmd/fsnotify).
-	//
-	//                      Some systems may send Write event for directories
-	//                      when the directory content changes.
-	//
-	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
-	//                      when a file is removed (or more accurately, when a
-	//                      link to an inode is removed). On kqueue it's sent
-	//                      when a file is truncated. On Windows it's never
-	//                      sent.
+type other struct {
 	Events chan Event
-
-	// Errors sends any errors.
 	Errors chan error
 }
 
-// NewWatcher creates a new Watcher.
-func NewWatcher() (*Watcher, error) {
+func newBackend(ev chan Event, errs chan error) (backend, error) {
 	return nil, errors.New("fsnotify not supported on the current platform")
 }
-
-// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
-// channel.
-//
-// The main use case for this is situations with a very large number of events
-// where the kernel buffer size can't be increased (e.g. due to lack of
-// permissions). An unbuffered Watcher will perform better for almost all use
-// cases, and whenever possible you will be better off increasing the kernel
-// buffers instead of adding a large userspace buffer.
-func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() }
-
-// Close removes all watches and closes the Events channel.
-func (w *Watcher) Close() error { return nil }
-
-// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
-// yet removed).
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) WatchList() []string { return nil }
-
-// Add starts monitoring the path for changes.
-//
-// A path can only be watched once; watching it more than once is a no-op and will
-// not return an error. Paths that do not yet exist on the filesystem cannot be
-// watched.
-//
-// A watch will be automatically removed if the watched path is deleted or
-// renamed. The exception is the Windows backend, which doesn't remove the
-// watcher on renames.
-//
-// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
-// filesystems (/proc, /sys, etc.) generally don't work.
-//
-// Returns [ErrClosed] if [Watcher.Close] was called.
-//
-// See [Watcher.AddWith] for a version that allows adding options.
-//
-// # Watching directories
-//
-// All files in a directory are monitored, including new files that are created
-// after the watcher is started. Subdirectories are not watched (i.e. it's
-// non-recursive).
-//
-// # Watching files
-//
-// Watching individual files (rather than directories) is generally not
-// recommended as many programs (especially editors) update files atomically: it
-// will write to a temporary file which is then moved to to destination,
-// overwriting the original (or some variant thereof). The watcher on the
-// original file is now lost, as that no longer exists.
-//
-// The upshot of this is that a power failure or crash won't leave a
-// half-written file.
-//
-// Watch the parent directory and use Event.Name to filter out files you're not
-// interested in. There is an example of this in cmd/fsnotify/file.go.
-func (w *Watcher) Add(name string) error { return nil }
-
-// AddWith is like [Watcher.Add], but allows adding options. When using Add()
-// the defaults described below are used.
-//
-// Possible options are:
-//
-//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
-//     other platforms. The default is 64K (65536 bytes).
-func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }
-
-// Remove stops monitoring the path for changes.
-//
-// Directories are always removed non-recursively. For example, if you added
-// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
-//
-// Removing a path that has not yet been added returns [ErrNonExistentWatch].
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) Remove(name string) error { return nil }
-
-// Supports reports if all the listed operations are supported by this platform.
-//
-// Create, Write, Remove, Rename, and Chmod are always supported. It can only
-// return false for an Op starting with Unportable.
-func (w *Watcher) xSupports(op Op) bool { return false }
+func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
+	return newBackend(ev, errs)
+}
+func (w *other) Close() error                              { return nil }
+func (w *other) WatchList() []string                       { return nil }
+func (w *other) Add(name string) error                     { return nil }
+func (w *other) AddWith(name string, opts ...addOpt) error { return nil }
+func (w *other) Remove(name string) error                  { return nil }
+func (w *other) xSupports(op Op) bool                      { return false }
diff --git a/backend_windows.go b/backend_windows.go
index 688e698..c54a630 100644
--- a/backend_windows.go
+++ b/backend_windows.go
@@ -3,9 +3,6 @@
 // Windows backend based on ReadDirectoryChangesW()
 //
 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
-//
-// Note: the documentation on the Watcher type and methods is generated from
-// mkdoc.zsh
 
 package fsnotify
 
@@ -25,112 +22,8 @@ import (
 	"golang.org/x/sys/windows"
 )
 
-// Watcher watches a set of paths, delivering events on a channel.
-//
-// A watcher should not be copied (e.g. pass it by pointer, rather than by
-// value).
-//
-// # Linux notes
-//
-// When a file is removed a Remove event won't be emitted until all file
-// descriptors are closed, and deletes will always emit a Chmod. For example:
-//
-//	fp := os.Open("file")
-//	os.Remove("file")        // Triggers Chmod
-//	fp.Close()               // Triggers Remove
-//
-// This is the event that inotify sends, so not much can be changed about this.
-//
-// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
-// for the number of watches per user, and fs.inotify.max_user_instances
-// specifies the maximum number of inotify instances per user. Every Watcher you
-// create is an "instance", and every path you add is a "watch".
-//
-// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
-// /proc/sys/fs/inotify/max_user_instances
-//
-// To increase them you can use sysctl or write the value to the /proc file:
-//
-//	# Default values on Linux 5.18
-//	sysctl fs.inotify.max_user_watches=124983
-//	sysctl fs.inotify.max_user_instances=128
-//
-// To make the changes persist on reboot edit /etc/sysctl.conf or
-// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
-// your distro's documentation):
-//
-//	fs.inotify.max_user_watches=124983
-//	fs.inotify.max_user_instances=128
-//
-// Reaching the limit will result in a "no space left on device" or "too many open
-// files" error.
-//
-// # kqueue notes (macOS, BSD)
-//
-// kqueue requires opening a file descriptor for every file that's being watched;
-// so if you're watching a directory with five files then that's six file
-// descriptors. You will run in to your system's "max open files" limit faster on
-// these platforms.
-//
-// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
-// control the maximum number of open files, as well as /etc/login.conf on BSD
-// systems.
-//
-// # Windows notes
-//
-// Paths can be added as "C:\path\to\dir", but forward slashes
-// ("C:/path/to/dir") will also work.
-//
-// When a watched directory is removed it will always send an event for the
-// directory itself, but may not send events for all files in that directory.
-// Sometimes it will send events for all times, sometimes it will send no
-// events, and often only for some files.
-//
-// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
-// value that is guaranteed to work with SMB filesystems. If you have many
-// events in quick succession this may not be enough, and you will have to use
-// [WithBufferSize] to increase the value.
-type Watcher struct {
-	// Events sends the filesystem change events.
-	//
-	// fsnotify can send the following events; a "path" here can refer to a
-	// file, directory, symbolic link, or special file like a FIFO.
-	//
-	//   fsnotify.Create    A new path was created; this may be followed by one
-	//                      or more Write events if data also gets written to a
-	//                      file.
-	//
-	//   fsnotify.Remove    A path was removed.
-	//
-	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
-	//                      old path as Event.Name, and a Create event will be
-	//                      sent with the new name. Renames are only sent for
-	//                      paths that are currently watched; e.g. moving an
-	//                      unmonitored file into a monitored directory will
-	//                      show up as just a Create. Similarly, renaming a file
-	//                      to outside a monitored directory will show up as
-	//                      only a Rename.
-	//
-	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
-	//                      also trigger a Write. A single "write action"
-	//                      initiated by the user may show up as one or multiple
-	//                      writes, depending on when the system syncs things to
-	//                      disk. For example when compiling a large Go program
-	//                      you may get hundreds of Write events, and you may
-	//                      want to wait until you've stopped receiving them
-	//                      (see the dedup example in cmd/fsnotify).
-	//
-	//                      Some systems may send Write event for directories
-	//                      when the directory content changes.
-	//
-	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
-	//                      when a file is removed (or more accurately, when a
-	//                      link to an inode is removed). On kqueue it's sent
-	//                      when a file is truncated. On Windows it's never
-	//                      sent.
+type readDirChangesW struct {
 	Events chan Event
-
-	// Errors sends any errors.
 	Errors chan error
 
 	port  windows.Handle // Handle to completion port
@@ -142,43 +35,34 @@ type Watcher struct {
 	closed  bool       // Set to true when Close() is first called
 }
 
-// NewWatcher creates a new Watcher.
-func NewWatcher() (*Watcher, error) {
-	return NewBufferedWatcher(50)
+func newBackend(ev chan Event, errs chan error) (backend, error) {
+	return newBufferedBackend(50, ev, errs)
 }
 
-// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
-// channel.
-//
-// The main use case for this is situations with a very large number of events
-// where the kernel buffer size can't be increased (e.g. due to lack of
-// permissions). An unbuffered Watcher will perform better for almost all use
-// cases, and whenever possible you will be better off increasing the kernel
-// buffers instead of adding a large userspace buffer.
-func NewBufferedWatcher(sz uint) (*Watcher, error) {
+func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
 	port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
 	if err != nil {
 		return nil, os.NewSyscallError("CreateIoCompletionPort", err)
 	}
-	w := &Watcher{
+	w := &readDirChangesW{
+		Events:  ev,
+		Errors:  errs,
 		port:    port,
 		watches: make(watchMap),
 		input:   make(chan *input, 1),
-		Events:  make(chan Event, sz),
-		Errors:  make(chan error),
 		quit:    make(chan chan<- error, 1),
 	}
 	go w.readEvents()
 	return w, nil
 }
 
-func (w *Watcher) isClosed() bool {
+func (w *readDirChangesW) isClosed() bool {
 	w.mu.Lock()
 	defer w.mu.Unlock()
 	return w.closed
 }
 
-func (w *Watcher) sendEvent(name, renamedFrom string, mask uint64) bool {
+func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool {
 	if mask == 0 {
 		return false
 	}
@@ -194,7 +78,7 @@ func (w *Watcher) sendEvent(name, renamedFrom string, mask uint64) bool {
 }
 
 // Returns true if the error was sent, or false if watcher is closed.
-func (w *Watcher) sendError(err error) bool {
+func (w *readDirChangesW) sendError(err error) bool {
 	if err == nil {
 		return true
 	}
@@ -206,8 +90,7 @@ func (w *Watcher) sendError(err error) bool {
 	}
 }
 
-// Close removes all watches and closes the Events channel.
-func (w *Watcher) Close() error {
+func (w *readDirChangesW) Close() error {
 	if w.isClosed() {
 		return nil
 	}
@@ -225,52 +108,9 @@ func (w *Watcher) Close() error {
 	return <-ch
 }
 
-// Add starts monitoring the path for changes.
-//
-// A path can only be watched once; watching it more than once is a no-op and will
-// not return an error. Paths that do not yet exist on the filesystem cannot be
-// watched.
-//
-// A watch will be automatically removed if the watched path is deleted or
-// renamed. The exception is the Windows backend, which doesn't remove the
-// watcher on renames.
-//
-// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
-// filesystems (/proc, /sys, etc.) generally don't work.
-//
-// Returns [ErrClosed] if [Watcher.Close] was called.
-//
-// See [Watcher.AddWith] for a version that allows adding options.
-//
-// # Watching directories
-//
-// All files in a directory are monitored, including new files that are created
-// after the watcher is started. Subdirectories are not watched (i.e. it's
-// non-recursive).
-//
-// # Watching files
-//
-// Watching individual files (rather than directories) is generally not
-// recommended as many programs (especially editors) update files atomically: it
-// will write to a temporary file which is then moved to to destination,
-// overwriting the original (or some variant thereof). The watcher on the
-// original file is now lost, as that no longer exists.
-//
-// The upshot of this is that a power failure or crash won't leave a
-// half-written file.
-//
-// Watch the parent directory and use Event.Name to filter out files you're not
-// interested in. There is an example of this in cmd/fsnotify/file.go.
-func (w *Watcher) Add(name string) error { return w.AddWith(name) }
+func (w *readDirChangesW) Add(name string) error { return w.AddWith(name) }
 
-// AddWith is like [Watcher.Add], but allows adding options. When using Add()
-// the defaults described below are used.
-//
-// Possible options are:
-//
-//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
-//     other platforms. The default is 64K (65536 bytes).
-func (w *Watcher) AddWith(name string, opts ...addOpt) error {
+func (w *readDirChangesW) AddWith(name string, opts ...addOpt) error {
 	if w.isClosed() {
 		return ErrClosed
 	}
@@ -301,15 +141,7 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
 	return <-in.reply
 }
 
-// Remove stops monitoring the path for changes.
-//
-// Directories are always removed non-recursively. For example, if you added
-// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
-//
-// Removing a path that has not yet been added returns [ErrNonExistentWatch].
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) Remove(name string) error {
+func (w *readDirChangesW) Remove(name string) error {
 	if w.isClosed() {
 		return nil
 	}
@@ -330,11 +162,7 @@ func (w *Watcher) Remove(name string) error {
 	return <-in.reply
 }
 
-// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
-// yet removed).
-//
-// Returns nil if [Watcher.Close] was called.
-func (w *Watcher) WatchList() []string {
+func (w *readDirChangesW) WatchList() []string {
 	if w.isClosed() {
 		return nil
 	}
@@ -377,7 +205,7 @@ const (
 	sysFSIGNORED    = 0x8000
 )
 
-func (w *Watcher) newEvent(name string, mask uint32) Event {
+func (w *readDirChangesW) newEvent(name string, mask uint32) Event {
 	e := Event{Name: name}
 	if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
 		e.Op |= Create
@@ -433,7 +261,7 @@ type (
 	watchMap map[uint32]indexMap
 )
 
-func (w *Watcher) wakeupReader() error {
+func (w *readDirChangesW) wakeupReader() error {
 	err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil)
 	if err != nil {
 		return os.NewSyscallError("PostQueuedCompletionStatus", err)
@@ -441,7 +269,7 @@ func (w *Watcher) wakeupReader() error {
 	return nil
 }
 
-func (w *Watcher) getDir(pathname string) (dir string, err error) {
+func (w *readDirChangesW) getDir(pathname string) (dir string, err error) {
 	attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname))
 	if err != nil {
 		return "", os.NewSyscallError("GetFileAttributes", err)
@@ -455,7 +283,7 @@ func (w *Watcher) getDir(pathname string) (dir string, err error) {
 	return
 }
 
-func (w *Watcher) getIno(path string) (ino *inode, err error) {
+func (w *readDirChangesW) getIno(path string) (ino *inode, err error) {
 	h, err := windows.CreateFile(windows.StringToUTF16Ptr(path),
 		windows.FILE_LIST_DIRECTORY,
 		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
@@ -498,7 +326,7 @@ func (m watchMap) set(ino *inode, watch *watch) {
 }
 
 // Must run within the I/O thread.
-func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
+func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) error {
 	pathname, recurse := recursivePath(pathname)
 
 	dir, err := w.getDir(pathname)
@@ -553,7 +381,7 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
 }
 
 // Must run within the I/O thread.
-func (w *Watcher) remWatch(pathname string) error {
+func (w *readDirChangesW) remWatch(pathname string) error {
 	pathname, recurse := recursivePath(pathname)
 
 	dir, err := w.getDir(pathname)
@@ -593,7 +421,7 @@ func (w *Watcher) remWatch(pathname string) error {
 }
 
 // Must run within the I/O thread.
-func (w *Watcher) deleteWatch(watch *watch) {
+func (w *readDirChangesW) deleteWatch(watch *watch) {
 	for name, mask := range watch.names {
 		if mask&provisional == 0 {
 			w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED)
@@ -609,7 +437,7 @@ func (w *Watcher) deleteWatch(watch *watch) {
 }
 
 // Must run within the I/O thread.
-func (w *Watcher) startRead(watch *watch) error {
+func (w *readDirChangesW) startRead(watch *watch) error {
 	err := windows.CancelIo(watch.ino.handle)
 	if err != nil {
 		w.sendError(os.NewSyscallError("CancelIo", err))
@@ -652,7 +480,7 @@ func (w *Watcher) startRead(watch *watch) error {
 // readEvents reads from the I/O completion port, converts the
 // received events into Event objects and sends them via the Events channel.
 // Entry point to the I/O thread.
-func (w *Watcher) readEvents() {
+func (w *readDirChangesW) readEvents() {
 	var (
 		n   uint32
 		key uintptr
@@ -818,7 +646,7 @@ func (w *Watcher) readEvents() {
 	}
 }
 
-func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
+func (w *readDirChangesW) toWindowsFlags(mask uint64) uint32 {
 	var m uint32
 	if mask&sysFSMODIFY != 0 {
 		m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
@@ -829,7 +657,7 @@ func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
 	return m
 }
 
-func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
+func (w *readDirChangesW) toFSnotifyFlags(action uint32) uint64 {
 	switch action {
 	case windows.FILE_ACTION_ADDED:
 		return sysFSCREATE
@@ -845,11 +673,7 @@ func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
 	return 0
 }
 
-// Supports reports if all the listed operations are supported by this platform.
-//
-// Create, Write, Remove, Rename, and Chmod are always supported. It can only
-// return false for an Op starting with Unportable.
-func (w *Watcher) xSupports(op Op) bool {
+func (w *readDirChangesW) xSupports(op Op) bool {
 	if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
 		op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
 		return false
diff --git a/fsnotify.go b/fsnotify.go
index e80e82e..980f754 100644
--- a/fsnotify.go
+++ b/fsnotify.go
@@ -32,6 +32,117 @@ import (
 	"strings"
 )
 
+// Watcher watches a set of paths, delivering events on a channel.
+//
+// A watcher should not be copied (e.g. pass it by pointer, rather than by
+// value).
+//
+// # Linux notes
+//
+// When a file is removed a Remove event won't be emitted until all file
+// descriptors are closed, and deletes will always emit a Chmod. For example:
+//
+//	fp := os.Open("file")
+//	os.Remove("file")        // Triggers Chmod
+//	fp.Close()               // Triggers Remove
+//
+// This is the event that inotify sends, so not much can be changed about this.
+//
+// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
+// for the number of watches per user, and fs.inotify.max_user_instances
+// specifies the maximum number of inotify instances per user. Every Watcher you
+// create is an "instance", and every path you add is a "watch".
+//
+// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
+// /proc/sys/fs/inotify/max_user_instances
+//
+// To increase them you can use sysctl or write the value to the /proc file:
+//
+//	# Default values on Linux 5.18
+//	sysctl fs.inotify.max_user_watches=124983
+//	sysctl fs.inotify.max_user_instances=128
+//
+// To make the changes persist on reboot edit /etc/sysctl.conf or
+// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
+// your distro's documentation):
+//
+//	fs.inotify.max_user_watches=124983
+//	fs.inotify.max_user_instances=128
+//
+// Reaching the limit will result in a "no space left on device" or "too many open
+// files" error.
+//
+// # kqueue notes (macOS, BSD)
+//
+// kqueue requires opening a file descriptor for every file that's being watched;
+// so if you're watching a directory with five files then that's six file
+// descriptors. You will run in to your system's "max open files" limit faster on
+// these platforms.
+//
+// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
+// control the maximum number of open files, as well as /etc/login.conf on BSD
+// systems.
+//
+// # Windows notes
+//
+// Paths can be added as "C:\\path\\to\\dir", but forward slashes
+// ("C:/path/to/dir") will also work.
+//
+// When a watched directory is removed it will always send an event for the
+// directory itself, but may not send events for all files in that directory.
+// Sometimes it will send events for all files, sometimes it will send no
+// events, and often only for some files.
+//
+// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
+// value that is guaranteed to work with SMB filesystems. If you have many
+// events in quick succession this may not be enough, and you will have to use
+// [WithBufferSize] to increase the value.
+type Watcher struct {
+	b backend
+
+	// Events sends the filesystem change events.
+	//
+	// fsnotify can send the following events; a "path" here can refer to a
+	// file, directory, symbolic link, or special file like a FIFO.
+	//
+	//   fsnotify.Create    A new path was created; this may be followed by one
+	//                      or more Write events if data also gets written to a
+	//                      file.
+	//
+	//   fsnotify.Remove    A path was removed.
+	//
+	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
+	//                      old path as Event.Name, and a Create event will be
+	//                      sent with the new name. Renames are only sent for
+	//                      paths that are currently watched; e.g. moving an
+	//                      unmonitored file into a monitored directory will
+	//                      show up as just a Create. Similarly, renaming a file
+	//                      to outside a monitored directory will show up as
+	//                      only a Rename.
+	//
+	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
+	//                      also trigger a Write. A single "write action"
+	//                      initiated by the user may show up as one or multiple
+	//                      writes, depending on when the system syncs things to
+	//                      disk. For example when compiling a large Go program
+	//                      you may get hundreds of Write events, and you may
+	//                      want to wait until you've stopped receiving them
+	//                      (see the dedup example in cmd/fsnotify).
+	//
+	//                      Some systems may send Write event for directories
+	//                      when the directory content changes.
+	//
+	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
+	//                      when a file is removed (or more accurately, when a
+	//                      link to an inode is removed). On kqueue it's sent
+	//                      when a file is truncated. On Windows it's never
+	//                      sent.
+	Events chan Event
+
+	// Errors sends any errors.
+	Errors chan error
+}
+
 // Event represents a file system notification.
 type Event struct {
 	// Path to the file or directory.
@@ -136,6 +247,105 @@ var (
 	xErrUnsupported = errors.New("fsnotify: not supported with this backend")
 )
 
+// NewWatcher creates a new Watcher.
+func NewWatcher() (*Watcher, error) {
+	ev, errs := make(chan Event), make(chan error)
+	b, err := newBackend(ev, errs)
+	if err != nil {
+		return nil, err
+	}
+	return &Watcher{b: b, Events: ev, Errors: errs}, nil
+}
+
+// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
+// channel.
+//
+// The main use case for this is situations with a very large number of events
+// where the kernel buffer size can't be increased (e.g. due to lack of
+// permissions). An unbuffered Watcher will perform better for almost all use
+// cases, and whenever possible you will be better off increasing the kernel
+// buffers instead of adding a large userspace buffer.
+func NewBufferedWatcher(sz uint) (*Watcher, error) {
+	ev, errs := make(chan Event), make(chan error)
+	b, err := newBufferedBackend(sz, ev, errs)
+	if err != nil {
+		return nil, err
+	}
+	return &Watcher{b: b, Events: ev, Errors: errs}, nil
+}
+
+// Add starts monitoring the path for changes.
+//
+// A path can only be watched once; watching it more than once is a no-op and will
+// not return an error. Paths that do not yet exist on the filesystem cannot be
+// watched.
+//
+// A watch will be automatically removed if the watched path is deleted or
+// renamed. The exception is the Windows backend, which doesn't remove the
+// watcher on renames.
+//
+// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
+// filesystems (/proc, /sys, etc.) generally don't work.
+//
+// Returns [ErrClosed] if [Watcher.Close] was called.
+//
+// See [Watcher.AddWith] for a version that allows adding options.
+//
+// # Watching directories
+//
+// All files in a directory are monitored, including new files that are created
+// after the watcher is started. Subdirectories are not watched (i.e. it's
+// non-recursive).
+//
+// # Watching files
+//
+// Watching individual files (rather than directories) is generally not
+// recommended as many programs (especially editors) update files atomically: it
+// will write to a temporary file which is then moved to to destination,
+// overwriting the original (or some variant thereof). The watcher on the
+// original file is now lost, as that no longer exists.
+//
+// The upshot of this is that a power failure or crash won't leave a
+// half-written file.
+//
+// Watch the parent directory and use Event.Name to filter out files you're not
+// interested in. There is an example of this in cmd/fsnotify/file.go.
+func (w *Watcher) Add(path string) error { return w.b.Add(path) }
+
+// AddWith is like [Watcher.Add], but allows adding options. When using Add()
+// the defaults described below are used.
+//
+// Possible options are:
+//
+//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
+//     other platforms. The default is 64K (65536 bytes).
+func (w *Watcher) AddWith(path string, opts ...addOpt) error { return w.b.AddWith(path, opts...) }
+
+// Remove stops monitoring the path for changes.
+//
+// Directories are always removed non-recursively. For example, if you added
+// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
+//
+// Removing a path that has not yet been added returns [ErrNonExistentWatch].
+//
+// Returns nil if [Watcher.Close] was called.
+func (w *Watcher) Remove(path string) error { return w.b.Remove(path) }
+
+// Close removes all watches and closes the Events channel.
+func (w *Watcher) Close() error { return w.b.Close() }
+
+// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
+// yet removed).
+//
+// Returns nil if [Watcher.Close] was called.
+func (w *Watcher) WatchList() []string { return w.b.WatchList() }
+
+// Supports reports if all the listed operations are supported by this platform.
+//
+// Create, Write, Remove, Rename, and Chmod are always supported. It can only
+// return false for an Op starting with Unportable.
+func (w *Watcher) xSupports(op Op) bool { return w.b.xSupports(op) }
+
 func (o Op) String() string {
 	var b strings.Builder
 	if o.Has(Create) {
@@ -186,6 +396,14 @@ func (e Event) String() string {
 }
 
 type (
+	backend interface {
+		Add(string) error
+		AddWith(string, ...addOpt) error
+		Remove(string) error
+		WatchList() []string
+		Close() error
+		xSupports(Op) bool
+	}
 	addOpt   func(opt *withOpts)
 	withOpts struct {
 		bufsize    int
diff --git a/mkdoc.zsh b/mkdoc.zsh
deleted file mode 100755
index 6e630ec..0000000
--- a/mkdoc.zsh
+++ /dev/null
@@ -1,262 +0,0 @@
-#!/usr/bin/env zsh
-[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
-setopt err_exit no_unset pipefail extended_glob
-
-# Simple script to update the godoc comments on all watchers so you don't need
-# to update the same comment 5 times.
-
-watcher=$(<<EOF
-// Watcher watches a set of paths, delivering events on a channel.
-//
-// A watcher should not be copied (e.g. pass it by pointer, rather than by
-// value).
-//
-// # Linux notes
-//
-// When a file is removed a Remove event won't be emitted until all file
-// descriptors are closed, and deletes will always emit a Chmod. For example:
-//
-//	fp := os.Open("file")
-//	os.Remove("file")        // Triggers Chmod
-//	fp.Close()               // Triggers Remove
-//
-// This is the event that inotify sends, so not much can be changed about this.
-//
-// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
-// for the number of watches per user, and fs.inotify.max_user_instances
-// specifies the maximum number of inotify instances per user. Every Watcher you
-// create is an "instance", and every path you add is a "watch".
-//
-// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
-// /proc/sys/fs/inotify/max_user_instances
-//
-// To increase them you can use sysctl or write the value to the /proc file:
-//
-//	# Default values on Linux 5.18
-//	sysctl fs.inotify.max_user_watches=124983
-//	sysctl fs.inotify.max_user_instances=128
-//
-// To make the changes persist on reboot edit /etc/sysctl.conf or
-// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
-// your distro's documentation):
-//
-//	fs.inotify.max_user_watches=124983
-//	fs.inotify.max_user_instances=128
-//
-// Reaching the limit will result in a "no space left on device" or "too many open
-// files" error.
-//
-// # kqueue notes (macOS, BSD)
-//
-// kqueue requires opening a file descriptor for every file that's being watched;
-// so if you're watching a directory with five files then that's six file
-// descriptors. You will run in to your system's "max open files" limit faster on
-// these platforms.
-//
-// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
-// control the maximum number of open files, as well as /etc/login.conf on BSD
-// systems.
-//
-// # Windows notes
-//
-// Paths can be added as "C:\\path\\to\\dir", but forward slashes
-// ("C:/path/to/dir") will also work.
-//
-// When a watched directory is removed it will always send an event for the
-// directory itself, but may not send events for all files in that directory.
-// Sometimes it will send events for all times, sometimes it will send no
-// events, and often only for some files.
-//
-// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
-// value that is guaranteed to work with SMB filesystems. If you have many
-// events in quick succession this may not be enough, and you will have to use
-// [WithBufferSize] to increase the value.
-EOF
-)
-
-new=$(<<EOF
-// NewWatcher creates a new Watcher.
-EOF
-)
-
-newbuffered=$(<<EOF
-// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
-// channel.
-//
-// The main use case for this is situations with a very large number of events
-// where the kernel buffer size can't be increased (e.g. due to lack of
-// permissions). An unbuffered Watcher will perform better for almost all use
-// cases, and whenever possible you will be better off increasing the kernel
-// buffers instead of adding a large userspace buffer.
-EOF
-)
-
-add=$(<<EOF
-// Add starts monitoring the path for changes.
-//
-// A path can only be watched once; watching it more than once is a no-op and will
-// not return an error. Paths that do not yet exist on the filesystem cannot be
-// watched.
-//
-// A watch will be automatically removed if the watched path is deleted or
-// renamed. The exception is the Windows backend, which doesn't remove the
-// watcher on renames.
-//
-// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
-// filesystems (/proc, /sys, etc.) generally don't work.
-//
-// Returns [ErrClosed] if [Watcher.Close] was called.
-//
-// See [Watcher.AddWith] for a version that allows adding options.
-//
-// # Watching directories
-//
-// All files in a directory are monitored, including new files that are created
-// after the watcher is started. Subdirectories are not watched (i.e. it's
-// non-recursive).
-//
-// # Watching files
-//
-// Watching individual files (rather than directories) is generally not
-// recommended as many programs (especially editors) update files atomically: it
-// will write to a temporary file which is then moved to to destination,
-// overwriting the original (or some variant thereof). The watcher on the
-// original file is now lost, as that no longer exists.
-//
-// The upshot of this is that a power failure or crash won't leave a
-// half-written file.
-//
-// Watch the parent directory and use Event.Name to filter out files you're not
-// interested in. There is an example of this in cmd/fsnotify/file.go.
-EOF
-)
-
-addwith=$(<<EOF
-// AddWith is like [Watcher.Add], but allows adding options. When using Add()
-// the defaults described below are used.
-//
-// Possible options are:
-//
-//   - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
-//     other platforms. The default is 64K (65536 bytes).
-EOF
-)
-
-remove=$(<<EOF
-// Remove stops monitoring the path for changes.
-//
-// Directories are always removed non-recursively. For example, if you added
-// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
-//
-// Removing a path that has not yet been added returns [ErrNonExistentWatch].
-//
-// Returns nil if [Watcher.Close] was called.
-EOF
-)
-
-close=$(<<EOF
-// Close removes all watches and closes the Events channel.
-EOF
-)
-
-watchlist=$(<<EOF
-// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
-// yet removed).
-//
-// Returns nil if [Watcher.Close] was called.
-EOF
-)
-
-supports=$(<<EOF
-// Supports reports if all the listed operations are supported by this platform.
-//
-// Create, Write, Remove, Rename, and Chmod are always supported. It can only
-// return false for an Op starting with Unportable.
-EOF
-)
-
-events=$(<<EOF
-	// Events sends the filesystem change events.
-	//
-	// fsnotify can send the following events; a "path" here can refer to a
-	// file, directory, symbolic link, or special file like a FIFO.
-	//
-	//   fsnotify.Create    A new path was created; this may be followed by one
-	//                      or more Write events if data also gets written to a
-	//                      file.
-	//
-	//   fsnotify.Remove    A path was removed.
-	//
-	//   fsnotify.Rename    A path was renamed. A rename is always sent with the
-	//                      old path as Event.Name, and a Create event will be
-	//                      sent with the new name. Renames are only sent for
-	//                      paths that are currently watched; e.g. moving an
-	//                      unmonitored file into a monitored directory will
-	//                      show up as just a Create. Similarly, renaming a file
-	//                      to outside a monitored directory will show up as
-	//                      only a Rename.
-	//
-	//   fsnotify.Write     A file or named pipe was written to. A Truncate will
-	//                      also trigger a Write. A single "write action"
-	//                      initiated by the user may show up as one or multiple
-	//                      writes, depending on when the system syncs things to
-	//                      disk. For example when compiling a large Go program
-	//                      you may get hundreds of Write events, and you may
-	//                      want to wait until you've stopped receiving them
-	//                      (see the dedup example in cmd/fsnotify).
-	//
-	//                      Some systems may send Write event for directories
-	//                      when the directory content changes.
-	//
-	//   fsnotify.Chmod     Attributes were changed. On Linux this is also sent
-	//                      when a file is removed (or more accurately, when a
-	//                      link to an inode is removed). On kqueue it's sent
-	//                      when a file is truncated. On Windows it's never
-	//                      sent.
-EOF
-)
-
-errors=$(<<EOF
-	// Errors sends any errors.
-EOF
-)
-
-set-cmt() {
-	local pat=$1
-	local cmt=$2
-
-	IFS=$'\n' local files=($(grep -n $pat backend_*~*_test.go))
-	for f in $files; do
-		IFS=':' local fields=($=f)
-		local file=$fields[1]
-		local end=$(( $fields[2] - 1 ))
-
-		# Find start of comment.
-		local start=0
-		IFS=$'\n' local lines=($(head -n$end $file))
-		for (( i = 1; i <= $#lines; i++ )); do
-			local line=$lines[-$i]
-			if ! grep -q '^[[:space:]]*//' <<<$line; then
-				start=$(( end - (i - 2) ))
-				break
-			fi
-		done
-
-		head -n $(( start - 1 )) $file  >/tmp/x
-		print -r -- $cmt                >>/tmp/x
-		tail -n+$(( end + 1 ))   $file  >>/tmp/x
-		mv /tmp/x $file
-	done
-}
-
-set-cmt '^type Watcher struct '             $watcher
-set-cmt '^func NewWatcher('                 $new
-set-cmt '^func NewBufferedWatcher('         $newbuffered
-set-cmt '^func (w \*Watcher) Add('          $add
-set-cmt '^func (w \*Watcher) AddWith('      $addwith
-set-cmt '^func (w \*Watcher) Remove('       $remove
-set-cmt '^func (w \*Watcher) Close('        $close
-set-cmt '^func (w \*Watcher) WatchList('    $watchlist
-set-cmt '^func (w \*Watcher) Supports('     $supports
-set-cmt '^[[:space:]]*Events *chan Event$'  $events
-set-cmt '^[[:space:]]*Errors *chan error$'  $errors


Test patch:
diff --git a/backend_fen_test.go b/backend_fen_test.go
index 3a930ca..06319c0 100644
--- a/backend_fen_test.go
+++ b/backend_fen_test.go
@@ -23,21 +23,21 @@ func TestRemoveState(t *testing.T) {
 
 	check := func(wantDirs, wantFiles int) {
 		t.Helper()
-		if len(w.watches) != wantFiles {
+		if len(w.b.(*fen).watches) != wantFiles {
 			var d []string
-			for k, v := range w.watches {
+			for k, v := range w.b.(*fen).watches {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v",
-				len(w.watches), wantFiles, strings.Join(d, "\n"))
+				len(w.b.(*fen).watches), wantFiles, strings.Join(d, "\n"))
 		}
-		if len(w.dirs) != wantDirs {
+		if len(w.b.(*fen).dirs) != wantDirs {
 			var d []string
-			for k, v := range w.dirs {
+			for k, v := range w.b.(*fen).dirs {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.dirs (have %d, want %d):\n%v",
-				len(w.dirs), wantDirs, strings.Join(d, "\n"))
+				len(w.b.(*fen).dirs), wantDirs, strings.Join(d, "\n"))
 		}
 	}
 
diff --git a/backend_inotify_test.go b/backend_inotify_test.go
index 40b9ef0..43db521 100644
--- a/backend_inotify_test.go
+++ b/backend_inotify_test.go
@@ -27,8 +27,8 @@ func TestRemoveState(t *testing.T) {
 
 	check := func(want int) {
 		t.Helper()
-		if w.watches.len() != want {
-			t.Error(w.watches)
+		if w.b.(*inotify).watches.len() != want {
+			t.Error(w.b.(*inotify).watches)
 		}
 	}
 
diff --git a/backend_kqueue_test.go b/backend_kqueue_test.go
index de8eac1..2f055ca 100644
--- a/backend_kqueue_test.go
+++ b/backend_kqueue_test.go
@@ -18,35 +18,36 @@ func TestRemoveState(t *testing.T) {
 	touch(t, file)
 
 	w := newWatcher(t, tmp)
+	kq := w.b.(*kqueue)
 	addWatch(t, w, tmp)
 	addWatch(t, w, file)
 
 	check := func(wantUser, wantTotal int) {
 		t.Helper()
 
-		if len(w.watches.path) != wantTotal {
+		if len(kq.watches.path) != wantTotal {
 			var d []string
-			for k, v := range w.watches.path {
+			for k, v := range kq.watches.path {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.watches.path (have %d, want %d):\n%v",
-				len(w.watches.path), wantTotal, strings.Join(d, "\n"))
+				len(kq.watches.path), wantTotal, strings.Join(d, "\n"))
 		}
-		if len(w.watches.wd) != wantTotal {
+		if len(kq.watches.wd) != wantTotal {
 			var d []string
-			for k, v := range w.watches.wd {
+			for k, v := range kq.watches.wd {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.watches.wd (have %d, want %d):\n%v",
-				len(w.watches.wd), wantTotal, strings.Join(d, "\n"))
+				len(kq.watches.wd), wantTotal, strings.Join(d, "\n"))
 		}
-		if len(w.watches.byUser) != wantUser {
+		if len(kq.watches.byUser) != wantUser {
 			var d []string
-			for k, v := range w.watches.byUser {
+			for k, v := range kq.watches.byUser {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.watches.byUser (have %d, want %d):\n%v",
-				len(w.watches.byUser), wantUser, strings.Join(d, "\n"))
+				len(kq.watches.byUser), wantUser, strings.Join(d, "\n"))
 		}
 	}
 
@@ -66,22 +67,23 @@ func TestRemoveState(t *testing.T) {
 	// of files watches. Just make sure they're 0 after everything is removed.
 	{
 		want := 0
-		if len(w.watches.byDir) != want {
+		if len(kq.watches.byDir) != want {
 			var d []string
-			for k, v := range w.watches.byDir {
+			for k, v := range kq.watches.byDir {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.watches.byDir (have %d, want %d):\n%v",
-				len(w.watches.byDir), want, strings.Join(d, "\n"))
+				len(kq.watches.byDir), want, strings.Join(d, "\n"))
 		}
 
-		if len(w.watches.seen) != want {
+		if len(kq.watches.seen) != want {
 			var d []string
-			for k, v := range w.watches.seen {
+			for k, v := range kq.watches.seen {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.watches.seen (have %d, want %d):\n%v",
-				len(w.watches.seen), want, strings.Join(d, "\n"))
+				len(kq.watches.seen), want, strings.Join(d, "\n"))
+			return
 		}
 	}
 }
diff --git a/backend_windows_test.go b/backend_windows_test.go
index 8a488c2..d2483b3 100644
--- a/backend_windows_test.go
+++ b/backend_windows_test.go
@@ -26,13 +26,13 @@ func TestRemoveState(t *testing.T) {
 
 	check := func(want int) {
 		t.Helper()
-		if len(w.watches) != want {
+		if len(w.b.(*readDirChangesW).watches) != want {
 			var d []string
-			for k, v := range w.watches {
+			for k, v := range w.b.(*readDirChangesW).watches {
 				d = append(d, fmt.Sprintf("%#v = %#v", k, v))
 			}
 			t.Errorf("unexpected number of entries in w.watches (have %d, want %d):\n%v",
-				len(w.watches), want, strings.Join(d, "\n"))
+				len(w.b.(*readDirChangesW).watches), want, strings.Join(d, "\n"))
 		}
 	}
 
@@ -61,7 +61,7 @@ func TestWindowsRemWatch(t *testing.T) {
 	if err := w.Remove(tmp); err != nil {
 		t.Fatalf("Could not remove the watch: %v\n", err)
 	}
-	if err := w.remWatch(tmp); err == nil {
+	if err := w.b.(*readDirChangesW).remWatch(tmp); err == nil {
 		t.Fatal("Should be fail with closed handle\n")
 	}
 }