Back to labs main.

Calmcode Labs Presents

tuilwind-css logotuilwind-css.

Tailwind for Terminal Apps.

Textual version 0.2.0 came out this week which higlighted a mayor milestone for the project: CSS for terminal apps. While it's not 100% just like CSS, the goal of it is to be as familiar as possible to build re-usable stylesheets for the Terminal User Interfaces (TUIs).

And while CSS is great, there's a reason that we don't use it to build calmcode: we use tailwind.css instead. We just find that the class based systen is more intuitive and we also really like how it allows you to just keep working in a single HTML file.

So what if we could have something like tailwindcss for a Textual?

Enter Tuilwind.css

We couldn't resist, and we started a project to facilitate tailwind-like classes for TUIs. The project is called tuilwind.css and it can be found on Github.

The project is very much an experiment right now because it's pretty early days for terminal apps, but we've found it a joy to work with allready.

To use it, you can just download the generated tuilwind.css locally:

wget https://github.com/koaning/tuilwindcss/blob/main/tuilwind.css

And from here you can just refer to it from your Textual app.

from textual.app import App, ComposeResult
from textual.widgets import Static


class PaddingDemo(App):
    CSS_PATH = "tuilwind.css"

    def compose(self) -> ComposeResult:
        """Called to add widgets to the app."""
        yield Static("p-1", classes="p-1 bg-blue-400")
        yield Static("p-2", classes="p-2 bg-blue-500")
        yield Static("p-3", classes="p-3 bg-blue-600")
        yield Static("p-4", classes="p-4 bg-blue-700")

if __name__ == "__main__":
    app = PaddingDemo()
    app.run()

Here's what this would look like in your terminal:

@font-face {
    font-family: "Fira Code";
    src: local("FiraCode-Regular"),
            url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
            url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
    font-style: normal;
    font-weight: 400;
}
@font-face {
    font-family: "Fira Code";
    src: local("FiraCode-Bold"),
            url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
            url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
    font-style: bold;
    font-weight: 700;
}

.terminal-2366273403-matrix {
    font-family: Fira Code, monospace;
    font-size: 20px;
    line-height: 24.4px;
    font-variant-east-asian: full-width;
}

.terminal-2366273403-title {
    font-size: 18px;
    font-weight: bold;
    font-family: arial;
}

.terminal-2366273403-r1 { fill: #c5c8c6 }

.terminal-2366273403-r2 { fill: #eaf3fe } .terminal-2366273403-r3 { fill: #e5eefd } .terminal-2366273403-r4 { fill: #e2eafc } .terminal-2366273403-r5 { fill: #e1e7f9 }

<defs>
<clipPath id="terminal-2366273403-clip-terminal">
  <rect x="0" y="0" width="975.0" height="584.5999999999999"></rect>
</clipPath>
<clipPath id="terminal-2366273403-line-0">
<rect x="0" y="1.5" width="976" height="24.65"></rect>
        </clipPath>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"></rect><text class="terminal-2366273403-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">PaddingDemo</text>
        <g transform="translate(26,22)">
        <circle cx="0" cy="0" r="7" fill="#ff5f57"></circle>
        <circle cx="22" cy="0" r="7" fill="#febc2e"></circle>
        <circle cx="44" cy="0" r="7" fill="#28c840"></circle>
        </g>

<g transform="translate(9, 41)" clip-path="url(#terminal-2366273403-clip-terminal)">
<rect fill="#60a5fa" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#60a5fa" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#60a5fa" x="12.2" y="25.9" width="36.6" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#60a5fa" x="48.8" y="25.9" width="927.2" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#60a5fa" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#3b82f6" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#3b82f6" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#3b82f6" x="0" y="123.5" width="24.4" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#3b82f6" x="24.4" y="123.5" width="36.6" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#3b82f6" x="61" y="123.5" width="915" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#3b82f6" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#3b82f6" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="0" y="269.9" width="36.6" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="36.6" y="269.9" width="36.6" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="73.2" y="269.9" width="902.8" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#2563eb" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="465.1" width="48.8" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="48.8" y="465.1" width="36.6" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="85.4" y="465.1" width="890.6" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"></rect><rect fill="#1d4ed8" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"></rect>
<g class="terminal-2366273403-matrix">
<text class="terminal-2366273403-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-2366273403-line-0)">

p-1 p-2 p-3 p-4

Examples

If you're curious to learn more, you can check out the gallery on the docs. As you'll see right away, the project doesn't cover all of tailwind's classes. But we hope the project covers enough to make it easier for you to get started.

ps. We are working on a textual course and a tailwind course, be tuned for that later!


Back to labs main.