Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
|
94bd4728ba | |
|
c3f5588365 | |
|
d5069b63f8 | |
|
692843678a | |
|
75c89281eb | |
|
6dd0052dcf | |
|
9d5bd8d5b9 | |
|
e37836bb61 | |
|
71c3ff6642 | |
|
0d2cd8082b | |
|
944fc8685d | |
|
d3f10b0341 | |
|
60d8edcb03 | |
|
78cbaac691 | |
|
71909226e1 |
|
@ -1,7 +1,9 @@
|
||||||
*.swp
|
*.swp
|
||||||
go.mod
|
go.*
|
||||||
go.sum
|
*.pb.go
|
||||||
|
|
||||||
files/
|
files/
|
||||||
|
|
||||||
xstartplacement
|
xstartplacement
|
||||||
|
startxplacement
|
||||||
|
devilspie/devilspie2
|
||||||
|
|
21
Makefile
21
Makefile
|
@ -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
|
||||||
|
|
|
@ -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'
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
|
@ -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!
|
|
@ -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.")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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("")
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
69
stuff.go
69
stuff.go
|
@ -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.")
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue