feat: add double-click as a bindable trigger (#1134)

* feat: add double-click as a bindable trigger

closes #313

* typo
This commit is contained in:
LoricAndre 2026-07-21 16:24:47 +02:00 committed by GitHub
parent b2a732efa0
commit 3644242897
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 201 additions and 103 deletions

View file

@ -3,6 +3,27 @@ name: Deploy APT repo
# Publishes the release's .deb packages as a (flat) APT repository under
# /apt on the gh-pages branch, alongside the coverage report at /coverage.
# keep_files preserves the coverage content (and vice versa).
#
# Requires APT_GPG_PRIVATE_KEY to be set :
#
# ```sh
# export GNUPGHOME=$(mktemp -d)
# cat > "$GNUPGHOME/skim-key" <<'EOF'
# %no-protection
# Key-Type: eddsa
# Key-Curve: ed25519
# Subkey-Type: ecdh
# Subkey-Curve: cv25519
# Name-Real: skim apt repo
# Name-Email: apt@skim-rs.github.io
# Expire-Date: 0
# %commit
# EOF
# gpg --batch --gen-key "$GNUPGHOME/skim-key" 2>&1 | tail -3
# KEYID=$(gpg --list-secret-keys --with-colons apt@skim-rs.github.io | awk -F: '/^sec:/{print $5; exit}')
# # Copy the output of the line below
# gpg --export-secret-keys --armor "$KEYID"
# ```
on:
workflow_call:
inputs:
@ -38,7 +59,6 @@ jobs:
# key) to enable signed-by verification.
env:
GPG_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }}
if: ${{ env.GPG_KEY != '' }}
run: |
echo "$GPG_KEY" | gpg --batch --import
KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}')

View file

@ -703,6 +703,7 @@ Frame rate is capped at 120 fps (`FRAME_TIME_MS = 1000/120`). `App::handle_event
| `needs_render` | `Arc<AtomicBool>` | Signal from matcher → event loop |
| `yank_register` | `String` | Cut/yank buffer |
| `query_history` / `cmd_history` | `Vec<String>` | History for ↑/↓ navigation |
| `last_left_click` | `Option<Instant>` | Detect two left clicks within the 500 ms `double-click` window |
**`App::handle_event()`** dispatches on `Event`:
@ -715,7 +716,7 @@ Event::Key(k) → handle_key(k) → [Action…] → tui.event_tx.send(Event:
Event::Action(a) → handle_action(a) → [Event…] → tui.event_tx.send(…)
Event::Paste(t) → input.insert_str(cleaned); on_query_changed()
Event::Resize(…) → app.resize(); run_preview()
Event::Mouse(…) → handle_mouse()
Event::Mouse(…) → handle_mouse() → normal handling + optional `double-click` key event
Event::PreviewReady → apply preview offset; needs_render()
Event::AppendItems → item_pool.append(); restart_matcher(false)
Event::ClearItems → item_pool.clear(); restart_matcher(true)
@ -958,6 +959,15 @@ Notable defaults:
User bindings from `--bind key:action[+action]` are parsed at startup and merged via `KeyMap::add_keymaps()`.
### Mouse Bindings
| Bind | Default | Fired when |
| --- | --- | --- |
| `double-click` | `Accept(None)` | two left-button presses occur within 500 ms; the first press still performs normal item selection |
`App::handle_mouse` recognizes the gesture after normal click handling and routes
it through the keymap using the reserved `SkimEvent::DoubleClick` key code.
### Synthetic Events (`SkimEvent`)
Besides real key presses, skim fires a few *synthetic* events that can be bound
@ -1307,16 +1317,17 @@ The global allocator is `mimalloc` (v3), chosen for its low-latency multi-thread
| `Skim::should_enter` | `src/skim.rs:434` | Filter/select-1/exit-0/sync gate |
| `Skim::output` | `src/skim.rs:539` | Collect & return SkimOutput |
| `Skim::tick` | `src/skim.rs:620` | Single async event loop iteration |
| `App::from_options` | `src/tui/app.rs:285` | Build all widgets from options |
| `App::run_preview` | `src/tui/app.rs:498` | Expand cmd, debounce, call Preview::spawn |
| `App::handle_event` | `src/tui/app.rs:623` | Dispatch all Event variants |
| `App::handle_action` | `src/tui/app.rs:828` | Apply action follow-up bindings |
| `App::dispatch_conditional` | `src/tui/app.rs:847` | Dispatch the selected conditional subaction chain without follow-up bindings |
| `App::dispatch_action` | `src/tui/app.rs:870` | Dispatch one Action variant without follow-up bindings |
| `App::from_options` | `src/tui/app.rs:289` | Build all widgets from options |
| `App::run_preview` | `src/tui/app.rs:503` | Expand cmd, debounce, call Preview::spawn |
| `App::handle_event` | `src/tui/app.rs:628` | Dispatch all Event variants |
| `App::handle_action` | `src/tui/app.rs:833` | Apply action follow-up bindings |
| `App::dispatch_conditional` | `src/tui/app.rs:852` | Dispatch the selected conditional subaction chain without follow-up bindings |
| `App::dispatch_action` | `src/tui/app.rs:875` | Dispatch one Action variant without follow-up bindings |
| `Tui::run_execute` | `src/tui/backend.rs:353` | Suspend reader, run `execute` child with its own tty stdin, restart reader |
| `App::restart_matcher` | `src/tui/app.rs:1355` | Kill old match pass, start new one |
| `App::expand_cmd` | `src/tui/app.rs:1431` | Substitute `{}`, `{q}`, `{n}` etc. |
| `Widget::render (App)` | `src/tui/app.rs:149` | Root render; calls all sub-widgets |
| `App::restart_matcher` | `src/tui/app.rs:1352` | Kill old match pass, start new one |
| `App::expand_cmd` | `src/tui/app.rs:1428` | Substitute `{}`, `{q}`, `{n}` etc. |
| `App::handle_mouse` | `src/tui/app.rs:1496` | Handle mouse behavior and emit `double-click` |
| `Widget::render (App)` | `src/tui/app.rs:151` | Root render; calls all sub-widgets |
| `Matcher::run` | `src/matcher.rs:~260` | Parallel match dispatch |
| `merge_worker_results` | `src/matcher.rs:28` | Merge k sorted runs → ProcessedItems |
| `ItemPool::append` | `src/item.rs:469` | Add items, notify matcher |
@ -1335,10 +1346,10 @@ The global allocator is `mimalloc` (v3), chosen for its low-latency multi-thread
| `popup::check_env` | `src/popup/mod.rs:72` | Guard: multiplexer present and not already in popup |
| `check_and_run_popup` | `src/bin/main.rs:131` | Check popup conditions, dispatch to popup::run_with |
| `sk_main` | `src/bin/main.rs:144` | CLI orchestration + output printing |
| `SkimEvent` | `src/binds.rs:26` | `change`/`start`/`load`/`result`/`focus`/`zero`/`one` synthetic events → reserved `KeyEvent` |
| `parse_key` | `src/binds.rs:223` | `"ctrl-a"``KeyEvent` |
| `parse_action_binds` | `src/binds.rs:329` | `"reload:first"`, `"act-up:suppress+down"` → action follow-up map |
| `parse_action_chain` | `src/binds.rs:377` | `"down+select"``Vec<Action>` |
| `SkimEvent` | `src/binds.rs:25` | Bindable synthetic events, including `double-click`, routed through reserved `KeyEvent`s |
| `parse_key` | `src/binds.rs:226` | `"ctrl-a"``KeyEvent` |
| `parse_action_binds` | `src/binds.rs:335` | `"reload:first"`, `"act-up:suppress+down"` → action follow-up map |
| `parse_action_chain` | `src/binds.rs:383` | `"down+select"``Vec<Action>` |
| `Action::name` | `src/tui/event.rs:331` | `Action` → canonical bind name (shared catalog with `parse_action`) |
| `Matcher::create_engine_factory_with_builder` | `src/matcher.rs:189` | Build engine factory chain from options |
| `ExactOrFuzzyEngineFactory::create_engine_with_case` | `src/engine/factory.rs:93` | Parse query prefixes, build engine |

115
README.md
View file

@ -81,77 +81,72 @@ The skim project contains several components:
## Package Managers
| OS | Package Manager | Command |
| -------------- | ----------------- | ---------------------------- |
| macOS | Homebrew | `brew install sk` |
| macOS | MacPorts | `sudo port install skim` |
| Alpine | apk | `apk add skim` |
| Arch | pacman | `pacman -S skim` |
| Fedora | COPR | see below |
| Gentoo | Portage | `emerge --ask app-misc/skim` |
| Guix | guix | `guix install skim` |
| Void | XBPS | `xbps-install -S skim` |
| Windows | winget | `winget install skim`|
| Windows | Scoop | `scoop install skim` |
| Debian/Ubuntu | `.deb` package | see below |
| Fedora/RHEL/SUSE | `.rpm` package | see below |
| OS | Package Manager | Command |
| -------------- | --------------- | ---------------------------- |
| macOS | Homebrew | `brew install sk` |
| macOS | MacPorts | `sudo port install skim` |
| Alpine | apk | `apk add skim` |
| Arch | pacman | `pacman -S skim` |
| Fedora | COPR | see below |
| Gentoo | Portage | `emerge --ask app-misc/skim` |
| Guix | guix | `guix install skim` |
| Void | XBPS | `xbps-install -S skim` |
| Windows | winget | `winget install skim` |
| Windows | Scoop | `scoop install skim` |
| Debian/Ubuntu | apt | see below |
| Fedora/RHEL | dnf | see below |
<a href="https://repology.org/project/skim-fuzzy-finder/versions">
<img src="https://repology.org/badge/vertical-allrepos/skim-fuzzy-finder.svg?columns=4" alt="Packaging status">
</a>
### Fedora
### Debian/Ubuntu
Up to date Fedora packages are provided via an unofficial community-maintained COPR repository.
A custom APT repository is available and updated automatically during each release:
1. Import the signing key
With wget:
```sh
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/skim.asc https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc
```
Or with cURL:
```sh
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc | sudo tee /etc/apt/keyrings/skim.asc > /dev/null
```
2. Add the repository
```sh
echo 'deb [signed-by=/etc/apt/keyrings/skim.asc] https://skim-rs.github.io/skim/apt ./' | sudo tee /etc/apt/sources.list.d/skim.list
sudo apt-get update
```
3. Install
```sh
sudo apt-get install skim
```
Alternatively, `.deb` packages are attached directly to every [release](https://github.com/skim-rs/skim/releases/latest).
Download the one matching your architecture and run `sudo dpkg -i skim_*_amd64.deb`
### Fedora/RHEL
Up-to-date Fedora/RHEL packages are provided via an unofficial community-maintained COPR repository.
```bash
sudo dnf copr enable sisyphus1813/skim
sudo dnf install skim
```
### Windows
Using [winget](https://learn.microsoft.com/windows/package-manager/):
```powershell
winget install skim
```
Or using [Scoop](https://scoop.sh/):
```powershell
scoop install skim
```
### Debian and RPM packages
Every [release](https://github.com/skim-rs/skim/releases/latest) ships prebuilt
`.deb` and `.rpm` packages for `amd64` and `arm64`. Each one installs the `sk`
executable, its man page and the bash/zsh/fish completions.
Download the package matching your distribution and architecture from the
[latest release](https://github.com/skim-rs/skim/releases/latest), then install it:
```sh
# Debian / Ubuntu (use skim_*_arm64.deb on ARM machines)
sudo dpkg -i skim_*_amd64.deb
# Fedora / RHEL / openSUSE (use skim-*.aarch64.rpm on ARM machines)
sudo rpm -i skim-*.x86_64.rpm
```
### APT repository (Debian/Ubuntu)
Instead of downloading a `.deb` manually, add the hosted APT repository to get
`sk` from `apt` and receive updates:
```sh
# Import the signing key
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/skim.asc https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc
# ...or with curl:
# curl -fsSL https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc | sudo tee /etc/apt/keyrings/skim.asc > /dev/null
# Add the repository
echo 'deb [signed-by=/etc/apt/keyrings/skim.asc] https://skim-rs.github.io/skim/apt ./' | sudo tee /etc/apt/sources.list.d/skim.list
sudo apt-get update
sudo apt-get install skim
```
Alternatively, `.rpm` packages are attached directly to every [release](https://github.com/skim-rs/skim/releases/latest).
Download it and run `sudo rpm -i skim-*.x86_64.rpm`
## Manually

View file

@ -199,8 +199,9 @@ history: History scheme: will force index as the first tiebreak
\fB\-b\fR, \fB\-\-bind\fR [\fI<BIND>...\fR] [default: ]
Comma\-separated key, event, and action bindings
`\-\-bind` takes comma\-separated `<trigger>:<action>` expressions. A trigger can be a key, a finder
event (`change`, `start`, `load`, `result`, `focus`, `zero`, or `one`), or an action name. Use the
`\-\-bind` takes comma\-separated `<trigger>:<action>` expressions. A trigger can be a key, the
`double\-click` mouse binding, a finder event (`change`, `start`, `load`, `result`, `focus`, `zero`, or
`one`), or an action name. Use the
`act\-` prefix for action triggers; it is recommended to avoid ambiguity and required when the action
name is also a key, for example `act\-up:last`. See the [KEYBINDS] section for details.
@ -835,6 +836,8 @@ Actions can take arguments, specified either between parentheses `reload(ls)` or
.br
* alt\-shift\-right
.br
* double\-click
.br
* any single character
.br
@ -871,7 +874,7 @@ Follow\-up chains use non\-recursive (`noremap`) semantics: their actions do not
.br
* abort: ctrl\-c ctrl\-q esc
.br
* accept(...): enter *the argument will be printed when the binding is triggered*
* accept(...): enter double\-click *the argument will be printed when the binding is triggered*
.br
* append\-and\-select
.br

View file

@ -17,11 +17,10 @@ use crate::tui::event::{self, Action};
/// The keymap is keyed by crossterm's [`KeyEvent`], which cannot express
/// "the query changed" or "reading finished" directly. Each variant is
/// therefore represented *transparently* as a reserved function-key code in the
/// high-`F` range (`F(249)``F(255)`) that no real terminal ever emits. The
/// seven variants are `change`, `start`, `load`, `result`, `focus`, `zero`, and
/// `one`. Giving these reserved codes named variants keeps them in one place
/// instead of scattering magic function-key literals across the codebase, and
/// lets [`parse_key`] accept every friendly event name.
/// high-`F` range (`F(248)``F(255)`) that no real terminal ever emits.
/// Giving these reserved codes named variants keeps them in one place instead
/// of scattering magic function-key literals across the codebase, and lets
/// [`parse_key`] accept every friendly event name.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum SkimEvent {
/// Fired once, when skim has started up and entered its event loop.
@ -40,6 +39,8 @@ pub enum SkimEvent {
Zero,
/// Fired when a completed search yields exactly one match.
One,
/// Fired after two left mouse-button presses no more than 500 ms apart.
DoubleClick,
}
impl SkimEvent {
@ -54,6 +55,7 @@ impl SkimEvent {
SkimEvent::Focus => KeyCode::F(251),
SkimEvent::Zero => KeyCode::F(250),
SkimEvent::One => KeyCode::F(249),
SkimEvent::DoubleClick => KeyCode::F(248),
}
}
@ -76,6 +78,7 @@ impl SkimEvent {
"focus" => Some(SkimEvent::Focus),
"zero" => Some(SkimEvent::Zero),
"one" => Some(SkimEvent::One),
"double-click" => Some(SkimEvent::DoubleClick),
_ => None,
}
}
@ -166,6 +169,7 @@ pub fn get_default_key_map() -> KeyMap {
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE), vec![Action::BackwardChar]);
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), vec![Action::ForwardChar]);
ret.insert(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), vec![Action::BackwardDeleteChar]);
ret.insert(SkimEvent::DoubleClick.key_event(), vec![Action::Accept(None)]);
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT), vec![Action::BackwardWord]);
@ -213,9 +217,8 @@ pub fn get_default_key_map() -> KeyMap {
/// Parses a key str into a crossterm `KeyEvent`.
///
/// In addition to keyboard names, accepts all seven names recognized by
/// [`SkimEvent::from_name`]: `change`, `start`, `load`, `result`, `focus`,
/// `zero`, and `one`.
/// In addition to keyboard names, accepts all names recognized by
/// [`SkimEvent::from_name`], including `change`, `start`, and `double-click`.
///
/// # Errors
/// Returns an error if the key string is empty, contains an unknown modifier,
@ -224,6 +227,9 @@ pub fn parse_key(key: &str) -> Result<KeyEvent> {
if key.is_empty() {
return Err(eyre!("Cannot parse empty key"));
}
if let Some(event) = SkimEvent::from_name(key) {
return Ok(event.key_event());
}
let parts = key.split('-').collect::<Vec<&str>>();
let mut mods = KeyModifiers::NONE;

View file

@ -140,6 +140,7 @@ fn skim_event_name_roundtrip() {
("focus", SkimEvent::Focus),
("zero", SkimEvent::Zero),
("one", SkimEvent::One),
("double-click", SkimEvent::DoubleClick),
] {
assert_eq!(SkimEvent::from_name(name), Some(event));
assert_eq!(parse_key(name).unwrap(), KeyEvent::from(event));
@ -147,10 +148,23 @@ fn skim_event_name_roundtrip() {
// Unknown names are not events.
assert_eq!(SkimEvent::from_name("nope"), None);
// A binding referencing an event name resolves to an action chain.
let keymap = KeyMap::from("start:first,load:last,change:first");
assert!(keymap.get(&SkimEvent::Start.key_event()).is_some());
assert!(keymap.get(&SkimEvent::Load.key_event()).is_some());
assert!(keymap.get(&SkimEvent::Change.key_event()).is_some());
let keymap = KeyMap::from("start:first,load:last,change:first,double-click:accept");
for event in [
SkimEvent::Start,
SkimEvent::Load,
SkimEvent::Change,
SkimEvent::DoubleClick,
] {
assert!(keymap.get(&event.key_event()).is_some());
}
}
#[test]
fn double_click_accepts_by_default() {
assert_eq!(
get_default_key_map().get(&SkimEvent::DoubleClick.key_event()),
Some(&vec![Accept(None)])
);
}
#[test]

View file

@ -119,6 +119,7 @@ const KEYS_SS: &str = "
* alt-shift-down
* alt-shift-left
* alt-shift-right
* double-click
* any single character
";
const BINDABLE_EVENTS_SS: &str = concat!(
@ -145,7 +146,7 @@ const ACTION_BINDINGS_SS: &str = concat!(
const ACTIONS_SS: &str = concat!(
"\n* abort: ctrl-c ctrl-q esc
* accept(...): enter *the argument will be printed when the binding is triggered*
* accept(...): enter double-click *the argument will be printed when the binding is triggered*
* append-and-select
* backward-char: ctrl-b left
* backward-delete-char: ctrl-h bspace

View file

@ -321,8 +321,9 @@ pub struct SkimOptions {
// --- Interface ---
/// Comma-separated key, event, and action bindings
///
/// `--bind` takes comma-separated `<trigger>:<action>` expressions. A trigger can be a key, a finder
/// event (`change`, `start`, `load`, `result`, `focus`, `zero`, or `one`), or an action name. Use the
/// `--bind` takes comma-separated `<trigger>:<action>` expressions. A trigger can be a key, the
/// `double-click` mouse binding, a finder event (`change`, `start`, `load`, `result`, `focus`, `zero`, or
/// `one`), or an action name. Use the
/// `act-` prefix for action triggers; it is recommended to avoid ambiguity and required when the action
/// name is also a key, for example `act-up:last`. See the [KEYBINDS] section for details.
///

View file

@ -43,6 +43,7 @@ static NUM_THREADS: LazyLock<usize> = LazyLock::new(|| {
const MATCHER_DEBOUNCE_MS: u128 = 200;
const HIDE_GRACE_MS: u128 = 500;
const DOUBLE_CLICK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
/// Application state for skim's TUI
#[allow(clippy::struct_excessive_bools)]
@ -130,6 +131,8 @@ pub struct App {
items_just_updated: bool,
/// Records if we are scrolling (mouse down on the scrollbar and no mouse up yet)
currently_scrolling: bool,
/// Time of the previous left click, used to recognize `double-click` bindings.
last_left_click: std::time::Instant,
/// Set by [`Skim::check_reader`] once the reader has finished producing
/// items. Reset on `reload`. Drives the one-shot `load` event.
pub(crate) reader_done: bool,
@ -267,6 +270,9 @@ impl Default for App {
reader_timer: std::time::Instant::now(),
items_just_updated: false,
currently_scrolling: false,
last_left_click: std::time::Instant::now()
.checked_sub(DOUBLE_CLICK_INTERVAL * 2)
.unwrap(),
reader_done: false,
load_event_fired: false,
result_pending: false,
@ -338,6 +344,9 @@ impl App {
.unwrap(),
pending_preview_run: false,
currently_scrolling: false,
last_left_click: std::time::Instant::now()
.checked_sub(DOUBLE_CLICK_INTERVAL * 2)
.unwrap(),
reader_done: false,
load_event_fired: false,
result_pending: false,
@ -929,10 +938,8 @@ impl App {
}
Bind(spec) => {
// Bind one or more `trigger:action[+action]` pairs, reusing the
// same parsing/merging logic as the `--bind` CLI option: key
// triggers merge into the keymap, action triggers into the
// follow-up action bindings. Existing bindings for the same
// triggers are replaced.
// same parsing/merging logic as the `--bind` CLI option.
// Existing bindings for the same triggers are replaced.
self.options.keymap.add_keymaps_str(spec);
self.options.action_binds.extend(crate::binds::parse_action_binds(
crate::binds::split_top_level(spec, ',').into_iter(),
@ -1284,9 +1291,6 @@ impl App {
}
Unbind(spec) => {
// Remove the bindings for one or more keys or action triggers.
// Keys win, mirroring `bind`: a name that parses as a real key
// unbinds the key; otherwise `act-up`/`first` style triggers are
// removed from the follow-up action bindings.
for trigger in crate::binds::split_top_level(spec, ',') {
match crate::binds::parse_key(trigger) {
Ok(parsed) => {
@ -1501,6 +1505,7 @@ impl App {
trace!("Got mouse event {mouse_event:?}");
let old_current = self.item_list.current;
let mut double_click = false;
match mouse_event.kind {
MouseEventKind::ScrollUp => {
@ -1526,6 +1531,10 @@ impl App {
return self.handle_action(&Action::Down(1));
}
MouseEventKind::Down(MouseButton::Left) => {
let now = std::time::Instant::now();
double_click = now.duration_since(self.last_left_click) <= DOUBLE_CLICK_INTERVAL;
self.last_left_click = now;
if let Some((inner, scrollbar_col)) = self.scrollbar_column()
&& mouse_pos.x == scrollbar_col
&& inner.contains(mouse_pos)
@ -1562,10 +1571,15 @@ impl App {
self.needs_render();
if self.item_list.current != old_current {
return Ok(self.on_selection_changed());
let mut events = if self.item_list.current == old_current {
Vec::new()
} else {
self.on_selection_changed()
};
if double_click {
events.push(Event::Key(SkimEvent::DoubleClick.into()));
}
Ok(vec![])
Ok(events)
}
fn toggle_spinner(&mut self) {
self.show_spinner = !self.show_spinner;

View file

@ -1482,6 +1482,39 @@ fn mouse_selection_change_requests_preview() -> Result<()> {
Ok(())
}
#[test]
fn double_click_keeps_first_click_and_emits_binding_event() -> Result<()> {
let mut app = app_with_items(&["a", "b", "c"]);
let _ = render(&mut app, 40, 6);
let inner = app.list_inner_area();
let item_one_row = inner.y + inner.height - 2;
let first = app.handle_mouse(mouse_down(inner.x, item_one_row))?;
assert_eq!(app.item_list.current, 1);
assert!(first.iter().any(|event| matches!(event, Event::RunPreview)));
assert!(
!first
.iter()
.any(|event| matches!(event, Event::Key(key) if *key == SkimEvent::DoubleClick.key_event()))
);
let second = app.handle_mouse(mouse_down(inner.x, item_one_row))?;
assert!(
second
.iter()
.any(|event| matches!(event, Event::Key(key) if *key == SkimEvent::DoubleClick.key_event()))
);
app.last_left_click = past_instant(std::time::Duration::from_millis(501));
let late = app.handle_mouse(mouse_down(inner.x, item_one_row))?;
assert!(
!late
.iter()
.any(|event| matches!(event, Event::Key(key) if *key == SkimEvent::DoubleClick.key_event()))
);
Ok(())
}
#[test]
fn mouse_selection_same_item_only_requests_render() -> Result<()> {
let mut app = app_with_items(&["a", "b", "c", "d", "e", "f"]);