bookmarks-v1.3 feat(add /bk launcher provider) (#165)

* feat: changed keybinds + new keybinds

Changes:

- ctrl+n -> ctrl+j
- ctrl+p -> ctrl+k

Additions:

- ctrl+h or left: return to the root level when in a folder
- ctrl+l or right: get into a folder

* feat: /bk Provider

- feat: /bk Provider
- chore: better preview imgs
- docs: updated README
- chore: updated Turkish translation strings
- chore: updated thumbnail

* feat: keybind changes + index tracking fix

- feat: Some more keybind changes
    - Added: ctrl + n for new bookmark
    - Changed: ctrl + f for search (changed from ctrl + s)
- fix: Added index tracking for bookmark entries
    - For keyboard-centric use, navigating to a folder by accident then
returning to root was annoying since no tracking meant that we'd always
land on the first entry within the folder or the root itself
- docs: updated README to reflected the changes and added a changelog
section

* fix translation merge

---------

Co-authored-by: Lemmy <studio@quadbyte.net>
This commit is contained in:
mgu
2026-07-30 21:22:53 -04:00
committed by GitHub
co-authored by Lemmy
parent 8cd39525e2
commit dabe695a93
13 changed files with 572 additions and 74 deletions
+128 -48
View File
@@ -1,28 +1,55 @@
# Bookmarks # Bookmarks
A bookmarks/shortcuts plugin for Noctalia. You can add folders and commands to a list of bookmarks. ![](./thumbnail.webp)
Reorder bookmarks, categorize them with folders, set glyphs and labels, and simply left click to
execute. All of it is user defined, stored in a JSON file you can backup. A user-managed bookmarks plugin for Noctalia. You can add bookmarks by selecting a glyph, a label,
and a shell command to execute. You can backup your bookmarks as well!
## Plugin ## Plugin
| Field | Value | | Field | Value |
| ------- | ---------------------------------- | | --------------- | ----------------------------------------------------------------- |
| ID | `dunarand/bookmarks` | | ID | `dunarand/bookmarks` |
| Entries | Bar widget: `bar`; panel: `panel`; | | Entries | Bar widget: `bar`; panel: `panel`; Launcher provider: `provider`; |
| Launcher Prefix | `/bk` |
Bar widget `bar` is not required. Instead, you can assign a keybind to directly open the `panel`,
which is shown in the Usage section.
## Requirements ## Requirements
- Noctalia v5.0.0 or higher - Noctalia v5.0.0 or higher
- `nohup` (Optional): For "Run in background" wrapper toggle - `nohup` (Optional): For "Run in background" wrapper toggle
## IPC & Keybinds
1. Open the bookmarks panel:
```sh
noctalia msg panel-toggle dunarand/bookmarks:panel
```
2. Open the bookmarks panel in search mode (immediately puts you into search mode) so that you can
use your bookmarks panel as a launcher:
```sh
noctalia msg panel-toggle dunarand/bookmarks:panel search
```
You can interact with the bookmarks list via keybinds:
| Keybind | Purpose |
| ------------------------- | ---------------------------------------------------------------- |
| `CTRL + J` or Down Arrow | Select the next (below) item |
| `CTRL + K` or Up Arrow | Select the previous (above) item |
| `CTRL + H` or Left Arrow | Return to the root level when in a folder |
| `CTRL + L` or Right Arrow | Get into a folder |
| `CTRL + F` | Search bookmarks |
| `CTRL + N` | New bookmark |
| `Enter` / `Return` | Execute the selected bookmark's command / Navigate into a folder |
## Usage ## Usage
You can add the widget to your bar or assign the following command to your compositor keybinds. For The plugin ships a bar widget, a panel, and a launcher provider. The bar widget just launches the
example, in Hyprland v0.55+ panel. The panel is able to be toggled via IPC. For example, in Hyprland v0.55 or higher, you can
assign it to a keybind as follows:
``` ```
hl.bind( hl.bind(
@@ -31,27 +58,14 @@ hl.bind(
) )
``` ```
You can interact with the bookmarks list via keybinds:
| Keybind | Purpose |
| ------------------------ | ---------------------------------------------------------------- |
| `CTRL + N` or Down Arrow | Select the next (below) item |
| `CTRL + P` or Up Arrow | Select the previous (above) item |
| `CTRL + S` | Search bookmarks |
| `Enter` / `Return` | Execute the selected bookmark's command / Navigate into a folder |
- Bookmarks and folders are listed in the main panel. - Bookmarks and folders are listed in the main panel.
![](./assets/preview-1.png) ![](./assets/preview-1.png)
- "?" button shows tooltips: bookmark command and its description.
![](./assets/preview-2.png)
- You can create or edit bookmarks by assigning them a glyph, a label, a command, and an - You can create or edit bookmarks by assigning them a glyph, a label, a command, and an
optional description. optional description.
![](./assets/preview-3.png) ![](./assets/preview-2.png)
- "Run in background" toggle wraps the command you defined in the following way: - "Run in background" toggle wraps the command you defined in the following way:
@@ -70,24 +84,86 @@ You can interact with the bookmarks list via keybinds:
- You can create folders and nest other bookmarks within folders. - You can create folders and nest other bookmarks within folders.
![](./assets/preview-4.png)
Folders cannot nest other folders. This is by design and it'll not change unless I find a Folders cannot nest other folders. This is by design and it'll not change unless I find a
genuine usecase. You can edit folders by clicking on the "pen" icon next to its name. genuine use case. You can edit folders by clicking on the "pen" icon next to its name.
- You can press the "eye" icon to enter edit mode where you can edit, delete, and reorder bookmarks. - You can press the "eye" icon to enter edit mode where you can edit, delete, and reorder bookmarks.
This is a setting that you can disable.
![](./assets/preview-5.png) ![](./assets/preview-3.png)
- Each bookmark can be edited anytime. - You can also use your launcher to query your bookmarks with the `/bk` prefix.
![](./assets/preview-6.png) ![](./assets/preview-4.png)
- Queries are folder-aware, meaning if you nested a bookmark inside a folder, the folder name will
be displayed as well.
## Bookmarks Data
The saved bookmarks are written to `$NOCTALIA_STATE_HOME/plugins/data/dunarand/bookmarks/data.json`. The saved bookmarks are written to `$NOCTALIA_STATE_HOME/plugins/data/dunarand/bookmarks/data.json`.
By default, `$NOCTALIA_STATE_HOME` should point to `~/.local/state/noctalia`. You can point to a By default, `$NOCTALIA_STATE_HOME` should point to `~/.local/state/noctalia`. You can point to a
different location for saving and backing up your bookmarks. This setting is configurable via different location for saving and backing up your bookmarks. This setting is configurable via
**plugin settings** under Settings -> Plugins. Only JSON format is accepted. **plugin settings** under Settings -> Plugins. Only JSON format is accepted.
You can manage the bookmark data via external scripts.
**Root**
```
[ <entry>, <entry>]
```
The root is a JSON array of entries. Order in the array is display order (used directly by drag-and
drop reordering).
**Entry**
Two types of entries exist: `bookmark` and `folder`.
### Bookmarks
```JSON
{
"type": "bookmark",
"glyph": "bookmark",
"label": "My Bookmark",
"cmd": "firefox",
"description": "Opens Firefox",
"runInBackground": false,
"runInTerminal": false
}
```
| Key | Type | Required / Default | Notes |
| ------------------------------------------------- | ------- | ------------------------------------------- | ----------------------------------------------- |
| `type` | string | `"bookmark"` | |
| `glyph` | string | falls back to `"bookmark"` if empty/missing | |
| `label` | string | required, enforced at save time | `""` fails validation |
| `cmd` | string | required for bookmarks | shell command to execute, enforced at save time |
| `description` | string | optional, defaults to `""` | shown in the info tooltip if enabled |
| `runInBackground` | boolean | optional, defaults to `false` | mutually exclusive with `runInTerminal` in |
| the UI but the schema itself doesn't enforce that |
| `runInTerminal` | boolean | optional, defaults to `false` |
### Folders
```JSON
{
"type": "folder",
"glyph": "folder",
"label": "My Folder",
"items": [ <bookmark>, <bookmark>, ... ]
}
```
| Key | Type | Required/Default | Notes |
| ------------------------------------ | ------ | ----------------------------------------------------------------------- | ---------- |
| `type` | string | `"folder"` |
| `glyph` | string | falls back to `"folder"` if empty/missing |
| `label` | string | required |
| `items` | array | optional, treated as `{}` if missing, `folder.items=folder.items or {}` | contents — |
| bookmarks only, one level of nesting |
## Settings ## Settings
The bar widget has the following settings: The bar widget has the following settings:
@@ -100,22 +176,26 @@ The bar widget has the following settings:
The plugin itself has the following settings: The plugin itself has the following settings:
| Setting | Type | Default | Description | | Setting | Type | Default | Description |
| ------------------ | ------ | ------- | ------------------------------------------------------------------------------------- | | -------------------- | -------- | ----------- | ------------------------------------------------------------------------------------- |
| `data_path` | `file` | | data.json file to store the saved bookmarks. Leave empty to use the default location. | | `data_path` | `file` | | data.json file to store the saved bookmarks. Leave empty to use the default location. |
| `show_info_button` | `bool` | `true` | Shows the "?" tooltip button on the bookmark entries. | | `show_info_button` | `bool` | `true` | Shows the "?" tooltip button on the bookmark entries. |
| `enable_bk_provider` | `bool` | `true` | Enable bookmarks launcher provider |
| `bk_sort_by` | `select` | `"history"` | Provider's sorting strategy. Options: `"history"`, `"usage_count"` |
## IPC ## Changelog
1. Open the bookmarks panel: ### v1.3.0
```sh - Added `/bk` launcher provider
noctalia msg panel-toggle dunarand/bookmarks:panel - Changed the following keybinds (check IPC & Keybinds section for what they do)
``` - CTRL + N changed to CTRL + J
- CTRL + P changed to CTRL + K
2. Open the bookmarks panel in search mode (immediately puts you into search mode) so that you can - CTRL + S changed to CTRL + F
use your bookmarks panel as a launcher: - Added the following keybinds (check IPC & Keybinds section for what they do)
- CTRL + H or Left Arrow
```sh - CTRL + L or Right Arrow
noctalia msg panel-toggle dunarand/bookmarks:panel search - CTRL + N
``` - Index tracking on root level for keyboard-centric usage
- Previously, when using a keyboard to navigate, going into a folder would reset the selected
entry's index to 1 causing fat fingers to be annoying
Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

+292
View File
@@ -0,0 +1,292 @@
-- Launcher provider: "/bk" fuzzy-searches bookmark labels (root + one level
-- of folders, bookmarks only - same scope as the panel's own search) and
-- runs the selected one on activation. Reads the same data.json the panel
-- writes, resolved the same way (configured data_path, else the plugin's
-- data dir), so no state is shared beyond the file on disk.
local function resolveDataPath()
local configured = noctalia.getConfig("data_path")
if type(configured) == "string" and noctalia.string.trim(configured) ~= "" then
return noctalia.expandPath(noctalia.string.trim(configured)), nil
end
local dataDir, dataDirErr = noctalia.pluginDataDir()
if dataDir == nil then
return nil, dataDirErr
end
return dataDir .. "/data.json", nil
end
local function entryType(entry)
return entry.type == "folder" and "folder" or "bookmark"
end
-- Reads and decodes data.json fresh on every query. The panel is the only
-- writer and bookmark lists are small, so there's no need to cache across
-- queries here.
local function loadBookmarks()
local path, pathErr = resolveDataPath()
if path == nil then
noctalia.log("bookmarks-provider: could not resolve data path: " .. tostring(pathErr))
return {}
end
if not noctalia.fileExists(path) then
return {}
end
local raw, readErr = noctalia.readFile(path)
if raw == nil then
noctalia.log("bookmarks-provider: failed to read data file: " .. tostring(readErr))
return {}
end
local decoded, decodeErr = noctalia.json.decode(raw)
if type(decoded) ~= "table" then
noctalia.log("bookmarks-provider: data file is corrupt: " .. tostring(decodeErr))
return {}
end
return decoded
end
-- Flat list of { entry, path } across root and one level of folders,
-- bookmarks only (folders themselves aren't launchable results).
local function flatten(bookmarks)
local flat = {}
for _, entry in ipairs(bookmarks) do
if entryType(entry) == "folder" then
for _, subEntry in ipairs(entry.items or {}) do
if entryType(subEntry) ~= "folder" then
table.insert(flat, { entry = subEntry, path = entry.label or "" })
end
end
else
table.insert(flat, { entry = entry, path = nil })
end
end
return flat
end
-- Bookmarks are indexed by a stable synthetic id (path-qualified label),
-- rather than by array position, since onActivate only receives the id
-- string and the list is re-read from disk on every query.
local function resultId(item)
return (item.path or "") .. "\30" .. (item.entry.label or "")
end
local function buildResults(query)
local bookmarks = loadBookmarks()
local flat = flatten(bookmarks)
local results = {}
for _, item in ipairs(flat) do
local score
if query == "" then
score = 0
else
score = noctalia.fuzzyScore(query, item.entry.label or "")
end
if score ~= nil then
table.insert(results, {
id = resultId(item),
title = item.entry.label or "",
subtitle = item.path,
glyph = item.entry.glyph or "bookmark",
score = score,
})
end
end
table.sort(results, function(a, b)
return (a.score or 0) > (b.score or 0)
end)
return results
end
local function providerEnabled()
return noctalia.getConfig("enable_bk_provider") == true
end
-- "history" (most-recently-used first) or "usage_count" (most-frequently-
-- used first). Only affects ordering when the query is empty; a non-empty
-- query always ranks by fuzzy match score, same as before.
local function sortBy()
local value = noctalia.getConfig("bk_sort_by")
if value == "usage_count" then
return "usage_count"
end
return "history"
end
local function statsPath()
local dataDir = noctalia.pluginDataDir()
if dataDir == nil then
return nil
end
return dataDir .. (sortBy() == "history" and "/bk_history.json" or "/bk_usage.json")
end
-- Reads and decodes the current stats file (history array or usage-count
-- map, per sortBy()), falling back to an empty table when the file is
-- missing, unreadable, or corrupt. Shared by getSortScores/recordActivation
-- so the read/decode/default boilerplate only lives in one place.
local function readStatsData()
local path = statsPath()
if path == nil then
return {}, nil
end
local file = noctalia.readFile(path)
if file == nil then
return {}, path
end
local decoded = noctalia.json.decode(file)
if type(decoded) ~= "table" then
return {}, path
end
return decoded, path
end
-- Scores used to order results when the query is empty, keyed by result id.
-- History ranks by recency (higher = more recently used); usage_count ranks
-- by frequency (higher = used more often). Reads/decodes the stats file
-- once for the whole batch rather than once per result. Ids missing from
-- the stats file fall back to 0 (unranked, sorted last).
local function getSortScores(ids)
local data = readStatsData()
local scores = {}
if sortBy() == "history" then
for _, id in ipairs(ids) do
local i = table.find(data, id)
scores[id] = i ~= nil and (#data - i) or 0
end
else
for _, id in ipairs(ids) do
scores[id] = data[id] or 0
end
end
return scores
end
-- Records an activation for ranking purposes: pushes `id` to the front of
-- the history list, or increments its usage count, depending on `sort_by`.
local function recordActivation(id)
local data, path = readStatsData()
if path == nil then
return
end
if sortBy() == "history" then
local i = table.find(data, id)
if i ~= nil then
table.remove(data, i)
end
table.insert(data, 1, id)
else
data[id] = (data[id] or 0) + 1
end
local encoded = noctalia.json.encode(data)
if encoded ~= nil then
noctalia.writeFile(path, encoded)
end
end
function onQuery(query)
query = noctalia.string.trim(query)
if not providerEnabled() then
launcher.setResults(query, {})
return
end
local results = buildResults(query)
if query == "" then
local ids = {}
for _, result in ipairs(results) do
table.insert(ids, result.id)
end
local scores = getSortScores(ids)
for _, result in ipairs(results) do
result.score = scores[result.id] or 0
end
table.sort(results, function(a, b)
return (a.score or 0) > (b.score or 0)
end)
end
if #results == 0 then
launcher.setResults(query, {
{
id = "",
title = noctalia.tr("no_bookmarks"),
glyph = "bookmark-off",
},
})
return
end
launcher.setResults(query, results)
end
function onActivate(id)
if id == "" or not providerEnabled() then
return
end
local wantPath, wantLabel = id:match("^([^\30]*)\30(.*)$")
if wantLabel == nil then
return
end
if wantPath == "" then
wantPath = nil
end
local bookmarks = loadBookmarks()
for _, item in ipairs(flatten(bookmarks)) do
if item.entry.label == wantLabel and (item.path or nil) == wantPath then
local entry = item.entry
local cmd = entry.cmd
if type(cmd) ~= "string" or cmd == "" then
noctalia.notifyError(noctalia.tr("title"), noctalia.tr("err_no_command"))
return
end
local label = entry.label or cmd
recordActivation(id)
if entry.runInTerminal == true then
noctalia.runInTerminal(cmd)
return
end
local finalCmd = cmd
if entry.runInBackground == true then
finalCmd = "nohup " .. cmd .. " >/dev/null 2>&1 &"
end
local ok = noctalia.runAsync(finalCmd, function(result)
if entry.runInBackground == true then
return
end
if result.exitCode ~= 0 then
local detail = noctalia.string.trim(result.stderr or "")
local msg = noctalia.tr(
"err_exit_code",
{ label = label, code = tostring(result.exitCode) }
)
if detail ~= "" then
msg = msg .. ": " .. detail
end
noctalia.notifyError(noctalia.tr("title"), msg)
end
end)
if not ok then
noctalia.notifyError(
noctalia.tr("title"),
noctalia.tr("err_launch_failed", { label = label })
)
end
return
end
end
end
+94 -19
View File
@@ -33,6 +33,37 @@ local focusSearchOnRender = false -- one-shot: grabs the search input on the nex
local selectedIndex = nil -- 1-based position within the currently visible list, or nil local selectedIndex = nil -- 1-based position within the currently visible list, or nil
-- Last selected index per scope, keyed by currentFolder (0 for root, since
-- folder indices start at 1). Restores position when re-entering a folder
-- or coming back to root instead of resetting to 1.
local rememberedSelection = {}
local function rememberSelection(folder)
rememberedSelection[folder or 0] = selectedIndex
end
local function recallSelection(folder, rowCount)
if rowCount == nil or rowCount <= 0 then
return nil
end
return math.min(math.max(rememberedSelection[folder or 0] or 1, 1), rowCount)
end
-- ui.scroll has no scroll-into-view API, so instead of scrolling to the
-- selection we only render a window of rows around it - the selection is
-- then always inside what's drawn. ROWS_PER_SCREEN is a rough fit for the
-- panel's height; doesn't need to be exact.
local ROWS_PER_SCREEN = 9
local function visibleWindow(count, selected)
if count <= ROWS_PER_SCREEN or selected == nil then
return 1, math.min(count, ROWS_PER_SCREEN)
end
local startIndex =
math.max(1, math.min(selected - ROWS_PER_SCREEN // 2, count - ROWS_PER_SCREEN + 1))
return startIndex, startIndex + ROWS_PER_SCREEN - 1
end
local draftKind = "bookmark" -- "bookmark" | "folder", set when the form opens local draftKind = "bookmark" -- "bookmark" | "folder", set when the form opens
local draftGlyph = "bookmark" local draftGlyph = "bookmark"
local draftLabel = "" local draftLabel = ""
@@ -689,9 +720,10 @@ local function listView()
}) })
) )
else else
local startIndex, endIndex = visibleWindow(#results, selectedIndex)
local rows = {} local rows = {}
for rowIndex, result in ipairs(results) do for rowIndex = startIndex, endIndex do
table.insert(rows, searchResultRow(result, rowIndex)) table.insert(rows, searchResultRow(results[rowIndex], rowIndex))
end end
table.insert(children, ui.scroll({ flexGrow = 1, gap = 2 }, rows)) table.insert(children, ui.scroll({ flexGrow = 1, gap = 2 }, rows))
end end
@@ -762,15 +794,20 @@ local function listView()
}) })
) )
else else
local rows = {}
local count = #list local count = #list
for index, entry in ipairs(list) do local startIndex, endIndex = visibleWindow(count, selectedIndex)
local rows = {}
for index = startIndex, endIndex do
if editMode then if editMode then
table.insert(rows, insertionGap(index, "before")) table.insert(rows, insertionGap(index, "before"))
end end
table.insert(rows, entryRow(entry, index)) table.insert(rows, entryRow(list[index], index))
end end
if editMode and count > 0 then -- The trailing "after last row" gap only belongs at the very end of
-- the real list, not at the end of a windowed slice that's cut off
-- partway through - an "after" gap mid-list would duplicate the
-- "before" gap of the row right after the window.
if editMode and count > 0 and endIndex == count then
table.insert(rows, insertionGap(count, "after")) table.insert(rows, insertionGap(count, "after"))
end end
table.insert(children, ui.scroll({ flexGrow = 1, gap = 2 }, rows)) table.insert(children, ui.scroll({ flexGrow = 1, gap = 2 }, rows))
@@ -976,16 +1013,20 @@ function _onOpenFolder(index)
if entry == nil or entryType(entry) ~= "folder" then if entry == nil or entryType(entry) ~= "folder" then
return return
end end
-- Remember where we were in root before diving into the folder.
rememberSelection(currentFolder)
-- currentFolder is only ever set from root (folders are one level deep), -- currentFolder is only ever set from root (folders are one level deep),
-- so `index` here is always an index into the root `bookmarks` array. -- so `index` here is always an index into the root `bookmarks` array.
currentFolder = index currentFolder = index
selectedIndex = 1 selectedIndex = recallSelection(currentFolder, #(entry.items or {}))
render() render()
end end
function _onBackToRoot() function _onBackToRoot()
-- Remember where we were inside the folder before leaving it.
rememberSelection(currentFolder)
currentFolder = nil currentFolder = nil
selectedIndex = 1 selectedIndex = recallSelection(currentFolder, #bookmarks)
render() render()
end end
@@ -1043,7 +1084,11 @@ function _onDeleteFolder()
noctalia.tr("title"), noctalia.tr("title"),
noctalia.tr("folder_deleted", { label = removed.label or "" }) noctalia.tr("folder_deleted", { label = removed.label or "" })
) )
-- Folder selections are now stale (indices after the deleted one
-- shifted), so drop everything but root's.
rememberedSelection = { [0] = rememberedSelection[0] }
currentFolder = nil currentFolder = nil
selectedIndex = recallSelection(currentFolder, #bookmarks)
render() render()
end end
@@ -1309,10 +1354,14 @@ end
-- `noctalia msg panel-open dunarand/bookmarks:panel search`, or any other -- `noctalia msg panel-open dunarand/bookmarks:panel search`, or any other
-- IPC caller) open the panel straight into a focused search box, turning -- IPC caller) open the panel straight into a focused search box, turning
-- the bookmark list into a fast fuzzy launcher without a second keypress. -- the bookmark list into a fast fuzzy launcher without a second keypress.
-- Root-only, same as ctrl+s: searching already isn't offered inside a -- Root-only, same as ctrl+f: searching already isn't offered inside a
-- folder, so context is ignored there. -- folder, so context is ignored there.
--
-- To open in search mode and close from a single keybind:
-- noctalia msg panel-toggle dunarand/bookmarks:panel search
function onOpen(context) function onOpen(context)
view = "list" view = "list"
editingIndex = nil editingIndex = nil
editingRoot = false editingRoot = false
editMode = false editMode = false
@@ -1320,6 +1369,7 @@ function onOpen(context)
searchQuery = "" searchQuery = ""
searchRev += 1 searchRev += 1
selectedIndex = 1 selectedIndex = 1
rememberedSelection = {}
loadBookmarks() loadBookmarks()
noctalia.state.set("bookmarks_open", true) noctalia.state.set("bookmarks_open", true)
if context == "search" then if context == "search" then
@@ -1332,12 +1382,18 @@ function onClose()
noctalia.state.set("bookmarks_open", false) noctalia.state.set("bookmarks_open", false)
end end
-- Keyboard navigation: ctrl+n/down and ctrl+p/up move the selection through -- Keyboard navigation:
-- the currently visible rows (search results or the active folder's list), -- - ctrl+j/down and ctrl+k/up move the selection through the currently
-- looping past either end; Return activates whatever is selected; ctrl+s -- visible rows (search results or the active folder's contents),
-- jumps focus to the search box (root view only, since that's the only -- wrapping past either end.
-- place it's rendered). Only acts on key-down (pressed == true) so each -- - ctrl+l/right enters the selected folder, if any.
-- chord fires once per press. -- - ctrl+h/left returns from a folder to the root list.
-- - Return activates the selected row (opens a folder or runs a bookmark).
-- - ctrl+f focuses the search box (root view only, since folders don't
-- have one).
-- - ctrl+n opens the new-bookmark form.
-- root-only - same restriction as the "New Folder" button).
-- Only acts on key-down (pressed == true) so each chord fires once per press.
function onKey(chord, pressed) function onKey(chord, pressed)
if not pressed then if not pressed then
return return
@@ -1346,7 +1402,7 @@ function onKey(chord, pressed)
return return
end end
if chord == "ctrl+s" then if chord == "ctrl+f" then
if currentFolder == nil then if currentFolder == nil then
searchRev += 1 searchRev += 1
focusSearchOnRender = true focusSearchOnRender = true
@@ -1355,6 +1411,18 @@ function onKey(chord, pressed)
return return
end end
if chord == "ctrl+n" then
_onNewBookmark()
return
end
if chord == "ctrl+h" or chord == "left" then
if currentFolder ~= nil then
_onBackToRoot()
end
return
end
local rows = visibleRows() local rows = visibleRows()
local count = #rows local count = #rows
if count == 0 then if count == 0 then
@@ -1362,21 +1430,28 @@ function onKey(chord, pressed)
return return
end end
if chord == "ctrl+n" or chord == "down" then if chord == "ctrl+j" or chord == "down" then
if selectedIndex == nil then if selectedIndex == nil then
selectedIndex = 1 selectedIndex = 1
else else
selectedIndex = selectedIndex % count + 1 selectedIndex = selectedIndex % count + 1
end end
render() render()
elseif chord == "ctrl+p" or chord == "up" then elseif chord == "ctrl+k" or chord == "up" then
if selectedIndex == nil then if selectedIndex == nil then
selectedIndex = count selectedIndex = count
else else
selectedIndex = (selectedIndex - 2) % count + 1 selectedIndex = (selectedIndex - 2) % count + 1
end end
render() render()
elseif chord == "Return" then elseif chord == "ctrl+l" or chord == "right" then
local row = rows[selectedIndex]
if row ~= nil and row.kind == "entry" then
if entryType(row.entry) == "folder" then
_onOpenFolder(row.index)
end
end
elseif chord == "return" then
local row = rows[selectedIndex] local row = rows[selectedIndex]
activateRow(row) activateRow(row)
end end
+33 -2
View File
@@ -1,6 +1,6 @@
id = "dunarand/bookmarks" id = "dunarand/bookmarks"
name = "Bookmarks" name = "Bookmarks"
version = "1.2.1" version = "1.3.0"
plugin_api = 13 plugin_api = 13
author = "dunarand" author = "dunarand"
license = "MIT" license = "MIT"
@@ -49,6 +49,37 @@ entry = "bar.luau"
description_key = "settings.text.description" description_key = "settings.text.description"
default = "Bookmarks" default = "Bookmarks"
[[setting]]
key = "enable_bk_provider"
type = "bool"
label_key = "settings.enable_bk_provider.label"
description_key = "settings.enable_bk_provider.description"
default = true
[[setting]]
key = "bk_sort_by"
type = "select"
label_key = "settings.bk_sort_by.label"
description_key = "settings.bk_sort_by.description"
default = "history"
visible_when = { key = "enable_bk_provider", values = ["true"] }
[[setting.options]]
value = "history"
label_key = "settings.bk_sort_by.options.history"
[[setting.options]]
value = "usage_count"
label_key = "settings.bk_sort_by.options.usage_count"
[[launcher_provider]]
id = "provider"
entry = "bk_provider.luau"
prefix = "bk"
glyph = "bookmark"
include_in_global_search = false
debounce_ms = 100
[[panel]] [[panel]]
id = "panel" id = "panel"
entry = "panel.luau" entry = "panel.luau"
@@ -57,4 +88,4 @@ height = 470
placement = "attached" placement = "attached"
position = "auto" position = "auto"
open_near_click = true open_near_click = true
capture_keys = ["ctrl+n", "ctrl+p", "up", "down", "Return", "ctrl+s"] capture_keys = ["ctrl+h", "ctrl+j", "ctrl+k", "ctrl+l", "left", "down", "up", "right", "ctrl+n", "ctrl+f", "return"]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 51 KiB

+12
View File
@@ -40,10 +40,22 @@
"save_button": "Save", "save_button": "Save",
"search_placeholder": "Search bookmarks...", "search_placeholder": "Search bookmarks...",
"settings": { "settings": {
"bk_sort_by": {
"description": "How bookmarks are ordered in \"/bk\" results when there's no search text.",
"label": "/bk sort order",
"options": {
"history": "Recently used",
"usage_count": "Most used"
}
},
"data_path": { "data_path": {
"description": "Where data.json is stored. Leave empty to use the default plugin data directory.", "description": "Where data.json is stored. Leave empty to use the default plugin data directory.",
"label": "Data file" "label": "Data file"
}, },
"enable_bk_provider": {
"description": "Adds a \"/bk\" launcher command to fuzzy-search and run your bookmarks.",
"label": "Enable /bk launcher provider"
},
"glyph": { "glyph": {
"description": "Bar widget's glyph", "description": "Bar widget's glyph",
"label": "Glyph" "label": "Glyph"
+13 -5
View File
@@ -25,7 +25,7 @@
"err_save_failed": "Kaydedilemedi: {error}", "err_save_failed": "Kaydedilemedi: {error}",
"folder_deleted": "\"{label}\" klasörü ve içeriği silindi", "folder_deleted": "\"{label}\" klasörü ve içeriği silindi",
"folder_name_field": "Klasör ismi", "folder_name_field": "Klasör ismi",
"glyph_field": "Sembol (ör. bookmark)", "glyph_field": "Simge (ör. bookmark)",
"hide_controls_tooltip": "Sıralama/düzenleme/silme kontrollerini gizle", "hide_controls_tooltip": "Sıralama/düzenleme/silme kontrollerini gizle",
"info_cmd_line": "Komut: {cmd}", "info_cmd_line": "Komut: {cmd}",
"info_description_line": "Açıklama: {description}", "info_description_line": "Açıklama: {description}",
@@ -40,24 +40,32 @@
"save_button": "Kaydet", "save_button": "Kaydet",
"search_placeholder": "Yer imlerinde ara...", "search_placeholder": "Yer imlerinde ara...",
"settings": { "settings": {
"bk_sort_by": {
"description": "\"/bk\" sonuçlarının arama yapılmadığı durumda yer imlerini nasıl sıralacağını belirler.",
"label": "/bk sıralama önceliği",
"options": {
"history": "Son kullanılanlar",
"usage_count": "En sık kullanılanlar"
}
},
"data_path": { "data_path": {
"description": "data.json dosyasının saklandığı konum. Varsayılan eklenti veri konumunu kullanmak için boş bırakın.", "description": "data.json dosyasının saklandığı konum. Varsayılan eklenti veri konumunu kullanmak için boş bırakın.",
"label": "Veri dosyası" "label": "Veri dosyası"
}, },
"glyph": { "glyph": {
"description": "Bar aracının sembolü (ör. bookmark)", "description": "Çubuk aracının simgesi (ör. bookmark)",
"label": "Sembol" "label": "Simge"
}, },
"show_info_button": { "show_info_button": {
"description": "Yer imlerinde (klasörlerde değil) üzerine gelindiğinde açıklamalarını ve komutlarını görüntülemek için bir \"?\" düğmesi göster.", "description": "Yer imlerinde (klasörlerde değil) üzerine gelindiğinde açıklamalarını ve komutlarını görüntülemek için bir \"?\" düğmesi göster.",
"label": "Bilgi butonunu göster" "label": "Bilgi butonunu göster"
}, },
"text": { "text": {
"description": "Bar aracının etiketi. Barda sadece sembolün görünmesi için boş bırakın.", "description": "Çubuk aracının etiketi. Çubukta sadece simgenin görünmesi için boş bırakın.",
"label": "Yazı" "label": "Yazı"
}, },
"tooltip": { "tooltip": {
"description": "Bar aracının isim etiketi", "description": "Çubuk aracının isim etiketi",
"label": "İsim etiketi" "label": "İsim etiketi"
} }
}, },