Compare commits

...

10 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
13 changed files with 656 additions and 14 deletions

5
.gitignore vendored
View File

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

View File

@ -3,17 +3,19 @@
VERSION = $(shell git describe --tags)
BUILDTIME = $(shell date +%Y.%m.%d)
default: placement.pb.go install
build:
GO111MODULE=off go build \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
./xstartplacement
./startxplacement
verbose:
GO111MODULE=off go build -v -x \
install: goimports
GO111MODULE=off go install \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
install:
GO111MODULE=off go install \
install-verbose: goimports vet
GO111MODULE=off go install -v -x \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
# makes a .deb package
@ -33,4 +35,12 @@ redomod:
clean:
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

View File

@ -1 +1,24 @@
# devils pie code to try to fix window placement
# 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
}

View File

@ -9,7 +9,7 @@ import (
"github.com/BurntSushi/xgb/xproto"
)
func main() {
func doDumpX() {
conn, err := xgb.NewConn()
if err != nil {
fmt.Println("Failed to connect to X server:", err)
@ -84,10 +84,9 @@ func main() {
}
*/
var test xproto.Drawable
geomReply, err := xproto.GetGeometry(conn, test).Reply()
geomReply, err := xproto.GetGeometry(conn, xproto.Drawable(child)).Reply()
if err != nil {
// fmt.Printf("err: %+v\n", err)
fmt.Printf("err: %+v\n", err)
// fmt.Printf("child geomReply: %+v\n", geomReply)
} else {
fmt.Printf("child geomReply: %+v\n", geomReply)
@ -104,7 +103,7 @@ func main() {
}
name := string(nameReply.Value)
if name == "Workspace1-Terminal" {
if name == "Terminal" {
target = child
break
}

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
}

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
}