diff --git a/CHANGELOG.md b/CHANGELOG.md index 33cf2c8..d583170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added +- Quick-start app entry points — `Thaum.app do … end` builds and runs an app from a block (an ordinary class body) in place of defining a named class, and `SomeApp.run` builds and runs an instance (forwarding the same options as `Thaum.run`). `Thaum.run(app)` remains the primitive for apps that need constructor arguments. - Dev log console — `Thaum.log` writes a debug side channel to a file (`Thaum.run(app, log: path)`), so you can log without corrupting the TUI (stdout is the UI). Logger-style API (`debug`/`info`/`warn`/`error`, lazy block form, exception formatting); off by default; truncates on open. While a log is active, framework-internal warnings route to it instead of stderr — `safe_invoke` handler exceptions at ERROR, emit-guard warnings at WARN. Companion examples `log_reader.rb` (tails and colorizes the file live) and `log_generator.rb`. See `docs/logging.md`. ## [0.2.1] - 2026-06-17 diff --git a/README.md b/README.md index 9f6167f..64cf3c5 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,27 @@ Requires Ruby 3.2 or newer. ## Usage -The `Hello World` example: +The quickest start — `Thaum.app` builds and runs an app from a block, in place +of defining a named class. The block is an ordinary class body: + +```ruby +require "thaum" + +Thaum.app do + def partition + vertical do + region(height: :fill) { Thaum::Text.new(content: "Hello World!", align: :center) } + end + end + + def on_key(event) + quit if event.key == :escape + end +end +``` + +For a longer-lived app, define a class and start it with `.run`. `include +Thaum::App` adds a `.run` class method: ```ruby require "thaum" @@ -63,16 +83,15 @@ class HelloWorldApp end def partition - vertical do - region(height: :fill) { @greeting } - end + vertical { region(height: :fill) { @greeting } } end end -Thaum.run(HelloWorldApp.new) - +HelloWorldApp.run ``` +`Thaum.run(app)` remains the primitive — use it directly when an app needs constructor arguments, e.g. `Thaum.run(LogReaderApp.new(path))`. + ## Examples The `examples/` directory has a runnable demo for every shipped sigil plus a few composed apps: diff --git a/examples/checkbox.rb b/examples/checkbox.rb index d7f1bf0..2812b2b 100644 --- a/examples/checkbox.rb +++ b/examples/checkbox.rb @@ -86,4 +86,4 @@ def partition end end -Thaum.run(CheckboxApp.new) +CheckboxApp.run diff --git a/examples/counter.rb b/examples/counter.rb index 0be5143..9f939da 100644 --- a/examples/counter.rb +++ b/examples/counter.rb @@ -47,4 +47,4 @@ def partition end end -Thaum.run(CounterApp.new) +CounterApp.run diff --git a/examples/hello_world.rb b/examples/hello_world.rb index 1004aeb..660eb91 100644 --- a/examples/hello_world.rb +++ b/examples/hello_world.rb @@ -1,28 +1,21 @@ # frozen_string_literal: true # Usage: bundle exec ruby examples/hello_world.rb -# A minimal "Hello World" app. Press esc to quit. +# A minimal "Hello World" app using Thaum.app in place of a named class. +# Press esc to quit. $LOAD_PATH.unshift File.expand_path("../lib", __dir__) require "thaum" -class HelloWorldApp - include Thaum::App - - def on_key(event) - quit if event.key == :escape - end - - def initialize - @greeting = Thaum::Text.new(content: "Hello World!", align: :center) - end - +Thaum.app do def partition vertical do - region(height: :fill) { @greeting } + region(height: :fill) { Thaum::Text.new(content: "Hello World!", align: :center) } end end -end -Thaum.run(HelloWorldApp.new) + def on_key(event) + quit if event.key == :escape + end +end diff --git a/examples/layout_demo.rb b/examples/layout_demo.rb index bdf8426..80ade0a 100644 --- a/examples/layout_demo.rb +++ b/examples/layout_demo.rb @@ -135,4 +135,4 @@ def middle end end -Thaum.run(LayoutDemoApp.new) +LayoutDemoApp.run diff --git a/examples/log_generator.rb b/examples/log_generator.rb index f178f7b..1cb20e1 100644 --- a/examples/log_generator.rb +++ b/examples/log_generator.rb @@ -82,4 +82,4 @@ def partition end end -Thaum.run(LogGeneratorApp.new, log: ARGV[0] || "thaum.log") if __FILE__ == $PROGRAM_NAME +LogGeneratorApp.run(log: ARGV[0] || "thaum.log") if __FILE__ == $PROGRAM_NAME diff --git a/examples/modal.rb b/examples/modal.rb index ce06314..cda9c81 100644 --- a/examples/modal.rb +++ b/examples/modal.rb @@ -73,4 +73,4 @@ def on_event(event) end end -Thaum.run(ModalApp.new) +ModalApp.run diff --git a/examples/mouse.rb b/examples/mouse.rb index 21c633b..30339e2 100644 --- a/examples/mouse.rb +++ b/examples/mouse.rb @@ -57,4 +57,4 @@ def partition end end -Thaum.run(MouseApp.new) +MouseApp.run diff --git a/examples/progress_bar.rb b/examples/progress_bar.rb index d098201..669e682 100644 --- a/examples/progress_bar.rb +++ b/examples/progress_bar.rb @@ -87,4 +87,4 @@ def partition end end -Thaum.run(ProgressApp.new) +ProgressApp.run diff --git a/examples/scroll_view.rb b/examples/scroll_view.rb index c43545e..a8300bc 100644 --- a/examples/scroll_view.rb +++ b/examples/scroll_view.rb @@ -61,4 +61,4 @@ def partition end end -Thaum.run(ScrollViewApp.new) +ScrollViewApp.run diff --git a/examples/spinner.rb b/examples/spinner.rb index d818f1e..5d1b66e 100644 --- a/examples/spinner.rb +++ b/examples/spinner.rb @@ -63,4 +63,4 @@ def spinner_row(spinner:, label:) end end -Thaum.run(SpinnerApp.new) +SpinnerApp.run diff --git a/examples/status_bar.rb b/examples/status_bar.rb index c1c67bb..d173d6d 100644 --- a/examples/status_bar.rb +++ b/examples/status_bar.rb @@ -62,4 +62,4 @@ def segments end end -Thaum.run(StatusBarApp.new) +StatusBarApp.run diff --git a/examples/stopwatch.rb b/examples/stopwatch.rb index e4afd74..af3e673 100644 --- a/examples/stopwatch.rb +++ b/examples/stopwatch.rb @@ -81,4 +81,4 @@ def cycle_theme end end -Thaum.run(StopwatchApp.new, tick: 0.05) +StopwatchApp.run(tick: 0.05) diff --git a/examples/tabs.rb b/examples/tabs.rb index d74d6a1..919b342 100644 --- a/examples/tabs.rb +++ b/examples/tabs.rb @@ -109,4 +109,4 @@ def partition end end -Thaum.run(TabsApp.new) +TabsApp.run diff --git a/examples/text.rb b/examples/text.rb index 0cd48f0..14975ab 100644 --- a/examples/text.rb +++ b/examples/text.rb @@ -98,4 +98,4 @@ def partition end end -Thaum.run(TextApp.new) +TextApp.run diff --git a/examples/theme_picker.rb b/examples/theme_picker.rb index d48f980..dcd5be8 100644 --- a/examples/theme_picker.rb +++ b/examples/theme_picker.rb @@ -92,4 +92,4 @@ def partition end end -Thaum.run(ThemePickerApp.new) +ThemePickerApp.run diff --git a/examples/todo.rb b/examples/todo.rb index 6d3d193..eebcf2e 100644 --- a/examples/todo.rb +++ b/examples/todo.rb @@ -239,4 +239,4 @@ def press(label) end end -Thaum.run(TodoApp.new) +TodoApp.run diff --git a/lib/thaum.rb b/lib/thaum.rb index 624e92d..a1cd06f 100644 --- a/lib/thaum.rb +++ b/lib/thaum.rb @@ -92,4 +92,27 @@ def self.safe_invoke(label) def self.run(app, tick: 0.1, threads: 4, log: nil) RunLoop.run(app:, tick:, threads:, log:) end + + # Quick-start: build an anonymous Thaum::App from the block body and run it, + # in place of defining a named class. The block is an ordinary class body — + # define `partition` and handlers with `def`: + # + # Thaum.app do + # def partition + # vertical { region(height: :fill) { Thaum::Text.new(content: "Hello") } } + # end + # + # def on_key(event) + # quit if event.key == :escape + # end + # end + # + # Forwards the same options as Thaum.run. + def self.app(**, &block) + raise ArgumentError, "Thaum.app requires a block" unless block + + klass = Class.new { include App } + klass.class_eval(&block) + klass.run(**) + end end diff --git a/lib/thaum/app.rb b/lib/thaum/app.rb index a3f8017..e38e28f 100644 --- a/lib/thaum/app.rb +++ b/lib/thaum/app.rb @@ -8,6 +8,15 @@ module App include Concerns::Modal include Concerns::TabNavigation + def self.included(base) = base.extend(ClassMethods) + + # Class methods added on include. + module ClassMethods + # Build and run an instance: `SomeApp.run(log: "/path")`. Forwards the + # same options as Thaum.run. + def run(**) = Thaum.run(new, **) + end + attr_reader :in_on_update, :modal_sigil, :modal_rect # --- Quit --- diff --git a/test/test_app_dsl.rb b/test/test_app_dsl.rb new file mode 100644 index 0000000..03b4b27 --- /dev/null +++ b/test/test_app_dsl.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "test_helper" + +# Quick-start entry points for Thaum::App: the `.run` class method and +# `Thaum.app { }`. +class TestAppDSL < Minitest::Test + def test_include_adds_run_class_method + klass = Class.new { include Thaum::App } + assert_respond_to klass, :run + end + + # The happy path enters the blocking run loop (no headless harness yet, and + # the suite doesn't stub), so it's exercised by examples/hello_world.rb. Here + # we only pin the missing-block guard, which needs no terminal. + def test_app_requires_a_block + assert_raises(ArgumentError) { Thaum.app } + end +end