Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Writing a New Service

This guide shows how to add a new backend service to ashell.

Read-Only D-Bus Service

Most new services will use D-Bus. Here’s a template:

Step 1: Create the Service Files

src/services/my_service/
├── mod.rs    # Service logic
└── dbus.rs   # D-Bus proxy definitions

Step 2: Define D-Bus Proxies (dbus.rs)

#![allow(unused)]
fn main() {
use zbus::proxy;

#[proxy(
    interface = "org.example.MyService1",
    default_service = "org.example.MyService",
    default_path = "/org/example/MyService"
)]
trait MyService1 {
    #[zbus(property)]
    fn status(&self) -> zbus::Result<String>;

    #[zbus(property)]
    fn value(&self) -> zbus::Result<u32>;
}
}

Step 3: Implement the Service (mod.rs)

#![allow(unused)]
fn main() {
use crate::services::{ReadOnlyService, ServiceEvent};
use iced::{Subscription, stream::channel};
use iced::futures::SinkExt;
use std::any::TypeId;

mod dbus;

// Define the update event
#[derive(Debug, Clone)]
pub enum UpdateEvent {
    StatusChanged(String),
    ValueError(u32),
}

// Define the service state
#[derive(Debug, Clone)]
pub struct MyService {
    pub status: String,
    pub value: u32,
}

impl ReadOnlyService for MyService {
    type UpdateEvent = UpdateEvent;
    type Error = String;

    fn update(&mut self, event: Self::UpdateEvent) {
        match event {
            UpdateEvent::StatusChanged(s) => self.status = s,
            UpdateEvent::ValueError(v) => self.value = v,
        }
    }

    fn subscribe() -> Subscription<ServiceEvent<Self>> {
        Subscription::run_with_id(
            TypeId::of::<Self>(),
            channel(10, async move |mut output| {
                // Connect to D-Bus
                let connection = zbus::Connection::system().await.unwrap();
                let proxy = dbus::MyService1Proxy::new(&connection).await.unwrap();

                // Send initial state
                let status = proxy.status().await.unwrap_or_default();
                let value = proxy.value().await.unwrap_or_default();
                let _ = output.send(ServiceEvent::Init(MyService { status, value })).await;

                // Watch for property changes
                let mut status_stream = proxy.receive_status_changed().await;
                loop {
                    use iced::futures::StreamExt;
                    if let Some(change) = status_stream.next().await {
                        if let Ok(new_status) = change.get().await {
                            let _ = output.send(
                                ServiceEvent::Update(UpdateEvent::StatusChanged(new_status))
                            ).await;
                        }
                    }
                }
            }),
        )
    }
}
}

Step 4: Register in services/mod.rs

#![allow(unused)]
fn main() {
pub mod my_service;
}

Step 5: Consume from a Module

In your module’s subscription:

#![allow(unused)]
fn main() {
use crate::services::my_service::MyService;
use crate::services::{ReadOnlyService, ServiceEvent};

pub fn subscription(&self) -> Subscription<Message> {
    MyService::subscribe().map(|event| Message::ServiceUpdate(event))
}
}

Bidirectional Service (with Commands)

If your service needs to accept commands, additionally implement the Service trait:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub enum Command {
    SetValue(u32),
}

impl Service for MyService {
    type Command = Command;

    fn command(&mut self, command: Self::Command) -> Task<ServiceEvent<Self>> {
        match command {
            Command::SetValue(val) => {
                self.value = val;
                Task::perform(
                    async move {
                        // Execute the D-Bus call
                        let connection = zbus::Connection::system().await.unwrap();
                        let proxy = dbus::MyService1Proxy::new(&connection).await.unwrap();
                        proxy.set_value(val).await.unwrap();
                        ServiceEvent::Update(UpdateEvent::ValueError(val))
                    },
                    |event| event,
                )
            }
        }
    }
}
}

Non-D-Bus Service

For services that don’t use D-Bus (e.g., file watching, IPC sockets):

#![allow(unused)]
fn main() {
fn subscribe() -> Subscription<ServiceEvent<Self>> {
    Subscription::run_with_id(
        TypeId::of::<Self>(),
        channel(10, async move |mut output| {
            // Your custom event source here
            // Could be: file watching, socket reading, periodic polling, etc.
            loop {
                let data = read_from_source().await;
                let _ = output.send(ServiceEvent::Update(data)).await;
            }
        }),
    )
}
}

Using ThrottleExt

If your service produces events very rapidly, use the throttle adapter:

#![allow(unused)]
fn main() {
use crate::services::throttle::ThrottleExt;

// In your subscription loop:
let throttled_stream = event_stream.throttle(Duration::from_millis(100));
}

This prevents UI updates from overwhelming the rendering pipeline.