feat: add peer list with click-to-copy (#6)
- Parse Peers from tailscale status --json into a model - Add ListView with hostname/IP rows below status row - Click hostname or IP to copy via wl-copy + toast confirmation - Increase popout height to 400 to accommodate peer list - Online peers shown in primary color, offline in surfaceVariantText
This commit is contained in:
parent
f980f4da21
commit
0d3f500a2c
2 changed files with 88 additions and 2 deletions
|
|
@ -73,3 +73,13 @@ If this project is used in a completely local-only or air-gapped context, refer
|
||||||
- For self-hosted instances, include `--host` in commands unless the git remote already points at the correct instance.
|
- For self-hosted instances, include `--host` in commands unless the git remote already points at the correct instance.
|
||||||
- `interrogate` and `to-prd` should read this file to know which CLI and host conventions to follow.
|
- `interrogate` and `to-prd` should read this file to know which CLI and host conventions to follow.
|
||||||
- When in doubt about the current instance, check `git remote -v` first.
|
- When in doubt about the current instance, check `git remote -v` first.
|
||||||
|
|
||||||
|
## Troubleshooting `fj` Failures
|
||||||
|
|
||||||
|
If a `fj` command fails on this or any self-hosted Forgejo instance, do not retry with different flags. Diagnose first:
|
||||||
|
|
||||||
|
1. `git remote -v` — note the URL form. URL-style remotes with explicit non-default ports (`ssh://git@host:NNNN/...`) cause `fj` to use that same port for HTTPS API calls, producing TLS errors. SCP-style remotes (`git@host:owner/repo.git`) avoid this; route non-default SSH ports via `~/.ssh/config`.
|
||||||
|
2. `cat ~/.local/share/forgejo-cli/keys.json` — the host key string is what `fj` uses for both auth lookup and API URL. A `:NNNN` suffix here is the most common root cause.
|
||||||
|
3. `fj whoami` (in-repo) and `fj -H https://<host> whoami` — comparing these tells you whether auth is stored under the bare host or under host-with-port.
|
||||||
|
|
||||||
|
`fj`'s syntax also has sharp edges worth noting: `fj issue comment <N> [BODY]` takes the body as a positional argument (not `--body`); there is no `fj issue list` — use `fj issue search "" -s open|closed|all`; and cross-repo issue references use `owner/repo#N`. When in doubt, run `<subcommand> --help` — `fj` is `clap`-based and its help output is reliable.
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@ PluginComponent {
|
||||||
property bool isConnected: false
|
property bool isConnected: false
|
||||||
property string tailscaleIP: ""
|
property string tailscaleIP: ""
|
||||||
property string currentExitNode: ""
|
property string currentExitNode: ""
|
||||||
|
property var peers: []
|
||||||
|
|
||||||
layerNamespacePlugin: "tailscalectl"
|
layerNamespacePlugin: "tailscalectl"
|
||||||
popoutWidth: 360
|
popoutWidth: 360
|
||||||
popoutHeight: 200
|
popoutHeight: 400
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 5000
|
interval: 5000
|
||||||
|
|
@ -46,10 +47,19 @@ PluginComponent {
|
||||||
root.isConnected = data.BackendState === "Running"
|
root.isConnected = data.BackendState === "Running"
|
||||||
root.tailscaleIP = (data.Self?.TailscaleIPs?.[0]) || ""
|
root.tailscaleIP = (data.Self?.TailscaleIPs?.[0]) || ""
|
||||||
root.currentExitNode = data.CurrentExitNode?.HostName || ""
|
root.currentExitNode = data.CurrentExitNode?.HostName || ""
|
||||||
|
root.peers = Object.keys(data.Peers || {}).map(function(key) {
|
||||||
|
var p = data.Peers[key]
|
||||||
|
return {
|
||||||
|
hostname: p.HostName || key,
|
||||||
|
ip: (p.TailscaleIPs && p.TailscaleIPs.length) ? p.TailscaleIPs[0] : "",
|
||||||
|
online: p.Online || false
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
root.isConnected = false
|
root.isConnected = false
|
||||||
root.tailscaleIP = ""
|
root.tailscaleIP = ""
|
||||||
root.currentExitNode = ""
|
root.currentExitNode = ""
|
||||||
|
root.peers = []
|
||||||
ToastService.showError("tailscalectl", "Failed to parse Tailscale status")
|
ToastService.showError("tailscalectl", "Failed to parse Tailscale status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +70,7 @@ PluginComponent {
|
||||||
root.isConnected = false
|
root.isConnected = false
|
||||||
root.tailscaleIP = ""
|
root.tailscaleIP = ""
|
||||||
root.currentExitNode = ""
|
root.currentExitNode = ""
|
||||||
|
root.peers = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +84,11 @@ PluginComponent {
|
||||||
toggleProcess.running = true
|
toggleProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"])
|
||||||
|
ToastService.showInfo("tailscalectl", "Copied: " + text)
|
||||||
|
}
|
||||||
|
|
||||||
popoutContent: Component {
|
popoutContent: Component {
|
||||||
PopoutComponent {
|
PopoutComponent {
|
||||||
headerText: "Tailscale"
|
headerText: "Tailscale"
|
||||||
|
|
@ -80,8 +96,9 @@ PluginComponent {
|
||||||
showCloseButton: true
|
showCloseButton: true
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
id: contentItem
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Theme.spacingM + statusRow.implicitHeight + Theme.spacingS + exitNodeText.implicitHeight + Theme.spacingM
|
height: Theme.spacingM + statusRow.implicitHeight + Theme.spacingM + peerList.height + Theme.spacingM
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: statusRow
|
id: statusRow
|
||||||
|
|
@ -128,6 +145,65 @@ PluginComponent {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: peerList
|
||||||
|
y: Theme.spacingM + statusRow.implicitHeight + Theme.spacingM
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
height: Math.min(root.peers.length * (Theme.fontSizeSmall + Theme.spacingXS), 200)
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
model: root.peers
|
||||||
|
interactive: true
|
||||||
|
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
width: peerList.width
|
||||||
|
height: Theme.fontSizeSmall + Theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: peerHostnameText.implicitWidth
|
||||||
|
height: peerHostnameText.implicitHeight
|
||||||
|
onClicked: {
|
||||||
|
root.copyToClipboard(modelData.hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: peerHostnameText
|
||||||
|
text: modelData.hostname
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: modelData.online ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
hoverEnabled: true
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: peerIpText.implicitWidth
|
||||||
|
height: peerIpText.implicitHeight
|
||||||
|
onClicked: {
|
||||||
|
root.copyToClipboard(modelData.ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: peerIpText
|
||||||
|
text: modelData.ip
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue