leftwm 0.5.4

A window manager for Adventurers
Documentation
use clap::{arg, command};
use leftwm_core::errors::{LeftError, Result};
use leftwm_core::models::dto::{DisplayState, ManagerState};
use liquid::Template;
use std::ffi::OsStr;
use std::path::Path;
use std::str;
use tokio::fs;
use tokio::io::{AsyncBufReadExt, BufReader, Lines};
use tokio::net::UnixStream;
use xdg::BaseDirectories;

type Partials = liquid::partials::EagerCompiler<liquid::partials::InMemorySource>;

#[tokio::main]
async fn main() -> Result<()> {
    let matches = get_command().get_matches();

    let template_file = matches.get_one::<String>("template");
    let string_literal = matches.get_one::<String>("string");
    let ws_id = matches.get_one("workspace").copied();
    let newline = matches.get_flag("newline");
    let once = matches.get_flag("quit");

    let mut stream_reader = stream_reader().await?;
    if let Some(template_file) = template_file {
        let path = Path::new(template_file);
        let partials = get_partials(path.parent()).await?;
        let template_str = fs::read_to_string(template_file).await?;
        let template = get_parsed_template(&template_str, Some(partials))?;
        while let Some(line) = stream_reader.next_line().await? {
            let _droppable = template_handler(&template, newline, ws_id, &line);
            if once {
                break;
            }
        }
    } else if let Some(string_literal) = string_literal {
        let template = get_parsed_template(string_literal, None)?;
        while let Some(line) = stream_reader.next_line().await? {
            let _droppable = template_handler(&template, newline, ws_id, &line);
            if once {
                break;
            }
        }
    } else {
        while let Some(line) = stream_reader.next_line().await? {
            let _droppable2 = raw_handler(&line);
            if once {
                break;
            }
        }
    }

    Ok(())
}

/// Given some string literal and optional partials, calculate the template and return a useful
/// error
fn get_parsed_template(string_literal: &str, partials: Option<Partials>) -> Result<Template> {
    let mut parser = liquid::ParserBuilder::with_stdlib();
    if let Some(partials) = partials {
        parser = parser.partials(partials);
    }

    match parser
        .build()
        .expect("Unable to build template")
        .parse(string_literal)
    {
        Ok(template) => Ok(template),
        Err(err) => {
            eprintln!("{err}");

            Err(LeftError::LiquidParsingError)
        }
    }
}

async fn get_partials(dir: Option<&Path>) -> Result<Partials> {
    let mut partials = Partials::empty();
    match dir {
        Some(d) => {
            let entries = fs::read_dir(d).await?;

            let partial_paths = partials_in_dir_entries(entries).await?;

            for path in partial_paths {
                partials.add(
                    path.path().as_path().to_str().unwrap_or(" "),
                    fs::read_to_string(path.path()).await?,
                );
            }
            Ok(partials)
        }
        None => Ok(partials),
    }
}

async fn partials_in_dir_entries(mut entries: fs::ReadDir) -> Result<Vec<fs::DirEntry>> {
    let mut partial_paths = vec![];
    while let Some(entry) = entries.next_entry().await? {
        let f_n = entry.file_name();
        if is_partial_filename(&f_n) {
            partial_paths.push(entry);
        }
    }
    Ok(partial_paths)
}

fn is_partial_filename(filename: &OsStr) -> bool {
    let f_n = filename.to_str().unwrap_or(" ");
    f_n.starts_with('_') && f_n.ends_with(".liquid")
}

fn raw_handler(line: &str) -> Result<()> {
    let s: ManagerState = serde_json::from_str(line)?;
    let display: DisplayState = s.into();
    let json = serde_json::to_string(&display)?;
    println!("{json}");
    Ok(())
}

fn template_handler(
    template: &liquid::Template,
    newline: bool,
    ws_id: Option<usize>,
    line: &str,
) -> Result<()> {
    let s: ManagerState = serde_json::from_str(line)?;
    let display: DisplayState = s.into();

    let globals = if let Some(ws_id) = ws_id {
        let json = serde_json::to_string(&display.workspaces[ws_id])?;
        let workspace: liquid::model::Object = serde_json::from_str(&json)?;
        let mut globals = liquid::model::Object::new();
        globals.insert(
            "window_title".into(),
            liquid::model::Value::scalar(display.window_title),
        );
        globals.insert("workspace".into(), liquid::model::Value::Object(workspace));
        globals
    } else {
        let json = serde_json::to_string(&display)?;
        let globals: liquid::model::Object = serde_json::from_str(&json)?;
        globals
    };

    let mut output = template.render(&globals).unwrap();
    output = str::replace(&output, "\r", "");
    // We use newline rather than !newline to avoid negative logic,
    // but note the difference between print! and println!. Trying to skip println!
    // will result in theme degradation, as in #263.
    if newline {
        print!("{output}");
    } else {
        output = str::replace(&output, "\n", "");
        println!("{output}");
    }
    Ok(())
}

async fn stream_reader() -> Result<Lines<BufReader<UnixStream>>> {
    let base = BaseDirectories::with_prefix("leftwm")?;
    let socket_file = base.place_runtime_file("current_state.sock")?;
    let stream = UnixStream::connect(socket_file).await?;
    Ok(BufReader::new(stream).lines())
}

fn get_command() -> clap::Command {
    command!("LeftWM State")
        .about("Prints out the current state of LeftWM")
        .help_template(leftwm::utils::get_help_template())
        .args(&[
            arg!(-t --template [FILE] "A liquid template to use for the output"),
            arg!(-s --string [STRING] "Use a liquid template string literal to use for the output"),
            arg!(-w --workspace [WS_NUM] "render only info about a given workspace [0..]")
                .value_parser(clap::value_parser!(usize)),
            arg!(-n --newline "Print new lines in the output"),
            arg!(-q --quit "Prints the state once and quits"),
        ])
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn correctly_identifies_partial_template_filenames() {
        let file_names = [
            "main.liquid",
            "_partial.liquid",
            "\u{7c0}nonascii-in-filename.liquid", // first char U07C0
            "1_partial.liquid",
            "_liquid.txt",
        ];
        let partials = file_names
            .iter()
            .map(|f_n| OsStr::new(*f_n))
            .filter(|f_n| is_partial_filename(f_n))
            .collect::<Vec<&OsStr>>();

        assert!(partials == vec![OsStr::new("_partial.liquid")]);
    }
}