feat: add terminal synchronization program

This commit is contained in:
Castor Regex 2025-08-24 22:11:17 -05:00 committed by Jeff Carr
parent e37836bb61
commit 9d5bd8d5b9
3 changed files with 349 additions and 0 deletions

121
stuff.go.disabled Normal file
View File

@ -0,0 +1,121 @@
package main
import (
"fmt"
"os"
"reflect"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
)
func main() {
conn, err := xgb.NewConn()
if err != nil {
fmt.Println("Failed to connect to X server:", err)
os.Exit(1)
}
defer conn.Close()
/*
// Start the terminal (replace with your app)
go func() {
if err := exec.Command("mate-terminal", "--title", "Workspace1-Terminal").Start(); err != nil {
fmt.Println("Error starting terminal:", err)
}
}()
// Wait for the window to appear
time.Sleep(2 * time.Second)
*/
// Get the root window
setup := xproto.Setup(conn)
root := setup.DefaultScreen(conn).Root
// List children windows
reply, err := xproto.QueryTree(conn, root).Reply()
if err != nil {
fmt.Println("Failed to query windows:", err)
os.Exit(1)
}
// Find the window with the specified title
var target xproto.Window
for _, child := range reply.Children {
// fmt.Printf("child: %+v\n", child)
/*
// Get the atom for _NET_WM_NAME
atomReply, err := xproto.InternAtom(conn, true, uint16(len("_NET_WM_NAME")), "_NET_WM_NAME").Reply()
if err != nil {
log.Fatalf("Failed to intern atom _NET_WM_NAME: %v", err)
}
netWmNameAtom := atomReply.Atom // Correct field to use
*/
/*
// Get the property for _NET_WM_NAME
nameReply, err := xproto.GetProperty(conn, false, child, netWmNameAtom, xproto.AtomString, 0, (1<<32)-1).Reply()
if err != nil {
log.Printf("Failed to get property _NET_WM_NAME: %v", err)
} else if len(nameReply.Value) > 0 {
fmt.Printf("Window name: %s\n", string(nameReply.Value))
}
*/
/*
// Get the atom for _NET_WM_NAME
atomReply, err := xproto.InternAtom(conn, true, uint16(len("_NET_WM_NAME")), "_NET_WM_NAME").Reply()
if err != nil {
log.Fatalf("Failed to intern atom _NET_WM_NAME: %v", err)
} else {
fmt.Printf("found atomic name: %s\n", string(atomReply.Value))
}
netWmNameAtom := atomReply.Atom
*/
/*
// Get the property for _NET_WM_NAME
nameReply, err := xproto.GetProperty(conn, false, child, netWmNameAtom, xproto.AtomString, 0, (1<<32)-1).Reply()
if err != nil {
log.Printf("Failed to get property _NET_WM_NAME: %v", err)
} else if len(nameReply.Value) > 0 {
fmt.Printf("Window name: %s\n", string(nameReply.Value))
}
*/
geomReply, err := xproto.GetGeometry(conn, xproto.Drawable(child)).Reply()
if err != nil {
fmt.Printf("err: %+v\n", err)
// fmt.Printf("child geomReply: %+v\n", geomReply)
} else {
fmt.Printf("child geomReply: %+v\n", geomReply)
}
nameReply, err := xproto.GetProperty(conn, false, child, xproto.AtomWmName, xproto.AtomString, 0, (1<<32)-1).Reply()
if err != nil {
// fmt.Printf("child err: %+v\n", err)
} else {
fmt.Printf("child %+v nameReply: %+v %s\n", reflect.TypeOf(child), nameReply, string(nameReply.Value))
}
if err != nil || len(nameReply.Value) == 0 {
continue
}
name := string(nameReply.Value)
if name == "Terminal" {
target = child
break
}
}
if target == 0 {
fmt.Println("Window not found.")
os.Exit(1)
}
// Move the window to workspace 1 and set its geometry
xproto.ConfigureWindow(conn, target, xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight,
[]uint32{100, 100, 800, 600})
fmt.Println("Window moved and resized.")
}

228
sync_terminals.go Normal file
View File

@ -0,0 +1,228 @@
package main
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// DesiredState represents a terminal window configuration from the file.
type DesiredState struct {
Path string
Geometry string // WIDTHxHEIGHT+X+Y
Workspace string
}
// CurrentState represents an open window's properties from wmctrl.
type CurrentState struct {
WindowID string
Workspace string
Geometry string // X,Y,Width,Height
Path string
}
func main() {
// 1. Read the desired state from the config file.
configFile := "/home/jcarr/go/src/gemini/xstartplacement.out"
desiredStates, err := parseDesiredState(configFile)
if err != nil {
fmt.Printf("Error parsing config file: %v\n", err)
return
}
// 2. Get the current state of all terminal windows.
currentStates, err := getCurrentState()
if err != nil {
fmt.Printf("Error getting current window state: %v\n", err)
return
}
// 3. Create a map of current windows for easy lookup.
currentMap := make(map[string]bool)
for _, window := range currentStates {
// Normalize the path for comparison.
currentMap[window.Path] = true
}
// 4. Compare desired state with current state and launch missing terminals.
for _, desired := range desiredStates {
if _, exists := currentMap[desired.Path]; !exists {
fmt.Printf("Terminal for path '%s' not found. Launching...\n", desired.Path)
launchTerminal(desired)
} else {
fmt.Printf("Terminal for path '%s' already exists. Skipping.\n", desired.Path)
}
}
fmt.Println("Terminal synchronization complete.")
}
// launchTerminal launches and configures a new mate-terminal.
func launchTerminal(state DesiredState) {
originalDir, _ := os.Getwd()
if err := os.Chdir(state.Path); err != nil {
fmt.Printf("Failed to change directory to %s: %v\n", state.Path, err)
return
}
defer os.Chdir(originalDir)
windowsBefore, err := getWindowList()
if err != nil {
fmt.Printf("Failed to get initial window list: %v\n", err)
return
}
cmd := exec.Command("mate-terminal", "--geometry", state.Geometry)
if err := cmd.Start(); err != nil {
fmt.Printf("Failed to launch terminal for %s: %v\n", state.Path, err)
return
}
var newWindowID string
for i := 0; i < 10; i++ {
time.Sleep(500 * time.Millisecond)
windowsAfter, _ := getWindowList()
newWindowID = findNewWindow(windowsBefore, windowsAfter)
if newWindowID != "" {
break
}
}
if newWindowID == "" {
fmt.Printf("Could not find new window for %s\n", state.Path)
return
}
exec.Command("wmctrl", "-i", "-r", newWindowID, "-t", state.Workspace).Run()
finalTitle := fmt.Sprintf("jcarr@framebook: %s", state.Path)
exec.Command("wmctrl", "-i", "-r", newWindowID, "-T", finalTitle).Run()
fmt.Printf("Successfully launched terminal for %s\n", state.Path)
}
// parseDesiredState reads the xstartplacement.out file.
func parseDesiredState(filePath string) ([]DesiredState, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
var states []DesiredState
scanner := bufio.NewScanner(file)
var currentState DesiredState
homeDir, _ := os.UserHomeDir()
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, " Title: ") {
title := strings.TrimSpace(strings.TrimPrefix(line, " Title: "))
parts := strings.SplitN(title, ": ", 2)
if len(parts) == 2 {
path := parts[1]
if strings.HasPrefix(path, "~") {
path = filepath.Join(homeDir, path[1:])
}
currentState.Path = path
}
} else if strings.HasPrefix(line, " Geometry: ") {
geomStr := strings.TrimSpace(strings.TrimPrefix(line, " Geometry: "))
var x, y, w, h string
fmt.Sscanf(geomStr, "X=%s Y=%s Width=%s Height=%s", &x, &y, &w, &h)
x = strings.TrimSuffix(x, ",")
y = strings.TrimSuffix(y, ",")
w = strings.TrimSuffix(w, ",")
currentState.Geometry = fmt.Sprintf("%sx%s+%s+%s", w, h, x, y)
} else if strings.HasPrefix(line, " Workspace: ") {
currentState.Workspace = strings.TrimSpace(strings.TrimPrefix(line, " Workspace: "))
} else if line == "---" {
if currentState.Path != "" {
states = append(states, currentState)
}
currentState = DesiredState{}
}
}
if currentState.Path != "" {
states = append(states, currentState)
}
return states, scanner.Err()
}
// getCurrentState gets all open mate-terminal windows.
func getCurrentState() ([]CurrentState, error) {
cmd := exec.Command("wmctrl", "-lG")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return nil, err
}
var states []CurrentState
scanner := bufio.NewScanner(&out)
homeDir, _ := os.UserHomeDir()
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "jcarr@framebook") {
continue
}
fields := strings.Fields(line)
if len(fields) < 8 {
continue
}
title := strings.Join(fields[7:], " ")
parts := strings.SplitN(title, ": ", 2)
if len(parts) != 2 {
continue
}
path := parts[1]
if strings.HasPrefix(path, "~") {
path = filepath.Join(homeDir, path[1:])
}
states = append(states, CurrentState{
WindowID: fields[0],
Workspace: fields[1],
Geometry: fmt.Sprintf("%s,%s,%s,%s", fields[2], fields[3], fields[4], fields[5]),
Path: path,
})
}
return states, nil
}
// Helper functions
func getWindowList() (map[string]string, error) {
cmd := exec.Command("wmctrl", "-l")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return nil, err
}
windows := make(map[string]string)
scanner := bufio.NewScanner(&out)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) > 0 {
windows[fields[0]] = strings.Join(fields[3:], " ")
}
}
return windows, nil
}
func findNewWindow(before, after map[string]string) string {
for id := range after {
if _, ok := before[id]; !ok {
return id
}
}
return ""
}