# FSEvent - Cross-Platform File System Monitoring

## Overview

FSEvent provides file system monitoring across Linux (fanotify), macOS (FSEvents), and Windows. Features configuration testing, working flag detection, depth control, and exclude path filtering.

## Programmer's Guide

### Main Module

```lua
local fsevent = require "fsevent"
```

### Core API Functions

#### `fsevent.init(config) -> string`

Initialize FSEvent with optional configuration.

**Parameters:**

- `config.maxDirectoryCount` (number, default: 1048576) - Maximum directories to monitor
- `config.latency` (number, default: 0.1) - Notification latency in seconds
- `config.followSymlink` (number, default: 1) - Symlink following level (0-2)
- `config.maxFiles` (number, default: 65535) - Maximum files for Windows
- `config.eventFlag` (table) - Event flag configuration

**Returns:** `"OK"` on success, error string on failure

**Example:**

```lua
local result = fsevent.init({
  latency = 0.05,
  maxDirectoryCount = 1024
})
```

#### `fsevent.setMonitorPath(pathArray) -> string`

Set paths to monitor with depth control.

**Parameters:**

- `pathArray` - Array of `{path, depth}` objects
  - `path` (string) - Directory path to monitor
  - `depth` (number) - Monitor depth: -1=unlimited, 0=root only, 2=2 levels

**Returns:** `"ok: N paths configured"` on success, error string on failure

**Example:**

```lua
fsevent.setMonitorPath({
  {path = "/project/src", depth = 3},
  {path = "/project/test", depth = 1},
  {path = "/project/docs", depth = 0}
})
```

#### `fsevent.getMonitorPath() -> table`

Get current monitoring paths.

**Returns:** Array of `{path, depth}` objects

**Example:**

```lua
local paths = fsevent.getMonitorPath()
for i, p in ipairs(paths) do
  print(p.path, "depth:", p.depth)
end
```

#### `fsevent.setExcludePath(pathArray) -> string`

Set exclude path patterns with wildcard support.

**Parameters:**

- `pathArray` - Array of pattern strings
  - `"*.log"` - Files ending with .log (start wildcard)
  - `".git*"` - Files/directories starting with .git (end wildcard)
  - `"*.config*"` - Files containing .config (both wildcards)
  - `"node_modules"` - Exact match

**Returns:** `"ok: exclude patterns configured"` on success, error string on failure

**Example:**

```lua
fsevent.setExcludePath({
  "/tmp*", ".git*", "*.DS_Store", "node_modules"
})
```

#### `fsevent.listen(maxEvents, timeout, latency) -> count, events`

Start monitoring and return events.

**Parameters:**

- `maxEvents` (number, default: infinity) - Maximum events before returning
- `timeout` (number, default: 3600) - Timeout in seconds
- `latency` (number, default: 0.01) - Event accumulation window in seconds

**Returns:**

- `count` (number) - Number of events received
- `events` (array) - Array of `{path, type, event}` objects

**Example:**

```lua
local count, events = fsevent.listen(100)
for i, e in ipairs(events) do
  print(e.path, e.type)
end
```

#### `fsevent.stop() -> string`

Stop monitoring.

**Returns:** `"ok: monitoring stopped"` on success

**Example:**

```lua
fsevent.stop()
```

#### `fsevent.reset() -> string`

Reset monitoring state.

**Returns:** `"ok: fsevent reset"` on success

**Example:**

```lua
fsevent.reset()
```

#### `fsevent.setDebug(level) -> void`

Set debug output level.

**Parameters:**

- `level` (number) - 0=off, 1=basic, 2=verbose

**Example:**

```lua
fsevent.setDebug(1)
```

#### `fsevent.createEventFlagConfig() -> table`

Create an event flag configuration structure for custom event filtering.

**Returns:** Event flag configuration table with all fields set to -1 (platform default)

**Example:**

```lua
local config = fsevent.createEventFlagConfig()
config.markFlag = fsevent.mark.SAFE_MODE
config.fileModify = 1
config.fileCreate = 1
config.fileDelete = 1

fsevent.init({eventFlag = config})
```

### Event Callback System

#### Callback Function Signature

```lua
function callback(eventCount, eventArray)
  -- Process events
  -- Return: 0=continue, 1=stop, 2=has pending events
end
```

#### Callback Return Values

**Return 0** - Continue monitoring, no pending events

```lua
return 0
```

**Return 1** - Stop monitoring immediately

```lua
return 1
```

**Return 2** - Has pending events, trigger short timeout for flush

When callback returns 2, the platform sends a synthetic timeout event `{timeout=true}` to flush accumulated events. This enables responsive event handling for rapid file changes.

```lua
if hasPendingEvents then
  return 2  -- Signal need for immediate flush
end
return 0
```

#### Event Object Structure

Each event in `eventArray` contains:

```lua
{
  evId = number,              -- Event ID
  path = string,              -- File path
  modify = number,            -- 1 if modified
  remove = number,            -- 1 if removed
  rename = number,            -- 1 if renamed
  create = number,            -- 1 if created
  access = number,            -- 1 if accessed
  open = number,              -- 1 if opened
  close = number,             -- 1 if closed
  execute = number,           -- 1 if executed
  isFile = number,            -- 1 if file
  isDir = number,             -- 1 if directory
  inodeMetaMod = number,      -- 1 if inode metadata changed
  xattrMod = number,          -- 1 if extended attributes changed
  finderInfoMod = number,     -- 1 if Finder info changed (macOS)
  changeOwner = number,       -- 1 if owner changed
  clone = number,             -- 1 if cloned
  metadataChange = number,    -- 1 if metadata changed
  onDirectory = number,       -- 1 if directory event
  eventOnChild = number,      -- 1 if child event
  rawFlag = number,           -- Raw platform flags
  fileSize = number,          -- File size in bytes
  lastModifiedTime = number,  -- Unix timestamp
  lastAccessTime = number,    -- Unix timestamp
  fileAttributes = number    -- File attributes/mode
}
```

#### Synthetic Timeout Event

When callback returns 2, platform sends:

```lua
{timeout = true}
```

Handle this event to flush accumulated events:

```lua
if event.timeout then
  flushAllPendingEvents()
end
```

### Configuration Presets

#### `fsevent.preset.safe`

Maximum compatibility, safe mode.

```lua
fsevent.init(fsevent.preset.safe)
```

#### `fsevent.preset.full`

All working events (recommended).

```lua
fsevent.init(fsevent.preset.full)
```

#### `fsevent.preset.minimal`

Performance focused.

```lua
fsevent.init(fsevent.preset.minimal)
```

#### `fsevent.preset.development`

All events for debugging.

```lua
fsevent.init(fsevent.preset.development)
```

#### `fsevent.preset.security`

No symlink following, basic events.

```lua
fsevent.init(fsevent.preset.security)
```

### Custom Event Flags

#### Mark Flags

```lua
fsevent.mark.ADD = 0x00000001
fsevent.mark.ONLYDIR = 0x00000008
fsevent.mark.MOUNT = 0x00000010
fsevent.mark.DONT_FOLLOW = 0x00000020
fsevent.mark.DIRECTORY_ONLY = 0x00000009
fsevent.mark.SAFE_MODE = 0x00000029
```

#### Event Flags

```lua
fsevent.event.FILE_OPEN = 0x00000001
fsevent.event.FILE_MODIFY = 0x00000002
fsevent.event.FILE_ACCESS = 0x00000004
fsevent.event.FILE_CLOSE_WRITE = 0x00000008
fsevent.event.FILE_CLOSE_NOWRITE = 0x00000010
fsevent.event.FILE_OPEN_EXEC = 0x00000020
fsevent.event.FILE_CREATE = 0x00000040
fsevent.event.FILE_DELETE = 0x00000080
fsevent.event.FILE_DELETE_SELF = 0x00000100
fsevent.event.FILE_MOVE_SELF = 0x00000200
fsevent.event.ON_DIRECTORY = 0x00000400
fsevent.event.METADATA_CHANGE = 0x00000800
fsevent.event.FILE_RENAME = 0x00001000
fsevent.event.EVENT_ON_CHILD = 0x00002000
```

### Complete Example

```lua
local fsevent = require "fsevent"

-- Initialize with preset
fsevent.init(fsevent.preset.full)

-- Set debug level (0=off, 1=basic, 2=verbose)
fsevent.setDebug(0)

-- Set paths to monitor
fsevent.setMonitorPath({
  {path = "/project/src", depth = -1},
  {path = "/project/test", depth = 2}
})

-- Listen loop
local timeout = 60 -- seconds
local retNum, fileArr
repeat
  retNum, fileArr = fsevent.listen(1, timeout)  -- max 1 event per call
  if retNum and retNum > 0 then
    for i, e in ipairs(fileArr) do
      print(e.path, "-", e.type)
    end
  elseif retNum == 0 then
    print("Timeout, continuing...")
  elseif retNum == -1 then
    print("Stop signal received")
    break
  elseif retNum == -2 then
    print("Error received")
    break
  end
until false

fsevent.reset()
```

## Path Configuration

### Depth Control

```bash
# Depth specifications
/path/to/monitor:2     # 2 levels deep
/path/to/monitor:0     # Main directory only
/path/to/monitor:-1    # Unlimited depth (default)
```

### Lua Path Setup

```lua
-- Set monitoring paths with depth
fsevent.setMonitorPath({
  {path = "/project/src", depth = 3},
  {path = "/project/test", depth = 1},
  {path = "/project/docs", depth = 0}
})
```

## Exclude Path System

### Platform-Specific Behavior

**Linux (fanotify)**: Excluded paths are never added to the watch list for maximum efficiency.

**macOS/Windows**: All paths are watched, and events from excluded paths are filtered out before callbacks.

### Default Excludes

- `.*` - Hidden files/directories
- `node_modules` - Node.js dependencies
- `build` - Build directories

### Custom Excludes

```lua
-- Set multiple exclude patterns at once
fsevent.setExcludePath({
  "tmp",           -- Exact path match
  "logs",          -- Exact path match
  "*.tmp",         -- Files ending with .tmp (start wildcard)
  ".git*",         -- Files/directories starting with .git (end wildcard)
  "*.config*",     -- Files containing .config (both wildcards)
  "node_modules"   -- Exact path match
})
```

### Wildcard Pattern Matching

**Supported Patterns:**

- `*.log` - Files ending with .log (start wildcard)
- `.git*` - Files/directories starting with .git (end wildcard)
- `*.config*` - Files containing .config (both wildcards)
- `*` - Matches everything
- Exact path matches (e.g., "node_modules")

**Not Supported:**

- `?` - Single-character wildcard

**How It Works:**

- Patterns match against filename component, not full path
- Start wildcards (`*suffix`) match filename ending
- End wildcards (`prefix*`) match filename beginning
- Both wildcards (`*middle*`) match filename containing pattern
- Also checks if pattern appears anywhere in full path
