//! Command-line interface.
use std::{
fs::{read_dir, OpenOptions},
path::{Path, PathBuf},
};
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use log::{debug, trace, LevelFilter};
use simplelog::WriteLogger;
use syslog::{BasicLogger, Facility, Formatter3164};
use xdg::BaseDirectories;
/// The options set at the command line.
#[derive(Parser, Debug)]
#[clap(about = "A console-based display manager.")]
pub struct Options {
// Imperative mood instead of present indicative to be consistent with clap's -h option.
// (Honestly, I'd use imperative everywhere, but the `std` crate doesn't, so I decided
// to be consistent with that.)
/// Focus the given TTY when rstdm starts.
#[clap(short, long, name = "TTY")]
pub focus_on_start: Option<i32>,
/// Enable DEBUG-level logging (twice for TRACE).
#[clap(short, long, parse(from_occurrences))]
pub verbose: u8,
/// Use `dbus-run-session` to start the desktop.
#[clap(short, long, parse(from_flag))]
pub dbus: Dbus,
/// Include this folder when searching for sessions (can be used more than once).
#[clap(short, long, name = "PATH")]
pub session_dirs: Vec<PathBuf>,
/// Log to this path instead of syslog.
#[clap(short, long, name = "PATH")]
pub log_file: Option<String>,
}
impl Options {
pub fn new() -> Self {
let mut out = Options::parse();
/// Return each of the XDG_DATA_DIRS entries with `leaf_dir` appended, so long as the
/// resulting path exists.
///
/// Function exists because firing off a "Couldn't read /usr/local/share/wayland-sessions:
/// no such directory" error line on every startup would be rude.
fn gather_default_dirs(leaf_dir: &str) -> impl Iterator<Item = PathBuf> + '_ {
let xdg = BaseDirectories::new().unwrap();
// unwrap: there's an error if and only if:
// 1. $HOME is unset, AND
// 2. rstdm's user -- usually `root` -- has no homedir.
xdg.get_data_dirs().into_iter().filter_map(move |mut p| {
p.push(leaf_dir);
trace!("Checking existence of {}", p.to_string_lossy());
match read_dir(&p) {
Ok(_) => Some(p),
Err(_) => None,
}
})
}
out.session_dirs = out
.session_dirs
.into_iter()
.chain(gather_default_dirs("wayland-sessions"))
.collect::<Vec<_>>();
out
}
fn verbosity(&self) -> LevelFilter {
match self.verbose {
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
}
}
}
/// Connects to a running syslog instance.
pub fn init_syslog(verbosity: LevelFilter) -> Result<()> {
let formatter = Formatter3164 {
facility: Facility::LOG_AUTH,
hostname: None,
process: "rstdm".into(),
pid: std::process::id(),
};
let logger =
syslog::unix(formatter).map_err(|e| anyhow!("Couldn't connect to syslog: {}", e))?;
log::set_boxed_logger(Box::new(BasicLogger::new(logger)))?;
log::set_max_level(verbosity);
debug!("Syslog initialized");
Ok(())
}
/// Initializes a file as a log sink.
pub fn init_log_file(path: &Path, verbosity: LevelFilter) -> Result<()> {
let file = OpenOptions::new()
.append(true)
.create(true)
.open(&path)
.with_context(|| anyhow!("Couldn't open {}", &path.display()))?;
WriteLogger::init(verbosity, Default::default(), file)?;
debug!("Logfile {} initialized", path.display());
Ok(())
}
/// Initializes either a log file or a syslog connection.
pub fn init_logger(opts: &Options) -> Result<()> {
match &opts.log_file {
Some(path) => init_log_file(Path::new(&path), opts.verbosity()),
None => init_syslog(opts.verbosity()),
}
}
#[derive(clap::ArgEnum, Debug, Clone)]
/// Whether a session should be run with D-Bus.
pub enum Dbus {
No,
Yes,
}
impl From<bool> for Dbus {
fn from(b: bool) -> Self {
match b {
false => Dbus::No,
true => Dbus::Yes,
}
}
}