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:

[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

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.

License

This project is licensed under the MIT License. See the 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 file for details.