use anyhow::Result;
use std::fs;
use std::path::PathBuf;
const CONFIG_TOML: &str = r#"# glance daemon configuration
# directories to watch for new files
watch_dirs = ["~/Pictures/Screenshots", "~/Downloads"]
# RTMIN+N signal to poke waybar on new file
signal_number = 8
# auto-dismiss the widget after N seconds
dismiss_seconds = 10
# ignore files with these suffixes (partial downloads, etc.)
ignore_suffixes = [".part", ".crdownload", ".tmp"]
# pixels from top of screen to below waybar (popup appears here)
bar_height = 57
# number of files to remember in history
history_size = 5
"#;
fn glance_bin() -> String {
std::env::current_exe()
.ok()
.and_then(|p| p.canonicalize().ok())
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| "glance".into())
}
fn waybar_module() -> String {
let bin = glance_bin();
format!(
r#"
"custom/glance": {{
"exec": "{bin} status",
"return-type": "json",
"interval": 5,
"signal": 8,
"on-click": "{bin} menu",
"on-click-right": "{bin} copy",
"on-scroll-up": "{bin} scroll up",
"on-scroll-down": "{bin} scroll down"
}}
"#
)
}
const WAYBAR_CSS: &str = r#"
/* glance widget */
#custom-glance {
padding: 0 8px;
color: #cdd6f4;
}
#custom-glance.active {
color: #a6e3a1;
}
#custom-glance.empty {
padding: 0;
}
"#;
fn config_base() -> PathBuf {
std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()))
.join(".config")
})
}
fn ok(msg: &str) {
eprintln!("\x1b[32m โ\x1b[0m {msg}");
}
fn skip(msg: &str) {
eprintln!("\x1b[90m ยท\x1b[0m {msg}");
}
fn contains(path: &PathBuf, needle: &str) -> bool {
fs::read_to_string(path)
.map(|s| s.contains(needle))
.unwrap_or(false)
}
fn setup_config() -> Result<()> {
let path = config_base().join("glance/config.toml");
if path.exists() {
skip(&format!("config already exists: {}", path.display()));
return Ok(());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, CONFIG_TOML)?;
ok(&format!("created {}", path.display()));
Ok(())
}
fn setup_waybar_module() -> Result<()> {
let base = config_base().join("waybar");
if !base.exists() {
skip("waybar config dir not found, skipping module setup");
return Ok(());
}
let config_file = ["config.jsonc", "config"]
.iter()
.map(|f| base.join(f))
.find(|p| p.exists());
let Some(config_file) = config_file else {
skip("no waybar config file found, skipping module setup");
return Ok(());
};
if contains(&config_file, "glance") {
skip("waybar module already configured");
return Ok(());
}
let modules_file = ["UserModules", "ModulesCustom"]
.iter()
.map(|f| base.join(f))
.find(|p| p.exists());
if let Some(mf) = modules_file {
if contains(&mf, "glance") {
skip("waybar module already in modules file");
return Ok(());
}
let content = fs::read_to_string(&mf)?;
if let Some(pos) = content.rfind('}') {
let module = waybar_module();
let mut new = String::with_capacity(content.len() + module.len() + 10);
let before = content[..pos].trim_end();
new.push_str(before);
let last_char = before.chars().rev().find(|c| !c.is_whitespace());
if last_char != Some('{') && last_char != Some(',') {
new.push(',');
}
new.push_str(&module);
new.push_str("}\n");
fs::write(&mf, new)?;
ok(&format!("added waybar module to {}", mf.display()));
}
} else {
let mut content = fs::read_to_string(&config_file)?;
content.push_str(&format!(
"\n// Add this to your modules config:\n// {}\n",
waybar_module().trim().replace('\n', "\n// ")
));
fs::write(&config_file, content)?;
ok(&format!(
"added waybar module snippet to {}",
config_file.display()
));
}
if let Ok(content) = fs::read_to_string(&config_file) {
if content.contains("modules-right")
&& !content.contains("custom/glance")
{
let new = content.replacen(
"\"modules-right\": [",
"\"modules-right\": [\n\t\"custom/glance\",",
1,
);
if new != content {
fs::write(&config_file, new)?;
ok("added custom/glance to modules-right");
}
}
}
Ok(())
}
fn setup_waybar_css() -> Result<()> {
let path = config_base().join("waybar/style.css");
if !path.exists() {
skip("waybar style.css not found, skipping CSS setup");
return Ok(());
}
if contains(&path, "custom-glance") {
skip("waybar CSS already has glance styles");
return Ok(());
}
let mut content = fs::read_to_string(&path)?;
content.push_str(WAYBAR_CSS);
fs::write(&path, content)?;
ok(&format!("appended styles to {}", path.display()));
Ok(())
}
fn setup_hyprland() -> Result<()> {
let path = config_base().join("hypr/hyprland.conf");
if !path.exists() {
skip("hyprland.conf not found, skipping autostart setup");
return Ok(());
}
if contains(&path, "glance") {
skip("hyprland autostart already configured");
return Ok(());
}
let mut content = fs::read_to_string(&path)?;
let bin = glance_bin();
content.push_str(&format!("\nexec-once = {bin} watch\n"));
content.push_str(&format!("bind = SUPER, V, exec, {bin} drag\n"));
fs::write(&path, content)?;
ok("added exec-once and SUPER+V keybind");
Ok(())
}
pub fn run() -> Result<()> {
eprintln!("\n glance init\n");
setup_config()?;
setup_waybar_module()?;
setup_waybar_css()?;
setup_hyprland()?;
eprintln!("\n Done! Restart Waybar to activate: pkill waybar && waybar &\n");
Ok(())
}