Skip to content

Commit

Permalink
Merge pull request #4 from Makaze/on-changes
Browse files Browse the repository at this point in the history
Feature: Watch for file changes
  • Loading branch information
Makaze authored Apr 17, 2024
2 parents e88b353 + f7360f8 commit 27aa411
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 9 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ started Neovim.
- [x] Scrollable output
- [x] Pause watching when in the background
- [x] Option to open in a configurable split window
- [x] Option to watch for file changes

#### Planned:
- [ ] ANSI color support
Expand All @@ -53,7 +54,7 @@ Using [lazy.nvim](https://github.com/nvim-telescope/telescope.nvim):
```lua
{
"Makaze/watch.nvim",
cmd = { "WatchStart", "WatchStop" },
cmd = { "WatchStart", "WatchStop", "WatchFile" },
}
```

Expand Down Expand Up @@ -95,7 +96,7 @@ watch.setup({

# Example Usage

You can use the Lua API or call the commands from the commandline. To watch the command `tree -cdC` every 500 milliseconds:
You can use the Lua API or call the commands from the commandline. To watch the command `tree -cdC` every 500 milliseconds and to watch the file `error.log` for changes:

### Lua API

Expand All @@ -115,6 +116,15 @@ watch.stop({ file = "tree -cdC" }) -- Stop watching `tree -cdC`
watch.stop() -- Stop all watchers
```

##### Watch a File for Changes

```lua
local watch = require("watch")
-- Use `%s` inside the command to insert the absolute path of the current file.
watch.start("cat %s", 3000, nil, "errog.log") -- Specify 3000 ms refresh
watch.start("cat %s", nil, nil, "errog.log") -- Default to 1000 ms refresh
```

### Ex Commands

##### Start
Expand All @@ -131,6 +141,14 @@ watch.stop() -- Stop all watchers
:WatchStop " Stop all watchers
```

##### Watch a File for Changes

```vim
" With error.log open and focused
:WatchFile cat 3000 " Specify 3000 ms refresh on the currently open file
:WatchFile cat " Default to 1000 ms refresh on the currently open file
```

# Documentation

For examples and technical documentation about commands and the Lua API see `:help watch`.
25 changes: 23 additions & 2 deletions doc/watch.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Author: Makaze <[email protected]>
License: GPLv3.0
Version: 0.2.0
Version: 0.3.0

================================================================================
INTRODUCTION *watch*
Expand Down Expand Up @@ -35,6 +35,7 @@ Features: ~
[x] Scrollable output
[x] Pause watching when in the background
[x] Option to open in a configurable split window
[x] Option to watch for file changes

Planned: ~
[ ] ANSI color support
Expand Down Expand Up @@ -67,6 +68,21 @@ COMMANDS *watch-commands*
Parameters: ~
{command...} (string) Shell command to stop watching.

:WatchFile {command...} {refresh_rate*} *WatchFile*
Starts a new watcher for the currently open file. Behaves like |WatchStart|,
but only runs the command if the file has been modified.

`NOTE:` Refresh rate values lower than 1000 will be increased to 1000 ms.

Parameters: ~
{command...} (string) Shell command to watch. Use `%s` inside the
command to insert the absolute path of the
current file.
{refresh_rate} (integer) Time between refreshes in milliseconds. Will
automically increase to a minimum of 1000.
Defaults to 1000.


================================================================================
CONFIGURATION *watch-config*

Expand Down Expand Up @@ -118,7 +134,7 @@ watch.setup({opts*}) *watch.setup()*
delete the buffer when calling |watch.stop()|. Default
false.

watch.start({command}, {refresh_rate*}, {bufnr*}) *watch.start()*
watch.start({command}, {refresh_rate*}, {bufnr*}, {file*}) *watch.start()*
Starts continually reloading a buffer's contents with a shell command. If
the command is aleady being watched, opens that buffer in the current
window.
Expand All @@ -129,6 +145,11 @@ watch.start({command}, {refresh_rate*}, {bufnr*}) *watch.start()*
milliseconds. Defaults to `500`.
{bufnr} (integer) (optional) The buffer number to load to.
Defaults to a new buffer.
{file} (string) (optional) The path of a file to
watch. If given, the command will be run
when the file is modified on the disk,
checking at an interval of {refresh_rate}.
Defaults to no file (timer only).

watch.stop({event*}) *watch.stop()*
Stops watching the specified command and detaches from the buffer. If no
Expand Down
52 changes: 48 additions & 4 deletions lua/watch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
--- @field refresh_rate integer The refresh rate for the watcher in milliseconds.
--- @field bufnr integer The buffer number attached to the watcher.
--- @field timer function The timer object attached to the watcher.
--- @field file string|nil The filename to watch (if applicable).
--- @field last_updated integer The time since the file was last checked. Used when watching files.

local Watch = {}

Expand Down Expand Up @@ -43,6 +45,21 @@ local function get_buf_by_name(name)
end)
end

--- Get the time a file was last updated, or `nil` if <= the result of last check.
---
--- @param path string The absolute file path.
--- @param last_check integer The unix timestamp to check against. Defaults to `0`.
--- @return integer|nil time
local function file_updated(path, last_check)
last_check = last_check or 0
local stat = uv.fs_stat(path)
if stat and stat.type == "file" and stat.mtime.sec > last_check then
return stat.mtime.sec
else
return nil
end
end

--- @type watch.Watcher[]
---
--- Global list of watchers and associated data.
Expand Down Expand Up @@ -119,6 +136,17 @@ Watch.update = function(command, bufnr)
return
end

local W = Watch.watchers[command]
if W.file then
local update = file_updated(W.file, W.last_updated)

if update then
Watch.watchers[command].last_updated = update
else
return
end
end

-- Execute your command and capture its output
-- Use vim.system for async
local code = vim.system(
Expand Down Expand Up @@ -165,10 +193,11 @@ end

--- Starts continually reloading a buffer's contents with a shell command. If the command is aleady being watched, then opens that buffer in the current window.
---
--- @param command string Shell command.
--- @param refresh_rate? integer Time between reloads in milliseconds. Defaults to `watch.config.refresh_rate`.
--- @param command string Shell command to watch. If `file` is given, then `%s` will expand to the filename.
--- @param refresh_rate? integer Time between reloads in milliseconds. Defaults to `watch.config.refresh_rate`. If `file` is provided, then it is increased to a minimum of 1000 ms.
--- @param bufnr? integer Buffer number to update. Defaults to a new buffer.
Watch.start = function(command, refresh_rate, bufnr)
--- @param file? string The absolute path of the file to watch. Defaults to `nil`.
Watch.start = function(command, refresh_rate, bufnr, file)
-- Check if command is nil
if not command or not string.len(command) then
vim.notify("[watch] Error: Empty command passed", vim.log.levels.ERROR)
Expand All @@ -184,6 +213,11 @@ Watch.start = function(command, refresh_rate, bufnr)
return
end

-- Expand %s to the filename
if file then
command = string.gsub(command, "%%s", file)
end

-- Open the buffer if already running
if Watch.watchers[command] then
bufnr = Watch.watchers[command].bufnr
Expand All @@ -203,6 +237,14 @@ Watch.start = function(command, refresh_rate, bufnr)
refresh_rate = Watch.config.refresh_rate
end

-- Minimum 1000 ms if file check
if file and refresh_rate < 1000 then
vim.notify(
"[watch] File watchers require refresh_rate >= 1000. Increasing to 1000 ms."
)
refresh_rate = 1000
end

-- Create a split based on configurations
local split = Watch.config.split or {}
if split and split.enabled then
Expand Down Expand Up @@ -255,6 +297,8 @@ Watch.start = function(command, refresh_rate, bufnr)
bufnr = bufnr,
refresh_rate = refresh_rate,
timer = timer,
file = file,
last_updated = 0,
}

Watch.watchers[command] = watcher
Expand All @@ -273,7 +317,7 @@ end
---
--- `WARNING:` If `watch.config.close_on_stop` is set to `true`, then affected buffers will also be deleted.
---
--- @param event? string|table The command name to stop. If string, then uses the string. If table, then uses `event.file`.
--- @param event? string|table The shell command to stop. If string, then uses the string. If table, then uses `event.file`.
Watch.stop = function(event)
-- Get the current buffer if it is a watcher
local bufname = nil
Expand Down
26 changes: 25 additions & 1 deletion plugin/watch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ end

--- Starts a new watcher.
---
--- @param cmd table The watched command. Final argument is the refresh rate in milliseconds, or nil if not a number.
--- @param cmd table The watched command. Refresh rate is the last argument given in milliseconds, or defaults to config if not a number.
command("WatchStart", function(cmd)
local args = cmd.fargs

Expand All @@ -27,6 +27,30 @@ command("WatchStart", function(cmd)
require("watch").start(table.concat(new_cmd, " "), refresh_rate, nil)
end, "+")

--- Starts a new watcher for the currently open file. Behaves like a normal watcher, but only runs the command if the file has been modified.
---
--- @param cmd table The watched command. Use `%s` inside the command to insert the absolute path of the current file. Refresh rate is the last argument given in milliseconds, or defaults to config if not a number (minimum 1000 for file watchers).
command("WatchFile", function(cmd)
local args = cmd.fargs

-- Add the last argument to command if not a number
local refresh_rate = tonumber(args[#args])
local from = 1
local to = refresh_rate and #args - 1 or #args

-- Get the command(s) from the arguments
local new_cmd = {}
for i = from, to do
table.insert(new_cmd, args[i])
end

local cmd_string = table.concat(new_cmd, " ")

local file = vim.fn.expand("%:p")

require("watch").start(cmd_string, refresh_rate, nil, file)
end, "+")

--- Stops a watcher.
---
--- `WARNING:` If `watch.config.close_on_stop` is set to `true`, then affected buffers will also be deleted.
Expand Down

0 comments on commit 27aa411

Please sign in to comment.