Module: Thaum::RunLoop
- Defined in:
- lib/thaum/run_loop.rb
Overview
Owns the main run loop: terminal setup, the event queue, mount/draw pass, suspend/resume choreography, and graceful teardown.
Class Method Summary collapse
- .build_tick_task(queue:, interval:) ⇒ Object
- .event_loop(app:, queue:, terminal:, renderer:, cols:, rows:) ⇒ Object
- .handle_resize(app:, event:, _cols: nil, _rows: nil) ⇒ Object
-
.install_resize_trap(queue:, terminal:) ⇒ Object
Push a ResizeEvent on SIGWINCH.
-
.install_suspend_traps(queue:) ⇒ Object
Push :suspend / :resume sentinels onto the main queue from signal handlers.
- .install_traps(queue:, terminal:) ⇒ Object
- .mount_pass(app:, renderer:, cols:, rows:) ⇒ Object
-
.run(app:, tick: 0.1, threads: 4) ⇒ Object
Startup entry point.
- .shutdown_pool(pool:) ⇒ Object
Class Method Details
.build_tick_task(queue:, interval:) ⇒ Object
95 96 97 98 99 100 101 102 |
# File 'lib/thaum/run_loop.rb', line 95 def build_tick_task(queue:, interval:) last_tick = Process.clock_gettime(Process::CLOCK_MONOTONIC) Concurrent::TimerTask.new(execution_interval: interval) do now = Process.clock_gettime(Process::CLOCK_MONOTONIC) queue.push(TickEvent.new(time: now, delta: now - last_tick)) last_tick = now end end |
.event_loop(app:, queue:, terminal:, renderer:, cols:, rows:) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/thaum/run_loop.rb', line 42 def event_loop(app:, queue:, terminal:, renderer:, cols:, rows:) until app.quit? event = queue.pop if event == :suspend cols, rows = Suspender.suspend(app:, terminal:, queue:) next end # Stray :resume outside a suspend window — ignore. next if event == :resume cols, rows = handle_resize(app:, event:, _cols: cols, _rows: rows) if event.is_a?(ResizeEvent) Dispatch.from_queue(app:, event:) next unless app.dirty? app.clear_dirty Thaum.safe_invoke("render") { Painter.paint(app:, renderer:, cols:, rows:) } end [cols, rows] end |
.handle_resize(app:, event:, _cols: nil, _rows: nil) ⇒ Object
85 86 87 88 89 90 91 92 93 |
# File 'lib/thaum/run_loop.rb', line 85 def handle_resize(app:, event:, _cols: nil, _rows: nil) cols = event.width rows = event.height Thaum.safe_invoke("App#run_partition") do app.run_partition(rect: Rect.new(x: 0, y: 0, width: cols, height: rows)) end Thaum.safe_invoke("App#recompute_modal_rect") { app.recompute_modal_rect } [cols, rows] end |
.install_resize_trap(queue:, terminal:) ⇒ Object
Push a ResizeEvent on SIGWINCH.
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/thaum/run_loop.rb', line 110 def install_resize_trap(queue:, terminal:) Signal.trap("WINCH") do w, h = terminal.size begin queue.push(ResizeEvent.new(width: w, height: h)) rescue ClosedQueueError nil end end end |
.install_suspend_traps(queue:) ⇒ Object
Push :suspend / :resume sentinels onto the main queue from signal handlers. The actual suspend dance happens in Suspender so the signal handler stays minimal (signal-handler context has tight restrictions).
124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/thaum/run_loop.rb', line 124 def install_suspend_traps(queue:) Signal.trap("TSTP") do queue.push(:suspend) rescue ClosedQueueError nil end Signal.trap("CONT") do queue.push(:resume) rescue ClosedQueueError nil end end |
.install_traps(queue:, terminal:) ⇒ Object
104 105 106 107 |
# File 'lib/thaum/run_loop.rb', line 104 def install_traps(queue:, terminal:) install_resize_trap(queue:, terminal:) install_suspend_traps(queue:) end |
.mount_pass(app:, renderer:, cols:, rows:) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/thaum/run_loop.rb', line 65 def mount_pass(app:, renderer:, cols:, rows:) app.run_partition(rect: Rect.new(x: 0, y: 0, width: cols, height: rows)) app.wire_sigils app.validate_focus_order_tree Thaum.safe_invoke("App#on_mount") { app.on_mount } Tree.walk(app) do |node| next unless node.is_a?(Sigil) || node.is_a?(Octagram) Thaum.safe_invoke("#{node.class}#on_mount") { node.on_mount } end first = Thaum.safe_invoke("App#initial_focus") { app.initial_focus } if first app.set_initial_focus(first) Thaum.safe_invoke("#{first.class}#on_focus") { first.on_focus } end Painter.paint(app:, renderer:, cols:, rows:) end |
.run(app:, tick: 0.1, threads: 4) ⇒ Object
Startup entry point. Blocks until app.quit is called. Returns nil.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/thaum/run_loop.rb', line 10 def run(app:, tick: 0.1, threads: 4) terminal = Terminal.new queue = Thread::Queue.new capability = Color.detect(ENV) warn "[Thaum] color not supported, rendering without color" if capability == :none renderer = Rendering::Renderer.new(capability: capability) pool = Concurrent::FixedThreadPool.new(threads) tick_task = build_tick_task(queue:, interval: tick) Thaum::Action.queue = queue Thaum::Action.pool = pool install_traps(queue:, terminal:) terminal.setup cols, rows = terminal.size mount_pass(app:, renderer:, cols:, rows:) input_reader = InputReader.new(input: $stdin, queue: queue) input_reader.start tick_task.execute event_loop(app:, queue:, terminal:, renderer:, cols:, rows:) ensure tick_task&.shutdown input_reader&.stop shutdown_pool(pool:) Thaum::Action.queue = nil Thaum::Action.pool = nil terminal&.teardown end |
.shutdown_pool(pool:) ⇒ Object
137 138 139 140 141 142 |
# File 'lib/thaum/run_loop.rb', line 137 def shutdown_pool(pool:) return unless pool pool.shutdown pool.wait_for_termination(1) || pool.kill end |