twc-rs 0.9.0

Fast single-binary CLI and interactive TUI dashboard for Timeweb Cloud
Documentation
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

//! Resource actions: the action vocabulary, pending state and the context menu.

use std::borrow::Cow;

use rust_i18n::t;

use super::ResourceTab;

/// A kind of action that can be performed on a resource.
///
/// Each [`ResourceTab`] declares which kinds apply to it via
/// [`ResourceTab::actions`]; the dashboard loop maps a chosen
/// `(tab, kind)` pair to the matching Timeweb API call.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActionKind {
    /// Power the resource on.
    Start,
    /// Gracefully power the resource off.
    Shutdown,
    /// Reboot the resource.
    Reboot,
    /// Create a clone of the resource.
    Clone,
    /// Create a backup of the resource.
    Backup,
    /// Permanently delete the resource.
    Delete
}

impl ActionKind {
    /// Returns the label shown in the action menu and confirmation prompt.
    #[must_use]
    pub const fn label(self) -> &'static str {
        match self {
            Self::Start => "Start",
            Self::Shutdown => "Shutdown",
            Self::Reboot => "Reboot",
            Self::Clone => "Clone",
            Self::Backup => "Backup",
            Self::Delete => "Delete"
        }
    }

    /// Returns the localized label shown in menus and confirmation prompts.
    #[must_use]
    pub fn display_label(self) -> Cow<'static, str> {
        match self {
            Self::Start => t!("app.action_start"),
            Self::Shutdown => t!("app.action_shutdown"),
            Self::Reboot => t!("app.action_reboot"),
            Self::Clone => t!("app.action_clone"),
            Self::Backup => t!("app.action_backup"),
            Self::Delete => t!("app.action_delete")
        }
    }

    /// Returns true when the action is destructive and irreversible.
    ///
    /// Destructive actions require an extra confirmation step.
    #[must_use]
    pub const fn is_destructive(self) -> bool {
        matches!(self, Self::Delete)
    }
}

/// A resource action awaiting confirmation, or ready to be dispatched.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingAction {
    /// The resource category the action targets.
    pub tab:           ResourceTab,
    /// The action to perform.
    pub kind:          ActionKind,
    /// Target resource id (numeric or UUID, depending on the resource).
    pub resource_id:   String,
    /// Target resource name, for display.
    pub resource_name: String
}

/// A context action menu opened over the selected resource.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActionMenu {
    /// The resource category the menu targets.
    pub tab:           ResourceTab,
    /// Target resource id (numeric or UUID, depending on the resource).
    pub resource_id:   String,
    /// Target resource name, for display.
    pub resource_name: String,
    /// Available actions, in display order.
    pub actions:       Vec<ActionKind>,
    /// Index of the highlighted action.
    pub selected:      usize
}

impl super::App {
    /// Opens the context action menu for the selected resource.
    ///
    /// No-op when the active tab declares no actions or nothing is selected.
    pub fn open_action_menu(&mut self) {
        let actions = self.active_tab.actions();
        if actions.is_empty() {
            self.status_message = Some(t!("app.no_actions").to_string());
            return;
        }
        if let Some((id, name)) = self.selected_resource() {
            self.action_menu = Some(ActionMenu {
                tab:           self.active_tab,
                resource_id:   id,
                resource_name: name,
                actions:       actions.to_vec(),
                selected:      0
            });
        }
    }

    /// Closes the action menu without choosing anything.
    pub fn close_action_menu(&mut self) {
        self.action_menu = None;
    }

    /// Returns true while the action menu is open.
    #[must_use]
    pub const fn action_menu_open(&self) -> bool {
        self.action_menu.is_some()
    }

    /// Returns the open action menu, for rendering.
    #[must_use]
    pub const fn action_menu(&self) -> Option<&ActionMenu> {
        self.action_menu.as_ref()
    }

    /// Moves the action-menu highlight to the next item (wraps).
    pub const fn menu_next(&mut self) {
        if let Some(menu) = self.action_menu.as_mut()
            && !menu.actions.is_empty()
        {
            menu.selected = (menu.selected + 1) % menu.actions.len();
        }
    }

    /// Moves the action-menu highlight to the previous item (wraps).
    pub const fn menu_previous(&mut self) {
        if let Some(menu) = self.action_menu.as_mut() {
            let len = menu.actions.len();
            if len > 0 {
                menu.selected = (menu.selected + len - 1) % len;
            }
        }
    }

    /// Chooses the highlighted action: destructive actions open a
    /// confirmation prompt, others are queued for dispatch immediately.
    pub fn menu_select(&mut self) {
        let Some(menu) = self.action_menu.take() else {
            return;
        };
        let Some(&action) = menu.actions.get(menu.selected) else {
            return;
        };
        let pending = PendingAction {
            tab:           menu.tab,
            kind:          action,
            resource_id:   menu.resource_id,
            resource_name: menu.resource_name
        };
        if action.is_destructive() {
            self.confirm = Some(pending);
        } else {
            self.dispatch = Some(pending);
        }
    }

    /// Confirms the pending destructive action, queueing it for dispatch.
    pub fn confirm_action(&mut self) {
        self.dispatch = self.confirm.take();
    }

    /// Dismisses the pending action without dispatching it.
    pub fn cancel_action(&mut self) {
        self.confirm = None;
    }

    /// Returns the action awaiting confirmation, for rendering the modal.
    #[must_use]
    pub const fn pending_action(&self) -> Option<&PendingAction> {
        self.confirm.as_ref()
    }

    /// Returns true while a confirmation modal is open.
    #[must_use]
    pub const fn awaiting_confirm(&self) -> bool {
        self.confirm.is_some()
    }

    /// Takes the action queued for dispatch, if the user confirmed one.
    #[must_use]
    pub const fn take_dispatch(&mut self) -> Option<PendingAction> {
        self.dispatch.take()
    }
}