From 9d5bd8d5b92fc91a6ff03398cd9f338dce4ca6b4 Mon Sep 17 00:00:00 2001 From: Castor Regex Date: Sun, 24 Aug 2025 22:11:17 -0500 Subject: feat: add terminal synchronization program --- launch_terminal.go | 202 +++++++++++++++++++++++++++++++++++++++++++++++ showAll.go | 202 ----------------------------------------------- stuff.go.disabled | 121 ++++++++++++++++++++++++++++ sync_terminals.go | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 551 insertions(+), 202 deletions(-) create mode 100644 launch_terminal.go delete mode 100644 showAll.go create mode 100644 stuff.go.disabled create mode 100644 sync_terminals.go diff --git a/launch_terminal.go b/launch_terminal.go new file mode 100644 index 0000000..be3a5d7 --- /dev/null +++ b/launch_terminal.go @@ -0,0 +1,202 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// WindowConfig holds the configuration for a single terminal window. +type WindowConfig struct { + Title string + Path string + Geometry string // In WIDTHxHEIGHT+X+Y format + Workspace string +} + +func main() { + // 1. Get current working directory. + pwd, err := os.Getwd() + if err != nil { + fmt.Println("Failed to get current directory:", err) + return + } + + // 2. Read and parse the configuration file. + configFile := "/home/jcarr/go/src/gemini/xstartplacement.out" + configs, err := parseConfig(configFile) + if err != nil { + fmt.Printf("Failed to parse config file '%s': %v\n", configFile, err) + return + } + + // 3. Find the best matching configuration for the current directory. + var bestMatch *WindowConfig + longestPrefix := 0 + for i, config := range configs { + if strings.HasPrefix(pwd, config.Path) { + if len(config.Path) > longestPrefix { + longestPrefix = len(config.Path) + bestMatch = &configs[i] + } + } + } + + if bestMatch == nil { + fmt.Printf("No configuration found for directory: %s\n", pwd) + return + } + targetConfig := bestMatch + fmt.Printf("Found matching configuration for path: %s\n", targetConfig.Path) + + // 4. Get the list of windows before launching the new terminal. + windowsBefore, err := getWindowList() + if err != nil { + fmt.Println("Failed to get initial window list:", err) + return + } + + // 5. Launch mate-terminal. + geomString := targetConfig.Geometry + cmd := exec.Command("mate-terminal", "--geometry", geomString) + if err := cmd.Start(); err != nil { + fmt.Println("Failed to start mate-terminal:", err) + return + } + fmt.Printf("Launched mate-terminal with geometry %s\n", geomString) + + // 6. Find the new window by comparing the window lists. + var newWindowID string + for i := 0; i < 10; i++ { + time.Sleep(500 * time.Millisecond) + windowsAfter, err := getWindowList() + if err != nil { + fmt.Println("Failed to get updated window list:", err) + continue + } + newWindowID = findNewWindow(windowsBefore, windowsAfter) + if newWindowID != "" { + break + } + } + + if newWindowID == "" { + fmt.Println("Could not find the new terminal window.") + return + } + fmt.Printf("Found new window with ID: %s\n", newWindowID) + + // 7. Move the window to the correct workspace. + cmd = exec.Command("wmctrl", "-i", "-r", newWindowID, "-t", targetConfig.Workspace) + if err := cmd.Run(); err != nil { + fmt.Println("Failed to move window to workspace:", err) + } else { + fmt.Printf("Moved window to workspace %s\n", targetConfig.Workspace) + } + + // 8. Set the final window title. + finalTitle := fmt.Sprintf("jcarr@framebook: %s", pwd) + cmd = exec.Command("wmctrl", "-i", "-r", newWindowID, "-T", finalTitle) + if err := cmd.Run(); err != nil { + fmt.Println("Failed to set final window title:", err) + } else { + fmt.Println("Window setup complete.") + } +} + +// getWindowList returns a map of window IDs to their titles. +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 +} + +// findNewWindow compares two maps of windows and returns the ID of the new window. +func findNewWindow(before, after map[string]string) string { + for id := range after { + if _, ok := before[id]; !ok { + return id + } + } + return "" +} + +// parseConfig remains the same as before. +func parseConfig(filePath string) ([]WindowConfig, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var configs []WindowConfig + scanner := bufio.NewScanner(file) + var currentConfig WindowConfig + + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("could not get user home directory: %w", err) + } + + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, " Title: ") { + title := strings.TrimSpace(strings.TrimPrefix(line, " Title: ")) + currentConfig.Title = title + parts := strings.SplitN(title, ": ", 2) + if len(parts) == 2 { + path := parts[1] + if strings.HasPrefix(path, "~") { + path = filepath.Join(homeDir, path[1:]) + } + currentConfig.Path = path + } + } else if strings.HasPrefix(line, " Geometry: ") { + geomStr := strings.TrimSpace(strings.TrimPrefix(line, " Geometry: ")) + var x, y, w, h string + _, err := fmt.Sscanf(geomStr, "X=%s Y=%s Width=%s Height=%s", &x, &y, &w, &h) + if err == nil { + x = strings.TrimSuffix(x, ",") + y = strings.TrimSuffix(y, ",") + w = strings.TrimSuffix(w, ",") + currentConfig.Geometry = fmt.Sprintf("%sx%s+%s+%s", w, h, x, y) + } + } else if strings.HasPrefix(line, " Workspace: ") { + currentConfig.Workspace = strings.TrimSpace(strings.TrimPrefix(line, " Workspace: ")) + } else if line == "---" { + if currentConfig.Path != "" { + configs = append(configs, currentConfig) + } + currentConfig = WindowConfig{} // Reset for the next entry + } + } + + if currentConfig.Path != "" { + configs = append(configs, currentConfig) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return configs, nil +} diff --git a/showAll.go b/showAll.go deleted file mode 100644 index be3a5d7..0000000 --- a/showAll.go +++ /dev/null @@ -1,202 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" -) - -// WindowConfig holds the configuration for a single terminal window. -type WindowConfig struct { - Title string - Path string - Geometry string // In WIDTHxHEIGHT+X+Y format - Workspace string -} - -func main() { - // 1. Get current working directory. - pwd, err := os.Getwd() - if err != nil { - fmt.Println("Failed to get current directory:", err) - return - } - - // 2. Read and parse the configuration file. - configFile := "/home/jcarr/go/src/gemini/xstartplacement.out" - configs, err := parseConfig(configFile) - if err != nil { - fmt.Printf("Failed to parse config file '%s': %v\n", configFile, err) - return - } - - // 3. Find the best matching configuration for the current directory. - var bestMatch *WindowConfig - longestPrefix := 0 - for i, config := range configs { - if strings.HasPrefix(pwd, config.Path) { - if len(config.Path) > longestPrefix { - longestPrefix = len(config.Path) - bestMatch = &configs[i] - } - } - } - - if bestMatch == nil { - fmt.Printf("No configuration found for directory: %s\n", pwd) - return - } - targetConfig := bestMatch - fmt.Printf("Found matching configuration for path: %s\n", targetConfig.Path) - - // 4. Get the list of windows before launching the new terminal. - windowsBefore, err := getWindowList() - if err != nil { - fmt.Println("Failed to get initial window list:", err) - return - } - - // 5. Launch mate-terminal. - geomString := targetConfig.Geometry - cmd := exec.Command("mate-terminal", "--geometry", geomString) - if err := cmd.Start(); err != nil { - fmt.Println("Failed to start mate-terminal:", err) - return - } - fmt.Printf("Launched mate-terminal with geometry %s\n", geomString) - - // 6. Find the new window by comparing the window lists. - var newWindowID string - for i := 0; i < 10; i++ { - time.Sleep(500 * time.Millisecond) - windowsAfter, err := getWindowList() - if err != nil { - fmt.Println("Failed to get updated window list:", err) - continue - } - newWindowID = findNewWindow(windowsBefore, windowsAfter) - if newWindowID != "" { - break - } - } - - if newWindowID == "" { - fmt.Println("Could not find the new terminal window.") - return - } - fmt.Printf("Found new window with ID: %s\n", newWindowID) - - // 7. Move the window to the correct workspace. - cmd = exec.Command("wmctrl", "-i", "-r", newWindowID, "-t", targetConfig.Workspace) - if err := cmd.Run(); err != nil { - fmt.Println("Failed to move window to workspace:", err) - } else { - fmt.Printf("Moved window to workspace %s\n", targetConfig.Workspace) - } - - // 8. Set the final window title. - finalTitle := fmt.Sprintf("jcarr@framebook: %s", pwd) - cmd = exec.Command("wmctrl", "-i", "-r", newWindowID, "-T", finalTitle) - if err := cmd.Run(); err != nil { - fmt.Println("Failed to set final window title:", err) - } else { - fmt.Println("Window setup complete.") - } -} - -// getWindowList returns a map of window IDs to their titles. -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 -} - -// findNewWindow compares two maps of windows and returns the ID of the new window. -func findNewWindow(before, after map[string]string) string { - for id := range after { - if _, ok := before[id]; !ok { - return id - } - } - return "" -} - -// parseConfig remains the same as before. -func parseConfig(filePath string) ([]WindowConfig, error) { - file, err := os.Open(filePath) - if err != nil { - return nil, err - } - defer file.Close() - - var configs []WindowConfig - scanner := bufio.NewScanner(file) - var currentConfig WindowConfig - - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("could not get user home directory: %w", err) - } - - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, " Title: ") { - title := strings.TrimSpace(strings.TrimPrefix(line, " Title: ")) - currentConfig.Title = title - parts := strings.SplitN(title, ": ", 2) - if len(parts) == 2 { - path := parts[1] - if strings.HasPrefix(path, "~") { - path = filepath.Join(homeDir, path[1:]) - } - currentConfig.Path = path - } - } else if strings.HasPrefix(line, " Geometry: ") { - geomStr := strings.TrimSpace(strings.TrimPrefix(line, " Geometry: ")) - var x, y, w, h string - _, err := fmt.Sscanf(geomStr, "X=%s Y=%s Width=%s Height=%s", &x, &y, &w, &h) - if err == nil { - x = strings.TrimSuffix(x, ",") - y = strings.TrimSuffix(y, ",") - w = strings.TrimSuffix(w, ",") - currentConfig.Geometry = fmt.Sprintf("%sx%s+%s+%s", w, h, x, y) - } - } else if strings.HasPrefix(line, " Workspace: ") { - currentConfig.Workspace = strings.TrimSpace(strings.TrimPrefix(line, " Workspace: ")) - } else if line == "---" { - if currentConfig.Path != "" { - configs = append(configs, currentConfig) - } - currentConfig = WindowConfig{} // Reset for the next entry - } - } - - if currentConfig.Path != "" { - configs = append(configs, currentConfig) - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return configs, nil -} diff --git a/stuff.go.disabled b/stuff.go.disabled new file mode 100644 index 0000000..e50ff3d --- /dev/null +++ b/stuff.go.disabled @@ -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.") +} diff --git a/sync_terminals.go b/sync_terminals.go new file mode 100644 index 0000000..2c557ba --- /dev/null +++ b/sync_terminals.go @@ -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 "" +} \ No newline at end of file -- cgit v1.2.3