fission-command-ui 0.14.1

Terminal UI application for the fission command
Documentation
use super::{ActionButton, ButtonTone};
use crate::actions::{select_device, select_target, SelectDevice, SelectTarget};
use crate::density::UiDensity;
use crate::state::{all_targets, target_label, UiDevice, UiState};
use crate::theme::UiPalette;
use fission::op::AlignItems;
use fission::prelude::*;
use fission_command_core::Target;

#[derive(Clone)]
pub struct KeyValueRow {
    pub label: String,
    pub value: String,
}

impl KeyValueRow {
    pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            value: value.into(),
        }
    }
}

impl From<KeyValueRow> for Widget {
    fn from(component: KeyValueRow) -> Self {
        let (_ctx, view) = fission::build::current::<UiState>();
        let palette = UiPalette::for_mode(view.state().theme_mode);
        Row {
            gap: Some(1.0),
            align_items: AlignItems::Center,
            children: vec![
                Text::new(format!("{}:", component.label))
                    .color(palette.muted)
                    .width(16.0)
                    .into(),
                Text::new(component.value.clone())
                    .color(palette.text)
                    .into(),
            ],
            ..Default::default()
        }
        .into()
    }
}
#[derive(Clone)]
pub struct TargetPicker {
    pub configured_only: bool,
}

impl From<TargetPicker> for Widget {
    fn from(component: TargetPicker) -> Self {
        let (ctx, view) = fission::build::current::<UiState>();
        let targets = if component.configured_only && !view.state().targets.is_empty() {
            view.state().targets.clone()
        } else {
            all_targets().to_vec()
        };
        let mut rows = vec![Text::new("Target")
            .color(UiPalette::for_mode(view.state().theme_mode).accent)
            .into()];
        for target in targets {
            rows.push(target_button(target, ctx, view));
        }
        Column {
            gap: Some(if view.state().compact_mode { 0.0 } else { 1.0 }),
            children: rows,
            ..Default::default()
        }
        .into()
    }
}
fn target_button(
    target: Target,
    ctx: BuildCtxHandle<UiState>,
    view: ViewHandle<UiState>,
) -> Widget {
    let selected = view.state().selected_target == Some(target);
    let action = with_reducer!(ctx, SelectTarget(target), select_target);
    ActionButton::new(target_label(target), action)
        .tone(if selected {
            ButtonTone::Primary
        } else {
            ButtonTone::Neutral
        })
        .width(18.0)
        .into()
}

#[derive(Clone)]
pub struct DeviceTable {
    pub devices: Vec<UiDevice>,
    pub selectable: bool,
    pub max_rows: usize,
}

impl From<DeviceTable> for Widget {
    fn from(component: DeviceTable) -> Self {
        let (ctx, view) = fission::build::current::<UiState>();
        let palette = UiPalette::for_mode(view.state().theme_mode);
        let mut rows = vec![Row {
            gap: Some(1.0),
            children: vec![
                Text::new("target").color(palette.muted).width(10.0).into(),
                Text::new("kind").color(palette.muted).width(15.0).into(),
                Text::new("status").color(palette.muted).width(12.0).into(),
                Text::new("name").color(palette.muted).into(),
            ],
            ..Default::default()
        }
        .into()];
        for device in component.devices.iter().take(component.max_rows) {
            rows.push(device_row(device, component.selectable, ctx, view));
        }
        if component.devices.is_empty() {
            rows.push(
                Text::new("No devices detected.")
                    .color(palette.warning)
                    .into(),
            );
        }
        Column {
            gap: Some(if view.state().compact_mode { 0.0 } else { 1.0 }),
            children: rows,
            ..Default::default()
        }
        .into()
    }
}
fn device_row(
    device: &UiDevice,
    selectable: bool,
    ctx: BuildCtxHandle<UiState>,
    view: ViewHandle<UiState>,
) -> Widget {
    let palette = UiPalette::for_mode(view.state().theme_mode);
    let status_color = if device.available {
        palette.success
    } else {
        palette.warning
    };
    let selected = view.state().selected_device.as_ref() == Some(&device.id);
    let row = Row {
        gap: Some(1.0),
        children: vec![
            Text::new(device.target.as_str())
                .color(palette.text)
                .width(10.0)
                .into(),
            Text::new(device.kind.clone())
                .color(palette.text)
                .width(15.0)
                .into(),
            Text::new(device.status.clone())
                .color(status_color)
                .width(12.0)
                .into(),
            Text::new(format!("{} ({})", device.name, device.id))
                .color(palette.text)
                .into(),
        ],
        ..Default::default()
    }
    .into();

    if selectable && device.available {
        let action = with_reducer!(ctx, SelectDevice(device.id.clone()), select_device);
        let density = UiDensity::new(view.state().compact_mode);
        Button {
            on_press: Some(action),
            height: Some(density.control_height()),
            padding: Some(density.control_padding()),
            background_fill: Some(fission::op::Fill::Solid(if selected {
                palette.subtle
            } else {
                palette.surface
            })),
            text_color: Some(palette.text),
            child: Some(row),
            ..Default::default()
        }
        .into()
    } else {
        row
    }
}