├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── coverage.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── desktop_unix.go ├── go.mod ├── go.sum └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "dependencies" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | labels: 14 | - "dependencies" 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | matrix: 8 | go-version: [~1.12, ^1] 9 | os: [ubuntu-latest] 10 | runs-on: ${{ matrix.os }} 11 | env: 12 | GO111MODULE: "on" 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Download Go modules 23 | run: go mod download 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: go test ./... 30 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | coverage: 6 | strategy: 7 | matrix: 8 | go-version: [^1] 9 | os: [ubuntu-latest] 10 | runs-on: ${{ matrix.os }} 11 | env: 12 | GO111MODULE: "on" 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Coverage 23 | env: 24 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | run: | 26 | go test -race -covermode atomic -coverprofile=profile.cov ./... 27 | GO111MODULE=off go get github.com/mattn/goveralls 28 | $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obs-scene-switcher 2 | *.toml 3 | dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod download 6 | builds: 7 | - 8 | id: "obs-scene-switcher" 9 | binary: obs-scene-switcher 10 | ldflags: -s -w -X main.Version={{ .Version }} -X main.CommitSHA={{ .Commit }} 11 | goos: 12 | - linux 13 | goarch: 14 | - amd64 15 | - arm64 16 | - 386 17 | - arm 18 | goarm: 19 | - 6 20 | - 7 21 | 22 | archives: 23 | - id: default 24 | builds: 25 | - obs-scene-switcher 26 | replacements: 27 | 386: i386 28 | amd64: x86_64 29 | 30 | nfpms: 31 | - 32 | builds: 33 | - obs-scene-switcher 34 | 35 | vendor: muesli 36 | homepage: "https://fribbledom.com/" 37 | maintainer: "Christian Muehlhaeuser " 38 | description: "Tracks your active window and switches OBS scenes accordingly" 39 | license: MIT 40 | formats: 41 | - deb 42 | - rpm 43 | bindir: /usr/bin 44 | 45 | signs: 46 | - artifacts: checksum 47 | 48 | checksum: 49 | name_template: 'checksums.txt' 50 | snapshot: 51 | name_template: "{{ .Tag }}-next" 52 | changelog: 53 | sort: asc 54 | filters: 55 | exclude: 56 | - '^docs:' 57 | - '^test:' 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # obs-scene-switcher 2 | 3 | [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/muesli/obs-scene-switcher) 4 | [![Go ReportCard](https://goreportcard.com/badge/muesli/obs-scene-switcher)](https://goreportcard.com/report/muesli/obs-scene-switcher) 5 | 6 | obs-scene-switcher is a command-line remote control for OBS. It requires the 7 | [obs-websocket](https://github.com/Palakis/obs-websocket) plugin to be installed on your system. 8 | 9 | ## Installation 10 | 11 | ### Packages & Binaries 12 | 13 | On Arch Linux you can simply install the package from the AUR: 14 | 15 | yay -S obs-scene-switcher 16 | 17 | Or download a binary from the [releases](https://github.com/muesli/obs-scene-switcher/releases) 18 | page. Linux (including ARM) binaries are available, as well as Debian and RPM 19 | packages. 20 | 21 | ### Build From Source 22 | 23 | Alternatively you can also build `obs-scene-switcher` from source. Make sure you 24 | have a working Go environment (Go 1.12 or higher is required). See the 25 | [install instructions](http://golang.org/doc/install.html). 26 | 27 | To build obs-scene-switcher, simply run: 28 | 29 | go get github.com/muesli/obs-scene-switcher 30 | 31 | ## Configuration 32 | 33 | Edit scenes.toml and define which scenes you want to be connected to which 34 | windows, e.g.: 35 | 36 | ``` 37 | [[scenes]] 38 | scene_name = "IDE" 39 | window_class = "code-oss" 40 | 41 | [[scenes]] 42 | scene_name = "Terminal" 43 | window_name = "Konsole" 44 | 45 | [[scenes]] 46 | scene_name = "Browser" 47 | window_class = "Chromium" 48 | 49 | [[away_scenes]] 50 | scene_name = "Be Right Back" 51 | ``` 52 | 53 | In plain english, this means that whenever you focus your `VS Code` window, OBS 54 | will be asked to switch to the scene called `IDE`. If you focus your `Konsole` 55 | window it switches to the scene `Terminal`, and so on. 56 | 57 | The `away_scenes` define scenes which, when currently active, temporarily stop 58 | automatic scene switching. This is useful for keeping special scenes active, 59 | like a "Be Right Back" mode you only want to manually disable again. 60 | 61 | ## Usage 62 | 63 | Start obs-scene-switcher: 64 | 65 | ```bash 66 | obs-scene-switcher -config scenes.toml 67 | ``` 68 | -------------------------------------------------------------------------------- /desktop_unix.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "image" 9 | "log" 10 | "time" 11 | 12 | "github.com/BurntSushi/xgb" 13 | "github.com/BurntSushi/xgb/screensaver" 14 | "github.com/BurntSushi/xgb/xproto" 15 | "github.com/BurntSushi/xgbutil" 16 | "github.com/BurntSushi/xgbutil/ewmh" 17 | "github.com/BurntSushi/xgbutil/xgraphics" 18 | ) 19 | 20 | type Xorg struct { 21 | conn *xgb.Conn 22 | util *xgbutil.XUtil 23 | root xproto.Window 24 | activeAtom *xproto.InternAtomReply 25 | netNameAtom *xproto.InternAtomReply 26 | nameAtom *xproto.InternAtomReply 27 | classAtom *xproto.InternAtomReply 28 | activeWindow Window 29 | } 30 | 31 | type ActiveWindowChangedEvent struct { 32 | Window Window 33 | } 34 | 35 | type WindowClosedEvent struct { 36 | Window Window 37 | } 38 | 39 | type Window struct { 40 | ID uint32 41 | Class string 42 | Name string 43 | Icon image.Image 44 | } 45 | 46 | var ( 47 | ErrNoValue = errors.New("empty value") 48 | ErrNoClass = errors.New("empty class") 49 | ) 50 | 51 | func Connect(display string) Xorg { 52 | var x Xorg 53 | var err error 54 | 55 | x.conn, err = xgb.NewConnDisplay(display) 56 | if err != nil { 57 | log.Fatal("xgb:", err) 58 | } 59 | 60 | x.util, err = xgbutil.NewConnDisplay(display) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | err = screensaver.Init(x.conn) 66 | if err != nil { 67 | log.Fatal("screensaver:", err) 68 | } 69 | 70 | setup := xproto.Setup(x.conn) 71 | x.root = setup.DefaultScreen(x.conn).Root 72 | 73 | drw := xproto.Drawable(x.root) 74 | screensaver.SelectInput(x.conn, drw, screensaver.EventNotifyMask) 75 | 76 | x.activeAtom = x.atom("_NET_ACTIVE_WINDOW") 77 | x.netNameAtom = x.atom("_NET_WM_NAME") 78 | x.nameAtom = x.atom("WM_NAME") 79 | x.classAtom = x.atom("WM_CLASS") 80 | 81 | x.spy(x.root) 82 | return x 83 | } 84 | 85 | func (x Xorg) Close() { 86 | x.util.Conn().Close() 87 | x.conn.Close() 88 | } 89 | 90 | func (x *Xorg) TrackWindows(ch chan interface{}, timeout time.Duration) { 91 | if win, ok := x.window(); ok { 92 | x.activeWindow = win 93 | 94 | if ch != nil { 95 | go func() { 96 | ch <- ActiveWindowChangedEvent{ 97 | Window: win, 98 | } 99 | }() 100 | } 101 | } 102 | 103 | events := make(chan xgb.Event, 1) 104 | go x.waitForEvent(events) 105 | 106 | go func() { 107 | for { 108 | select { 109 | case event := <-events: 110 | switch e := event.(type) { 111 | case xproto.DestroyNotifyEvent: 112 | ch <- WindowClosedEvent{ 113 | Window: Window{ 114 | ID: uint32(e.Window), 115 | }, 116 | } 117 | 118 | case xproto.PropertyNotifyEvent: 119 | if win, ok := x.window(); ok { 120 | if win.ID != x.activeWindow.ID { 121 | x.activeWindow = win 122 | if ch != nil { 123 | go func() { 124 | ch <- ActiveWindowChangedEvent{ 125 | Window: win, 126 | } 127 | }() 128 | } 129 | } 130 | // Wakeup 131 | } 132 | case screensaver.NotifyEvent: 133 | switch e.State { 134 | case screensaver.StateOn: 135 | // Snooze(x.queryIdle()) 136 | default: 137 | // Wakeup 138 | } 139 | } 140 | case <-time.After(timeout): 141 | // Snooze(x.queryIdle()) 142 | } 143 | } 144 | }() 145 | } 146 | 147 | // ActiveWindow returns the currently active window 148 | func (x Xorg) ActiveWindow() Window { 149 | return x.activeWindow 150 | } 151 | 152 | func (x Xorg) RequestActivation(w Window) error { 153 | return ewmh.ActiveWindowReq(x.util, xproto.Window(w.ID)) 154 | } 155 | 156 | func (x Xorg) atom(aname string) *xproto.InternAtomReply { 157 | a, err := xproto.InternAtom(x.conn, true, uint16(len(aname)), aname).Reply() 158 | if err != nil { 159 | log.Fatal("atom:", err) 160 | } 161 | return a 162 | } 163 | 164 | func (x Xorg) property(w xproto.Window, a *xproto.InternAtomReply) (*xproto.GetPropertyReply, error) { 165 | return xproto.GetProperty(x.conn, false, w, a.Atom, xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply() 166 | } 167 | 168 | func (x Xorg) active() xproto.Window { 169 | p, err := x.property(x.root, x.activeAtom) 170 | if err != nil { 171 | return x.root 172 | } 173 | return xproto.Window(xgb.Get32(p.Value)) 174 | } 175 | 176 | func (x Xorg) name(w xproto.Window) (string, error) { 177 | name, err := x.property(w, x.netNameAtom) 178 | if err != nil { 179 | return "", err 180 | } 181 | if string(name.Value) == "" { 182 | name, err = x.property(w, x.nameAtom) 183 | if err != nil { 184 | return "", err 185 | } 186 | if string(name.Value) == "" { 187 | return "", ErrNoValue 188 | } 189 | } 190 | return string(name.Value), nil 191 | } 192 | 193 | func (x Xorg) icon(w xproto.Window) (image.Image, error) { 194 | icon, err := xgraphics.FindIcon(x.util, w, 64, 64) 195 | if err != nil { 196 | log.Printf("Could not find icon for window %d.", w) 197 | return nil, err 198 | } 199 | 200 | return icon, nil 201 | } 202 | 203 | func (x Xorg) class(w xproto.Window) (string, error) { 204 | class, err := x.property(w, x.classAtom) 205 | if err != nil { 206 | return "", err 207 | } 208 | zero := []byte{0} 209 | s := bytes.Split(bytes.TrimSuffix(class.Value, zero), zero) 210 | if l := len(s); l > 0 && len(s[l-1]) != 0 { 211 | return string(s[l-1]), nil 212 | } 213 | return "", ErrNoClass 214 | } 215 | 216 | func (x Xorg) window() (Window, bool) { 217 | id := x.active() 218 | /* skip invalid window id */ 219 | if id == 0 { 220 | return Window{}, false 221 | } 222 | class, err := x.class(id) 223 | if err != nil { 224 | return Window{}, false 225 | } 226 | name, err := x.name(id) 227 | if err != nil { 228 | return Window{}, false 229 | } 230 | icon, err := x.icon(id) 231 | if err != nil { 232 | return Window{}, false 233 | } 234 | x.spy(id) 235 | 236 | return Window{ 237 | ID: uint32(id), 238 | Class: class, 239 | Name: name, 240 | Icon: icon, 241 | }, true 242 | } 243 | 244 | func (x Xorg) spy(w xproto.Window) { 245 | xproto.ChangeWindowAttributes(x.conn, w, xproto.CwEventMask, 246 | []uint32{xproto.EventMaskPropertyChange | xproto.EventMaskStructureNotify}) 247 | } 248 | 249 | func (x Xorg) waitForEvent(events chan<- xgb.Event) { 250 | for { 251 | ev, err := x.conn.WaitForEvent() 252 | if err != nil { 253 | log.Println("wait for event:", err) 254 | continue 255 | } 256 | events <- ev 257 | } 258 | } 259 | 260 | func (x Xorg) queryIdle() time.Duration { 261 | info, err := screensaver.QueryInfo(x.conn, xproto.Drawable(x.root)).Reply() 262 | if err != nil { 263 | log.Println("query idle:", err) 264 | return 0 265 | } 266 | return time.Duration(info.MsSinceUserInput) * time.Millisecond 267 | } 268 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muesli/obs-scene-switcher 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect 7 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect 8 | github.com/BurntSushi/toml v1.2.1 9 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 10 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 11 | github.com/christopher-dG/go-obs-websocket v0.0.0-20181224025342-2efc3605bff5 12 | github.com/spf13/cobra v1.4.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= 2 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= 3 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= 4 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 5 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 6 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 7 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= 8 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 9 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= 10 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= 11 | github.com/christopher-dG/go-obs-websocket v0.0.0-20181224025342-2efc3605bff5 h1:VtKPsvxzKt/+EnkhcPp0Xg7MDjt/a+CNRSj5phITbjo= 12 | github.com/christopher-dG/go-obs-websocket v0.0.0-20181224025342-2efc3605bff5/go.mod h1:hFg9UFHefvNCvpWpYtOaP/VT2HyokIJsmV1AUBjpTeQ= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 14 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 15 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 16 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 17 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 18 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 19 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 20 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 21 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 22 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 23 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 24 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/BurntSushi/toml" 11 | obsws "github.com/christopher-dG/go-obs-websocket" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | configFile string 17 | host string 18 | port uint32 19 | 20 | config Config 21 | 22 | rootCmd = &cobra.Command{ 23 | Use: "obs-scene-switcher", 24 | Short: "obs-scene-switcher tracks your active window and switches scenes accordingly", 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | return execute() 27 | }, 28 | } 29 | 30 | client *obsws.Client 31 | recentWindows []Window 32 | ) 33 | 34 | type Scene struct { 35 | SceneName string `toml:"scene_name"` 36 | WindowClass string `toml:"window_class"` 37 | WindowName string `toml:"window_name"` 38 | } 39 | 40 | type Scenes []Scene 41 | 42 | type Config struct { 43 | Scenes Scenes `toml:"scenes"` 44 | AwayScenes Scenes `toml:"away_scenes"` 45 | } 46 | 47 | func LoadConfig(filename string) (Config, error) { 48 | config := Config{} 49 | b, err := ioutil.ReadFile(filename) 50 | if err != nil { 51 | return config, err 52 | } 53 | 54 | _, err = toml.Decode(string(b), &config) 55 | return config, err 56 | } 57 | 58 | func main() { 59 | var err error 60 | config, err = LoadConfig(configFile) 61 | if err != nil { 62 | fmt.Println("could not load config file:", configFile) 63 | os.Exit(1) 64 | } 65 | 66 | if err := rootCmd.Execute(); err != nil { 67 | fmt.Println(err) 68 | os.Exit(1) 69 | } 70 | 71 | if client != nil { 72 | _ = client.Disconnect() 73 | } 74 | } 75 | 76 | func init() { 77 | cobra.OnInitialize(connectOBS) 78 | rootCmd.PersistentFlags().StringVar(&configFile, "config", "scenes.toml", "path to config file") 79 | rootCmd.PersistentFlags().StringVar(&host, "host", "localhost", "host to connect to") 80 | rootCmd.PersistentFlags().Uint32VarP(&port, "port", "p", 4444, "port to connect to") 81 | } 82 | 83 | func handleActiveWindowChanged(event ActiveWindowChangedEvent) { 84 | fmt.Println(fmt.Sprintf("Active window changed to %s (%d, %s)", 85 | event.Window.Class, event.Window.ID, event.Window.Name)) 86 | 87 | // remove dupes 88 | i := 0 89 | for _, rw := range recentWindows { 90 | if rw.ID == event.Window.ID { 91 | continue 92 | } 93 | 94 | recentWindows[i] = rw 95 | i++ 96 | } 97 | recentWindows = recentWindows[:i] 98 | 99 | recentWindows = append([]Window{event.Window}, recentWindows...) 100 | if len(recentWindows) > 15 { 101 | recentWindows = recentWindows[0:15] 102 | } 103 | 104 | req := obsws.NewGetCurrentSceneRequest() 105 | resp, err := req.SendReceive(*client) 106 | for _, v := range config.AwayScenes { 107 | if err == nil && resp.Name == v.SceneName { 108 | fmt.Println("Skipping switch, in Away mode!") 109 | return 110 | } 111 | } 112 | 113 | for _, v := range config.Scenes { 114 | if event.Window.Class == v.WindowClass || 115 | event.Window.Name == v.WindowName { 116 | req := obsws.NewSetCurrentSceneRequest(v.SceneName) 117 | _ = req.Send(*client) 118 | } 119 | } 120 | } 121 | 122 | func handleWindowClosed(event WindowClosedEvent) { 123 | i := 0 124 | for _, rw := range recentWindows { 125 | if rw.ID == event.Window.ID { 126 | continue 127 | } 128 | 129 | recentWindows[i] = rw 130 | i++ 131 | } 132 | recentWindows = recentWindows[:i] 133 | } 134 | 135 | func execute() error { 136 | x := Connect(os.Getenv("DISPLAY")) 137 | defer x.Close() 138 | 139 | tch := make(chan interface{}) 140 | x.TrackWindows(tch, time.Second) 141 | 142 | for e := range tch { 143 | switch event := e.(type) { 144 | case ActiveWindowChangedEvent: 145 | handleActiveWindowChanged(event) 146 | 147 | case WindowClosedEvent: 148 | handleWindowClosed(event) 149 | } 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func connectOBS() { 156 | // disable obsws logging 157 | obsws.Logger = log.New(ioutil.Discard, "", log.LstdFlags) 158 | 159 | client = &obsws.Client{Host: host, Port: int(port)} 160 | if err := client.Connect(); err != nil { 161 | fmt.Println(err) 162 | os.Exit(1) 163 | } 164 | 165 | // Set the amount of time we can wait for a response. 166 | obsws.SetReceiveTimeout(time.Second * 2) 167 | } 168 | --------------------------------------------------------------------------------