mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-06-30 10:11:35 +02:00
386 lines
12 KiB
Rust
386 lines
12 KiB
Rust
#![warn(clippy::all)]
|
|
#![allow(
|
|
clippy::missing_errors_doc,
|
|
clippy::redundant_pub_crate,
|
|
clippy::significant_drop_tightening,
|
|
clippy::significant_drop_in_scrutinee,
|
|
clippy::doc_markdown
|
|
)]
|
|
|
|
use std::env::temp_dir;
|
|
use std::net::Shutdown;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::Ordering;
|
|
#[cfg(feature = "deadlock_detection")]
|
|
use std::time::Duration;
|
|
|
|
use clap::Parser;
|
|
use clap::ValueEnum;
|
|
use color_eyre::eyre;
|
|
use color_eyre::eyre::bail;
|
|
use crossbeam_utils::Backoff;
|
|
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
|
|
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
|
use komorebi::animation::AnimationEngine;
|
|
use komorebi::replace_env_in_path;
|
|
use parking_lot::Mutex;
|
|
#[cfg(feature = "deadlock_detection")]
|
|
use parking_lot::deadlock;
|
|
use serde::Deserialize;
|
|
use sysinfo::Process;
|
|
use sysinfo::ProcessesToUpdate;
|
|
use tracing_appender::non_blocking::WorkerGuard;
|
|
use tracing_subscriber::EnvFilter;
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use uds_windows::UnixStream;
|
|
|
|
use komorebi::CUSTOM_FFM;
|
|
use komorebi::DATA_DIR;
|
|
use komorebi::HOME_DIR;
|
|
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
|
use komorebi::SESSION_ID;
|
|
use komorebi::border_manager;
|
|
use komorebi::focus_manager;
|
|
use komorebi::load_configuration;
|
|
use komorebi::monitor_reconciliator;
|
|
use komorebi::process_command::listen_for_commands;
|
|
use komorebi::process_command::listen_for_commands_tcp;
|
|
use komorebi::process_event::listen_for_events;
|
|
use komorebi::process_movement::listen_for_movements;
|
|
use komorebi::reaper;
|
|
use komorebi::stackbar_manager;
|
|
use komorebi::state::State;
|
|
use komorebi::static_config::StaticConfig;
|
|
use komorebi::theme_manager;
|
|
use komorebi::transparency_manager;
|
|
use komorebi::window_manager::WindowManager;
|
|
use komorebi::windows_api::WindowsApi;
|
|
use komorebi::winevent_listener;
|
|
|
|
fn setup(log_level: LogLevel) -> eyre::Result<(WorkerGuard, WorkerGuard)> {
|
|
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
|
unsafe {
|
|
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
|
}
|
|
}
|
|
|
|
color_eyre::install()?;
|
|
|
|
if std::env::var("RUST_LOG").is_err() {
|
|
unsafe {
|
|
std::env::set_var(
|
|
"RUST_LOG",
|
|
match log_level {
|
|
LogLevel::Error => "error",
|
|
LogLevel::Warn => "warn",
|
|
LogLevel::Info => "info",
|
|
LogLevel::Debug => "debug",
|
|
LogLevel::Trace => "trace",
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
|
|
let color_appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi.log");
|
|
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
|
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
|
|
|
|
tracing::subscriber::set_global_default(
|
|
tracing_subscriber::fmt::Subscriber::builder()
|
|
.with_env_filter(EnvFilter::from_default_env())
|
|
.finish()
|
|
.with(
|
|
tracing_subscriber::fmt::Layer::default()
|
|
.with_writer(non_blocking)
|
|
.with_ansi(false),
|
|
)
|
|
.with(
|
|
tracing_subscriber::fmt::Layer::default()
|
|
.with_writer(color_non_blocking)
|
|
.with_ansi(true),
|
|
),
|
|
)?;
|
|
|
|
// https://github.com/tokio-rs/tracing/blob/master/examples/examples/panic_hook.rs
|
|
// Set a panic hook that records the panic as a `tracing` event at the
|
|
// `ERROR` verbosity level.
|
|
//
|
|
// If we are currently in a span when the panic occurred, the logged event
|
|
// will include the current span, allowing the context in which the panic
|
|
// occurred to be recorded.
|
|
std::panic::set_hook(Box::new(|panic| {
|
|
// If the panic has a source location, record it as structured fields.
|
|
panic.location().map_or_else(
|
|
|| {
|
|
tracing::error!(message = %panic);
|
|
},
|
|
|location| {
|
|
// On nightly Rust, where the `PanicInfo` type also exposes a
|
|
// `message()` method returning just the message, we could record
|
|
// just the message instead of the entire `fmt::Display`
|
|
// implementation, avoiding the duplciated location
|
|
tracing::error!(
|
|
message = %panic,
|
|
panic.file = location.file(),
|
|
panic.line = location.line(),
|
|
panic.column = location.column(),
|
|
);
|
|
},
|
|
);
|
|
}));
|
|
|
|
Ok((guard, color_guard))
|
|
}
|
|
|
|
#[cfg(feature = "deadlock_detection")]
|
|
#[tracing::instrument]
|
|
fn detect_deadlocks() {
|
|
// Create a background thread which checks for deadlocks every 10s
|
|
std::thread::spawn(move || {
|
|
loop {
|
|
tracing::info!("running deadlock detector");
|
|
std::thread::sleep(Duration::from_secs(5));
|
|
let deadlocks = deadlock::check_deadlock();
|
|
if deadlocks.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
tracing::error!("{} deadlocks detected", deadlocks.len());
|
|
for (i, threads) in deadlocks.iter().enumerate() {
|
|
tracing::error!("deadlock #{}", i);
|
|
for t in threads {
|
|
tracing::error!("thread id: {:#?}", t.thread_id());
|
|
tracing::error!("{:#?}", t.backtrace());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#[derive(Default, Deserialize, ValueEnum, Clone)]
|
|
#[serde(rename_all = "snake_case")]
|
|
enum LogLevel {
|
|
Error,
|
|
Warn,
|
|
#[default]
|
|
Info,
|
|
Debug,
|
|
Trace,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[clap(author, about, version = komorebi::build::CLAP_LONG_VERSION)]
|
|
struct Opts {
|
|
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
|
#[clap(short, long = "ffm")]
|
|
focus_follows_mouse: bool,
|
|
/// Wait for `komorebic complete-configuration` to be sent before processing events
|
|
#[clap(short, long)]
|
|
await_configuration: bool,
|
|
/// Start a TCP server on the given port to allow the direct sending of SocketMessages
|
|
#[clap(short, long)]
|
|
tcp_port: Option<usize>,
|
|
/// Path to a static configuration JSON file
|
|
#[clap(short, long)]
|
|
#[clap(value_parser = replace_env_in_path)]
|
|
config: Option<PathBuf>,
|
|
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
|
|
#[clap(long)]
|
|
clean_state: bool,
|
|
/// Level of log output verbosity
|
|
#[clap(long, value_enum, default_value_t=LogLevel::Info)]
|
|
log_level: LogLevel,
|
|
}
|
|
|
|
#[tracing::instrument]
|
|
#[allow(clippy::cognitive_complexity)]
|
|
fn main() -> eyre::Result<()> {
|
|
let opts: Opts = Opts::parse();
|
|
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
|
|
|
|
let mut set_foreground_window_retries = 5;
|
|
let mut set_foreground_window_succeeded = false;
|
|
|
|
let process_id = WindowsApi::current_process_id();
|
|
while set_foreground_window_retries > 0 && !set_foreground_window_succeeded {
|
|
match WindowsApi::allow_set_foreground_window(process_id) {
|
|
Ok(_) => {
|
|
set_foreground_window_succeeded = true;
|
|
}
|
|
Err(error) => {
|
|
tracing::error!("{error}");
|
|
set_foreground_window_retries -= 1;
|
|
}
|
|
}
|
|
|
|
if set_foreground_window_retries == 0 {
|
|
bail!("failed call to AllowSetForegroundWindow after 5 retries");
|
|
}
|
|
}
|
|
|
|
WindowsApi::set_process_dpi_awareness_context()?;
|
|
|
|
let session_id = WindowsApi::process_id_to_session_id()?;
|
|
SESSION_ID.store(session_id, Ordering::SeqCst);
|
|
|
|
let mut system = sysinfo::System::new();
|
|
system.refresh_processes(ProcessesToUpdate::All, true);
|
|
|
|
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
|
|
|
|
if matched_procs.len() > 1 {
|
|
let mut len = matched_procs.len();
|
|
for proc in matched_procs {
|
|
if let Some(executable_path) = proc.exe()
|
|
&& executable_path.to_string_lossy().contains("shims")
|
|
{
|
|
len -= 1;
|
|
}
|
|
}
|
|
|
|
if len > 1 {
|
|
tracing::error!(
|
|
"komorebi.exe is already running, please exit the existing process before starting a new one"
|
|
);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// File logging worker guard has to have an assignment in the main fn to work
|
|
let (_guard, _color_guard) = setup(opts.log_level)?;
|
|
|
|
WindowsApi::foreground_lock_timeout()?;
|
|
|
|
winevent_listener::start();
|
|
|
|
#[cfg(feature = "deadlock_detection")]
|
|
detect_deadlocks();
|
|
|
|
let static_config = opts.config.map_or_else(
|
|
|| {
|
|
let komorebi_json = HOME_DIR.join("komorebi.json");
|
|
if komorebi_json.is_file() {
|
|
Option::from(komorebi_json)
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
Option::from,
|
|
);
|
|
|
|
std::fs::create_dir_all(&*DATA_DIR)?;
|
|
|
|
let wm = if let Some(config) = &static_config {
|
|
tracing::info!(
|
|
"creating window manager from static configuration file: {}",
|
|
config.display()
|
|
);
|
|
|
|
Arc::new(Mutex::new(StaticConfig::preload(
|
|
config,
|
|
winevent_listener::event_rx(),
|
|
None,
|
|
)?))
|
|
} else {
|
|
Arc::new(Mutex::new(WindowManager::new(
|
|
winevent_listener::event_rx(),
|
|
None,
|
|
)?))
|
|
};
|
|
|
|
wm.lock().init()?;
|
|
|
|
if let Some(config) = &static_config {
|
|
StaticConfig::postload(config, &wm)?;
|
|
}
|
|
|
|
if !opts.await_configuration && !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
|
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
|
};
|
|
|
|
if static_config.is_none() {
|
|
std::thread::spawn(|| load_configuration().expect("could not load configuration"));
|
|
|
|
if opts.await_configuration {
|
|
let backoff = Backoff::new();
|
|
while !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
|
backoff.snooze();
|
|
}
|
|
}
|
|
}
|
|
|
|
let dumped_state = temp_dir().join("komorebi.state.json");
|
|
|
|
if !opts.clean_state && dumped_state.is_file() {
|
|
if let Ok(state) = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?) {
|
|
wm.lock().apply_state(state);
|
|
} else {
|
|
tracing::warn!(
|
|
"cannot apply state from {}; state struct is not up to date",
|
|
dumped_state.display()
|
|
);
|
|
}
|
|
}
|
|
|
|
wm.lock().retile_all(false)?;
|
|
|
|
border_manager::listen_for_notifications(wm.clone());
|
|
stackbar_manager::listen_for_notifications(wm.clone());
|
|
transparency_manager::listen_for_notifications(wm.clone());
|
|
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
|
reaper::listen_for_notifications(wm.clone(), wm.lock().known_hwnds.clone());
|
|
focus_manager::listen_for_notifications(wm.clone());
|
|
theme_manager::listen_for_notifications();
|
|
|
|
listen_for_commands(wm.clone());
|
|
|
|
if let Some(port) = opts.tcp_port {
|
|
listen_for_commands_tcp(wm.clone(), port);
|
|
}
|
|
|
|
listen_for_events(wm.clone());
|
|
|
|
if CUSTOM_FFM.load(Ordering::SeqCst) {
|
|
listen_for_movements(wm.clone());
|
|
}
|
|
|
|
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
|
ctrlc::set_handler(move || {
|
|
ctrlc_sender
|
|
.send(())
|
|
.expect("could not send signal on ctrl-c channel");
|
|
})?;
|
|
|
|
ctrlc_receiver
|
|
.recv()
|
|
.expect("could not receive signal on ctrl-c channel");
|
|
|
|
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
|
|
|
let state = State::from(&*wm.lock());
|
|
std::fs::write(dumped_state, serde_json::to_string_pretty(&state)?)?;
|
|
|
|
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
|
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
|
|
wm.lock().restore_all_windows(false)?;
|
|
AnimationEngine::wait_for_all_animations();
|
|
|
|
if WindowsApi::focus_follows_mouse()? {
|
|
WindowsApi::disable_focus_follows_mouse()?;
|
|
}
|
|
|
|
let sockets = komorebi::SUBSCRIPTION_SOCKETS.lock();
|
|
for path in (*sockets).values() {
|
|
if let Ok(stream) = UnixStream::connect(path) {
|
|
stream.shutdown(Shutdown::Both)?;
|
|
}
|
|
}
|
|
|
|
let socket = DATA_DIR.join("komorebi.sock");
|
|
let _ = std::fs::remove_file(socket);
|
|
|
|
std::process::exit(130);
|
|
}
|