Compare commits

...

1 Commits

Author SHA1 Message Date
LGUG2Z
506600d689 feat(bar): don't think i'll pursue this 2022-04-25 08:04:18 -07:00
27 changed files with 2952 additions and 274 deletions

2047
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,5 +4,6 @@ members = [
"derive-ahk",
"komorebi",
"komorebi-core",
"komorebi-bar",
"komorebic"
]

31
komorebi-bar/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "komorebi-bar"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi = { path = "../komorebi" }
komorebi-core = { path = "../komorebi-core" }
as-any = "0.3"
chrono = "0.4"
color-eyre = "0.6"
eframe = "0.17"
egui = "0.17"
lazy_static = "1.4"
miow = "0.4"
schemafy = "0.6"
serde = "1"
serde_json = "1"
parking_lot = "0.12"
local-ip-address = "0.4"
clipboard-win = "4.4"
sysinfo = "0.23"
[dependencies.windows]
version = "0.35"
features = [
"Win32_Graphics_Gdi",
]

125
komorebi-bar/src/bar.rs Normal file
View File

@@ -0,0 +1,125 @@
use crate::date::Date;
use crate::ram::Ram;
use crate::time::Time;
use crate::widget::BarWidget;
use crate::widget::Output;
use crate::widget::Widget;
use crate::IpAddress;
use crate::Storage;
use crate::Workspaces;
use clipboard_win::set_clipboard_string;
use color_eyre::owo_colors::OwoColorize;
use eframe::epi::App;
use eframe::epi::Frame;
use egui::style::Margin;
use egui::CentralPanel;
use egui::Color32;
use egui::Context;
use egui::Direction;
use egui::Layout;
use egui::Rounding;
use std::process::Command;
use std::sync::atomic::Ordering;
pub struct Bar {
pub background_rgb: Color32,
pub text_rgb: Color32,
pub workspaces: Workspaces,
pub time: Time,
pub date: Date,
pub ip_address: IpAddress,
pub memory: Ram,
pub storage: Storage,
}
impl App for Bar {
fn update(&mut self, ctx: &Context, frame: &Frame) {
let custom_frame = egui::Frame {
margin: Margin::symmetric(8.0, 8.0),
rounding: Rounding::none(),
fill: self.background_rgb,
..Default::default()
};
CentralPanel::default().frame(custom_frame).show(ctx, |ui| {
ui.horizontal(|horizontal| {
horizontal.style_mut().visuals.override_text_color = Option::from(self.text_rgb);
horizontal.with_layout(Layout::left_to_right(), |ltr| {
for (i, workspace) in self.workspaces.output().iter().enumerate() {
if workspace == "komorebi offline" {
ltr.label(workspace);
} else {
ctx.request_repaint();
if ltr
.selectable_label(*self.workspaces.selected.lock() == i, workspace)
.clicked()
{
let mut selected = self.workspaces.selected.lock();
*selected = i;
if let Err(error) = Workspaces::focus(i) {
eprintln!("{}", error)
};
}
}
}
});
horizontal.with_layout(Layout::right_to_left(), |rtl| {
for time in self.time.output() {
ctx.request_repaint();
if rtl.button(format!("🕐 {}", time)).clicked() {
self.time.format.toggle()
};
}
for date in self.date.output() {
if rtl.button(format!("📅 {}", date)).clicked() {
self.date.format.next()
};
}
for memory in self.memory.output() {
if rtl.button(format!("🐏 {}", memory)).clicked() {
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).output()
{
eprintln!("{}", error)
}
};
}
for disk in self.storage.output() {
if rtl.button(format!("🖴 {}", disk)).clicked() {
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
disk.split(' ').collect::<Vec<&str>>()[0],
])
.output()
{
eprintln!("{}", error)
}
};
}
for ip in self.ip_address.output() {
if rtl.button(format!("🌐 {}", ip)).clicked() {
if let Err(error) =
Command::new("cmd.exe").args(["/C", "ncpa.cpl"]).output()
{
eprintln!("{}", error)
}
};
}
});
})
});
}
fn name(&self) -> &str {
"komorebi-bar"
}
}

46
komorebi-bar/src/date.rs Normal file
View File

@@ -0,0 +1,46 @@
use crate::widget::BarWidget;
pub enum DateFormat {
MonthDateYear,
YearMonthDate,
DateMonthYear,
DayDateMonthYear,
}
impl DateFormat {
pub fn next(&mut self) {
match self {
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
};
}
fn fmt_string(&self) -> String {
match self {
DateFormat::MonthDateYear => String::from("%D"),
DateFormat::YearMonthDate => String::from("%F"),
DateFormat::DateMonthYear => String::from("%v"),
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
}
}
}
pub struct Date {
pub format: DateFormat,
}
impl Date {
pub fn init(format: DateFormat) -> Self {
Self { format }
}
}
impl BarWidget for Date {
fn output(&mut self) -> Vec<String> {
vec![chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()]
}
}

View File

@@ -0,0 +1,27 @@
use crate::widget::BarWidget;
use local_ip_address::find_ifa;
use local_ip_address::local_ip;
pub struct IpAddress {
pub interface: String,
}
impl IpAddress {
pub fn init(interface: String) -> Self {
IpAddress { interface }
}
}
impl BarWidget for IpAddress {
fn output(&mut self) -> Vec<String> {
if let Ok(interfaces) = local_ip_address::list_afinet_netifas() {
if let Some((interface, ip_address)) =
local_ip_address::find_ifa(interfaces, &self.interface)
{
return vec![format!("{}: {}", interface, ip_address)];
}
}
vec![format!("{}: disconnected", self.interface)]
}
}

80
komorebi-bar/src/main.rs Normal file
View File

@@ -0,0 +1,80 @@
mod bar;
mod date;
mod ip_address;
mod ram;
mod storage;
mod time;
mod widget;
mod workspaces;
use crate::ip_address::IpAddress;
use crate::ram::Ram;
use crate::storage::Storage;
use bar::Bar;
use color_eyre::Result;
use date::Date;
use date::DateFormat;
use eframe::run_native;
use eframe::NativeOptions;
use egui::Color32;
use egui::Pos2;
use egui::Vec2;
use komorebi::WindowsApi;
use time::Time;
use time::TimeFormat;
use windows::Win32::Graphics::Gdi::HMONITOR;
use workspaces::Workspaces;
fn main() -> Result<()> {
let workspaces = Workspaces::init(0)?;
let time = Time::init(TimeFormat::TwentyFourHour);
let date = Date::init(DateFormat::DayDateMonthYear);
let ip_address = IpAddress::init(String::from("Ethernet"));
let app = Bar {
background_rgb: Color32::from_rgb(255, 0, 0),
text_rgb: Color32::from_rgb(255, 255, 255),
workspaces,
time,
date,
ip_address,
memory: Ram,
storage: Storage,
};
let mut win_option = NativeOptions {
decorated: false,
..Default::default()
};
// let hmonitors = WindowsApi::valid_hmonitors()?;
// for hmonitor in hmonitors {
// let info = WindowsApi::monitor_info(hmonitor)?;
// }
let info = WindowsApi::monitor_info_w(HMONITOR(65537))?;
let offset = Offsets {
vertical: 10.0,
horizontal: 200.0,
};
win_option.initial_window_pos = Option::from(Pos2::new(
info.rcWork.left as f32 + offset.horizontal,
info.rcWork.top as f32 + offset.vertical * 2.0,
));
win_option.initial_window_size = Option::from(Vec2::new(
info.rcWork.right as f32 - (offset.horizontal * 2.0),
info.rcWork.top as f32 - offset.vertical,
));
win_option.always_on_top = true;
run_native(Box::new(app), win_option);
}
struct Offsets {
vertical: f32,
horizontal: f32,
}

15
komorebi-bar/src/ram.rs Normal file
View File

@@ -0,0 +1,15 @@
use crate::widget::BarWidget;
use sysinfo::RefreshKind;
use sysinfo::System;
use sysinfo::SystemExt;
pub struct Ram;
impl BarWidget for Ram {
fn output(&mut self) -> Vec<String> {
let sys = System::new_with_specifics(RefreshKind::new().with_memory());
let used = sys.used_memory();
let total = sys.total_memory();
vec![format!("RAM: {}%", (used * 100) / total)]
}
}

View File

@@ -0,0 +1,35 @@
use crate::widget::BarWidget;
use crate::widget::Output;
use crate::widget::Widget;
use color_eyre::Result;
use sysinfo::DiskExt;
use sysinfo::RefreshKind;
use sysinfo::System;
use sysinfo::SystemExt;
pub struct Storage;
impl BarWidget for Storage {
fn output(&mut self) -> Vec<String> {
let sys = System::new_with_specifics(RefreshKind::new().with_disks_list());
let mut disks = vec![];
for disk in sys.disks() {
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
disks.push(format!(
"{} {}%",
mount.to_string_lossy(),
(used * 100) / total
))
}
disks.reverse();
disks
}
}

40
komorebi-bar/src/time.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::widget::BarWidget;
pub enum TimeFormat {
TwelveHour,
TwentyFourHour,
}
impl TimeFormat {
pub fn toggle(&mut self) {
match self {
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
};
}
fn fmt_string(&self) -> String {
match self {
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
TimeFormat::TwentyFourHour => String::from("%T"),
}
}
}
pub struct Time {
pub format: TimeFormat,
}
impl Time {
pub fn init(format: TimeFormat) -> Self {
Self { format }
}
}
impl BarWidget for Time {
fn output(&mut self) -> Vec<String> {
vec![chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()]
}
}

View File

@@ -0,0 +1,25 @@
use as_any::AsAny;
use color_eyre::Result;
#[derive(Debug, Clone)]
pub enum Output {
SingleBox(String),
MultiBox(Vec<String>),
}
#[derive(Debug, Copy, Clone)]
pub enum RepaintStrategy {
Default,
Constant,
}
pub trait Widget: AsAny {
fn output(&mut self) -> Result<Output>;
fn repaint_strategy(&self) -> RepaintStrategy {
RepaintStrategy::Default
}
}
pub trait BarWidget {
fn output(&mut self) -> Vec<String>;
}

View File

@@ -0,0 +1,179 @@
use crate::widget::BarWidget;
use color_eyre::Report;
use color_eyre::Result;
use komorebi::Notification;
use komorebi::State;
use miow::pipe::NamedPipe;
use parking_lot::Mutex;
use std::io::Read;
use std::process::Command;
use std::sync::Arc;
use std::thread;
use std::thread::sleep;
use std::time::Duration;
pub struct Workspaces {
pub enabled: bool,
pub monitor_idx: usize,
pub connected: Arc<Mutex<bool>>,
pub pipe: Arc<Mutex<NamedPipe>>,
pub state: Arc<Mutex<State>>,
pub selected: Arc<Mutex<usize>>,
}
impl BarWidget for Workspaces {
fn output(&mut self) -> Vec<String> {
let state = self.state.lock();
let mut workspaces = vec![];
if let Some(primary_monitor) = state.monitors.elements().get(self.monitor_idx) {
for (i, workspace) in primary_monitor.workspaces().iter().enumerate() {
workspaces.push(if let Some(name) = workspace.name() {
name.clone()
} else {
format!("{}", i + 1)
});
}
}
if workspaces.is_empty() || !*self.connected.lock() {
vec!["komorebi offline".to_string()]
} else {
workspaces
}
}
}
const PIPE: &str = r#"\\.\pipe\"#;
impl Workspaces {
pub fn focus(index: usize) -> Result<()> {
Ok(Command::new("cmd.exe")
.args([
"/C",
"komorebic.exe",
"focus-workspace",
&format!("{}", index),
])
.output()
.map(|_| ())?)
}
pub fn init(monitor_idx: usize) -> Result<Self> {
let name = format!("bar-{}", monitor_idx);
let pipe = format!("{}\\{}", PIPE, name);
let mut named_pipe = NamedPipe::new(pipe)?;
let mut output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", &name])
.output()?;
while !output.status.success() {
println!(
"komorebic.exe failed with error code {:?}, retrying in 5 seconds...",
output.status.code()
);
sleep(Duration::from_secs(5));
output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", &name])
.output()?;
}
named_pipe.connect()?;
let mut buf = vec![0; 4096];
let mut bytes_read = named_pipe.read(&mut buf)?;
let mut data = String::from_utf8(buf[0..bytes_read].to_vec())?;
while data == "\n" {
bytes_read = named_pipe.read(&mut buf)?;
data = String::from_utf8(buf[0..bytes_read].to_vec())?;
}
let notification: Notification = serde_json::from_str(&data)?;
let mut workspaces = Self {
enabled: true,
monitor_idx,
connected: Arc::new(Mutex::new(true)),
pipe: Arc::new(Mutex::new(named_pipe)),
state: Arc::new(Mutex::new(notification.state)),
selected: Arc::new(Mutex::new(0)),
};
workspaces.listen()?;
Ok(workspaces)
}
pub fn listen(&mut self) -> Result<()> {
let state = self.state.clone();
let pipe = self.pipe.clone();
let connected = self.connected.clone();
let selected = self.selected.clone();
thread::spawn(move || -> Result<()> {
let mut buf = vec![0; 4096];
loop {
let mut named_pipe = pipe.lock();
match (*named_pipe).read(&mut buf) {
Ok(bytes_read) => {
let data = String::from_utf8(buf[0..bytes_read].to_vec())?;
if data == "\n" {
continue;
}
let notification: Notification = serde_json::from_str(&data)?;
let mut sl = selected.lock();
*sl = notification.state.monitors.elements()[0].focused_workspace_idx();
let mut st = state.lock();
*st = notification.state;
}
Err(error) => {
// Broken pipe
if error.raw_os_error().unwrap() == 109 {
{
let mut cn = connected.lock();
*cn = false;
}
named_pipe.disconnect()?;
let mut output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", "bar"])
.output()?;
while !output.status.success() {
println!(
"komorebic.exe failed with error code {:?}, retrying in 5 seconds...",
output.status.code()
);
sleep(Duration::from_secs(5));
output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", "bar"])
.output()?;
}
named_pipe.connect()?;
{
let mut cn = connected.lock();
*cn = true;
}
} else {
return Err(Report::from(error));
}
}
}
}
});
Ok(())
}
}

View File

@@ -8,6 +8,14 @@ repository = "https://github.com/LGUG2Z/komorebi"
license = "MIT"
edition = "2021"
[lib]
name = "komorebi"
path = "src/lib.rs"
[[bin]]
name = "komorebi"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -3,14 +3,15 @@ use std::collections::VecDeque;
use getset::Getters;
use nanoid::nanoid;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone, Serialize, Getters, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get = "pub")]
id: String,
windows: Ring<Window>,

236
komorebi/src/lib.rs Normal file
View File

@@ -0,0 +1,236 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use which::which;
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
use komorebi_core::HidingBehaviour;
use komorebi_core::SocketMessage;
#[macro_use]
mod ring;
mod container;
mod monitor;
mod process_command;
mod process_event;
mod process_movement;
mod set_window_position;
mod styles;
mod window;
mod window_manager;
mod window_manager_event;
mod windows_api;
mod windows_callbacks;
mod winevent;
mod winevent_listener;
mod workspace;
pub use process_command::listen_for_commands;
pub use process_event::listen_for_events;
pub use process_movement::listen_for_movements;
pub use window_manager::State;
pub use window_manager::WindowManager;
pub use window_manager_event::WindowManagerEvent;
pub use windows_api::WindowsApi;
pub use winevent_listener::WinEventListener;
lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![
"explorer.exe".to_string(),
"firefox.exe".to_string(),
"chrome.exe".to_string(),
"idea64.exe".to_string(),
"ApplicationFrameHost.exe".to_string(),
"steam.exe".to_string(),
]));
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"firefox.exe".to_string(),
"idea64.exe".to_string(),
]));
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, (usize, usize)>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
// mstsc.exe creates these on Windows 11 when a WSL process is launched
// https://github.com/LGUG2Z/komorebi/issues/74
"OPContainerClass".to_string(),
"IHWindowClass".to_string()
]));
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"X410.exe".to_string(),
"mstsc.exe".to_string(),
"vcxsrv.exe".to_string(),
]));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
Arc::new(Mutex::new(HidingBehaviour::Minimize));
pub static ref HOME_DIR: PathBuf = {
if let Ok(home_path) = std::env::var("KOMOREBI_CONFIG_HOME") {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
}
} else {
dirs::home_dir().expect("there is no home directory")
}
};
}
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// This is the path on Windows 10
let mut current = hkcu
.open_subkey(format!(
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
SESSION_ID.load(Ordering::SeqCst)
))
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
// This is the path on Windows 11
if current.is_none() {
current = hkcu
.open_subkey(r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops"#)
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
}
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
// exist until one has been created in the task view
// The registry value will also not exist on user login if virtual desktops have been created
// but the task view has not been initiated
// In both of these cases, we return None, and the virtual desktop validation will never run. In
// the latter case, if the user desires this validation after initiating the task view, komorebi
// should be restarted, and then when this // fn runs again for the first time, it will pick up
// the value of CurrentVirtualDesktop and validate against it accordingly
current
}
pub fn load_configuration() -> Result<()> {
let home = HOME_DIR.clone();
let mut config_v1 = home.clone();
config_v1.push("komorebi.ahk");
let mut config_v2 = home;
config_v2.push("komorebi.ahk2");
if config_v1.exists() && which("autohotkey.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v1
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("autohotkey.exe")
.arg(config_v1.as_os_str())
.output()?;
} else if config_v2.exists() && which("AutoHotkey64.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v2
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("AutoHotkey64.exe")
.arg(config_v2.as_os_str())
.output()?;
};
Ok(())
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Notification {
pub event: NotificationEvent,
pub state: State,
}
fn notify_subscribers(notification: &str) {
let mut stale_subscriptions = vec![];
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
for (subscriber, pipe) in subscriptions.iter_mut() {
match writeln!(pipe, "{}", notification) {
Ok(_) => {
tracing::debug!("pushed notification to subscriber: {}", subscriber);
}
Err(error) => {
// ERROR_FILE_NOT_FOUND
// 2 (0x2)
// The system cannot find the file specified.
// ERROR_NO_DATA
// 232 (0xE8)
// The pipe is being closed.
// Remove the subscription; the process will have to subscribe again
if let Some(2 | 232) = error.raw_os_error() {
let subscriber_cl = subscriber.clone();
stale_subscriptions.push(subscriber_cl);
}
}
}
}
for subscriber in stale_subscriptions {
tracing::warn!("removing stale subscription: {}", subscriber);
subscriptions.remove(&subscriber);
}
}

View File

@@ -1,127 +1,37 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU32;
use clap::Parser;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
use std::sync::atomic::Ordering;
use std::sync::Arc;
#[cfg(feature = "deadlock_detection")]
use std::thread;
#[cfg(feature = "deadlock_detection")]
use std::time::Duration;
use clap::Parser;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Serialize;
use sysinfo::Process;
use sysinfo::ProcessExt;
use sysinfo::SystemExt;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use which::which;
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
use komorebi_core::HidingBehaviour;
use komorebi_core::SocketMessage;
use crate::process_command::listen_for_commands;
use crate::process_event::listen_for_events;
use crate::process_movement::listen_for_movements;
use crate::window_manager::State;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
#[macro_use]
mod ring;
mod container;
mod monitor;
mod process_command;
mod process_event;
mod process_movement;
mod set_window_position;
mod styles;
mod window;
mod window_manager;
mod window_manager_event;
mod windows_api;
mod windows_callbacks;
mod winevent;
mod winevent_listener;
mod workspace;
lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![
"explorer.exe".to_string(),
"firefox.exe".to_string(),
"chrome.exe".to_string(),
"idea64.exe".to_string(),
"ApplicationFrameHost.exe".to_string(),
"steam.exe".to_string(),
]));
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"firefox.exe".to_string(),
"idea64.exe".to_string(),
]));
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, (usize, usize)>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
// mstsc.exe creates these on Windows 11 when a WSL process is launched
// https://github.com/LGUG2Z/komorebi/issues/74
"OPContainerClass".to_string(),
"IHWindowClass".to_string()
]));
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"X410.exe".to_string(),
"mstsc.exe".to_string(),
"vcxsrv.exe".to_string(),
]));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
Arc::new(Mutex::new(HidingBehaviour::Minimize));
static ref HOME_DIR: PathBuf = {
if let Ok(home_path) = std::env::var("KOMOREBI_CONFIG_HOME") {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
}
} else {
dirs::home_dir().expect("there is no home directory")
}
};
}
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
use komorebi::listen_for_commands;
use komorebi::listen_for_events;
use komorebi::listen_for_movements;
use komorebi::load_configuration;
use komorebi::WinEventListener;
use komorebi::WindowManager;
use komorebi::WindowManagerEvent;
use komorebi::WindowsApi;
use komorebi::CUSTOM_FFM;
use komorebi::HOME_DIR;
use komorebi::SESSION_ID;
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
@@ -187,134 +97,6 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
Ok((guard, color_guard))
}
pub fn load_configuration() -> Result<()> {
let home = HOME_DIR.clone();
let mut config_v1 = home.clone();
config_v1.push("komorebi.ahk");
let mut config_v2 = home;
config_v2.push("komorebi.ahk2");
if config_v1.exists() && which("autohotkey.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v1
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("autohotkey.exe")
.arg(config_v1.as_os_str())
.output()?;
} else if config_v2.exists() && which("AutoHotkey64.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v2
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("AutoHotkey64.exe")
.arg(config_v2.as_os_str())
.output()?;
};
Ok(())
}
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// This is the path on Windows 10
let mut current = hkcu
.open_subkey(format!(
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
SESSION_ID.load(Ordering::SeqCst)
))
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
// This is the path on Windows 11
if current.is_none() {
current = hkcu
.open_subkey(r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops"#)
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
}
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
// exist until one has been created in the task view
// The registry value will also not exist on user login if virtual desktops have been created
// but the task view has not been initiated
// In both of these cases, we return None, and the virtual desktop validation will never run. In
// the latter case, if the user desires this validation after initiating the task view, komorebi
// should be restarted, and then when this // fn runs again for the first time, it will pick up
// the value of CurrentVirtualDesktop and validate against it accordingly
current
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct Notification {
pub event: NotificationEvent,
pub state: State,
}
pub fn notify_subscribers(notification: &str) -> Result<()> {
let mut stale_subscriptions = vec![];
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
for (subscriber, pipe) in subscriptions.iter_mut() {
match writeln!(pipe, "{}", notification) {
Ok(_) => {
tracing::debug!("pushed notification to subscriber: {}", subscriber);
}
Err(error) => {
// ERROR_FILE_NOT_FOUND
// 2 (0x2)
// The system cannot find the file specified.
// ERROR_NO_DATA
// 232 (0xE8)
// The pipe is being closed.
// Remove the subscription; the process will have to subscribe again
if let Some(2 | 232) = error.raw_os_error() {
let subscriber_cl = subscriber.clone();
stale_subscriptions.push(subscriber_cl);
}
}
}
}
for subscriber in stale_subscriptions {
tracing::warn!("removing stale subscription: {}", subscriber);
subscriptions.remove(&subscriber);
}
Ok(())
}
#[cfg(feature = "deadlock_detection")]
#[tracing::instrument]
fn detect_deadlocks() {
@@ -389,7 +171,7 @@ fn main() -> Result<()> {
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
let winevent_listener = WinEventListener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(

View File

@@ -8,6 +8,7 @@ use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Rect;
@@ -16,7 +17,9 @@ use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
#[derive(
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
id: isize,
@@ -25,7 +28,7 @@ pub struct Monitor {
#[getset(get = "pub", set = "pub")]
work_area_size: Rect,
workspaces: Ring<Workspace>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get_mut = "pub")]
workspace_names: HashMap<usize, String>,
}

View File

@@ -722,7 +722,7 @@ impl WindowManager {
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
})?)?;
})?);
}
Ok(())

View File

@@ -495,7 +495,7 @@ impl WindowManager {
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::WindowManager(*event),
state: self.as_ref().into(),
})?)?;
})?);
tracing::info!("processed: {}", event.window().to_string());
Ok(())

View File

@@ -1,9 +1,10 @@
use std::collections::VecDeque;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Ring<T> {
elements: VecDeque<T>,
focused: usize,

View File

@@ -7,6 +7,7 @@ use color_eyre::Result;
use schemars::JsonSchema;
use serde::ser::Error;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use windows::Win32::Foundation::HWND;
@@ -26,7 +27,7 @@ use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy, JsonSchema)]
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
pub struct Window {
pub(crate) hwnd: isize,
}

View File

@@ -12,6 +12,7 @@ use hotwatch::notify::DebouncedEvent;
use hotwatch::Hotwatch;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixListener;
@@ -66,7 +67,7 @@ pub struct WindowManager {
pub pending_move_op: Option<(usize, usize, usize)>,
}
#[derive(Debug, Serialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct State {
pub monitors: Ring<Monitor>,
pub is_paused: bool,

View File

@@ -2,13 +2,14 @@ use std::fmt::Display;
use std::fmt::Formatter;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::window::Window;
use crate::winevent::WinEvent;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
#[derive(Debug, Copy, Clone, Serialize, JsonSchema)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "content")]
pub enum WindowManagerEvent {
Destroy(WinEvent, Window),
@@ -89,6 +90,7 @@ impl Display for WindowManagerEvent {
}
impl WindowManagerEvent {
#[must_use]
pub const fn window(self) -> Window {
match self {
WindowManagerEvent::Destroy(_, window)
@@ -106,6 +108,7 @@ impl WindowManagerEvent {
}
}
#[must_use]
pub fn from_win_event(winevent: WinEvent, window: Window) -> Option<Self> {
match winevent {
WinEvent::ObjectDestroy => Option::from(Self::Destroy(winevent, window)),

View File

@@ -239,12 +239,14 @@ impl WindowsApi {
.process()
}
#[must_use]
pub fn monitor_from_window(hwnd: HWND) -> isize {
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
}
#[must_use]
pub fn monitor_from_point(point: POINT) -> isize {
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
@@ -364,6 +366,7 @@ impl WindowsApi {
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
}
#[must_use]
pub fn window_thread_process_id(hwnd: HWND) -> (u32, u32) {
let mut process_id: u32 = 0;
@@ -374,10 +377,12 @@ impl WindowsApi {
(process_id, thread_id)
}
#[must_use]
pub fn current_thread_id() -> u32 {
unsafe { GetCurrentThreadId() }
}
#[must_use]
pub fn current_process_id() -> u32 {
unsafe { GetCurrentProcessId() }
}
@@ -532,18 +537,25 @@ impl WindowsApi {
))
}
#[must_use]
pub fn is_window(hwnd: HWND) -> bool {
unsafe { IsWindow(hwnd) }.into()
}
#[must_use]
pub fn is_window_visible(hwnd: HWND) -> bool {
unsafe { IsWindowVisible(hwnd) }.into()
}
#[must_use]
pub fn is_iconic(hwnd: HWND) -> bool {
unsafe { IsIconic(hwnd) }.into()
}
pub fn monitor_info(hmonitor: isize) -> Result<MONITORINFO> {
Self::monitor_info_w(HMONITOR(hmonitor))
}
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFO> {
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
@@ -566,7 +578,7 @@ impl WindowsApi {
}
#[allow(dead_code)]
pub fn system_parameters_info_w(
fn system_parameters_info_w(
action: SYSTEM_PARAMETERS_INFO_ACTION,
ui_param: u32,
pv_param: *mut c_void,

View File

@@ -1,6 +1,7 @@
#![allow(clippy::use_self)]
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
@@ -88,7 +89,7 @@ use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Display, JsonSchema)]
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display, JsonSchema)]
#[repr(u32)]
#[allow(dead_code)]
pub enum WinEvent {

View File

@@ -32,14 +32,15 @@ pub struct WinEventListener {
outgoing_events: Arc<Mutex<Sender<WindowManagerEvent>>>,
}
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> WinEventListener {
WinEventListener {
hook: Arc::new(AtomicIsize::new(0)),
outgoing_events: outgoing,
}
}
impl WinEventListener {
#[must_use]
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> Self {
Self {
hook: Arc::new(AtomicIsize::new(0)),
outgoing_events: outgoing,
}
}
pub fn start(self) {
let hook = self.hook.clone();
let outgoing = self.outgoing_events.lock().clone();

View File

@@ -8,6 +8,7 @@ use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Axis;
@@ -22,19 +23,21 @@ use crate::ring::Ring;
use crate::window::Window;
use crate::windows_api::WindowsApi;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
#[derive(
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Workspace {
#[getset(set = "pub")]
#[getset(get = "pub", set = "pub")]
name: Option<String>,
containers: Ring<Container>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
monocle_container: Option<Container>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get_copy = "pub", set = "pub")]
monocle_container_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
maximized_window: Option<Window>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get_copy = "pub", set = "pub")]
maximized_window_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub")]
@@ -49,7 +52,7 @@ pub struct Workspace {
workspace_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
container_padding: Option<i32>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get = "pub", set = "pub")]
latest_layout: Vec<Rect>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]