mirror of
https://github.com/lotabout/skim.git
synced 2026-09-09 23:06:17 -04:00
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:
parent
b2a732efa0
commit
3644242897
22
.github/workflows/apt.yml
vendored
22
.github/workflows/apt.yml
vendored
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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
115
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
src/binds.rs
22
src/binds.rs
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue