2 releases

0.1.1 May 13, 2022
0.1.0 May 13, 2022

#1315 in GUI

MIT license

58KB
415 lines

douglas

Douglas is a tiny terminal UI framework built on the Elm architecture. It's heavily inspired by bubbletea and iced.

Usage

Building a UI with Douglas is a cinch!

use douglas::{Config, Program};

struct App;

impl Program for App {
    type Message = ();

    fn view(&self) -> String {
        "Hello, world!\n".into()
    }
}

fn main() {
    App.run(&mut Config::default()).unwrap();
}

Granted, this isn't very exciting. Douglas apps just need 3 ingredients:

  1. init initialize your state model, and perform any setup
  2. update handle messages and user input
  3. view render your UI (you've seen this already!)
use douglas::{Command, Config, Mailbox, Program};

// declare your state model
struct App {
    counter: usize,
}

// setup a constructor
impl App {
    fn new() -> Self {
        Self {
            counter: 0,
        }
    }
}

// declare message type
enum Message {
    Increment,
    Decrement,
}

impl Program for App {
    type Message = Message;

    fn init(&mut self, _: Mailbox<Self::Message>) -> Command<Self::Message> {
        // send an Increment message right away
        Command::send(Message::Increment)
    }

    fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
        // you can pattern match on incoming messages
        match message {
            Message::Increment => self.counter += 1,
            Message::Decrement => self.counter -= 1,
        }

        Command::none()
    }

    fn view(&self) -> String {
        format!("count: {}\n", self.counter)
    }
}

fn main() {
    let app = App::new();
    let config = Config::default();
    app.run(&mut config).unwrap();
}

Finally, if you need to do any cleanup, you may implement exit:

use douglas::Program;

impl Program for App {
    // (...snip...)

    fn exit(self) {
        println!("Goodbye, cruel world!");
    }
}

Handling Events

In addition to update, apps may implement on_event to send messages in response to terminal events such as keypresses:

use douglas::{Command, Program};
use crossterm::event::{Event, KeyCode, KeyEvent};

impl Program for App {
    // (...snip...)

    fn on_event(event: Event) -> Command<Self::Message> {
        match event {
            Event::Key(KeyEvent { code: KeyCode::Up, .. }) =>
                Command::send(Message::Increment),
            _ => Command::none()
        }
    }
}

External Interaction

Sometimes it's useful to respond to events that occur outside of your program's lifecycle, like responding when data becomes available on the network or sending a message on a recurring interval. You can use your program's mailbox:

use douglas::{Command, Mailbox, Program, Timer};
use crossterm::event::{Event, KeyCode, KeyEvent};
use std::time::Duration;

struct App {
    timer: Timer,
}

#[derive(Clone)]
enum Message {
    Tick,
}

impl App {
    fn new() -> Self {
        Self {
            timer: Timer::new(Duration::from_millis(1_000), Message::Tick),
        }
    }
}

impl Program for App {
    // (...snip...)

    fn init(&mut self, mailbox: Mailbox<Self::Message>) -> Command<Self::Message> {
        self.timer.start(mailbox);

        Command::none()
    }

    // make sure to clean up!
    fn exit(mut self) {
        self.timer.stop();
    }
}

Examples

You can check out the examples directory to see some projects in action. You can also run an example directly:

cargo run --package hello_world

Contributing

This project is a work in progress. All contributions are welcome!

This project adheres to the Contributor Covenant code of conduct. Please be nice to each other 🙂

License

This project is made available under the MIT License. Copyright 2022 Aaron Ross, all rights reserved.

Dependencies

~2.5MB
~42K SLoC