GithubHelp home page GithubHelp logo

ratatouille's Introduction

Ratatouille

Build Status Module Version Hex Docs Total Download License Last Updated

Ratatouille is a declarative terminal UI kit for Elixir for building rich text-based terminal applications similar to how you write HTML.

It builds on top of the termbox API (using the Elixir bindings from ex_termbox).

For the API Reference, see: https://hexdocs.pm/ratatouille.

Toby Toby, a terminal-based Erlang observer built with Ratatouille

Table of Contents

Getting Started

Ratatouille implements The Elm Architecture (TEA) as a way to structure application logic. This fits quite naturally in Elixir and is part of what makes Ratatouille declarative. If you've already used TEA on the web, this should feel very familiar.

As with a GenServer definition, Ratatouille apps only implement a behaviour by defining callbacks and don't know how to start or run themselves. It's the application runtime that handles all of those (sometimes tricky) details.

Building an Application

Let's build a simple application that displays an integer counter which can be incremented when the user presses + and decremented when the user presses -.

First a quick clarification, since we're using the word "application" a lot. For our purposes, an application is a terminal application, and not necessarily an OTP application, but your terminal application could also be an OTP application. We'll cover that in Packaging and Distributing Applications below.

Back to the counter app. First we'll look at the entire example, then we'll go through it line by line to see what each line does. You can also find this example in the repo and run it with mix run.

# examples/counter.exs

defmodule Counter do
  @behaviour Ratatouille.App

  import Ratatouille.View

  def init(_context), do: 0

  def update(model, msg) do
    case msg do
      {:event, %{ch: ?+}} -> model + 1
      {:event, %{ch: ?-}} -> model - 1
      _ -> model
    end
  end

  def render(model) do
    view do
      label(content: "Counter is #{model} (+/-)")
    end
  end
end

Ratatouille.run(Counter)

At the top, we define a new module (Counter) for the app and we inform Elixir that it will implement the Ratatouille.App behaviour. This just ensures we're warned if we forget to implement a callback and serves as documentation that this is a Ratatouille app.

defmodule Counter do
  @behaviour Ratatouille.App

  # ...
end

Next, we import the View DSL from Ratatouille.View:

import Ratatouille.View

The View DSL provides element builder functions like view, row, table, label that you can use to define views. Think of them like HTML tags.

init/1

The init/1 callback defines the initial model. "Model" is the Elm architecture's term for what we often call "state" in Elixir/Erlang. As with a GenServer, the state (our model) will later be passed to callbacks when things happen in order to allow the app to update it.

The model can be any Erlang term. For larger apps, it's helpful to use maps or structs to organize different pieces of the state. Here, we just have an integer counter, so we return 0 as our initial model:

defmodule Counter do
  # ...

  def init(_context), do: 0

  # ...
end

update/2

The update/2 callback defines how to transform the model when a particular message is received. Ratatouille's runtime will automatically call update/2 when terminal events occur (pressing a key, resizing the window, clicking the mouse, etc.). We can also send ourselves messages via subscriptions and commands.

Here, we'd like to increment the counter when we get a ?+ key press and decrement it when get a ?-. Event messages are based on the underlying termbox events and characters are given as code points (e.g., ?a is 97).

defmodule Counter do
  # ...

  def update(model, msg) do
    case msg do
      {:event, %{ch: ?+}} -> model + 1
      {:event, %{ch: ?-}} -> model - 1
      _ -> model
    end
  end

  # ...
end

It's a good idea to provide a fallback clause in case we don't know how to handle a message. This way the app won't crash if the user presses a key that the app doesn't handle. But if things stop working as you expect, try removing the fallback to see if important messages are going unmatched.

render/1

The render/1 callback defines a view to display the model. The runtime will call it as needed when it needs to update the terminal window.

Like an HTML document, a view is defined as a tree of elements (nodes). Elements have attributes (e.g., text: bold) and children (nested content). While helper functions can return arbitrary element trees, the render/1 callback must return a view tree starting with a root view element---it's sort of like the <body> tag in HTML.

defmodule Counter do
  # ...

  def render(model) do
    view do
      label(content: "Counter is #{model} (+/-)")
    end
  end

  # ...
end

Running it

There's a final and very important line at the bottom:

Ratatouille.run(Counter)

This starts the application runtime with our app definition. Options can be passed as a second argument. This is an easy way to run simple apps. For more complicated ones, it's recommended to define an OTP application.

That's it---now you can run the program with mix run <file>. To run the bundled example:

$ mix run examples/counter.exs

You should see the counter we defined, be able to make changes to it with + and -, and be able to quit using q.

Views

Ratatouille's views are trees of elements similar to HTML in structure. For example, here's how to define a two-column layout:

view do
  row do
    column size: 6 do
      panel title: "Left Column" do
        label(content: "Text on the left")
      end
    end

    column size: 6 do
      panel title: "Right Column" do
        label(content: "Text on the right")
      end
    end
  end
end

The DSL

As you might have noticed, Ratatouille provides a small DSL on top of Elixir for defining views. These are functions and macros which accept attributes and/or child elements in different formats. For example, a column element can be defined in all of the following ways:

column()

column(size: 12)

column do
  # ... child elements ...
end

column size: 12 do
  # ... child elements ...
end

All of these evaluate to a %Ratatouille.Renderer.Element{tag: :column} struct. The macros provide syntactic sugar, but under the hood it's all structs.

Here's a list of all the elements provided by Ratatouille.View:

Element Description
bar Block-level element for creating title, status or menu bars
canvas A free-form canvas for drawing arbitrary shapes
canvas_cell A canvas cell which represents one square of the canvas
chart Element for plotting a series as a multi-line chart
column Container occupying a vertical segment of the grid
label Block-level element for displaying text
overlay Container overlaid on top of the view
panel Container with a border and title used to demarcate content
row Container used to define grid layouts with one or more columns
sparkline Element for plotting a series in a single line
table Container for displaying data in rows and columns
table_cell Element representing a table cell
table_row Container representing a row of the table
text Inline element for displaying uniformly-styled text
tree Container for displaying data as a tree of nodes
tree_node Container representing a tree node
view Top-level container

Adding Logic

Because it's just Elixir code, you can freely mix in Elixir syntax and abstract views using functions:

label(content: a_variable)

view do
  case current_tab do
    :one -> render_tab_one()
    :two -> render_tab_two()
  end
end
if window.width > 80 do
  row do
    column(size: 6)
    column(size: 6)
  end
else
  row do
    column(size: 12)
  end
end

Styling

Attributes are used to style text and other content:

# Labels are block-level, so this makes text within the whole block red.
label(content: "Red text", color: :red)

# Nested inline text elements can be used to style differently within a label.
label do
  text(content: "R", color: :red)
  text(content: "G", color: :green)
  text(content: "B", color: :blue)
end

# `color` sets the foreground, while `background` sets the background.
label(content: "Black on white", color: :black, background: :white)

# `attributes` accepts a list of text attributes, here `:bold` and `:underline`.
label(content: "Bold and underlined text", attributes: [:bold, :underline])

Styling is still being developed, so it's not currently possible to style every aspect of every element, but this will improve with time.

Views are Strict

Most web browsers will happily try to make sense of any HTML you give them. For example, you can put a td directly under a div and the content will likely still be rendered.

Ratatouille takes a different, more strict approach and first validates that the view tree is well-structured. If it's not valid, an error is raised explaining the problem. This is intended to provide quick feedback when something's wrong. Restricting the set of valid views also helps to simplify the rendering implementation.

It's helpful to keep the following things in mind when defining views:

  • Each tag has a list of allowed child tags. For example, a row may only have elements with the column tag as direct descendants.
  • Each tag has a list of attributes. Some attributes are required, and these must be set. Optional attributes have some default behavior when unset. It's not allowed to set an attribute that's not in the list.
  • A view element must be the root element of any view tree you'd like to render.

See the list of elements above for documentation on each element.

Example Applications

The following examples show off different aspects of the framework:

Name Description
rendering.exs A rendering demo of all the supported elements
counter.exs How to create a simple app with state and updates
editor.exs How to use receive and display user input
multiple_views.exs How to render different views/tabs based on a selection
subscriptions.exs How to subscribe to multiple intervals
commands.exs How to run commands asynchronously and receive the results
snake.exs How to make a simple game
documentation_browser.exs How to render and scroll multiline content

With the repository cloned locally, run an example with mix run examples/<example>.exs. Examples can be quit with q or CTRL-c (unless indicated otherwise).

Under the Hood

The application runtime abstracts away many of the details concerning how the terminal window is updated and how events are received. If you're interested in how these things actually work, or if the runtime doesn't support your use case, see this guide:

https://hexdocs.pm/ratatouille/under-the-hood.html

Packaging and Distributing

Warning: This part is still rough around the edges.

While it's easy to run apps while developing with mix run, packaging them for others to easily run is a bit more complicated. Depending on the type of app you're building, it might not be reasonable to assume that users have any Elixir or Erlang tools installed. Terminal apps are usually distributed as binary executables so that they can just be run as such without additional dependencies. Fortunately, this is possible using OTP releases that bundle ERTS.

Defining an OTP Application

In order to create an OTP release, we first need to define an OTP application that runs the terminal application. Ratatouille.Runtime.Supervisor takes care of starting all the necessary runtime components, so we start this supervisor under the OTP application supervisor and pass it a Ratatouille app definition (along with any other runtime configuration).

For example, the OTP application for toby looks like this:

defmodule Toby do
  use Application

  def start(_type, _args) do
    children = [
      {Ratatouille.Runtime.Supervisor, runtime: [app: Toby.App]},
      # other workers...
    ]

    Supervisor.start_link(
      children,
      strategy: :one_for_one,
      name: Toby.Supervisor
    )
  end
end

Executable Releases with Distillery

We'll use Distillery to create the OTP release, as it can even create distributable, self-contained executables. Releases built on a given architecture can generally be run on machines of the same architecture.

Follow the Distillery guide to generate a release configuration:

https://hexdocs.pm/distillery/introduction/installation.html

In order to make a "batteries-included" release, it's important that you have include_erts set to true:

environment :prod do
  # ...
  set(include_erts: true)
  # ...
end

Now it's possible to generate the release:

MIX_ENV=prod mix release --executable --transient

This creates a Distillery release that bundles the Erlang runtime and the application. Start it in the foreground, e.g.:

_build/prod/rel/toby/bin/toby.run foreground

You can also move this executable somewhere else (e.g., to a directory in your $PATH). A current caveat is that it must be able to unpack itself, as Distillery executables are self-extracting archives.

Projects using Ratatouille

For inspiration or ideas on how to structure your application, check out this list of projects built with Ratatouille:

  • tefter/cli - the command-line client for Tefter
  • toby - a terminal-based Erlang observer

If you have a project you'd like to include here, just open a PR to add it to the list.

Installation

From Hex

Add Ratatouille as a dependency in your project's mix.exs:

def deps do
  [
    {:ratatouille, "~> 0.5.0"}
  ]
end

From Source

To try out the master branch, first clone the repo:

git clone https://github.com/ndreynolds/ratatouille.git
cd ratatouille

Next, fetch the deps:

mix deps.get

Finally, try out one of the included examples/:

mix run examples/rendering.exs

If you see lots of things drawn on your terminal screen, you're good to go. Use "q" to quit in the examples (unless otherwise specified).

Roadmap

  • Apps
    • Application Runtime
    • Subscriptions
    • Commands
  • Views / Rendering
    • Rendering engine with basic elements
    • More configurable charts (axis label, color, multiple lines, etc.)
    • Uniform support for text styling (incl. inheritance)
    • Automatic translation to termbox styling constants
      • For example, color: :red instead of color: Constants.color(:red).
    • Rendering optimizations (view diffing, more efficient updates, etc.)
  • Events
    • Translate termbox events to a cleaner format
      • Dealing with the integer constants is inconvenient. These could be converted to atoms by the event manager.
  • Terminal Backend
    • ex_termbox NIFs
    • Alternative port-based termbox backend
  • Customization
    • Registering custom element renderers
      • This would support using custom elements (e.g. my_table()) that are defined outside of the core library.

Contributing

Contributions are much appreciated. They don't necessarily have to come in the form of code, I'm also very thankful for bug reports, documentation improvements, questions, and suggestions.

Running the Tests

Run the unit tests as usual:

mix test

Ratatouille also includes integration tests of the bundled examples. These aren't included in the default suite because they actually run the example apps. The integration suite can be run like so:

mix test --only integration

Copyright and License

Copyright (c) 2018 Nick Reynolds

This software is released under the MIT License.

ratatouille's People

Contributors

albertored avatar fitblip avatar iboard avatar jvantuyl avatar kianmeng avatar ndreynolds avatar trescenzi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ratatouille's Issues

Dynamically retrieve dimensions of elements

As mentioned in #9 and the commit that closed the issue, the viewport element helps greatly with scrolling content, but it's limited by the lack of an ability to obtain child element dimensions.

This means that while we can render a table element with some amount of nested table_cells, we can't compare the rendered height of the table with the amount of table_cells.

This means that one can't easily do such a thing as calculating that with 100 table_cell elements in a 99 row high table, adjusting the viewport offset_y should only possibly go one down, and then only one up.

I'm aware there's a fair amount of corner cases that need to be accounted for (not to mention the general work necessary to implement the idea), so I'm opening this issue mostly to have a place to discuss what the requirements are for implementing this kind of feature.

Support text wrapping / multiline content

To display multiline content like a man page, markdown document, function documentation or a web page, it needs to be possible to provide a large string of text that may or may not contain line breaks, and have this text rendered such that it is wrapped to fit within the bounding rendering box (and not cut off).

Ratatouille should render multiline text in this way when directed. It should respect explicit line breaks and add additional breaks as necessary to prevent content from being cut off.

To showcase this, it would be interesting to build a small application to display Elixir documentation. Maybe a searchable version of IEx.Helpers.h/1.

Examples

man (via less) performs this sort of text reflowing:


DESCRIPTION
       grep  searches for PATTERN in each FILE.  A FILE of “-”
       stands for  standard  input.   If  no  FILE  is  given,
       recursive  searches  examine the working directory, and
       nonrecursive searches read standard input.  By default,
       grep prints the matching lines.

       In  addition,  the variant programs egrep and fgrep are
       the same as grep -E and grep -F,  respectively.   These
       variants  are deprecated, but are provided for backward
       compatibility.

64 columns


DESCRIPTION
       grep  searches  for PATTERN in
       each  FILE.   A  FILE  of  “-”
       stands for standard input.  If
       no FILE  is  given,  recursive
       searches  examine  the working
       directory,  and   nonrecursive
       searches  read standard input.
       By default,  grep  prints  the
       matching lines.

       In   addition,   the   variant
       programs egrep and  fgrep  are
       the   same   as   grep -E  and
       grep -F, respectively.   These
       variants  are  deprecated, but
       are  provided   for   backward
       compatibility.

39 columns

Redesigning the terminal event messages

Current Situtation

Currently, the runtime / event manager sends terminal events as messages to the application in the following format:

{:event, 
  %ExTermbox.Event{
    ch: 0, 
    h: 38, 
    key: 0, 
    mod: 0, 
    type: 2, 
    w: 147, 
    x: 0, 
    y: 0
  }}

The ExTermbox.Event struct is directly based off of the C struct from the termbox API, which uses integer codes for the event type, key (based on terminfo), modifier, character, etc.:

https://github.com/nsf/termbox/blob/master/src/termbox.h

So far, the recommended way to match events on a certain key has been to match the integer for that key by first looking up the constant, e.g.:

@ctrl_c Constants.key(:ctrl_c)

case message do
  {:event, %{key: @ctrl_c}} ->
    # handle ctrl-c keypress

This project evolved out of the termbox bindings library, so this event API made sense in the beginning.

Problems

Constants are clumsy

Looking up constants and storing them in an attribute just to pattern match on them feels clumsy and is an unnecessary extra step. It should be possible to simplify the above as such:

case message do
  {:event, %{key: :ctrl_c}} ->
    # handle ctrl-c keypress

However, one issue with this is that some keys are defined to have the same integer value. For example, ctrl-~ (tilde) and ctrl-2 are both set to 0x00. Which begs the question: which one do we set as key above?

Not integrated with views and rendering

So far, events are completely separate from the views. The "target" of an event is always the terminal. I think we can do better than that. If I click on a label element, it should be possible for the rendering library to figure out that I've clicked that element and expose that information via the event.

Union type has a lot of extraneous information

The event struct is really a sort of union type---each event has a different type and depending on the type, certain information will be filled in and other information will be blank. From a usability standpoint, this can be very confusing.

Ideas

Extended events

In order to (mostly) maintain backwards compatibility, we could take the existing ExTermbox.Event{} struct and extend it with computed information, e.g.:

%Ratatouille.Event{
  ch: 0,
  key: 1,
  key_name: :ctrl_a,
  mod: 0,
  mod_name: :alt,
  type: 1,
  type_name: :key,
  w: 0,
  h: 0,
  x: 0,
  y: 0,
  target: nil
}

AFAIK, it would only break code that explicitly matches the struct (vs. matching any map with the keys).

New event structs by type (Breaking change)

Another approach would be to totally overhaul it in a new major version:

%Ratatouille.KeyEvent{
  key: :a,
  mod: :alt,
  key_code: 0,
  character_code: 97
}

%Ratatouille.MouseEvent{
  x: 21,
  y: 56,
  target: %Element{tag: :label, ...}
}

%Ratatouille.ResizeEvent{
  width: 45,
  height: 21
}

These would also be sent as messages in a new style:

{:key, %KeyEvent{}}
{:mouse, %MouseEvent{}}
{:resize, %ResizeEvent{}}

Other Ideas ???

Next Steps

I'd like to get some feedback and let the problem simmer for a while before I start changing anything.

Screenshots in readme

There are none.

I will run the examples today anyway, I could just quickly make some screenshots and add them to the readme.

Would you be interested or do you think that stuff will change soon and it would be a burden?

Move the cursor on the interface

Hi,
Thanks for this tool.
Can we (or could we) handle cursor movement? In the documentation_browser for instance, using the arrow keys do update the right panel and the selected module appears with a right arrow sign (>) before it, but the cursor itself is at the very bottom of the window. Perhaps I'm more sensitive than others to it, because as a screen reader user, it's necessary to move line by line toward the top of the window each time a key is pressed (the screen reader focuses on the system cursor by default). In this kind of situation, it would be great if the system cursor were placed on the selected element.
I see several ways to do it. A "focused" option could be added to the DSL elements, though it might be misleading as only one visible element on the current screen could be focused and this wouldn't allow to handle all situations (for instance, in the documentation browser, we might want the cursor to be on the first character of the title and not on the > sign, nor at the end of the line).
Is that already implemented and I missed it somehow? Is there a current workaround even if it's not implemented?
Thanks again,
Vincent

Support offsetting content

In order to implement scrolling in a UI, it should be possible to render content such that it's offset by some number of rows and/or columns.

Examples

Horizontal scrolling inside a table

The major use case I have in mind is horizontal scrolling within a table. For example, take the following table:

│ PID           Name or Initial Func                       Reds    Memory  MsgQ  Current Functio │
│ #PID<0.0.0>   init                                       3730    21680   0     init:boot_loop/ │
│ #PID<0.1.0>   erts_code_purger                           22934   26864   0     erts_code_purge │
│ #PID<0.2.0>   erts_literal_area_collector:start/0        22076   2688    0     erts_literal_ar │
│ #PID<0.3.0>   erts_dirty_process_signal_handler:start/0  575     2688    0     erts_dirty_proc │
│ #PID<0.4.0>   erts_dirty_process_signal_handler:start/0  46      2688    0     erts_dirty_proc │

The table will be further truncated whenever the screen width does not accomodate rendering additional columns, e.g.:

│ PID           Name or Initial Func                       R |
│ #PID<0.0.0>   init                                       3 |
│ #PID<0.1.0>   erts_code_purger                           2 |
│ #PID<0.2.0>   erts_literal_area_collector:start/0        2 |
│ #PID<0.3.0>   erts_dirty_process_signal_handler:start/0  5 |
│ #PID<0.4.0>   erts_dirty_process_signal_handler:start/0  4 |

It should be possible to change where the rendering starts by adding an offset on the x-axis. Here the offset is two columns of the terminal, meaning we've scrolled two columns to the right:

│ D           Name or Initial Func                       Red |
│ ID<0.0.0>   init                                       373 |
│ ID<0.1.0>   erts_code_purger                           229 |
│ ID<0.2.0>   erts_literal_area_collector:start/0        220 |
│ ID<0.3.0>   erts_dirty_process_signal_handler:start/0  575 |
│ ID<0.4.0>   erts_dirty_process_signal_handler:start/0  46  |

We can continue scrolling right by increasing the x offset to four columns:

│           Name or Initial Func                       Reds  │
│ <0.0.0>   init                                       3730  │
│ <0.1.0>   erts_code_purger                           22934 │
│ <0.2.0>   erts_literal_area_collector:start/0        22076 │
│ <0.3.0>   erts_dirty_process_signal_handler:start/0  575   │
│ <0.4.0>   erts_dirty_process_signal_handler:start/0  46    │

less/more-style pager

Similar to how we've applied an x-axis offset above, it can be helpful to apply an offset to the y-axis:

GREP(1)                 General Commands Manual                 GREP(1)

NAME
       grep, egrep, fgrep - print lines matching a pattern

SYNOPSIS
       grep [OPTIONS] PATTERN [FILE...]
       grep [OPTIONS] -e PATTERN ... [FILE...]
       grep [OPTIONS] -f FILE ... [FILE...]

DESCRIPTION
       grep  searches  for  PATTERN in each FILE.  A FILE of “-” stands
       for standard input.  If no FILE  is  given,  recursive  searches
       examine  the  working  directory, and nonrecursive searches read
       standard input.  By default, grep prints the matching lines.

With 2 rows of offset applied:

NAME
       grep, egrep, fgrep - print lines matching a pattern

SYNOPSIS
       grep [OPTIONS] PATTERN [FILE...]
       grep [OPTIONS] -e PATTERN ... [FILE...]
       grep [OPTIONS] -f FILE ... [FILE...]

DESCRIPTION
       grep  searches  for  PATTERN in each FILE.  A FILE of “-” stands
       for standard input.  If no FILE  is  given,  recursive  searches
       examine  the  working  directory, and nonrecursive searches read
       standard input.  By default, grep prints the matching lines.

       In addition, the variant programs egrep and fgrep are  the  same
       as  grep -E  and  grep -F,  respectively.   These  variants  are

Contrasting with Scrolling on the Web

While HTML defines a structured document, it makes few prescriptions as to how the content is actually rendered. In the case of a scrollbar, it's the web browser that ultimately decides to show a scrollbar based on directives from CSS and whether or not the content would overflow the allotted rendering region. How the browser's viewport looks at any given moment depends on the HTML, the CSS, running scripts, and the browser's own internal state and environment.

Ratatouille provides both a language for defining such a structured document and a rendering engine for rendering the document. Rendering in Ratatouille is a pure function. Rendering the same document onto the same canvas should always produce an identical resulting canvas. Whether the resulting canvas is then output to a terminal or as a string is just a small implementation detail. Unlike the browser, Ratatouille never directly changes what's rendered in response to user input. Rather, it only provides a pattern for it---an application needs to define a new document in response to the input event, and Ratatouille will render this new document. In this way, rendering and event handling are decoupled in Ratatouille.

This also ties back into scrolling. Since rendering in Ratatouille is just a pure function of a canvas and a document, any rendering directives (height, width, color, etc.) would need to be stored within the canvas (which stores dimensions and cells) or the document. Unlike the document, the canvas isn't hierarchical---it's only top-level information. That means we could store a scroll offset on the top-level, but not for nested elements such as a table within a tab pane.

Implementation

I think this means we need to somehow encode the offsets in the document itself, e.g.

table(offset_x: 5) do
  table_row do
    table_cell(content: "foo")
  end
end

A more elegant approach could be to define a container to handle offseting arbitrary child elements:

viewport(offset_x: 5, offset_y: 3)
  table do
    table_row do
      table_cell(content: "foo")
    end
  end
end

The rendering logic for the viewport element could initially be relatively simple. E.g., given a canvas with a rendering region of 100 columns by 40 rows, and a desired x-offset of 5 and y-offset of 3:

  1. Make a copy of the canvas with no cells and an adjusted rendering region of 105 columns and 43 rows.
  2. Render the viewport's content onto this copy.
  3. Shift all of the rendered cells in the copy by -5 columns and -3 rows.
  4. Merge the copy with the original canvas and return the result.

label doesn't accept color attribute?

19:42:59.285 [error] Error in application loop:
  ** (MatchError) no match of right hand side value: 
{:error, "Invalid attributes: 'label' does not accept attributes [:color]"}
    (ratatouille) lib/ratatouille/runtime.ex:123: Ratatouille.Runtime.loop/1
    (ratatouille) lib/ratatouille/runtime.ex:109: Ratatouille.Runtime.run/1
    (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

My code:

defmodule Counter do
  @behaviour Ratatouille.App

  import Ratatouille.View

  def init(_context), do: 0  

  def update(model, msg) do
    case msg do
      {:event, %{ch: ?+}} -> model + 1
      {:event, %{ch: ?-}} -> model - 1
      _ -> model
    end
  end

  def render(model) do
    view do
      label(content: "Counter is #{model} (+/-)", color: :green)
    end
  end

end

Ratatouille.run(Counter)

From the docs:

# Labels are block-level, so this makes text within the whole block red.
label(content: "Red text", color: :red)

Okay...so let's copy and paste the exact line from the docs into my code:

defmodule Counter do
  @behaviour Ratatouille.App

  import Ratatouille.View

  def init(_context), do: 0  

  def update(model, msg) do
    case msg do
      {:event, %{ch: ?+}} -> model + 1
      {:event, %{ch: ?-}} -> model - 1
      _ -> model
    end
  end

  def render(_model) do
    view do
      label(content: "Red text", color: :red)
    end
  end

end

Ratatouille.run(Counter)

Same error:

9:54:27.071 [error] Error in application loop:
  ** (MatchError) no match of right hand side value: 
{:error, "Invalid attributes: 'label' does not accept attributes [:color]"}
    (ratatouille) lib/ratatouille/runtime.ex:123: Ratatouille.Runtime.loop/1
    (ratatouille) lib/ratatouille/runtime.ex:109: Ratatouille.Runtime.run/1
    (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

mix.exs:

  ...
  ...
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      {:ratatouille, "~> 0.4.0"}
    ]
  end

I get the same error when I try a background style:

  def render(_model) do
    view do
      #label(content: "Counter is #{model} (+/-)")
      label(content: "Red text", background: :green)
    end
  end

error:

0:39:51.208 [error] Error in application loop:
  ** (MatchError) no match of right hand side value: 
{:error, "Invalid attributes: 'label' does not accept attributes [:background]"}
    (ratatouille) lib/ratatouille/runtime.ex:123: Ratatouille.Runtime.loop/1
    (ratatouille) lib/ratatouille/runtime.ex:109: Ratatouille.Runtime.run/1
    (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

Same thing with a panel instead of a label:

defmodule Counter do
  @behaviour Ratatouille.App

  import Ratatouille.View

  def init(_context), do: 0  #like initial state of gen_server
                              #becomes initial value of `model`

  def update(model, msg) do
    case msg do
      {:event, %{ch: ?+}} -> model + 1
      {:event, %{ch: ?-}} -> model - 1
      _ -> model
    end
  end

  def render(_model) do
    view do
      #label(content: "Counter is #{model} (+/-)")
      
      panel(content: "Red text", background: :green)
    end
  end

end

Ratatouille.run(Counter)

error:

20:45:27.522 [error] Error in application loop:
  ** (MatchError) no match of right hand side value: 
{:error, "Invalid attributes: 'panel' does not accept attributes [:background, :content]"}
    (ratatouille) lib/ratatouille/runtime.ex:123: Ratatouille.Runtime.loop/1
    (ratatouille) lib/ratatouille/runtime.ex:109: Ratatouille.Runtime.run/1
    (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

Colored borders

Hi,
thanks a lot for this awesome piece of software. I am impressed by command line tools like "lazygit" and started migrating my own invoicing software from Ruby on Rails to Elixir at first with a terminal UI.
Ratatouille comes in handy here. There is one thing I'm missing (maybe it's there and I did not see it ): it would be great to have a color option for the borders of a panel.
With this option you could have "active" panels like in lazygit or lazydocker.
I tried to add the color to the panel borders, but could find out how to get them from the panel call to the rendering.

Best regards,

Christian

Testing Update

I'd like to be able to write tests for the update function of my Ratatouille app, but I'm having some trouble.

Based off the counter example:

  describe "update" do
    test "when + is pressed, the model is incremented by 1" do
      assert Counter.App.update(0, {:event, %{ch: ?+}}) == 1
    end
  end

Fails with (UndefinedFunctionError) function Counter.App.update/2 is undefined or private

Trouble composing several elements

Not sure If I am just misusing ratatouille or it is not possible but anyway I would like to clarify some behaviour

If I have the following for rendering two panels

def render_first do
  column(size: 12) do
    panel(title: "First", height: 4)
  end
end

def render_second do
  column(size: 12) do
    panel(title: "Second", height: 4)
  end
end

I can do it like this and both rows show up as expected:

def render(model) do
  view do
    row do render_first end
    row do render_second end
  end
end

However, if I do it like this, only the last defined row will ever show up:

def render_both do
  row do render_first end
  row do render_second end
end

def render(model) do
  view do
    render_both
  end
end

It works if I change it to the following:

def render_both do
  panel do
    row do render_first end
    row do render_second end
  end
end

def render(model) do
  view do
    render_both
  end
end

but then I have a panel and the border, which I don't want.

[bug] Ratatouille.Window failed to start

Hi folks,

@kessejones and I have been working on a personal project for a while.
Since yesterday, he can't run the project because of the error below.
On my computer, it's working fine.

We tried solving it with #11, but it didn't work.

** (EXIT from #PID<0.94.0>) shutdown: failed to start child: Ratatouille.Window
    ** (EXIT) an exception was raised:
        ** (MatchError) no match of right hand side value: {:error, -1}
            (ratatouille 0.5.1) lib/ratatouille/window.ex:104: Ratatouille.Window.init/1
            (stdlib 3.16.1) gen_server.erl:423: :gen_server.init_it/2
            (stdlib 3.16.1) gen_server.erl:390: :gen_server.init_it/6
            (stdlib 3.16.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

Error getting dependency ex_termbox

Hi,
I could not get the dependency ex_termbox when running mix deps.get

mix.exs

defp deps do
    [
      {:ratatouille, "~> 0.5.0"}
    ]
end
$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
  asciichart 1.0.0
  elixir_make 0.6.0
  ex_termbox 1.0.1
  ratatouille 0.5.0
* Updating ex_termbox (Hex package)
Request failed (403)
** (Mix) Package fetch failed and no cached copy available (https://repo.hex.pm/tarballs/ex_termbox-1.0.1.tar)

I've checked the dependency in master and is set to {:ex_termbox, "~> 1.0"}
https://github.com/ndreynolds/ratatouille/blob/master/mix.exs#L33

In hex.pm the last ex_termbox release is 1.0.0
https://hex.pm/packages/ex_termbox/1.0.0

Any idea why is trying to install ex_termbox 1.0.1?

Thanks
The library looks amazing

Mo' colors, mo' problems (extended color support?)

Howdy!

First off I want to say thanks for all your hard work on Ratatouille and ExTermbox! They're both really awesome and useful packages.

Now onto my issue 😈

I was wondering if we this could be relaxed to accept any valid color in the extended color palette instead of only the eight, definitely supported everywhere but limited, colors:

@valid_color_codes Constants.colors() |> Map.values()
@valid_attribute_codes Constants.attributes() |> Map.values()
def to_terminal_color(code)
when is_integer(code) and code in @valid_color_codes do
code
end

I noticed ExTermbox doesn't have any problem with accepting valid integers outside of the eight named colors, but because of that validation Ratatouille does not. I'm imagining something like this would remedy it:

diff --git a/lib/ratatouille/renderer/attributes.ex b/lib/ratatouille/renderer/attributes.ex
index ee1f26b..18403c3 100644
--- a/lib/ratatouille/renderer/attributes.ex
+++ b/lib/ratatouille/renderer/attributes.ex
@@ -6,10 +6,11 @@ defmodule Ratatouille.Renderer.Attributes do
   alias Ratatouille.Constants

   @valid_color_codes Constants.colors() |> Map.values()
+  @valid_color_codes_extended 0x11..0xe8
   @valid_attribute_codes Constants.attributes() |> Map.values()

   def to_terminal_color(code)
-      when is_integer(code) and code in @valid_color_codes do
+  when is_integer(code) and code in @valid_color_codes or @valid_color_codes_extended do
     code
   end

I'd be happy to push a PR if you're interested in this approach, or if you can give some guidance on an approach you'd like (or even why this is all a terrrrrrible idea).

Thank you for your time.

ElixirLS: error starting Ratatouille.Window

Hello!

As I started up a project in VSCode I found that the ElixirLS plugin marks a copy-paste of the Counter example with this error:

** (exit) shutdown: failed to start child: Ratatouille.Window
    ** (EXIT) an exception was raised:
        ** (MatchError) no match of right hand side value: {:error, :already_running}
            lib/ratatouille/window.ex:104: Ratatouille.Window.init/1
            (stdlib) gen_server.erl:374: :gen_server.init_it/2
            (stdlib) gen_server.erl:342: :gen_server.init_it/6
            (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

ratatouille-error

It seems like ElixirLS is compiling/running the example in the background to check for any errors, but it's not expecting the Ratatouille.run(Counter) at the end. This error goes away if I remove Ratatouille.run or import Ratatouille.View.

I'm not sure if this is due to this library, ElixirLS, or a misconfiguration on my end 🤷‍♂️. Let me know if I should redirect this to ElixirLS!

Support aligning text content within block-level elements

Similar to the CSS text-align property, it should be possible to specify the alignment of text content inside of block-level elements in Ratatouille.

For example:

label(text_align: :center, content: "Centered content")

In the above example, the renderer should try to create even margins to the left and right of the content within the current render box.

Ignore state which raises errors

I'm looking through the code and fund the loop cycle.

loop(%State{state | model: model, subscriptions: subscriptions})

Right now on any Exception it'll abort the window. I think user wants to get a message that something is wrong but probably we could avoid aborting the whole application and loosing all state here via loading the previous "working" state back.

Or at least allow to configure custom handler for this event.

Two rows child of the root view share X axis

Code:

view do
  row do
    column(size: 4) do
      panel(title: "Journey's Logs", color: :red, height: :fill) do
        # for l in Enum.take(history, )
      end
    end
  end
  row do
    column(size: 6) do
      panel(title: "Your orders, mi'lord?", color: :red, height: :fill) do

      end
    end
  end
end

Result:

Env: Ubuntu 18.04 via WSL2

Cursor functionality

Hi, I'm exploring the idea of writing terminal editor using your library.

Thank you for all the great work.

I've tried editor.exs but the issue here is the cursor symbol has an additional space and when you move it, the text will be jumping by that symbol. The cursor itself should not contain any width.

The solution I came to is using ExTermbox.Bindings.set_cursor/2 but it limits implementation to only one cursor and I still need spaceless symbol for multi-cursor feature.

It would be great to extend a view DSL to provide an ability to render cursors within a pane. I could look into how to implement that.

What do you think?

Thank you

Segfault

I'm using the current master and built an app using Toby as an example. Its quite far along now, but I'm frequently running into segfauls. I attached gdb to a running session and got this backtrace.

I was also wondering if you are using a patched version of termbox in ex_termbox. [Just checked] This sort of thing could be a fun project to look at Rust-based NIFs again.

[Insert] ❯❯❯ gdb
GNU gdb (GDB) 8.1.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) attach 16638
Attaching to process 16638
[New LWP 16655]
[New LWP 16656]
[New LWP 16657]
[New LWP 16659]
[New LWP 16660]
[New LWP 16661]
[New LWP 16662]
[New LWP 16663]
[New LWP 16664]
[New LWP 16665]
[New LWP 16666]
[New LWP 16667]
[New LWP 16668]
[New LWP 16669]
[New LWP 16670]
[New LWP 16671]
[New LWP 16672]
[New LWP 16673]
[New LWP 16674]
[New LWP 16675]
[New LWP 16676]
[New LWP 16677]
[New LWP 16678]
[New LWP 16679]
[New LWP 16680]
[New LWP 16681]
[New LWP 16682]
[New LWP 16683]
[New LWP 16684]
[New LWP 16685]
[New LWP 16686]
[New LWP 16697]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/nix/store/7gx4kiv5m0i7d7qkixq2cwzbr10lvxwc-glibc-2.27/lib/libthread_db.so.1".
0x00007f8a0877e49f in __GI___select (nfds=0, readfds=0x0, writefds=0x0, exceptfds=0x0, timeout=0x0)
    at ../sysdeps/unix/sysv/linux/select.c:41
41      ../sysdeps/unix/sysv/linux/select.c: No such file or directory.
(gdb) continue
Continuing.
[Thread 0x7f89ac9b5700 (LWP 16697) exited]
[New Thread 0x7f894b97f700 (LWP 17391)]

Thread 11 "7_scheduler" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f89c482d700 (LWP 16665)]
0x00000000004a5db1 in mbc_free ()
(gdb) bt
#0  0x00000000004a5db1 in mbc_free ()
#1  0x00000000004acaaa in erts_alcu_free_thr_spec ()
#2  0x00000000005f46e2 in load_nif_2 ()
#3  0x00000000004611c7 in process_main ()
#4  0x000000000045713b in sched_thread_func ()
#5  0x000000000068da20 in thr_wrapper ()
#6  0x00007f8a08c575a7 in start_thread (arg=0x7f89c482d700) at pthread_create.c:463
#7  0x00007f8a0878622f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

Key events stall on linux

Often when rapidly pressing keys, it seems some events get eaten/stall (without hitting the recv loop). This is on ubuntu 20.04 with OTP23 Elixir 1.11.

[BUG] Sparkline issue when series max and min are the same

Hi.

Thanks for this library, was able to create a small monitoring tool for my requirement. Just want to report on a possible bug where a Sparkline series has the same min and max value possibly causing an arithmetic error. The snippet below has this error:

panel title: "LINES:" do
  sparkline(series: [1])     # Error
  sparkline(series: [1,1])  # Error
  sparkline(series: [1,2])  # Works
end

I noticed in Ratatouille.Renderer.Element.Sparkline.normalize has this possibility:

defp normalize({min, max}, value) do
  x = (value - min) / (max - min)
  round(x * @range)
end

Not sure if this is intended or is better handled by another component. Thanks again.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.