├── .gitignore ├── README.md ├── auto └── auto.go ├── cmd ├── chat │ ├── assets │ │ ├── app.css │ │ ├── app.js │ │ ├── gopher.png │ │ └── index.html │ └── chat.go ├── gopher │ ├── Makefile │ ├── bindata.go │ ├── bitmap.go │ ├── data │ │ ├── out01.png │ │ ├── out02.png │ │ ├── out03.png │ │ └── waiting.png │ ├── gopher.go │ ├── gopher.ico │ ├── gopher.rc │ ├── main.go │ └── window.go ├── gopherc │ └── main.go ├── gopherfeed │ └── gopherfeed.go ├── hey-gopher │ ├── assets │ │ ├── index.html │ │ ├── out01.png │ │ ├── out02.png │ │ ├── out03.png │ │ └── waiting.png │ └── main.go └── sl │ └── sl.cmd ├── gopher.go └── misc ├── screenshot ├── 9Xf0BTM.gif ├── BgiIAj9.gif ├── DTIBM9W.gif ├── K9h25F5.png ├── OKqbF7n.gif ├── PMVBSJ2.png ├── UEdmHYI.png └── xt550tv.gif └── vim ├── autoload └── gopher.vim └── plugin └── heygopher.vim /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe* 2 | *.syso 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gopher 2 | 3 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/9Xf0BTM.gif) 4 | 5 | Love Gopher 6 | 7 | This is a desktop mascot application running on your desktop on windows. 8 | 9 | ## Feature 10 | 11 | At the first, you need to run `gopher.exe`. 12 | 13 | ### Walking 14 | 15 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/9Xf0BTM.gif) 16 | 17 | So Sweet! 18 | 19 | ### SL 20 | 21 | If you did mis-type `ls` as `sl`. 22 | 23 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/BgiIAj9.gif) 24 | 25 | So Fast! 26 | 27 | ### Notification 28 | 29 | This repository bundled `gopherc.exe` that is client application to operate Gopher. 30 | 31 | ``` 32 | gopherc -m Hello 33 | ``` 34 | 35 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/DTIBM9W.gif) 36 | 37 | You need more gophers? 38 | 39 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/xt550tv.gif) 40 | 41 | Hello Gopher! 42 | 43 | ### Jumping 44 | 45 | ``` 46 | gopherc -j 47 | ``` 48 | 49 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/OKqbF7n.gif) 50 | 51 | Looking Good! 52 | 53 | ### Vim plugin 54 | 55 | Use `misc/vim` if you are vimmer. 56 | 57 | ``` 58 | :HeyGopher おなかすいた 59 | ``` 60 | 61 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/K9h25F5.png) 62 | 63 | So don't worry even if you are remaining alone at your office and you feel lonely. 64 | 65 | ### RSS Notification 66 | 67 | Run `gopherfeed.exe` to aggregate RSS feed. 68 | 69 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/UEdmHYI.png) 70 | 71 | It's Nice! 72 | 73 | ### Websocket Chat 74 | 75 | ![](https://raw.githubusercontent.com/mattn/gopher/master/misc/screenshot/PMVBSJ2.png) 76 | 77 | It seems that the members of the chat room are talking on your windows desktop. 78 | 79 | ## Requirements 80 | 81 | No, this is fully statically executable file. And this's not CGO. 82 | But unfortunately, this only works on windows. 83 | 84 | ## Installation 85 | 86 | ``` 87 | cd cmd\gopher 88 | mingw32-make 89 | ``` 90 | And copy `gopher.exe` into the path which is contained in %PATH% environment variables. 91 | 92 | ## License 93 | 94 | This code is provided under the MIT license. See http://mattn.mit-license.org/ . 95 | The image files of gopher which is created by Renee French are provided under the [Creative Commons 3.0 Attribution license](https://creativecommons.org/licenses/by/3.0/). 96 | 97 | ## Author 98 | 99 | Yasuhiro Matsumoto (a.k.a mattn) 100 | -------------------------------------------------------------------------------- /auto/auto.go: -------------------------------------------------------------------------------- 1 | package auto 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mattn/gopher" 7 | ) 8 | 9 | func init() { 10 | for i := 1; i <= 10; i++ { 11 | gopher.Create(fmt.Sprintf("ʕ◔ϖ◔ʔ .oO( I'm No%d )", i)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cmd/chat/assets/app.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | body{ font-family: Meiryo, Verdana, sans-serif; } 3 | #logo { float: left; } 4 | #chat { margin-left: 200px } 5 | textarea { width: 500px; padding: 10px; } 6 | #whoami { font-weight: bold; } 7 | -------------------------------------------------------------------------------- /cmd/chat/assets/app.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var ws = new WebSocket("ws://" + location.host + "/chat"); 3 | ws.onerror = function(m) { 4 | console.log("Error occured: " + m.data); 5 | }; 6 | ws.onmessage = function(m) { 7 | var msg = JSON.parse(m.data); 8 | switch (msg.type) { 9 | case 'whoami': $('#whoami').text(msg.user); break; 10 | case 'message': $('#chatbox').append($('

').text(msg.user + ": " + msg.value)); break; 11 | } 12 | }; 13 | $('#send').click(function() { 14 | ws.send(JSON.stringify({type: 'message', value: $('#text').val()})); 15 | $('#text').val(""); 16 | }); 17 | }); 18 | // vim:set et ts=2: 19 | -------------------------------------------------------------------------------- /cmd/chat/assets/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/chat/assets/gopher.png -------------------------------------------------------------------------------- /cmd/chat/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gopher Chat 6 | 7 | 8 | 9 | 10 | 11 |

Gopher Chat

12 | 15 |
16 |

I am ...

17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /cmd/chat/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | 13 | "github.com/mattn/gopher" 14 | "golang.org/x/net/websocket" 15 | ) 16 | 17 | var ( 18 | conns = make(map[*websocket.Conn]string) 19 | id = 0 20 | mutex sync.Mutex 21 | ) 22 | 23 | type msg struct { 24 | Type string `json:"type"` 25 | User string `json:"user"` 26 | Value string `json:"value"` 27 | } 28 | 29 | func nextId() int { 30 | mutex.Lock() 31 | defer mutex.Unlock() 32 | id++ 33 | return id 34 | } 35 | 36 | func server(ws *websocket.Conn) { 37 | defer ws.Close() 38 | 39 | user := fmt.Sprintf("user%03d", nextId()) 40 | b, err := json.Marshal(&msg{Type: "whoami", User: user}) 41 | if err != nil { 42 | log.Println("login failed:", err.Error()) 43 | return 44 | } 45 | if err := websocket.Message.Send(ws, string(b)); err != nil { 46 | log.Println("login failed:", err.Error()) 47 | return 48 | } 49 | b, err = json.Marshal(&msg{Type: "join", User: user}) 50 | if err != nil { 51 | log.Println("login failed:", err.Error()) 52 | return 53 | } 54 | for conn, _ := range conns { 55 | if err := websocket.Message.Send(conn, string(b)); err != nil { 56 | log.Println("send failed:", err) 57 | } 58 | } 59 | conns[ws] = user 60 | 61 | if ws.Request().URL.Query().Get("mode") == "" { 62 | gopher.Create(user) 63 | } 64 | 65 | for { 66 | var b []byte 67 | if err := websocket.Message.Receive(ws, &b); err != nil { 68 | if err != io.EOF { 69 | log.Println("receive failed:", err.Error()) 70 | } 71 | delete(conns, ws) 72 | for _, t := range gopher.LookupByName(user) { 73 | t.Terminate() 74 | } 75 | return 76 | } 77 | 78 | var m msg 79 | err := json.Unmarshal(b, &m) 80 | if err != nil { 81 | log.Println("send failed:", err.Error()) 82 | continue 83 | } 84 | m.User = user 85 | 86 | b, err = json.Marshal(&m) 87 | if err != nil { 88 | log.Println("send failed:", err.Error()) 89 | continue 90 | } 91 | 92 | for conn, _ := range conns { 93 | if err := websocket.Message.Send(conn, string(b)); err != nil { 94 | log.Println("send failed:", err) 95 | } 96 | } 97 | 98 | if m.Type == "message" { 99 | for _, u := range gopher.LookupByName(user) { 100 | u.Message(user+": "+m.Value, "") 101 | } 102 | } 103 | } 104 | } 105 | 106 | func main() { 107 | sig := make(chan os.Signal, 1) 108 | signal.Notify(sig, os.Interrupt) 109 | go func() { 110 | <-sig 111 | for _, t := range gopher.Lookup() { 112 | t.Terminate() 113 | } 114 | os.Exit(0) 115 | }() 116 | 117 | http.Handle("/", http.FileServer(http.Dir("assets"))) 118 | http.Handle("/chat", websocket.Handler(server)) 119 | log.Println("started at :8888") 120 | log.Fatal(http.ListenAndServe(":8888", nil)) 121 | } 122 | -------------------------------------------------------------------------------- /cmd/gopher/Makefile: -------------------------------------------------------------------------------- 1 | IMAGES = $(wildcard data/*) 2 | 3 | all : gopher.exe 4 | 5 | gopher.exe : main.go bindata.go Makefile gopher.syso 6 | go build -ldflags="-H windowsgui" 7 | 8 | bindata.go : $(IMAGES) main.go 9 | go-bindata data 10 | 11 | gopher.syso : gopher.rc 12 | windres gopher.rc gopher.syso 13 | 14 | clean : 15 | -rm *.syso *.exe 16 | -------------------------------------------------------------------------------- /cmd/gopher/bitmap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "image" 7 | _ "image/png" 8 | "sync" 9 | "unsafe" 10 | 11 | "github.com/cwchiu/go-winapi" 12 | glib "github.com/mattn/gopher" 13 | ) 14 | 15 | type sceneInfo struct { 16 | hBitmap winapi.HBITMAP 17 | hRgn winapi.HRGN 18 | off int // offset to play walking scene 19 | } 20 | 21 | func hBitmapFromImage(img image.Image) (winapi.HBITMAP, error) { 22 | var bi winapi.BITMAPV5HEADER 23 | bi.BiSize = uint32(unsafe.Sizeof(bi)) 24 | bi.BiWidth = int32(img.Bounds().Dx()) 25 | bi.BiHeight = -int32(img.Bounds().Dy()) 26 | bi.BiPlanes = 1 27 | bi.BiBitCount = 32 28 | bi.BiCompression = winapi.BI_BITFIELDS 29 | bi.BV4RedMask = 0x00FF0000 30 | bi.BV4GreenMask = 0x0000FF00 31 | bi.BV4BlueMask = 0x000000FF 32 | bi.BV4AlphaMask = 0xFF000000 33 | 34 | hdc := winapi.GetDC(0) 35 | defer winapi.ReleaseDC(0, hdc) 36 | 37 | var bits unsafe.Pointer 38 | hBitmap := winapi.CreateDIBSection( 39 | hdc, &bi.BITMAPINFOHEADER, winapi.DIB_RGB_COLORS, &bits, 0, 0) 40 | switch hBitmap { 41 | case 0, winapi.ERROR_INVALID_PARAMETER: 42 | return 0, errors.New("CreateDIBSection failed") 43 | } 44 | 45 | ba := (*[1 << 30]byte)(unsafe.Pointer(bits)) 46 | i := 0 47 | for y := img.Bounds().Min.Y; y != img.Bounds().Max.Y; y++ { 48 | for x := img.Bounds().Min.X; x != img.Bounds().Max.X; x++ { 49 | r, g, b, a := img.At(x, y).RGBA() 50 | ba[i+3] = byte(a >> 8) 51 | ba[i+2] = byte(r >> 8) 52 | ba[i+1] = byte(g >> 8) 53 | ba[i+0] = byte(b >> 8) 54 | i += 4 55 | } 56 | } 57 | return hBitmap, nil 58 | } 59 | 60 | func loadImage(filename string) (image.Image, error) { 61 | img, _, err := image.Decode(bytes.NewBuffer(MustAsset(filename))) 62 | return img, err 63 | } 64 | 65 | func reverseImage(img image.Image) image.Image { 66 | // make reversed image 67 | newimg := image.NewRGBA(img.Bounds()) 68 | Dx := img.Bounds().Dx() 69 | for y := img.Bounds().Min.Y; y != img.Bounds().Max.Y; y++ { 70 | for x := img.Bounds().Min.X; x != img.Bounds().Max.X; x++ { 71 | newimg.Set(Dx-x, y, img.At(x, y)) 72 | } 73 | } 74 | return newimg 75 | } 76 | 77 | func makeGopher() (*Gopher, error) { 78 | var err error 79 | var img [8]image.Image 80 | 81 | files := []string{ 82 | "data/out01.png", 83 | "data/out02.png", 84 | "data/out03.png", 85 | "data/waiting.png", 86 | } 87 | 88 | var wg sync.WaitGroup 89 | 90 | // make scene 1, 2, 3, and schene waiting 91 | for i, fname := range files { 92 | img[i], err = loadImage(fname) 93 | if err != nil { 94 | return nil, err 95 | } 96 | hBitmap[i], err = hBitmapFromImage(img[i]) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | // create region for window 102 | wg.Add(1) 103 | go func(i int) { 104 | hRgn[i] = toRgn(img[i]) 105 | wg.Done() 106 | }(i) 107 | } 108 | 109 | // make reverse step 4, 5, 6, and waiting for right 110 | for i := 4; i <= 7; i++ { 111 | img[i] = reverseImage(img[i-4]) 112 | hBitmap[i], err = hBitmapFromImage(img[i]) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | // create region for window 118 | wg.Add(1) 119 | go func(i int) { 120 | hRgn[i] = toRgn(img[i]) 121 | wg.Done() 122 | }(i) 123 | } 124 | 125 | wg.Wait() 126 | 127 | bounds := img[0].Bounds() 128 | var rc winapi.RECT 129 | winapi.SystemParametersInfo(winapi.SPI_GETWORKAREA, 0, unsafe.Pointer(&rc), 0) 130 | 131 | screenWidth = int(rc.Right) - bounds.Dx() 132 | scenes = [2][5]sceneInfo{ 133 | { 134 | {hBitmap[0], hRgn[0], 0}, 135 | {hBitmap[1], hRgn[1], 2}, 136 | {hBitmap[2], hRgn[2], 4}, 137 | {hBitmap[1], hRgn[1], 2}, 138 | {hBitmap[3], hRgn[3], 0}, 139 | }, 140 | { 141 | {hBitmap[4], hRgn[4], 0}, 142 | {hBitmap[5], hRgn[5], 2}, 143 | {hBitmap[6], hRgn[6], 4}, 144 | {hBitmap[5], hRgn[5], 2}, 145 | {hBitmap[7], hRgn[7], 0}, 146 | }, 147 | } 148 | 149 | mode := Walking 150 | dx := 10 151 | if *slmode { 152 | mode = SL 153 | dx = 20 154 | } 155 | 156 | // initialize gopher 157 | return &Gopher{ 158 | task: make(chan glib.Msg, 50), 159 | x: -bounds.Dx(), 160 | y: int(rc.Bottom) - bounds.Dy(), 161 | w: bounds.Dx(), 162 | h: bounds.Dy(), 163 | dx: dx, 164 | dy: 0, 165 | wait: 0, 166 | mode: mode, 167 | }, nil 168 | } 169 | 170 | func toRgn(img image.Image) winapi.HRGN { 171 | hRgn := winapi.CreateRectRgn(0, 0, 0, 0) 172 | for y := img.Bounds().Min.Y; y != img.Bounds().Max.Y; y++ { 173 | opaque := false 174 | v := 0 175 | for x := img.Bounds().Min.X; x != img.Bounds().Max.X; x++ { 176 | _, _, _, a := img.At(x, y).RGBA() 177 | // combine transparent colors 178 | if a > 0 { 179 | if !opaque { 180 | opaque = true 181 | v = x 182 | } 183 | } else { 184 | if opaque { 185 | addMask(hRgn, v, y, x, y+1) 186 | opaque = false 187 | } 188 | } 189 | } 190 | if opaque { 191 | addMask(hRgn, v, y, img.Bounds().Max.X, y+1) 192 | } 193 | } 194 | return hRgn 195 | } 196 | 197 | func addMask(hRgn winapi.HRGN, left, top, right, bottom int) { 198 | mask := winapi.CreateRectRgn(int32(left), int32(top), int32(right), int32(bottom)) 199 | winapi.CombineRgn(hRgn, mask, hRgn, winapi.RGN_OR) 200 | winapi.DeleteObject(winapi.HGDIOBJ(mask)) 201 | } 202 | -------------------------------------------------------------------------------- /cmd/gopher/data/out01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/gopher/data/out01.png -------------------------------------------------------------------------------- /cmd/gopher/data/out02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/gopher/data/out02.png -------------------------------------------------------------------------------- /cmd/gopher/data/out03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/gopher/data/out03.png -------------------------------------------------------------------------------- /cmd/gopher/data/waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/gopher/data/waiting.png -------------------------------------------------------------------------------- /cmd/gopher/gopher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | gopherlib "github.com/mattn/gopher" 5 | ) 6 | 7 | type ( 8 | mode int 9 | scene int 10 | ) 11 | 12 | const ( 13 | Walking mode = iota 14 | Jumping 15 | HighJumping 16 | Waiting 17 | Stopping 18 | SL 19 | ) 20 | 21 | type Gopher struct { 22 | msg gopherlib.Msg // current message 23 | task chan gopherlib.Msg // message queue 24 | x, y int // location of gopher 25 | w, h int // size of gopher 26 | dx, dy int // amount of movement 27 | wait int // counter for delay 28 | mode mode // animation mode 29 | scene scene // index of scenes 30 | } 31 | 32 | func (g *Gopher) NextScene() scene { 33 | g.scene += 1 34 | if g.scene == 4 { 35 | g.scene = 0 36 | } 37 | return g.scene 38 | } 39 | 40 | func (g *Gopher) Scene() scene { 41 | return g.scene 42 | } 43 | 44 | func (g *Gopher) X() int { 45 | return g.x 46 | } 47 | 48 | func (g *Gopher) Y() int { 49 | return g.y 50 | } 51 | 52 | func (g *Gopher) W() int { 53 | return g.w 54 | } 55 | 56 | func (g *Gopher) H() int { 57 | return g.h 58 | } 59 | 60 | func (g *Gopher) Dx() int { 61 | return g.dx 62 | } 63 | 64 | func (g *Gopher) Dy() int { 65 | return g.dy 66 | } 67 | 68 | func (g *Gopher) Motion() { 69 | switch g.mode { 70 | case HighJumping: 71 | g.x += g.dx / 2 72 | g.y += g.dy 73 | g.dy++ 74 | if g.dy > 20 { 75 | g.SetMode(Walking) 76 | } 77 | case Jumping: 78 | g.x += g.dx / 2 79 | g.y += g.dy 80 | g.dy++ 81 | if g.dy > 10 { 82 | g.SetMode(Walking) 83 | } 84 | default: 85 | g.x += g.dx 86 | g.y += g.dy 87 | } 88 | } 89 | 90 | func (g *Gopher) Move(dx, dy int) { 91 | g.x = dx 92 | g.y = dy 93 | } 94 | 95 | func (g *Gopher) MoveTo(x, y int) { 96 | g.x = x 97 | g.y = y 98 | } 99 | 100 | func (g *Gopher) Take() (string, bool) { 101 | select { 102 | case g.msg = <-g.task: 103 | return g.msg.Method, true 104 | default: 105 | } 106 | return "", false 107 | } 108 | 109 | func (g *Gopher) Mode() mode { 110 | return g.mode 111 | } 112 | 113 | func (g *Gopher) ClearMsg() { 114 | for len(g.task) > 0 { 115 | <-g.task 116 | } 117 | } 118 | 119 | func (g *Gopher) PushMsg(m gopherlib.Msg) { 120 | g.task <- m 121 | } 122 | 123 | func (g *Gopher) SetMode(m mode) { 124 | g.mode = m 125 | switch m { 126 | case Walking: 127 | g.dy = 0 128 | case Stopping: 129 | g.wait = 10 130 | case Jumping: 131 | g.dy = -10 132 | case HighJumping: 133 | g.scene = 0 134 | g.dy = -20 135 | case Waiting: 136 | g.scene = 0 137 | g.wait = 100 138 | } 139 | } 140 | 141 | func (g *Gopher) Turn() { 142 | g.dx = -g.dx // turn over 143 | } 144 | 145 | func (g *Gopher) WakeUp() { 146 | g.wait = 0 147 | } 148 | 149 | func (g *Gopher) Time() int { 150 | return g.wait 151 | } 152 | 153 | func (g *Gopher) Idle() int { 154 | g.wait-- 155 | return g.wait 156 | } 157 | 158 | func (g *Gopher) CurrentSceneInfo() sceneInfo { 159 | s := g.scene 160 | if g.mode == Waiting { 161 | s = 4 162 | } 163 | dir := 0 164 | if g.dx < 0 { 165 | dir = 1 166 | } 167 | return scenes[dir][s] 168 | } 169 | 170 | func (g *Gopher) SetContent(c string) { 171 | g.msg.Content = c 172 | } 173 | 174 | func (g *Gopher) Content() string { 175 | return g.msg.Content 176 | } 177 | 178 | func (g *Gopher) Link() string { 179 | return g.msg.Link 180 | } 181 | 182 | func (g *Gopher) Busy() bool { 183 | return len(gopher.task) == cap(gopher.task) 184 | } 185 | -------------------------------------------------------------------------------- /cmd/gopher/gopher.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/gopher/gopher.ico -------------------------------------------------------------------------------- /cmd/gopher/gopher.rc: -------------------------------------------------------------------------------- 1 | #include "windows.h" 2 | 3 | #define VER_FILEVERSION 1,0,0,0 4 | #define VER_FILEVERSION_STR "1.0.0.0\0" 5 | #define VER_PRODUCTVERSION 1,0,0,0 6 | #define VER_PRODUCTVERSION_STR "1.0\0" 7 | 8 | VS_VERSION_INFO VERSIONINFO 9 | FILEVERSION VER_FILEVERSION 10 | PRODUCTVERSION VER_PRODUCTVERSION 11 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 12 | FILEOS VOS__WINDOWS32 13 | FILETYPE VFT_APP 14 | FILESUBTYPE VFT2_UNKNOWN 15 | BEGIN 16 | BLOCK "StringFileInfo" 17 | BEGIN 18 | BLOCK "040904b0" 19 | BEGIN 20 | VALUE "Comments", "" 21 | VALUE "CompanyName", "" 22 | VALUE "FileDescription", "Gopher" 23 | VALUE "FileVersion", VER_FILEVERSION_STR 24 | VALUE "InternalName", "Gopher" 25 | VALUE "OriginalFileName", "Gopher.exe" 26 | VALUE "LegalCopyright", "(c) mattn" 27 | VALUE "ProductName", "Gopher" 28 | VALUE "ProductVersion", VER_PRODUCTVERSION_STR 29 | END 30 | END 31 | BLOCK "VarFileInfo" 32 | BEGIN 33 | VALUE "Translation", 0x409, 1200 34 | END 35 | END 36 | 37 | IDI_GYAGOWIN ICON "gopher.ico" 38 | -------------------------------------------------------------------------------- /cmd/gopher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func main() { 8 | os.Exit(runGopher()) 9 | } 10 | -------------------------------------------------------------------------------- /cmd/gopher/window.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "math/rand" 10 | "net/url" 11 | "os" 12 | "syscall" 13 | "time" 14 | "unsafe" 15 | 16 | "github.com/cwchiu/go-winapi" 17 | gopherlib "github.com/mattn/gopher" 18 | ) 19 | 20 | const ( 21 | DT_TOP = 0x00000000 22 | DT_LEFT = 0x00000000 23 | DT_CENTER = 0x00000001 24 | DT_RIGHT = 0x00000002 25 | DT_VCENTER = 0x00000004 26 | DT_BOTTOM = 0x00000008 27 | DT_WORDBREAK = 0x00000010 28 | DT_SINGLELINE = 0x00000020 29 | DT_EXPANDTABS = 0x00000040 30 | DT_TABSTOP = 0x00000080 31 | DT_NOCLIP = 0x00000100 32 | DT_EXTERNALLEADING = 0x00000200 33 | DT_CALCRECT = 0x00000400 34 | DT_NOPREFIX = 0x00000800 35 | DT_INTERNAL = 0x00001000 36 | DT_EDITCONTROL = 0x00002000 37 | DT_PATH_ELLIPSIS = 0x00004000 38 | DT_END_ELLIPSIS = 0x00008000 39 | DT_MODIFYSTRING = 0x00010000 40 | DT_RTLREADING = 0x00020000 41 | DT_WORD_ELLIPSIS = 0x00040000 42 | DT_NOFULLWIDTHCHARBREAK = 0x00080000 43 | DT_HIDEPREFIX = 0x00100000 44 | DT_PREFIXONLY = 0x00200000 45 | LWA_COLORKEY = 0x00001 46 | LWA_ALPHA = 0x00002 47 | ) 48 | 49 | type tagCOPYDATASTRUCT struct { 50 | DwData uintptr 51 | CbData uint32 52 | LpData unsafe.Pointer 53 | } 54 | 55 | var ( 56 | user32 = syscall.NewLazyDLL("user32.dll") 57 | procSetWindowRgn = user32.NewProc("SetWindowRgn") 58 | procSetLayeredWindowAttributes = user32.NewProc("SetLayeredWindowAttributes") 59 | procReplyMessage = user32.NewProc("ReplyMessage") 60 | procDrawText = user32.NewProc("DrawTextW") 61 | shell32 = syscall.NewLazyDLL("shell32.dll") 62 | procShellExecute = shell32.NewProc("ShellExecuteW") 63 | ) 64 | 65 | var ( 66 | hBitmap [8]winapi.HBITMAP 67 | hRgn [8]winapi.HRGN 68 | hFont winapi.HFONT 69 | white = winapi.RGB(0xFF, 0xFF, 0xFF) 70 | black = winapi.RGB(0x00, 0x00, 0x00) 71 | 72 | gopher *Gopher 73 | 74 | screenWidth int // screen width 75 | 76 | // sceneInfo have two directions. first is going right, second is left. 77 | // they have four scenesInfo, last one is gopher that have balloon. 78 | scenes [2][5]sceneInfo 79 | 80 | name = flag.String("n", "", "name of gopher") 81 | slmode = flag.Bool("sl", false, "sl mode") 82 | ) 83 | 84 | func updateWindowRegion(hWnd winapi.HWND) { 85 | s := gopher.CurrentSceneInfo() 86 | tmp := winapi.CreateRectRgn(0, 0, 0, 0) 87 | winapi.CombineRgn(tmp, s.hRgn, 0, winapi.RGN_COPY) 88 | winapi.SetWindowPos(hWnd, 0, int32(gopher.X()), int32(gopher.Y()-s.off), 0, 0, 89 | winapi.SWP_NOSIZE|winapi.SWP_NOZORDER|winapi.SWP_NOOWNERZORDER) 90 | procSetWindowRgn.Call(uintptr(hWnd), uintptr(tmp), uintptr(1)) 91 | winapi.InvalidateRect(hWnd, nil, false) 92 | } 93 | 94 | func openBrowser(hWnd winapi.HWND, uri string) { 95 | if _, err := url.Parse(uri); err != nil { 96 | return 97 | } 98 | procShellExecute.Call( 99 | uintptr(hWnd), 100 | uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("open"))), 101 | uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(uri))), 102 | 0, 103 | 0, 104 | uintptr(winapi.SW_SHOW)) 105 | } 106 | 107 | func paintGopher(hWnd winapi.HWND) { 108 | var ps winapi.PAINTSTRUCT 109 | s := gopher.CurrentSceneInfo() 110 | hdc := winapi.BeginPaint(hWnd, &ps) 111 | hCompatDC := winapi.CreateCompatibleDC(hdc) 112 | winapi.SelectObject(hCompatDC, winapi.HGDIOBJ(s.hBitmap)) 113 | winapi.BitBlt(hdc, 0, 0, int32(gopher.W()), int32(gopher.H()), hCompatDC, 0, 0, winapi.SRCCOPY) 114 | if gopher.Mode() == Waiting { 115 | pt, _ := syscall.UTF16PtrFromString(gopher.Content()) 116 | winapi.SetTextColor(hdc, black) 117 | winapi.SetBkMode(hdc, winapi.TRANSPARENT) 118 | rc := winapi.RECT{13, 135, 190, 186} 119 | old := winapi.SelectObject(hdc, winapi.HGDIOBJ(hFont)) 120 | procDrawText.Call( 121 | uintptr(hdc), 122 | uintptr(unsafe.Pointer(pt)), uintptr(len([]rune(gopher.Content()))), 123 | uintptr(unsafe.Pointer(&rc)), 124 | DT_LEFT|DT_VCENTER|DT_NOPREFIX|DT_EDITCONTROL|DT_WORDBREAK) 125 | winapi.SelectObject(hdc, old) 126 | } 127 | winapi.DeleteDC(hCompatDC) 128 | winapi.EndPaint(hWnd, &ps) 129 | } 130 | 131 | func handleMethod(hWnd winapi.HWND, method string) { 132 | switch method { 133 | case "terminate": 134 | gopher.SetMode(Stopping) 135 | case "jump": 136 | gopher.SetMode(HighJumping) 137 | case "message": 138 | gopher.SetMode(Waiting) 139 | winapi.SetWindowPos(hWnd, 0, 0, 0, 0, 0, 140 | winapi.SWP_NOMOVE|winapi.SWP_NOSIZE|winapi.SWP_NOOWNERZORDER) 141 | updateWindowRegion(hWnd) 142 | default: 143 | gopher.SetMode(Waiting) 144 | gopher.SetContent("What do you mean?") 145 | updateWindowRegion(hWnd) 146 | } 147 | } 148 | 149 | func randomAction() { 150 | if rand.Int()%10 == 0 { 151 | gopher.SetMode(Jumping) 152 | } else if rand.Int()%30 == 0 { 153 | gopher.Turn() 154 | } 155 | } 156 | 157 | func animateGopher(hWnd winapi.HWND) { 158 | switch gopher.Mode() { 159 | case Stopping: 160 | if gopher.Idle() <= 0 { 161 | winapi.DestroyWindow(hWnd) 162 | } 163 | gopher.NextScene() 164 | procSetLayeredWindowAttributes.Call(uintptr(hWnd), uintptr(white), uintptr(gopher.Time()*25), LWA_ALPHA) 165 | gopher.Motion() 166 | case Waiting: 167 | if gopher.Idle() <= 0 { 168 | gopher.SetMode(Walking) 169 | } 170 | case SL: 171 | gopher.NextScene() 172 | gopher.Motion() 173 | case Walking: 174 | if gopher.NextScene() == 0 { 175 | method, ok := gopher.Take() 176 | if ok { 177 | handleMethod(hWnd, method) 178 | } else { 179 | randomAction() 180 | } 181 | } 182 | gopher.Motion() 183 | default: 184 | gopher.Motion() 185 | } 186 | 187 | if gopher.Mode() == SL { 188 | if gopher.X() > screenWidth+gopher.W() { 189 | winapi.DestroyWindow(hWnd) 190 | return 191 | } 192 | } else { 193 | // turn over 194 | if (gopher.Dx() > 0 && gopher.X() > screenWidth) || (gopher.Dx() < 0 && gopher.X() < 0) { 195 | gopher.Turn() 196 | } 197 | } 198 | 199 | // redraw one 200 | if gopher.Mode() != Waiting { 201 | updateWindowRegion(hWnd) 202 | } 203 | } 204 | 205 | func clickGopher(hWnd winapi.HWND) { 206 | if winapi.GetKeyState(winapi.VK_SHIFT) < 0 { 207 | gopher.ClearMsg() 208 | gopher.PushMsg(gopherlib.Msg{Method: "terminate"}) 209 | return 210 | } 211 | 212 | switch gopher.Mode() { 213 | case Waiting: 214 | if gopher.Link() != "" { 215 | openBrowser(hWnd, gopher.Link()) 216 | gopher.WakeUp() 217 | } 218 | case Walking: 219 | gopher.SetMode(Jumping) 220 | } 221 | } 222 | 223 | func ipcGopher(hWnd winapi.HWND, cds *tagCOPYDATASTRUCT) { 224 | if gopher.Busy() { 225 | procReplyMessage.Call(2) 226 | return 227 | } 228 | b := make([]byte, cds.CbData) 229 | copy(b, (*(*[1 << 20]byte)(cds.LpData))[:]) 230 | var msg gopherlib.Msg 231 | if err := json.Unmarshal(b, &msg); err != nil { 232 | procReplyMessage.Call(1) 233 | return 234 | } 235 | procReplyMessage.Call(0) 236 | 237 | switch msg.Method { 238 | case "terminate": 239 | gopher.ClearMsg() 240 | gopher.PushMsg(gopherlib.Msg{Method: "terminate"}) 241 | case "clear": 242 | gopher.ClearMsg() 243 | default: 244 | gopher.PushMsg(msg) 245 | } 246 | } 247 | 248 | func showErrorMessage(hWnd winapi.HWND, msg string) { 249 | s, _ := syscall.UTF16PtrFromString(msg) 250 | t, _ := syscall.UTF16PtrFromString(gopherlib.WINDOW_CLASS) 251 | winapi.MessageBox(hWnd, s, t, winapi.MB_ICONWARNING|winapi.MB_OK) 252 | } 253 | 254 | func runGopher() int { 255 | var buf bytes.Buffer 256 | flag.CommandLine.SetOutput(&buf) 257 | flag.Usage = func() { 258 | fmt.Fprintf(&buf, "Usage of %s:\n", os.Args[0]) 259 | flag.PrintDefaults() 260 | showErrorMessage(0, buf.String()) 261 | os.Exit(1) 262 | } 263 | flag.Parse() 264 | 265 | rand.Seed(time.Now().UnixNano()) 266 | 267 | hInstance := winapi.GetModuleHandle(nil) 268 | 269 | if registerWindowClass(hInstance) == 0 { 270 | showErrorMessage(0, "registerWindowClass failed") 271 | return 1 272 | } 273 | 274 | if err := initializeInstance(hInstance, winapi.SW_SHOW); err != nil { 275 | showErrorMessage(0, err.Error()) 276 | return 1 277 | } 278 | 279 | var msg winapi.MSG 280 | for winapi.GetMessage(&msg, 0, 0, 0) != 0 { 281 | winapi.TranslateMessage(&msg) 282 | winapi.DispatchMessage(&msg) 283 | } 284 | 285 | finalizeInstance(hInstance) 286 | 287 | return int(msg.WParam) 288 | } 289 | 290 | func registerWindowClass(hInstance winapi.HINSTANCE) winapi.ATOM { 291 | var wc winapi.WNDCLASSEX 292 | 293 | wc.CbSize = uint32(unsafe.Sizeof(winapi.WNDCLASSEX{})) 294 | wc.Style = 0 295 | wc.LpfnWndProc = syscall.NewCallback(wndProc) 296 | wc.CbClsExtra = 0 297 | wc.CbWndExtra = 0 298 | wc.HInstance = hInstance 299 | wc.HIcon = winapi.LoadIcon(hInstance, winapi.MAKEINTRESOURCE(132)) 300 | wc.HCursor = winapi.LoadCursor(0, winapi.MAKEINTRESOURCE(winapi.IDC_HAND)) 301 | wc.HbrBackground = winapi.HBRUSH(winapi.GetStockObject(winapi.WHITE_BRUSH)) 302 | wc.LpszMenuName = nil 303 | wc.LpszClassName, _ = syscall.UTF16PtrFromString(gopherlib.WINDOW_CLASS) 304 | 305 | return winapi.RegisterClassEx(&wc) 306 | } 307 | 308 | func initializeInstance(hInstance winapi.HINSTANCE, nCmdShow int) error { 309 | var err error 310 | gopher, err = makeGopher() 311 | if err != nil { 312 | return err 313 | } 314 | 315 | hFont = winapi.CreateFont( 316 | 15, 0, 0, 0, winapi.FW_NORMAL, 0, 0, 0, 317 | winapi.ANSI_CHARSET, winapi.OUT_DEVICE_PRECIS, 318 | winapi.CLIP_DEFAULT_PRECIS, winapi.DEFAULT_QUALITY, 319 | winapi.VARIABLE_PITCH|winapi.FF_ROMAN, nil) 320 | 321 | title := *name 322 | if title == "" { 323 | title = gopherlib.WINDOW_CLASS 324 | } 325 | pc, _ := syscall.UTF16PtrFromString(gopherlib.WINDOW_CLASS) 326 | pt, _ := syscall.UTF16PtrFromString(title) 327 | hWnd := winapi.CreateWindowEx( 328 | winapi.WS_EX_TOOLWINDOW|winapi.WS_EX_TOPMOST|winapi.WS_EX_NOACTIVATE|winapi.WS_EX_LAYERED, 329 | pc, pt, winapi.WS_POPUP, 330 | int32(gopher.X()), 331 | int32(gopher.X()), 332 | int32(gopher.W()), 333 | int32(gopher.H()), 334 | 0, 0, hInstance, nil) 335 | if hWnd == 0 { 336 | return errors.New("CreateWindowEx failed") 337 | } 338 | 339 | updateWindowRegion(hWnd) 340 | 341 | procSetLayeredWindowAttributes.Call(uintptr(hWnd), uintptr(white), 255, LWA_ALPHA) 342 | winapi.ShowWindow(hWnd, int32(nCmdShow)) 343 | winapi.SetTimer(hWnd, 1, 50, 0) 344 | return nil 345 | } 346 | 347 | func finalizeInstance(hInstance winapi.HINSTANCE) error { 348 | winapi.DeleteObject(winapi.HGDIOBJ(hFont)) 349 | for _, h := range hBitmap { 350 | winapi.DeleteObject(winapi.HGDIOBJ(h)) 351 | } 352 | for _, h := range hRgn { 353 | winapi.DeleteObject(winapi.HGDIOBJ(h)) 354 | } 355 | return nil 356 | } 357 | 358 | func wndProc(hWnd winapi.HWND, msg uint32, wParam uintptr, lParam uintptr) uintptr { 359 | switch msg { 360 | case winapi.WM_COPYDATA: 361 | ipcGopher(hWnd, (*tagCOPYDATASTRUCT)(unsafe.Pointer(lParam))) 362 | case winapi.WM_LBUTTONDOWN: 363 | clickGopher(hWnd) 364 | case winapi.WM_PAINT: 365 | paintGopher(hWnd) 366 | case winapi.WM_TIMER: 367 | animateGopher(hWnd) 368 | case winapi.WM_DESTROY: 369 | winapi.PostQuitMessage(0) 370 | default: 371 | return winapi.DefWindowProc(hWnd, msg, wParam, lParam) 372 | } 373 | return 0 374 | } 375 | -------------------------------------------------------------------------------- /cmd/gopherc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "time" 9 | 10 | "github.com/mattn/gopher" 11 | ) 12 | 13 | var ( 14 | exit = flag.Bool("x", false, "exit all gophers") 15 | list = flag.Bool("l", false, "list gophers") 16 | message = flag.String("m", "", "send message to gopher") 17 | link = flag.String("u", "", "send url to gopher") 18 | clear = flag.Bool("c", false, "clear message queue") 19 | jump = flag.Bool("j", false, "jump gopher") 20 | ) 21 | 22 | func main() { 23 | flag.Parse() 24 | 25 | gophers := gopher.Lookup() 26 | 27 | if *list { 28 | for _, gopher := range gophers { 29 | fmt.Println(gopher.Hwnd(), gopher.Name()) 30 | } 31 | os.Exit(0) 32 | } 33 | 34 | if len(gophers) == 0 { 35 | fmt.Fprintln(os.Stderr, "Gopher not exists") 36 | os.Exit(1) 37 | } 38 | 39 | if *exit { 40 | // broadcast terminate message 41 | for _, gopher := range gophers { 42 | gopher.Terminate() 43 | } 44 | return 45 | } 46 | 47 | if *jump { 48 | // broadcast jump message 49 | for _, gopher := range gophers { 50 | gopher.Jump() 51 | } 52 | return 53 | } 54 | 55 | if *clear { 56 | // broadcast clear message 57 | for _, gopher := range gophers { 58 | gopher.Clear() 59 | } 60 | if *message == "" { 61 | return 62 | } 63 | } 64 | 65 | if *message != "" { 66 | // choice one of gophers to display message 67 | rand.Seed(time.Now().UnixNano()) 68 | gopher := gophers[rand.Int()%len(gophers)] 69 | for { 70 | if err := gopher.Message(*message, *link); err == nil { 71 | break 72 | } 73 | time.Sleep(200 * time.Millisecond) 74 | } 75 | } else { 76 | flag.Usage() 77 | os.Exit(1) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /cmd/gopherfeed/gopherfeed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "math/rand" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fatih/color" 13 | rss "github.com/mattn/go-pkg-rss" 14 | "github.com/mattn/go-runewidth" 15 | "github.com/mattn/gopher" 16 | ) 17 | 18 | var ( 19 | file = flag.String("f", "", "list of rss/atom feeds") 20 | ) 21 | 22 | func findGopher() *gopher.Gopher { 23 | gophers := gopher.Lookup() 24 | if len(gophers) == 0 { 25 | return nil 26 | } 27 | return gophers[rand.Int()%len(gophers)] 28 | } 29 | 30 | func notify(item *rss.Item) { 31 | for _, link := range item.Links { 32 | var err error 33 | if gopher := findGopher(); gopher != nil { 34 | err = gopher.Message(item.Title, link.Href) 35 | } 36 | if err != nil { 37 | title := runewidth.Truncate(item.Title, 79, "...") 38 | fmt.Fprintf(color.Output, "%s\n\t%s\n", 39 | color.YellowString("%v", title), 40 | color.MagentaString("%v", link.Href)) 41 | } 42 | } 43 | } 44 | 45 | func main() { 46 | flag.Parse() 47 | 48 | rand.Seed(time.Now().UnixNano()) 49 | 50 | uris := []string{} 51 | 52 | uris = append(uris, flag.Args()...) 53 | if *file != "" { 54 | b, err := ioutil.ReadFile(*file) 55 | if err != nil { 56 | fmt.Fprintln(os.Stderr, os.Args[0]+":", err) 57 | os.Exit(1) 58 | } 59 | for _, line := range strings.Split(string(b), "\n") { 60 | uris = append(uris, strings.TrimSpace(line)) 61 | } 62 | } 63 | 64 | if len(uris) == 0 { 65 | uris = []string{"http://feeds.feedburner.com/hatena/b/hotentry"} 66 | } 67 | for { 68 | for _, uri := range uris { 69 | err := rss.New(5, true, 70 | func(feed *rss.Feed, newchannels []*rss.Channel) { 71 | fmt.Fprintf(color.Output, "%d new channel(s) in %s\n", 72 | len(newchannels), color.GreenString("%v", feed.Url)) 73 | }, 74 | func(feed *rss.Feed, ch *rss.Channel, newitems []*rss.Item) { 75 | fmt.Fprintf(color.Output, "%d new item(s) in %s\n", 76 | len(newitems), color.GreenString("%v", feed.Url)) 77 | for _, item := range newitems { 78 | notify(item) 79 | } 80 | }, 81 | ).Fetch(uri, nil) 82 | 83 | if err != nil { 84 | fmt.Fprintf(os.Stderr, "[e] %s: %s", uri, err) 85 | } 86 | time.Sleep(10 * time.Second) 87 | } 88 | time.Sleep(3 * time.Minute) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/hey-gopher/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hey Gopher 6 | 21 | 22 | 58 | 59 | 60 |
61 |

Hey! Gopher!

62 |
63 | 64 |
65 |
66 | 67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /cmd/hey-gopher/assets/out01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/hey-gopher/assets/out01.png -------------------------------------------------------------------------------- /cmd/hey-gopher/assets/out02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/hey-gopher/assets/out02.png -------------------------------------------------------------------------------- /cmd/hey-gopher/assets/out03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/hey-gopher/assets/out03.png -------------------------------------------------------------------------------- /cmd/hey-gopher/assets/waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/cmd/hey-gopher/assets/waiting.png -------------------------------------------------------------------------------- /cmd/hey-gopher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "math/rand" 6 | "net/http" 7 | "os/exec" 8 | 9 | "github.com/mattn/gopher" 10 | ) 11 | 12 | type info struct { 13 | NumGopher int `json:"num_gopher"` 14 | } 15 | 16 | func findGopher() *gopher.Gopher { 17 | gophers := gopher.Lookup() 18 | if len(gophers) == 0 { 19 | return nil 20 | } 21 | return gophers[rand.Int()%len(gophers)] 22 | } 23 | 24 | func main() { 25 | http.Handle("/", http.FileServer(http.Dir("assets"))) 26 | http.HandleFunc("/stat", func(w http.ResponseWriter, r *http.Request) { 27 | w.Header().Set("Content-Type", "application/json") 28 | json.NewEncoder(w).Encode(&info{len(gopher.Lookup())}) 29 | }) 30 | http.HandleFunc("/start", func(w http.ResponseWriter, r *http.Request) { 31 | if r.Method == "POST" { 32 | exec.Command("gopher").Start() 33 | } 34 | }) 35 | http.HandleFunc("/say", func(w http.ResponseWriter, r *http.Request) { 36 | msg := r.FormValue("message") 37 | if r.Method != "POST" || msg == "" { 38 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 39 | return 40 | } 41 | if gopher := findGopher(); gopher != nil { 42 | gopher.Message(msg, "") 43 | } 44 | }) 45 | http.ListenAndServe(":8080", nil) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/sl/sl.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | gopher -sl 3 | 4 | -------------------------------------------------------------------------------- /gopher.go: -------------------------------------------------------------------------------- 1 | package gopher 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/cwchiu/go-winapi" 7 | "os/exec" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | const ( 13 | WINDOW_CLASS = "Gopher" 14 | ) 15 | 16 | type tagCOPYDATASTRUCT struct { 17 | DwData uintptr 18 | CbData uint32 19 | LpData unsafe.Pointer 20 | } 21 | 22 | var ( 23 | user32 = syscall.NewLazyDLL("user32.dll") 24 | procGetClassName = user32.NewProc("GetClassNameW") 25 | ErrInvalidRequest = errors.New("Invalid request") 26 | ErrTooManyRequest = errors.New("Too many request") 27 | ) 28 | 29 | type Msg struct { 30 | Method string `json:"method"` 31 | Content string `json:"content"` 32 | Link string `json:"link"` 33 | } 34 | 35 | type Gopher struct { 36 | wnd winapi.HWND 37 | } 38 | 39 | var cb = syscall.NewCallback(func(h syscall.Handle, p uintptr) uintptr { 40 | var w [256]uint16 41 | procGetClassName.Call(uintptr(h), uintptr(unsafe.Pointer(&w[0])), 255) 42 | if syscall.UTF16ToString(w[:]) == WINDOW_CLASS { 43 | gophers := (*[]*Gopher)(unsafe.Pointer(p)) 44 | *gophers = append(*gophers, &Gopher{winapi.HWND(h)}) 45 | } 46 | return 1 47 | }) 48 | 49 | // Lookup find gophers available. 50 | func Lookup() []*Gopher { 51 | gophers := []*Gopher{} 52 | winapi.EnumChildWindows(0, cb, uintptr(unsafe.Pointer(&gophers))) 53 | return gophers 54 | } 55 | 56 | // LookupByName find gophers by name. 57 | func LookupByName(name string) []*Gopher { 58 | gophers := []*Gopher{} 59 | for _, t := range Lookup() { 60 | var buf [1024]uint16 61 | if winapi.GetWindowText(t.wnd, &buf[0], 1024) > 0 { 62 | if syscall.UTF16ToString(buf[:]) == name { 63 | gophers = append(gophers, &Gopher{winapi.HWND(t.wnd)}) 64 | } 65 | } 66 | } 67 | return gophers 68 | } 69 | 70 | // Create gopher. 71 | func Create(name string) error { 72 | return exec.Command("gopher", "-n", name).Start() 73 | } 74 | 75 | func sendMessage(hWnd winapi.HWND, m *Msg) error { 76 | b, err := json.Marshal(&m) 77 | if err != nil { 78 | return err 79 | } 80 | var cds tagCOPYDATASTRUCT 81 | cds.CbData = uint32(len(b)) 82 | cds.LpData = unsafe.Pointer(&b[0]) 83 | ret := winapi.SendMessage(hWnd, winapi.WM_COPYDATA, 0, uintptr(unsafe.Pointer(&cds))) 84 | switch ret { 85 | case 1: 86 | return ErrInvalidRequest 87 | case 2: 88 | return ErrTooManyRequest 89 | } 90 | return nil 91 | } 92 | 93 | // Terminate gopher. If the message can't be arrived to gopher, or message 94 | // queue is full of capacity, return error. 95 | func (g *Gopher) Terminate() error { 96 | return sendMessage(g.wnd, &Msg{Method: "terminate"}) 97 | } 98 | 99 | // Message send text and link. Both can be empty string. If the message can't 100 | // be arrived to gopher, or message queue is full of capacity, return error. 101 | func (g *Gopher) Message(m string, l string) error { 102 | return sendMessage(g.wnd, &Msg{ 103 | Method: "message", 104 | Content: m, 105 | Link: l, 106 | }) 107 | } 108 | 109 | // Clear request to clear message queue. If the message can't be arrived to 110 | // gopher, return error. 111 | func (g *Gopher) Clear() error { 112 | return sendMessage(g.wnd, &Msg{ 113 | Method: "clear", 114 | }) 115 | } 116 | 117 | // Jump send request jumping to gopher. If the message can't be arrived to 118 | // gopher, or message queue is full of capacity, return error. 119 | func (g *Gopher) Jump() error { 120 | return sendMessage(g.wnd, &Msg{ 121 | Method: "jump", 122 | }) 123 | } 124 | 125 | func (g *Gopher) Name() string { 126 | var buf [1024]uint16 127 | if winapi.GetWindowText(g.wnd, &buf[0], 1024) > 0 { 128 | return syscall.UTF16ToString(buf[:]) 129 | } 130 | return "" 131 | } 132 | 133 | func (g *Gopher) Hwnd() winapi.HWND { 134 | return g.wnd 135 | } 136 | -------------------------------------------------------------------------------- /misc/screenshot/9Xf0BTM.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/9Xf0BTM.gif -------------------------------------------------------------------------------- /misc/screenshot/BgiIAj9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/BgiIAj9.gif -------------------------------------------------------------------------------- /misc/screenshot/DTIBM9W.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/DTIBM9W.gif -------------------------------------------------------------------------------- /misc/screenshot/K9h25F5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/K9h25F5.png -------------------------------------------------------------------------------- /misc/screenshot/OKqbF7n.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/OKqbF7n.gif -------------------------------------------------------------------------------- /misc/screenshot/PMVBSJ2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/PMVBSJ2.png -------------------------------------------------------------------------------- /misc/screenshot/UEdmHYI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/UEdmHYI.png -------------------------------------------------------------------------------- /misc/screenshot/xt550tv.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/gopher/d28e0cd2e3b2fa5326645c51f8e0b53273137123/misc/screenshot/xt550tv.gif -------------------------------------------------------------------------------- /misc/vim/autoload/gopher.vim: -------------------------------------------------------------------------------- 1 | function! gopher#hey_gopher(...) 2 | let ret = system('gopherc -m ' . shellescape(join(a:000, ' '))) 3 | if v:shell_error != 0 4 | echohl Error | echom substitute(ret, '[\r\n]', '', 'g') | echohl None 5 | endif 6 | endfunction 7 | 8 | function! gopher#good_bye_gophers(...) 9 | let ret = system('gopherc -x ') 10 | if v:shell_error != 0 11 | echohl Error | echom substitute(ret, '[\r\n]', '', 'g') | echohl None 12 | endif 13 | endfunction 14 | 15 | 16 | -------------------------------------------------------------------------------- /misc/vim/plugin/heygopher.vim: -------------------------------------------------------------------------------- 1 | if !executable('gopherc') 2 | finish 3 | endif 4 | 5 | command! -nargs=+ HeyGopher call gopher#hey_gopher() 6 | command! -nargs=0 GoodByeGophers call gopher#good_bye_gophers() 7 | --------------------------------------------------------------------------------