Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
BINARY_NAME=clipse
INSTALL_DIR?=/usr/local/bin/
INSTALL_DIR?=$(HOME)/.local/bin

wayland:
CGO_ENABLED=0 go build -tags wayland -o $(BINARY_NAME)
Expand All @@ -10,11 +10,11 @@ x11:
darwin:
go build -tags darwin -o $(BINARY_NAME)

run: build
run: wayland
./$(BINARY_NAME)

install: build
install -m 755 $(BINARY_NAME) $(INSTALL_DIR)
install: wayland
install -Dm 755 $(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)

clean:
go clean
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ __If any values from this file are removed, they will not be readded when the pr
| `tempDir` | string | Directory used for image files. |
| `enableMouse` | bool | Enables mouse interaction in the UI. |
| `enableDescription` | bool | Shows additional descriptive text for clipboard entries. |
| `search` | map | Fuzzy search engine and ranking options. See [Search](#search). |
| `keyBindings` | map | Custom keybind definitions. |
| `autoPaste` | map | Auto-paste options. |
| `imageDisplay` | map | Image display options (basic/kitty/sixel). |
Expand Down Expand Up @@ -380,6 +381,34 @@ Absolute paths starting with `/`, paths relative to the user home dir using `~`,
| `keyBindings.up` | string | Moves selection up by one entry. |
| `keyBindings.yankFilter` | string | Copies the current filter text. |

## Search

The `search` object configures how filter matches are scored and ordered.

| Option | Type | Description |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `search.engine` | string | `"default"` (existing behavior, backed by `sahilm/fuzzy`) or `"fzf"` (fzf v2 scoring from `github.com/junegunn/fzf`). Default `"default"`. |
| `search.algo` | string | `"v1"` or `"v2"` — fzf scoring algorithm. Default `"v2"`. Ignored when `engine` is `"default"`. |
| `search.caseSensitivity` | string | `"smart"`, `"respect"`, or `"ignore"`. `smart` is case-insensitive unless the query contains uppercase. Default `"smart"`. Fzf engine only. |
| `search.normalize` | bool | Strip diacritics so `cafe` matches `café`. Default `true`. Fzf engine only. |
| `search.tiebreak` | string[] | Ordering applied when fzf scores tie. Any of `"score"`, `"length"`, `"index"`, `"frecency"`. Default `["score","frecency","index"]`. Fzf engine only. |

Opting into fzf scoring with frecency:

```json
{
"search": {
"engine": "fzf",
"algo": "v2",
"caseSensitivity": "smart",
"normalize": true,
"tiebreak": ["score", "frecency", "index"]
}
}
```

Adding `"frecency"` to `tiebreak` makes clipse track how often and how recently each entry was selected (written as `useCount` and `lastUsed` into `clipboard_history.json`) and surface frequently used entries first when fzf scores tie.

Key bindings can take multiple keys delimited by `,`.

For example:
Expand Down
33 changes: 27 additions & 6 deletions app/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app
import (
"fmt"
"strings"
"time"
"unicode"

"github.com/charmbracelet/bubbles/help"
Expand All @@ -12,6 +13,7 @@ import (
tea "github.com/charmbracelet/bubbletea"

"github.com/savedra1/clipse/config"
"github.com/savedra1/clipse/search"
"github.com/savedra1/clipse/utils"
)

Expand Down Expand Up @@ -95,7 +97,7 @@ func NewModel() Model {
del := m.newItemDelegate()

clipboardList := list.New(entryItems, del, 0, 0)
clipboardList.Filter = sanitizedFilter
clipboardList.Filter = buildFilter(clipboardItems)
clipboardList.KeyMap = defaultOverrides(config.ClipseConfig.KeyBindings) // override default list keys with custom values
clipboardList.Title = clipboardTitle // set hardcoded title
clipboardList.SetShowHelp(false) // override with custom
Expand Down Expand Up @@ -128,12 +130,31 @@ func NewModel() Model {
return m
}

func sanitizedFilter(term string, targets []string) []list.Rank {
sanitized := make([]string, len(targets))
for i, t := range targets {
sanitized[i] = stripNonPrintable(t)
func buildFilter(items []config.ClipboardItem) func(string, []string) []list.Rank {
meta := make(map[string]search.ItemMeta, len(items))
for _, it := range items {
lastUsed, _ := time.Parse(utils.DateLayout, it.LastUsed)
key := stripNonPrintable(utils.Shorten(it.Value, config.ClipseConfig.MaxEntryLength))
meta[key] = search.ItemMeta{UseCount: it.UseCount, LastUsed: lastUsed}
}
lookup := func(target string) search.ItemMeta {
return meta[target]
}
sc := config.ClipseConfig.Search
inner := search.Filter(search.Config{
Engine: sc.Engine,
Algo: sc.Algo,
CaseSensitivity: sc.CaseSensitivity,
Normalize: sc.Normalize,
Tiebreak: sc.Tiebreak,
}, lookup)
return func(term string, targets []string) []list.Rank {
sanitized := make([]string, len(targets))
for i, t := range targets {
sanitized[i] = stripNonPrintable(t)
}
return inner(term, sanitized)
}
return list.DefaultFilter(term, sanitized)
}

func stripNonPrintable(s string) string {
Expand Down
16 changes: 16 additions & 0 deletions app/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,15 @@ func (m *Model) filterMatches() []string {
return filteredItems
}

func recordUse(timeStamp string) {
if timeStamp == "" {
return
}
if err := config.RecordUse(timeStamp); err != nil {
utils.LogERROR(fmt.Sprintf("failed to record frecency use: %s", err))
}
}

func (m Model) handleChooseOperation(i item, cmds []tea.Cmd) (Model, []tea.Cmd, bool) {
selectedItems := m.selectedItems()
if len(selectedItems) < 1 {
Expand All @@ -563,6 +572,8 @@ func (m Model) handleChooseOperation(i item, cmds []tea.Cmd) (Model, []tea.Cmd,
display.DisplayServer.CopyText(i.titleFull)
}

recordUse(i.timeStamp)

if KeepEnabled {
cmds = append(
cmds,
Expand All @@ -583,6 +594,11 @@ func (m Model) handleChooseOperation(i item, cmds []tea.Cmd) (Model, []tea.Cmd,

display.DisplayServer.CopyText(yank)

recordUse(i.timeStamp)
for _, item := range selectedItems {
recordUse(item.TimeStamp)
}

if KeepEnabled {
statusMsg := "Copied to clipboard: *selected items*"
display.DisplayServer.CopyText(yank)
Expand Down
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ type Config struct {
AutoPaste AutoPaste `json:"autoPaste"`
EnableMouse bool `json:"enableMouse"`
EnableDescription bool `json:"enableDescription"`
Search SearchConfig `json:"search"`
}

type SearchConfig struct {
Engine string `json:"engine"`
Algo string `json:"algo"`
CaseSensitivity string `json:"caseSensitivity"`
Normalize bool `json:"normalize"`
Tiebreak []string `json:"tiebreak"`
}

type AutoPaste struct {
Expand Down
11 changes: 11 additions & 0 deletions config/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,16 @@ func defaultConfig() Config {
Keybind: defaultAutoPasteKeyBind,
Buffer: defaultAutoPasteBuffer,
},
Search: defaultSearchConfig(),
}
}

func defaultSearchConfig() SearchConfig {
return SearchConfig{
Engine: "default",
Algo: "v2",
CaseSensitivity: "smart",
Normalize: true,
Tiebreak: []string{"score", "frecency", "index"},
}
}
14 changes: 14 additions & 0 deletions config/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type ClipboardItem struct {
Recorded string `json:"recorded"`
FilePath string `json:"filePath"`
Pinned bool `json:"pinned"`
UseCount int `json:"useCount,omitempty"`
LastUsed string `json:"lastUsed,omitempty"`
}

type ClipboardHistory struct {
Expand Down Expand Up @@ -335,6 +337,18 @@ func TogglePinClipboardItem(timeStamp string) (bool, error) {
return pinned, nil
}

func RecordUse(timeStamp string) error {
data := fileContents()
for i, item := range data.ClipboardHistory {
if item.Recorded == timeStamp {
data.ClipboardHistory[i].UseCount = item.UseCount + 1
data.ClipboardHistory[i].LastUsed = utils.GetTime()
return WriteUpdate(data)
}
}
return nil
}

func SanitizeHistory() error {
data := fileContents()
newData := ClipboardHistory{}
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-vgo/robotgo v1.0.0
github.com/junegunn/fzf v0.71.0
github.com/mitchellh/go-ps v1.0.0
gopkg.in/bendahl/uinput.v1 v1.2.0
)
Expand All @@ -20,11 +22,11 @@ require (
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gen2brain/shm v0.1.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.2.0 // indirect
github.com/jezek/xgb v1.2.0 // indirect
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/otiai10/gosseract/v2 v2.4.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/jezek/xgb v1.2.0 h1:LzgkD11wOrPnxXEqo588cnjUt4NwMHrFh/tgajo50Q0=
github.com/jezek/xgb v1.2.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/junegunn/fzf v0.71.0 h1:vPmJH1MUlysSczjn2HZ6+6KSMe8HxXUy323g9x9RmUQ=
github.com/junegunn/fzf v0.71.0/go.mod h1:xlXX2/rmsccKQUnr9QOXPDi5DyV9cM0UjKy/huScBeE=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand Down
Loading