#![doc(html_logo_url = "https://github.com/kgilmer/elbey/blob/main/elbey.svg")]
mod app;
mod cache;
mod values;
use std::process::exit;
use std::sync::{Arc, Mutex};
use crate::values::*;
use anyhow::Context;
use app::{AppDescriptor, Elbey, ElbeyFlags};
use argh::FromArgs;
use cache::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::reexport::{Anchor, KeyboardInteractivity, Layer};
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
use iced_layershell::Application;
use lazy_static::lazy_static;
lazy_static! {
static ref CACHE: Arc<Mutex<Cache>> = Arc::new(Mutex::new(Cache::new(find_all_apps)));
}
#[derive(FromArgs)]
struct EbleyArgs {
#[argh(option)]
height: Option<u32>,
#[argh(option)]
width: Option<u32>,
#[argh(option)]
theme: Option<String>,
#[argh(option)]
font_size: Option<u16>,
#[argh(option)]
icon_size: Option<u16>,
#[argh(option, short = 't')]
_style_sheet: Option<String>,
}
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::parse("1F2430").expect("Cannot parse color"),
text: Color::parse("637599").expect("Cannot parse color"),
primary: Color::parse("171B24").expect("Cannot parse color"),
success: Color::parse("D5FF80").expect("Cannot parse color"),
danger: Color::parse("12151C").expect("Cannot parse color"),
},
)))),
_ => None,
}
}
fn main() -> Result<(), iced_layershell::Error> {
let args: EbleyArgs = argh::from_env();
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,
},
flags: ElbeyFlags {
apps_loader: load_apps,
app_launcher: launch_app,
theme: if args.theme.is_some() {
if let Some(theme) = parse_theme(&args.theme.unwrap()) {
theme
} else {
DEFAULT_THEME
}
} else {
DEFAULT_THEME
},
window_size: (
args.width
.unwrap_or(DEFAULT_WINDOW_WIDTH)
.try_into()
.unwrap(),
args.height
.unwrap_or(DEFAULT_WINDOW_HEIGHT)
.try_into()
.unwrap(),
),
icon_size: args.icon_size.unwrap_or(DEFAULT_ICON_SIZE),
},
id: Some(PROGRAM_NAME.to_string()),
fonts: vec![],
default_font: Font::DEFAULT,
default_text_size: Pixels::from(args.font_size.unwrap_or(DEFAULT_TEXT_SIZE)),
antialiasing: true,
virtual_keyboard_support: None,
};
Elbey::run(iced_settings)
}
fn launch_app(entry: &AppDescriptor) -> anyhow::Result<()> {
let args = shell_words::split(entry.exec.as_str())?;
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 cache = CACHE.lock().expect("Failed to acquire cache");
if cache.is_empty() {
find_all_apps()
} else {
cache.read_all().unwrap_or(find_all_apps())
}
}
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
}
}