├── LICENSE ├── README.md ├── blink.go └── cmd ├── blink └── main.go └── cli └── cli.go /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Blink 3 | ===== 4 | 5 | This package lets you make your keyboard lights blink. You can use blinking 6 | keyboard lights for all kinds of fun things, such as [notifications of when 7 | someone visits your 8 | website!](http://lelandbatey.com/posts/2016/12/Making-lights-blink-for-each-HTTP-request/) 9 | 10 | Install 11 | ------- 12 | 13 | If you just run "go install" on this, you'll end up with a binary that's only 14 | usable by running `sudo`, and that's no fun! 15 | 16 | Instead, since this binary is quite harmless, we want anyone to be able to run 17 | it as root! So, to do that we can do this: 18 | 19 | # Assuming you're in this directory, will put an executable named "blink" 20 | # in current directory 21 | go build -o blink ./cmd/blink 22 | # Set the owning user to be root 23 | sudo chown root blink 24 | # Set the sticky bit on the executable owned by root, so no matter who 25 | # launches it it's launched with root priveleges. 26 | sudo chmod u+s blink 27 | 28 | And now that you've got your magical "always runs as root" executable, you can 29 | place it wherever you want (such as in your `$PATH`). 30 | 31 | -------------------------------------------------------------------------------- /blink.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package blink 3 | 4 | import ( 5 | "io" 6 | "log" 7 | "os" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | const device = "/dev/console" 15 | 16 | // ioctl is a helper function for making an ioctl call using Go's syscall package. 17 | // Thanks Dave Cheney, what a guy!: 18 | // https://github.com/davecheney/pcap/blob/10760a170da6335ec1a48be06a86f494b0ef74ab/bpf.go#L45 19 | func ioctl(fd int, request, argp uintptr) error { 20 | _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), request, argp) 21 | if errno != 0 { 22 | return os.NewSyscallError("ioctl", errno) 23 | } 24 | return nil 25 | } 26 | 27 | // Do will turn on the keyboard lights for the given amount of time. Yes ALL 28 | // the keyboard lights. 29 | func Do(onLen time.Duration) error { 30 | // This is probably not safe. I ported this to Go from Python using four 31 | // year old Go code about how to make ioctl calls in Go 32 | console_fd, err := syscall.Open(device, os.O_RDONLY|syscall.O_CLOEXEC, 0666) 33 | defer func() { 34 | if err := syscall.Close(console_fd); err != nil { 35 | log.Printf("Failed to close file descriptor for /dev/console, fd %v", console_fd) 36 | } 37 | }() 38 | if err != nil { 39 | return errors.Wrapf(err, "cannot open %q using syscall \"O_RDONLY|O_CLOEXEC 0666\"", device) 40 | } 41 | 42 | // KDSETLED is an ioctl argument for manually changing the state of 43 | // keyboard LEDs. You can find an excellent example of how it's used, with 44 | // further references, here: 45 | // http://www.tldp.org/LDP/lkmpg/2.6/html/x1194.html 46 | KDSETLED := 0x4B32 47 | 48 | // These values are defined in 'include/uapi/linux/kd.h' of the Linux 49 | // kernel source. 50 | SCR_LED := 0x01 51 | NUM_LED := 0x02 52 | CAP_LED := 0x04 53 | 54 | all_on := SCR_LED | NUM_LED | CAP_LED 55 | // restore will restore the previous value of the keyboard lights. Must be 56 | // a value higher than 7, so we choose 0xFF. 57 | restore := 0xFF 58 | if err := ioctl(console_fd, uintptr(KDSETLED), uintptr(all_on)); err != nil { 59 | return err 60 | } 61 | time.Sleep(onLen) 62 | if err = ioctl(console_fd, uintptr(KDSETLED), uintptr(restore)); err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // DoOnDelim will call blink for duration every time a delimiter is read on 70 | // the reader and will not blink for at least that duration. 71 | func DoOnDelim(duration time.Duration, r io.Reader, delimiter string) error { 72 | delim := []byte(delimiter) 73 | dpos := 0 74 | buf := make([]byte, 1) 75 | for { 76 | _, err := r.Read(buf) 77 | if err == io.EOF { 78 | break 79 | } 80 | if err != nil { 81 | return errors.Wrap(err, "cannot continue reading input") 82 | } 83 | if buf[0] == delim[dpos] { 84 | // We found the delimiter guys, do the blink! 85 | if dpos == len(delim)-1 { 86 | dpos = 0 87 | if err := Do(duration); err != nil { 88 | return err 89 | } 90 | time.Sleep(duration) 91 | } else { 92 | dpos += 1 93 | } 94 | } else { 95 | dpos = 0 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /cmd/blink/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/lelandbatey/blink/cmd/cli" 7 | ) 8 | 9 | func main() { 10 | os.Exit(cli.Run()) 11 | } 12 | -------------------------------------------------------------------------------- /cmd/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "syscall" 8 | "time" 9 | 10 | flag "github.com/spf13/pflag" 11 | "golang.org/x/crypto/ssh/terminal" 12 | 13 | "github.com/lelandbatey/blink" 14 | ) 15 | 16 | func Run() int { 17 | onlen := flag.Int64("onlen", 1000, "Length of time to turn on the lights, in milliseconds") 18 | delim := flag.String("delim", "\\n", "String to blink on") 19 | 20 | flag.Parse() 21 | 22 | d, err := strconv.Unquote(fmt.Sprintf("\"%s\"", *delim)) 23 | if err == nil { 24 | *delim = d 25 | } 26 | 27 | duration := time.Duration(*onlen) * time.Millisecond 28 | if terminal.IsTerminal(syscall.Stdin) { 29 | err = blink.Do(duration) 30 | } else { 31 | err = blink.DoOnDelim(duration, os.Stdin, *delim) 32 | } 33 | 34 | if err != nil { 35 | fmt.Println(err) 36 | return 1 37 | } 38 | 39 | return 0 40 | } 41 | --------------------------------------------------------------------------------