├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── dirgui-gif │ └── main.go └── dirgui │ ├── main.go │ └── ui.go ├── go.mod ├── go.sum └── rfb ├── image.go ├── rfb-protocol.txt └── rfb.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | dirgui 8 | dirgui-gif 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tom Lieber 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 | # dirgui 2 | 3 | @rsnous on [Jan 11, 2021](https://twitter.com/rsnous/status/1348883726642544640): "idea: filesystem<->GUI adapter, where a directory turns into a form, executable files inside that directory turn into buttons in the form, text files into text areas, image files into image views, etc" 4 | 5 | @rsnous on [Jan 13, 2021](https://twitter.com/rsnous/status/1349426809088065536): "now wonder if you could make a graphical application that you just VNC into" 6 | 7 | And so dirgui was born… 8 | 9 | * cmd/dirgui/main.go implements a VNC server to host the GUI, using the RFB 3.3 or 3.8 protocols, specifically 10 | * cmd/dirgui/ui.go implements a GUI (drawn with Go's built-in image library) that creates a widget for each file in a directory, a button for each executable and a single-line text field for all other files 11 | 12 | Custom per-file editors are supported. For example, to use a custom editor for foo.gif, build cmd/dirgui-gif and copy/symlink its binary to "foo.gif.gui". dirgui-gif implements a VNC server whose contents will be spliced into dirgui. (Key and pointer events are not yet forwarded, though…) 13 | 14 | --- 15 | 16 | For help, e-mail tom@alltom.com or contact [@alltom](https://twitter.com/alltom) on Twitter 17 | 18 | I recommend copying the parts you need into your project. I don't consider this module's API stable at all. 19 | -------------------------------------------------------------------------------- /cmd/dirgui-gif/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "flag" 7 | "fmt" 8 | "github.com/alltom/dirgui/rfb" 9 | "image" 10 | "image/draw" 11 | "image/gif" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "net" 16 | "os" 17 | "time" 18 | ) 19 | 20 | var vncAddr = flag.String("parent_vnc_addr", "", "If present, instead of starting a VNC server, will connect to the given addr as a VNC server") 21 | 22 | func main() { 23 | flag.Parse() 24 | 25 | if flag.NArg() != 1 { 26 | log.Fatalf("expected one argument, the path to a GIF, but found %q", flag.Args()) 27 | } 28 | 29 | g, err := loadGif(flag.Arg(0)) 30 | if err != nil { 31 | log.Fatalf("couldn't load gif: %v", err) 32 | } 33 | 34 | bounds := g.Image[0].Bounds() 35 | for _, img := range g.Image { 36 | bounds = bounds.Union(img.Bounds()) 37 | } 38 | 39 | var frames []image.Image 40 | accum := image.NewNRGBA(bounds) 41 | draw.Draw(accum, accum.Bounds(), g.Image[0], image.ZP, draw.Src) 42 | for _, img := range g.Image { 43 | draw.Draw(accum, accum.Bounds(), img, image.ZP, draw.Over) 44 | 45 | frame := image.NewNRGBA(bounds) 46 | draw.Draw(frame, frame.Bounds(), accum, image.ZP, draw.Src) 47 | frames = append(frames, frame) 48 | } 49 | 50 | imgs := make(chan image.Image) 51 | 52 | go func() { 53 | for i := 0; ; i = (i + 1) % len(frames) { 54 | imgs <- frames[i] 55 | time.Sleep(time.Millisecond * time.Duration(g.Delay[i]*10)) 56 | } 57 | }() 58 | 59 | if *vncAddr == "" { 60 | ln, err := net.Listen("tcp", "127.0.0.1:5900") 61 | if err != nil { 62 | log.Fatalf("couldn't listen: %v", err) 63 | } 64 | log.Print("listening…") 65 | for { 66 | conn, err := ln.Accept() 67 | if err != nil { 68 | log.Fatalf("couldn't accept connection: %v", err) 69 | } 70 | log.Print("accepted connection") 71 | go func(conn net.Conn) { 72 | if err := rfbServe(conn, bounds, imgs); err != nil { 73 | log.Printf("serve failed: %v", err) 74 | } 75 | if err := conn.Close(); err != nil { 76 | log.Printf("couldn't close connection: %v", err) 77 | } 78 | }(conn) 79 | } 80 | } else { 81 | conn, err := net.Dial("tcp", *vncAddr) 82 | if err != nil { 83 | log.Fatalf("couldn't connect to %q: %v", *vncAddr, err) 84 | } 85 | 86 | if err := rfbServe(conn, bounds, imgs); err != nil { 87 | log.Printf("serve failed: %v", err) 88 | } 89 | if err := conn.Close(); err != nil { 90 | log.Printf("couldn't close connection: %v", err) 91 | } 92 | } 93 | } 94 | 95 | func loadGif(path string) (*gif.GIF, error) { 96 | f, err := os.Open(path) 97 | if err != nil { 98 | return nil, fmt.Errorf("couldn't open %q: %v", path, err) 99 | } 100 | defer f.Close() 101 | 102 | return gif.DecodeAll(f) 103 | } 104 | 105 | func rfbServe(conn io.ReadWriter, rect image.Rectangle, imgs <-chan image.Image) error { 106 | buf := make([]byte, 256) 107 | w := bufio.NewWriter(conn) 108 | 109 | var bo = binary.BigEndian 110 | var pixelFormat = rfb.PixelFormat{ 111 | BitsPerPixel: 32, 112 | BitDepth: 24, 113 | BigEndian: true, 114 | TrueColor: true, 115 | 116 | RedMax: 255, 117 | GreenMax: 255, 118 | BlueMax: 255, 119 | RedShift: 24, 120 | GreenShift: 16, 121 | BlueShift: 8, 122 | } 123 | var updateRequest rfb.FramebufferUpdateRequest 124 | var update rfb.FramebufferUpdate 125 | 126 | if _, err := io.WriteString(conn, "RFB 003.008\n"); err != nil { 127 | return fmt.Errorf("couldn't write ProtocolVersion: %v", err) 128 | } 129 | 130 | var major, minor int 131 | if _, err := io.ReadFull(conn, buf[:12]); err != nil { 132 | return fmt.Errorf("couldn't read ProtocolVersion: %v", err) 133 | } 134 | if _, err := fmt.Sscanf(string(buf[:12]), "RFB %03d.%03d\n", &major, &minor); err != nil { 135 | return fmt.Errorf("couldn't parse ProtocolVersion %q: %v", buf[:12], err) 136 | } 137 | 138 | if major == 3 && minor == 3 { 139 | // RFB 3.3 140 | bo.PutUint32(buf, 1) 141 | if _, err := conn.Write(buf[:4]); err != nil { 142 | return fmt.Errorf("couldn't write authentication scheme: %v", err) 143 | } 144 | } else if major == 3 && minor == 8 { 145 | // RFB 3.8 146 | if _, err := conn.Write([]byte{1, 1}); err != nil { 147 | return fmt.Errorf("couldn't write security types: %v", err) 148 | } 149 | 150 | if _, err := io.ReadFull(conn, buf[:1]); err != nil { 151 | return fmt.Errorf("couldn't read security type: %v", err) 152 | } 153 | if buf[0] != 1 { 154 | return fmt.Errorf("client must use security type 1, got %q", buf[0]) 155 | } 156 | 157 | if _, err := conn.Write([]byte{0}); err != nil { 158 | return fmt.Errorf("couldn't confirm security type: %v", err) 159 | } 160 | } else { 161 | return fmt.Errorf("server only supports RFB 3.3 and 3.8, but client requested %d.%d", major, minor) 162 | } 163 | 164 | if _, err := io.ReadFull(conn, buf[:1]); err != nil { 165 | return fmt.Errorf("couldn't read ClientInit: %v", err) 166 | } 167 | if buf[0] == 0 { 168 | log.Print("client requests other clients be disconnected") 169 | } else { 170 | log.Print("client requests other clients remain connected") 171 | } 172 | 173 | bo.PutUint16(buf[0:], uint16(rect.Max.X)) 174 | bo.PutUint16(buf[2:], uint16(rect.Max.Y)) 175 | pixelFormat.Write(buf[4:], bo) 176 | bo.PutUint32(buf[20:], 3) // length of name 177 | copy(buf[24:], "YO!") 178 | if _, err := conn.Write(buf[:27]); err != nil { 179 | return fmt.Errorf("couldn't write ServerInit: %v", err) 180 | } 181 | 182 | for { 183 | if _, err := io.ReadFull(conn, buf[:1]); err != nil { 184 | return fmt.Errorf("couldn't read message type: %v", err) 185 | } 186 | switch buf[0] { 187 | case 0: // SetPixelFormat 188 | if _, err := io.ReadFull(conn, buf[:3+rfb.PixelFormatEncodingLength]); err != nil { 189 | return fmt.Errorf("couldn't read pixel format in SetPixelFormat: %v", err) 190 | } 191 | pixelFormat.Read(buf[3:], bo) 192 | 193 | case 2: // SetEncodings 194 | if _, err := io.ReadFull(conn, buf[:3]); err != nil { 195 | return fmt.Errorf("couldn't read number of encodings in SetEncodings: %v", err) 196 | } 197 | encodingCount := bo.Uint16(buf[1:]) 198 | if _, err := io.Copy(ioutil.Discard, &io.LimitedReader{R: conn, N: 4 * int64(encodingCount)}); err != nil { 199 | return fmt.Errorf("couldn't read SetEncodings encoding list: %v", err) 200 | } 201 | 202 | case 3: // FramebufferUpdateRequest 203 | if _, err := io.ReadFull(conn, buf[:rfb.FramebufferUpdateRequestEncodingLength]); err != nil { 204 | return fmt.Errorf("couldn't read FramebufferUpdateRequest: %v", err) 205 | } 206 | updateRequest.Read(buf, bo) 207 | 208 | img := rfb.NewPixelFormatImage(pixelFormat, image.Rect(int(updateRequest.X), int(updateRequest.Y), int(updateRequest.X)+int(updateRequest.Width), int(updateRequest.Y)+int(updateRequest.Height))) 209 | draw.Draw(img, img.Bounds(), <-imgs, image.ZP, draw.Src) 210 | update.Rectangles = []*rfb.FramebufferUpdateRect{ 211 | &rfb.FramebufferUpdateRect{ 212 | X: updateRequest.X, Y: updateRequest.Y, Width: updateRequest.Width, Height: updateRequest.Height, 213 | EncodingType: 0, PixelData: img.Pix, 214 | }, 215 | } 216 | 217 | if _, err := w.Write([]byte{0, 0}); err != nil { // message type and padding 218 | return fmt.Errorf("couldn't write FramebufferUpdate header: %v", err) 219 | } 220 | if err := update.Write(w, bo); err != nil { 221 | return fmt.Errorf("couldn't write FramebufferUpdate: %v", err) 222 | } 223 | if err := w.Flush(); err != nil { 224 | return fmt.Errorf("couldn't write FramebufferUpdate: %v", err) 225 | } 226 | 227 | case 4: // KeyEvent 228 | if _, err := io.Copy(ioutil.Discard, &io.LimitedReader{R: conn, N: rfb.KeyEventEncodingLength}); err != nil { 229 | return fmt.Errorf("couldn't read KeyEvent: %v", err) 230 | } 231 | 232 | case 5: // PointerEvent 233 | if _, err := io.Copy(ioutil.Discard, &io.LimitedReader{R: conn, N: rfb.PointerEventEncodingLength}); err != nil { 234 | return fmt.Errorf("couldn't read PointerEvent: %v", err) 235 | } 236 | 237 | case 6: // ClientCutText 238 | if _, err := io.ReadFull(conn, buf[:7]); err != nil { 239 | return fmt.Errorf("couldn't read text length in ClientCutText: %v", err) 240 | } 241 | length := bo.Uint32(buf[3:]) 242 | if _, err := io.Copy(ioutil.Discard, &io.LimitedReader{R: conn, N: int64(length)}); err != nil { 243 | return fmt.Errorf("couldn't read ClientCutText text: %v", err) 244 | } 245 | 246 | default: 247 | return fmt.Errorf("received unrecognized message %d", buf[0]) 248 | } 249 | } 250 | 251 | return nil 252 | } 253 | -------------------------------------------------------------------------------- /cmd/dirgui/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "flag" 7 | "fmt" 8 | "github.com/alltom/dirgui/rfb" 9 | "golang.org/x/text/encoding/charmap" 10 | "image" 11 | "io" 12 | "log" 13 | "net" 14 | ) 15 | 16 | func main() { 17 | flag.Parse() 18 | 19 | ln, err := net.Listen("tcp", "127.0.0.1:5900") 20 | if err != nil { 21 | log.Fatalf("couldn't listen: %v", err) 22 | } 23 | log.Print("listening…") 24 | for { 25 | conn, err := ln.Accept() 26 | if err != nil { 27 | log.Fatalf("couldn't accept connection: %v", err) 28 | } 29 | log.Print("accepted connection") 30 | go func(conn net.Conn) { 31 | if err := rfbServe(conn); err != nil { 32 | log.Printf("serve failed: %v", err) 33 | } 34 | if err := conn.Close(); err != nil { 35 | log.Printf("couldn't close connection: %v", err) 36 | } 37 | }(conn) 38 | } 39 | } 40 | 41 | func rfbServe(conn io.ReadWriter) error { 42 | buf := make([]byte, 256) 43 | w := bufio.NewWriter(conn) 44 | 45 | var bo = binary.BigEndian 46 | var pixelFormat = rfb.PixelFormat{ 47 | BitsPerPixel: 32, 48 | BitDepth: 24, 49 | BigEndian: true, 50 | TrueColor: true, 51 | 52 | RedMax: 255, 53 | GreenMax: 255, 54 | BlueMax: 255, 55 | RedShift: 24, 56 | GreenShift: 16, 57 | BlueShift: 8, 58 | } 59 | var updateRequest rfb.FramebufferUpdateRequest 60 | var update rfb.FramebufferUpdate 61 | var keyEvent rfb.KeyEvent 62 | var pointerEvent rfb.PointerEvent 63 | 64 | if _, err := io.WriteString(conn, "RFB 003.008\n"); err != nil { 65 | return fmt.Errorf("couldn't write ProtocolVersion: %v", err) 66 | } 67 | 68 | var major, minor int 69 | if _, err := io.ReadFull(conn, buf[:12]); err != nil { 70 | return fmt.Errorf("couldn't read ProtocolVersion: %v", err) 71 | } 72 | if _, err := fmt.Sscanf(string(buf[:12]), "RFB %03d.%03d\n", &major, &minor); err != nil { 73 | return fmt.Errorf("couldn't parse ProtocolVersion %q: %v", buf[:12], err) 74 | } 75 | 76 | if major == 3 && minor == 3 { 77 | // RFB 3.3 78 | bo.PutUint32(buf, 2) // VNC authentication (macOS won't connect without authentication) 79 | if _, err := conn.Write(buf[:20]); err != nil { 80 | return fmt.Errorf("couldn't write authentication scheme: %v", err) 81 | } 82 | 83 | if _, err := io.ReadFull(conn, buf[:16]); err != nil { 84 | return fmt.Errorf("couldn't read challenge response: %v", err) 85 | } 86 | 87 | bo.PutUint32(buf, 0) // OK 88 | if _, err := conn.Write(buf[:4]); err != nil { 89 | return fmt.Errorf("couldn't write authentication response: %v", err) 90 | } 91 | } else if major == 3 && minor == 8 { 92 | // RFB 3.8 93 | if _, err := conn.Write([]byte{1, 1}); err != nil { 94 | return fmt.Errorf("couldn't write security types: %v", err) 95 | } 96 | 97 | if _, err := io.ReadFull(conn, buf[:1]); err != nil { 98 | return fmt.Errorf("couldn't read security type: %v", err) 99 | } 100 | if buf[0] != 1 { 101 | return fmt.Errorf("client must use security type 1, got %q", buf[0]) 102 | } 103 | 104 | if _, err := conn.Write([]byte{0}); err != nil { 105 | return fmt.Errorf("couldn't confirm security type: %v", err) 106 | } 107 | } else { 108 | return fmt.Errorf("server only supports RFB 3.3 and 3.8, but client requested %d.%d", major, minor) 109 | } 110 | 111 | log.Print("reading ClientInit…") 112 | if _, err := io.ReadFull(conn, buf[:1]); err != nil { 113 | return fmt.Errorf("couldn't read ClientInit: %v", err) 114 | } 115 | 116 | windowRect := updateUI(image.NewNRGBA(image.ZR), &keyEvent, &pointerEvent) 117 | if windowRect.Min != image.Pt(0, 0) { 118 | panic(fmt.Sprintf("window origin must be (0, 0), but it's %v", windowRect.Min)) 119 | } 120 | bo.PutUint16(buf[0:], uint16(windowRect.Dx())) // width 121 | bo.PutUint16(buf[2:], uint16(windowRect.Dy())) // height 122 | pixelFormat.Write(buf[4:], bo) 123 | bo.PutUint32(buf[20:], 6) // length of name 124 | copy(buf[24:], "dirgui") 125 | if _, err := conn.Write(buf[:30]); err != nil { 126 | return fmt.Errorf("couldn't write ServerInit: %v", err) 127 | } 128 | 129 | for { 130 | if _, err := io.ReadFull(conn, buf[:1]); err != nil { 131 | return fmt.Errorf("couldn't read message type: %v", err) 132 | } 133 | switch buf[0] { 134 | case 0: // SetPixelFormat 135 | if _, err := io.ReadFull(conn, buf[:3+rfb.PixelFormatEncodingLength]); err != nil { 136 | return fmt.Errorf("couldn't read pixel format in SetPixelFormat: %v", err) 137 | } 138 | pixelFormat.Read(buf[3:], bo) 139 | 140 | case 2: // SetEncodings 141 | if _, err := io.ReadFull(conn, buf[:3]); err != nil { 142 | return fmt.Errorf("couldn't read number of encodings in SetEncodings: %v", err) 143 | } 144 | encodingCount := bo.Uint16(buf[1:]) 145 | if int(encodingCount)*4 > len(buf) { 146 | return fmt.Errorf("can only read %d encodings, but SetEncodings came with %d", len(buf)/4, encodingCount) 147 | } 148 | if _, err := io.ReadFull(conn, buf[:4*encodingCount]); err != nil { 149 | return fmt.Errorf("couldn't read list of encodings in SetEncodings: %v", err) 150 | } 151 | requestedEncodings := make([]int32, encodingCount) 152 | for i := range requestedEncodings { 153 | requestedEncodings[i] = int32(bo.Uint32(buf[4*i:])) 154 | } 155 | log.Printf("client requested one of %d encodings: %v", encodingCount, requestedEncodings) 156 | 157 | case 3: // FramebufferUpdateRequest 158 | if _, err := io.ReadFull(conn, buf[:rfb.FramebufferUpdateRequestEncodingLength]); err != nil { 159 | return fmt.Errorf("couldn't read FramebufferUpdateRequest: %v", err) 160 | } 161 | updateRequest.Read(buf, bo) 162 | 163 | img := rfb.NewPixelFormatImage(pixelFormat, image.Rect(int(updateRequest.X), int(updateRequest.Y), int(updateRequest.X)+int(updateRequest.Width), int(updateRequest.Y)+int(updateRequest.Height))) 164 | updateUI(img, &keyEvent, &pointerEvent) 165 | update.Rectangles = []*rfb.FramebufferUpdateRect{ 166 | &rfb.FramebufferUpdateRect{ 167 | X: updateRequest.X, Y: updateRequest.Y, Width: updateRequest.Width, Height: updateRequest.Height, 168 | EncodingType: 0, PixelData: img.Pix, 169 | }, 170 | } 171 | 172 | if _, err := w.Write([]byte{0, 0}); err != nil { // message type and padding 173 | return fmt.Errorf("couldn't write FramebufferUpdate header: %v", err) 174 | } 175 | if err := update.Write(w, bo); err != nil { 176 | return fmt.Errorf("couldn't write FramebufferUpdate: %v", err) 177 | } 178 | if err := w.Flush(); err != nil { 179 | return fmt.Errorf("couldn't write FramebufferUpdate: %v", err) 180 | } 181 | 182 | case 4: // KeyEvent 183 | if _, err := io.ReadFull(conn, buf[:rfb.KeyEventEncodingLength]); err != nil { 184 | return fmt.Errorf("couldn't read KeyEvent: %v", err) 185 | } 186 | keyEvent.Read(buf, bo) 187 | updateUI(image.NewNRGBA(image.ZR), &keyEvent, &pointerEvent) 188 | 189 | case 5: // PointerEvent 190 | if _, err := io.ReadFull(conn, buf[:rfb.PointerEventEncodingLength]); err != nil { 191 | return fmt.Errorf("couldn't read PointerEvent: %v", err) 192 | } 193 | pointerEvent.Read(buf, bo) 194 | updateUI(image.NewNRGBA(image.ZR), &keyEvent, &pointerEvent) 195 | 196 | case 6: // ClientCutText 197 | if _, err := io.ReadFull(conn, buf[:7]); err != nil { 198 | return fmt.Errorf("couldn't read text length in ClientCutText: %v", err) 199 | } 200 | length := bo.Uint32(buf[3:]) 201 | if int(length) > len(buf) { 202 | return fmt.Errorf("can only read text up to length %d, but ClientCutText came with %d", len(buf), length) 203 | } 204 | if _, err := io.ReadFull(conn, buf[:length]); err != nil { 205 | return fmt.Errorf("couldn't read text in ClientCutText: %v", err) 206 | } 207 | converted, err := charmap.ISO8859_1.NewDecoder().Bytes(buf[:length]) 208 | if err != nil { 209 | return fmt.Errorf("couldn't convert text to UTF-8 in ClientCutText: %v", err) 210 | } 211 | text := string(converted) 212 | log.Printf("client copied text: %q", text) 213 | 214 | default: 215 | return fmt.Errorf("received unrecognized message %d", buf[0]) 216 | } 217 | } 218 | 219 | return nil 220 | } 221 | -------------------------------------------------------------------------------- /cmd/dirgui/ui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "flag" 7 | "fmt" 8 | "github.com/alltom/dirgui/rfb" 9 | "golang.org/x/image/font" 10 | "golang.org/x/image/font/basicfont" 11 | "golang.org/x/image/math/fixed" 12 | "image" 13 | "image/color" 14 | "image/draw" 15 | "io" 16 | "io/ioutil" 17 | "log" 18 | "net" 19 | "os" 20 | "os/exec" 21 | "path/filepath" 22 | "strings" 23 | "sync" 24 | ) 25 | 26 | const windowWidth = 40 * 8 27 | 28 | var ( 29 | primaryColor = color.NRGBA{0x60, 0x02, 0xee, 0xff} 30 | primaryLightColor = color.NRGBA{0x99, 0x46, 0xff, 0xff} 31 | ) 32 | 33 | type Widget struct { 34 | fileInfo os.FileInfo 35 | 36 | // files 37 | content string 38 | editor EditorState 39 | loading bool 40 | saving bool 41 | 42 | // executables 43 | running bool 44 | 45 | // files with guis 46 | guiSize image.Point 47 | lastGuiImg image.Image 48 | guiLock sync.Mutex 49 | 50 | button1 ButtonState // read for files, run for executables 51 | button2 ButtonState // save for files 52 | } 53 | 54 | type ButtonState struct { 55 | clicking bool 56 | } 57 | 58 | type EditorState struct { 59 | lastKeySym uint32 60 | } 61 | 62 | var wdir string 63 | var widgets []*Widget 64 | var once sync.Once 65 | 66 | func getWidgets() { 67 | switch flag.NArg() { 68 | case 0: 69 | wdir = "." 70 | case 1: 71 | wdir = flag.Arg(0) 72 | default: 73 | log.Fatalf("Expected 0 or 1 arguments, but found %d", flag.NArg()) 74 | } 75 | 76 | infos, err := ioutil.ReadDir(wdir) 77 | if err != nil { 78 | log.Fatalf("couldn't read directory %q: %v", wdir, err) 79 | } 80 | 81 | for _, info := range infos { 82 | if info.IsDir() { 83 | continue 84 | } 85 | if strings.HasPrefix(info.Name(), ".") { 86 | continue 87 | } 88 | if len(widgets) > 0 && info.Name() == (widgets[len(widgets)-1].fileInfo.Name()+".gui") { 89 | widget := widgets[len(widgets)-1] 90 | 91 | cmd := &exec.Cmd{ 92 | Path: info.Name(), 93 | Args: []string{info.Name(), widget.fileInfo.Name()}, 94 | Dir: wdir, 95 | Stdout: os.Stdout, 96 | Stderr: os.Stderr, 97 | } 98 | 99 | imgs := make(chan image.Image) 100 | bounds, err := nestRfb(cmd, imgs) 101 | if err != nil { 102 | log.Fatalf("couldn't launch nested rfb: %v", err) 103 | } 104 | widget.guiSize = bounds.Max 105 | widget.lastGuiImg = image.NewRGBA(image.Rect(0, 0, widget.guiSize.X, widget.guiSize.Y)) 106 | 107 | go func(widget *Widget, imgs chan image.Image) { 108 | for img := range imgs { 109 | widget.guiLock.Lock() 110 | widget.lastGuiImg = img 111 | widget.guiLock.Unlock() 112 | } 113 | }(widget, imgs) 114 | 115 | continue 116 | } 117 | widgets = append(widgets, &Widget{fileInfo: info}) 118 | } 119 | } 120 | 121 | func updateUI(img draw.Image, keyEvent *rfb.KeyEvent, pointerEvent *rfb.PointerEvent) image.Rectangle { 122 | once.Do(getWidgets) 123 | 124 | var y = 8 // top padding 125 | 126 | // background color 127 | draw.Draw(img, img.Bounds(), image.NewUniform(color.White), image.ZP, draw.Src) 128 | 129 | for idx, widget := range widgets { 130 | if widget.guiSize != image.ZP { // has a remote GUI 131 | label(widget.fileInfo.Name(), image.Rect(8, y, windowWidth-16, y+8), img) 132 | y += 2 * 8 133 | 134 | widget.guiLock.Lock() 135 | draw.Draw(img, image.Rect(8, y, 8+widget.guiSize.X, y+widget.guiSize.Y), widget.lastGuiImg, image.ZP, draw.Src) 136 | widget.guiLock.Unlock() 137 | y += widget.guiSize.Y + 8 138 | } else if widget.fileInfo.Mode().Perm()&0111 != 0 { // executable 139 | label := widget.fileInfo.Name() 140 | if widget.running { 141 | label += "..." 142 | } 143 | if button(&widget.button1, label, image.Rect(8, y, 30*8, y+3*8), img, pointerEvent) && !widget.running { 144 | cmd := &exec.Cmd{Path: widget.fileInfo.Name(), Dir: wdir, Stdout: os.Stdout, Stderr: os.Stderr} 145 | widget.running = true 146 | go func(widget *Widget, cmd *exec.Cmd) { 147 | if err := cmd.Run(); err != nil { 148 | log.Printf("exec failed: %v", err) 149 | } 150 | widget.running = false 151 | }(widget, cmd) 152 | } 153 | y += 3 * 8 154 | } else { // not executable 155 | label(widget.fileInfo.Name(), image.Rect(8, y, windowWidth-16, y+8), img) 156 | y += 2 * 8 157 | 158 | x := 8 159 | 160 | edit(&widget.editor, &widget.content, image.Rect(x, y, x+22*8, y+3*8), img, keyEvent, pointerEvent) 161 | x += 23 * 8 162 | 163 | label := "Load" 164 | if widget.loading { 165 | label += "..." 166 | } 167 | if button(&widget.button1, "Load", image.Rect(x, y, x+7*8, y+3*8), img, pointerEvent) && !widget.loading && !widget.saving { 168 | widget.loading = true 169 | go func(widget *Widget) { 170 | path := filepath.Join(wdir, widget.fileInfo.Name()) 171 | f, err := os.Open(path) 172 | if err != nil { 173 | log.Printf("couldn't open %q: %v", path, err) 174 | widget.loading = false 175 | return 176 | } 177 | defer f.Close() 178 | 179 | scanner := bufio.NewScanner(f) 180 | if scanner.Scan() { 181 | widget.content = scanner.Text() 182 | } 183 | 184 | if err = scanner.Err(); err != nil { 185 | log.Printf("couldn't read %q: %v", path, err) 186 | } 187 | widget.loading = false 188 | }(widget) 189 | } 190 | x += 8 * 8 191 | 192 | label = "Save" 193 | if widget.saving { 194 | label += "..." 195 | } 196 | if button(&widget.button2, label, image.Rect(x, y, x+7*8, y+3*8), img, pointerEvent) && !widget.loading && !widget.saving { 197 | widget.saving = true 198 | go func(widget *Widget, content string) { 199 | path := filepath.Join(wdir, widget.fileInfo.Name()) 200 | if err := ioutil.WriteFile(path, []byte(content), 0666); err != nil { 201 | log.Printf("couldn't write %q: %v", path, err) 202 | } 203 | widget.saving = false 204 | }(widget, widget.content) 205 | } 206 | 207 | y += 3 * 8 208 | } 209 | 210 | y += 8 211 | if idx < len(widgets)-1 { 212 | y += 8 213 | } 214 | } 215 | 216 | return image.Rect(0, 0, windowWidth, y) 217 | } 218 | 219 | func label(text string, rect image.Rectangle, img draw.Image) { 220 | fd := &font.Drawer{ 221 | Dst: img, 222 | Src: image.NewUniform(color.Black), 223 | Face: basicfont.Face7x13, 224 | Dot: fixed.Point26_6{fixed.I(rect.Min.X), fixed.I(rect.Max.Y)}, 225 | } 226 | fd.DrawString(text) 227 | } 228 | 229 | func button(state *ButtonState, text string, rect image.Rectangle, img draw.Image, pointerEvent *rfb.PointerEvent) bool { 230 | hovering := image.Pt(int(pointerEvent.X), int(pointerEvent.Y)).In(rect) 231 | buttonDown := pointerEvent.ButtonMask&1 != 0 232 | 233 | // TODO: Require that the click started on the button. 234 | var clicked bool 235 | if state.clicking { 236 | if !buttonDown { 237 | clicked = hovering 238 | state.clicking = false 239 | } 240 | } else { 241 | if hovering && buttonDown { 242 | state.clicking = true 243 | } 244 | } 245 | 246 | c := image.Uniform{primaryColor} 247 | if hovering { 248 | if buttonDown { 249 | c.C = color.Black 250 | } else { 251 | c.C = primaryLightColor 252 | } 253 | } 254 | draw.Draw(img, rect, &c, image.ZP, draw.Src) 255 | 256 | fd := &font.Drawer{ 257 | Dst: img, 258 | Src: image.NewUniform(color.White), 259 | Face: basicfont.Face7x13, 260 | Dot: fixed.Point26_6{fixed.I(rect.Min.X + 8), fixed.I(rect.Max.Y - 8)}, 261 | } 262 | fd.DrawString(text) 263 | 264 | return clicked 265 | } 266 | 267 | func edit(state *EditorState, text *string, rect image.Rectangle, img draw.Image, keyEvent *rfb.KeyEvent, pointerEvent *rfb.PointerEvent) { 268 | draw.Draw(img, rect, image.NewUniform(color.Black), image.ZP, draw.Src) 269 | draw.Draw(img, rect.Inset(1), image.NewUniform(color.White), image.ZP, draw.Src) 270 | 271 | hovering := image.Pt(int(pointerEvent.X), int(pointerEvent.Y)).In(rect) 272 | if hovering { 273 | if keyEvent.Pressed { 274 | if state.lastKeySym != keyEvent.KeySym { 275 | if keyEvent.KeySym >= 32 && keyEvent.KeySym <= 126 { 276 | *text += string([]uint8{uint8(keyEvent.KeySym)}) 277 | } else if keyEvent.KeySym == 0xff08 { 278 | *text = (*text)[:len(*text)-1] 279 | } 280 | } 281 | state.lastKeySym = keyEvent.KeySym 282 | } else { 283 | state.lastKeySym = 0 284 | } 285 | } 286 | 287 | fd := &font.Drawer{ 288 | Dst: img, 289 | Src: image.NewUniform(color.Black), 290 | Face: basicfont.Face7x13, 291 | Dot: fixed.Point26_6{fixed.I(rect.Min.X + 8), fixed.I(rect.Max.Y - 8)}, 292 | } 293 | fd.DrawString(*text) 294 | } 295 | 296 | func nestRfb(cmd *exec.Cmd, imgs chan image.Image) (image.Rectangle, error) { 297 | ln, err := net.Listen("tcp", "127.0.0.1:") 298 | if err != nil { 299 | return image.ZR, fmt.Errorf("couldn't listen: %v", err) 300 | } 301 | 302 | log.Printf("starting subprocess at %s…", ln.Addr().String()) 303 | cmd.Args = append([]string{cmd.Args[0], "--parent_vnc_addr", ln.Addr().String()}, cmd.Args[1:]...) 304 | if err := cmd.Start(); err != nil { 305 | return image.ZR, fmt.Errorf("couldn't start subprocess: %v", err) 306 | } 307 | 308 | log.Print("waiting for subprocess connection…") 309 | conn, err := ln.Accept() 310 | if err != nil { 311 | log.Fatalf("couldn't accept connection: %v", err) 312 | } 313 | 314 | bounds := make(chan image.Rectangle, 1) 315 | boundsCallback := func(rect image.Rectangle) { 316 | bounds <- rect 317 | } 318 | imageCallback := func(img *rfb.PixelFormatImage) { 319 | imgs <- img 320 | } 321 | 322 | go func() { 323 | defer cmd.Process.Kill() 324 | 325 | log.Print("starting VNC client for subprocess…") 326 | if err := rfbClient(conn, boundsCallback, imageCallback); err != nil { 327 | log.Printf("[rfbClient] client failed: %v", err) 328 | } 329 | if err := conn.Close(); err != nil { 330 | log.Printf("couldn't close connection: %v", err) 331 | } 332 | }() 333 | 334 | return <-bounds, nil 335 | } 336 | 337 | // rfbClient communicates over conn as an RFB 3.3 client and calls callback with the composite framebuffer after each update, then requests another update. callback must not retain the image after it returns. 338 | func rfbClient(conn io.ReadWriter, boundsCallback func(image.Rectangle), callback func(*rfb.PixelFormatImage)) error { 339 | buf := make([]byte, 256) 340 | 341 | var bo = binary.BigEndian 342 | var width, height uint16 343 | var pixelFormat rfb.PixelFormat 344 | var framebuffer *rfb.PixelFormatImage 345 | var updateRequest rfb.FramebufferUpdateRequest 346 | 347 | if _, err := io.ReadFull(conn, buf[:12]); err != nil { 348 | return fmt.Errorf("couldn't read ProtocolVersion: %v", err) 349 | } 350 | // Disregard, since every server should support 3.3 351 | 352 | if _, err := io.WriteString(conn, "RFB 003.003\n"); err != nil { 353 | return fmt.Errorf("couldn't write ProtocolVersion: %v", err) 354 | } 355 | 356 | if _, err := io.ReadFull(conn, buf[:4]); err != nil { 357 | return fmt.Errorf("couldn't read authentication scheme: %v", err) 358 | } 359 | authenticationScheme := bo.Uint32(buf[:4]) 360 | if authenticationScheme != 1 { 361 | return fmt.Errorf("authentication is not supported, but server requested scheme %d", authenticationScheme) 362 | } 363 | 364 | // Send ClientInitialization 365 | buf[0] = 1 // Share desktop with other clients 366 | if _, err := conn.Write(buf[:1]); err != nil { 367 | return fmt.Errorf("couldn't write ClientInitialization: %v", err) 368 | } 369 | 370 | if _, err := io.ReadFull(conn, buf[:4+rfb.PixelFormatEncodingLength+4]); err != nil { 371 | return fmt.Errorf("couldn't read ServerInitialization: %v", err) 372 | } 373 | width = bo.Uint16(buf[0:]) 374 | height = bo.Uint16(buf[2:]) 375 | pixelFormat.Read(buf[4:], bo) 376 | nameLength := bo.Uint32(buf[4+rfb.PixelFormatEncodingLength:]) 377 | if nameLength > uint32(len(buf)) { 378 | return fmt.Errorf("server name must be less than %d bytes, but it's %d bytes", len(buf), nameLength) 379 | } 380 | if _, err := io.ReadFull(conn, buf[:nameLength]); err != nil { 381 | return fmt.Errorf("couldn't read server name: %v", err) 382 | } 383 | 384 | boundsCallback(image.Rect(0, 0, int(width), int(height))) 385 | framebuffer = rfb.NewPixelFormatImage(pixelFormat, image.Rect(0, 0, int(width), int(height))) 386 | updateRequest = rfb.FramebufferUpdateRequest{ 387 | Incremental: true, 388 | X: 0, Y: 0, 389 | Width: width, Height: height, 390 | } 391 | 392 | buf[0] = 3 // FramebufferUpdateRequest 393 | updateRequest.Write(buf[1:], bo) 394 | if _, err := conn.Write(buf[:1+rfb.FramebufferUpdateRequestEncodingLength]); err != nil { 395 | return fmt.Errorf("couldn't write FramebufferUpdateRequest: %v", err) 396 | } 397 | 398 | for { 399 | if _, err := io.ReadFull(conn, buf[:1]); err != nil { 400 | return fmt.Errorf("couldn't read message type: %v", err) 401 | } 402 | switch buf[0] { 403 | case 0: // FramebufferUpdate 404 | if _, err := io.ReadFull(conn, buf[:3]); err != nil { 405 | return fmt.Errorf("couldn't read FramebufferUpdate: %v", err) 406 | } 407 | rectangleCount := bo.Uint16(buf[1:]) 408 | for i := uint16(0); i < rectangleCount; i++ { 409 | var rect rfb.FramebufferUpdateRect 410 | if err := rect.Read(conn, bo, pixelFormat); err != nil { 411 | return fmt.Errorf("couldn't read rectangle %d: %v", i, err) 412 | } 413 | img := &rfb.PixelFormatImage{ 414 | Pix: rect.PixelData, 415 | Rect: image.Rect(int(rect.X), int(rect.Y), int(rect.X)+int(rect.Width), int(rect.Y)+int(rect.Height)), 416 | PixelFormat: pixelFormat, 417 | } 418 | draw.Draw(framebuffer, framebuffer.Bounds(), img, image.ZP, draw.Src) 419 | callback(framebuffer) 420 | 421 | buf[0] = 3 // FramebufferUpdateRequest 422 | updateRequest.Write(buf[1:], bo) 423 | if _, err := conn.Write(buf[:1+rfb.FramebufferUpdateRequestEncodingLength]); err != nil { 424 | return fmt.Errorf("couldn't write FramebufferUpdateRequest: %v", err) 425 | } 426 | } 427 | 428 | case 1: // SetColourMapEntries 429 | return fmt.Errorf("SetColourMapEntries is not supported") 430 | 431 | case 2: // Bell 432 | // Whee! 433 | 434 | case 3: // ServerCutText 435 | if _, err := io.ReadFull(conn, buf[:7]); err != nil { 436 | return fmt.Errorf("couldn't read ServerCutText: %v", err) 437 | } 438 | length := bo.Uint32(buf[3:]) 439 | // Not supported, so throw it away. 440 | io.Copy(ioutil.Discard, &io.LimitedReader{R: conn, N: int64(length)}) 441 | 442 | default: 443 | return fmt.Errorf("received unrecognized message %d", buf[0]) 444 | } 445 | 446 | } 447 | 448 | return nil 449 | } 450 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alltom/dirgui 2 | 3 | go 1.15 4 | 5 | require ( 6 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6 7 | golang.org/x/text v0.3.5 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= 2 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 3 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 4 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= 5 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 6 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 7 | -------------------------------------------------------------------------------- /rfb/image.go: -------------------------------------------------------------------------------- 1 | package rfb 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "fmt" 7 | "encoding/binary" 8 | ) 9 | 10 | type PixelFormatImage struct { 11 | Pix []uint8 12 | Rect image.Rectangle 13 | PixelFormat PixelFormat 14 | } 15 | 16 | func NewPixelFormatImage(pixelFormat PixelFormat, bounds image.Rectangle) *PixelFormatImage { 17 | bytesPerPixel := int(pixelFormat.BitsPerPixel / 8) 18 | return &PixelFormatImage{make([]uint8, bytesPerPixel*bounds.Dx()*bounds.Dy()), bounds, pixelFormat} 19 | } 20 | 21 | func (img *PixelFormatImage) ColorModel() color.Model { 22 | panic("not implemented") 23 | } 24 | 25 | func (img *PixelFormatImage) Bounds() image.Rectangle { 26 | return img.Rect 27 | } 28 | 29 | func (img *PixelFormatImage) At(x, y int) color.Color { 30 | idx := img.idx(x, y) 31 | bo := img.bo() 32 | var pixel uint32 33 | switch img.PixelFormat.BitsPerPixel { 34 | case 8: 35 | pixel = uint32(img.Pix[idx]) 36 | case 16: 37 | pixel = uint32(bo.Uint16(img.Pix[idx:])) 38 | case 32: 39 | pixel = bo.Uint32(img.Pix[idx:]) 40 | default: 41 | panic(fmt.Sprintf("BitsPerPixel must be 8, 16, or 32, but it's %d", img.PixelFormat.BitsPerPixel)) 42 | } 43 | r := (pixel >> img.PixelFormat.RedShift) & uint32(img.PixelFormat.RedMax) 44 | g := (pixel >> img.PixelFormat.GreenShift) & uint32(img.PixelFormat.GreenMax) 45 | b := (pixel >> img.PixelFormat.BlueShift) & uint32(img.PixelFormat.BlueMax) 46 | if img.PixelFormat.RedMax != 255 || img.PixelFormat.GreenMax != 255 || img.PixelFormat.BlueMax != 255 { 47 | panic(fmt.Sprintf("max red, green, and blue must be 255, but are %d, %d, and %d", img.PixelFormat.RedMax, img.PixelFormat.GreenMax, img.PixelFormat.BlueMax)) 48 | } 49 | return color.NRGBA{uint8(r), uint8(g), uint8(b), 0xff} 50 | } 51 | 52 | func (img *PixelFormatImage) Set(x, y int, c color.Color) { 53 | nrgba := color.NRGBAModel.Convert(c).(color.NRGBA) 54 | 55 | if img.PixelFormat.RedMax != 255 || img.PixelFormat.GreenMax != 255 || img.PixelFormat.BlueMax != 255 { 56 | panic(fmt.Sprintf("max red, green, and blue must be 255, but are %d, %d, and %d", img.PixelFormat.RedMax, img.PixelFormat.GreenMax, img.PixelFormat.BlueMax)) 57 | } 58 | var pixel uint32 59 | pixel |= (uint32(nrgba.R) & uint32(img.PixelFormat.RedMax)) << img.PixelFormat.RedShift 60 | pixel |= (uint32(nrgba.G) & uint32(img.PixelFormat.GreenMax)) << img.PixelFormat.GreenShift 61 | pixel |= (uint32(nrgba.B) & uint32(img.PixelFormat.BlueMax)) << img.PixelFormat.BlueShift 62 | 63 | idx := img.idx(x, y) 64 | bo := img.bo() 65 | switch img.PixelFormat.BitsPerPixel { 66 | case 8: 67 | img.Pix[idx] = uint8(pixel) 68 | case 16: 69 | bo.PutUint16(img.Pix[idx:], uint16(pixel)) 70 | case 32: 71 | bo.PutUint32(img.Pix[idx:], pixel) 72 | default: 73 | panic(fmt.Sprintf("BitsPerPixel must be 8, 16, or 32, but it's %d", img.PixelFormat.BitsPerPixel)) 74 | } 75 | } 76 | 77 | func (img *PixelFormatImage) bo() binary.ByteOrder { 78 | if img.PixelFormat.BigEndian { 79 | return binary.BigEndian 80 | } 81 | return binary.LittleEndian 82 | } 83 | 84 | func (img *PixelFormatImage) idx(x, y int) int { 85 | bytesPerPixel := int(img.PixelFormat.BitsPerPixel / 8) 86 | return (bytesPerPixel*img.Rect.Dx())*(y-img.Rect.Min.Y) + bytesPerPixel*(x-img.Rect.Min.X) 87 | } 88 | -------------------------------------------------------------------------------- /rfb/rfb-protocol.txt: -------------------------------------------------------------------------------- 1 | https://tools.ietf.org/html/rfc6143 (version 3.8?) 2 | 3 | usually TCP/IP connection port 5900 4 | multi-byte integers (except pixel values) are big-endian 5 | 6 | connection phases: 7 | handshake: agree on protocol version and security type 8 | server sends ProtocolVersion w/highest supported protocol version 9 | 12 bytes "RFB xxx.yyy\n" 10 | xxx.yyy is left-padded version number, like 003.008 11 | client replies with specific version (same format?) 12 | server sends security types it supports 13 | U8: number of security types 14 | one U8 for each security type 15 | 1 is no security 16 | client replies with U8 indicating the requested security type 17 | server replies with SecurityResult 18 | U32: 0 if OK, 1 if failed 19 | initialization: exchange ClientInit and ServerInit messages 20 | client sends ClientInit 21 | U8: 0 if other clients should be disconnected, any other value otherwise 22 | server sends ServerInit 23 | U16: framebuffer width 24 | U16: framebuffer height 25 | PIXEL_FORMAT (16 bytes) 26 | U8: bits per pixel (8, 16, or 32) 27 | U8: depth (number of useful bits in the pixel value, <= previous value) 28 | U8: big-endian flag (non-zero if multi-byte pixels are big-endian) 29 | U8: true color flag (0 if palette colors are used, otherwise decoded according to values below) 30 | U16: red max (max red value, 2^(num red bits) - 1) (big-endian) 31 | U16: green max 32 | U16: blue max 33 | U8: red shift (# shifts required to get the red color value) 34 | U8: green shift 35 | U8: blue shift 36 | 3 bytes of padding 37 | U32: length of the desktop's name 38 | name of the desktop as ASCII 39 | normal: message exchange, each preceded by message-type byte 40 | messages from client 41 | SetPixelFormat 42 | U8: 0 43 | 3 bytes of padding 44 | PIXEL_FORMAT (see above) 45 | SetEncodings 46 | server can always use raw, and is free to ignore SetEncodings in general 47 | U8: 2 48 | 1 byte of padding 49 | U16: number of encodings 50 | S32 for each encoding type 51 | 0: raw 52 | 1: CopyRect 53 | 2: RRE 54 | 5: Hextile 55 | 15: TRLE 56 | 16: ZRLE 57 | -239: Cursor pseudo-encoding 58 | -223: DesktopSize pseudo-encoding 59 | FramebufferUpdateRequest 60 | U8: 3 61 | U8: 0 if full update requested, otherwise incremental update is fine 62 | U16: x position 63 | U16: y position 64 | U16: width 65 | U16: height 66 | KeyEvent 67 | U8: 4 68 | U8: 0 on release, otherwise pressed 69 | 2 bytes of padding 70 | U32: keysym (see X window system, https://pkg.go.dev/golang.org/x/exp/shiny/driver/internal/x11key) 71 | PointerEvent 72 | U8: 5 73 | U8: button mask 74 | U16: x position 75 | U16: y position 76 | ClientCutText (update paste buffer) 77 | U8: 6 78 | 3 bytes of padding 79 | U32: text length 80 | ISO 8859-1 (Latin-1) byte array 81 | messages from the server 82 | FramebufferUpdate 83 | U8: 0 84 | 1 byte of padding 85 | U16: number of rectangles 86 | U16: x position 87 | U16: y position 88 | U16: width 89 | U16: height 90 | S32: encoding type 91 | pixel data 92 | Raw: width*height*bytesPerPixel byte array 93 | CopyRect (copy from this rect on the client) (shouldn't refer to regions covered by other rectangles in this update) 94 | U16: source x position 95 | U16: source y position 96 | TRLE (good, but complicated) 97 | ZRLE (good, but complicated) 98 | SetColorMapEntries 99 | Bell 100 | U8: 2 101 | ServerCutText (update paste buffer) 102 | U8: 3 103 | 3 bytes of padding 104 | U32: text length 105 | ISO 8859-1 (Latin-1) byte array 106 | 107 | https://web.archive.org/web/20140921005313/http://grox.net/doc/apps/vnc/rfbproto.pdf 108 | Version 3.3 109 | -------------------------------------------------------------------------------- /rfb/rfb.go: -------------------------------------------------------------------------------- 1 | package rfb 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type PixelFormat struct { 10 | BitsPerPixel uint8 11 | BitDepth uint8 12 | BigEndian bool 13 | TrueColor bool 14 | 15 | RedMax uint16 16 | GreenMax uint16 17 | BlueMax uint16 18 | RedShift uint8 19 | GreenShift uint8 20 | BlueShift uint8 21 | } 22 | 23 | type FramebufferUpdateRequest struct { 24 | Incremental bool 25 | X uint16 26 | Y uint16 27 | Width uint16 28 | Height uint16 29 | } 30 | 31 | type KeyEvent struct { 32 | Pressed bool 33 | KeySym uint32 34 | } 35 | 36 | type PointerEvent struct { 37 | ButtonMask uint8 38 | X uint16 39 | Y uint16 40 | } 41 | 42 | type FramebufferUpdate struct { 43 | Rectangles []*FramebufferUpdateRect 44 | } 45 | 46 | type FramebufferUpdateRect struct { 47 | X uint16 48 | Y uint16 49 | Width uint16 50 | Height uint16 51 | EncodingType uint32 // Unsigned per spec, but often interpreted signed 52 | PixelData []byte 53 | } 54 | 55 | const ( 56 | PixelFormatEncodingLength = 16 57 | FramebufferUpdateRequestEncodingLength = 9 58 | KeyEventEncodingLength = 7 59 | PointerEventEncodingLength = 5 60 | ) 61 | 62 | // buf must contain at least PixelFormatEncodingLength bytes. 63 | func (pf *PixelFormat) Read(buf []byte, bo binary.ByteOrder) { 64 | pf.BitsPerPixel = buf[0] 65 | pf.BitDepth = buf[1] 66 | pf.BigEndian = buf[2] != 0 67 | pf.TrueColor = buf[3] != 0 68 | 69 | pf.RedMax = bo.Uint16(buf[4:]) 70 | pf.GreenMax = bo.Uint16(buf[6:]) 71 | pf.BlueMax = bo.Uint16(buf[8:]) 72 | pf.RedShift = buf[10] 73 | pf.GreenShift = buf[11] 74 | pf.BlueShift = buf[12] 75 | } 76 | 77 | // buf must contain at least PixelFormatEncodingLength bytes. 78 | func (pf *PixelFormat) Write(buf []byte, bo binary.ByteOrder) { 79 | buf[0] = pf.BitsPerPixel 80 | buf[1] = pf.BitDepth 81 | if pf.BigEndian { 82 | buf[2] = 1 83 | } else { 84 | buf[2] = 0 85 | } 86 | if pf.TrueColor { 87 | buf[3] = 1 88 | } else { 89 | buf[3] = 0 90 | } 91 | bo.PutUint16(buf[4:], pf.RedMax) 92 | bo.PutUint16(buf[6:], pf.GreenMax) 93 | bo.PutUint16(buf[8:], pf.BlueMax) 94 | buf[10] = pf.RedShift 95 | buf[11] = pf.GreenShift 96 | buf[12] = pf.BlueShift 97 | } 98 | 99 | // buf must contain at least FramebufferUpdateRequestEncodingLength bytes. 100 | func (r *FramebufferUpdateRequest) Read(buf []byte, bo binary.ByteOrder) { 101 | r.Incremental = buf[0] != 0 102 | r.X = bo.Uint16(buf[1:]) 103 | r.Y = bo.Uint16(buf[3:]) 104 | r.Width = bo.Uint16(buf[5:]) 105 | r.Height = bo.Uint16(buf[7:]) 106 | } 107 | 108 | // buf must contain at least FramebufferUpdateRequestEncodingLength bytes. 109 | func (r *FramebufferUpdateRequest) Write(buf []byte, bo binary.ByteOrder) { 110 | if r.Incremental { 111 | buf[0] = 1 112 | } else { 113 | buf[0] = 0 114 | } 115 | bo.PutUint16(buf[1:], r.X) 116 | bo.PutUint16(buf[3:], r.Y) 117 | bo.PutUint16(buf[5:], r.Width) 118 | bo.PutUint16(buf[7:], r.Height) 119 | } 120 | 121 | // buf must contain at least KeyEventEncodingLength bytes. 122 | func (e *KeyEvent) Read(buf []byte, bo binary.ByteOrder) { 123 | e.Pressed = buf[0] != 0 124 | e.KeySym = bo.Uint32(buf[3:]) 125 | } 126 | 127 | // buf must contain at least PointerEventEncodingLength bytes. 128 | func (e *PointerEvent) Read(buf []byte, bo binary.ByteOrder) { 129 | e.ButtonMask = buf[0] 130 | e.X = bo.Uint16(buf[1:]) 131 | e.Y = bo.Uint16(buf[3:]) 132 | } 133 | 134 | func (rect *FramebufferUpdateRect) Read(r io.Reader, bo binary.ByteOrder, pixelFormat PixelFormat) error { 135 | var buf [12]byte 136 | if _, err := io.ReadFull(r, buf[:]); err != nil { 137 | return err 138 | } 139 | rect.X = bo.Uint16(buf[0:]) 140 | rect.Y = bo.Uint16(buf[2:]) 141 | rect.Width = bo.Uint16(buf[4:]) 142 | rect.Height = bo.Uint16(buf[6:]) 143 | rect.EncodingType = bo.Uint32(buf[8:]) 144 | if rect.EncodingType != 0 { 145 | return fmt.Errorf("only raw encoding is supported, but it is %d", rect.EncodingType) 146 | } 147 | rect.PixelData = make([]byte, int(pixelFormat.BitsPerPixel/8)*int(rect.Width)*int(rect.Height)) 148 | if _, err := io.ReadFull(r, rect.PixelData); err != nil { 149 | return err 150 | } 151 | return nil 152 | } 153 | 154 | func (u *FramebufferUpdate) Write(w io.Writer, bo binary.ByteOrder) error { 155 | if err := binary.Write(w, bo, uint16(len(u.Rectangles))); err != nil { 156 | return err 157 | } 158 | for _, rect := range u.Rectangles { 159 | var buf [12]byte 160 | bo.PutUint16(buf[0:], rect.X) 161 | bo.PutUint16(buf[2:], rect.Y) 162 | bo.PutUint16(buf[4:], rect.Width) 163 | bo.PutUint16(buf[6:], rect.Height) 164 | bo.PutUint32(buf[8:], uint32(rect.EncodingType)) 165 | if _, err := w.Write(buf[:]); err != nil { 166 | return err 167 | } 168 | if _, err := w.Write(rect.PixelData); err != nil { 169 | return err 170 | } 171 | } 172 | return nil 173 | } 174 | --------------------------------------------------------------------------------