#![doc(html_logo_url = "https://github.com/kgilmer/elbey/blob/main/elbey.svg")]
mod app;
mod values;
use std::process::exit;
use std::sync::{Arc, Mutex};
use crate::values::*;
use anyhow::Context;
use app::{Elbey, ElbeyFlags};
use argh::FromArgs;
use elbey_cache::{delete_cache_dir_with_namespace, AppDescriptor, Cache};
use freedesktop_desktop_entry::{
current_desktop, default_paths, get_languages_from_env, DesktopEntry, Iter,
};
use iced::theme::{Custom, Palette};
use iced::{Color, Font, Pixels, Theme};
use iced_layershell::application;
use iced_layershell::reexport::{Anchor, KeyboardInteractivity, Layer};
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
use lazy_static::lazy_static;
lazy_static! {
pub(crate) static ref CACHE: Arc<Mutex<Cache>> = Arc::new(Mutex::new(
Cache::new_with_namespace(find_all_apps, CACHE_NAMESPACE,)
));
}
const CACHE_NAMESPACE: &str = "elbey";
#[derive(FromArgs)]
struct EbleyArgs {
#[argh(option)]
height: Option<u32>,
#[argh(option)]
width: Option<u32>,
#[argh(option)]
theme: Option<String>,
#[argh(option)]
filter_font_size: Option<u16>,
#[argh(option)]
entries_font_size: Option<u16>,
#[argh(option)]
icon_size: Option<u16>,
#[argh(option)]
hint: Option<String>,
#[argh(switch)]
list_search_paths: bool,
#[argh(switch)]
reset_cache: bool,
}
fn parse_theme(name: &str) -> Option<Theme> {
match name {
"CatppuccinFrappe" => Some(Theme::CatppuccinFrappe),
"CatppuccinLatte" => Some(Theme::CatppuccinLatte),
"CatppuccinMacchiato" => Some(Theme::CatppuccinMacchiato),
"CatppuccinMocha" => Some(Theme::CatppuccinMocha),
"Dark" => Some(Theme::Dark),
"Dracula" => Some(Theme::Dracula),
"Ferra" => Some(Theme::Ferra),
"GruvboxDark" => Some(Theme::GruvboxDark),
"GruvboxLight" => Some(Theme::GruvboxLight),
"KanagawaDragon" => Some(Theme::KanagawaDragon),
"KanagawaLotus" => Some(Theme::KanagawaLotus),
"KanagawaWave" => Some(Theme::KanagawaWave),
"Light" => Some(Theme::Light),
"Moonfly" => Some(Theme::Moonfly),
"Nightfly" => Some(Theme::Nightfly),
"Nord" => Some(Theme::Nord),
"Oxocarbon" => Some(Theme::Oxocarbon),
"TokyoNight" => Some(Theme::TokyoNight),
"TokyoNightLight" => Some(Theme::TokyoNightLight),
"TokyoNightStorm" => Some(Theme::TokyoNightStorm),
"AyuMirage" => Some(Theme::Custom(Arc::new(Custom::new(
"AyuMirage".to_string(),
Palette {
background: Color::from_rgb8(0x1F, 0x24, 0x30),
text: Color::from_rgb8(0x63, 0x75, 0x99),
primary: Color::from_rgb8(0x17, 0x1B, 0x24),
success: Color::from_rgb8(0xD5, 0xFF, 0x80),
warning: Color::from_rgb8(0xFF, 0xC1, 0x4E),
danger: Color::from_rgb8(0x12, 0x15, 0x1C),
},
)))),
_ => None,
}
}
fn parse_hint(args: &EbleyArgs) -> String {
if let Some(h) = &args.hint {
if h.len() > 16 {
eprintln!("hint string must be 16 characters or fewer");
exit(1);
}
h.clone()
} else {
DEFAULT_HINT.to_string()
}
}
fn main() -> Result<(), iced_layershell::Error> {
let args: EbleyArgs = argh::from_env();
if args.list_search_paths {
for path in default_paths() {
println!("{}", path.display());
}
return Ok(());
}
if args.reset_cache {
if let Err(err) = delete_cache_dir_with_namespace(CACHE_NAMESPACE) {
eprintln!("Failed to delete cache: {err}");
}
return Ok(());
}
let theme = args
.theme
.as_deref()
.and_then(parse_theme)
.unwrap_or(DEFAULT_THEME);
let flags = ElbeyFlags {
apps_loader: load_apps,
app_launcher: launch_app,
theme,
icon_size: args.icon_size.unwrap_or(DEFAULT_ICON_SIZE),
hint: parse_hint(&args),
filter_font_size: args.filter_font_size.unwrap_or(DEFAULT_TEXT_SIZE),
entries_font_size: args.entries_font_size.unwrap_or(DEFAULT_TEXT_SIZE),
};
let iced_settings = Settings {
layer_settings: LayerShellSettings {
size: Some((
args.width.unwrap_or(DEFAULT_WINDOW_WIDTH),
args.height.unwrap_or(DEFAULT_WINDOW_HEIGHT),
)),
exclusive_zone: DEFAULT_WINDOW_HEIGHT as i32,
anchor: Anchor::all(),
start_mode: StartMode::Active,
layer: Layer::Overlay,
margin: (0, 0, 0, 0),
keyboard_interactivity: KeyboardInteractivity::Exclusive,
events_transparent: false,
},
id: Some(PROGRAM_NAME.to_string()),
fonts: vec![],
default_font: Font::DEFAULT,
default_text_size: Pixels::from(u32::from(
args.filter_font_size.unwrap_or(DEFAULT_TEXT_SIZE),
)),
antialiasing: true,
..Settings::default()
};
let flags_for_boot = flags.clone();
application(
move || Elbey::new(flags_for_boot.clone()),
Elbey::namespace,
Elbey::update,
Elbey::view,
)
.subscription(Elbey::subscription)
.theme(|state: &Elbey| Some(state.theme()))
.settings(iced_settings)
.run()
}
fn launch_app(entry: &AppDescriptor) -> anyhow::Result<()> {
let exec = entry.exec.as_deref().context("Missing exec command")?;
let args = shell_words::split(exec)?;
let args = args
.iter()
.filter(|entry| !entry.starts_with('%'))
.collect::<Vec<&String>>();
std::process::Command::new(args[0])
.args(&args[1..])
.spawn()
.context("Failed to spawn app")
.map(|_| ())?;
if let Ok(cache) = CACHE.lock().as_mut() {
cache.update(entry)?;
} else {
eprint!("Failed to acquire cache");
}
exit(0);
}
fn load_apps() -> Vec<AppDescriptor> {
let mut cache = CACHE.lock().expect("Failed to acquire cache");
cache.load_from_apps_loader()
}
fn find_all_apps() -> Vec<AppDescriptor> {
let locales = get_languages_from_env();
let app_list_iter = Iter::new(default_paths())
.entries(Some(&locales))
.filter(|entry| !entry.no_display())
.filter(|entry| entry.desktop_entry("Name").is_some()) .filter(|entry| entry.exec().is_some());
let mut app_list = if let Some(current_desktop) = current_desktop() {
app_list_iter
.filter(|entry| matching_show_in_filter(entry, ¤t_desktop))
.filter(|entry| matching_no_show_in_filter(entry, ¤t_desktop))
.map(AppDescriptor::from)
.collect::<Vec<_>>()
} else {
app_list_iter.map(AppDescriptor::from).collect::<Vec<_>>()
};
app_list.sort_by(|a, b| a.title.cmp(&b.title));
app_list
}
fn matching_show_in_filter(entry: &DesktopEntry, current_desktop: &[String]) -> bool {
if let Some(show_in) = entry.only_show_in() {
for show_in_desktop in show_in {
for desktop in current_desktop.iter() {
if show_in_desktop == desktop {
return true;
}
}
}
false
} else {
true
}
}
fn matching_no_show_in_filter(entry: &DesktopEntry, current_desktop: &[String]) -> bool {
if let Some(no_show_in) = entry.not_show_in() {
for show_in_desktop in no_show_in {
for desktop in current_desktop.iter() {
if show_in_desktop == desktop {
return false;
}
}
}
true
} else {
true
}
}