hyprscratch 0.2.3

Scratchpad functionality for Hyprland
use hyprland::data::{Client, Clients, Workspace};
use hyprland::dispatch::*;
use hyprland::event_listener::EventListenerMutable;
use hyprland::prelude::*;
use hyprland::Result;
use regex::Regex;
use std::fs::remove_file;
use std::io::prelude::*;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::Path;

fn summon(title: &str, cmd: &str) -> Result<()> {
    let clients_with_title = &Clients::get()?
        .filter(|x| x.initial_title == title)
        .collect::<Vec<_>>();

    if clients_with_title.is_empty() {
        hyprland::dispatch!(Exec, cmd)?;
    } else {
        let pid = clients_with_title[0].pid as u32;
        if clients_with_title[0].workspace.id == Workspace::get_active()?.id {
            hyprland::dispatch!(FocusWindow, WindowIdentifier::ProcessId(pid))?;
        } else {
            hyprland::dispatch!(
                MoveToWorkspaceSilent,
                WorkspaceIdentifierWithSpecial::Relative(0),
                Some(WindowIdentifier::ProcessId(pid))
            )?;
            hyprland::dispatch!(FocusWindow, WindowIdentifier::ProcessId(pid))?;
        }
        Dispatch::call(hyprland::dispatch::DispatchType::BringActiveToTop)?;
    }
    Ok(())
}

fn scratchpad(title: &str, args: &[String]) -> Result<()> {
    let cl = Client::get_active()?;
    let mut stream = UnixStream::connect("/tmp/hyprscratch.sock")?;
    let mut titles = String::new();
    stream.read_to_string(&mut titles)?;

    match cl {
        Some(cl) => {
            if (!args.contains(&"stack".to_string()) && (cl.floating && titles.contains(&cl.title)))
                || cl.initial_title == title
            {
                hyprland::dispatch!(
                    MoveToWorkspaceSilent,
                    WorkspaceIdentifierWithSpecial::Id(42),
                    None
                )?;
            }

            if cl.initial_title != title {
                summon(title, &args[0])?;
            }
        }
        None => summon(title, &args[0])?,
    }
    Ok(())
}

fn hideall() -> Result<()> {
    Clients::get()?
        .filter(|x| x.floating && x.workspace.id == Workspace::get_active().unwrap().id)
        .for_each(|x| {
            hyprland::dispatch!(
                MoveToWorkspaceSilent,
                WorkspaceIdentifierWithSpecial::Id(42),
                Some(WindowIdentifier::ProcessId(x.pid as u32))
            )
            .unwrap()
        });
    Ok(())
}

fn get_config() {
    let [titles, commands, options] = parse_config();
    titles.iter().enumerate().for_each(|(i, x)| {
        println!(
            "Title: {}, Command: {}, Options: {}",
            x, commands[i], options[i]
        )
    });
}

fn dequote(string: &String) -> String {
    let dequoted = match &string[0..1] {
        "\"" | "'" => &string[1..string.len() - 1],
        _ => string,
    };
    dequoted.to_string()
}

fn parse_config() -> [Vec<String>; 3] {
    let lines_with_hyprscratch_regex = Regex::new("hyprscratch.+").unwrap();
    let hyprscratch_args_regex = Regex::new("\".+?\"|'.+?'|\\w+").unwrap();
    let mut buf: String = String::new();

    let mut titles: Vec<String> = Vec::new();
    let mut commands: Vec<String> = Vec::new();
    let mut options: Vec<String> = Vec::new();

    std::fs::File::open(format!(
        "{}/.config/hypr/hyprland.conf",
        std::env::var("HOME").unwrap()
    ))
    .unwrap()
    .read_to_string(&mut buf)
    .unwrap();

    let lines: Vec<&str> = lines_with_hyprscratch_regex
        .find_iter(&buf)
        .map(|x| x.as_str())
        .collect();

    for line in lines {
        let parsed_line = &hyprscratch_args_regex
            .find_iter(line)
            .map(|x| x.as_str().to_string())
            .collect::<Vec<_>>()[..];

        if parsed_line.len() == 1 {
            continue;
        }

        match parsed_line[1].as_str() {
            "clean" | "hideall" => (),
            _ => {
                titles.push(dequote(&parsed_line[1]));
                commands.push(dequote(&parsed_line[2]));

                if parsed_line.len() > 3 {
                    options.push(parsed_line[3..].join(" "));
                } else {
                    options.push(String::from(""));
                }
            }
        };
    }
    [titles, commands, options]
}

fn move_floating(titles: Vec<String>) {
    if let Ok(clients) = Clients::get() {
        clients
            .filter(|x| x.floating && x.workspace.id != 42 && titles.contains(&x.initial_title))
            .for_each(|x| {
                hyprland::dispatch!(
                    MoveToWorkspaceSilent,
                    WorkspaceIdentifierWithSpecial::Id(42),
                    Some(WindowIdentifier::ProcessId(x.pid as u32))
                )
                .unwrap()
            })
    }
}

fn clean(cli_options: &[String], titles: &[String], options: &[String]) -> Result<()> {
    let mut ev = EventListenerMutable::new();

    let titles_clone = titles.to_owned();
    let unshiny_titles: Vec<String> = titles
        .iter()
        .cloned()
        .enumerate()
        .filter(|&(i, _)| !options[i].contains("shiny"))
        .map(|(_, x)| x)
        .collect();

    ev.add_workspace_change_handler(move |_, _| {
        move_floating(titles_clone.clone());
    });

    if cli_options.contains(&"spotless".to_string()) {
        ev.add_active_window_change_handler(move |_, _| {
            if let Some(cl) = Client::get_active().unwrap() {
                if !cl.floating {
                    move_floating(unshiny_titles.clone());
                } else {
                    Dispatch::call(hyprland::dispatch::DispatchType::BringActiveToTop).unwrap();
                }
            }
        });
    }
    std::thread::spawn(|| ev.start_listener());
    Ok(())
}

fn autospawn(titles: &[String], commands: &[String], options: &[String]) {
    let clients = Clients::get()
        .unwrap()
        .map(|x| x.initial_title)
        .collect::<Vec<_>>();

    commands
        .iter()
        .enumerate()
        .filter(|&(i, _)| options[i].contains("onstart") && !clients.contains(&titles[i]))
        .for_each(|(_, x)| {
            hyprland::dispatch!(Exec, &x.replacen('[', "[workspace 42 silent;", 1)).unwrap()
        });
}

fn initialize(title: &str, args: &[String]) -> Result<()> {
    let mut cli_args = args.join(" ");
    cli_args.push_str(title);

    let [titles, commands, options] = parse_config();
    autospawn(&titles, &commands, &options);

    if cli_args.contains("clean") {
        clean(args, &titles, &options)?;
    }

    let path_to_sock = Path::new("/tmp/hyprscratch.sock");
    if path_to_sock.exists() {
        remove_file(path_to_sock)?;
    }

    let listener = UnixListener::bind(path_to_sock)?;
    for stream in listener.incoming() {
        match stream {
            Ok(mut stream) => {
                let titles_string = format!("{titles:?}");
                stream.write_all(titles_string.as_bytes())?;
            }
            Err(_) => {
                break;
            }
        };
    }
    Ok(())
}

fn main() -> Result<()> {
    let args = std::env::args().collect::<Vec<String>>();
    let title = match args.len() {
        0 | 1 => String::from(""),
        2.. => args[1].clone(),
    };

    let empty_sting_array = [String::new()];
    let cli_args = match args.len() {
        0..=2 => &empty_sting_array,
        3.. => &args[2..],
    };

    match title.as_str() {
        "clean" | "" => initialize(&title, cli_args)?,
        "get-config" => get_config(),
        "hideall" => hideall()?,
        _ => scratchpad(&title, cli_args)?,
    }
    Ok(())
}