├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bar.go ├── bar_test.go ├── command.go ├── command_test.go ├── doc.go ├── marks.go ├── marks_test.go ├── outputs.go ├── outputs_test.go ├── socket.go ├── socket_test.go ├── subscribe.go ├── subscribe_test.go ├── tree.go ├── tree_test.go ├── tree_utils.go ├── utils_test.go ├── version.go ├── version_test.go ├── workspaces.go └── workspaces_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | .idea 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | coverage.txt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: go 4 | 5 | go: 6 | - 1.6 7 | - 1.7 8 | - 1.8.x 9 | 10 | script: 11 | - go test -v -coverprofile=coverage.txt -covermode=atomic 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/mdirkse/i3ipc-go?status.svg)](http://godoc.org/github.com/mdirkse/i3ipc-go/) 2 | [![Build Status](https://travis-ci.org/mdirkse/i3ipc-go.svg?branch=master)](https://travis-ci.org/mdirkse/i3ipc-go) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/mdirkse/i3ipc-go)](https://goreportcard.com/report/github.com/mdirkse/i3ipc-go) 4 | [![codecov](https://codecov.io/gh/mdirkse/i3ipc-go/branch/master/graph/badge.svg)](https://codecov.io/gh/mdirkse/i3ipc-go) 5 | [![BCH compliance](https://bettercodehub.com/edge/badge/mdirkse/i3ipc-go?branch=master)](https://bettercodehub.com/) 6 | 7 | i3ipc 8 | ===== 9 | 10 | Overview 11 | -------- 12 | i3ipc is a golang library for convenient access to the IPC API of the [i3 window manager](http://i3wm.org). If you want to take a look at the documentation then head to [http://godoc.org/github.com/mdirkse/i3ipc-go/][doc]. 13 | 14 | Compatibility 15 | ------------- 16 | This library can be used with the i3 IPC as it is as of at least version [4.13](https://github.com/i3/i3/releases/tag/4.13). However, according to the i3 maintainers: 17 | > The IPC isn't versioned. It can change with every release and usually does in one way or another. We only try to avoid breaking changes to the documented values since we consider those stable, but with good enough reason even those can change. 18 | 19 | We'll do our best to make this library track the i3 IPC as closely as possible, but if you find anything missing (or broken) please file an issue or (even better) submit a pull request. 20 | 21 | Usage 22 | ----- 23 | Thanks to Go's built-in git support, you can start using i3ipc with a simple 24 | 25 | import "github.com/mdirkse/i3ipc" 26 | 27 | For everything except subscriptions, you will want to create an IPCSocket over which the communication will take place. This object has methods for all message types that i3 will accept, though some might be split into multiple methods (eg. *Get_Bar_Config*). You can create such a socket quite easily: 28 | 29 | ipcsocket, err := i3ipc.GetIPCSocket() 30 | 31 | As a simple example of what you could do next, let's get the version of i3 over our new socket: 32 | 33 | version, err := ipcsocket.GetVersion() 34 | 35 | For further commands, refer to `go doc` or use the aforementioned [website][doc]. 36 | 37 | ### Subscriptions 38 | i3ipc handles subscriptions in a convenient way: you don't have to think about managing the socket or watch out for unordered replies. The appropriate method simply returns a channel from which you can read Event objects. 39 | 40 | Here's a simple example - we start the event listener, we subscribe to workspace events, then simple print all of them as we receive them: 41 | 42 | i3ipc.StartEventListener() 43 | ws_events, err := i3ipc.Subscribe(i3ipc.I3WorkspaceEvent) 44 | for { 45 | event := <-ws_events 46 | fmt.Printf("Received an event: %v\n", event) 47 | } 48 | 49 | i3ipc currently has no way of subscribing to multiple event types over a single channel. If you want this, you can simply create multiple subscriptions, then demultiplex those channels yourself - `select` is your friend. 50 | 51 | Credits 52 | ------- 53 | Many thanks to [proxypoke](https://github.com/proxypoke) for originally starting this project. 54 | 55 | [doc]: http://godoc.org/github.com/mdirkse/i3ipc-go/ 56 | -------------------------------------------------------------------------------- /bar.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "encoding/json" 17 | ) 18 | 19 | // I3Bar represents the configuration of a bar. For documentation of the 20 | // fields, refer to http://i3wm.org/docs/ipc.html#_bar_config_reply. 21 | type I3Bar struct { 22 | ID string 23 | Mode string 24 | Position string 25 | StatusCommand string `json:"status_command"` 26 | Font string 27 | WorkspaceButtons bool `json:"workspace_buttons"` 28 | BindingModeIndicator bool `json:"binding_mode_indicator"` 29 | Verbose bool 30 | Colors Colors 31 | } 32 | 33 | // Colors represents colors as used in I3Bar. 34 | type Colors struct { 35 | Background string 36 | Statusline string 37 | Separator string 38 | FocusedBackground string `json:"focused_background"` 39 | FocusedStatusline string `json:"focused_statusline"` 40 | FocusedSeparator string `json:"focused_separator"` 41 | FocusedWorkspaceBg string `json:"focused_workspace_bg"` 42 | FocusedWorkspaceBorder string `json:"focused_workspace_border"` 43 | FocusedWorkspaceText string `json:"focused_workspace_text"` 44 | ActiveWorkspaceBg string `json:"active_workspace_bg"` 45 | ActiveWorkspaceBorder string `json:"active_workspace_border"` 46 | ActiveWorkspaceText string `json:"active_workspace_text"` 47 | InactiveWorkspaceBg string `json:"inactive_workspace_bg"` 48 | InactiveWorkspaceBorder string `json:"inactive_workspace_border"` 49 | InactiveWorkspaceText string `json:"inactive_workspace_text"` 50 | UrgentWorkspaceBg string `json:"urgent_workspace_bg"` 51 | UrgentWorkspaceBorder string `json:"urgent_workspace_border"` 52 | UrgentWorkspaceText string `json:"urgent_workspace_text"` 53 | BindingModeBg string `json:"binding_mode_bg"` 54 | BindingModeBorder string `json:"binding_mode_border"` 55 | BindingModeText string `json:"binding_mode_text"` 56 | } 57 | 58 | // GetBarIds fetches a list of IDs for all configured bars. 59 | func (socket *IPCSocket) GetBarIds() (ids []string, err error) { 60 | jsonReply, err := socket.Raw(I3GetBarConfig, "") 61 | if err != nil { 62 | return 63 | } 64 | 65 | err = json.Unmarshal(jsonReply, &ids) 66 | return 67 | } 68 | 69 | // GetBarConfig returns the configuration of the bar with the given ID. 70 | func (socket *IPCSocket) GetBarConfig(id string) (bar I3Bar, err error) { 71 | jsonReply, err := socket.Raw(I3GetBarConfig, id) 72 | if err != nil { 73 | return 74 | } 75 | 76 | err = json.Unmarshal(jsonReply, &bar) 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /bar_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | func TestGetBarConfig(t *testing.T) { 20 | ipc, _ := GetIPCSocket() 21 | 22 | go startTestIPCSocket(testMessages["bar"]) 23 | 24 | ids, err := ipc.GetBarIds() 25 | if err != nil { 26 | t.Errorf("Getting bar IDs failed: %v", err) 27 | } 28 | 29 | id := ids[0] 30 | //bar, err := GetBarConfig(ipc) 31 | _, err = ipc.GetBarConfig(id) 32 | if err != nil { 33 | t.Errorf("Getting bar config failed: %v", err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "encoding/json" 17 | ) 18 | 19 | // CommandError for replies from a command to i3. 20 | type CommandError string 21 | 22 | func (error CommandError) Error() string { 23 | return string(error) 24 | } 25 | 26 | // Struct for replies from command messages. 27 | type commandReply struct { 28 | Success bool 29 | Error string 30 | } 31 | 32 | // Command sends a command to i3. 33 | // FIXME: Doesn't support chained commands yet. 34 | func (socket *IPCSocket) Command(action string) (success bool, err error) { 35 | jsonReply, err := socket.Raw(I3Command, action) 36 | if err != nil { 37 | return 38 | } 39 | 40 | var cmdReply []commandReply 41 | err = json.Unmarshal(jsonReply, &cmdReply) 42 | if err != nil { 43 | return 44 | } 45 | 46 | if len(cmdReply) == 0 { 47 | success = true 48 | return 49 | } 50 | 51 | success = cmdReply[0].Success 52 | if cmdReply[0].Error == "" { 53 | err = nil 54 | } else { 55 | err = CommandError(cmdReply[0].Error) 56 | } 57 | 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | func TestCommand(t *testing.T) { 20 | ipc, _ := GetIPCSocket() 21 | 22 | go startTestIPCSocket(testMessages["command"]) 23 | 24 | defer ipc.Close() 25 | 26 | // `exec /bin/true` is a good NOP operation for testing 27 | success, err := ipc.Command("exec /bin/true") 28 | if !success { 29 | t.Error("Unsuccessful command.") 30 | } 31 | if err != nil { 32 | t.Errorf("An error occurred during command: %v", err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | // Package i3ipc implements interprocess communication facilities with the i3 14 | // window manager. 15 | package i3ipc 16 | -------------------------------------------------------------------------------- /marks.go: -------------------------------------------------------------------------------- 1 | // Author: slowpoke 2 | // Repository: https://github.com/proxypoke/i3ipc 3 | // 4 | // This program is free software under the terms of the 5 | // Do What The Fuck You Want To Public License. 6 | // It comes without any warranty, to the extent permitted by 7 | // applicable law. For a copy of the license, see COPYING or 8 | // head to http://sam.zoy.org/wtfpl/COPYING. 9 | 10 | package i3ipc 11 | 12 | import ( 13 | "encoding/json" 14 | ) 15 | 16 | // GetMarks returns a list of marks which are currently set. 17 | func (socket *IPCSocket) GetMarks() (marks []string, err error) { 18 | jsonReply, err := socket.Raw(I3GetMarks, "") 19 | if err != nil { 20 | return 21 | } 22 | 23 | err = json.Unmarshal(jsonReply, &marks) 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /marks_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | func TestGetMarks(t *testing.T) { 20 | ipc, _ := GetIPCSocket() 21 | 22 | go startTestIPCSocket(testMessages["marks"]) 23 | 24 | _, err := ipc.GetMarks() 25 | if err != nil { 26 | t.Errorf("Getting marks failed: %v", err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /outputs.go: -------------------------------------------------------------------------------- 1 | // Author: slowpoke 2 | // Repository: https://github.com/proxypoke/i3ipc 3 | // 4 | // This program is free software under the terms of the 5 | // Do What The Fuck You Want To Public License. 6 | // It comes without any warranty, to the extent permitted by 7 | // applicable law. For a copy of the license, see COPYING or 8 | // head to http://sam.zoy.org/wtfpl/COPYING. 9 | 10 | package i3ipc 11 | 12 | import ( 13 | "encoding/json" 14 | ) 15 | 16 | // Output represents an output. For documentation of the fields, 17 | // refer to http://i3wm.org/docs/ipc.html#_outputs_reply. 18 | type Output struct { 19 | Name string 20 | Active bool 21 | CurrentWorkspace string `json:"current_workspace"` 22 | Rect Rect 23 | } 24 | 25 | // GetOutputs fetches the list of current outputs. 26 | func (socket *IPCSocket) GetOutputs() (outputs []Output, err error) { 27 | jsonReply, err := socket.Raw(I3GetOutputs, "") 28 | if err != nil { 29 | return 30 | } 31 | 32 | err = json.Unmarshal(jsonReply, &outputs) 33 | if err == nil { 34 | return 35 | } 36 | // Outputs which aren't displaying any workspace will have JSON-null set as 37 | // their value for current_workspace. Since Go's equivalent, nil, can't be 38 | // assigned to strings, it will cause Unmarshall to return with an 39 | // UnmarshalTypeError, but otherwise correctly unmarshal the JSON input. We 40 | // simply ignore this error due to this reason. 41 | if _, ok := err.(*json.UnmarshalTypeError); ok { 42 | err = nil 43 | } 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /outputs_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | func TestGetOutputs(t *testing.T) { 20 | ipc, _ := GetIPCSocket() 21 | 22 | go startTestIPCSocket(testMessages["outputs"]) 23 | 24 | //outputs, err := GetOutputs(ipc) 25 | _, err := ipc.GetOutputs() 26 | if err != nil { 27 | t.Errorf("Getting output list failed: %v", err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /socket.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "net" 19 | "os/exec" 20 | "strings" 21 | "unsafe" 22 | ) 23 | 24 | const ( 25 | // Magic string for the IPC API. 26 | _Magic string = "i3-ipc" 27 | // The length of the i3 message "header" is 14 bytes: 6 for the _Magic 28 | // string, 4 for the length of the payload (int32 in native byte order) and 29 | // another 4 for the message type (also int32 in NBO). 30 | _Headerlen = 14 31 | ) 32 | 33 | var cs connSource = &socketConnSource{} 34 | 35 | // A Message from i3. Can either be a Reply or an Event. 36 | type Message struct { 37 | Payload []byte 38 | IsEvent bool 39 | Type int32 40 | } 41 | 42 | // A MessageType that Raw() accepts. 43 | type MessageType int32 44 | 45 | const ( 46 | I3Command MessageType = iota 47 | I3GetWorkspaces 48 | I3Subscribe 49 | I3GetOutputs 50 | I3GetTree 51 | I3GetMarks 52 | I3GetBarConfig 53 | I3GetVersion 54 | ) 55 | 56 | // MessageTypeError for unknown message types. 57 | type MessageTypeError string 58 | 59 | func (error MessageTypeError) Error() string { 60 | return string(error) 61 | } 62 | 63 | // MessageError for communication failures. 64 | type MessageError string 65 | 66 | func (error MessageError) Error() string { 67 | return string(error) 68 | } 69 | 70 | // IPCSocket represents a Unix socket to communicate with i3. 71 | type IPCSocket struct { 72 | socket net.Conn 73 | open bool 74 | subscribers []chan Event 75 | } 76 | 77 | type connSource interface { 78 | newConn() (net.Conn, error) 79 | } 80 | type socketConnSource struct{} 81 | 82 | // The default socket factory gets the socket via i3 83 | func (dsf *socketConnSource) newConn() (conn net.Conn, err error) { 84 | var out bytes.Buffer 85 | 86 | cmd := exec.Command("i3", "--get-socketpath") 87 | cmd.Stdout = &out 88 | err = cmd.Run() 89 | if err != nil { 90 | return 91 | } 92 | 93 | path := strings.TrimSpace(out.String()) 94 | conn, err = net.Dial("unix", path) 95 | return 96 | } 97 | 98 | // Close the connection to the underlying Unix socket. 99 | func (socket *IPCSocket) Close() error { 100 | socket.open = false 101 | return socket.socket.Close() 102 | } 103 | 104 | // GetIPCSocket creates a new IPC socket. 105 | func GetIPCSocket() (ipc *IPCSocket, err error) { 106 | ipc = &IPCSocket{} 107 | sock, err := cs.newConn() 108 | ipc.socket = sock 109 | ipc.open = true 110 | return 111 | } 112 | 113 | // Receive a raw json bytestring from the socket and return a Message. 114 | func (socket *IPCSocket) recv() (msg Message, err error) { 115 | header := make([]byte, _Headerlen) 116 | n, err := socket.socket.Read(header) 117 | 118 | // Check if this is a valid i3 message. 119 | if n != _Headerlen || err != nil { 120 | return 121 | } 122 | magicString := string(header[:len(_Magic)]) 123 | if magicString != _Magic { 124 | err = MessageError(fmt.Sprintf( 125 | "Invalid magic string: got %q, expected %q.", 126 | magicString, _Magic)) 127 | return 128 | } 129 | 130 | var bytelen [4]byte 131 | // Copy the byte values from the slice into the byte array. This is 132 | // necessary because the address of a slice does not point to the actual 133 | // values in memory. 134 | for i, b := range header[len(_Magic) : len(_Magic)+4] { 135 | bytelen[i] = b 136 | } 137 | length := *(*int32)(unsafe.Pointer(&bytelen)) 138 | 139 | msg.Payload = make([]byte, length) 140 | n, err = socket.socket.Read(msg.Payload) 141 | if n != int(length) || err != nil { 142 | return 143 | } 144 | 145 | // Figure out the type of message. 146 | var bytetype [4]byte 147 | for i, b := range header[len(_Magic)+4 : len(_Magic)+8] { 148 | bytetype[i] = b 149 | } 150 | messageType := *(*uint32)(unsafe.Pointer(&bytetype)) 151 | 152 | // Reminder: event messages have the highest bit of the type set to 1 153 | if messageType>>31 == 1 { 154 | msg.IsEvent = true 155 | } 156 | // Use the remaining bits 157 | msg.Type = int32(messageType & 0x7F) 158 | 159 | return 160 | } 161 | 162 | // Raw sends raw messages to i3. Returns a json byte string. 163 | func (socket *IPCSocket) Raw(messageType MessageType, args string) (jsonReply []byte, err error) { 164 | // Set up the parts of the message. 165 | var ( 166 | message = []byte(_Magic) 167 | payload = []byte(args) 168 | length = int32(len(payload)) 169 | bytelen [4]byte 170 | bytetype [4]byte 171 | ) 172 | 173 | // Black Magic™. 174 | bytelen = *(*[4]byte)(unsafe.Pointer(&length)) 175 | bytetype = *(*[4]byte)(unsafe.Pointer(&messageType)) 176 | 177 | for _, b := range bytelen { 178 | message = append(message, b) 179 | } 180 | for _, b := range bytetype { 181 | message = append(message, b) 182 | } 183 | for _, b := range payload { 184 | message = append(message, b) 185 | } 186 | 187 | _, err = socket.socket.Write(message) 188 | if err != nil { 189 | return 190 | } 191 | 192 | msg, err := socket.recv() 193 | if err == nil { 194 | jsonReply = msg.Payload 195 | } 196 | if msg.IsEvent { 197 | err = MessageTypeError("Received an event instead of a reply.") 198 | } 199 | return 200 | } 201 | -------------------------------------------------------------------------------- /socket_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | func TestGetIPCSocket(t *testing.T) { 20 | startTestIPCSocket(testMessages[""]) 21 | ipc, _ := GetIPCSocket() 22 | ipc.Close() 23 | if ipc.open { 24 | t.Error("IPC socket appears open after closing.") 25 | } 26 | } 27 | 28 | func TestRaw(t *testing.T) { 29 | ipc, _ := GetIPCSocket() 30 | 31 | go startTestIPCSocket(testMessages["workspaces"]) 32 | 33 | _, err := ipc.Raw(I3GetWorkspaces, "") 34 | if err != nil { 35 | t.Errorf("Raw message sending failed: %v", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /subscribe.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "encoding/json" 17 | "log" 18 | ) 19 | 20 | // EventType for subscribable events. 21 | type EventType int32 22 | 23 | // Enumeration of currently available event types. 24 | const ( 25 | I3WorkspaceEvent EventType = iota 26 | I3OutputEvent 27 | I3ModeEvent 28 | I3WindowEvent 29 | I3BarConfigUpdateEvent 30 | I3BindingEvent 31 | // private value used for setting up internal stuff in init() 32 | // The idea is that if there's a new type of event added to i3, it only 33 | // needs to be added here and in the payloads slice below, and the rest of 34 | // the code won't need to change. 35 | eventmax 36 | ) 37 | 38 | // This slice is used to map event types to their string representation. 39 | var payloads = []string{"workspace", "output", "mode", "window", "barconfig_update", "binding"} 40 | 41 | // AddEventType dynamically adds an event type by defining a name for it. 42 | // Just in case i3 adds a new one and this library hasn't been updated yet. 43 | // Returns the EventType which gets assigned to it. 44 | // 45 | // XXX: If you use this to add more than one new event type, add them in the 46 | // RIGHT ORDER. I hope this case never pops up (because that would mean that 47 | // this library is severely outdated), but I thought I'd warn you anyways. 48 | func AddEventType(name string) (eventType EventType) { 49 | payloads = append(payloads, name) 50 | return EventType(len(payloads) - 1) 51 | } 52 | 53 | // Event describes an event reply from i3. 54 | type Event struct { 55 | Type EventType 56 | // "change" is the name of the single field of the JSON map that i3 sends 57 | // when an event occurs, describing what happened. 58 | Change string 59 | } 60 | 61 | // Struct for replies from subscribe messages. 62 | type subscribeReply struct { 63 | Success bool 64 | } 65 | 66 | // SubscribeError represents a subscription-related error. 67 | type SubscribeError string 68 | 69 | func (subscribeError SubscribeError) Error() string { 70 | return string(subscribeError) 71 | } 72 | 73 | // Private subscribe function. Sets up the socket. 74 | func (socket *IPCSocket) subscribe(eventType EventType) (err error) { 75 | jsonReply, err := socket.Raw(I3Subscribe, "[\""+payloads[eventType]+"\"]") 76 | if err != nil { 77 | return 78 | } 79 | 80 | var subsReply subscribeReply 81 | err = json.Unmarshal(jsonReply, &subsReply) 82 | if err != nil { 83 | return 84 | } 85 | 86 | if !subsReply.Success { 87 | // TODO: Better error description. 88 | err = SubscribeError("Could not subscribe.") 89 | } 90 | return 91 | } 92 | 93 | // Subscribe to an event type. Returns a channel from which events can be read. 94 | func Subscribe(eventType EventType) (subs chan Event, err error) { 95 | if eventType >= eventmax || eventType < 0 { 96 | err = SubscribeError("No such event type.") 97 | return 98 | } 99 | subs = make(chan Event) 100 | eventSockets[eventType].subscribers = append( 101 | eventSockets[eventType].subscribers, subs) 102 | return 103 | } 104 | 105 | // Listen for events on this socket, multiplexing them to all subscribers. 106 | // 107 | // XXX: This will cause all messages which are not events to be DROPPED. 108 | func (socket *IPCSocket) listen() { 109 | for { 110 | if !socket.open { 111 | break 112 | } 113 | msg, err := socket.recv() 114 | // XXX: This ignores all errors. Maybe a FIXME, maybe not. 115 | if err != nil { 116 | continue 117 | } 118 | // Drop non-event messages. 119 | if !msg.IsEvent { 120 | continue 121 | } 122 | 123 | var event Event 124 | event.Type = EventType(msg.Type) 125 | json.Unmarshal(msg.Payload, &event) 126 | 127 | // Send each subscriber the event in a nonblocking manner. 128 | for _, subscriber := range socket.subscribers { 129 | select { 130 | case subscriber <- event: // NOP 131 | default: 132 | // If the event can't be written, just ignore this 133 | // subscriber. 134 | } 135 | } 136 | } 137 | } 138 | 139 | var eventSockets []*IPCSocket 140 | 141 | // StartEventListener makes the library listen to events on the i3 socket 142 | func StartEventListener() { 143 | // Check whether we have as much payloads as we have event types. You know, 144 | // just in case I'm coding on my third Club-Mate at 0400 in the morning when 145 | // updating this lib. 146 | if len(payloads) != int(eventmax) { 147 | log.Fatalf("Too much or not enough payloads: got %d, expected %d.\n", 148 | len(payloads), int(eventmax)) 149 | } 150 | 151 | // Set up an IPCSocket to receive events for every type of event. 152 | var ev EventType 153 | for ; ev < eventmax; ev++ { 154 | sock, err := GetIPCSocket() 155 | if err != nil { 156 | log.Fatalf("Can't get i3 socket. Please make sure i3 is running. %v.", err) 157 | } 158 | err = sock.subscribe(ev) 159 | if err != nil { 160 | log.Fatalf("Can't subscribe: %v", err) 161 | } 162 | go sock.listen() 163 | if err != nil { 164 | log.Fatalf("Can't set up event sockets: %v", err) 165 | } 166 | 167 | eventSockets = append(eventSockets, sock) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /subscribe_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | // TODO: fix this test 20 | func TestInit(t *testing.T) { 21 | // Because StartEventListener sends events to the socket we have to start 22 | // the test socket before calling it. In other tests we can first call 23 | // GetIPCSocket(), get the socket, and then start the server end of the socket 24 | // with startTestIPCSocket to be sure that the test socket is created 25 | // (newConn being called) before something is written to it. Here we don't have 26 | // that assurance, we're relying on timing, which is kind of terrible, but seems 27 | // to work. Should refactor this to be provably correct though. 28 | go startTestIPCSocket(testMessages["subscribe"]) 29 | 30 | StartEventListener() 31 | 32 | for _, s := range eventSockets { 33 | if !s.open { 34 | t.Error("Init failed: closed event socket found.") 35 | } 36 | } 37 | if len(eventSockets) != int(eventmax) { 38 | t.Errorf("Too much or not enough event sockets. Got %d, expected %d.\n", 39 | len(eventSockets), int(eventmax)) 40 | } 41 | 42 | _, err := Subscribe(I3WorkspaceEvent) 43 | if err != nil { 44 | t.Errorf("Failed to subscribe: %f\n", err) 45 | } 46 | // TODO: A test to ensure that subscriptions work as intended. 47 | } 48 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "encoding/json" 17 | ) 18 | 19 | // I3Node represents a Node in the i3 tree. For documentation of the fields, 20 | // refer to http://i3wm.org/docs/ipc.html#_tree_reply. 21 | type I3Node struct { 22 | //int32 isn't large enough to hold all the ids 23 | ID int64 24 | Name string 25 | Type string 26 | Border string 27 | CurrentBorderWidth int32 `json:"current_border_width"` 28 | Layout string 29 | Orientation string 30 | Percent float64 31 | Rect Rect 32 | WindowRect Rect 33 | DecoRect Rect `json:"deco_rect"` 34 | Geometry Rect 35 | Window int32 36 | Urgent bool 37 | Focused bool 38 | Floating_Nodes []I3Node 39 | Nodes []I3Node 40 | Parent *I3Node 41 | 42 | // Properties, not listed in docs: 43 | Window_Properties struct { 44 | // Transient_for ? 45 | Title string 46 | Instance string 47 | Class string 48 | } 49 | // Swallows []I3Node ? 50 | Sticky bool 51 | Floating string 52 | Last_Split_Layout string 53 | // Focus []I3Node ? 54 | Fullscreen_Mode int32 55 | Scratchpad_State string 56 | Workspace_Layout string 57 | } 58 | 59 | // Traverses the tree setting correct reference to a parent node. 60 | func setParent(node, parent *I3Node) { 61 | 62 | node.Parent = parent 63 | 64 | for i := range node.Nodes { 65 | setParent(&node.Nodes[i], node) 66 | } 67 | for i := range node.Floating_Nodes { 68 | setParent(&node.Floating_Nodes[i], node) 69 | } 70 | } 71 | 72 | // GetTree fetches the layout tree. 73 | func (socket *IPCSocket) GetTree() (root I3Node, err error) { 74 | jsonReply, err := socket.Raw(I3GetTree, "") 75 | if err != nil { 76 | return 77 | } 78 | 79 | defer setParent(&root, nil) 80 | 81 | err = json.Unmarshal(jsonReply, &root) 82 | if err == nil { 83 | return 84 | } 85 | // For an explanation of this error silencing, see GetOutputs(). 86 | if _, ok := err.(*json.UnmarshalTypeError); ok { 87 | err = nil 88 | } 89 | return 90 | } 91 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | // Author: slowpoke 2 | // Repository: https://github.com/proxypoke/i3ipc 3 | // 4 | // This program is free software under the terms of the 5 | // Do What The Fuck You Want To Public License. 6 | // It comes without any warranty, to the extent permitted by 7 | // applicable law. For a copy of the license, see COPYING or 8 | // head to http://sam.zoy.org/wtfpl/COPYING. 9 | 10 | package i3ipc 11 | 12 | import ( 13 | "testing" 14 | ) 15 | 16 | func TestGetTree(t *testing.T) { 17 | ipc, _ := GetIPCSocket() 18 | 19 | go startTestIPCSocket(testMessages["tree"]) 20 | 21 | //root, err := GetTree(ipc) 22 | _, err := ipc.GetTree() 23 | if err != nil { 24 | t.Errorf("Getting tree failed: %v", err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tree_utils.go: -------------------------------------------------------------------------------- 1 | package i3ipc 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // Returns the root node of the tree. 9 | func (self *I3Node) Root() *I3Node { 10 | if self.Parent == nil { 11 | return self 12 | } 13 | return self.Parent.Root() 14 | } 15 | 16 | // Returns a slice of all descendent nodes. 17 | func (self *I3Node) Descendents() []*I3Node { 18 | 19 | var collectDescendents func(*I3Node, []*I3Node) []*I3Node 20 | 21 | // Collects descendent nodes recursively 22 | collectDescendents = func(n *I3Node, collected []*I3Node) []*I3Node { 23 | for i := range n.Nodes { 24 | collected = append(collected, &n.Nodes[i]) 25 | collected = collectDescendents(&n.Nodes[i], collected) 26 | } 27 | for i := range n.Floating_Nodes { 28 | collected = append(collected, &n.Floating_Nodes[i]) 29 | collected = collectDescendents(&n.Floating_Nodes[i], collected) 30 | } 31 | return collected 32 | } 33 | 34 | return collectDescendents(self, nil) 35 | } 36 | 37 | // Returns nodes that has no children nodes (leaves). 38 | func (self *I3Node) Leaves() (leaves []*I3Node) { 39 | 40 | nodes := self.Descendents() 41 | 42 | for i := range nodes { 43 | node := nodes[i] 44 | 45 | is_dockarea := node.Parent != nil && node.Parent.Type == "dockarea" 46 | 47 | if len(node.Nodes) == 0 && node.Type == "con" && !is_dockarea { 48 | leaves = append(leaves, node) 49 | } 50 | } 51 | return 52 | } 53 | 54 | // Returns all nodes of workspace type. 55 | func (self *I3Node) Workspaces() (workspaces []*I3Node) { 56 | 57 | nodes := self.Descendents() 58 | 59 | for i := range nodes { 60 | i3_special := strings.HasPrefix(nodes[i].Name, "__") 61 | if nodes[i].Type == "workspace" && !i3_special { 62 | workspaces = append(workspaces, nodes[i]) 63 | } 64 | } 65 | return 66 | } 67 | 68 | // Returns a node that is being focused now. 69 | func (self *I3Node) FindFocused() *I3Node { 70 | 71 | nodes := self.Descendents() 72 | 73 | for i := range nodes { 74 | if nodes[i].Focused { 75 | return nodes[i] 76 | } 77 | } 78 | return nil 79 | } 80 | 81 | // Returns a node that has given id. 82 | func (self *I3Node) FindByID(ID int64) *I3Node { 83 | 84 | nodes := self.Descendents() 85 | 86 | for i := range nodes { 87 | if nodes[i].ID == ID { 88 | return nodes[i] 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | // Returns a node that has given window id. 95 | func (self *I3Node) FindByWindow(window int32) *I3Node { 96 | 97 | nodes := self.Descendents() 98 | 99 | for i := range nodes { 100 | if nodes[i].Window == window { 101 | return nodes[i] 102 | } 103 | } 104 | return nil 105 | } 106 | 107 | // Returns nodes which name matches the regexp. 108 | func (self *I3Node) FindNamed(name string) []*I3Node { 109 | 110 | nodes := self.Descendents() 111 | reName := regexp.MustCompile(name) 112 | var found []*I3Node 113 | 114 | for _, node := range nodes { 115 | if reName.MatchString(node.Name) { 116 | found = append(found, node) 117 | } 118 | } 119 | return found 120 | } 121 | 122 | // Looks for a workspace up the tree. 123 | func (self *I3Node) Workspace() *I3Node { 124 | 125 | if self.Parent == nil { // the root node 126 | return nil 127 | } 128 | if self.Parent.Type == "workspace" { 129 | return self.Parent 130 | } 131 | 132 | return self.Parent.Workspace() 133 | } 134 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package i3ipc 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | ) 7 | 8 | type i3Message struct { 9 | messageType MessageType 10 | payload string 11 | } 12 | 13 | type testRequestResponse struct { 14 | req, res i3Message 15 | } 16 | 17 | var server, client net.Conn 18 | 19 | // Init the test connSource 20 | func init() { 21 | cs = &testConnSource{} 22 | } 23 | 24 | // Listens for input and the produces i3 message output according 25 | // to the req/resp data it is given. Will repeat for every testRequestResponse 26 | // in the array, so make sure the IPC operation you're testing has the same 27 | // number of IPC calls as there are elements in the array. 28 | func startTestIPCSocket(data []testRequestResponse) { 29 | defer server.Close() 30 | var tmp []byte 31 | length := make([]byte, 4) 32 | mType := make([]byte, 4) 33 | 34 | // For every pre-programmed request/response pair we listen to the pipe and then respond 35 | for _, trr := range data { 36 | tmp = make([]byte, 256) 37 | ipcMsg := make([]byte, 0) 38 | 39 | binary.LittleEndian.PutUint32(length, uint32(len(trr.res.payload))) 40 | binary.LittleEndian.PutUint32(mType, uint32(trr.res.messageType)) 41 | 42 | for _, a := range [][]byte{[]byte(_Magic), length, mType, []byte(trr.res.payload)} { 43 | ipcMsg = append(ipcMsg, a...) 44 | } 45 | 46 | server.Read(tmp) 47 | server.Write(ipcMsg) 48 | } 49 | } 50 | 51 | type testConnSource struct{} 52 | 53 | // Creates a new pipe that is used to supply test values to the socket 54 | func (dsf *testConnSource) newConn() (net.Conn, error) { 55 | // If we get called again after already opening a connection 56 | // (during the event subscription test for instance) 57 | // then make sure to close the existing connection (if there 58 | // is one) so we don't leak connections 59 | if server != nil { 60 | server.Close() 61 | } 62 | 63 | server, client = net.Pipe() 64 | return client, nil 65 | } 66 | 67 | // Test messages used in the various unit test. The source for most of the test JSON is: http://i3wm.org/docs/ipc.html 68 | var testMessages = map[string][]testRequestResponse{ 69 | "bar": {{ 70 | i3Message{I3GetBarConfig, ""}, 71 | i3Message{I3GetBarConfig, "[\"bar-bxuqzf\"]"}, 72 | }, { 73 | i3Message{I3GetBarConfig, "bar-bxuqzf"}, 74 | i3Message{I3GetBarConfig, "{\"id\": \"bar-bxuqzf\",\"mode\": \"dock\",\"position\": \"bottom\",\"status_command\": \"i3status\",\"font\": \"-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1\",\"workspace_buttons\": true,\"binding_mode_indicator\": true,\"verbose\": false,\"colors\": {\"background\": \"#c0c0c0\",\"statusline\": \"#00ff00\",\"focused_workspace_text\": \"#ffffff\",\"focused_workspace_bg\": \"#000000\"}}"}, 75 | }}, 76 | "command": {{ 77 | i3Message{I3Command, "exec /bin/true"}, 78 | i3Message{I3Command, "[{ \"success\": true }]"}, 79 | }}, 80 | "marks": {{ 81 | i3Message{I3GetMarks, ""}, 82 | i3Message{I3GetMarks, "[]"}, 83 | }}, 84 | "outputs": {{ 85 | i3Message{I3GetOutputs, ""}, 86 | i3Message{I3GetOutputs, "[{\"name\": \"LVDS1\",\"active\": true,\"current_workspace\": \"4\",\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 800}},{\"name\": \"VGA1\",\"active\": true,\"current_workspace\": \"1\",\"rect\": {\"x\": 1280,\"y\": 0,\"width\": 1280,\"height\": 1024}}]"}, 87 | }}, 88 | "subscribe": { 89 | { 90 | i3Message{I3Subscribe, ""}, 91 | i3Message{I3Subscribe, "{ \"success\": true }"}, 92 | }, 93 | { 94 | i3Message{I3Subscribe, ""}, 95 | i3Message{I3Subscribe, "{ \"success\": true }"}, 96 | }, 97 | { 98 | i3Message{I3Subscribe, ""}, 99 | i3Message{I3Subscribe, "{ \"success\": true }"}, 100 | }, 101 | { 102 | i3Message{I3Subscribe, ""}, 103 | i3Message{I3Subscribe, "{ \"success\": true }"}, 104 | }, 105 | { 106 | i3Message{I3Subscribe, ""}, 107 | i3Message{I3Subscribe, "{ \"success\": true }"}, 108 | }, 109 | { 110 | i3Message{I3Subscribe, ""}, 111 | i3Message{I3Subscribe, "{ \"success\": true }"}, 112 | }, 113 | }, 114 | "tree": {{ 115 | i3Message{I3GetTree, ""}, 116 | i3Message{I3GetTree, "{\"id\": 6875648,\"name\": \"root\",\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 800},\"nodes\": [{\"id\": 6878320,\"name\": \"LVDS1\",\"layout\": \"output\",\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 800},\"nodes\": [{\"id\": 6878784,\"name\": \"topdock\",\"layout\": \"dockarea\",\"orientation\": \"vertical\",\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 0}},{\"id\": 6879344,\"name\": \"content\",\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 782},\"nodes\": [{\"id\": 6880464,\"name\": \"1\",\"orientation\": \"horizontal\",\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 782},\"floating_nodes\": [],\"nodes\": [{\"id\": 6929968,\"name\": \"#aa0000\",\"border\": \"normal\",\"percent\": 1,\"rect\": {\"x\": 0,\"y\": 18,\"width\": 1280,\"height\": 782}}]}]},{\"id\": 6880208,\"name\": \"bottomdock\",\"layout\": \"dockarea\",\"orientation\": \"vertical\",\"rect\": {\"x\": 0,\"y\": 782,\"width\": 1280,\"height\": 18},\"nodes\": [{\"id\": 6931312,\"name\": \"#00aa00\",\"percent\": 1,\"rect\": {\"x\": 0,\"y\": 782,\"width\": 1280,\"height\": 18}}]}]}]}"}, 117 | }}, 118 | "version": {{ 119 | i3Message{I3GetVersion, ""}, 120 | i3Message{I3GetVersion, "{\"human_readable\" : \"4.2-169-gf80b877 (2012-08-05, branch \\\"next\\\")\",\"loaded_config_file_name\" : \"/home/hwangcc23/.i3/config\",\"minor\" : 2,\"patch\" : 0,\"major\" : 4}"}, 121 | }}, 122 | "workspaces": {{ 123 | i3Message{I3GetWorkspaces, ""}, 124 | i3Message{I3GetWorkspaces, "[{\"num\": 0,\"name\": \"1\",\"visible\": true,\"focused\": true,\"urgent\": false,\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 800},\"output\": \"LVDS1\"},{\"num\": 1,\"name\": \"2\",\"visible\": false,\"focused\": false,\"urgent\": false,\"rect\": {\"x\": 0,\"y\": 0,\"width\": 1280,\"height\": 800},\"output\": \"LVDS1\"}]"}, 125 | }}, 126 | } 127 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "encoding/json" 17 | ) 18 | 19 | // I3Version represents the version of i3. For documentation of the 20 | // fields, refer to http://i3wm.org/docs/ipc.html#_version_reply. 21 | type I3Version struct { 22 | Major int32 23 | Minor int32 24 | Patch int32 25 | HumanReadable string `json:"human_readable"` 26 | LoadedConfigFileName string `json:"loaded_config_file_name"` 27 | } 28 | 29 | // GetVersion fetches the version of i3. 30 | func (socket *IPCSocket) GetVersion() (version I3Version, err error) { 31 | jsonReply, err := socket.Raw(I3GetVersion, "") 32 | if err != nil { 33 | return 34 | } 35 | 36 | err = json.Unmarshal(jsonReply, &version) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | func TestGetVersion(t *testing.T) { 20 | ipc, _ := GetIPCSocket() 21 | 22 | go startTestIPCSocket(testMessages["version"]) 23 | 24 | _, err := ipc.GetVersion() 25 | if err != nil { 26 | t.Errorf("Getting version failed: %v", err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /workspaces.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "encoding/json" 17 | ) 18 | 19 | // Workspace represents a workspace. For documentation of the fields, 20 | // refer to http://i3wm.org/docs/ipc.html#_workspaces_reply. 21 | type Workspace struct { 22 | Num int32 23 | Name string 24 | Visible bool 25 | Focused bool 26 | Urgent bool 27 | Rect Rect 28 | Output string 29 | } 30 | 31 | // Rect represents the geometry of a window, output or workspace. 32 | type Rect struct { 33 | X int32 34 | Y int32 35 | Width int32 36 | Height int32 37 | } 38 | 39 | // GetWorkspaces fetches a list of all current workspaces. 40 | func (socket *IPCSocket) GetWorkspaces() (workspaces []Workspace, err error) { 41 | jsonReply, err := socket.Raw(I3GetWorkspaces, "") 42 | if err != nil { 43 | return 44 | } 45 | 46 | err = json.Unmarshal(jsonReply, &workspaces) 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /workspaces_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package i3ipc 14 | 15 | import ( 16 | "testing" 17 | ) 18 | 19 | func TestGetWorkspaces(t *testing.T) { 20 | ipc, _ := GetIPCSocket() 21 | 22 | go startTestIPCSocket(testMessages["workspaces"]) 23 | 24 | _, err := ipc.GetWorkspaces() 25 | if err != nil { 26 | t.Errorf("Getting workspace list failed: %v", err) 27 | } 28 | } 29 | --------------------------------------------------------------------------------