use ratatui::{prelude::Stylize, style::Color, text::Span};
use serde::Deserialize;
use std::process::Command;
use crate::logging;
#[derive(Deserialize, Debug)]
struct Window {
address: String,
class: String,
title: String,
workspace: Workspace,
}
#[derive(Deserialize, Debug)]
struct ActiveWindow {
address: String,
}
#[derive(Deserialize, Debug)]
struct Workspace {
id: i32,
}
#[derive(Debug, Clone)]
pub struct WindowInfo {
address: String,
icon: String,
class: String,
title: String,
workspace_id: i32,
}
#[derive(Debug, Default, Clone)]
pub struct Windows {
pub windows: Vec<WindowInfo>,
active_window: String,
}
impl Windows {
pub fn new() -> Self {
let (windows, active_window) = get_windows().unwrap_or_default();
Self {
windows,
active_window,
}
}
pub fn update(&mut self) {
let (windows, active_window) = get_windows().unwrap_or_default();
self.windows = windows;
self.active_window = active_window;
}
pub fn render_as_spans(&self, colorize: bool) -> Vec<Span<'_>> {
self.windows
.iter()
.map(|w| {
if w.address == self.active_window {
if colorize {
let (bg_color, fg_color) = get_brand_color(&w.class, &w.title);
Span::raw(format!(" {} ", w.icon)).bg(bg_color).fg(fg_color)
} else {
Span::raw(format!(" {} ", w.icon))
.bg(Color::White)
.fg(Color::Black)
}
} else if colorize {
Span::raw(format!(" {} ", w.icon)).fg(Color::White)
} else {
Span::raw(format!(" {} ", w.icon)).fg(Color::White)
}
})
.collect::<Vec<Span>>()
}
}
fn get_windows() -> Option<(Vec<WindowInfo>, String)> {
let clients_output = Command::new("hyprctl")
.args(["clients", "-j"])
.output()
.expect("failed to get clients");
if !clients_output.status.success() {
logging::log_component_error(
"WINDOWS",
str::from_utf8(&clients_output.stderr).unwrap_or("unknown error"),
);
return None;
}
let clients_stdout = str::from_utf8(&clients_output.stdout).unwrap();
let windows: Vec<Window> =
serde_json::from_str(clients_stdout).expect("failed to parse windows");
let active_output = Command::new("hyprctl")
.args(["activewindow", "-j"])
.output()
.expect("failed to get active window");
let active_address = if active_output.status.success() {
let active_stdout = str::from_utf8(&active_output.stdout).unwrap();
let active_window: ActiveWindow =
serde_json::from_str(active_stdout).unwrap_or(ActiveWindow {
address: String::new(),
});
active_window.address
} else {
String::new()
};
let mut window_infos: Vec<WindowInfo> = windows
.iter()
.filter(|w| w.workspace.id > 0) .map(|w| WindowInfo {
address: w.address.clone(),
icon: get_app_icon(&w.class, &w.title),
class: w.class.clone(),
title: w.title.clone(),
workspace_id: w.workspace.id,
})
.collect();
window_infos.sort_by_key(|w| (w.workspace_id, w.address.clone()));
let window_infos = window_infos;
Some((window_infos, active_address))
}
fn get_app_icon(class: &str, title: &str) -> String {
let title_lower = title.to_lowercase();
if title_lower.starts_with("nvim") || title_lower.contains("neovim") {
return "".to_string();
} else if title_lower.starts_with("vim") {
return "".to_string();
} else if title_lower.starts_with("emacs") {
return "".to_string();
} else if title_lower.starts_with("nano") {
return "".to_string();
} else if title_lower.starts_with("htop") || title_lower.starts_with("btop") {
return "".to_string();
} else if title_lower.starts_with("yazi") {
return "".to_string();
} else if title_lower.starts_with("ranger") || title_lower.starts_with("lf") {
return "".to_string();
} else if title_lower.starts_with("git") {
return "".to_string();
} else if title_lower.starts_with("man") {
return "".to_string();
} else if title_lower.starts_with("ssh") {
return "".to_string();
} else if title_lower.starts_with("cmus") || title_lower.starts_with("ncmpcpp") {
return "".to_string();
}
match class.to_lowercase().as_str() {
"firefox" | "firefox-developer-edition" => "".to_string(),
"google-chrome" | "chrome" => "".to_string(),
"chromium" => "".to_string(),
"brave-browser" => "".to_string(),
"librewolf" => "".to_string(),
"vivaldi" => "".to_string(),
"opera" => "".to_string(),
"edge" => "".to_string(),
"helium" => "".to_string(),
"kitty" => "".to_string(),
"alacritty" => "".to_string(),
"gnome-terminal" => "".to_string(),
"konsole" => "".to_string(),
"xterm" => "".to_string(),
"neovide" => "".to_string(),
"code" | "code-oss" => "".to_string(),
"sublime_text" => "".to_string(),
"zathura" => "".to_string(),
"evince" => "".to_string(),
"okular" => "".to_string(),
"qpdfview" => "".to_string(),
"mupdf" => "".to_string(),
"qview" => "".to_string(),
"feh" => "".to_string(),
"nomacs" => "".to_string(),
"gwenview" => "".to_string(),
"eog" => "".to_string(),
"sxiv" => "".to_string(),
"mpv" => "".to_string(),
"vlc" => "".to_string(),
"smplayer" => "".to_string(),
"celluloid" => "".to_string(),
"spotify" => "".to_string(),
"rhythmbox" => "".to_string(),
"audacious" => "".to_string(),
"cmus" => "".to_string(),
"ncmpcpp" => "".to_string(),
"gimp" => "".to_string(),
"aseprite" => "".to_string(),
"inkscape" => "".to_string(),
"blender" => "".to_string(),
"krita" => "".to_string(),
"obs" => "".to_string(),
"discord" => "".to_string(),
"telegramdesktop" | "telegram" => "".to_string(),
"slack" => "".to_string(),
"signal" => "".to_string(),
"thunderbird" => "".to_string(),
"geary" => "".to_string(),
"thunar" => "".to_string(),
"dolphin" => "".to_string(),
"nautilus" => "".to_string(),
"pcmanfm" => "".to_string(),
"nvtop" => "".to_string(),
"pavucontrol" => "".to_string(),
"networkmanager_dmenu" => "".to_string(),
"libreoffice-writer" => "".to_string(),
"libreoffice-calc" => "".to_string(),
"libreoffice-impress" => "".to_string(),
"onlyoffice-desktopeditors" => "".to_string(),
"postman" => "".to_string(),
"insomnia" => "".to_string(),
"gitkraken" => "".to_string(),
"figma-linux" => "".to_string(),
"wine" | "winecfg" => "".to_string(),
"steam" => "".to_string(),
"lutris" => "".to_string(),
"heroic" => "".to_string(),
"minecraft" => "".to_string(),
_ => "".to_string(),
}
}
fn get_brand_color(class: &str, title: &str) -> (Color, Color) {
let title_lower = title.to_lowercase();
let class_lower = class.to_lowercase();
if title_lower.starts_with("nvim") || title_lower.contains("neovim") {
return (Color::Rgb(0, 107, 84), Color::White); } else if title_lower.starts_with("vim") {
return (Color::Rgb(19, 134, 71), Color::White); } else if title_lower.starts_with("emacs") {
return (Color::Rgb(146, 35, 127), Color::White); } else if title_lower.starts_with("htop") || title_lower.starts_with("btop") {
return (Color::Rgb(255, 152, 0), Color::Black); } else if title_lower.starts_with("yazi") {
return (Color::Rgb(255, 200, 87), Color::Black); } else if title_lower.starts_with("ranger") || title_lower.starts_with("lf") {
return (Color::Rgb(41, 128, 185), Color::White); } else if title_lower.starts_with("git") {
return (Color::Rgb(240, 80, 50), Color::White); } else if title_lower.starts_with("ssh") {
return (Color::Rgb(0, 100, 200), Color::White); } else if title_lower.starts_with("cmus") || title_lower.starts_with("ncmpcpp") {
return (Color::Rgb(29, 185, 84), Color::White); }
match class_lower.as_str() {
"firefox" | "firefox-developer-edition" | "librewolf" => {
(Color::Rgb(255, 119, 0), Color::Black)
} "google-chrome" | "chrome" | "chromium" => (Color::Rgb(66, 133, 244), Color::Black), "brave-browser" => (Color::Rgb(250, 72, 41), Color::White), "vivaldi" | "opera" => (Color::Rgb(235, 90, 70), Color::White), "edge" => (Color::Rgb(0, 120, 215), Color::White), "helium" => (Color::Rgb(0, 184, 169), Color::White),
"kitty" => (Color::Rgb(103, 117, 140), Color::White), "alacritty" | "gnome-terminal" | "konsole" | "xterm" => {
(Color::Rgb(46, 52, 64), Color::White)
}
"neovide" => (Color::Rgb(0, 107, 84), Color::White), "code" | "code-oss" => (Color::Rgb(27, 127, 243), Color::White), "sublime_text" => (Color::Rgb(255, 93, 0), Color::White),
"zathura" | "evince" | "okular" | "qpdfview" | "mupdf" => {
(Color::Rgb(198, 40, 40), Color::White)
}
"qview" | "feh" | "nomacs" | "gwenview" | "eog" | "sxiv" => {
(Color::Rgb(156, 39, 176), Color::White)
}
"mpv" | "vlc" | "smplayer" | "celluloid" => (Color::Rgb(237, 101, 46), Color::White),
"spotify" | "rhythmbox" | "audacious" => (Color::Rgb(29, 185, 84), Color::White),
"gimp" | "krita" => (Color::Rgb(103, 72, 145), Color::White), "aseprite" => (Color::Rgb(255, 255, 255), Color::Black), "inkscape" => (Color::Rgb(0, 116, 178), Color::White), "blender" => (Color::Rgb(245, 129, 49), Color::White), "obs" => (Color::Rgb(146, 52, 220), Color::White),
"discord" => (Color::Rgb(88, 101, 242), Color::White), "telegramdesktop" | "telegram" => (Color::Rgb(39, 156, 204), Color::White), "slack" => (Color::Rgb(254, 0, 84), Color::White), "signal" => (Color::Rgb(83, 189, 238), Color::White), "thunderbird" | "geary" => (Color::Rgb(0, 112, 193), Color::White),
"thunar" | "dolphin" | "nautilus" | "pcmanfm" => (Color::Rgb(41, 128, 185), Color::White),
"nvtop" => (Color::Rgb(0, 173, 181), Color::White), "pavucontrol" => (Color::Rgb(233, 84, 32), Color::White), "networkmanager_dmenu" => (Color::Rgb(0, 184, 169), Color::White),
"libreoffice-writer" | "onlyoffice-desktopeditors" => {
(Color::Rgb(18, 52, 86), Color::White)
} "libreoffice-calc" => (Color::Rgb(43, 87, 135), Color::White), "libreoffice-impress" => (Color::Rgb(233, 63, 51), Color::White),
"postman" => (Color::Rgb(255, 89, 94), Color::White), "insomnia" => (Color::Rgb(148, 66, 156), Color::White), "gitkraken" => (Color::Rgb(64, 84, 178), Color::White), "figma-linux" => (Color::Rgb(0, 112, 243), Color::White), "wine" | "winecfg" => (Color::Rgb(143, 31, 35), Color::White),
"steam" => (Color::Rgb(0, 47, 71), Color::White), "lutris" => (Color::Rgb(201, 32, 44), Color::White), "heroic" => (Color::Rgb(162, 49, 162), Color::White), "minecraft" => (Color::Rgb(46, 125, 50), Color::White),
_ => (Color::Gray, Color::White),
}
}