feat(komorebic): pan the Scrolling layout with the fingers on a touchpad

komorebic pan --stream reads the gestures of fingers on a touchpad from standard input, as
whkd writes them for a swipe of three or four fingers (docs/gesture-stream.md in its
repository): a line for each begin, move, end and cancel, with the distances in centimetres,
and any field or message that it does not know ignored. The row follows the fingers while they
are down, and when they lift it comes to rest with a column against an edge of the screen,
going at the speed that the times of the moves say that the fingers were. Focus goes to the
nearest column in view if the focused one is not. A gesture that is given up, or that is
not finished when the stream stops, puts the row back to what it was.

komorebic works out nothing but what it is asked to: --sensitivity multiplies the distances,
--reversed flips the way that the row goes, and --paging takes a step for every two
centimetres that the fingers go, as komorebic pan does, and not move the row with them. Moves
that wait while one is told are added up, as whkd says that they may be.

A gesture is told to komorebi on one connection, which it handles in order, as messages
that are sent on a connection each are not. With ghost movement the pictures that cover the
windows near the screen are taken when the fingers go down, a move of the fingers sets them
where the row puts the windows and does nothing else, and when the fingers lift they glide to
where it comes to rest, over the time that an animation takes, and the windows are shown. Without
it the workspace is laid out a step at a time, as it is for a drag. Ten centimetres of the
fingers move the row the width of the screen. The golden transcript of whkd and the swipes
of its demo are fixtures of the tests that read the stream.
This commit is contained in:
LGUG2Z committed 2026-10-30 15:45:37 -07:00
1 parent a63057c232
commit a65b5c231e
15 files changed
+2001 -11

No files matched your search

+15 -3
View File
@@ -1,15 +1,27 @@
```
Move the view of the Scrolling layout a column to the left or the right, which does nothing in any other layout
Move the view of the Scrolling layout a column to the left or the right, or with the fingers on a touchpad, which does nothing in any other layout
Usage: pan <DIRECTION>
Usage: pan [OPTIONS] [DIRECTION]
Arguments:
<DIRECTION>
[DIRECTION]
The way that the view goes
[possible values: left, right]
Options:
--stream
Read the gestures of fingers on a touchpad from standard input, as whkd writes them for a swipe of three or four fingers, and pan with them
--sensitivity <SENSITIVITY>
What the distances of a gesture are multiplied by, which is 1 if it is left out, with --stream
--reversed
Go the other way to the fingers, with --stream
--paging
Take a step for every two centimetres that the fingers go, and not move the view with them, with --stream
-h, --help
Print help
+27
View File
@@ -146,6 +146,33 @@ alt + wheeldown : komorebic pan right
It does nothing in the other layouts.
### With the fingers on a touchpad
`komorebic pan --stream` reads the gestures of fingers on a touchpad from standard input and moves the view with them: the
row follows the fingers while they are down, and comes to rest with a column against an edge of the screen when they lift,
at the speed that they were going. [whkd](https://github.com/LGUG2Z/whkd) writes that stream for a swipe of three or
four fingers:
```
swipe3 : komorebic pan --stream
```
Windows does not give three-finger gestures to programs, so whkd reads the touchpad itself, and Windows Settings should be
set to do nothing for the same gesture.
- A swipe of 10 centimetres across the touchpad moves the row the width of the screen. `--sensitivity` multiplies that, and
`--reversed` makes the row go the other way to the fingers.
- With `--paging` the row does not follow the fingers: it takes a step, as `komorebic pan` does, for every two
centimetres that they go.
- A gesture that is given up, or whose stream stops, puts the row back to where it was. Vertical movement is not used.
- A swipe with a direction, `swipe3left : komorebic pan left`, runs once when the fingers have gone far enough. It does not
stop a `swipe3` stream on the same fingers: with both bound, the row follows the fingers and the step is taken as well, so
bind one or the other.
- It does nothing in the other layouts.
The format of the stream is whkd's, and is written down in `docs/gesture-stream.md` in its repository: a line for each
`begin`, `move`, `end` and `cancel`, with the distances in centimetres. Fields that are not known are ignored.
## Resizing
[`komorebic resize-edge`](../cli/resize-edge.md) and [`komorebic resize-axis`](../cli/resize-axis.md) change the width of
+26
View File
@@ -62,6 +62,7 @@ pub use komorebi::core::MoveBehaviour;
pub use komorebi::core::OperationBehaviour;
pub use komorebi::core::OperationDirection;
pub use komorebi::core::PanDirection;
pub use komorebi::core::PanGesture;
pub use komorebi::core::PathExt;
pub use komorebi::core::Rect;
pub use komorebi::core::Sizing;
@@ -105,6 +106,31 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
/// A connection to komorebi that messages are written to one after the other, which komorebi
/// handles in the order that they were written, as it does the lines of a batch. Messages sent
/// with [`send_message`] are each on a connection of their own, which komorebi handles on a
/// thread of its own, so two that are sent together can be handled in either order.
pub struct Connection {
stream: UnixStream,
}
impl Connection {
pub fn open() -> std::io::Result<Self> {
let socket = DATA_DIR.join(KOMOREBI);
let stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
Ok(Self { stream })
}
pub fn send(&mut self, message: &SocketMessage) -> std::io::Result<()> {
let mut line = serde_json::to_string(message)?;
line.push('\n');
self.stream.write_all(line.as_bytes())
}
}
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
where
Q: Borrow<SocketMessage>,
+16
View File
@@ -27,6 +27,22 @@ impl PanDirection {
}
}
/// What the fingers of a gesture that pans the strip do, as far as the strip is concerned
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PanGesture {
/// The fingers have landed
Begin,
/// The fingers moved by `dx` and `dy` centimetres on the touchpad since the last time, to
/// the right and down if they are positive, at `t` milliseconds on a clock that only the
/// differences between the times of a gesture mean anything on
Move { dx: f64, dy: f64, t: f64 },
/// The fingers lifted
End,
/// The gesture was given up: the strip goes back to where it was
Cancel,
}
#[cfg(test)]
mod tests {
use super::*;
+2
View File
@@ -39,6 +39,7 @@ pub use komorebi_layouts::MAX_RATIOS;
pub use komorebi_layouts::MIN_RATIO;
pub use komorebi_layouts::OperationDirection;
pub use komorebi_layouts::PanDirection;
pub use komorebi_layouts::PanGesture;
pub use komorebi_layouts::Rect;
pub use komorebi_layouts::ScrollingLayoutOptions;
pub use komorebi_layouts::Sizing;
@@ -134,6 +135,7 @@ pub enum SocketMessage {
Expel,
ToggleColumnStack,
Pan(PanDirection),
PanGesture(PanGesture),
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
FlipLayout(Axis),
ToggleWorkspaceWindowContainerBehaviour,
+267 -2
View File
@@ -14,13 +14,16 @@
//! it began puts the neighbours back.
use crate::WindowsApi;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::GHOST_MOVEMENT_ENABLED;
use crate::animation::ghost::GhostWindow;
use crate::animation::ghost::is_swap_chain_window;
use crate::animation::ghost::nudge_swap_chain_window;
use crate::animation::lerp::Lerp;
use crate::animation::snapshot;
use crate::border_manager;
use crate::com::SetCloak;
use crate::core::AnimationStyle;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::core::Sizing;
@@ -58,7 +61,8 @@ impl Snapping {
/// `paced` is for the steps of a drag, which leave a window that has not
/// got to where it was last sent for the next step: the end of a drag puts
/// everything where it is to be
fn new(paced: bool) -> Self {
#[must_use]
pub fn new(paced: bool) -> Self {
PACED.store(paced, Ordering::SeqCst);
STEPPING.store(true, Ordering::SeqCst);
Self { paced }
@@ -217,6 +221,9 @@ struct Cover {
ghost: GhostWindow,
/// Where the picture is
at: Rect,
/// Where the picture is when the strip has not been moved, which is not where the window is
/// if it is parked at the edge of the screen
origin: Rect,
/// Where the window is to go, which is where the last step put the picture
target: Rect,
}
@@ -232,6 +239,15 @@ pub fn is_covered(hwnd: isize) -> bool {
.is_some_and(|covers| covers.contains_key(&hwnd))
}
/// A workspace is about to be laid out a step at a time by something that is not a drag: the
/// pictures of the last of them are taken away, and the windows that were sent somewhere
/// are forgotten.
pub fn begin_steps() {
GENERATION.fetch_add(1, Ordering::SeqCst);
uncover(false);
SENT.lock().take();
}
/// A window that is to be resized for a step of a drag, from `from` to `to`, and
/// whether it is not to be sent there: it is covered.
///
@@ -272,6 +288,7 @@ pub fn follow(hwnd: isize, from: &Rect, to: &Rect) -> bool {
vacant.insert(Cover {
ghost,
at: *from,
origin: *from,
target: *to,
});
}
@@ -287,6 +304,12 @@ pub fn follow(hwnd: isize, from: &Rect, to: &Rect) -> bool {
};
cover.target = *to;
// where a glide is to end is all that it is told: it is the glide that moves the picture
if GLIDING.load(Ordering::SeqCst) {
return true;
}
match cover.ghost.update(*to, 255) {
Ok(()) => {
cover.at = *to;
@@ -310,6 +333,181 @@ pub fn follow(hwnd: isize, from: &Rect, to: &Rect) -> bool {
true
}
/// Whether the covers are to be told where they go and not moved there, as they are moved to
/// it by [`glide`]
static GLIDING: AtomicBool = AtomicBool::new(false);
/// Covers are told where they go, and not moved there, while this is held
pub struct Gliding;
impl Gliding {
pub fn begin() -> Self {
GLIDING.store(true, Ordering::SeqCst);
Self
}
}
impl Drop for Gliding {
fn drop(&mut self) {
GLIDING.store(false, Ordering::SeqCst);
}
}
/// Where a picture is `progress` of the way (0 to 1) from where it was to where it goes: it
/// slows as it gets there, as the windows of the animations do
fn glide_frame(from: &Rect, to: &Rect, progress: f64) -> Rect {
from.lerp(*to, progress.clamp(0.0, 1.0), AnimationStyle::EaseOutCubic)
}
/// Move the pictures of the covers from where they are to where their windows are to go, as
/// long as an animation takes, and nothing else is done until they are there. For the windows
/// of a strip that is let go of, which then does not jump to where it comes to rest.
pub fn glide() {
let duration = Duration::from_millis(ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst));
let mut covers = COVERS.lock();
let Some(covers) = covers.as_mut() else {
return;
};
let moving: Vec<_> = covers
.iter()
.filter(|(_, cover)| cover.at != cover.target)
.map(|(hwnd, cover)| (*hwnd, cover.at, cover.target))
.collect();
if moving.is_empty() || duration.is_zero() {
return;
}
let started = Instant::now();
loop {
let progress = started.elapsed().as_secs_f64() / duration.as_secs_f64();
for (hwnd, from, to) in &moving {
let rect = glide_frame(from, to, progress.min(1.0));
if let Some(cover) = covers.get_mut(hwnd)
&& cover.ghost.update(rect, 255).is_ok()
{
cover.at = rect;
border_manager::animate_to(*hwnd, rect);
}
}
if progress >= 1.0 {
break;
}
// one frame of the screen at a time, as the animations are
unsafe {
let _ = windows::Win32::Graphics::Dwm::DwmFlush();
}
}
}
/// Whether a window at `rect` is near enough to a work area to be seen soon: within a width
/// of it
pub fn is_near(rect: &Rect, work_area: &Rect) -> bool {
// a Rect has a width and a height where it would have a right and a bottom
let width = work_area.right;
rect.left + rect.right > work_area.left - width
&& rect.left < work_area.left + work_area.right + width
}
/// Cover the windows that are given with where their pictures are to be, which is not where
/// they are if they are parked at the edge of the screen, all of them at once, so that the
/// first step of something that moves them does not wait for a picture of each to be taken
/// one after the other. A window that is covered already is left as it is. Only with ghost
/// movement.
pub fn cover_all(windows: &[(isize, Rect)]) {
if !GHOST_MOVEMENT_ENABLED.load(Ordering::Relaxed) {
return;
}
let wanted: Vec<_> = windows
.iter()
.copied()
.filter(|(hwnd, _)| !is_covered(*hwnd))
.filter(|(hwnd, _)| {
!crate::HIDDEN_HWNDS.lock().contains(hwnd)
&& !WindowsApi::is_window_cloaked(*hwnd).unwrap_or(false)
})
.collect();
let made: Vec<_> = wanted
.into_iter()
.map(|(hwnd, origin)| {
std::thread::spawn(move || {
let rect = WindowsApi::window_rect(hwnd).ok()?;
// the picture is taken before the window is touched
match GhostWindow::create(hwnd, origin, None) {
Ok(ghost) => Some((hwnd, rect, origin, ghost)),
Err(error) => {
tracing::debug!("cover: no cover for hwnd {hwnd}: {error}");
None
}
}
})
})
.collect();
let mut covers = COVERS.lock();
let covers = covers.get_or_insert_with(HashMap::new);
for (hwnd, rect, origin, ghost) in made
.into_iter()
.filter_map(|made| made.join().ok().flatten())
{
border_manager::set_ghosted(hwnd, true);
SetCloak(Window { hwnd }.hwnd(), 1, 2);
covers.insert(
hwnd,
Cover {
ghost,
at: origin,
origin,
target: rect,
},
);
}
}
/// `rect` moved `shift` pixels to the right, or to the left if it is negative
fn shifted(rect: &Rect, shift: f64) -> Rect {
#[allow(clippy::cast_possible_truncation)]
let by = shift.round() as i32;
Rect {
left: rect.left + by,
..*rect
}
}
/// Put the pictures of the covers where they are when the strip is `shift` pixels from where
/// it was when they were taken, which is all there is to a step of a pan: the windows are not
/// touched and nothing is laid out. Whether there were any.
pub fn shift_covers(shift: f64) -> bool {
let mut covers = COVERS.lock();
let Some(covers) = covers.as_mut().filter(|covers| !covers.is_empty()) else {
return false;
};
for (hwnd, cover) in covers.iter_mut() {
let rect = shifted(&cover.origin, shift);
if cover.ghost.update(rect, 255).is_ok() {
cover.at = rect;
border_manager::animate_to(*hwnd, rect);
}
}
true
}
/// Take the covers away. After a drag `show` is true: each window is put where it is
/// to go, made to lay itself out there and to build its swap chain again, all of it
/// hidden, and then shown, and the picture fades out over it. For another drag, which
@@ -342,7 +540,9 @@ pub fn uncover(show: bool) {
/// Put a window that was covered where it is to go, show it, and fade its picture out
fn reveal(hwnd: isize, cover: Cover) {
let Cover { ghost, at, target } = cover;
let Cover {
ghost, at, target, ..
} = cover;
if WindowsApi::is_window(hwnd) {
if WindowsApi::position_window(hwnd, &target, false, false).is_ok() {
@@ -852,6 +1052,71 @@ impl WindowManager {
#[cfg(test)]
mod tests {
#[test]
fn a_picture_is_shifted_along_the_screen_and_nowhere_else() {
let rect = Rect {
left: 100,
top: 20,
right: 400,
bottom: 300,
};
assert_eq!(super::shifted(&rect, 0.0), rect);
assert_eq!(super::shifted(&rect, 50.4).left, 150);
assert_eq!(super::shifted(&rect, -250.6).left, -151);
assert_eq!(super::shifted(&rect, -250.6).top, 20);
assert_eq!(super::shifted(&rect, 9.0).right, 400);
}
#[test]
fn a_picture_glides_from_where_it_was_to_where_it_goes_and_slows_as_it_gets_there() {
let from = Rect {
left: 0,
top: 0,
right: 400,
bottom: 300,
};
let to = Rect {
left: 1000,
top: 100,
right: 400,
bottom: 300,
};
assert_eq!(super::glide_frame(&from, &to, 0.0), from);
assert_eq!(super::glide_frame(&from, &to, 1.0), to);
assert_eq!(super::glide_frame(&from, &to, 7.0), to, "no further");
assert_eq!(super::glide_frame(&from, &to, -1.0), from);
// more than half of the way in the first half of the time
assert!(super::glide_frame(&from, &to, 0.5).left > 500);
// and the size is not changed on the way when it is not to be
assert_eq!(super::glide_frame(&from, &to, 0.3).right, 400);
}
#[test]
fn a_window_is_near_a_work_area_within_a_width_of_it() {
let area = Rect {
left: 0,
top: 0,
right: 1000,
bottom: 800,
};
let at = |left: i32, right: i32| Rect {
left,
top: 0,
right: right - left,
bottom: 800,
};
assert!(super::is_near(&at(0, 1000), &area));
assert!(super::is_near(&at(900, 1900), &area), "just in view");
assert!(super::is_near(&at(1900, 2900), &area), "a width away");
assert!(!super::is_near(&at(2100, 3100), &area), "two widths away");
assert!(super::is_near(&at(-1900, -900), &area));
assert!(!super::is_near(&at(-3100, -2100), &area));
}
use super::*;
fn rect(left: i32, top: i32, width: i32, height: i32) -> Rect {
+407 -1
View File
@@ -1,9 +1,50 @@
//! Moving the strip of the Scrolling layout across the screen without changing which window has
//! focus, by a step, which is what this has been asked for so far.
//! focus: by a step, and with the fingers on a touchpad.
//!
//! A gesture is a begin, any number of moves, and an end, or a cancel. The strip follows the
//! fingers for as long as they are down, and the workspace is laid out a step at a time as the
//! windows of a drag are, with the pictures that cover them, and when the fingers lift it
//! comes to rest with a column against an edge of the screen.
use crate::core::PanDirection;
use crate::core::PanGesture;
use crate::core::Rect;
use crate::live_resize;
use crate::live_resize::Snapping;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use color_eyre::eyre;
use komorebi_scrolling::ScrollingSpace;
use komorebi_scrolling::SwipeTracker;
use parking_lot::Mutex;
use std::time::Instant;
/// How far the fingers go across a touchpad, in centimetres, to move the strip as far as the
/// width of the screen
const CM_PER_SCREEN: f64 = 10.0;
/// A gesture that is going on
struct Pan {
monitor_idx: usize,
workspace_idx: usize,
/// What the strip was before the gesture, for putting it back if it is given up
saved: ScrollingSpace<String>,
/// How fast the fingers are going
tracker: SwipeTracker,
/// How far into the strip the screen was when it began
offset: f64,
/// When it began, and the time of the touchpad at the first move, for the log
began: Instant,
first_t: Option<f64>,
}
static PAN: Mutex<Option<Pan>> = Mutex::new(None);
/// How far the strip is moved on a screen that is `screen_width` pixels wide by fingers that
/// move `cm` centimetres, in pixels, to the right if it is positive
fn distance_on_screen(cm: f64, screen_width: i32) -> f64 {
cm * f64::from(screen_width) / CM_PER_SCREEN
}
impl WindowManager {
/// Move the view of the focused workspace one step, a column, to the left or the right, and
@@ -27,4 +68,369 @@ impl WindowManager {
self.update_focused_workspace(focus_moved && self.mouse_follows_focus, focus_moved)
}
/// What the fingers of a gesture do
pub fn pan_gesture(&mut self, gesture: PanGesture) -> eyre::Result<()> {
match gesture {
PanGesture::Begin => self.pan_begin(),
PanGesture::Move { dx, t, .. } => self.pan_move(dx, t),
PanGesture::End => self.pan_end(),
PanGesture::Cancel => self.pan_cancel(),
}
}
fn pan_begin(&mut self) -> eyre::Result<()> {
// a gesture that was not finished is given up, as a new one begins
self.pan_cancel()?;
let monitor_idx = self.focused_monitor_idx();
let workspace_idx = self.focused_workspace_idx()?;
let Some(saved) = self.focused_workspace_mut()?.begin_scrolling_pan() else {
return Ok(());
};
live_resize::begin_steps();
// the pictures that the strip is moved by are taken now, all at once, and not as the
// first move of the fingers asks for them, one after the other
let work_area = self.focused_monitor_work_area()?;
let workspace = self.focused_workspace()?;
let near: Vec<_> = origins(&workspace.scrolling_lefts(), |hwnd| {
WindowsApi::window_rect(hwnd).ok()
})
.into_iter()
.filter(|(_, origin)| live_resize::is_near(origin, &work_area))
.collect();
let offset = workspace.scrolling_view_offset().unwrap_or_default();
live_resize::cover_all(&near);
*PAN.lock() = Some(Pan {
monitor_idx,
workspace_idx,
saved,
tracker: SwipeTracker::new(),
offset,
began: Instant::now(),
first_t: None,
});
Ok(())
}
fn pan_move(&mut self, dx_cm: f64, t_ms: f64) -> eyre::Result<()> {
let mut guard = PAN.lock();
let Some(pan) = guard.as_mut() else {
return Ok(());
};
// the workspace that the fingers are on is not the one that is on show any more
if self.focused_monitor_idx() != pan.monitor_idx
|| self.focused_workspace_idx()? != pan.workspace_idx
{
drop(guard);
return self.pan_cancel();
}
let workspace = self.focused_workspace_mut()?;
let Some(width) = workspace.scrolling_view_width() else {
return Ok(());
};
let dx = distance_on_screen(dx_cm, width);
workspace.pan_scrolling(dx);
pan.tracker.push(t_ms / 1000.0, dx);
// how far behind the fingers a move is handled is how far the wall clock has got ahead
// of the touchpad's since the first
let first = *pan.first_t.get_or_insert(t_ms);
let fingers = t_ms - first;
let wall = pan.began.elapsed().as_secs_f64() * 1000.0;
tracing::trace!(
"pan: move of {dx:.1} px at {fingers:.0} ms of the fingers, handled at {wall:.0} ms ({:+.0} ms)",
wall - fingers
);
let offset = workspace.scrolling_view_offset().unwrap_or(pan.offset);
let shift = pan.offset - offset;
drop(guard);
// the windows are under their pictures, and it is the pictures that go with the fingers
if live_resize::shift_covers(shift) {
return Ok(());
}
// without them the workspace is laid out a step at a time
let _snapping = Snapping::new(true);
self.update_focused_workspace(false, false)
}
fn pan_end(&mut self) -> eyre::Result<()> {
let Some(pan) = PAN.lock().take() else {
return Ok(());
};
if self.focused_monitor_idx() != pan.monitor_idx
|| self.focused_workspace_idx()? != pan.workspace_idx
{
return Ok(());
}
let focus_moved = self
.focused_workspace_mut()?
.end_scrolling_pan(pan.tracker.velocity());
// the end of it is laid out at once, and the windows that were covered are shown when
// this is dropped, but the pictures of them are not sent to where they go: they glide
// there, from where the fingers left them, as the windows of an animation do
let _snapping = Snapping::new(false);
{
let _gliding = live_resize::Gliding::begin();
self.update_focused_workspace(focus_moved && self.mouse_follows_focus, focus_moved)?;
}
live_resize::glide();
Ok(())
}
fn pan_cancel(&mut self) -> eyre::Result<()> {
let Some(pan) = PAN.lock().take() else {
return Ok(());
};
if self.focused_monitor_idx() != pan.monitor_idx
|| self.focused_workspace_idx()? != pan.workspace_idx
{
return Ok(());
}
self.focused_workspace_mut()?
.cancel_scrolling_pan(pan.saved);
let _snapping = Snapping::new(false);
{
let _gliding = live_resize::Gliding::begin();
self.update_focused_workspace(false, false)?;
}
live_resize::glide();
Ok(())
}
}
/// Where the pictures of the windows go when the strip has not been moved: each window's own
/// rectangle, with its left edge where the strip puts it. The strip does not say where the
/// screen is, so the windows that are where it puts them (the ones on the screen) say that, and
/// the rest, which are parked at an edge, are put where they would be.
///
/// `lefts` is each window with how far its left edge is from that of the screen, and whether
/// it is on the screen, and `rect_of` is where a window is. Nothing is covered if no window is
/// where it is put.
fn origins(
lefts: &[(isize, i32, bool)],
rect_of: impl Fn(isize) -> Option<Rect>,
) -> Vec<(isize, Rect)> {
let Some(offset) = lefts
.iter()
.filter(|(_, _, on_screen)| *on_screen)
.find_map(|(hwnd, left, _)| Some(rect_of(*hwnd)?.left - left))
else {
return vec![];
};
lefts
.iter()
.filter_map(|(hwnd, left, _)| {
let rect = rect_of(*hwnd)?;
Some((
*hwnd,
Rect {
left: left + offset,
..rect
},
))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::SocketMessage;
#[test]
fn pictures_go_where_the_strip_puts_the_windows_and_parked_windows_are_not_where_they_are() {
let at = |left| Rect {
left,
top: 30,
right: 400,
bottom: 300,
};
// the strip: one window past the left edge (parked, so not where it is put), two on the
// screen, which starts at 10, and one past the right edge
let lefts = [
(1, -300, false),
(2, 50, true),
(3, 450, true),
(4, 900, false),
];
let rects = |hwnd| match hwnd {
1 => Some(at(-385)),
2 => Some(at(60)),
3 => Some(at(460)),
4 => Some(at(1205)),
_ => None,
};
let origins = origins(&lefts, rects);
let lefts_of: Vec<_> = origins
.iter()
.map(|(hwnd, rect)| (*hwnd, rect.left))
.collect();
assert_eq!(lefts_of, vec![(1, -290), (2, 60), (3, 460), (4, 910)]);
// and nothing else of where they are changes
assert!(
origins
.iter()
.all(|(_, rect)| rect.top == 30 && rect.right == 400)
);
}
#[test]
fn nothing_is_covered_when_no_window_is_where_it_is_put() {
let lefts = [(1, -300, false), (2, 900, false)];
assert!(origins(&lefts, |_| Some(Rect::default())).is_empty());
// or when the windows cannot be found
assert!(origins(&[(1, 0, true)], |_| None).is_empty());
}
#[test]
fn fingers_that_go_across_a_touchpad_move_the_strip_across_the_screen() {
assert!((distance_on_screen(10.0, 5000) - 5000.0).abs() < 1e-9);
assert!((distance_on_screen(1.0, 5000) - 500.0).abs() < 1e-9);
assert!((distance_on_screen(-0.5, 1200) + 60.0).abs() < 1e-9);
assert!(distance_on_screen(0.0, 1200).abs() < f64::EPSILON);
}
/// The moves of a swipe of about two seconds at about 125 times a second, with a time that
/// is a millisecond or two out for each, as a touchpad gives them, the fingers going at
/// `lift` centimetres a second for the last 200 milliseconds and at `earlier` before that
/// (eased in from rest). Each is how far they went since the one before, and when.
fn swipe(earlier: f64, lift: f64) -> Vec<(f64, f64)> {
let mut moves = Vec::new();
let mut t = 118_000.0;
for step in 1..=250 {
let jitter = f64::from((step * 37) % 7 - 3) * 0.5;
let next = 118_000.0 + f64::from(step) * 8.0 + jitter;
let dt = (next - t) / 1000.0;
let elapsed = f64::from(step) * 8.0;
let speed = if elapsed > 1800.0 {
lift
} else {
earlier * (elapsed / 1800.0)
};
moves.push((speed * dt, next));
t = next;
}
moves
}
/// How fast the strip is going when the fingers lift, in centimetres of the fingers a
/// second, for these moves on a screen that is 5000 pixels wide
fn lift_speed(moves: &[(f64, f64)]) -> f64 {
let mut tracker = SwipeTracker::new();
for (cm, t) in moves {
tracker.push(t / 1000.0, distance_on_screen(*cm, 5000));
}
// a centimetre of the fingers is 500 pixels on that screen
tracker.velocity() / 500.0
}
#[test]
fn the_speed_that_the_fingers_lift_at_is_what_the_strip_is_let_go_at() {
// fast, with the time of every move a little out
let speed = lift_speed(&swipe(8.0, 16.0));
assert!((speed - 16.0).abs() < 0.8, "16 cm/s, got {speed:.2}");
// a flick
let speed = lift_speed(&swipe(20.0, 33.0));
assert!((speed - 33.0).abs() < 1.7, "33 cm/s, got {speed:.2}");
// one that eased to a stop and lifts almost still
let speed = lift_speed(&swipe(8.0, 0.2));
assert!(speed < 0.5, "almost still, got {speed:.2}");
}
/// The moves of what whkd writes for a swipe, which is what komorebic tells it of them: how
/// far the fingers went, in centimetres, and when
fn moves_of(transcript: &str) -> Vec<(f64, f64)> {
let field = |line: &str, key: &str| -> Option<f64> {
line.split_whitespace()
.find_map(|word| word.strip_prefix(key)?.strip_prefix('='))
.and_then(|value| value.parse().ok())
};
transcript
.lines()
.filter(|line| line.starts_with("move "))
.filter_map(|line| Some((field(line, "dx")?, field(line, "t")?)))
.collect()
}
#[test]
fn the_swipes_that_whkd_writes_lift_at_the_speed_that_they_were_made_to() {
let long = lift_speed(&moves_of(include_str!(
"../../komorebic/fixtures/pan3-long.transcript.txt"
)));
let flick = lift_speed(&moves_of(include_str!(
"../../komorebic/fixtures/pan3-flick.transcript.txt"
)));
let slow = lift_speed(&moves_of(include_str!(
"../../komorebic/fixtures/pan3-slow.transcript.txt"
)));
// 0.128 cm every 8 ms, and 0.267 every 8
assert!((long - 16.0).abs() < 0.8, "16 cm/s, got {long:.2}");
assert!((flick - 33.0).abs() < 1.7, "33 cm/s, got {flick:.2}");
// slowing from 1.9 cm/s to 0.75 over the last 150 ms, which is what the speed is the
// average of: a nudge, and not a throw
assert!(slow > 1.0 && slow < 2.5, "about 1.6 cm/s, got {slow:.2}");
assert!(flick > long && long > 5.0 * slow);
}
#[test]
fn what_has_hardly_moved_is_not_going_anywhere() {
assert!(lift_speed(&[]).abs() < f64::EPSILON);
assert!(lift_speed(&[(1.0, 100.0)]).abs() < f64::EPSILON);
// two moves at the same time have no speed
assert!(lift_speed(&[(1.0, 100.0), (1.0, 100.0)]).abs() < f64::EPSILON);
}
#[test]
fn a_gesture_goes_over_the_socket_as_it_is() {
for gesture in [
PanGesture::Begin,
PanGesture::Move {
dx: 0.42,
dy: -0.03,
t: 118_345.0,
},
PanGesture::End,
PanGesture::Cancel,
] {
let message = SocketMessage::PanGesture(gesture);
let json = serde_json::to_string(&message).unwrap();
let SocketMessage::PanGesture(back) = serde_json::from_str(&json).unwrap() else {
panic!("not a gesture: {json}");
};
assert_eq!(back, gesture, "{json}");
}
}
}
+10
View File
@@ -930,6 +930,7 @@ impl WindowManager {
self.update_focused_workspace(false, false)?;
}
SocketMessage::Pan(direction) => self.pan(direction)?,
SocketMessage::PanGesture(gesture) => self.pan_gesture(gesture)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
SocketMessage::LayoutRatios(ref columns, ref rows) => {
@@ -2267,6 +2268,15 @@ pub fn read_commands_uds(
for line in reader.lines() {
let message = SocketMessage::from_str(&line?)?;
// the fingers of a gesture can be still for longer than a command is waited for, and
// what they do next is told on this connection: it is closed when they have lifted
if matches!(
message,
SocketMessage::PanGesture(crate::core::PanGesture::Begin)
) {
let _ = stream.set_read_timeout(Some(Duration::from_secs(30)));
}
match wm.try_lock_for(Duration::from_secs(1)) {
None => {
tracing::warn!(
+65
View File
@@ -764,6 +764,47 @@ impl Workspace {
}
}
/// Give up a pan: the strip is put back to what it was when it began.
pub(crate) fn cancel_scrolling_pan(&mut self, saved: ScrollingSpace<String>) {
if self.scrolling.is_some() {
self.scrolling = Some(saved);
}
}
/// How far into the strip the left edge of the screen is, in pixels. A strip that is moved
/// by `dx` has this smaller by `dx`.
pub(crate) fn scrolling_view_offset(&self) -> Option<f64> {
self.scrolling.as_ref().map(ScrollingSpace::view_offset)
}
/// Where the strip puts the window that shows in each container, as how far its left edge
/// is from the left edge of the screen, and whether it is on the screen there and so
/// where it really is: windows that are not are parked at an edge.
pub(crate) fn scrolling_lefts(&self) -> Vec<(isize, i32, bool)> {
let Some(space) = self.scrolling.as_ref() else {
return vec![];
};
self.containers()
.iter()
.zip(space.layout())
.filter_map(|(container, placement)| {
let hwnd = container.focused_window()?.hwnd;
let on_screen = !matches!(
placement.visibility,
Visibility::OffLeft | Visibility::OffRight
);
Some((hwnd, placement.rect.x, on_screen))
})
.collect()
}
/// How wide the screen is that the strip is moved across, in pixels.
pub(crate) fn scrolling_view_width(&self) -> Option<i32> {
self.scrolling.as_ref().map(|space| space.viewport().width)
}
/// How far one step of a pan goes, if nothing says: a column of the width that a new one
/// is given.
pub(crate) fn scrolling_pan_step(&self) -> Option<i32> {
@@ -1173,4 +1214,28 @@ mod tests {
workspace.layout_scrolling(&AREA, 0);
assert_eq!(workspace.scrolling_pan_step(), Some(300));
}
#[test]
fn a_pan_that_is_given_up_puts_the_strip_back() {
let mut workspace = workspace(3, 6);
workspace.layout_scrolling(&AREA, 0);
workspace.focus_container(1);
let before = workspace.layout_scrolling(&AREA, 0);
let saved = workspace.begin_scrolling_pan().expect("a strip to pan");
workspace.pan_scrolling(-500.0);
workspace.cancel_scrolling_pan(saved);
assert_eq!(workspace.layout_scrolling(&AREA, 0), before);
assert_eq!(workspace.focused_container_idx(), 1);
}
#[test]
fn the_strip_is_moved_across_a_screen_as_wide_as_the_work_area() {
let mut workspace = workspace(3, 6);
assert_eq!(workspace.scrolling_view_width(), None, "no layout yet");
workspace.layout_scrolling(&AREA, 0);
assert_eq!(workspace.scrolling_view_width(), Some(1200));
}
}
@@ -0,0 +1,5 @@
begin fingers=3 x=4.000 y=2.000 w=10.500 h=7.200 t=118337
move dx=0.420 dy=-0.030 x=4.420 y=1.970 t=118345
move dx=0.510 dy=-0.020 x=4.930 y=1.950 t=118353
move dx=0.470 dy=0.000 x=5.400 y=1.950 t=118361
end x=5.400 y=1.950 t=118369
@@ -0,0 +1,18 @@
begin fingers=3 x=5.000 y=3.000 w=11.700 h=7.500 t=100048
move dx=0.267 dy=0.000 x=5.267 y=3.000 t=100088
move dx=0.267 dy=0.000 x=5.533 y=3.000 t=100096
move dx=0.267 dy=0.000 x=5.800 y=3.000 t=100104
move dx=0.267 dy=0.000 x=6.067 y=3.000 t=100112
move dx=0.267 dy=0.000 x=6.333 y=3.000 t=100120
move dx=0.267 dy=0.000 x=6.600 y=3.000 t=100128
swipe swipe3right
move dx=0.267 dy=0.000 x=6.867 y=3.000 t=100136
move dx=0.267 dy=0.000 x=7.133 y=3.000 t=100144
move dx=0.267 dy=0.000 x=7.400 y=3.000 t=100152
move dx=0.267 dy=0.000 x=7.667 y=3.000 t=100160
move dx=0.267 dy=0.000 x=7.933 y=3.000 t=100168
move dx=0.267 dy=0.000 x=8.200 y=3.000 t=100176
move dx=0.267 dy=0.000 x=8.467 y=3.000 t=100184
move dx=0.267 dy=0.000 x=8.733 y=3.000 t=100192
move dx=0.267 dy=0.000 x=9.000 y=3.000 t=100200
end x=9.000 y=3.000 t=100208
+218
View File
@@ -0,0 +1,218 @@
begin fingers=3 x=5.000 y=3.000 w=11.700 h=7.500 t=100048
move dx=0.007 dy=0.000 x=5.015 y=3.001 t=100110
move dx=0.008 dy=0.000 x=5.023 y=3.001 t=100119
move dx=0.010 dy=0.001 x=5.033 y=3.002 t=100129
move dx=0.012 dy=0.001 x=5.045 y=3.003 t=100136
move dx=0.014 dy=0.001 x=5.059 y=3.004 t=100142
move dx=0.015 dy=0.001 x=5.074 y=3.004 t=100153
move dx=0.017 dy=0.001 x=5.091 y=3.005 t=100160
move dx=0.018 dy=0.001 x=5.109 y=3.007 t=100168
move dx=0.020 dy=0.001 x=5.129 y=3.008 t=100174
move dx=0.022 dy=0.001 x=5.151 y=3.009 t=100184
move dx=0.023 dy=0.001 x=5.174 y=3.010 t=100193
move dx=0.025 dy=0.001 x=5.199 y=3.012 t=100199
move dx=0.026 dy=0.002 x=5.225 y=3.013 t=100207
move dx=0.027 dy=0.002 x=5.252 y=3.015 t=100217
move dx=0.029 dy=0.002 x=5.281 y=3.017 t=100223
move dx=0.030 dy=0.002 x=5.311 y=3.019 t=100231
move dx=0.032 dy=0.002 x=5.343 y=3.021 t=100240
move dx=0.033 dy=0.002 x=5.376 y=3.023 t=100248
move dx=0.034 dy=0.002 x=5.410 y=3.025 t=100256
move dx=0.035 dy=0.002 x=5.446 y=3.027 t=100262
move dx=0.037 dy=0.002 x=5.482 y=3.029 t=100272
move dx=0.038 dy=0.002 x=5.520 y=3.031 t=100281
move dx=0.039 dy=0.002 x=5.559 y=3.034 t=100287
move dx=0.040 dy=0.002 x=5.599 y=3.036 t=100294
move dx=0.041 dy=0.002 x=5.640 y=3.038 t=100303
move dx=0.042 dy=0.003 x=5.682 y=3.041 t=100312
move dx=0.043 dy=0.003 x=5.726 y=3.044 t=100318
move dx=0.044 dy=0.003 x=5.770 y=3.046 t=100327
move dx=0.045 dy=0.003 x=5.815 y=3.049 t=100334
move dx=0.046 dy=0.003 x=5.861 y=3.052 t=100342
move dx=0.047 dy=0.003 x=5.909 y=3.055 t=100351
move dx=0.048 dy=0.003 x=5.956 y=3.057 t=100360
move dx=0.049 dy=0.003 x=6.005 y=3.060 t=100369
move dx=0.050 dy=0.003 x=6.055 y=3.063 t=100375
move dx=0.050 dy=0.003 x=6.105 y=3.066 t=100383
move dx=0.051 dy=0.003 x=6.156 y=3.069 t=100390
move dx=0.052 dy=0.003 x=6.208 y=3.072 t=100400
move dx=0.053 dy=0.003 x=6.261 y=3.076 t=100406
move dx=0.053 dy=0.003 x=6.314 y=3.079 t=100414
move dx=0.054 dy=0.003 x=6.368 y=3.082 t=100423
move dx=0.054 dy=0.003 x=6.422 y=3.085 t=100431
move dx=0.055 dy=0.003 x=6.477 y=3.089 t=100439
move dx=0.056 dy=0.003 x=6.533 y=3.092 t=100448
swipe swipe3right
move dx=0.056 dy=0.003 x=6.589 y=3.095 t=100454
move dx=0.057 dy=0.003 x=6.646 y=3.099 t=100464
move dx=0.057 dy=0.003 x=6.703 y=3.102 t=100472
move dx=0.057 dy=0.003 x=6.760 y=3.106 t=100481
move dx=0.058 dy=0.003 x=6.818 y=3.109 t=100487
move dx=0.058 dy=0.003 x=6.876 y=3.113 t=100497
move dx=0.058 dy=0.004 x=6.934 y=3.116 t=100504
move dx=0.059 dy=0.004 x=6.993 y=3.120 t=100511
move dx=0.059 dy=0.004 x=7.052 y=3.123 t=100520
move dx=0.059 dy=0.004 x=7.111 y=3.127 t=100527
move dx=0.059 dy=0.004 x=7.171 y=3.130 t=100535
move dx=0.060 dy=0.004 x=7.230 y=3.134 t=100543
move dx=0.060 dy=0.004 x=7.290 y=3.137 t=100552
move dx=0.060 dy=0.004 x=7.350 y=3.141 t=100561
move dx=0.060 dy=0.004 x=7.410 y=3.145 t=100567
move dx=0.060 dy=0.004 x=7.470 y=3.148 t=100576
move dx=0.060 dy=0.004 x=7.530 y=3.152 t=100584
move dx=0.060 dy=0.004 x=7.590 y=3.155 t=100591
move dx=0.060 dy=0.004 x=7.650 y=3.159 t=100600
move dx=0.060 dy=0.004 x=7.710 y=3.163 t=100609
move dx=0.060 dy=0.004 x=7.770 y=3.166 t=100615
move dx=0.060 dy=0.004 x=7.829 y=3.170 t=100624
move dx=0.059 dy=0.004 x=7.889 y=3.173 t=100632
move dx=0.059 dy=0.004 x=7.948 y=3.177 t=100640
move dx=0.059 dy=0.004 x=8.007 y=3.180 t=100648
move dx=0.059 dy=0.004 x=8.066 y=3.184 t=100654
move dx=0.058 dy=0.004 x=8.124 y=3.187 t=100664
move dx=0.058 dy=0.003 x=8.182 y=3.191 t=100672
move dx=0.058 dy=0.003 x=8.240 y=3.194 t=100680
move dx=0.057 dy=0.003 x=8.297 y=3.198 t=100688
move dx=0.057 dy=0.003 x=8.354 y=3.201 t=100694
move dx=0.057 dy=0.003 x=8.411 y=3.205 t=100703
move dx=0.056 dy=0.003 x=8.467 y=3.208 t=100712
move dx=0.056 dy=0.003 x=8.523 y=3.211 t=100718
move dx=0.055 dy=0.003 x=8.578 y=3.215 t=100728
move dx=0.054 dy=0.003 x=8.632 y=3.218 t=100735
move dx=0.054 dy=0.003 x=8.686 y=3.221 t=100744
move dx=0.053 dy=0.003 x=8.739 y=3.224 t=100752
move dx=0.053 dy=0.003 x=8.792 y=3.228 t=100760
move dx=0.052 dy=0.003 x=8.844 y=3.231 t=100769
move dx=0.051 dy=0.003 x=8.895 y=3.234 t=100774
move dx=0.050 dy=0.003 x=8.945 y=3.237 t=100785
move dx=0.050 dy=0.003 x=8.995 y=3.240 t=100792
move dx=0.049 dy=0.003 x=9.044 y=3.243 t=100799
move dx=0.048 dy=0.003 x=9.091 y=3.245 t=100809
move dx=0.047 dy=0.003 x=9.139 y=3.248 t=100815
move dx=0.046 dy=0.003 x=9.185 y=3.251 t=100824
move dx=0.045 dy=0.003 x=9.230 y=3.254 t=100832
move dx=0.044 dy=0.003 x=9.274 y=3.256 t=100839
move dx=0.043 dy=0.003 x=9.318 y=3.259 t=100846
move dx=0.042 dy=0.003 x=9.360 y=3.262 t=100856
move dx=0.041 dy=0.002 x=9.401 y=3.264 t=100863
move dx=0.040 dy=0.002 x=9.441 y=3.266 t=100873
move dx=0.039 dy=0.002 x=9.480 y=3.269 t=100880
move dx=0.038 dy=0.002 x=9.518 y=3.271 t=100887
move dx=0.037 dy=0.002 x=9.554 y=3.273 t=100897
move dx=0.035 dy=0.002 x=9.590 y=3.275 t=100904
move dx=0.034 dy=0.002 x=9.624 y=3.277 t=100912
move dx=0.033 dy=0.002 x=9.657 y=3.279 t=100918
move dx=0.032 dy=0.002 x=9.689 y=3.281 t=100929
move dx=0.030 dy=0.002 x=9.719 y=3.283 t=100935
move dx=0.029 dy=0.002 x=9.748 y=3.285 t=100944
move dx=0.027 dy=0.002 x=9.775 y=3.287 t=100952
move dx=0.026 dy=0.002 x=9.801 y=3.288 t=100959
move dx=0.025 dy=0.001 x=9.826 y=3.290 t=100968
move dx=0.023 dy=0.001 x=9.849 y=3.291 t=100974
move dx=0.022 dy=0.001 x=9.871 y=3.292 t=100983
move dx=0.020 dy=0.001 x=9.891 y=3.293 t=100991
move dx=0.018 dy=0.001 x=9.909 y=3.295 t=101001
move dx=0.017 dy=0.001 x=9.926 y=3.296 t=101008
move dx=0.015 dy=0.001 x=9.941 y=3.296 t=101016
move dx=0.014 dy=0.001 x=9.955 y=3.297 t=101023
move dx=0.012 dy=0.001 x=9.967 y=3.298 t=101033
move dx=0.010 dy=0.001 x=9.977 y=3.299 t=101039
move dx=0.008 dy=0.000 x=9.985 y=3.299 t=101048
move dx=0.007 dy=0.000 x=9.991 y=3.299 t=101056
move dx=-0.005 dy=-0.001 x=9.991 y=3.299 t=101105
move dx=-0.007 dy=-0.001 x=9.984 y=3.298 t=101110
move dx=-0.009 dy=-0.001 x=9.975 y=3.297 t=101121
move dx=-0.011 dy=-0.001 x=9.964 y=3.296 t=101126
move dx=-0.013 dy=-0.001 x=9.951 y=3.295 t=101135
move dx=-0.014 dy=-0.001 x=9.937 y=3.294 t=101143
move dx=-0.016 dy=-0.002 x=9.921 y=3.292 t=101151
move dx=-0.018 dy=-0.002 x=9.903 y=3.290 t=101159
move dx=-0.019 dy=-0.002 x=9.884 y=3.288 t=101168
move dx=-0.021 dy=-0.002 x=9.863 y=3.286 t=101177
move dx=-0.022 dy=-0.002 x=9.841 y=3.284 t=101182
move dx=-0.024 dy=-0.002 x=9.817 y=3.282 t=101192
move dx=-0.025 dy=-0.002 x=9.792 y=3.279 t=101198
move dx=-0.026 dy=-0.003 x=9.766 y=3.277 t=101208
move dx=-0.027 dy=-0.003 x=9.738 y=3.274 t=101216
move dx=-0.029 dy=-0.003 x=9.710 y=3.271 t=101222
move dx=-0.030 dy=-0.003 x=9.680 y=3.268 t=101231
move dx=-0.031 dy=-0.003 x=9.649 y=3.265 t=101240
move dx=-0.032 dy=-0.003 x=9.617 y=3.262 t=101247
move dx=-0.033 dy=-0.003 x=9.585 y=3.258 t=101254
move dx=-0.034 dy=-0.003 x=9.551 y=3.255 t=101265
move dx=-0.034 dy=-0.003 x=9.517 y=3.252 t=101273
move dx=-0.035 dy=-0.004 x=9.481 y=3.248 t=101280
move dx=-0.036 dy=-0.004 x=9.446 y=3.245 t=101287
move dx=-0.037 dy=-0.004 x=9.409 y=3.241 t=101295
move dx=-0.037 dy=-0.004 x=9.372 y=3.237 t=101304
move dx=-0.038 dy=-0.004 x=9.334 y=3.233 t=101313
move dx=-0.038 dy=-0.004 x=9.296 y=3.230 t=101319
move dx=-0.039 dy=-0.004 x=9.257 y=3.226 t=101328
move dx=-0.039 dy=-0.004 x=9.218 y=3.222 t=101335
move dx=-0.039 dy=-0.004 x=9.179 y=3.218 t=101342
move dx=-0.040 dy=-0.004 x=9.140 y=3.214 t=101353
move dx=-0.040 dy=-0.004 x=9.100 y=3.210 t=101359
move dx=-0.040 dy=-0.004 x=9.060 y=3.206 t=101367
move dx=-0.040 dy=-0.004 x=9.020 y=3.202 t=101375
move dx=-0.040 dy=-0.004 x=8.980 y=3.198 t=101384
move dx=-0.040 dy=-0.004 x=8.940 y=3.194 t=101390
move dx=-0.040 dy=-0.004 x=8.900 y=3.190 t=101399
move dx=-0.040 dy=-0.004 x=8.860 y=3.186 t=101408
move dx=-0.040 dy=-0.004 x=8.821 y=3.182 t=101417
move dx=-0.039 dy=-0.004 x=8.782 y=3.178 t=101425
move dx=-0.039 dy=-0.004 x=8.743 y=3.174 t=101430
move dx=-0.039 dy=-0.004 x=8.704 y=3.170 t=101441
move dx=-0.038 dy=-0.004 x=8.666 y=3.167 t=101448
move dx=-0.038 dy=-0.004 x=8.628 y=3.163 t=101457
move dx=-0.037 dy=-0.004 x=8.591 y=3.159 t=101465
move dx=-0.037 dy=-0.004 x=8.554 y=3.155 t=101470
move dx=-0.036 dy=-0.004 x=8.519 y=3.152 t=101479
move dx=-0.035 dy=-0.004 x=8.483 y=3.148 t=101488
move dx=-0.034 dy=-0.003 x=8.449 y=3.145 t=101495
move dx=-0.034 dy=-0.003 x=8.415 y=3.142 t=101505
move dx=-0.033 dy=-0.003 x=8.383 y=3.138 t=101511
move dx=-0.032 dy=-0.003 x=8.351 y=3.135 t=101520
move dx=-0.031 dy=-0.003 x=8.320 y=3.132 t=101526
move dx=-0.030 dy=-0.003 x=8.290 y=3.129 t=101537
move dx=-0.029 dy=-0.003 x=8.262 y=3.126 t=101545
move dx=-0.027 dy=-0.003 x=8.234 y=3.123 t=101553
move dx=-0.026 dy=-0.003 x=8.208 y=3.121 t=101559
move dx=-0.025 dy=-0.002 x=8.183 y=3.118 t=101567
move dx=-0.024 dy=-0.002 x=8.159 y=3.116 t=101577
move dx=-0.022 dy=-0.002 x=8.137 y=3.114 t=101584
move dx=-0.021 dy=-0.002 x=8.116 y=3.112 t=101591
move dx=-0.019 dy=-0.002 x=8.097 y=3.110 t=101600
move dx=-0.018 dy=-0.002 x=8.079 y=3.108 t=101606
move dx=-0.016 dy=-0.002 x=8.063 y=3.106 t=101615
move dx=-0.014 dy=-0.001 x=8.049 y=3.105 t=101624
move dx=-0.013 dy=-0.001 x=8.036 y=3.104 t=101633
move dx=-0.011 dy=-0.001 x=8.025 y=3.103 t=101640
move dx=-0.009 dy=-0.001 x=8.016 y=3.102 t=101647
move dx=-0.007 dy=-0.001 x=8.009 y=3.101 t=101656
move dx=-0.005 dy=-0.001 x=8.004 y=3.100 t=101663
move dx=0.128 dy=0.000 x=8.128 y=3.100 t=101688
move dx=0.128 dy=0.000 x=8.256 y=3.100 t=101696
move dx=0.128 dy=0.000 x=8.384 y=3.100 t=101705
move dx=0.128 dy=0.000 x=8.512 y=3.100 t=101713
move dx=0.128 dy=0.000 x=8.640 y=3.100 t=101721
move dx=0.128 dy=0.000 x=8.768 y=3.100 t=101728
move dx=0.128 dy=0.000 x=8.896 y=3.100 t=101735
move dx=0.128 dy=0.000 x=9.024 y=3.100 t=101743
move dx=0.128 dy=0.000 x=9.152 y=3.100 t=101751
move dx=0.128 dy=0.000 x=9.280 y=3.100 t=101759
move dx=0.128 dy=0.000 x=9.408 y=3.100 t=101769
move dx=0.128 dy=0.000 x=9.536 y=3.100 t=101774
move dx=0.128 dy=0.000 x=9.664 y=3.100 t=101783
move dx=0.128 dy=0.000 x=9.792 y=3.100 t=101792
move dx=0.128 dy=0.000 x=9.920 y=3.100 t=101800
move dx=0.128 dy=0.000 x=10.048 y=3.100 t=101809
move dx=0.128 dy=0.000 x=10.176 y=3.100 t=101814
move dx=0.128 dy=0.000 x=10.304 y=3.100 t=101822
move dx=0.128 dy=0.000 x=10.432 y=3.100 t=101831
move dx=0.128 dy=0.000 x=10.560 y=3.100 t=101841
move dx=0.128 dy=0.000 x=10.688 y=3.100 t=101847
move dx=0.128 dy=0.000 x=10.816 y=3.100 t=101856
move dx=0.128 dy=0.000 x=10.944 y=3.100 t=101864
move dx=0.128 dy=0.000 x=11.072 y=3.100 t=101872
move dx=0.128 dy=0.000 x=11.200 y=3.100 t=101880
end x=11.200 y=3.100 t=101888
@@ -0,0 +1,67 @@
begin fingers=3 x=5.000 y=3.000 w=11.700 h=7.500 t=100048
move dx=0.006 dy=0.001 x=5.020 y=3.002 t=100128
move dx=0.007 dy=0.001 x=5.027 y=3.002 t=100136
move dx=0.008 dy=0.001 x=5.035 y=3.003 t=100144
move dx=0.009 dy=0.001 x=5.044 y=3.004 t=100152
move dx=0.010 dy=0.001 x=5.053 y=3.005 t=100160
move dx=0.011 dy=0.001 x=5.064 y=3.006 t=100168
move dx=0.011 dy=0.001 x=5.075 y=3.007 t=100176
move dx=0.012 dy=0.001 x=5.088 y=3.008 t=100184
move dx=0.013 dy=0.001 x=5.101 y=3.009 t=100192
move dx=0.014 dy=0.001 x=5.114 y=3.010 t=100200
move dx=0.014 dy=0.001 x=5.129 y=3.012 t=100208
move dx=0.015 dy=0.001 x=5.144 y=3.013 t=100216
move dx=0.016 dy=0.001 x=5.160 y=3.015 t=100224
move dx=0.016 dy=0.001 x=5.176 y=3.016 t=100232
move dx=0.017 dy=0.002 x=5.193 y=3.018 t=100240
move dx=0.017 dy=0.002 x=5.210 y=3.019 t=100248
move dx=0.018 dy=0.002 x=5.228 y=3.021 t=100256
move dx=0.018 dy=0.002 x=5.247 y=3.022 t=100264
move dx=0.019 dy=0.002 x=5.266 y=3.024 t=100272
move dx=0.019 dy=0.002 x=5.285 y=3.026 t=100280
move dx=0.020 dy=0.002 x=5.305 y=3.028 t=100288
move dx=0.020 dy=0.002 x=5.325 y=3.030 t=100296
move dx=0.020 dy=0.002 x=5.345 y=3.031 t=100304
move dx=0.021 dy=0.002 x=5.366 y=3.033 t=100312
move dx=0.021 dy=0.002 x=5.387 y=3.035 t=100320
move dx=0.021 dy=0.002 x=5.408 y=3.037 t=100328
move dx=0.021 dy=0.002 x=5.430 y=3.039 t=100336
move dx=0.022 dy=0.002 x=5.451 y=3.041 t=100344
move dx=0.022 dy=0.002 x=5.473 y=3.043 t=100352
move dx=0.022 dy=0.002 x=5.495 y=3.045 t=100360
move dx=0.022 dy=0.002 x=5.517 y=3.047 t=100368
move dx=0.022 dy=0.002 x=5.539 y=3.049 t=100376
move dx=0.022 dy=0.002 x=5.561 y=3.051 t=100384
move dx=0.022 dy=0.002 x=5.583 y=3.053 t=100392
move dx=0.022 dy=0.002 x=5.605 y=3.055 t=100400
move dx=0.022 dy=0.002 x=5.627 y=3.057 t=100408
move dx=0.022 dy=0.002 x=5.649 y=3.059 t=100416
move dx=0.022 dy=0.002 x=5.670 y=3.061 t=100424
move dx=0.021 dy=0.002 x=5.692 y=3.063 t=100432
move dx=0.021 dy=0.002 x=5.713 y=3.065 t=100440
move dx=0.021 dy=0.002 x=5.734 y=3.067 t=100448
move dx=0.021 dy=0.002 x=5.755 y=3.069 t=100456
move dx=0.020 dy=0.002 x=5.775 y=3.070 t=100464
move dx=0.020 dy=0.002 x=5.795 y=3.072 t=100472
move dx=0.020 dy=0.002 x=5.815 y=3.074 t=100480
move dx=0.019 dy=0.002 x=5.834 y=3.076 t=100488
move dx=0.019 dy=0.002 x=5.853 y=3.078 t=100496
move dx=0.018 dy=0.002 x=5.872 y=3.079 t=100504
move dx=0.018 dy=0.002 x=5.890 y=3.081 t=100512
move dx=0.017 dy=0.002 x=5.907 y=3.082 t=100520
move dx=0.017 dy=0.002 x=5.924 y=3.084 t=100528
move dx=0.016 dy=0.001 x=5.940 y=3.085 t=100536
move dx=0.016 dy=0.001 x=5.956 y=3.087 t=100544
move dx=0.015 dy=0.001 x=5.971 y=3.088 t=100552
move dx=0.014 dy=0.001 x=5.986 y=3.090 t=100560
move dx=0.014 dy=0.001 x=5.999 y=3.091 t=100568
move dx=0.013 dy=0.001 x=6.012 y=3.092 t=100576
move dx=0.012 dy=0.001 x=6.025 y=3.093 t=100584
move dx=0.011 dy=0.001 x=6.036 y=3.094 t=100592
move dx=0.011 dy=0.001 x=6.047 y=3.095 t=100600
move dx=0.010 dy=0.001 x=6.056 y=3.096 t=100608
move dx=0.009 dy=0.001 x=6.065 y=3.097 t=100616
move dx=0.008 dy=0.001 x=6.073 y=3.098 t=100624
move dx=0.007 dy=0.001 x=6.080 y=3.098 t=100632
move dx=0.006 dy=0.001 x=6.086 y=3.099 t=100640
end x=6.100 y=3.100 t=100688
+49 -5
View File
@@ -6,6 +6,8 @@ use chrono::Utc;
use komorebi_client::PathExt;
use komorebi_client::replace_env_in_path;
use komorebi_client::splash;
mod pan;
use std::fs::File;
use std::fs::OpenOptions;
use std::io;
@@ -1013,8 +1015,27 @@ struct ScrollingLayoutColumns {
#[derive(Parser)]
struct Pan {
/// The way that the view goes
#[clap(value_enum)]
direction: PanDirection,
#[clap(
value_enum,
required_unless_present = "stream",
conflicts_with = "stream"
)]
direction: Option<PanDirection>,
/// Read the gestures of fingers on a touchpad from standard input, as whkd writes them for
/// a swipe of three or four fingers, and pan with them
#[clap(long)]
stream: bool,
/// What the distances of a gesture are multiplied by, which is 1 if it is left out, with
/// --stream
#[clap(long)]
sensitivity: Option<f64>,
/// Go the other way to the fingers, with --stream
#[clap(long)]
reversed: bool,
/// Take a step for every two centimetres that the fingers go, and not move the view with
/// them, with --stream
#[clap(long)]
paging: bool,
}
#[derive(Parser)]
@@ -1310,8 +1331,8 @@ enum SubCommand {
/// Stack the windows of the focused column of the Scrolling layout, or spread the focused
/// stack out into a column with a window in each slot
ToggleColumnStack,
/// Move the view of the Scrolling layout a column to the left or the right, which does
/// nothing in any other layout
/// Move the view of the Scrolling layout a column to the left or the right, or with the
/// fingers on a touchpad, which does nothing in any other layout
#[clap(arg_required_else_help = true)]
Pan(Pan),
/// Set the layout column and row ratios for the focused workspace
@@ -3005,7 +3026,30 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
send_message(&SocketMessage::ToggleColumnStack)?;
}
SubCommand::Pan(args) => {
send_message(&SocketMessage::Pan(args.direction))?;
// a flag that has a default counts as given, so what only goes with --stream
// cannot be left to the parser to say
if !args.stream && (args.sensitivity.is_some() || args.reversed || args.paging) {
Opts::command()
.error(
clap::error::ErrorKind::ArgumentConflict,
"--sensitivity, --reversed and --paging can only be used with --stream",
)
.exit();
}
if args.stream {
pan::run(
BufReader::new(io::stdin()),
pan::Options {
sensitivity: args.sensitivity.unwrap_or(1.0),
reversed: args.reversed,
paging: args.paging,
},
&mut pan::Socket::new(komorebi_client::Connection::open),
)?;
} else if let Some(direction) = args.direction {
send_message(&SocketMessage::Pan(direction))?;
}
}
SubCommand::LayoutRatios(args) => {
if args.columns.is_none() && args.rows.is_none() {
+809
View File
@@ -0,0 +1,809 @@
//! Reading the stream of gestures that `komorebic pan --stream` is given, which whkd writes for a
//! swipe of three or four fingers on a touchpad, and telling komorebi what the fingers do.
//!
//! The format is whkd's, and is written down in `docs/gesture-stream.md` in its repository: 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 lets 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));
}
}