Compare commits

...

15 Commits

Author SHA1 Message Date
Jeff Carr 94bd4728ba load/save config files 2025-09-10 17:31:25 -05:00
Jeff Carr c3f5588365 new GUI codebase 2025-09-09 05:11:23 -05:00
Jeff Carr d5069b63f8 use new bash code 2025-09-08 16:27:02 -05:00
Jeff Carr 692843678a minor 2025-08-28 11:29:19 -05:00
Jeff Carr 75c89281eb try to make --restore work 2025-08-28 04:26:32 -05:00
Jeff Carr 6dd0052dcf restructor code 2025-08-25 10:02:14 -05:00
Castor Regex 9d5bd8d5b9 feat: add terminal synchronization program 2025-08-24 22:11:17 -05:00
Castor Regex e37836bb61 feat: update xstartplacement to launch and place terminals 2025-08-24 22:08:12 -05:00
Jeff Carr 71c3ff6642 more 2025-08-22 11:20:18 -05:00
Castor Gemini 0d2cd8082b feat: Add tools to manage terminal geometry
This commit introduces two main changes:

1.  A new program `showAll.go` that uses `wmctrl` to find all
    running terminals and print their geometry and workspace. This
    provides the core functionality for saving window positions.

2.  The existing `stuff.go` program has been fixed to correctly
    find and modify a terminal window. It now targets a generic
    "Terminal" window and correctly retrieves its geometry,
    allowing it to move and resize it successfully.
2025-08-22 11:17:55 -05:00
Jeff Carr 944fc8685d builds and lists window names 2024-12-23 02:41:40 -06:00
Jeff Carr d3f10b0341 something then something else
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-12-23 00:46:50 -06:00
Jeff Carr 60d8edcb03 some lua thing. lists windows okay. notsure
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-12-23 00:45:14 -06:00
Jeff Carr 78cbaac691 just interesting sample code 2024-12-04 03:14:59 -06:00
Jeff Carr 71909226e1 messing around
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-11-24 09:46:07 -06:00
18 changed files with 869 additions and 77 deletions

6
.gitignore vendored
View File

@ -1,7 +1,9 @@
*.swp *.swp
go.mod go.*
go.sum *.pb.go
files/ files/
xstartplacement xstartplacement
startxplacement
devilspie/devilspie2

View File

@ -3,16 +3,19 @@
VERSION = $(shell git describe --tags) VERSION = $(shell git describe --tags)
BUILDTIME = $(shell date +%Y.%m.%d) BUILDTIME = $(shell date +%Y.%m.%d)
default: placement.pb.go install
build: build:
GO111MODULE=off go build \ GO111MODULE=off go build \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}" -ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
./startxplacement
verbose: install: goimports
GO111MODULE=off go build -v -x \ GO111MODULE=off go install \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}" -ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
install: install-verbose: goimports vet
GO111MODULE=off go install \ GO111MODULE=off go install -v -x \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}" -ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
# makes a .deb package # makes a .deb package
@ -32,4 +35,12 @@ redomod:
clean: clean:
rm -f go.* rm -f go.*
rm -f virtigo* rm -f *.pb.go
rm -f startxplacement*
vet:
GO111MODULE=off go vet
@echo 'go vet' worked for this application
placement.pb.go: placement.proto
autogenpb --proto placement.proto

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# This is an attempt to redo 'startx' not as a bash script this time
GOALS:
* use a protobuf TEXT config file
* store global file in ~/etc/startx.text
* store user config file in ~/.config/startx.text
* parse additional settings from ~/.config/startx.d/
* restore the geometry of the apps
* make a GUI save/edit/restore tool
CURRENTLY WORKING:
* works with x windows
* restores mate-terminal on debian
TODO:
* wayland support
* other programs like firefox & keepass
* actually execute from lightdm
* run underneath mate-session
* run underneath all the other WM
* rename binary 'startx'

56
argv.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
import (
"fmt"
"os"
)
/*
this parses the command line arguements using alex flint's go-arg
*/
var argv args
type args struct {
Restore string `arg:"--restore" help:"restore terminal windows from a config file"`
Save *EmptyCmd `arg:"subcommand:save" help:"save current window geometries to the your config file"`
DumpX *EmptyCmd `arg:"subcommand:dumpx" help:"show your current window geometries"`
Dump *EmptyCmd `arg:"subcommand:dump" help:"show your current window geometries"`
List *EmptyCmd `arg:"subcommand:list" help:"list entries in your config file"`
Force bool `arg:"--force" help:"try to strong arm things"`
Verbose bool `arg:"--verbose" help:"show more output"`
Bash bool `arg:"--bash" help:"generate bash completion"`
BashAuto []string `arg:"--auto-complete" help:"todo: move this to go-arg"`
}
type EmptyCmd struct {
}
func (args) Version() string {
return ARGNAME + " " + VERSION + " Built on " + BUILDTIME
}
func (a args) Description() string {
return `
startxplacment -- run this after 'startx' to restore all your apps
will attempt to launch your terminal windows on the right Workspaces
and with the right geometries. TODO: restore the bash working paths
`
}
func (a args) DoAutoComplete(argv []string) {
switch argv[0] {
case "dump":
fmt.Println("--terminals")
default:
if argv[0] == ARGNAME {
// list the subcommands here
fmt.Println("--restore save dump dumpx list")
}
}
os.Exit(0)
}

102
common.go Normal file
View File

@ -0,0 +1,102 @@
package main
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// 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
}
// 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
}

16
devilspie/Makefile Normal file
View File

@ -0,0 +1,16 @@
PKGINC = $(shell pkg-config --cflags --libs glib-2.0 libwnck-3.0 lua5.1)
all: build run
# gcc *.c -o test
run:
# lists out windows found?
echo apt install lua-posix
./devilspie2 -l
./devilspie2 -w
./devilspie2 -d -e -f lua
build:
reset
gcc *.c -o devilspie2 ${PKGINC} \
-lwnck-3 -lgtk-3 -lgdk-3 -lz -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -llua5.1 -lX11 -lXinerama

View File

@ -47,6 +47,10 @@
#define HAVE_GTK3 #define HAVE_GTK3
#endif #endif
#define PACKAGE "jcarr"
#define LOCALEDIR "/tmp/jcarr"
#define DEVILSPIE2_VERSION "jwc"
/** /**
* *
*/ */
@ -237,7 +241,6 @@ void print_list(GSList *list)
} }
} }
/** /**
* *
*/ */

46
devilspie/lua/awesome.lua Normal file
View File

@ -0,0 +1,46 @@
-- Support Awesome 3.5 WM
local posix = require("posix");
local os = require("os");
local awesome = "/usr/bin/awesome-client"
if not posix.stat(awesome, "type") == "file" then
awesome = nil;
end
-- Check for tiling mode
function is_tiling()
if awesome then
return true;
end
return false;
end
-- Make window floating
-- Parameters: state - true to make window floating, else make window tiled
function set_tile_floating( state )
if not awesome then
return nil;
end
if state then state = "true" else state = "false" end
local xid = get_window_xid();
local command = "echo ";
command = command .. "'";
command = command .. " local naughty = require(\"naughty\");";
command = command .. " local awcl = require(\"awful.client\");";
command = command .. " local client = require(\"client\");";
command = command .. " for k, c in pairs( client.get() ) do";
command = command .. " if c.window == " .. xid .. " then";
command = command .. " awcl.floating.set(c, " .. state .. ");";
command = command .. " end";
command = command .. " end";
command = command .. "'";
command = command .. " | ";
command = command .. awesome;
debug_print("Awesome floating: " .. command);
return os.execute( command );
end

View File

@ -0,0 +1,29 @@
--[[
This file is part of devilspie2
Copyright (C) 2023 Darren Salt
This is an example primarily intended for use in your own
configuration files etc. without causing licence contamination.
As such, no licence conditions are attached; it may be modified and
redistributed freely. Essentially, do what you want with it.
That said, retaining proper attribution would be appreciated.
]]
-- Optional, but probably useful. (Technical feedback would be helpful.)
set_adjust_for_decoration(true)
-- Set up some variables containing likely-to-be-referenced values
win_class = get_window_class()
win_role = get_window_role()
win_name = get_window_name()
app_name = get_application_name()
ins_name = get_class_instance_name()
if ins_name == nil then ins_name = '[nil]' end
grp_name = get_class_group_name()
if grp_name == nil then grp_name = '[nil]' end
-- Debug output ("devilspie2 -d")
decorated = get_window_is_decorated() and "yes" or "no"
debug_print("\nName: '" .. win_name .. "'\nApp: '" .. app_name .. "'\nClass: " .. win_class .. "\nRole: <" .. win_role .. ">")
debug_print ("Process: '" .. get_process_name() .. "'\nDecorated (jwc note. this is in the lua script): " .. decorated)
debug_print ("Instance: '" .. ins_name .. "' & '" .. grp_name .. "'")
-- Add your stuff here!

121
doDumpX.go Normal file
View File

@ -0,0 +1,121 @@
package main
import (
"fmt"
"os"
"reflect"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
)
func doDumpX() {
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.")
}

22
exit.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
import (
"os"
"go.wit.com/log"
)
func okExit(thing string) {
if thing != "" {
log.Info("regex exit:", thing, "ok")
}
os.Exit(0)
}
func badExit(err error) {
log.Info("regex failed: ", err)
os.Exit(-1)
}

106
launch_terminal.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"fmt"
"os"
"os/exec"
"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 doLaunch() {
// 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.
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.")
}
}

61
main.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"fmt"
"go.wit.com/dev/alexflint/arg"
"go.wit.com/lib/gui/prep"
"go.wit.com/log"
)
// sent via -ldflags
var VERSION string
var BUILDTIME string
// used for shell auto completion
var ARGNAME string = "startxplacement"
// using this for now. triggers config save
var configSave bool
var configFile string = "/home/jcarr/.config/startxplacement.out"
func main() {
me = new(mainType)
prep.Bash(ARGNAME, argv.DoAutoComplete) // todo: this line should be: prep.Bash(argv)
me.myGui = prep.Gui() // prepares the GUI package for go-args
me.pp = arg.MustParse(&argv)
if argv.DumpX != nil {
doDumpX()
}
if argv.List != nil {
log.Info("list the config")
okExit("")
}
if argv.Dump != nil {
// 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
}
fmt.Printf("%v\n", currentStates)
okExit("")
}
if argv.Restore != "" {
log.Info("restore here")
okExit("")
}
// doGui()
okExit("")
}

16
placement.config.go Normal file
View File

@ -0,0 +1,16 @@
package main
// functions to import and export the protobuf
// data to and from config files
import (
"go.wit.com/lib/config"
)
func (pb *Placements) ConfigSave() error {
return config.ConfigSave(pb)
}
func (pb *Placements) ConfigLoad() error {
return config.ConfigLoad(pb, ARGNAME, "placements")
}

31
placement.proto Normal file
View File

@ -0,0 +1,31 @@
syntax = "proto3";
package main;
message Placement {
message Size {
int64 w = 1;
int64 h = 2;
}
message Offset {
int64 x = 1;
int64 y = 2;
}
message Geom {
Size size = 1;
Offset offset = 2;
}
// used for grid layouts
string name = 1; // `autogenpb:sort` `autogenpb:unique`
Geom geom = 2;
int32 workspace = 3; // what workspace to show the app on
string wd = 4; // working dir. Tries to set xterm path at start to this
repeated string argv = 5; // argv. argv[0] should be the executable name
string namespace = 6; // namespace of the executable (go.wit.com/apps/forge)
}
message Placements { // `autogenpb:marshal` `autogenpb:mutex`
string uuid = 1; // `autogenpb:uuid:31769bcb-5865-4926-b7d6-501083312eea`
string version = 2; // `autogenpb:version:v0.0.1`
repeated Placement Placement = 3;
string filename = 4; // used by the config save function
}

17
structs.go Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
import (
"go.wit.com/dev/alexflint/arg"
"go.wit.com/lib/gui/prep"
)
var me *mainType
// this app's variables
type mainType struct {
pp *arg.Parser // for parsing the command line args. Yay to alexf lint!
myGui *prep.GuiPrep // the gui toolkit handle
}

View File

@ -1,69 +0,0 @@
package main
import (
"fmt"
"os"
"os/exec"
"time"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
"go.wit.com/log"
)
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 {
nameReply, err := xproto.GetProperty(conn, false, child,
xproto.AtomWmName, xproto.AtomString, 0, (1<<32)-1).Reply()
if err != nil || len(nameReply.Value) == 0 {
continue
}
name := string(nameReply.Value)
log.Info("found name:", name)
if name == "Workspace1-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.")
}

198
sync_terminals.go Normal file
View File

@ -0,0 +1,198 @@
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 doSync() {
// 1. Read the desired state from the config file.
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 startxplacement.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
}