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

@@ -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));
}
}