mod config;
mod i18n;
mod layout;
mod process_watcher;
mod remote_memory;
mod shell_locator;
mod startup;
mod types;
mod wait_drive;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::ExitCode;
use i18n::{t, t_args};
use types::{DesktopProfile, Resolution};
const PROFILES_DIR: &str = "profiles";
fn main() -> ExitCode {
let args: Vec<String> = env::args().skip(1).collect();
match run(&args) {
Ok(message) => {
println!("{message}");
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("{}", t_args("errors.generic", &[("error", &error)]));
ExitCode::FAILURE
}
}
}
fn run(args: &[String]) -> Result<String, String> {
match args.first().map(String::as_str) {
Some("save") => {
let name = profile_name_arg(args)?;
let current = shell_locator::current_resolution();
let mut profile = layout::save_layout(&name, current).map_err(|e| format!("{e}"))?;
if let Ok(existing) = load_profile(&name) {
profile.resolutions = existing.resolutions;
}
save_profile(&profile)?;
Ok(t_args(
"save.success",
&[
("name", &profile.profile_name),
("count", &profile.icons.len().to_string()),
("width", &profile.resolution.width.to_string()),
("height", &profile.resolution.height.to_string()),
],
))
}
Some("save-res") => {
let name = profile_name_arg(args)?;
let resolution = match args.get(2) {
Some(raw) => parse_resolution(raw)?,
None => shell_locator::current_resolution(),
};
let current = layout::save_layout(&name, resolution).map_err(|e| format!("{e}"))?;
let key = format!("{}x{}", resolution.width, resolution.height);
let count = current.icons.len();
let mut profile = match load_profile(&name) {
Ok(existing) => existing,
Err(_) => current.clone(),
};
profile.resolutions.insert(key.clone(), current.icons);
save_profile(&profile)?;
Ok(t_args(
"save_res.success",
&[
("name", &name),
("count", &count.to_string()),
("resolution", &key),
],
))
}
Some("profile-update") => {
let name = profile_name_arg(args)?;
let mut profile = load_profile(&name)
.map_err(|_| t_args("profile_update.not_found", &[("name", &name)]))?;
let current = shell_locator::current_resolution();
let captured = layout::save_layout(&name, current).map_err(|e| format!("{e}"))?;
profile.resolution = captured.resolution;
profile.timestamp_utc = captured.timestamp_utc;
profile.icons = captured.icons.clone();
let key = format!("{}x{}", current.width, current.height);
if profile.resolutions.contains_key(&key) {
profile.resolutions.insert(key.clone(), captured.icons);
}
let count = profile.icons.len();
save_profile(&profile)?;
Ok(t_args(
"profile_update.success",
&[
("name", &name),
("count", &count.to_string()),
("width", ¤t.width.to_string()),
("height", ¤t.height.to_string()),
],
))
}
Some("restore") => {
let name = profile_name_arg(args)?;
let profile = load_profile(&name)?;
let moved = layout::restore_layout(&profile).map_err(|e| format!("{e}"))?;
Ok(t_args(
"restore.success",
&[
("name", &name),
("moved", &moved.to_string()),
("count", &profile.icons.len().to_string()),
],
))
}
Some("list") => {
let profiles = list_profiles()?;
if profiles.is_empty() {
Ok(t("list.empty"))
} else {
Ok(t_args("list.some", &[("list", &profiles.join("\n"))]))
}
}
Some("lang") => run_lang(args),
Some("startup") => match args.get(1).map(String::as_str) {
Some("on") => {
let name = args
.get(2)
.filter(|n| !n.trim().is_empty())
.ok_or_else(|| t("startup.missing_profile"))?;
if profile_path(name).exists() {
startup::enable(name)
} else {
startup::enable(name).map(|msg| {
format!(
"{msg}\n{}",
t_args("startup.warn_profile_missing", &[("name", name)])
)
})
}
}
Some("off") => startup::disable(),
Some("status") | None => startup::status(),
Some(_) => Err(t("startup.usage")),
},
Some("wait-drive") => match args.get(1).map(String::as_str) {
Some("on") | Some("true") => {
let nome = args
.get(2)
.filter(|n| !n.trim().is_empty())
.ok_or_else(|| t("wait_drive.missing_profile"))?;
validate_profile_name(nome)?;
let timeout = timeout_arg(args)?;
if profile_path(nome).exists() {
wait_drive::enable(nome, timeout.map(|t| t.as_secs()))
} else {
wait_drive::enable(nome, timeout.map(|t| t.as_secs())).map(|msg| {
format!(
"{msg}\n{}",
t_args("wait_drive.warn_profile_missing", &[("name", nome)])
)
})
}
}
Some("off") | Some("false") => wait_drive::disable(),
Some("status") | None => wait_drive::status(),
Some("run") => wait_drive_run(),
Some(_) => Err(t("wait_drive.usage")),
},
Some("help") | Some("--help") | Some("-h") | None => Ok(help_text()),
Some(outro) => Err(t_args(
"errors.unknown_command",
&[("command", outro), ("help", &help_text())],
)),
}
}
fn run_lang(args: &[String]) -> Result<String, String> {
match args
.get(1)
.map(String::as_str)
.filter(|s| !s.trim().is_empty())
{
None => Ok(t_args("lang.current", &[("lang", &config::get_language())])),
Some(sigla) => {
let path = exe_dir().join("langs").join(format!("{sigla}.json"));
if !path.exists() && i18n::embedded_language(sigla).is_none() {
return Err(t_args("lang.not_found", &[("lang", sigla)]));
}
config::set_language(sigla)?;
Ok(t_args("lang.changed", &[("lang", sigla)]))
}
}
}
fn exe_dir() -> PathBuf {
env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."))
}
fn help_text() -> String {
t("help.text")
}
fn timeout_arg(args: &[String]) -> Result<Option<std::time::Duration>, String> {
match args.iter().position(|a| a == "--timeout" || a == "-t") {
Some(pos) => {
let segundos: u64 = args
.get(pos + 1)
.and_then(|v| v.parse().ok())
.ok_or_else(|| t("errors.invalid_timeout"))?;
Ok(Some(std::time::Duration::from_secs(segundos)))
}
None => Ok(None),
}
}
fn validate_profile_name(name: &str) -> Result<(), String> {
if name.chars().any(|c| "\\/:*?\"<>|".contains(c)) {
return Err(t("errors.profile_name_invalid"));
}
Ok(())
}
fn profile_name_arg(args: &[String]) -> Result<String, String> {
let name = args
.get(1)
.filter(|n| !n.trim().is_empty())
.ok_or_else(|| t("errors.profile_name_missing"))?;
validate_profile_name(name)?;
Ok(name.clone())
}
fn wait_drive_run() -> Result<String, String> {
let config = wait_drive::load_config();
if !config.enabled {
return Err(t("wait_drive.disabled_error"));
}
let perfil = config.profile.unwrap();
let timeout = config.timeout_secs.map(std::time::Duration::from_secs);
println!(
"{}",
t_args(
"wait_drive.run_waiting",
&[
("process", process_watcher::GOOGLE_DRIVE_DESCRIPTION),
(
"timeout",
&timeout
.map(|t| t_args(
"wait_drive.run_timeout_suffix",
&[("seconds", &t.as_secs().to_string())]
))
.unwrap_or_else(|| t("wait_drive.run_no_timeout_suffix")),
),
],
)
);
let esperou = process_watcher::wait_for_process(
process_watcher::GOOGLE_DRIVE_DESCRIPTION,
timeout,
std::time::Duration::from_secs(5),
)?;
println!(
"{}",
t_args(
"wait_drive.run_detected",
&[
("seconds", &esperou.as_secs().to_string()),
("name", &perfil)
],
)
);
let perfil_dados = load_profile(&perfil)?;
let movidos = layout::restore_layout(&perfil_dados).map_err(|e| format!("{e}"))?;
Ok(t_args(
"wait_drive.run_restored",
&[
("name", &perfil),
("moved", &movidos.to_string()),
("count", &perfil_dados.icons.len().to_string()),
],
))
}
fn profiles_base_dir() -> PathBuf {
exe_dir().join(PROFILES_DIR)
}
fn profile_path(name: &str) -> PathBuf {
profiles_base_dir().join(format!("{name}.json"))
}
fn parse_resolution(raw: &str) -> Result<Resolution, String> {
let (w, h) = raw
.split_once(['x', 'X'])
.ok_or_else(|| t_args("errors.resolution_invalid", &[("raw", raw)]))?;
let width: u32 = w
.parse()
.map_err(|_| t_args("errors.resolution_width_invalid", &[("raw", raw)]))?;
let height: u32 = h
.parse()
.map_err(|_| t_args("errors.resolution_height_invalid", &[("raw", raw)]))?;
if width == 0 || height == 0 {
return Err(t("errors.resolution_zero"));
}
Ok(Resolution { width, height })
}
fn save_profile(profile: &DesktopProfile) -> Result<(), String> {
fs::create_dir_all(profiles_base_dir()).map_err(|e| format!("{e}"))?;
let json = serde_json::to_string_pretty(profile).map_err(|e| format!("{e}"))?;
fs::write(profile_path(&profile.profile_name), json).map_err(|e| format!("{e}"))
}
fn load_profile(name: &str) -> Result<DesktopProfile, String> {
let path = profile_path(name);
let data = fs::read_to_string(&path).map_err(|_| {
t_args(
"errors.profile_not_found",
&[("name", name), ("path", &path.display().to_string())],
)
})?;
serde_json::from_str(&data)
.map_err(|e| t_args("errors.profile_invalid", &[("error", &e.to_string())]))
}
fn list_profiles() -> Result<Vec<String>, String> {
let mut names = Vec::new();
let dir = match fs::read_dir(profiles_base_dir()) {
Ok(dir) => dir,
Err(_) => return Ok(names),
};
for entry in dir.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("json") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
names.push(stem.to_string());
}
}
}
names.sort();
Ok(names)
}