mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-13 22:13:02 +01:00
Compare commits
1 Commits
monitor-pr
...
feature/ko
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580f70a5a1 |
60
Cargo.lock
generated
60
Cargo.lock
generated
@@ -17,6 +17,15 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
@@ -251,6 +260,19 @@ version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.8"
|
||||
@@ -392,6 +414,12 @@ dependencies = [
|
||||
"notify",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
@@ -452,6 +480,18 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||
|
||||
[[package]]
|
||||
name = "json_dotpath"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
@@ -462,6 +502,24 @@ dependencies = [
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "komokana"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"json_dotpath",
|
||||
"log",
|
||||
"miow 0.4.0",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "komorebi"
|
||||
version = "0.1.10"
|
||||
@@ -945,6 +1003,8 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,5 +4,6 @@ members = [
|
||||
"derive-ahk",
|
||||
"komorebi",
|
||||
"komorebi-core",
|
||||
"komorebic"
|
||||
"komorebic",
|
||||
"komokana"
|
||||
]
|
||||
|
||||
4
justfile
4
justfile
@@ -15,9 +15,13 @@ install-komorebic:
|
||||
install-komorebi:
|
||||
cargo +stable install --path komorebi --locked
|
||||
|
||||
install-komokana:
|
||||
cargo +stable install --path komokana --locked
|
||||
|
||||
install:
|
||||
just install-komorebic
|
||||
just install-komorebi
|
||||
just install-komokana
|
||||
komorebic ahk-library
|
||||
cat '%USERPROFILE%\.config\komorebi\komorebic.lib.ahk' > komorebic.lib.sample.ahk
|
||||
|
||||
|
||||
25
komokana/Cargo.toml
Normal file
25
komokana/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "komokana"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "3", features = ["derive", "wrap_help"] }
|
||||
color-eyre = "0.6"
|
||||
dirs = "4"
|
||||
env_logger = "0.9"
|
||||
json_dotpath = "1"
|
||||
log = "0.4"
|
||||
miow = "0.4"
|
||||
parking_lot = "0.12"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.8"
|
||||
|
||||
[dependencies.windows]
|
||||
version = "0.39"
|
||||
features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
]
|
||||
37
komokana/src/configuration.rs
Normal file
37
komokana/src/configuration.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
#![allow(clippy::use_self)]
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub type Configuration = Vec<Entry>;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Entry {
|
||||
pub exe: String,
|
||||
pub target_layer: String,
|
||||
pub title_overrides: Option<Vec<TitleOverride>>,
|
||||
pub virtual_key_overrides: Option<Vec<VirtualKeyOverride>>,
|
||||
pub virtual_key_ignores: Option<Vec<i32>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TitleOverride {
|
||||
pub title: String,
|
||||
pub strategy: Strategy,
|
||||
pub target_layer: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VirtualKeyOverride {
|
||||
pub virtual_key_code: i32,
|
||||
pub targer_layer: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Strategy {
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
Contains,
|
||||
Equals,
|
||||
}
|
||||
376
komokana/src/main.rs
Normal file
376
komokana/src/main.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use clap::Parser;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Report;
|
||||
use color_eyre::Result;
|
||||
use dirs::home_dir;
|
||||
use json_dotpath::DotPaths;
|
||||
use miow::pipe::NamedPipe;
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::json;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState;
|
||||
|
||||
use crate::configuration::Configuration;
|
||||
use crate::configuration::Strategy;
|
||||
|
||||
mod configuration;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
#[clap(short = 'p', long)]
|
||||
kanata_port: i32,
|
||||
#[clap(short, long, default_value = "~/komokana.yaml")]
|
||||
configuration: String,
|
||||
#[clap(short, long)]
|
||||
default_layer: String,
|
||||
#[clap(short, long, action)]
|
||||
tmpfile: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli: Cli = Cli::parse();
|
||||
let configuration = resolve_windows_path(&cli.configuration)?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
env_logger::builder().format_timestamp(None).init();
|
||||
|
||||
let mut komokana = Komokana::init(
|
||||
configuration,
|
||||
cli.kanata_port,
|
||||
cli.default_layer,
|
||||
cli.tmpfile,
|
||||
)?;
|
||||
|
||||
komokana.listen();
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
|
||||
struct Komokana {
|
||||
komorebi: Arc<Mutex<NamedPipe>>,
|
||||
kanata: Arc<Mutex<TcpStream>>,
|
||||
configuration: Configuration,
|
||||
default_layer: String,
|
||||
tmpfile: bool,
|
||||
}
|
||||
|
||||
const PIPE: &str = r#"\\.\pipe\"#;
|
||||
|
||||
impl Komokana {
|
||||
pub fn init(
|
||||
configuration: PathBuf,
|
||||
kanata_port: i32,
|
||||
default_layer: String,
|
||||
tmpfile: bool,
|
||||
) -> Result<Self> {
|
||||
let name = "komokana";
|
||||
let pipe = format!("{}\\{}", PIPE, name);
|
||||
let mut cfg = home_dir().expect("could not look up home dir");
|
||||
cfg.push("komokana.yaml");
|
||||
|
||||
let configuration: Configuration =
|
||||
serde_yaml::from_str(&std::fs::read_to_string(configuration)?)?;
|
||||
|
||||
let named_pipe = NamedPipe::new(pipe)?;
|
||||
|
||||
let mut output = Command::new("cmd.exe")
|
||||
.args(["/C", "komorebic.exe", "subscribe", name])
|
||||
.output()?;
|
||||
|
||||
while !output.status.success() {
|
||||
log::warn!(
|
||||
"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()?;
|
||||
log::debug!("connected to komorebi");
|
||||
|
||||
let stream = TcpStream::connect(format!("localhost:{kanata_port}"))?;
|
||||
log::debug!("connected to kanata");
|
||||
|
||||
Ok(Self {
|
||||
komorebi: Arc::new(Mutex::new(named_pipe)),
|
||||
kanata: Arc::new(Mutex::new(stream)),
|
||||
configuration,
|
||||
default_layer,
|
||||
tmpfile,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn listen(&mut self) {
|
||||
let pipe = self.komorebi.clone();
|
||||
let stream = self.kanata.clone();
|
||||
let stream_read = self.kanata.clone();
|
||||
let tmpfile = self.tmpfile;
|
||||
log::info!("listening");
|
||||
|
||||
thread::spawn(move || -> Result<()> {
|
||||
let mut read_stream = stream_read.lock().try_clone()?;
|
||||
drop(stream_read);
|
||||
|
||||
loop {
|
||||
let mut buf = vec![0; 1024];
|
||||
if let Ok(bytes_read) = read_stream.read(&mut buf) {
|
||||
let data = String::from_utf8(buf[0..bytes_read].to_vec())?;
|
||||
if data == "\n" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let notification: serde_json::Value = serde_json::from_str(&data)?;
|
||||
|
||||
if notification.dot_has("LayerChange.new") {
|
||||
if let Some(new) = notification.dot_get::<String>("LayerChange.new")? {
|
||||
log::info!("current layer: {new}");
|
||||
if tmpfile {
|
||||
let mut tmp = std::env::temp_dir();
|
||||
tmp.push("kanata_layer");
|
||||
std::fs::write(tmp, new)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let config = self.configuration.clone();
|
||||
let default_layer = self.default_layer.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: serde_json::Value = serde_json::from_str(&data)?;
|
||||
if notification.dot_has("event.content.1.exe") {
|
||||
if let (Some(exe), Some(title), Some(kind)) = (
|
||||
notification.dot_get::<String>("event.content.1.exe")?,
|
||||
notification.dot_get::<String>("event.content.1.title")?,
|
||||
notification.dot_get::<String>("event.type")?,
|
||||
) {
|
||||
match kind.as_str() {
|
||||
"Show" => handle_event(
|
||||
&config,
|
||||
&stream,
|
||||
&default_layer,
|
||||
Event::Show,
|
||||
&exe,
|
||||
&title,
|
||||
)?,
|
||||
"FocusChange" => handle_event(
|
||||
&config,
|
||||
&stream,
|
||||
&default_layer,
|
||||
Event::FocusChange,
|
||||
&exe,
|
||||
&title,
|
||||
)?,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// Broken pipe
|
||||
if error.raw_os_error().expect("could not get raw os error") == 109 {
|
||||
named_pipe.disconnect()?;
|
||||
|
||||
let mut output = Command::new("cmd.exe")
|
||||
.args(["/C", "komorebic.exe", "subscribe", "bar"])
|
||||
.output()?;
|
||||
|
||||
while !output.status.success() {
|
||||
log::warn!(
|
||||
"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()?;
|
||||
} else {
|
||||
return Err(Report::from(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(
|
||||
configuration: &Configuration,
|
||||
stream: &Arc<Mutex<TcpStream>>,
|
||||
default_layer: &str,
|
||||
event: Event,
|
||||
exe: &str,
|
||||
title: &str,
|
||||
) -> Result<()> {
|
||||
let target = calculate_target(
|
||||
configuration,
|
||||
event,
|
||||
exe,
|
||||
title,
|
||||
if matches!(event, Event::FocusChange) {
|
||||
Option::from(default_layer)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(target) = target {
|
||||
let mut stream = stream.lock();
|
||||
let request = json!({
|
||||
"ChangeLayer": {
|
||||
"new": target,
|
||||
}
|
||||
});
|
||||
|
||||
stream.write_all(request.to_string().as_bytes())?;
|
||||
log::debug!("request sent: {request}");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Event {
|
||||
Show,
|
||||
FocusChange,
|
||||
}
|
||||
|
||||
fn calculate_target(
|
||||
configuration: &Configuration,
|
||||
event: Event,
|
||||
exe: &str,
|
||||
title: &str,
|
||||
default: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let mut new_layer = default;
|
||||
for entry in configuration {
|
||||
if entry.exe == exe {
|
||||
if matches!(event, Event::FocusChange) {
|
||||
new_layer = Option::from(entry.target_layer.as_str());
|
||||
}
|
||||
|
||||
if let Some(title_overrides) = &entry.title_overrides {
|
||||
for title_override in title_overrides {
|
||||
match title_override.strategy {
|
||||
Strategy::StartsWith => {
|
||||
if title.starts_with(&title_override.title) {
|
||||
new_layer = Option::from(title_override.target_layer.as_str());
|
||||
}
|
||||
}
|
||||
Strategy::EndsWith => {
|
||||
if title.ends_with(&title_override.title) {
|
||||
new_layer = Option::from(title_override.target_layer.as_str());
|
||||
}
|
||||
}
|
||||
Strategy::Contains => {
|
||||
if title.contains(&title_override.title) {
|
||||
new_layer = Option::from(title_override.target_layer.as_str());
|
||||
}
|
||||
}
|
||||
Strategy::Equals => {
|
||||
if title.eq(&title_override.title) {
|
||||
new_layer = Option::from(title_override.target_layer.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This acts like a default target layer within the application
|
||||
// which defaults back to the entry's main target layer
|
||||
if new_layer.is_none() {
|
||||
new_layer = Option::from(entry.target_layer.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(event, Event::FocusChange) {
|
||||
if let Some(virtual_key_overrides) = &entry.virtual_key_overrides {
|
||||
for virtual_key_override in virtual_key_overrides {
|
||||
if unsafe { GetKeyState(virtual_key_override.virtual_key_code) } < 0 {
|
||||
new_layer = Option::from(virtual_key_override.targer_layer.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(virtual_key_ignores) = &entry.virtual_key_ignores {
|
||||
for virtual_key in virtual_key_ignores {
|
||||
if unsafe { GetKeyState(*virtual_key) } < 0 {
|
||||
new_layer = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new_layer.and_then(|new_layer| Option::from(new_layer.to_string()))
|
||||
}
|
||||
|
||||
fn resolve_windows_path(raw_path: &str) -> Result<PathBuf> {
|
||||
let path = if raw_path.starts_with('~') {
|
||||
raw_path.replacen(
|
||||
'~',
|
||||
&dirs::home_dir()
|
||||
.ok_or_else(|| anyhow!("there is no home directory"))?
|
||||
.display()
|
||||
.to_string(),
|
||||
1,
|
||||
)
|
||||
} else {
|
||||
raw_path.to_string()
|
||||
};
|
||||
|
||||
let full_path = PathBuf::from(path);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot parse directory"))?;
|
||||
|
||||
let file = full_path
|
||||
.components()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("cannot parse filename"))?;
|
||||
|
||||
let mut canonicalized = std::fs::canonicalize(parent)?;
|
||||
canonicalized.push(file);
|
||||
|
||||
Ok(canonicalized)
|
||||
}
|
||||
Reference in New Issue
Block a user