Skip to content

Instantly share code, notes, and snippets.

@KonnorRogers
Created August 3, 2026 02:14
Show Gist options
  • Select an option

  • Save KonnorRogers/9c2532d07795bb3fb72cc25c0b7eb4e9 to your computer and use it in GitHub Desktop.

Select an option

Save KonnorRogers/9c2532d07795bb3fb72cc25c0b7eb4e9 to your computer and use it in GitHub Desktop.
File watching (polling) in DragonRuby
class FileWatcher
# This can get potentially deep if you have a lot of files or directories. Consider a fiber if its taking up too much time.
def self.list_files_recursive(directory, max_iterations_for_fiber: 100, fiber: false)
idx = 0
files = []
all_files = []
# Seed with full paths so we never lose directory context. `.reverse_each` makes `.pop()` yield entries in listing order. This is like the old "list_files_recursive" which actually used recursion, but by using a loop we can yield to a fiber on potentially large directories.
DR.list_files(directory).reverse_each do |f|
files << File.join(directory, f)
Fiber.yield if fiber && idx % max_iterations_for_fiber == 0
end
while file_path = files.pop
idx += 1
# file_path is a full path now, so check the basename for the hidden rule.
next if File.basename(file_path).start_with?(".")
stat = DR.stat_file(file_path)
next if !stat
stat.path = file_path
if stat[:file_type] == :directory
# Directories aren't part of the result; just descend.
DR.list_files(file_path).reverse_each do |f|
idx += 1
files << File.join(file_path, f)
Fiber.yield if fiber && idx % max_iterations_for_fiber == 0
end
next
end
all_files << stat
Fiber.yield if fiber && idx % max_iterations_for_fiber == 0
end
all_files
end
attr_accessor :cache, :files_per_tick
def initialize(&block)
@cache = {}
@modified_files = []
@callback = block || proc {}
@files_per_tick = 100
end
def tick_watch(*directories)
if @fiber_watch && @fiber_watch.alive?
@fiber_watch.resume
else
@fiber_watch = Fiber.new { watch(*directories, fiber: true) }
end
end
def watch(*directories, fiber: false)
directories.flatten.each do |directory|
FileWatcher.list_files_recursive(directory, fiber: fiber, max_iterations_for_fiber: @files_per_tick).each do |stat|
add(stat)
end
end
end
def watched_files
@cache.values
end
# Flushes any modified files and calls your block on it.
def flush
while stat = @modified_files.pop()
@callback.call(stat)
end
end
# Will add a stat to the cache.
def add(stat)
if !stat
return
end
existing_stat = @cache[stat.path]
if !existing_stat || existing_stat.mod_time != stat.mod_time || existing_stat.file_size != stat.file_size
@cache[stat.path] = stat
@modified_files << stat
end
end
def delete(stat)
@cache.delete(stat.path)
end
end
## Usage
require "./file_watcher.rb"
module Main
attr_accessor :game
def tick(args)
if !$game
$game = App::Game.new
end
if !@file_watcher
# Watch the sprites directory and register the callback to reset the sprite if its been modified
@file_watcher = FileWatcher.new("sprites") { |stat| DR.reset_sprite(stat.path) }
end
$game.tick(args)
@file_watcher.tick_watch("sprites") # iterates through 100 files
@file_watcher.flush # calls your callback on any modified files.
end
def reset(args)
$game = nil
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment