crates/veilvoice-cli/src/failsafe.rs

what this file is for · veilvoice-cli · 112 lines · the same file on GitHub

The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.


// SPDX-License-Identifier: GPL-3.0-or-later
//! `veilvoice failsafe` is the safety catch. What it can and cannot do.
//!
//! # In plain words
//!
//! Shows whether the safety catch is on, what it would do, and what is holding
//! a microphone right now. It is the same guard the desktop application runs;
//! this is how to look at it from a terminal.
//!
//! The limits are printed every time, because the one thing a reader must not
//! come away believing is that this stops their computer handing a microphone
//! to another program. It notices, and it acts, and there is a moment between
//! the two.

use crate::theme::{colour, field, heading, paint, warn};
use veilvoice_guard::failsafe::{Finding, Guard, Holder, Posture};


/// Show what Failsafe would make of this machine right now.
pub fn show(veiling: Option<&str>) -> Result<(), String> {
    println!("{}", heading("Failsafe"));
    println!();
    println!("{}", field("posture", Posture::default().label()));
    for line in crate::sentry::wrap(Posture::default().note(), 70) {
        println!("    {line}");
    }
    println!();

    // What holds a microphone, through the same feed the application uses.
    let (holders, problems) = microphone_holders();
    let mut guard = Guard::new();
    guard.posture = Posture::default();
    guard.veiling = veiling.map(str::to_string);
    // Asked as though veiling were running, because the question a reader has
    // at a terminal is "what would happen", and answering `Idle` whenever live
    // mode is not open would tell them nothing at all.
    guard.live = true;

    println!("{}", heading("What is holding a microphone"));
    if holders.is_empty() && problems.is_empty() {
        println!("{}", paint(colour::MUTED, "  nothing"));
    }
    for holder in &holders {
        let mine = veiling
            .map(|ours| {
                holder
                    .device
                    .as_deref()
                    .map(|d| d.to_lowercase().contains(&ours.to_lowercase()))
                    .unwrap_or(false)
            })
            .unwrap_or(false);
        let tint = if mine { colour::MUTED } else { colour::YELLOW };
        let where_ = holder.device.clone().unwrap_or_else(|| "?".into());
        println!("{}", paint(tint, &format!("  {}  ({where_})", holder.app)));
    }
    for problem in &problems {
        println!("{}", warn(problem));
    }

    println!();
    let finding = guard.look(&holders, &problems);
    let tint = if finding.is_alarming() {
        colour::YELLOW
    } else {
        colour::MUTED
    };
    for line in crate::sentry::wrap(&finding.phrasing(), 70) {
        println!("{}", paint(tint, &format!("  {line}")));
    }

    println!();
    println!("{}", paint(colour::YELLOW, "WHAT THIS CANNOT DO"));
    for note in [
        veilvoice_guard::failsafe::CANNOT_PREVENT,
        veilvoice_guard::failsafe::NEVER_CLOSES,
    ] {
        for line in crate::sentry::wrap(note, 72) {
            println!("  {line}");
        }
        println!();
    }
    Ok(())
}



/// Everything holding a microphone, through the shared watch layer.
fn microphone_holders() -> (Vec<Holder>, Vec<String>) {
    let feed = veilvoice_watch::scan();
    let mut problems = Vec::new();
    let uses = match feed {
        Ok(uses) => uses,
        Err(error) => {
            problems.push(error.to_string());
            Vec::new()
        }
    };
    let holders = uses
        .into_iter()
        .filter(|use_| use_.kind == veilvoice_watch::DeviceKind::Microphone)
        .map(|use_| Holder {
            app: use_.app,
            pid: use_.pid,
            device: use_.device,
        })
        .collect();
    (holders, problems)
}



/// A named finding, for the tests to reach without a machine.
#[allow(dead_code)]
pub fn describe(finding: &Finding) -> String {
    finding.phrasing()
}