Class: Tuile::EventQueue
- Inherits:
-
Object
- Object
- Tuile::EventQueue
- Defined in:
- lib/tuile/event_queue.rb,
sig/tuile.rbs
Overview
An event queue. The idea is that all UI-related updates run from the thread which runs the event queue only; this removes any need for locking and/or need for thread-safety mechanisms.
Any events (keypress, timer, term resize – WINCH) are captured in background threads; instead of processing the events directly the events are pushed into the event queue: this causes the events to be processed centrally, by a single thread only.
Defined Under Namespace
Classes: BackgroundColorEvent, ColorSchemeEvent, EmptyQueueEvent, ErrorEvent, KeyEvent, PasteEvent, TTYSizeEvent, Ticker
Instance Method Summary collapse
-
#await_empty ⇒ void
Awaits until the event queue is empty (all events have been processed).
- #event_loop ⇒ void
-
#in_loop_thread? ⇒ Boolean
@return — true if this thread is the one running #run_loop.
-
#initialize(listen_for_keys: true) ⇒ EventQueue
constructor
@param
listen_for_keys— if true, fires KeyEvent. -
#post(event) ⇒ void
Posts event into the event queue.
-
#run_loop ⇒ void
Runs the event loop and blocks.
-
#running? ⇒ Boolean
@return — true if a #run_loop is in progress on any thread.
-
#start_key_thread ⇒ void
Starts listening for stdin, firing KeyEvent on keypress.
-
#stop ⇒ void
Stops ongoing #run_loop.
-
#submit(&block) ⇒ void
Submits block to be run in the event queue.
-
#tick(seconds, &block) ⇒ void
Schedules
blockto fire on the event-loop thread everyseconds, passing a 0-based monotonically increasing tick counter —tick(0.2)fires five times a second. -
#tick_fps(fps, &block) ⇒ void
Frames-per-second convenience over #tick:
tick_fps(15)is exactlytick(1.0 / 15). -
#trap_winch ⇒ void
Trap the WINCH signal (TTY resize signal) and fire TTYSizeEvent.
Constructor Details
#initialize(listen_for_keys: true) ⇒ EventQueue
@param listen_for_keys — if true, fires KeyEvent.
14 15 16 17 18 |
# File 'lib/tuile/event_queue.rb', line 14 def initialize(listen_for_keys: true) @queue = Thread::Queue.new @listen_for_keys = listen_for_keys @run_lock = Mutex.new end |
Instance Method Details
#await_empty ⇒ void
This method returns an undefined value.
Awaits until the event queue is empty (all events have been processed).
47 48 49 50 51 |
# File 'lib/tuile/event_queue.rb', line 47 def await_empty latch = Concurrent::CountDownLatch.new(1) submit { latch.count_down } latch.wait end |
#event_loop ⇒ void
This method returns an undefined value.
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/tuile/event_queue.rb', line 351 def event_loop loop do yield EmptyQueueEvent.instance if @queue.empty? event = @queue.pop break if event.nil? if event.is_a? ErrorEvent begin raise event.error rescue StandardError # Re-raise wrapped so the original error is preserved as `cause` # while the loop's own backtrace shows up in the wrapper. raise Tuile::Error, "background event raised: #{event.error.class}: #{event.error.}" end else yield event end end end |
#in_loop_thread? ⇒ Boolean
@return — true if this thread is the one running #run_loop.
153 |
# File 'lib/tuile/event_queue.rb', line 153 def in_loop_thread? = @run_lock.owned? |
#post(event) ⇒ void
This method returns an undefined value.
Posts event into the event queue. The event may be of any type. Since the event is passed between threads, the event object should be frozen.
The function may be called from any thread.
@param event — the event to post to the queue, should be frozen.
26 27 28 29 30 |
# File 'lib/tuile/event_queue.rb', line 26 def post(event) raise ArgumentError, "event passed across threads must be frozen, got #{event.inspect}" unless event.frozen? @queue << event end |
#run_loop ⇒ void
This method returns an undefined value.
Runs the event loop and blocks. Must be run from at most one thread at the same time. Blocks until some thread calls #stop. Calls block for all events; the block is always called from the thread running this function.
Any exception raised by the block is re-thrown, causing this function to
terminate. Wrap the block body in rescue if you want to handle errors
without tearing down the loop — see Screen#event_loop for an example.
Procs are yielded too. A #submited block arrives as a Proc event;
the consumer is responsible for invoking it (typically event.call).
Yielding rather than dispatching inline means a raise inside the
submitted block flows through the consumer's rescue like any other
event-handler error, instead of bypassing it.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/tuile/event_queue.rb', line 120 def run_loop(&) raise ArgumentError, "run_loop requires a block" unless block_given? @run_lock.synchronize do start_key_thread if @listen_for_keys begin trap_winch event_loop(&) ensure Signal.trap("WINCH", "SYSTEM_DEFAULT") if @key_thread # Kill returns immediately, but the key thread is typically # blocked inside $stdin.getch with a termios snapshot saved in # io-console's C-level ensure. If we let it run to completion # *after* the outer $stdin.raw block has exited (e.g. when an # exception is escaping run_event_loop), the late tcsetattr # restores raw mode and leaves the terminal with ONLCR off — # the stack trace then prints as one un-wrapped soft line. # Joining here forces the restore to happen while we're still # nested inside $stdin.raw, so raw's own restoration is the # final write and the terminal lands in cooked mode. @key_thread.kill @key_thread.join end @queue.clear end end end |
#running? ⇒ Boolean
@return — true if a #run_loop is in progress on any thread.
150 |
# File 'lib/tuile/event_queue.rb', line 150 def running? = @run_lock.locked? |
#start_key_thread ⇒ void
This method returns an undefined value.
Starts listening for stdin, firing KeyEvent on keypress.
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 |
# File 'lib/tuile/event_queue.rb', line 373 def start_key_thread @key_thread = Thread.new do loop do key = Keys.getkey event = if key == Keys::PASTE_START PasteEvent.new(Keys.read_paste) else Mouse.parse(key) || ColorSchemeEvent.parse(key) || BackgroundColorEvent.parse(key) || KeyEvent.new(key) end post event end rescue StandardError => e post ErrorEvent.new(e) end end |
#stop ⇒ void
161 162 163 164 |
# File 'lib/tuile/event_queue.rb', line 161 def stop @queue.clear post(nil) end |
#submit(&block) ⇒ void
This method returns an undefined value.
Submits block to be run in the event queue. Returns immediately.
The function may be called from any thread.
The block runs only while a loop is draining: submitted before the first #run_loop it waits for one, submitted after the last it never runs.
41 42 43 |
# File 'lib/tuile/event_queue.rb', line 41 def submit(&block) @queue << block end |
#tick(seconds, &block) ⇒ void
This method returns an undefined value.
Schedules block to fire on the event-loop thread every seconds,
passing a 0-based monotonically increasing tick counter — tick(0.2)
fires five times a second. Use it for periodic UI refresh (poll a status,
redraw a clock); for animation, #tick_fps reads more naturally. The
returned Ticker controls the schedule — Tuile::EventQueue::Ticker#cancel stops it.
Errors: if block raises, the Ticker cancels itself and the
exception flows through the normal event-loop error path
(Screen#on_error by default) — auto-cancel keeps a broken block from
spamming on_error at the tick rate.
Tickers reuse concurrent-ruby's shared timer thread, so adding more
tickers doesn't add threads.
@param seconds — interval between firings, must be positive. Fractional values are fine (tick(0.05) ⇒ ~20 firings a second).
73 74 75 76 77 78 79 80 |
# File 'lib/tuile/event_queue.rb', line 73 def tick(seconds, &block) raise ArgumentError, "block required" unless block unless seconds.is_a?(Numeric) && seconds.positive? raise ArgumentError, "seconds must be a positive Numeric, got #{seconds.inspect}" end Ticker.new(self, seconds, block) end |
#tick_fps(fps, &block) ⇒ void
This method returns an undefined value.
Frames-per-second convenience over #tick: tick_fps(15) is exactly
tick(1.0 / 15). Reads naturally for animation (a /-\| spinner, a
progress pulse) where you think in frames, not intervals.
@param fps — firings per second, must be positive. Fractional values are fine (tick_fps(0.5) ⇒ one firing every two seconds).
91 92 93 94 95 96 97 98 |
# File 'lib/tuile/event_queue.rb', line 91 def tick_fps(fps, &block) raise ArgumentError, "block required" unless block unless fps.is_a?(Numeric) && fps.positive? raise ArgumentError, "fps must be a positive Numeric, got #{fps.inspect}" end tick(1.0 / fps, &block) end |
#trap_winch ⇒ void
This method returns an undefined value.
Trap the WINCH signal (TTY resize signal) and fire TTYSizeEvent.
392 393 394 395 396 397 398 |
# File 'lib/tuile/event_queue.rb', line 392 def trap_winch Signal.trap("WINCH") do post TTYSizeEvent.create rescue StandardError => e post ErrorEvent.new(e) end end |