feat: add tmux-provider plugin (#151)
* feat: add tmux-provider plugin * fix: invalid thumbnail name * fix: invalid translation keys * docs: README updated to include launcher provider entry Also fixed wording for requirements (validation failed for some reason?)
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
# tmux Provider
|
||||||
|
|
||||||
|
A launcher provider plugin for searching running tmux sessions, or tmuxp configurations, and
|
||||||
|
attaching the sessions.
|
||||||
|
|
||||||
|
## Plugin
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| --------------- | ----------------------------- |
|
||||||
|
| ID | `dunarand/tmux-provider` |
|
||||||
|
| Entry | Launcher provider: `provider` |
|
||||||
|
| Launcher Prefix | `/tm` |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Noctalia v5.0.0 or higher
|
||||||
|
- Requires `tmux`: [Install tmux](https://github.com/tmux/tmux/wiki/installing)
|
||||||
|
- `tmuxp`: Optional dependency, [install tmuxp](https://tmuxp.git-pull.com/)
|
||||||
|
|
||||||
|
tmuxp is completely optional. If you want tmuxp configurations to be included in the search, enable
|
||||||
|
tmuxp option in the plugin settings.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Simply open your launcher and type the session you're looking for after the `/tm` prefix.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Selecting an entry and pressing Return / Enter launches the default terminal and attaches the tmux
|
||||||
|
session.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
| ----------- | ------ | ------- | ---------------------------------------------------------- |
|
||||||
|
| `use_tmuxp` | `bool` | `false` | Enables tmuxp configurations to be included in the search. |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
**Limitations:**
|
||||||
|
|
||||||
|
- With the current implementation, we attach sessions via the following commands:
|
||||||
|
|
||||||
|
- **tmux:**
|
||||||
|
|
||||||
|
```
|
||||||
|
tmux attach -t <session>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **tmuxp:**
|
||||||
|
|
||||||
|
```
|
||||||
|
tmuxp load <session>
|
||||||
|
```
|
||||||
|
|
||||||
|
- The current implementation only utilizes tmuxp. Other tmux configuration tools such as tmuxinator
|
||||||
|
will be added in the future.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
id = "dunarand/tmux-provider"
|
||||||
|
name = "tmux Provider"
|
||||||
|
version = "0.1.0"
|
||||||
|
plugin_api = 3
|
||||||
|
author = "dunarand"
|
||||||
|
license = "MIT"
|
||||||
|
icon = "terminal"
|
||||||
|
description = "Find and attach tmux sessions from the launcher. Type /tm to list tmux sessions or tmuxp configurations."
|
||||||
|
tags = ["launcher", "productivity", "development"]
|
||||||
|
dependencies = ["tmux", "tmuxp"]
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "use_tmuxp"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.use_tmuxp.label"
|
||||||
|
description_key = "settings.use_tmuxp.description"
|
||||||
|
default = false
|
||||||
|
|
||||||
|
[[launcher_provider]]
|
||||||
|
id = "provider"
|
||||||
|
entry = "tmux_provider.luau"
|
||||||
|
prefix = "tm"
|
||||||
|
glyph = "terminal"
|
||||||
|
include_in_global_search = false
|
||||||
|
debounce_ms = 0
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,237 @@
|
|||||||
|
local tinsert = table.insert
|
||||||
|
local tsort = table.sort
|
||||||
|
local sformat = string.format
|
||||||
|
|
||||||
|
-- Per-query in-flight state: both tmux and tmuxp results must land before
|
||||||
|
-- we render, since either can be requested for the same query.
|
||||||
|
local sessionsCache
|
||||||
|
local tmuxpCache
|
||||||
|
local pendingQuery: string? = nil
|
||||||
|
local pendingTmux = false
|
||||||
|
local pendingTmuxp = false
|
||||||
|
|
||||||
|
local function tmuxInstalled(): boolean
|
||||||
|
return noctalia.commandExists("tmux")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tmuxpInstalled(): boolean
|
||||||
|
return noctalia.commandExists("tmuxp")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shellQuote(s: string): string
|
||||||
|
return "'" .. s:gsub("'", "'\"'\"'") .. "'"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parses `tmux ls` output into a list of { name, attached }
|
||||||
|
-- Format per line: "name: N windows (created ...) [(attached)]"
|
||||||
|
local function parseTmuxLs(output: string)
|
||||||
|
local sessions = {}
|
||||||
|
for line in output:gmatch("[^\r\n]+") do
|
||||||
|
local name = line:match("^([^:]+):")
|
||||||
|
if name then
|
||||||
|
local attached = line:find("%(attached%)") ~= nil
|
||||||
|
tinsert(sessions, { name = name, attached = attached })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return sessions
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parses `tmuxp ls --json` output into a list of { name, session_name }
|
||||||
|
local function parseTmuxpLs(output: string)
|
||||||
|
local configs = {}
|
||||||
|
|
||||||
|
local data, err = noctalia.json.decode(output)
|
||||||
|
if not data or not data.workspaces then
|
||||||
|
if err then
|
||||||
|
noctalia.log("tmux-provider: failed to parse tmuxp ls --json output: " .. tostring(err))
|
||||||
|
end
|
||||||
|
return configs
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, workspace in ipairs(data.workspaces) do
|
||||||
|
if workspace.name then
|
||||||
|
tinsert(configs, {
|
||||||
|
name = workspace.name,
|
||||||
|
session_name = workspace.session_name or workspace.name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return configs
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Builds the combined result list: running tmux sessions first, then tmuxp
|
||||||
|
-- configs that aren't already running as a live session.
|
||||||
|
local function buildResults(query: string)
|
||||||
|
local results = {}
|
||||||
|
local runningNames = {}
|
||||||
|
|
||||||
|
if sessionsCache then
|
||||||
|
for _, session in ipairs(sessionsCache) do
|
||||||
|
runningNames[session.name] = true
|
||||||
|
|
||||||
|
local score: number?
|
||||||
|
if query == "" then
|
||||||
|
score = 0
|
||||||
|
else
|
||||||
|
score = noctalia.fuzzyScore(query, session.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if score ~= nil then
|
||||||
|
tinsert(results, {
|
||||||
|
id = "attach:" .. session.name,
|
||||||
|
title = session.name,
|
||||||
|
subtitle = session.attached and noctalia.tr("session_attached")
|
||||||
|
or noctalia.tr("session_detached"),
|
||||||
|
glyph = "terminal-2",
|
||||||
|
score = score,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if tmuxpCache then
|
||||||
|
for _, config in ipairs(tmuxpCache) do
|
||||||
|
if not runningNames[config.session_name] then
|
||||||
|
local score: number?
|
||||||
|
if query == "" then
|
||||||
|
score = -1 -- rank below live sessions when query is empty
|
||||||
|
else
|
||||||
|
score = noctalia.fuzzyScore(query, config.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if score ~= nil then
|
||||||
|
tinsert(results, {
|
||||||
|
id = "tmuxp:" .. config.session_name,
|
||||||
|
title = config.name,
|
||||||
|
subtitle = noctalia.tr("tmuxp_config"),
|
||||||
|
glyph = "file-text",
|
||||||
|
score = score,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tsort(results, function(a, b)
|
||||||
|
return (a.score or 0) > (b.score or 0)
|
||||||
|
end)
|
||||||
|
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
|
||||||
|
local function showResults(query: string)
|
||||||
|
local results = buildResults(query)
|
||||||
|
|
||||||
|
if #results > 0 then
|
||||||
|
launcher.setResults(query, results)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not tmuxInstalled() then
|
||||||
|
launcher.setResults(query, {
|
||||||
|
{
|
||||||
|
id = "",
|
||||||
|
title = noctalia.tr("tmux_not_found"),
|
||||||
|
subtitle = noctalia.tr("tmux_not_found_subtitle"),
|
||||||
|
glyph = "alert-triangle",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
launcher.setResults(query, {
|
||||||
|
{
|
||||||
|
id = "",
|
||||||
|
title = noctalia.tr("no_sessions_found"),
|
||||||
|
subtitle = noctalia.tr("no_sessions_subtitle"),
|
||||||
|
glyph = "loader",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Renders once every in-flight fetch for this query has landed.
|
||||||
|
local function maybeShowResults(query: string)
|
||||||
|
if pendingQuery ~= query then
|
||||||
|
return -- a newer query superseded this one
|
||||||
|
end
|
||||||
|
if pendingTmux or pendingTmuxp then
|
||||||
|
return -- still waiting on one of the two sources
|
||||||
|
end
|
||||||
|
showResults(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function refreshSessions(query: string)
|
||||||
|
pendingQuery = query
|
||||||
|
|
||||||
|
if tmuxInstalled() then
|
||||||
|
pendingTmux = true
|
||||||
|
noctalia.runAsync("tmux ls", function(result)
|
||||||
|
if result.exitCode == 0 then
|
||||||
|
sessionsCache = parseTmuxLs(result.stdout)
|
||||||
|
else
|
||||||
|
-- tmux exits non-zero both on "no server running" (expected,
|
||||||
|
-- empty list) and on real errors. We can't reliably match the
|
||||||
|
-- "no server" message across tmux versions/locales, so we just
|
||||||
|
-- treat any non-zero exit as "no sessions" and log it at low
|
||||||
|
-- severity for diagnostics rather than surfacing it as an error.
|
||||||
|
if result.stderr and result.stderr ~= "" then
|
||||||
|
noctalia.log("tmux-provider: tmux ls: " .. result.stderr)
|
||||||
|
end
|
||||||
|
sessionsCache = {}
|
||||||
|
end
|
||||||
|
pendingTmux = false
|
||||||
|
maybeShowResults(query)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
sessionsCache = {}
|
||||||
|
pendingTmux = false
|
||||||
|
end
|
||||||
|
|
||||||
|
local use_tmuxp: boolean = noctalia.getConfig("use_tmuxp") or false
|
||||||
|
if use_tmuxp and tmuxpInstalled() then
|
||||||
|
pendingTmuxp = true
|
||||||
|
noctalia.runAsync("tmuxp ls --json", function(result)
|
||||||
|
if result.exitCode == 0 then
|
||||||
|
tmuxpCache = parseTmuxpLs(result.stdout)
|
||||||
|
else
|
||||||
|
noctalia.log(
|
||||||
|
"tmux-provider: tmuxp ls --json failed: "
|
||||||
|
.. tostring(result.stderr or result.stdout)
|
||||||
|
)
|
||||||
|
tmuxpCache = {}
|
||||||
|
end
|
||||||
|
pendingTmuxp = false
|
||||||
|
maybeShowResults(query)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
tmuxpCache = {}
|
||||||
|
pendingTmuxp = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- In case both sources were skipped synchronously (e.g. neither installed)
|
||||||
|
maybeShowResults(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
function onQuery(query: string)
|
||||||
|
query = noctalia.string.trim(query)
|
||||||
|
refreshSessions(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
function onActivate(id: string)
|
||||||
|
if id == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local kind, name = id:match("^(%a+):(.+)$")
|
||||||
|
if not kind or not name then
|
||||||
|
noctalia.log("tmux-provider: onActivate received malformed id: " .. tostring(id))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if kind == "attach" then
|
||||||
|
noctalia.runInTerminal(sformat("tmux attach -t %s", shellQuote(name)))
|
||||||
|
elseif kind == "tmuxp" then
|
||||||
|
noctalia.runInTerminal(sformat("tmuxp load %s", shellQuote(name)))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"session_attached": "Attached",
|
||||||
|
"session_detached": "Detached",
|
||||||
|
"tmuxp_config": "tmuxp config",
|
||||||
|
"tmux_not_found": "tmux not found",
|
||||||
|
"tmux_not_found_subtitle": "Install tmux to use this provider",
|
||||||
|
"no_sessions_found": "No sessions found",
|
||||||
|
"no_sessions_subtitle": "Start a tmux session to see it here",
|
||||||
|
"settings": {
|
||||||
|
"use_tmuxp": {
|
||||||
|
"label": "Enable tmuxp",
|
||||||
|
"description": "Also list tmuxp session configurations (via `tmuxp ls`)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user