feat(04-02): add vmux i3bar subcommand with i3status wrapping
- runI3Bar: standalone mode (polling 2s) and i3status wrapping mode - i3bar protocol v1: header + infinite JSON array on stdout - queryVmuxBlock: daemon offline fallback in gray - CLI: vmux i3bar auto-detects i3status in PATH - Stdout sync after each write to avoid buffering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
124
i3bar.go
124
i3bar.go
@@ -1,8 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// I3BarBlock represents a single block in the i3bar protocol.
|
// I3BarBlock represents a single block in the i3bar protocol.
|
||||||
@@ -50,3 +56,121 @@ func formatI3BarBlocks(sessions []SessionInfo) []I3BarBlock {
|
|||||||
|
|
||||||
return []I3BarBlock{{FullText: text, Color: color, Name: "vmux"}}
|
return []I3BarBlock{{FullText: text, Color: color, Name: "vmux"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// queryVmuxBlock fetches sessions from daemon and returns the vmux i3bar block.
|
||||||
|
// Returns a "daemon offline" block if the daemon is unreachable.
|
||||||
|
func queryVmuxBlock(sockPath string) I3BarBlock {
|
||||||
|
client := NewClient(sockPath)
|
||||||
|
resp, err := client.Send(Request{Action: "list"})
|
||||||
|
if err != nil {
|
||||||
|
return I3BarBlock{FullText: "vmux: daemon offline", Color: "#888888", Name: "vmux"}
|
||||||
|
}
|
||||||
|
if !resp.OK {
|
||||||
|
return I3BarBlock{FullText: "vmux: error", Color: "#888888", Name: "vmux"}
|
||||||
|
}
|
||||||
|
blocks := formatI3BarBlocks(resp.Sessions)
|
||||||
|
return blocks[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeI3BarLine writes a JSON array of blocks as an i3bar protocol line.
|
||||||
|
// Uses os.Stdout.Write + Sync to avoid buffering issues.
|
||||||
|
func writeI3BarLine(blocks []I3BarBlock, first bool) {
|
||||||
|
data, err := json.Marshal(blocks)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("i3bar marshal error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var line string
|
||||||
|
if first {
|
||||||
|
line = string(data) + "\n"
|
||||||
|
} else {
|
||||||
|
line = "," + string(data) + "\n"
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte(line))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// runI3Bar runs the i3bar protocol loop on stdout.
|
||||||
|
// If i3statusCmd is non-empty, it wraps i3status output and prepends vmux blocks.
|
||||||
|
// If empty, runs in standalone mode with periodic polling.
|
||||||
|
func runI3Bar(sockPath, i3statusCmd string) {
|
||||||
|
if i3statusCmd != "" {
|
||||||
|
runI3BarWrapped(sockPath, i3statusCmd)
|
||||||
|
// If i3status exits, fall through to standalone
|
||||||
|
}
|
||||||
|
runI3BarStandalone(sockPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runI3BarStandalone(sockPath string) {
|
||||||
|
os.Stdout.Write([]byte("{\"version\":1}\n"))
|
||||||
|
os.Stdout.Write([]byte("[\n"))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
|
||||||
|
first := true
|
||||||
|
for {
|
||||||
|
block := queryVmuxBlock(sockPath)
|
||||||
|
writeI3BarLine([]I3BarBlock{block}, first)
|
||||||
|
first = false
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runI3BarWrapped(sockPath, i3statusCmd string) {
|
||||||
|
cmd := exec.Command("sh", "-c", i3statusCmd)
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("i3bar: cannot pipe i3status stdout: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Printf("i3bar: cannot start i3status: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||||
|
|
||||||
|
// Forward header line (e.g. {"version":1})
|
||||||
|
if !scanner.Scan() {
|
||||||
|
log.Print("i3bar: i3status produced no output")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte(scanner.Text() + "\n"))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
|
||||||
|
// Forward opening bracket
|
||||||
|
if !scanner.Scan() {
|
||||||
|
log.Print("i3bar: i3status produced no opening bracket")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte(scanner.Text() + "\n"))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
|
||||||
|
first := true
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
trimmed := strings.TrimLeft(line, ",")
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocks []I3BarBlock
|
||||||
|
if err := json.Unmarshal([]byte(trimmed), &blocks); err != nil {
|
||||||
|
// Not a JSON array line, forward as-is
|
||||||
|
os.Stdout.Write([]byte(line + "\n"))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
vmuxBlock := queryVmuxBlock(sockPath)
|
||||||
|
blocks = append([]I3BarBlock{vmuxBlock}, blocks...)
|
||||||
|
|
||||||
|
writeI3BarLine(blocks, first)
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Wait()
|
||||||
|
}
|
||||||
|
|||||||
13
main.go
13
main.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -141,6 +142,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Println("Daemon stopped.")
|
fmt.Println("Daemon stopped.")
|
||||||
|
|
||||||
|
case "i3bar":
|
||||||
|
if err := EnsureDaemon(sockPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
i3statusCmd := ""
|
||||||
|
if _, err := exec.LookPath("i3status"); err == nil {
|
||||||
|
i3statusCmd = "i3status"
|
||||||
|
}
|
||||||
|
runI3Bar(sockPath, i3statusCmd)
|
||||||
|
|
||||||
case "daemon":
|
case "daemon":
|
||||||
runDaemon(sockPath)
|
runDaemon(sockPath)
|
||||||
|
|
||||||
@@ -199,6 +211,7 @@ Commands:
|
|||||||
switch <query> Switch to the workspace of the matching session
|
switch <query> Switch to the workspace of the matching session
|
||||||
label <id> <text> Assign a label to a session
|
label <id> <text> Assign a label to a session
|
||||||
focus <minutes> Suppress notifications for N minutes
|
focus <minutes> Suppress notifications for N minutes
|
||||||
|
i3bar Output i3bar JSON (use as status_command in i3 config)
|
||||||
stop Stop the vmux daemon
|
stop Stop the vmux daemon
|
||||||
daemon Run the daemon in foreground (used internally)
|
daemon Run the daemon in foreground (used internally)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user