use eframe::{egui, egui_glow, egui_wgpu, wgpu, HardwareAcceleration, Renderer};
use std::path::{Path, PathBuf};
use std::process::Command;
const DESKTOP_FILE_NAME: &str = "io.github.tknott95.Guth.desktop";
const LEGACY_DESKTOP_FILE_NAME: &str = "io.knott.KnottFiles.desktop";
const DESKTOP_ENTRY: &str = include_str!("../data/io.github.tknott95.Guth.desktop");
const HELP: &str = "Guth - native Rust file manager\n\nUSAGE:\n guth [OPTIONS] [DIRECTORY]\n\nOPTIONS:\n --install-desktop Install the Guth desktop entry for Open With support\n --set-default Install the entry and set Guth as the directory handler\n --renderer <glow|wgpu> Select the rendering backend\n --gpu <MODE> required, preferred, or off\n --gles <1|2> Select a Glow GLES shader fallback\n --wgpu-backend <LIST> Comma-separated WGPU backends\n --wgpu-power <high|low> Select WGPU power preference\n --vsync / --no-vsync Enable or disable vertical sync\n -h, --help Print help\n -V, --version Print version\n";
fn main() -> eframe::Result<()> {
let startup = StartupConfig::from_env_and_args();
if startup.show_help {
print!("{HELP}");
return Ok(());
}
if startup.show_version {
println!("guth {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if startup.install_desktop {
match install_desktop_integration(startup.set_default) {
Ok(path) => {
println!("Installed Guth desktop integration at {}", path.display());
return Ok(());
}
Err(error) => {
eprintln!("guth: {error}");
std::process::exit(1);
}
}
}
let runtime_info = startup.runtime_info();
let start_path = startup.start_path.clone();
let viewport = egui::ViewportBuilder::default()
.with_title("Guth")
.with_app_id("io.github.tknott95.Guth")
.with_inner_size([1240.0, 800.0])
.with_min_inner_size([760.0, 520.0]);
let options = eframe::NativeOptions {
viewport,
renderer: startup.renderer,
hardware_acceleration: startup.hardware_acceleration,
shader_version: startup.shader_version,
wgpu_options: startup.wgpu_options,
vsync: startup.vsync,
dithering: true,
..Default::default()
};
eframe::run_native(
"Guth",
options,
Box::new(move |cc| {
Ok(Box::new(guth::app::GuthApp::new(
cc,
start_path,
runtime_info,
)))
}),
)
}
struct StartupConfig {
start_path: Option<PathBuf>,
renderer: Renderer,
hardware_acceleration: HardwareAcceleration,
shader_version: Option<egui_glow::ShaderVersion>,
wgpu_options: egui_wgpu::WgpuConfiguration,
vsync: bool,
install_desktop: bool,
set_default: bool,
show_help: bool,
show_version: bool,
}
impl StartupConfig {
fn from_env_and_args() -> Self {
let mut config = Self {
start_path: None,
renderer: renderer_from_env(),
hardware_acceleration: hardware_acceleration_from_env(),
shader_version: shader_version_from_env(),
wgpu_options: wgpu_options_from_env(),
vsync: bool_from_env("GUTH_VSYNC").unwrap_or(true),
install_desktop: false,
set_default: false,
show_help: false,
show_version: false,
};
let mut args = std::env::args_os().skip(1).peekable();
while let Some(arg) = args.next() {
let text = arg.to_string_lossy();
if text == "--renderer" {
if let Some(value) = args.next() {
config.renderer = parse_renderer(&value.to_string_lossy());
}
} else if let Some(value) = text.strip_prefix("--renderer=") {
config.renderer = parse_renderer(value);
} else if text == "--gpu" {
if let Some(value) = args.next() {
config.hardware_acceleration =
parse_hardware_acceleration(&value.to_string_lossy());
}
} else if let Some(value) = text.strip_prefix("--gpu=") {
config.hardware_acceleration = parse_hardware_acceleration(value);
} else if text == "--gles" {
if let Some(value) = args.next() {
config.shader_version = parse_shader_version(&value.to_string_lossy());
}
} else if let Some(value) = text.strip_prefix("--gles=") {
config.shader_version = parse_shader_version(value);
} else if text == "--wgpu-backend" {
if let Some(value) = args.next() {
config.wgpu_options.supported_backends =
parse_wgpu_backends(&value.to_string_lossy());
}
} else if let Some(value) = text.strip_prefix("--wgpu-backend=") {
config.wgpu_options.supported_backends = parse_wgpu_backends(value);
} else if text == "--wgpu-power" {
if let Some(value) = args.next() {
config.wgpu_options.power_preference =
parse_wgpu_power(&value.to_string_lossy());
}
} else if let Some(value) = text.strip_prefix("--wgpu-power=") {
config.wgpu_options.power_preference = parse_wgpu_power(value);
} else if text == "--no-vsync" {
config.vsync = false;
} else if text == "--vsync" {
config.vsync = true;
} else if text == "--install-desktop" {
config.install_desktop = true;
} else if text == "--set-default" {
config.install_desktop = true;
config.set_default = true;
} else if text == "--help" || text == "-h" {
config.show_help = true;
} else if text == "--version" || text == "-V" {
config.show_version = true;
} else if !text.starts_with('-') && config.start_path.is_none() {
config.start_path = Some(PathBuf::from(arg));
}
}
config
}
fn runtime_info(&self) -> guth::app::RuntimeInfo {
guth::app::RuntimeInfo {
renderer: match self.renderer {
Renderer::Glow => "GLOW / OPENGL".to_string(),
Renderer::Wgpu => "WGPU".to_string(),
},
gpu_mode: match self.hardware_acceleration {
HardwareAcceleration::Required => "REQUIRED".to_string(),
HardwareAcceleration::Preferred => "PREFERRED".to_string(),
HardwareAcceleration::Off => "OFF".to_string(),
},
shader: match self.shader_version {
Some(egui_glow::ShaderVersion::Es100) => "GLES 100".to_string(),
Some(egui_glow::ShaderVersion::Es300) => "GLES 300".to_string(),
Some(egui_glow::ShaderVersion::Gl120) => "GLSL 120".to_string(),
Some(egui_glow::ShaderVersion::Gl140) => "GLSL 140".to_string(),
None => "AUTO".to_string(),
},
wgpu_backends: format!("{:?}", self.wgpu_options.supported_backends),
wgpu_power: format!("{:?}", self.wgpu_options.power_preference),
frame_latency: self
.wgpu_options
.desired_maximum_frame_latency
.map(|value| value.to_string())
.unwrap_or_else(|| "AUTO".to_string()),
vsync: self.vsync,
}
}
}
fn renderer_from_env() -> Renderer {
std::env::var("GUTH_RENDERER")
.map(|value| parse_renderer(&value))
.unwrap_or(Renderer::Glow)
}
fn hardware_acceleration_from_env() -> HardwareAcceleration {
std::env::var("GUTH_GPU")
.map(|value| parse_hardware_acceleration(&value))
.unwrap_or(HardwareAcceleration::Required)
}
fn shader_version_from_env() -> Option<egui_glow::ShaderVersion> {
std::env::var("GUTH_GLES")
.ok()
.and_then(|value| parse_shader_version(&value))
}
fn wgpu_options_from_env() -> egui_wgpu::WgpuConfiguration {
let mut options = egui_wgpu::WgpuConfiguration::default();
if let Ok(value) = std::env::var("GUTH_WGPU_BACKEND") {
options.supported_backends = parse_wgpu_backends(&value);
}
if let Ok(value) = std::env::var("GUTH_WGPU_POWER") {
options.power_preference = parse_wgpu_power(&value);
}
if let Ok(value) = std::env::var("GUTH_FRAME_LATENCY") {
options.desired_maximum_frame_latency =
value.parse::<u32>().ok().filter(|value| *value > 0);
}
options
}
fn install_desktop_integration(set_default: bool) -> Result<PathBuf, String> {
let data_home = std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share")))
.ok_or_else(|| "Cannot determine XDG data directory".to_string())?;
let applications = data_home.join("applications");
std::fs::create_dir_all(&applications)
.map_err(|error| format!("Cannot create {}: {error}", applications.display()))?;
let executable = std::env::current_exe()
.map_err(|error| format!("Cannot locate the Guth executable: {error}"))?;
let desktop_path = applications.join(DESKTOP_FILE_NAME);
let default_handler = default_directory_handler();
let legacy_was_default = matches!(
&default_handler,
Ok(Some(handler)) if handler == LEGACY_DESKTOP_FILE_NAME
);
std::fs::write(&desktop_path, desktop_entry_for_executable(&executable)?).map_err(|error| {
format!(
"Cannot write desktop entry {}: {error}",
desktop_path.display()
)
})?;
let legacy_path = applications.join(LEGACY_DESKTOP_FILE_NAME);
refresh_desktop_database(&applications)?;
if set_default || legacy_was_default {
set_default_directory_handler()?;
}
let can_remove_legacy = set_default || default_handler.is_ok();
if legacy_path.is_file() && can_remove_legacy {
std::fs::remove_file(&legacy_path).map_err(|error| {
format!(
"Cannot remove legacy desktop entry {}: {error}",
legacy_path.display()
)
})?;
refresh_desktop_database(&applications)?;
} else if legacy_path.is_file() {
eprintln!(
"guth: preserving {} because the current directory handler could not be queried",
legacy_path.display()
);
}
Ok(desktop_path)
}
fn desktop_entry_for_executable(executable: &Path) -> Result<String, String> {
let executable = executable
.to_str()
.ok_or_else(|| "Guth executable path is not valid UTF-8".to_string())?;
let escaped = escape_desktop_exec_arg(executable);
Ok(DESKTOP_ENTRY.replace("Exec=guth %f", &format!("Exec=\"{escaped}\" %f")))
}
fn escape_desktop_exec_arg(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
match character {
'\\' => escaped.push_str("\\\\\\\\"),
'"' => escaped.push_str("\\\\\\\""),
'`' => escaped.push_str("\\\\`"),
'$' => escaped.push_str("\\\\$"),
'%' => escaped.push_str("%%"),
_ => escaped.push(character),
}
}
escaped
}
fn default_directory_handler() -> Result<Option<String>, String> {
let output = Command::new("xdg-mime")
.args(["query", "default", "inode/directory"])
.output()
.map_err(|error| format!("Cannot query the default directory handler: {error}"))?;
if !output.status.success() {
return Err(format!(
"xdg-mime query exited with status {}",
output.status
));
}
let handler = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok((!handler.is_empty()).then_some(handler))
}
fn set_default_directory_handler() -> Result<(), String> {
let status = Command::new("xdg-mime")
.args(["default", DESKTOP_FILE_NAME, "inode/directory"])
.status()
.map_err(|error| format!("Cannot run xdg-mime: {error}"))?;
if status.success() {
Ok(())
} else {
Err(format!("xdg-mime exited with status {status}"))
}
}
fn refresh_desktop_database(applications: &Path) -> Result<(), String> {
match Command::new("update-desktop-database")
.arg(applications)
.status()
{
Ok(status) if status.success() => Ok(()),
Ok(status) => Err(format!(
"update-desktop-database exited with status {status}"
)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(format!("Cannot refresh desktop database: {error}")),
}
}
fn parse_renderer(value: &str) -> Renderer {
match value.to_ascii_lowercase().as_str() {
"wgpu" | "vulkan" | "metal" | "dx12" => Renderer::Wgpu,
_ => Renderer::Glow,
}
}
fn parse_hardware_acceleration(value: &str) -> HardwareAcceleration {
match value.to_ascii_lowercase().as_str() {
"off" | "0" | "false" | "software" => HardwareAcceleration::Off,
"preferred" | "auto" => HardwareAcceleration::Preferred,
_ => HardwareAcceleration::Required,
}
}
fn parse_shader_version(value: &str) -> Option<egui_glow::ShaderVersion> {
match value.to_ascii_lowercase().as_str() {
"1" | "100" | "es100" | "gles1" => Some(egui_glow::ShaderVersion::Es100),
"2" | "3" | "300" | "es300" | "gles2" | "gles3" => Some(egui_glow::ShaderVersion::Es300),
_ => None,
}
}
fn parse_wgpu_backends(value: &str) -> wgpu::Backends {
let mut backends = wgpu::Backends::empty();
for part in value.split(',').map(str::trim) {
match part.to_ascii_lowercase().as_str() {
"primary" => backends |= wgpu::Backends::PRIMARY,
"secondary" => backends |= wgpu::Backends::SECONDARY,
"vulkan" | "vk" => backends |= wgpu::Backends::VULKAN,
"gl" | "gles" | "opengl" | "opengles" => backends |= wgpu::Backends::GL,
"metal" => backends |= wgpu::Backends::METAL,
"dx12" | "d3d12" => backends |= wgpu::Backends::DX12,
_ => {}
}
}
if backends.is_empty() {
wgpu::Backends::PRIMARY | wgpu::Backends::GL
} else {
backends
}
}
fn parse_wgpu_power(value: &str) -> wgpu::PowerPreference {
match value.to_ascii_lowercase().as_str() {
"low" | "low-power" | "integrated" => wgpu::PowerPreference::LowPower,
_ => wgpu::PowerPreference::HighPerformance,
}
}
fn bool_from_env(name: &str) -> Option<bool> {
std::env::var(name).ok().map(|value| {
!matches!(
value.to_ascii_lowercase().as_str(),
"0" | "false" | "off" | "no"
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_renderer_modes() {
assert_eq!(parse_renderer("glow"), Renderer::Glow);
assert_eq!(parse_renderer("wgpu"), Renderer::Wgpu);
assert_eq!(parse_renderer("vulkan"), Renderer::Wgpu);
}
#[test]
fn parses_wgpu_backend_list() {
let backends = parse_wgpu_backends("vulkan,gl");
assert!(backends.contains(wgpu::Backends::VULKAN));
assert!(backends.contains(wgpu::Backends::GL));
assert!(!backends.contains(wgpu::Backends::METAL));
}
#[test]
fn desktop_entry_uses_exact_executable_and_directory_mime() {
let entry = desktop_entry_for_executable(Path::new("/tmp/Guth Bin/guth"))
.expect("render desktop entry");
assert!(entry.contains("Name=Guth"));
assert!(entry.contains("Exec=\"/tmp/Guth Bin/guth\" %f"));
assert!(entry.contains("MimeType=inode/directory;"));
assert!(entry.contains("StartupWMClass=io.github.tknott95.Guth"));
}
#[test]
fn desktop_exec_escaping_handles_both_parsing_layers() {
assert_eq!(escape_desktop_exec_arg("$`\\\"%"), r#"\\$\\`\\\\\\\"%%"#);
}
}