streamdeck-oxide 0.1.1

A high-level framework for creating Stream Deck applications in Rust
Documentation
# StreamDeck Oxide

A high-level framework for creating Stream Deck applications in Rust.

## Features

- Button rendering with text, icons, and custom images
- View system for organizing buttons into screens
- Navigation between views
- Event handling for button presses
- Async support for fetching state and handling actions

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
streamdeck-oxide = "0.1.0"
```

### Other dependencies

`libudev` is required for HID support. You can install it using your package
manager. `flake.nix` with a dev shell is provided for development.

## Usage

```rust
use std::sync::Arc;

use streamdeck_oxide::{
    button::RenderConfig, elgato_streamdeck, generic_array::{typenum::{U3, U5}}, md_icons, navigation::NavigationEntry, run_with_external_triggers, theme::Theme, view::{
        customizable::{ClickButton, CustomizableView, ToggleButton},
        View,
    }, ExternalTrigger
};

/// Application context for our Stream Deck app
#[derive(Debug, Clone)]
struct AppContext {
    message: String,
}

/// Navigation structure for our Stream Deck app
#[derive(Debug, Clone, Default, PartialEq)]
enum Navigation {
    #[default]
    Main,
    Settings,
    Notification(String),
}

impl NavigationEntry<U5, U3, AppContext>
    for Navigation
{
    fn get_view(
        &self,
    ) -> Result<
        Box<
            dyn View<
                U5,
                U3,
                AppContext,
                Navigation,
            >,
        >,
        Box<dyn std::error::Error>,
    > {
        match self {
            Navigation::Main => {
                let mut view = CustomizableView::default();

                // Add a toggle button
                view.set_button(
                    0,
                    0,
                    ToggleButton::new(
                        "Toggle",
                        Some(md_icons::filled::ICON_HOME),
                        |_ctx: AppContext| async move {
                            println!("Fetching toggle state");
                            // In a real app, you might fetch this from some external state
                            Ok(false)
                        },
                        |ctx: AppContext, state: bool| async move {
                            println!("Toggled state: {}", state);
                            println!("Message: {}", ctx.message);
                            Ok(())
                        },
                    ),
                )?;

                // Add a click button
                view.set_button(
                    1,
                    0,
                    ClickButton::new(
                        "Click",
                        Some(md_icons::filled::ICON_TOUCH_APP),
                        |ctx: AppContext| async move {
                            println!("Button clicked!");
                            println!("Message: {}", ctx.message);
                            Ok(())
                        },
                    ),
                )?;

                // Add a navigation button to settings
                view.set_navigation(
                    0,
                    2,
                    Navigation::Settings,
                    "Settings",
                    Some(md_icons::sharp::ICON_SETTINGS),
                )?;

                Ok(Box::new(view))
            }
            Navigation::Settings => {
                let mut view = CustomizableView::default();

                // Add some settings buttons
                view.set_button(
                    0,
                    0,
                    ClickButton::new(
                        "Option 1",
                        Some(md_icons::filled::ICON_BRIGHTNESS_5),
                        |_ctx| async move {
                            println!("Option 1 selected");
                            Ok(())
                        },
                    ),
                )?;

                view.set_button(
                    1,
                    0,
                    ClickButton::new(
                        "Option 2",
                        Some(md_icons::filled::ICON_VOLUME_UP),
                        |_ctx| async move {
                            println!("Option 2 selected");
                            Ok(())
                        },
                    ),
                )?;

                // Add a button that fails to fetch data
                view.set_button(
                    2,
                    0,
                    ClickButton::new(
                        "Fail",
                        Some(md_icons::filled::ICON_ERROR),
                        |_ctx| async move {
                            println!("This button will fail");
                            Err("Failed to fetch data".into())
                        },
                    ),
                )?;

                // Add a navigation button to go back to main
                view.set_navigation(
                    4,
                    2,
                    Navigation::Main,
                    "Back",
                    Some(md_icons::sharp::ICON_ARROW_BACK),
                )?;

                Ok(Box::new(view))
            }
            Navigation::Notification(content) => {
                let mut view = CustomizableView::default();

                view.set_button(
                    2,
                    1,
                    ClickButton::new(
                        content,
                        Some(md_icons::filled::ICON_NOTIFICATIONS),
                        |_ctx| async move { Ok(()) },
                    ),
                )?;

                view.set_navigation(
                    4,
                    2,
                    Navigation::Main,
                    "Back",
                    Some(md_icons::sharp::ICON_ARROW_BACK),
                )?;

                Ok(Box::new(view))
            }
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("StreamDeck Example Application");
    println!("------------------------------");

    // Connect to the Stream Deck
    let hid = elgato_streamdeck::new_hidapi()?;
    let devices = elgato_streamdeck::list_devices(&hid);

    println!("Looking for Stream Deck devices...");

    let (kind, serial) = devices
        .into_iter()
        .find(|(kind, _)| *kind == elgato_streamdeck::info::Kind::Mk2)
        .ok_or("No Stream Deck found")?;

    println!("Found Stream Deck: {:?} ({})", kind, serial);

    let deck = Arc::new(elgato_streamdeck::AsyncStreamDeck::connect(
        &hid, kind, &serial,
    )?);
    println!("Connected to Stream Deck successfully!");

    // Create configuration
    let config = RenderConfig::default();
    let theme = Theme::light(); // Use light theme for this example

    // Create application context
    let context = AppContext {
        message: "Hello from StreamDeck Example!".to_string(),
    };

    println!("Starting Stream Deck application...");
    println!("Press Ctrl+C to exit");

    let (sender, receiver) = tokio::sync::mpsc::channel::<ExternalTrigger<Navigation, U5, U3, AppContext>>(1);

    // Create an endless loop sending a notification every 15 seconds
    let future = async move {
        loop {
            tokio::time::sleep(tokio::time::Duration::from_secs(15)).await;
            let result = sender.send(ExternalTrigger::new(Navigation::Notification("Hello!".to_string()), true)).await;
            if let Err(e) = result {
                println!("Failed to send notification: {}", e);
            } else {
                println!("Notification sent!");
            }
        }
    };

    // Run the notification loop
    tokio::spawn(future);

    // Run the application
    run_with_external_triggers::<Navigation, U5, U3, AppContext>(
        theme, config, deck, context, receiver
    )
    .await?;

    Ok(())
}
```

## Documentation

For more detailed documentation, see the
[API documentation](https://docs.rs/streamdeck-oxide).

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
for details.

This project is using Roboto font for rendering text. The font files are
included in the `fonts` directory. Roboto is licensed under the Apache License.
See the
[LICENSE-ROBOTO](https://github.com/googlefonts/roboto-2/blob/main/LICENSE) file
for details.