Node commandline application to replace selected text in a file.
node-edge-tts is a module that using Microsoft Edge's online TTS (Text-to-Speech) service on the Node.js
A high-quality & reliable TTS React Hooks library
React Native Text-To-Speech module for Android and iOS
Google TTS (Text-To-Speech) for node.js
High-quality text-to-speech for the web
Universal text-to-speech library using Microsoft Edge's online TTS service. Works in Node.js and browsers WITHOUT needing Microsoft Edge, Windows, or an API key
Use Microsoft Edge's online text-to-speech service from JavaScript WITHOUT needing Microsoft Edge or Windows or an API key
Text to Speech (TTS) addon for qvac
Edge TTS is a package that allows access to the online text-to-speech service used by Microsoft Edge without the need for Microsoft Edge, Windows, or an API key.
An Azure Speech Service module that uses the Microsoft Edge Read Aloud API.
Disambiguates common bus stop abbreviations automatically.
Free Text-to-Speech using Microsoft Edge TTS
OpenAI plugin for LiveKit Node Agents
Text-to-speech using Google Translate
React hook and component for converting text to speech using the Web Speech API or Amazon Polly.
Build ultra fast, tiny, and cross-platform desktop apps with Typescript.
TTS interfaces and types for PIE Assessment Toolkit - No UI dependencies
realtime voice chat with pi.dev
Voice pipeline for Cloudflare Agents — STT, TTS, VAD, streaming, and SFU utilities
Qwen TTS Realtime WebSocket client with audio playback
Web version of rhasspy/piper running locally in the browser
An Azure Speech Service module that uses the Microsoft Edge Read Aloud API.
Official Soniox SDK for Node
Are your gems taking up too much disk space? Documentation got you down? Comments stuck in your craw? Tests taking too much space? Fix all that by with <tt>gem tldr</tt>. I know that disk space is at a premium these days with an introductory netbook or a small AWS EC2 instance containing a mere 160GB. <tt>gem tldr</tt> removes the test directory, build artifacts like .c and .h files, comments in your ruby source files and comments in your ruby source.
TKXXS provides a very simple and very easy to use GUI (graphical user interface) for Ruby; It gives you a persistent output window and popping up (modal) dialogs for input; For a screenshot, see: <tt>https://github.com/Axel2/tkxxs/blob/master/images/screenshot.png</tt>; I tested it on Windows, only; Got user report, that it works on Ubuntu, too. TKXXS shall: * improve the usability of little applications, which otherwise would use a command line interface (CLI); for example by a GUI-file chooser * give a simple GUI front-end for apps, which take parameters on the command line. (stdout can easily be redirected to the OutputWindow.) * take only little more effort and coding time over programming a CLI; * be able to easily upgrade existing CLI-applications; * be comfortable in use (e.g. provide incremental search, tool-tip-help, ...); * be easy to install. Drawbacks: * I'v tested it only on Windows, but got user report, that it works on Ubuntu, too.l * For sure some more drawbacks which I'm not aware of now. TKXXS uses TK (easy to install).
The SCAI interface is used when the merchant wishes to keep the acquirer on her/his own website for the whole duration of the transaction (client payment details transits through *both* the merchant site and the saferpay database ) whereas VT implies a redirect to the saferpay site. == FEATURES/PROBLEMS: * supports both common credit cards and direct debit cards ("Lastschrift") * support for VT style payments is incomplete == SYNOPSIS: Init (info from saferpay test account; they're the same for all test accounts): @pan = "9451123100000004" # Saferpay test PAN @accountid = "99867-94913159" # Saferpay test ACCOUNTID @exp = "1107" # This will change for other test accounts I guess... Might just be three months ahead of Time.now @sfp = Saferpay.new( @accountid, @pan, @exp ) Reserve: <tt>@sfp.reserve(30000, "USD")</tt> Amounts are divided by 100. We're talking cents here, not dollars... Capture last transaction: <tt>@sfp.capture</tt> Capture with a transacaton ID "4hj34hj4hh34h4j3hj4h334": <tt>@sfp.capture("4hj34hj4hh34h4j3hj4h334")</tt> == REQUIREMENTS:
== Devise::Revokable A module for Devise[http://github.com/plataformatec/devise] This gem was created by "borrowing" heavily from Devise::Invitable[http://github.com/scambra/devise_invitable] It exists to extend Devise to provide the basis for what is essentially the reverse of the standard <tt>confirmable</tt> module. Where <tt>confirmable</tt> sends an email and awaits a response, before confirming a new registration, <tt>revokable</tt> allows immediate access and sends an email which provides a link to "revoke" the account if it was created fraudulently. This is useful if you want to lower the barrier to entry to creating accounts, and clearly, if account security isn't a concern. Note that tests are non-existent. Use freely but at your own risk. === Configuring It works like normal Devise modules. Add the <tt>:revokable</tt> module to the declaration. # in user.rb devise :revokable # plus other devise modules If the user who received the revocation email follows the provided link and confirms revocation, the account will effectively be "revoked" and inactive, unable to log in. Additionally, you may want to override <tt>#revoke!</tt> to perfom additional revocation on the account, e.g. deleting posts made, resetting personal information, etc. The super method yields to a block for this purpose. # in user.rb def revoke! super do self.some_method_that_resets_me! end end That's about the extent of it. As with typical devise modules you can override the mailers and views with your own. Additionally you can define the module accessor <tt>@@mailer</tt> on the module with a proc to handle your mail if you need to. This proc is yielded two arguments, the method name (e.g. :revocation_instructions), and the affected resource. # in config/initializers/devise_revokable.rb require 'devise_revokable' require 'my_mailer' DeviseRevokable.mailer = proc {|method_name, resource| MyMailer.send(:method_name, resource) }
= dm-is-published This plugin makes it very easy to add different states to your models, like 'draft' vs 'live'. By default it also adds validations of the field value. Originally inspired by the Rails plugin +acts_as_publishable+ by <b>fr.ivolo.us</b>. == Installation # Add GitHub to your RubyGems sources $ gem sources -a http://gems.github.com $ (sudo)? gem install kematzy-dm-is-published <b>NB! Depends upon the whole DataMapper suite being installed, and has ONLY been tested with DM 0.10.0 (next branch).</b> == Getting Started First of all, for a better understanding of this gem, make sure you study the '<tt>dm-is-published/spec/integration/published_spec.rb</tt>' file. ---- Require +dm-is-published+ in your app. require 'dm-core' # must be required first require 'dm-is-published' Lets say we have an Article class, and each Article can have a current state, ie: whether it's Live, Draft or an Obituary awaiting the death of someone famous (real or rumored) class Article include DataMapper::Resource property :id, Serial property :title, String ...<snip> is :published end Once you have your Article model we can create our Articles just as normal Article.create(:title => 'Example 1') The instance of <tt>Article.get(1)</tt> now has the following things for free: * a <tt>:publish_status</tt> attribute with the value <tt>'live'</tt>. Default choices are <tt>[ :live, :draft, :hidden ]</tt>. * <tt>:is_live?, :is_draft? or :is_hidden?</tt> methods that returns true/false based upon the state. * <tt>:save_as_live</tt>, <tt>:save_as_draft</tt> or <tt>:save_as_hidden</tt> converts the instance to the state and saves it. * <tt>:publishable?</tt> method that returns true for models where <tt>is :published </tt> has been declared, but <b>false</b> for those where it has not been declared. The Article class also gets a bit of new functionality: Article.all(:draft) => finds all Articles with :publish_status = :draft Article.all(:draft, :author => @author_joe ) => finds all Articles with :publish_status = :draft and author == Joe Todo Need to write more documentation here.. == Usage Scenarios In a Blog/Publishing scenario you could use it like this: class Article ...<snip>... is :published :live, :draft, :hidden end Whereas in another scenario - like in a MenuItem model for a Restaurant - you could use it like this: class MenuItem ...<snip>... is :published :on, :off # the item is either on the menu or not end == RTFM As I said above, for a better understanding of this gem/plugin, make sure you study the '<tt>dm-is-published/spec/integration/published_spec.rb</tt>' file. == Errors / Bugs If something is not behaving intuitively, it is a bug, and should be reported. Report it here: http://github.com/kematzy/dm-is-published/issues == Credits Copyright (c) 2009-07-11 [kematzy gmail com] Loosely based on the ActsAsPublishable plugin by [http://fr.ivolo.us/posts/acts-as-publishable] == Licence Released under the MIT license.
== README.md: #ScheduledResource This gem is for displaying how things are used over time -- a schedule for a set of "resources". You can configure the elements of the schedule and there are utilities and protocols to connect them: - Configuration (specification and management), - Query interfaces (a REST-like API and internal protocols to query the models), and - A basic Rails controller implementation. We have a way to configure the schedule, internal methods to generate the data, and a way to retrieve data from the client. However this gem is largely view-framework agnostic. We could use a variety of client-side packages or even more traditional Rails view templates to generate HTML. In any case, to get a good feel in a display like this we need some client-side code. The gem includes client-side modules to: - Manage <b>time and display geometries</b> with "infinite" scroll along the time axis. - <b>Format display cells</b> in ways specific to the resource models. - <b>Update text justification</b> as the display is scrolled horizontally. ## Configuration A **scheduled resource** is something that can be used for one thing at a time. So if "Rocky & Bullwinkle" is on channel 3 from 10am to 11am on Saturday, then 'channel 3' is the <u>resource</u> and that showing of the episode is a <u>resource-use</u> block. Resources and use-blocks are typically Rails models. Each resource and its use-blocks get one row in the display. That row has a label to the left with some timespan visible on the rest of the row. Something else you would expect see in a schedule would be headers and labels -- perhaps one row with the date and another row with the hour. Headers and labels also fit the model of resources and use-blocks. Basic timezone-aware classes (ZTime*) for those are included in this gem. ### Config File The schedule configuration comes from <tt>config/resource_schedule.yml</tt> which has three top-level sections: - ResourceKinds: A hash where the key is a Resource and the value is a UseBlock. (Both are class names), - Resources: A list where each item is a Resource Class followed by one or more resource ids, and - visibleTime: The visible timespan of the schedule in seconds. The example file <tt>config/resource_schedule.yml</tt> (installed when you run <tt>schedulize</tt>) should be enough to display a two-row schedule with just the date above and the hour below. Of course you can monkey-patch or subclass these classes for your own needs. ### The schedule API The 'schedule' endpoint uses parameters <tt>t1</tt> and <tt>t2</tt> to specify a time interval for the request. A third parameter <tt>inc</tt> allows an initial time window to be expanded without repeating blocks that span those boundaries. The time parameters _plus the configured resources_ define the data to be returned. ### More About Configuration Management The <b>ScheduledResource</b> class manages resource and use-block class names, id's and labels for a schedule according to the configuration file. A ScheduledResource instance ties together: 1. A resource class (eg TvStation), 2. An id (a channel number in this example), and 3. Strings and other assets that will go into the DOM. The id is used to - select a resource _instance_ and - select instances of the _resource use block_ class (eg Program instances). The id _could_ be a database id but more often is something a little more suited to human use in the configuration. In any case it is used by model class method <tt>(resource_use_block_class).get_all_blocks()</tt> to select the right use-blocks for the resource. A resource class name and id are are joined with a '_' to form a tag that also serves as an id for the DOM. Once the configuration yaml is loaded that data is maintained in the session structure. Of course having a single configuration file limits the application's usefulness. A more general approach would be to have a user model with login and configuration would be associated with the user. ## Installation Add this line to your application's Gemfile: ```ruby gem 'scheduled_resource' ``` And then execute: $ bundle Or install it yourself as: $ gem install scheduled_resource Then from your application's root execute: $ schedulize . This will install a few image placeholders, client-side modules and a stylesheet under <tt>vendor/assets</tt>, an example configuration in <tt>config/resource_schedule.yml</tt> and an example controller in <tt>app/controllers/schedule_controller.rb</tt>. Also, if you use $ bundle show scheduled_resource to locate the installed source you can browse example classes <tt>lib/z_time_*.rb</tt> and the controller helper methods in <tt>lib/scheduled_resource/helper.rb</tt> ## Testing This gem also provides for a basic test application using angularjs to display a minimal but functional schedule showing just the day and hour headers in two different timezones (US Pacific and Eastern). Proceed as follows, starting with a fresh Rails app: $ rails new test_sr As above, add the gem to the Gemfile, then $ cd test_sr $ bundle $ schedulize . Add lines such as these to <tt>config/routes.rb</tt> get "/schedule/index" => "schedule#index" get "/schedule" => "schedule#schedule" Copy / merge these files from the gem source into the test app: $SR_SRC/app/views/layouts/application.html.erb $SR_SRC/app/views/schedule/index.html.erb $SR_SRC/app/assets/javascripts/{angular.js,script.js,controllers.js} and add <tt>//= require angular</tt> to application.js just below the entries for <tt>jquery</tt>. After you run the server and browse to http://0.0.0.0:3000/schedule/index you should see the four time-header rows specified by the sample config file. ## More Examples A better place to see the use of this gem is at [tv4](https://github.com/emeyekayee/tv4). Specifically, models <tt>app/models/event.rb</tt> and <tt>app/models/station.rb</tt> give better examples of implementing the ScheduledResource protocol and adapting to a db schema organized along somewhat different lines. ## Contributing 1. Fork it ( https://github.com/emeyekayee/scheduled_resource/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request
== What's this? {ComicFury}[https://comicfury.com] is an excellent no-bullshit webcomic hosting site created and maintained by the legend Kyo. You should support them on {Patreon}[https://www.patreon.com/comicfury]! {Jekyll}[https://jekyllrb.com] is a highly regarded and widespread static site generator. It builds simple slowly-changing content into HTML files using templates. RageRender allows you to use your ComicFury templates to generate a static version of your webcomic site using Jekyll. You just supply your templates, comics and blogs, and RageRender will output a site that mimics your ComicFury site. Well, I say "mimics". Output is a static site, which means all of the interactive elements of ComicFury don't work. This includes comments, subscriptions, search, and comic management. === But why?! RageRender allows those of us who work on making changes to ComicFury site templates to test our changes before we put them live. With RageRender, you can edit your CSS, HTML templates and site settings before you upload them to ComicFury. This makes the process of testing changes quicker and makes it much more likely that you catch mistakes before any comic readers have a chance to see them. RageRender doesn't compete with the most excellent ComicFury (who's Patreon you should contribute to, as I do!) – you should continue to use ComicFury for all your day-to-day artistic rage management needs. But if you find yourself making changes to a site design, RageRender may be able to help you. == Getting started First, you need to have {Ruby}[https://www.ruby-lang.org/] and {Bundler}[https://bundle.io/] installed. The Jekyll site has {good guides on how to do that}[https://jekyllrb.com/docs/installation/] depending on your operating system. To set up a new site, open a terminal and type: mkdir mycomic && cd mycomic bundle init bundle add jekyll bundle add ragerender --group=jekyll_plugins Now you can add comics! Add the image into an <tt>images</tt> folder: mkdir images cp 'cool comic.jpg' 'images/My first page.jpg' The file name of the image will be the title of your comic page. And that's it, you added your first comic! If you want to add an author note, create a text file in a folder called <tt>_comics</tt> that has the same file name, but with a <tt>.txt</tt> extension: mkdir _comics echo "Check out my cool comic y'all!" > '_comics/My first page.txt' Or use HTML: echo "This is my <strong>first</strong> page!" > '_comics/My first page.html' Generate the site using: bundle exec jekyll build Or start a local website to see it in your browser: bundle exec jekyll serve # Now visit http://localhost:4000! === Customising your site You'll notice a few things that might be off about your site, including that the webcomic title and author name are probably not what you were expecting. You can create a configuration file to tell RageRender the important details. Put something like this in your webcomic folder and call it <tt>_config.yml</tt>: title: "My awesome webcomic!" slogan: "It's the best!" description: > My epic story about how him and her fell into a romantic polycule with they and them status: active genres: - Comedy - Romance defaults: - scope: path: '' values: author: "John smith" theme: ragerender Your webcomic now has its basic information set up. === Adding your layouts If you want to use your own layout code, then create a <tt>_layouts</tt> directory and put the contents of each of your ComicFury layout tabs in there, and then put your CSS in the main folder. The easiest way is to go to your Webcomic Management, click "Edit Layout", then in the box labelled "Useful", click "Download Layout Backup". Pass this file to RageRender, which will <tt>unpack</tt> it for you: bundle exec jekyll unpack mycomic-2025-09-13.cflxml You should end up with a full set of files like: _layouts archive.html blog-archive.html blog-display.html comic-page.html error-page.html overall.html overview.html search.html layout.css Now when you build your site, your custom templates and styles will be used instead. === Adding blogs Add your blogs into a folder called <tt>_posts</tt>: cat _posts/2025-05-29-my-new-comic.md Hey guys, welcome to my new comic! It's gonna be so sick! Note that the name of your blog post has to include the date and the title, or it'll be ignored. === Customising comics and blogs You can add {Front Matter}[https://jekyllrb.com/docs/front-matter/] to set the details of your author notes and blogs manually: --- title: "spooky comic page" date: "2025-03-05 16:20" image: "images/ghost.png" author: "Jane doe" description: "Some spooky mouseover text" keywords: [excellent, comic page, spooky] custom: # use yes and no for tickbox settings spooky: yes # use text in quotes for short texts mantra: "live long and prosper" # use indented text for long texts haiku: > Testing webcomics Now easier than ever Thanks to RageRender transcript: > The transcript contains a machine-readable version of all the text in your comic image. comments: - author: "Skippy" date: "13 Mar 2025, 3.45 PM" comment: "Wow this is so sick!" --- Your author note still goes at the end, like this! === Adding extra pages You can add extra pages just by adding new HTML files to your webcomic folder. The name of the file becomes the URL that it will use. Pages by default won't be embedded into your 'Overall' layout. You can change that and more with optional Front Matter: --- # Include this line to set the page title title: "Bonus content" # Include this line to hide the page from the navigation menu hidden: yes # Include this line to embed this page in the overall layout layout: Overall --- <h1>yo check out my bonus content!</h1> === Controlling the front page As on ComicFury you have a few options for setting the front page of you site. You control this by setting a <tt>frontpage</tt> key in your site config. - <tt>latest</tt> will display the latest comic (also the default) - <tt>first</tt> will display the first comic - <tt>chapter</tt> will display the first comic in the latest chapter - <tt>blog</tt> will display the list of blog posts - <tt>archive</tt> will display the comic archive - <tt>overview</tt> will display the comic overview (blogs and latest page) - anything else will display the extra page that has the matching <tt>slug</tt> in its Front Matter === Comics with custom HTML code You can use custom HTML code in place of an image for your comic page. Instead of creating an image, just create an HTML file in your <tt>images</tt> folder: cat '<video src="/files/my-animation.webm"></video>" > images/1.html === Multi-image comics You can add up to 12 images to each comic page on ComicFury. To do that in RageRender, add each image to an <tt>images</tt> key in your comic page: --- title: "Comic with many pages" date: "2026-04-20 16:20" images: - /images/first.png - /images/second.png - /images/third.png --- === Testing search pages Live search does not work in RageRender, as your site is statically built and can't respond to new data from the browser. However, you can simulate a search when you build the site to help test search results designs. To do that, add a `searchterm` to the search page using defaults in your `_config.yml`: defaults: - scope: path: '' layout: search values: searchterm: "my character" The search that gets performed will be somewhat similar to how ComicFury will search your comic, but may not be exactly the same. === Putting changes on ComicFury Once you're done making changes, you can <tt>pack</tt> your layout: bundle exec jekyll pack The resulting file can be uploaded to ComicFury by going to your Webcomic Management, clicking "Edit Layout", then in the box labelled "Useful", click "Restore Layout Backup". === Stuff that doesn't work Here is a probably incomplete list of things you can expect to be different about your local site compared to ComicFury: - Any comments you specify in Front Matter will be present, but you can't add new ones - Search doesn't do anything at all - Saving and loading your place in the comic isn't implemented - GET and POST variables in templates are ignored and will always be blank - Random numbers in templates will be random only once per site build, not once per page call == Without Jekyll RageRender can also be used without Jekyll to turn ComicFury templates into templates in other languages. E.g: gem install ragerender echo "[c:iscomicpage]<div>[f:js|v:comictitle]</div>[/]" > template.html ruby $(gem which ragerender/to_liquid) template.html # {% if iscomicpage %}<div>{{ comictitle | escape }}</div>{% endif %} ruby $(gem which ragerender/to_erb) template.html # <% if iscomicpage %><div><%= js(comictitle) %></div><% end %> You still need to pass the correct variables to these templates; browse {this unofficial documentation}[https://github.com/heyeinin/comicfury-documentation] or RageRender::ComicDrop etc. to see which variables work on which templates. == Get help That's not a proclamation but an invitation! Reach out if you're having trouble by {raising an issue}[https://github.com/simonwo/ragerender/issues] or posting in the ComicFury forums.
== Confidently Build Terminal Apps Rooibos[https://rooibos.run] helps you build interactive terminal applications. Keep your code understandable and testable as it scales. Rooibos handles keyboard, mouse, and async work so you can focus on behavior and user experience. gem install rooibos <i>Currently in beta. APIs may change before 1.0.</i> === Get Started in Seconds rooibos new my_app cd my_app rooibos run That's it. You have a working app with keyboard navigation, mouse support, and clickable buttons. Open <tt>lib/my_app.rb</tt> to make it your own. --- === The Pattern \Rooibos uses Model-View-Update, the architecture behind Elm[https://guide.elm-lang.org/architecture/], Redux[https://redux.js.org/], and {Bubble Tea}[https://github.com/charmbracelet/bubbletea]. State lives in one place. Updates flow in one direction. The runtime handles rendering and runs background work for you. --- === Hello, MVU The simplest \Rooibos app. Press any key to increment the counter. Press <tt>Ctrl</tt>+<tt>C</tt> to quit. require "rooibos" module Counter # Init: How do you create the initial model? Init = -> { 0 } # View: What does the user see? View = -> (model, tui) { tui.paragraph(text: <<~END) } Current count: #{model}. Press any key to increment. Press Ctrl+C to quit. END # Update: What happens when things change? Update = -> (message, model) { if message.ctrl_c? Rooibos::Command.exit elsif message.key? model + 1 end } end Rooibos.run(Counter) That's the whole pattern: Model holds state, Init creates it, View renders it, and Update changes it. The runtime handles everything else. --- === Your First Real Application A file browser in sixty lines. It opens files, navigates directories, handles errors, styles directories and hidden files differently, and supports vim-style keyboard shortcuts. If you can do this much with this little code, imagine how easy _your_ app will be to build. require "rooibos" module FileBrowser # Model: What state does your app need? Model = Data.define(:path, :entries, :selected, :error) Init = -> { path = Dir.pwd entries = Entries[path] Ractor.make_shareable( # Ensures thread safety Model.new(path:, entries:, selected: entries.first, error: nil)) } View = -> (model, tui) { tui.block( titles: [model.error || model.path, { content: KEYS, position: :bottom, alignment: :right}], borders: [:all], border_style: if model.error then tui.style(fg: :red) else nil end, children: [tui.list(items: model.entries.map(&ListItem[model, tui]), selected_index: model.entries.index(model.selected), highlight_symbol: "", highlight_style: tui.style(modifiers: [:reversed]))] ) } Update = -> (message, model) { return model.with(error: ERROR) if message.error? model = model.with(error: nil) if model.error && message.key? if message.ctrl_c? || message.q? then Rooibos::Command.exit elsif message.home? || message.g? then model.with(selected: model.entries.first) elsif message.end? || message.G? then model.with(selected: model.entries.last) elsif message.up_arrow? || message.k? then Select[:-, model] elsif message.down_arrow? || message.j? then Select[:+, model] elsif message.enter? then Open[model] elsif message.escape? then Navigate[File.dirname(model.path), model] end } private # Lines below this are implementation details KEYS = "↑/↓/Home/End: Select | Enter: Open | Esc: Navigate Up | q: Quit" ERROR = "Sorry, opening the selected file failed." ListItem = -> (model, tui) { -> (name) { modifiers = name.start_with?(".") ? [:dim] : [] fg = :blue if name.end_with?("/") tui.list_item(content: name, style: tui.style(fg:, modifiers:)) } } Select = -> (operator, model) { new_index = model.entries.index(model.selected).public_send(operator, 1) model.with(selected: model.entries[new_index.clamp(0, model.entries.length - 1)]) } Open = -> (model) { full = File.join(model.path, model.selected.delete_suffix("/")) model.selected.end_with?("/") ? Navigate[full, model] : Rooibos::Command.open(full) } Navigate = -> (path, model) { entries = Entries[path] model.with(path:, entries:, selected: entries.first, error: nil) } Entries = -> (path) { Dir.children(path).map { |name| File.directory?(File.join(path, name)) ? "#{name}/" : name }.sort_by { |name| [name.end_with?("/") ? 0 : 1, name.downcase] } } end Rooibos.run(FileBrowser) --- === Batteries Included ==== Commands Applications fetch data, run shell commands, and set timers. \Rooibos Commands run off the main thread and send results back as messages. <b>HTTP requests:</b> Update = -> (message, model) { case message in :fetch_users [model.with(loading: true), Rooibos::Command.http(:get, "/api/users", :got_users)] in { type: :http, envelope: :got_users, status: 200, body: } model.with(loading: false, users: JSON.parse(body)) in { type: :http, envelope: :got_users, status: } model.with(error: "HTTP #{status}") end } <b>Shell commands:</b> Update = -> (message, model) { case message in :list_files Rooibos::Command.system("ls -la", :listed_files) in { type: :system, envelope: :listed_files, stdout:, status: 0 } model.with(files: stdout.lines.map(&:chomp)) in { type: :system, envelope: :listed_files, stderr:, status: } model.with(error: stderr) end } <b>Timers:</b> Update = -> (message, model) { case message in { type: :timer, envelope: :tick, elapsed: } [model.with(frame: model.frame + 1), Rooibos::Command.wait(1.0 / 24, :tick)] end } <b>And more!</b> \Rooibos includes <tt>all</tt>, <tt>batch</tt>, <tt>bubble</tt>, <tt>cancel</tt>, <tt>custom</tt>, <tt>deliver</tt>, <tt>exit</tt>, <tt>http</tt>, <tt>map</tt>, <tt>open</tt>, <tt>system</tt>, <tt>tick</tt>, and <tt>wait</tt> commands. You can also define your own custom commands for complex orchestration. Every command produces a message, and Update handles it the same way. ==== Testing \Rooibos makes TUIs so easy to test, you'll save more time by writing tests than by not testing. <b>Unit test Update, View, and Init.</b> No terminal needed. Test helpers included. def test_moves_selection_down_with_j model = Ractor.make_shareable(FileBrowser::Model.new( path: "/", entries: %w[bin exe lib], selected: "bin", error: nil)) message = RatatuiRuby::Event::Key.new(code: "j") result = FileBrowser::Update.call(message, model) assert_equal "exe", result.selected end <b>Style assertions.</b> Draw to a headless terminal, verify colors and modifiers. def test_directories_are_blue with_test_terminal(60, 10) do model = Ractor.make_shareable(FileBrowser::Model.new( path: "/", entries: %w[file.txt subdir/], selected: "file.txt", error: nil)) widget = FileBrowser::View.call(model, RatatuiRuby::TUI.new) RatatuiRuby.draw { |frame| frame.render_widget(widget, frame.area) } assert_blue(1, 2) # "subdir/" at column 1, row 2 end end <b>System tests.</b> Inject events, run the full app, snapshot the result. def test_selection_moves_down with_test_terminal(120, 30) do Dir.mktmpdir do |dir| FileUtils.touch(File.join(dir, "a")) FileUtils.touch(File.join(dir, "b")) FileUtils.touch(File.join(dir, "c")) inject_key(:down) inject_key(:ctrl_c) # Tests use explicit params to inject deterministic initial state. Rooibos.run( model: Ractor.make_shareable(FileBrowser::Model.new( path: dir, entries: %w[a b c], selected: "a", error: nil)), view: FileBrowser::View, update: FileBrowser::Update ) assert_snapshots("selection_moved_down") do |lines| title = "┌/tmp/test#{'─' * 107}┐" lines.map do |l| l.gsub(/┌#{Regexp.escape(dir)}[^┐]*┐/, title) end end end end end Snapshots record both plain text and ANSI colors. Normalization blocks mask dynamic content (timestamps, temp paths) for cross-platform reproducibility. Run <tt>UPDATE_SNAPSHOTS=1 rake test</tt> to regenerate baselines. ==== Scale Up Large applications decompose into fragments. Each fragment has its own Model, View, Update, and Init. Parents compose children. The pattern scales. The Router DSL eliminates boilerplate: module Dashboard include Rooibos::Router route :stats, to: StatsPanel route :network, to: NetworkPanel receive_events :ctrl_c, -> { Rooibos::Command.exit } only when: -> (_message, model) { !model.modal_open } do receive_events :q, -> { Rooibos::Command.exit } forward_events :s, to: :stats, as: :fetch forward_events :p, to: :network, as: :ping end Update = from_router # ... Model, Init, View below end Declare routes and event handlers. The router generates Update for you. Use guards to ignore messages when needed. ==== CLI The <tt>rooibos</tt> command scaffolds projects and runs applications. rooibos new my_app # Generate project structure rooibos run # Run the app in current directory Generated apps include tests, type signatures, and a working welcome screen with keyboard and mouse support. --- === The Ecosystem \Rooibos builds on RatatuiRuby[https://www.ratatui-ruby.dev], a Rubygem built on Ratatui[https://ratatui.rs]. You get native performance with the joy of Ruby. \Rooibos is one way to manage state and composition. Kit is another. ==== Rooibos[https://www.rooibos.run] Model-View-Update architecture. Inspired by Elm, Bubble Tea, and React + Redux. Your UI is a pure function of state. - Functional programming with MVU - Commands work off the main thread - Messages, not callbacks, drive updates ==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Coming Soon) Component-based architecture. Encapsulate state, input handling, and rendering in reusable pieces. - OOP with stateful components - Separate UI state from domain logic - Built-in focus management & click handling Both use the same widget library and rendering engine. Pick the paradigm that fits your brain. --- === Links [Get Started] {Getting Started}[https://www.rooibos.run/docs/trunk/doc/getting_started/index_md.html], {Tutorial}[https://www.rooibos.run/docs/trunk/doc/tutorial/index_md.html], {Examples}[https://www.rooibos.run/docs/trunk/examples/app_fractal_dashboard/README_md.html] [Coming From...] {React/Redux}[https://www.rooibos.run/docs/trunk/doc/getting_started/for_react_developers_md.html], {BubbleTea}[https://www.rooibos.run/docs/trunk/doc/getting_started/for_go_developers_md.html], {Textual}[https://www.rooibos.run/docs/trunk/doc/getting_started/for_python_developers_md.html] [Learn More] {Essentials}[https://www.rooibos.run/docs/trunk/doc/essentials/index_md.html], {Scaling Up}[https://www.rooibos.run/docs/trunk/doc/scaling_up/index_md.html], {Best Practices}[https://www.rooibos.run/docs/trunk/doc/best_practices/index_md.html], {Troubleshooting}[https://www.rooibos.run/docs/trunk/doc/troubleshooting/index_md.html] [Community] {Forum}[https://forum.setdef.com/c/rooibos], {Announcements}[https://forum.setdef.com/tags/c/rooibos/announcement], {Bug Tracker}[https://forum.setdef.com/tags/c/rooibos/bug], {Contribution Guide}[https://github.com/setdef/Rooibos/blob/trunk/CONTRIBUTING.md], {Code of Conduct}[https://github.com/setdef/Rooibos/blob/trunk/CODE_OF_CONDUCT.md] --- [Website] https://rooibos.run [Source] https://github.com/setdef/Rooibos [RubyGems] https://rubygems.org/gems/rooibos © 2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets: MIT-0
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.