desktopd 1.0.0

Browse sway/i3 windows and browser tabs.
Documentation
use futures::{SinkExt, StreamExt};

use async_std::task;
use async_tungstenite::async_std::connect_async;
use async_tungstenite::tungstenite::protocol::Message;
use desktopd::browser::*;
use desktopd::message::*;
use notify_rust::Notification;
use skim::prelude::*;
use tabular::{Row, Table};
use url::Url;

struct Wrapper {
    client: DesktopdClient,
    line: String,
}

impl SkimItem for Wrapper {
    fn text(&self) -> Cow<str> {
        Cow::Borrowed(&self.line)
    }
}

type SinkHole = futures::stream::SplitSink<
    async_tungstenite::WebSocketStream<async_std::net::TcpStream>,
    async_tungstenite::tungstenite::Message,
>;

async fn run(tx_item: SkimItemSender) -> SinkHole {
    let (ws_stream, _) = connect_async("ws://127.0.0.1:8080")
        .await
        .unwrap_or_else(|e| {
            Notification::new()
                .summary("desktopd")
                .body("Error: could not connect to daemon.")
                .show()
                .expect("Could not show notification");
            panic!("Fatal error: {}", e)
        });

    let init = DesktopdMessage::Connect(ConnectionType::Cli);
    let msg = Message::Text(serde_json::to_string(&init).unwrap());

    let (mut write, mut read) = ws_stream.split();

    write.send(msg).await.expect("Could not send init message");

    let response = read.next().await.expect("No response").expect("Error");

    let msg: DesktopdMessage = response
        .to_text()
        .map(|txt| serde_json::from_str(txt))
        .expect("Could not parse")
        .expect("Could not parse");

    if let DesktopdMessage::ClientList { data } = msg {
        let mut table = Table::new("{:<} {:<} {:<}");

        for item in &data {
            let row = match item {
                DesktopdClient::Window { data } => {
                    let id_or_class = if data.app_id.len() == 0 {
                        data.class.to_owned()
                    } else {
                        data.app_id.to_owned()
                    };

                    Row::new()
                        .with_cell("app")
                        .with_cell(&id_or_class)
                        .with_cell(&data.name)
                }

                DesktopdClient::Tab { data } => {
                    let formatted_title = if data.title.len() > 80 {
                        format!("{}...", &data.title[0..80])
                    } else {
                        data.title.clone()
                    };

                    let mut url = Url::parse(&data.url).unwrap();
                    url.set_path("");
                    url.set_query(None);
                    url.set_fragment(None);

                    Row::new()
                        .with_cell("tab")
                        .with_cell(formatted_title)
                        .with_cell(url.to_string())
                }
            };
            table.add_row(row);
        }

        table
            .to_string()
            .split("\n")
            .zip(data)
            .map(|(line, client)| Wrapper {
                client,
                line: line.to_owned(),
            })
            .for_each(|wrap| tx_item.send(Arc::new(wrap)).unwrap())
    }

    write
}

#[async_std::main]
async fn main() {
    let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();

    let mut write_handle = task::block_on(run(tx_item.clone()));

    drop(tx_item);

    let options = SkimOptionsBuilder::default()
        .multi(false)
        .preview(None)
        .build()
        .unwrap();

    let selected_items = Skim::run_with(&options, Some(rx_item))
        .map(|out| match out.final_key {
            Key::ESC => Vec::new(),
            _ => out.selected_items,
        })
        .unwrap_or_else(|| Vec::new());

    for item in selected_items.iter() {
        if let Some(wrapper) = (**item).as_any().downcast_ref::<Wrapper>() {
            use CliRequest as Req;
            use DesktopdClient as DC;
            use DesktopdMessage as DM;
            let command = match &wrapper.client {
                DC::Window { data } => DM::CliRequest(Req::FocusWindow { id: data.id }),

                DC::Tab { data } => DM::CliRequest(Req::FocusTab(BrowserTabRef {
                    tab_id: data.id,
                    window_id: data.window_id,
                })),
            };

            let msg = Message::Text(serde_json::to_string(&command).unwrap());

            write_handle
                .send(msg)
                .await
                .expect("could not send message");
        }
    }
}