use crate::logs::*;
use crate::utils::*;
use crate::{DEFAULT_LOGFILE, DEFAULT_SOCKET};
use hyprland::Result;
use std::cmp::max;
use std::fs::File;
use std::io::{self, prelude::*};
use std::os::unix::net::UnixStream;
use std::process::{Command, Stdio};
use std::vec;
type ParsedConfig<'a> = (&'a str, Vec<Vec<&'a str>>, Vec<Vec<&'a str>>);
fn print_table_outline(symbols: (char, char, char), widths: &[usize]) {
let mut outline_str = format!("{}", symbols.0);
let length = widths.len();
for (i, width) in widths.iter().enumerate() {
outline_str.push_str(&"─".repeat(width + 2));
if i < length - 1 {
outline_str.push(symbols.1);
} else {
outline_str.push(symbols.2);
}
}
println!("{outline_str}");
}
fn color(str: &str) -> String {
let col_titles = [
"Name",
"Title/Class",
"Command",
"Options",
"Group",
"Scratchpads",
];
let mut colored_str = str
.replace(';', "?")
.replace('[', "[\x1b[0;36m")
.replace(']', "\x1b[0;0m]")
.replace('?', "\x1b[0;0m;\x1b[0;36m");
if str.contains(".conf") {
colored_str = colored_str.replace(str, &format!("\x1b[0;35m{str}\x1b[0;0m"));
}
for title in col_titles {
colored_str = colored_str.replace(title, &format!("\x1b[0;33m{title}\x1b[0;0m"));
}
colored_str
}
fn fancify(x: usize, str: &str) -> String {
let str = if str.len() <= x {
str.to_string() + &" ".repeat(max(x - str.chars().count(), 0))
} else {
str[..x - 3].to_string() + "..."
};
color(&str)
}
fn print_table_row(data: &[&str], widths: &[usize]) {
if data.len() != widths.len() {
return;
}
let mut row_str = "│".to_string();
for (width, field) in widths.iter().zip(data) {
row_str.push(' ');
row_str.push_str(&fancify(*width, field));
row_str.push(' ');
row_str.push('│');
}
println!("{row_str}");
}
fn max_len(xs: &Vec<&str>, min: usize, max: usize) -> usize {
xs.iter()
.map(|x| x.chars().count())
.max()
.unwrap_or_default()
.max(min)
.min(max)
}
fn get_config_data(socket: Option<&str>) -> Result<String> {
let mut stream = UnixStream::connect(socket.unwrap_or(DEFAULT_SOCKET))?;
stream.write_all("get-config?".as_bytes())?;
let mut buf = String::new();
stream.read_to_string(&mut buf)?;
Ok(buf)
}
fn print_group_table(group_data: &[Vec<&str>]) {
let [names, scratchpadss] = &group_data[0..2] else {
return;
};
let max_chars = 80;
let field_widths = vec![
max_len(names, 5, max_chars),
max_len(scratchpadss, 11, max_chars),
];
print_table_outline(('┌', '┬', '┐'), &field_widths);
print_table_row(&["Group", "Scratchpads"], &field_widths);
print_table_outline(('├', '┼', '┤'), &field_widths);
for (name, scratchpads) in names.iter().zip(scratchpadss) {
print_table_row(&[name, scratchpads], &field_widths);
}
print_table_outline(('└', '┴', '┘'), &field_widths);
}
fn get_centered_conf(conf: &str, width: usize) -> String {
let config = if width <= conf.len() {
conf.split('/').next_back().unwrap_log(file!(), line!())
} else {
conf
};
let center_fix = if width % 2 == 0 {
config.len() + config.len() % 2
} else {
config.len() - 1
};
format!(
"{}{}{}",
" ".repeat(max(width / 2 - config.len() / 2, 0)),
config,
" ".repeat(max((width - center_fix) / 2, 0))
)
}
fn print_scratchpad_table(scratchpad_data: &[Vec<&str>], conf: &str) {
let [names, titles, commands, options] = &scratchpad_data[0..4] else {
return;
};
let max_chars = 60;
let field_widths = vec![
max_len(names, 4, max_chars),
max_len(titles, 11, max_chars),
max_len(commands, 7, max_chars),
max_len(options, 7, max_chars),
];
let config_str = get_centered_conf(conf, field_widths.iter().sum::<usize>() + 9);
print_table_outline(('┌', '─', '┐'), &[config_str.len()]);
print_table_row(&[&config_str], &[config_str.len()]);
print_table_outline(('├', '┬', '┤'), &field_widths);
print_table_row(
&["Name", "Title/Class", "Command", "Options"],
&field_widths,
);
print_table_outline(('├', '┼', '┤'), &field_widths);
for (((name, title), command), option) in names.iter().zip(titles).zip(commands).zip(options) {
print_table_row(&[name, title, command, option], &field_widths);
}
print_table_outline(('└', '┴', '┘'), &field_widths);
}
fn print_raw_data(data: &[Vec<&str>]) {
for row in (0..data[0].len())
.map(|i| data.iter().map(|inner| inner[i]).collect::<Vec<&str>>())
.collect::<Vec<Vec<&str>>>()
{
println!("{}", row.join(" "));
}
}
fn print_raw((conf, scratchpad_data, group_data): ParsedConfig) {
println!("{conf}\n");
println!("## SCRATCHPADS ##\n");
print_raw_data(&scratchpad_data);
if !group_data.is_empty() {
println!("\n## GROUPS ##\n");
print_raw_data(&group_data);
}
}
fn print_tables((conf, scratchpad_data, group_data): ParsedConfig) {
print_scratchpad_table(&scratchpad_data, conf);
if !group_data.is_empty() {
print_group_table(&group_data);
}
}
fn parse_data(data: &str, field_num: usize) -> Vec<Vec<&str>> {
let parsed_data = &data
.splitn(field_num, '\u{2C01}')
.map(|x| x.split('\u{2C02}').collect::<Vec<_>>())
.collect::<Vec<_>>()[0..field_num];
if parsed_data.len() < field_num {
let _ = log("Config data could not be parsed".into(), Error);
return vec![];
}
parsed_data.to_vec()
}
fn parse_config_data(data: &str) -> ParsedConfig {
let (sc_fields, g_fields) = (4, 2);
match data.splitn(3, '\u{2C00}').collect::<Vec<_>>()[..] {
[c, scd, gd] => (c, parse_data(scd, sc_fields), parse_data(gd, g_fields)),
[c, scd] => (c, parse_data(scd, sc_fields), vec![]),
_ => {
let _ = log("Could not get configuration data".into(), Error);
("", vec![], vec![])
}
}
}
pub fn print_config(socket: Option<&str>, raw: bool) -> Result<()> {
let data = get_config_data(socket)?;
let parsed_data = parse_config_data(&data);
if raw {
print_raw(parsed_data);
} else {
print_tables(parsed_data);
}
Ok(())
}
fn get_log_data() -> Result<String> {
let mut file = File::open(DEFAULT_LOGFILE)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
Ok(buf)
}
pub fn print_logs(raw: bool) -> Result<()> {
if let Ok(data) = get_log_data() {
if raw {
println!("{}", data.trim());
return Ok(());
}
let log_str = data
.replace("ERROR", "\x1b[0;31mERROR\x1b[0;0m")
.replace("DEBUG", "\x1b[0;32mDEBUG\x1b[0;0m")
.replace("WARN", "\x1b[0;33mWARN\x1b[0;0m")
.replace("INFO", "\x1b[0;36mINFO\x1b[0;0m");
println!("{}", log_str.trim());
} else {
println!("Logs are empty");
}
Ok(())
}
pub fn print_full_raw(socket: Option<&str>) -> Result<()> {
println!("Hyprscratch v{}", env!("CARGO_PKG_VERSION"));
println!("### LOGS ###\n");
print_logs(true)?;
println!("\n### CONFIGURATION ###\n");
print_config(socket, true)?;
Ok(())
}
fn run_basic_menu(socket: Option<&str>, list: &str, action: &str) -> Result<()> {
print!("Exisiting scratchpads:\n{list}\nEnter scratchpad: ");
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
send_request(socket, action, name.trim())
}
pub fn menu(socket: Option<&str>, mode: &str, action: &str) -> Result<()> {
let mut stream = UnixStream::connect(socket.unwrap_or(DEFAULT_SOCKET))?;
stream.write_all("menu?".as_bytes())?;
let list = read_into_string(&mut stream)?;
let action = if action.is_empty() {
if mode == "toggle" || mode == "show" || mode == "hide" {
mode
} else {
"toggle"
}
} else {
action
};
let run_cmd = |cmd: &str, args: &[&str]| -> Result<()> {
let mut proc = Command::new(cmd)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
if let Some(ref mut stdin) = proc.stdin {
stdin.write_all(list.as_bytes())?;
}
let output = proc.wait_with_output()?;
if output.status.success() {
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if name.is_empty() {
let _ = log("No scratchpad given to menu".into(), Warn);
return Ok(());
}
send_request(socket, action, &name)?;
}
Ok(())
};
match mode {
"rofi" => run_cmd("rofi", &["-dmenu", "-i", "-p", "Hyprscratch"]),
"fzf" => run_cmd("fzf", &["--prompt", &format!("hyprscratch {action} ")]),
_ => run_basic_menu(socket, &list, action),
}
.unwrap_or_else(|e| {
let _ = log(format!("Menu {mode} was unsucessfull: {e}"), Warn);
});
Ok(())
}
pub fn print_help() {
println!(
"Usage:
Daemon:
hypscratch init [options...]
Scratchpads:
hyprscratch title command [options...]
DAEMON OPTIONS
clean Hide scratchpads on workspace change
spotless Hide scratchpads on focus change
eager Spawn scratchpads hidden on start
no-auto-reload Don't reload the configuration when the configuration file is updated
config </path/to/config> Specify a path to the configuration file
SCRATCHPAD OPTIONS
ephemeral Close the scratchpad when it is hidden
persist Prevent the scratchpad from being replaced when a new one is summoned
cover Prevent the scratchpad from replacing another one if one is already present
sticky Prevent the scratchpad from being hidden by 'clean'
shiny Prevent the scratchpad from being hidden by 'spotless'
lazy Prevent the scratchpad from being spawned by 'eager'
show Only creates or brings up the scratchpad
hide Only hides the scratchpad
poly Toggle all scratchpads matching the title simultaneously
pin Keep the scratchpad active through workspace changes
tiled Makes a tiled scratchpad instead of a floating one
monitor <id|name> Restrict the scratchpad to a specified monitor
group <name> Add the scratchpad to the specified group
special Use Hyprland's special workspace, ignores most other options
EXTRA COMMANDS
cycle [normal|special] Cycle between [only normal | only special] scratchpads
toggle <name> Toggles the scratchpad with the given name
show <name> Shows the scratchpad with the given name
hide <name> Hides the scratchpad with the given name
previous [show|hide] Spawn the previous non-active scratchpad
menu [fzf|rofi] Spawn a menu to search through and trigger scratchpads.
hide-all Hide all scratchpads
kill-all Close all scratchpads
reload (-r) [config] Update the config file
get-config (-g) Print parsed config file
kill (-k) Kill the hyprscratch daemon
logs (-l) Print log file contents
version (-v) Print current version
help (-h) Print this help message");
}