refactor(komorebic): the macOS komorebic uses the pan module of komorebic-core

komorebi-for-mac/komorebic/src/pan.rs (810 lines) is deleted: it differed from the Windows copy by three doc comments only (whkd wording versus a generic one, 21 changed lines), so the shared module keeps the Windows wording of those comments; no code differs. pan --stream runs komorebic_core::pan with a PanLink over the client connection.

Verified: macOS fake-CC cargo check --locked --workspace --all-targets: 0 errors. The 16 pan tests of the macOS tree were the same tests as Windows'; they run in komorebic-core (27 passed). Not verified on macOS itself.
This commit is contained in:
LGUG2Z committed 2026-10-31 01:37:54 -07:00
1 parent 7c655cbc25
commit 0345fccba2
2 files changed
+11 -813

No files matched your search

+11 -3
View File
@@ -14,6 +14,7 @@ use komorebic_core::cli::Meta;
use komorebic_core::cli::command;
use komorebic_core::docgen::DocFormat;
use komorebic_core::messages::Messages;
use komorebic_core::pan;
use komorebic_core::platform::macos::*;
use lazy_static::lazy_static;
use std::fs::File;
@@ -30,8 +31,6 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use sysinfo::ProcessesToUpdate;
mod pan;
lazy_static! {
static ref HAS_CUSTOM_CONFIG_HOME: AtomicBool = AtomicBool::new(false);
static ref HOME_DIR: PathBuf = {
@@ -82,6 +81,15 @@ fn send_or_query(messages: Messages) -> eyre::Result<()> {
Ok(())
}
/// The connection to the daemon that the gesture stream tells what the fingers do over.
struct PanLink(komorebi_client::Connection);
impl pan::Link for PanLink {
fn send(&mut self, message: &SocketMessage) -> io::Result<()> {
self.0.send(message)
}
}
fn main() -> eyre::Result<()> {
let opts = komorebic_core::cli::parse::<MacosCommands>(&META);
@@ -775,7 +783,7 @@ exit 1
reversed: args.reversed,
paging: args.paging,
},
&mut pan::Socket::new(komorebi_client::Connection::open),
&mut pan::Socket::new(|| komorebi_client::Connection::open().map(PanLink)),
)?;
} else if let Some(direction) = args.direction {
send_message(&SocketMessage::Pan(direction))?;
-810
View File
@@ -1,810 +0,0 @@
//! Reading the stream of gestures that `komorebic pan --stream` is given, which is what a program
//! that reads a touchpad writes for a swipe of fingers, and telling komorebi what the fingers do.
//!
//! The format is the gesture stream that whkd writes on Windows, and anything that writes it can
//! be piped in: one message to a line, a name and then `key=value` fields in any order, the
//! distances in centimetres. A field that is not known is ignored, and so is a message that is not
//! known.
use komorebi_client::PanDirection;
use komorebi_client::PanGesture;
use komorebi_client::SocketMessage;
use std::collections::VecDeque;
use std::io;
use std::io::BufRead;
use std::sync::mpsc;
/// How far the fingers go with `--paging` for each step, in centimetres
pub const PAGE_CM: f64 = 2.0;
/// One thing that the fingers do
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Message {
Begin,
Move { dx: f64, dy: f64, t: f64 },
End,
Cancel,
}
/// What a line of the stream comes to
#[derive(Clone, Debug, PartialEq)]
pub enum Line {
Message(Message),
/// Nothing to do: a line with nothing in it, or a comment, or a message that is not known
Ignored,
/// A message that is known, and is not what it should be
Bad(String),
}
/// What a line of the stream says
#[must_use]
pub fn parse_line(line: &str) -> Line {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return Line::Ignored;
}
let mut words = line.split_whitespace();
let Some(name) = words.next() else {
return Line::Ignored;
};
let fields: Vec<(&str, &str)> = words.filter_map(|word| word.split_once('=')).collect();
let number = |key: &str| -> Option<Result<f64, String>> {
fields
.iter()
.find(|(name, _)| *name == key)
.map(|(_, value)| {
value
.parse::<f64>()
.ok()
.filter(|number| number.is_finite())
.ok_or_else(|| format!("{key}={value} is not a number"))
})
};
match name {
"begin" => Line::Message(Message::Begin),
"end" => Line::Message(Message::End),
"cancel" => Line::Message(Message::Cancel),
"move" => {
let dx = match number("dx") {
Some(Ok(dx)) => dx,
Some(Err(error)) => return Line::Bad(error),
None => return Line::Bad(String::from("a move has no dx")),
};
let t = match number("t") {
Some(Ok(t)) => t,
Some(Err(error)) => return Line::Bad(error),
None => return Line::Bad(String::from("a move has no t")),
};
let dy = match number("dy") {
Some(Ok(dy)) => dy,
Some(Err(error)) => return Line::Bad(error),
None => 0.0,
};
Line::Message(Message::Move { dx, dy, t })
}
_ => Line::Ignored,
}
}
/// What `--stream` is asked to do with the distances
#[derive(Clone, Copy, Debug)]
pub struct Options {
/// What the distances are multiplied by
pub sensitivity: f64,
/// The strip goes the other way to the fingers
pub reversed: bool,
/// One step for each [`PAGE_CM`] that the fingers go, and not the strip going with them
pub paging: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
sensitivity: 1.0,
reversed: false,
paging: false,
}
}
}
/// Works out what komorebi is told for what the fingers do
#[derive(Debug)]
pub struct Forwarder {
options: Options,
/// Whether a gesture is going on
active: bool,
/// How far the fingers have gone since the last step, with paging
page: f64,
}
impl Forwarder {
#[must_use]
pub const fn new(options: Options) -> Self {
Self {
options,
active: false,
page: 0.0,
}
}
/// What komorebi is told for this. Fingers that move when there was no beginning, and an end
/// or a cancel for a gesture that is not going on, are not anything.
pub fn handle(&mut self, message: Message) -> Vec<SocketMessage> {
let gesture = |gesture| vec![SocketMessage::PanGesture(gesture)];
match message {
Message::Begin => {
self.active = true;
self.page = 0.0;
if self.options.paging {
Vec::new()
} else {
gesture(PanGesture::Begin)
}
}
Message::Move { dx, dy, t } if self.active => {
let sign = if self.options.reversed { -1.0 } else { 1.0 };
let dx = dx * self.options.sensitivity * sign;
let dy = dy * self.options.sensitivity;
if self.options.paging {
self.page += dx;
self.pages()
} else {
gesture(PanGesture::Move { dx, dy, t })
}
}
Message::End if self.active => {
self.active = false;
if self.options.paging {
Vec::new()
} else {
gesture(PanGesture::End)
}
}
Message::Cancel if self.active => {
self.active = false;
if self.options.paging {
Vec::new()
} else {
gesture(PanGesture::Cancel)
}
}
_ => Vec::new(),
}
}
/// The stream is over: a gesture that is still going on was not finished, so it is given up
pub fn finish(&mut self) -> Vec<SocketMessage> {
let unfinished = self.active && !self.options.paging;
self.active = false;
if unfinished {
vec![SocketMessage::PanGesture(PanGesture::Cancel)]
} else {
Vec::new()
}
}
/// The steps that what the fingers have gone since the last one comes to. Fingers that go to
/// the right take the strip with them, which is to look to the left.
fn pages(&mut self) -> Vec<SocketMessage> {
let mut steps = Vec::new();
while self.page.abs() >= PAGE_CM {
let direction = if self.page > 0.0 {
PanDirection::Left
} else {
PanDirection::Right
};
self.page -= PAGE_CM.copysign(self.page);
steps.push(SocketMessage::Pan(direction));
}
steps
}
}
/// The next message to handle, with the moves that follow it added to it if it is one: when
/// the stream comes faster than it can be handled, the distances add up and the time is the
/// last one, as whkd says that they may.
pub fn next_merged(queue: &mut VecDeque<Message>) -> Option<Message> {
let first = queue.pop_front()?;
let Message::Move {
mut dx,
mut dy,
mut t,
} = first
else {
return Some(first);
};
while let Some(Message::Move {
dx: more_dx,
dy: more_dy,
t: later,
}) = queue.front().copied()
{
dx += more_dx;
dy += more_dy;
t = later;
queue.pop_front();
}
Some(Message::Move { dx, dy, t })
}
/// Where komorebi is told what the fingers do
pub trait Sink {
fn send(&mut self, message: &SocketMessage) -> io::Result<()>;
/// The gesture that was being told is over
fn gesture_over(&mut self);
}
/// A way of telling komorebi things, in order
pub trait Link {
fn send(&mut self, message: &SocketMessage) -> io::Result<()>;
}
impl Link for komorebi_client::Connection {
fn send(&mut self, message: &SocketMessage) -> io::Result<()> {
komorebi_client::Connection::send(self, message)
}
}
/// Tells komorebi what the fingers do over one connection for each gesture. Messages that are
/// sent on a connection each are not handled in the order that they were sent, so a gesture is
/// told on one, which is made when it is first needed and let go of when the gesture is over.
/// If it cannot be written to the connection is made again once, as komorebi may have let go of
/// one that nothing has been written to for a while.
pub struct Socket<L, F> {
open: F,
link: Option<L>,
}
impl<L: Link, F: FnMut() -> io::Result<L>> Socket<L, F> {
pub const fn new(open: F) -> Self {
Self { open, link: None }
}
fn try_send(&mut self, message: &SocketMessage) -> io::Result<()> {
if self.link.is_none() {
self.link = Some((self.open)()?);
}
match self.link.as_mut() {
Some(link) => link.send(message),
None => Ok(()),
}
}
}
impl<L: Link, F: FnMut() -> io::Result<L>> Sink for Socket<L, F> {
fn send(&mut self, message: &SocketMessage) -> io::Result<()> {
if self.try_send(message).is_err() {
self.link = None;
return self.try_send(message);
}
Ok(())
}
fn gesture_over(&mut self) {
self.link = None;
}
}
/// Read gestures from `input` until it closes and tell komorebi what the fingers do with `sink`
pub fn run(
input: impl BufRead + Send + 'static,
options: Options,
sink: &mut impl Sink,
) -> io::Result<()> {
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
for line in input.lines() {
let Ok(line) = line else {
break;
};
match parse_line(&line) {
Line::Message(message) => {
if sender.send(message).is_err() {
break;
}
}
Line::Ignored => {}
Line::Bad(why) => eprintln!("pan: {why}: {line}"),
}
}
});
let mut forwarder = Forwarder::new(options);
let mut queue = VecDeque::new();
while let Ok(first) = receiver.recv() {
queue.push_back(first);
queue.extend(receiver.try_iter());
while let Some(message) = next_merged(&mut queue) {
let over = matches!(message, Message::End | Message::Cancel);
for told in forwarder.handle(message) {
sink.send(&told)?;
}
if over {
sink.gesture_over();
}
}
}
for told in forwarder.finish() {
sink.send(&told)?;
}
sink.gesture_over();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const TRANSCRIPT: &str = include_str!("../fixtures/gesture-stream.transcript.txt");
fn gestures(told: &[SocketMessage]) -> Vec<PanGesture> {
told.iter()
.filter_map(|message| match message {
SocketMessage::PanGesture(gesture) => Some(*gesture),
_ => None,
})
.collect()
}
fn messages(text: &str) -> Vec<Message> {
text.lines()
.filter_map(|line| match parse_line(line) {
Line::Message(message) => Some(message),
_ => None,
})
.collect()
}
/// What is told, and how many gestures have been over
#[derive(Default)]
struct Recorder {
told: Vec<SocketMessage>,
over: usize,
}
impl Sink for Recorder {
fn send(&mut self, message: &SocketMessage) -> io::Result<()> {
self.told.push(message.clone());
Ok(())
}
fn gesture_over(&mut self) {
self.over += 1;
}
}
/// A link that remembers what it was written, and whose connections are numbered
struct Wire {
connection: usize,
written: Written,
fail_next: Failing,
}
impl Link for Wire {
fn send(&mut self, message: &SocketMessage) -> io::Result<()> {
if self.fail_next.replace(false) {
return Err(io::Error::new(io::ErrorKind::BrokenPipe, "let go of"));
}
self.written
.borrow_mut()
.push((self.connection, message.clone()));
Ok(())
}
}
type Written = std::rc::Rc<std::cell::RefCell<Vec<(usize, SocketMessage)>>>;
type Failing = std::rc::Rc<std::cell::Cell<bool>>;
#[allow(clippy::type_complexity)]
fn wire() -> (
Socket<Wire, impl FnMut() -> io::Result<Wire>>,
Written,
Failing,
) {
let written = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let fail_next = std::rc::Rc::new(std::cell::Cell::new(false));
let mut made = 0;
let socket = Socket::new({
let written = written.clone();
let fail_next = fail_next.clone();
move || {
made += 1;
Ok(Wire {
connection: made,
written: written.clone(),
fail_next: fail_next.clone(),
})
}
});
(socket, written, fail_next)
}
#[test]
fn a_gesture_is_told_on_one_connection_and_the_next_has_its_own() {
let (mut socket, written, _) = wire();
let message = |gesture| SocketMessage::PanGesture(gesture);
let moved = PanGesture::Move {
dx: 1.0,
dy: 0.0,
t: 1.0,
};
socket.send(&message(PanGesture::Begin)).unwrap();
socket.send(&message(moved)).unwrap();
socket.send(&message(moved)).unwrap();
socket.send(&message(PanGesture::End)).unwrap();
socket.gesture_over();
socket.send(&message(PanGesture::Begin)).unwrap();
let connections: Vec<_> = written.borrow().iter().map(|(number, _)| *number).collect();
assert_eq!(connections, vec![1, 1, 1, 1, 2]);
}
#[test]
fn a_connection_that_komorebi_let_go_of_is_made_again_once() {
let (mut socket, written, fail_next) = wire();
let message = SocketMessage::PanGesture(PanGesture::Begin);
socket.send(&message).unwrap();
fail_next.set(true);
socket.send(&message).unwrap();
let connections: Vec<_> = written.borrow().iter().map(|(number, _)| *number).collect();
assert_eq!(
connections,
vec![1, 2],
"the second was written on a new one"
);
}
#[test]
fn a_message_that_cannot_be_written_twice_is_an_error() {
let mut socket = Socket::new(|| -> io::Result<Wire> {
Err(io::Error::new(io::ErrorKind::NotFound, "no komorebi"))
});
assert!(
socket
.send(&SocketMessage::PanGesture(PanGesture::Begin))
.is_err()
);
}
#[test]
fn what_is_told_ends_each_gesture_that_ends_and_the_stream_that_stops() {
let input = b"begin\nmove dx=1 t=1\nend\nbegin\nmove dx=1 t=2\n".to_vec();
let mut sink = Recorder::default();
run(io::Cursor::new(input), Options::default(), &mut sink).unwrap();
// the first ended, and the second was given up when the stream stopped
assert_eq!(sink.over, 2);
assert_eq!(gestures(&sink.told).last(), Some(&PanGesture::Cancel));
}
fn step(dx: f64) -> Message {
Message::Move {
dx,
dy: 0.0,
t: 0.0,
}
}
#[test]
fn what_whkd_writes_is_read_as_it_was_written() {
assert_eq!(
messages(TRANSCRIPT),
vec![
Message::Begin,
Message::Move {
dx: 0.42,
dy: -0.03,
t: 118_345.0
},
Message::Move {
dx: 0.51,
dy: -0.02,
t: 118_353.0
},
Message::Move {
dx: 0.47,
dy: 0.0,
t: 118_361.0
},
Message::End,
]
);
}
/// What whkd writes for a swipe of three fingers that goes on for about two seconds, a
/// flick, and one that eases to a stop: `whkd touchpad --demo <name> --instant`
const LONG: &str = include_str!("../fixtures/pan3-long.transcript.txt");
const FLICK: &str = include_str!("../fixtures/pan3-flick.transcript.txt");
const SLOW: &str = include_str!("../fixtures/pan3-slow.transcript.txt");
/// How many lines of a transcript are not part of the stream: whkd's demo says which swipe
/// it saw, as `swipe swipe3right`, among them, and nothing says that the program that it
/// runs is told it, so it is ignored like any other message that is not known
fn others(transcript: &str) -> usize {
transcript
.lines()
.filter(|line| line.starts_with("swipe "))
.count()
}
#[test]
fn every_line_that_whkd_writes_for_a_swipe_is_read_and_nothing_is_said_to_be_wrong() {
for (transcript, lines) in [(LONG, 218), (FLICK, 18), (SLOW, 67)] {
assert_eq!(transcript.lines().count(), lines);
for line in transcript.lines() {
match parse_line(line) {
Line::Message(_) => {}
Line::Ignored => assert!(line.starts_with("swipe "), "ignored: {line}"),
Line::Bad(why) => panic!("{why}: {line}"),
}
}
let messages = messages(transcript);
assert_eq!(messages.first(), Some(&Message::Begin));
assert_eq!(messages.last(), Some(&Message::End));
assert_eq!(messages.len(), lines - others(transcript));
}
assert_eq!(others(LONG) + others(FLICK) + others(SLOW), 2);
}
#[test]
fn a_swipe_is_told_to_komorebi_move_for_move() {
for transcript in [LONG, FLICK, SLOW] {
let mut forwarder = Forwarder::new(Options::default());
let told: Vec<_> = messages(transcript)
.into_iter()
.flat_map(|message| forwarder.handle(message))
.collect();
let gestures = gestures(&told);
assert_eq!(
gestures.len(),
transcript.lines().count() - others(transcript)
);
assert_eq!(gestures.first(), Some(&PanGesture::Begin));
assert_eq!(gestures.last(), Some(&PanGesture::End));
// the distances add up to how far the fingers went across the touchpad
let across: f64 = gestures
.iter()
.filter_map(|gesture| match gesture {
PanGesture::Move { dx, .. } => Some(*dx),
_ => None,
})
.sum();
assert!(across > 1.0, "{across}");
}
}
#[test]
fn a_field_that_is_not_known_is_not_anything_and_nor_is_a_message() {
// x, y, w, h, and a field of the future, which whkd says to ignore without a word
assert_eq!(
parse_line("move dx=1.5 dy=0.25 x=4.420 y=1.970 t=10 pressure=0.7"),
Line::Message(Message::Move {
dx: 1.5,
dy: 0.25,
t: 10.0
})
);
assert_eq!(
parse_line("begin fingers=3 x=4.000 y=2.000 w=10.500 h=7.200 t=118337"),
Line::Message(Message::Begin)
);
assert_eq!(
parse_line("end x=5.4 y=1.95 t=118369"),
Line::Message(Message::End)
);
assert_eq!(parse_line("pinch scale=1.1"), Line::Ignored);
assert_eq!(parse_line(""), Line::Ignored);
assert_eq!(parse_line(" "), Line::Ignored);
assert_eq!(parse_line("# a comment"), Line::Ignored);
}
#[test]
fn a_move_that_is_not_what_it_should_be_is_said_to_be() {
assert!(matches!(parse_line("move dy=1 t=3"), Line::Bad(_)), "no dx");
assert!(matches!(parse_line("move dx=1 dy=1"), Line::Bad(_)), "no t");
assert!(matches!(parse_line("move dx=wide t=3"), Line::Bad(_)));
assert!(matches!(parse_line("move dx=NaN t=3"), Line::Bad(_)));
assert_eq!(
parse_line("move dx=2 t=3"),
Line::Message(Message::Move {
dx: 2.0,
dy: 0.0,
t: 3.0
}),
"dy is not needed"
);
}
#[test]
fn the_strip_goes_with_the_fingers_and_the_times_are_told_as_they_are() {
let mut forwarder = Forwarder::new(Options::default());
let told: Vec<_> = messages(TRANSCRIPT)
.into_iter()
.flat_map(|message| forwarder.handle(message))
.collect();
let gestures = gestures(&told);
assert_eq!(gestures.len(), 5);
assert_eq!(gestures[0], PanGesture::Begin);
assert_eq!(
gestures[1],
PanGesture::Move {
dx: 0.42,
dy: -0.03,
t: 118_345.0
}
);
assert_eq!(gestures[4], PanGesture::End);
assert!(
forwarder.finish().is_empty(),
"it ended, so nothing is left"
);
}
#[test]
fn sensitivity_scales_the_distances_and_reversing_flips_the_way_it_goes() {
let mut forwarder = Forwarder::new(Options {
sensitivity: 2.0,
reversed: true,
paging: false,
});
forwarder.handle(Message::Begin);
let told = forwarder.handle(Message::Move {
dx: 1.0,
dy: 0.5,
t: 1.0,
});
assert_eq!(
gestures(&told),
vec![PanGesture::Move {
dx: -2.0,
dy: 1.0,
t: 1.0
}]
);
}
#[test]
fn what_has_no_beginning_is_not_anything() {
let mut forwarder = Forwarder::new(Options::default());
assert!(forwarder.handle(step(1.0)).is_empty());
assert!(forwarder.handle(Message::End).is_empty());
assert!(forwarder.handle(Message::Cancel).is_empty());
assert!(forwarder.finish().is_empty());
}
#[test]
fn a_stream_that_stops_in_the_middle_of_a_gesture_gives_it_up() {
let mut forwarder = Forwarder::new(Options::default());
forwarder.handle(Message::Begin);
forwarder.handle(step(1.0));
assert_eq!(gestures(&forwarder.finish()), vec![PanGesture::Cancel]);
assert!(forwarder.finish().is_empty(), "once");
}
#[test]
fn fingers_that_go_far_enough_take_a_step_each_with_paging() {
let mut forwarder = Forwarder::new(Options {
paging: true,
..Options::default()
});
assert!(forwarder.handle(Message::Begin).is_empty());
assert!(forwarder.handle(step(1.5)).is_empty(), "not far enough");
// 1.5 and 0.7 is 2.2: a step to the left, as the strip goes right with the fingers
let told = forwarder.handle(step(0.7));
assert!(matches!(
told.as_slice(),
[SocketMessage::Pan(PanDirection::Left)]
));
// 0.2 is left over, and with 4.0 more it is two steps
let told = forwarder.handle(step(4.0));
assert_eq!(told.len(), 2);
// and the other way: 0.2 and 8.5 back is 8.3, which is four
let told = forwarder.handle(step(-8.5));
assert_eq!(told.len(), 4);
assert!(
told.iter()
.all(|message| matches!(message, SocketMessage::Pan(PanDirection::Right)))
);
assert!(forwarder.handle(Message::End).is_empty());
assert!(forwarder.finish().is_empty());
}
#[test]
fn moves_that_wait_are_added_up_and_nothing_else_is_put_out_of_its_place() {
let mut queue: VecDeque<Message> = [
Message::Begin,
Message::Move {
dx: 1.0,
dy: 0.5,
t: 1.0,
},
Message::Move {
dx: 2.0,
dy: 0.25,
t: 2.0,
},
Message::Move {
dx: 4.0,
dy: 0.0,
t: 3.0,
},
Message::End,
]
.into();
assert_eq!(next_merged(&mut queue), Some(Message::Begin));
assert_eq!(
next_merged(&mut queue),
Some(Message::Move {
dx: 7.0,
dy: 0.75,
t: 3.0
})
);
assert_eq!(next_merged(&mut queue), Some(Message::End));
assert_eq!(next_merged(&mut queue), None);
}
#[test]
fn a_stream_is_told_to_komorebi_in_order_and_given_up_if_it_stops() {
let input = b"begin fingers=3\nmove dx=1.0 dy=0 t=1\nmove dx=2.0 dy=0 t=2\n".to_vec();
let mut sink = Recorder::default();
run(io::Cursor::new(input), Options::default(), &mut sink).unwrap();
let gestures = gestures(&sink.told);
assert_eq!(gestures[0], PanGesture::Begin);
assert!(matches!(gestures[1], PanGesture::Move { .. }));
assert_eq!(gestures.last(), Some(&PanGesture::Cancel));
}
}