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>
@@ -1,28 +1,55 @@
|
||||
# Bookmarks
|
||||
|
||||
A bookmarks/shortcuts plugin for Noctalia. You can add folders and commands to a list of bookmarks.
|
||||
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
|
||||
|
||||
| Field | Value |
|
||||
| ------- | ---------------------------------- |
|
||||
| ID | `dunarand/bookmarks` |
|
||||
| Entries | Bar widget: `bar`; panel: `panel`; |
|
||||
|
||||
Bar widget `bar` is not required. Instead, you can assign a keybind to directly open the `panel`,
|
||||
which is shown in the Usage section.
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------- |
|
||||
| ID | `dunarand/bookmarks` |
|
||||
| Entries | Bar widget: `bar`; panel: `panel`; Launcher provider: `provider`; |
|
||||
| Launcher Prefix | `/bk` |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Noctalia v5.0.0 or higher
|
||||
- `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
|
||||
|
||||
You can add the widget to your bar or assign the following command to your compositor keybinds. For
|
||||
example, in Hyprland v0.55+
|
||||
The plugin ships a bar widget, a panel, and a launcher provider. The bar widget just launches the
|
||||
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(
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
- "?" button shows tooltips: bookmark command and its description.
|
||||
|
||||

|
||||
|
||||
- You can create or edit bookmarks by assigning them a glyph, a label, a command, and an
|
||||
optional description.
|
||||
|
||||

|
||||

|
||||
|
||||
- "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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
This is a setting that you can disable.
|
||||
|
||||

|
||||

|
||||
|
||||
- Each bookmark can be edited anytime.
|
||||
- You can also use your launcher to query your bookmarks with the `/bk` prefix.
|
||||
|
||||

|
||||

|
||||
- 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`.
|
||||
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
|
||||
**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
|
||||
|
||||
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:
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ------------------ | ------ | ------- | ------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| Setting | Type | Default | Description |
|
||||
| -------------------- | -------- | ----------- | ------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `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
|
||||
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
|
||||
```
|
||||
- Added `/bk` launcher provider
|
||||
- Changed the following keybinds (check IPC & Keybinds section for what they do)
|
||||
- CTRL + N changed to CTRL + J
|
||||
- CTRL + P changed to CTRL + K
|
||||
- CTRL + S changed to CTRL + F
|
||||
- Added the following keybinds (check IPC & Keybinds section for what they do)
|
||||
- CTRL + H or Left Arrow
|
||||
- CTRL + L or Right Arrow
|
||||
- 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
|
||||
|
||||
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 277 KiB |
@@ -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
|
||||
@@ -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
|
||||
|
||||
-- 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 draftGlyph = "bookmark"
|
||||
local draftLabel = ""
|
||||
@@ -689,9 +720,10 @@ local function listView()
|
||||
})
|
||||
)
|
||||
else
|
||||
local startIndex, endIndex = visibleWindow(#results, selectedIndex)
|
||||
local rows = {}
|
||||
for rowIndex, result in ipairs(results) do
|
||||
table.insert(rows, searchResultRow(result, rowIndex))
|
||||
for rowIndex = startIndex, endIndex do
|
||||
table.insert(rows, searchResultRow(results[rowIndex], rowIndex))
|
||||
end
|
||||
table.insert(children, ui.scroll({ flexGrow = 1, gap = 2 }, rows))
|
||||
end
|
||||
@@ -762,15 +794,20 @@ local function listView()
|
||||
})
|
||||
)
|
||||
else
|
||||
local rows = {}
|
||||
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
|
||||
table.insert(rows, insertionGap(index, "before"))
|
||||
end
|
||||
table.insert(rows, entryRow(entry, index))
|
||||
table.insert(rows, entryRow(list[index], index))
|
||||
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"))
|
||||
end
|
||||
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
|
||||
return
|
||||
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),
|
||||
-- so `index` here is always an index into the root `bookmarks` array.
|
||||
currentFolder = index
|
||||
selectedIndex = 1
|
||||
selectedIndex = recallSelection(currentFolder, #(entry.items or {}))
|
||||
render()
|
||||
end
|
||||
|
||||
function _onBackToRoot()
|
||||
-- Remember where we were inside the folder before leaving it.
|
||||
rememberSelection(currentFolder)
|
||||
currentFolder = nil
|
||||
selectedIndex = 1
|
||||
selectedIndex = recallSelection(currentFolder, #bookmarks)
|
||||
render()
|
||||
end
|
||||
|
||||
@@ -1043,7 +1084,11 @@ function _onDeleteFolder()
|
||||
noctalia.tr("title"),
|
||||
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
|
||||
selectedIndex = recallSelection(currentFolder, #bookmarks)
|
||||
render()
|
||||
end
|
||||
|
||||
@@ -1309,10 +1354,14 @@ end
|
||||
-- `noctalia msg panel-open dunarand/bookmarks:panel search`, or any other
|
||||
-- IPC caller) open the panel straight into a focused search box, turning
|
||||
-- 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.
|
||||
--
|
||||
-- To open in search mode and close from a single keybind:
|
||||
-- noctalia msg panel-toggle dunarand/bookmarks:panel search
|
||||
function onOpen(context)
|
||||
view = "list"
|
||||
|
||||
editingIndex = nil
|
||||
editingRoot = false
|
||||
editMode = false
|
||||
@@ -1320,6 +1369,7 @@ function onOpen(context)
|
||||
searchQuery = ""
|
||||
searchRev += 1
|
||||
selectedIndex = 1
|
||||
rememberedSelection = {}
|
||||
loadBookmarks()
|
||||
noctalia.state.set("bookmarks_open", true)
|
||||
if context == "search" then
|
||||
@@ -1332,12 +1382,18 @@ function onClose()
|
||||
noctalia.state.set("bookmarks_open", false)
|
||||
end
|
||||
|
||||
-- Keyboard navigation: ctrl+n/down and ctrl+p/up move the selection through
|
||||
-- the currently visible rows (search results or the active folder's list),
|
||||
-- looping past either end; Return activates whatever is selected; ctrl+s
|
||||
-- jumps focus to the search box (root view only, since that's the only
|
||||
-- place it's rendered). Only acts on key-down (pressed == true) so each
|
||||
-- chord fires once per press.
|
||||
-- Keyboard navigation:
|
||||
-- - ctrl+j/down and ctrl+k/up move the selection through the currently
|
||||
-- visible rows (search results or the active folder's contents),
|
||||
-- wrapping past either end.
|
||||
-- - ctrl+l/right enters the selected folder, if any.
|
||||
-- - 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)
|
||||
if not pressed then
|
||||
return
|
||||
@@ -1346,7 +1402,7 @@ function onKey(chord, pressed)
|
||||
return
|
||||
end
|
||||
|
||||
if chord == "ctrl+s" then
|
||||
if chord == "ctrl+f" then
|
||||
if currentFolder == nil then
|
||||
searchRev += 1
|
||||
focusSearchOnRender = true
|
||||
@@ -1355,6 +1411,18 @@ function onKey(chord, pressed)
|
||||
return
|
||||
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 count = #rows
|
||||
if count == 0 then
|
||||
@@ -1362,21 +1430,28 @@ function onKey(chord, pressed)
|
||||
return
|
||||
end
|
||||
|
||||
if chord == "ctrl+n" or chord == "down" then
|
||||
if chord == "ctrl+j" or chord == "down" then
|
||||
if selectedIndex == nil then
|
||||
selectedIndex = 1
|
||||
else
|
||||
selectedIndex = selectedIndex % count + 1
|
||||
end
|
||||
render()
|
||||
elseif chord == "ctrl+p" or chord == "up" then
|
||||
elseif chord == "ctrl+k" or chord == "up" then
|
||||
if selectedIndex == nil then
|
||||
selectedIndex = count
|
||||
else
|
||||
selectedIndex = (selectedIndex - 2) % count + 1
|
||||
end
|
||||
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]
|
||||
activateRow(row)
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id = "dunarand/bookmarks"
|
||||
name = "Bookmarks"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
plugin_api = 13
|
||||
author = "dunarand"
|
||||
license = "MIT"
|
||||
@@ -49,6 +49,37 @@ entry = "bar.luau"
|
||||
description_key = "settings.text.description"
|
||||
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]]
|
||||
id = "panel"
|
||||
entry = "panel.luau"
|
||||
@@ -57,4 +88,4 @@ height = 470
|
||||
placement = "attached"
|
||||
position = "auto"
|
||||
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"]
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 51 KiB |
@@ -40,10 +40,22 @@
|
||||
"save_button": "Save",
|
||||
"search_placeholder": "Search bookmarks...",
|
||||
"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": {
|
||||
"description": "Where data.json is stored. Leave empty to use the default plugin data directory.",
|
||||
"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": {
|
||||
"description": "Bar widget's glyph",
|
||||
"label": "Glyph"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"err_save_failed": "Kaydedilemedi: {error}",
|
||||
"folder_deleted": "\"{label}\" klasörü ve içeriği silindi",
|
||||
"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",
|
||||
"info_cmd_line": "Komut: {cmd}",
|
||||
"info_description_line": "Açıklama: {description}",
|
||||
@@ -40,24 +40,32 @@
|
||||
"save_button": "Kaydet",
|
||||
"search_placeholder": "Yer imlerinde ara...",
|
||||
"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": {
|
||||
"description": "data.json dosyasının saklandığı konum. Varsayılan eklenti veri konumunu kullanmak için boş bırakın.",
|
||||
"label": "Veri dosyası"
|
||||
},
|
||||
"glyph": {
|
||||
"description": "Bar aracının sembolü (ör. bookmark)",
|
||||
"label": "Sembol"
|
||||
"description": "Çubuk aracının simgesi (ör. bookmark)",
|
||||
"label": "Simge"
|
||||
},
|
||||
"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.",
|
||||
"label": "Bilgi butonunu göster"
|
||||
},
|
||||
"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ı"
|
||||
},
|
||||
"tooltip": {
|
||||
"description": "Bar aracının isim etiketi",
|
||||
"description": "Çubuk aracının isim etiketi",
|
||||
"label": "İsim etiketi"
|
||||
}
|
||||
},
|
||||
|
||||