Compare commits

...

1 Commits

Author SHA1 Message Date
LGUG2Z
580f70a5a1 feat(komokana): initial commit
This is the initial commit of a minimal working version of komokana, a
daemon that I'm starting to use both as a kanata current layer
notification widget, and as a way to automatically change kanata layers
when different conditions are met in the komorebi subscriber event
stream.

I'm not sure if this will finally be merged into the master branch or
not, but for now I'll keep the code here.
2022-07-21 14:24:41 -07:00
6 changed files with 504 additions and 1 deletions

60
Cargo.lock generated
View File

@@ -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",
]

View File

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

View File

@@ -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
View 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",
]

View 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
View 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)
}