tp-note 1.13.8

Minimalist note taking: save and edit your clipboard content as a note file
//! Receives strings by a message channel, queues them and displays them
//! one by one in popup alert windows.

use crate::config::CONFIG_PATH;
use crate::VERSION;
use lazy_static::lazy_static;
use msgbox::IconType;
use std::env;
use std::path::PathBuf;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::RecvTimeoutError;
use std::sync::mpsc::SendError;
use std::sync::mpsc::SyncSender;
use std::sync::Mutex;
use std::thread;
use std::thread::sleep;
use std::time::Duration;

lazy_static! {
/// The message queue accepting strings for being shown as
/// popup alert windows.
    static ref MESSAGE_CHANNEL: (SyncSender<String>, Mutex<Receiver<String>>) = {
        let (tx, rx) = sync_channel(QUEUE_LEN);
        (tx, Mutex::new(rx))
    };
}

lazy_static! {
    /// Window title followed by version.
    static ref DIALOG_TITLE_LINE: String = format!(
        "{} (v{})",
        &DIALOG_TITLE,
        VERSION.unwrap_or("unknown")
    );
}

lazy_static! {
    /// Some additional debugging information added to the end of error messages.
    static ref DIALOG_ERR_TAIL: String = {
        let mut args_str = String::new();
        for argument in env::args() {
            args_str.push_str(argument.as_str());
            args_str.push(' ');
        };

        format!(
            "\n\
            __________\n\
            Additional technical details:\n\
            *    Command line parameters:\n\
            {}\n\
            *    Configuration file:\n\
            {}",
            args_str,
            &*CONFIG_PATH
                .as_ref()
                .unwrap_or(&PathBuf::from("no path found"))
                .to_str()
                .unwrap_or_default()
        )
    };
}

lazy_static! {
    /// This mutex does not hold any data. When it is locked, it indicates,
    /// that the `AlertService` is still busy and should not get shut down.
    static ref BUSY_LOCK: Mutex<()> = Mutex::new(());
}

/// The number of messages that will be queued.
/// As error messages can drop in by every thread and we can only
/// show one alert window at the same time, they must be queued.
pub const QUEUE_LEN: usize = 30;

/// Window title of the message alert box.
const DIALOG_TITLE: &str = "Tp-Note";

/// The `AlertService` reports to be busy as long as there
/// is is a message window open and beyond that also
/// `KEEP_ALIVE` milliseconds after the last
/// message window got closed by the user.
#[cfg(feature = "message-box")]
const KEEP_ALIVE: u64 = 1000;

/// Extra timeout for the `flush()` method, before it checks if there is still
/// an open popup alert window.  We wait a moment just in case that there are
/// pending messages we have not received yet. 1 millisecond is enough, we wait
/// 10 just to be sure.
const FLUSH_TIMEOUT: u64 = 10;

pub struct AlertService {}

impl AlertService {
    /// Initializes the service. Call once when the application starts.
    /// Drop strings in the`MESSAGE_CHANNEL` to use this service.
    pub fn init() {
        // Setup the `AlertService`.
        // Set up the channel now.
        lazy_static::initialize(&MESSAGE_CHANNEL);
        thread::spawn(move || {
            // this will block until the previous message has been received
            AlertService::run();
        });
    }

    /// Alert service, receiving Strings to display in a popup window.
    fn run() {
        // Get the receiver.
        let (_, rx) = &*MESSAGE_CHANNEL;
        let rx = rx.lock().unwrap();

        // We start with the lock released.
        let mut opt_guard = None;
        loop {
            let msg = if opt_guard.is_none() {
                // As there is no lock, we block here until the next message comes.
                // `recv()` should never return `Err`. This can only happen when
                // the sending half of a channel (or sync_channel) is disconnected,
                // implying that no further messages will ever be received.
                // As this should never happen, we panic this thread then.
                Some(rx.recv().unwrap())
            } else {
                // There is a lock because we just received another message.
                // If the next `KEEP_ALIVE` milliseconds no
                // other message comes in, we release the lock again.
                match rx.recv_timeout(Duration::from_millis(KEEP_ALIVE)) {
                    Ok(s) => Some(s),
                    Err(RecvTimeoutError::Timeout) => None,
                    // The sending half of a channel (or sync_channel) is `Disconnected`,
                    // implies that no further messages will ever be received.
                    // As this should never happen, we panic this thread then.
                    Err(RecvTimeoutError::Disconnected) => panic!(),
                }
            };

            // We received a message.
            match msg {
                Some(s) => {
                    // If the lock is released, lock it now.
                    if opt_guard.is_none() {
                        opt_guard = BUSY_LOCK.try_lock().ok();
                    }
                    // This blocks until the user closes the alert window.
                    Self::print_error(&s);
                }
                // `ALERT_SERVICE_KEEP_ALIVE` milliseconds are over and still no
                // new message. We release the lock again.
                None => {
                    // Here the `guard` goes out of scope and the lock is released.
                    opt_guard = None;
                    //
                }
            }
        }
    }

    /// The `AlertService` keeps holding a lock until `KEEP_ALIVE` milliseconds
    /// after the user has closed that last error alert window. Only then, it
    /// releases the lock. This function blocks until the lock is released.
    pub fn flush() {
        // See constant documentation why we wait here.
        sleep(Duration::from_millis(FLUSH_TIMEOUT));
        // This might block, if a guard in `run()` holds already a lock.
        let _res = BUSY_LOCK.lock();
    }

    #[inline]
    /// Concatenates some extra debugging info to `msg` and pushes it into
    /// queue. In case the message queue is full, the method blocks until there
    /// is more free space. Make sure to initialize before with
    /// `AlertService::init()`. Returns an `SendError` if nobody listens on
    /// `rx` of the queue. This can happen, e.g. if `AlertService::init()` has
    /// not been called before.
    pub fn push_debug_str(mut msg: String) -> Result<(), SendError<String>> {
        msg.push_str(&DIALOG_ERR_TAIL);

        Self::push_str(msg)
    }

    #[inline]
    /// Pushes `msg` into queue. In case the message queue is full, the method
    /// blocks until there is more free space. Make sure to initialize before
    /// with `AlertService::init()` Returns an `SendError` if nobody listens on
    /// `rx` of the queue. This can happen, e.g. if `AlertService::init()` has
    /// not been called before.
    pub fn push_str(msg: String) -> Result<(), SendError<String>> {
        let (tx, _) = &*MESSAGE_CHANNEL;
        tx.send(msg)
    }

    #[inline]
    /// Pops up an error message box and prints `msg`.
    /// Blocks until the user closes the window.
    fn print_error(msg: &str) {
        let _ = msgbox::create(&*DIALOG_TITLE_LINE, msg, IconType::Info);
    }
}