├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bindings.go ├── brightness.go ├── build └── PKGBUILD ├── devices.go ├── funcd.service ├── main.go ├── touchpad.go ├── users.go └── volume.go /.gitignore: -------------------------------------------------------------------------------- 1 | /funcd 2 | /build/* 3 | !/build/ 4 | !/build/PKGBUILD 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Egor Malyutin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | build: 4 | go build 5 | 6 | run: build 7 | su -c "./funcd" # Exactly su -c, NOT sudo 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # funcd 2 | funcd is daemon for functional keys (works without X11). It can: 3 | - Decrease brightness 4 | - Increase brightness 5 | - Toggle brightness 6 | - Toggle touchpad 7 | - Mute volume 8 | - Decrease volume 9 | - Increase volume 10 | 11 | ### Status of project 12 | All basic functional is already done, but you can feel free to open an issue with feature request or/and pull request :) 13 | 14 | ## Installation 15 | 16 | ### Dependencies 17 | - amixer 18 | - xinput 19 | 20 | #### Install on Arch Linux 21 | Just install AUR package [funcd-git](https://aur.archlinux.org/packages/funcd-git/) and enable systemd service: 22 | ```bash 23 | sudo systemctl enable funcd.service 24 | ``` 25 | 26 | #### Building from source 27 | ```bash 28 | go get github.com/malyutinegor/funcd 29 | cd $GOPATH/src/github.com/malyutinegor/funcd 30 | go build 31 | sudo cp ./funcd /usr/bin/ 32 | sudo cp ./funcd.service /etc/systemd/system/ 33 | sudo systemctl enable funcd.service 34 | ``` 35 | -------------------------------------------------------------------------------- /bindings.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/gvalkov/golang-evdev" 5 | ) 6 | 7 | const brightnessDiff = 0.03 8 | 9 | var lastBrightness float64 = 1 10 | 11 | func screensaverBinding(ev *InputEvent) error { 12 | kv := NewKeyEvent(ev) 13 | if kv.State == KeyUp { 14 | br, err := getBrightness() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if br == 0 { 20 | return setBrightness(lastBrightness) 21 | } else { 22 | lastBrightness = br 23 | return setBrightness(0) 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | func toggleTouchpadBinding(ev *InputEvent) error { 30 | kv := NewKeyEvent(ev) 31 | if kv.State == KeyUp { 32 | return toggleTouchpad() 33 | } 34 | return nil 35 | } 36 | 37 | var bindings = map[int]func(*InputEvent) error{ 38 | KEY_SCREENLOCK: screensaverBinding, 39 | KEY_SCREENSAVER: screensaverBinding, 40 | KEY_BRIGHTNESSUP: func(ev *InputEvent) error { 41 | kv := NewKeyEvent(ev) 42 | if kv.State != KeyUp { 43 | return incBrightness(brightnessDiff) 44 | } 45 | return nil 46 | }, 47 | KEY_BRIGHTNESSDOWN: func(ev *InputEvent) error { 48 | kv := NewKeyEvent(ev) 49 | if kv.State != KeyUp { 50 | return decBrightness(brightnessDiff) 51 | } 52 | return nil 53 | }, 54 | 55 | KEY_TOUCHPAD_TOGGLE: toggleTouchpadBinding, 56 | 57 | KEY_MUTE: func(ev *InputEvent) error { 58 | kv := NewKeyEvent(ev) 59 | if kv.State == KeyUp { 60 | return toggleVolume() 61 | } 62 | return nil 63 | }, 64 | KEY_VOLUMEDOWN: func(ev *InputEvent) error { 65 | kv := NewKeyEvent(ev) 66 | if kv.State != KeyUp { 67 | return decVolume() 68 | } 69 | return nil 70 | }, 71 | KEY_VOLUMEUP: func(ev *InputEvent) error { 72 | kv := NewKeyEvent(ev) 73 | if kv.State != KeyUp { 74 | return incVolume() 75 | } 76 | return nil 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /brightness.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | // "os" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "math" 9 | "path" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // Port of https://github.com/kevva/brightness/blob/master/lib/linux.js 15 | 16 | const dir = "/sys/class/backlight" 17 | 18 | func getBrightnessH(device string) (int, error) { 19 | b, err := ioutil.ReadFile(path.Join(dir, device, "brightness")) 20 | if err != nil { 21 | return 0, err 22 | } 23 | 24 | n, err := strconv.Atoi(strings.TrimSpace(string(b))) 25 | if err != nil { 26 | return 0, err 27 | } 28 | 29 | return n, nil 30 | } 31 | 32 | func getMaxBrightnessH(device string) (int, error) { 33 | b, err := ioutil.ReadFile(path.Join(dir, device, "max_brightness")) 34 | if err != nil { 35 | return 0, err 36 | } 37 | 38 | n, err := strconv.Atoi(strings.TrimSpace(string(b))) 39 | if err != nil { 40 | return 0, err 41 | } 42 | 43 | return n, nil 44 | } 45 | 46 | func setBrightnessH(device string, val int) error { 47 | return ioutil.WriteFile(path.Join(dir, device, "brightness"), []byte(fmt.Sprint(val)), 0644) 48 | } 49 | 50 | func getBacklightH() (string, error) { 51 | files, err := ioutil.ReadDir(dir) 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | if len(files) == 0 { 57 | return "", errors.New("No backlight device found") 58 | } 59 | 60 | return files[0].Name(), nil 61 | } 62 | 63 | func getBrightness() (float64, error) { 64 | device, err := getBacklightH() 65 | if err != nil { 66 | return 0, err 67 | } 68 | 69 | max, err := getMaxBrightnessH(device) 70 | if err != nil { 71 | return 0, err 72 | } 73 | 74 | current, err := getBrightnessH(device) 75 | if err != nil { 76 | return 0, err 77 | } 78 | 79 | return float64(current) / float64(max), nil 80 | } 81 | 82 | func setBrightness(val float64) error { 83 | if val < 0 { 84 | val = 0 85 | } else if val > 1 { 86 | val = 1 87 | } 88 | 89 | device, err := getBacklightH() 90 | if err != nil { 91 | return err 92 | } 93 | 94 | max, err := getMaxBrightnessH(device) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | brightness := int(math.Floor(val * float64(max))) 100 | 101 | return setBrightnessH(device, brightness) 102 | } 103 | 104 | func withBrightness(f func(float64) float64) error { 105 | b, err := getBrightness() 106 | if err != nil { 107 | return err 108 | } 109 | 110 | end := f(b) 111 | 112 | return setBrightness(end) 113 | } 114 | 115 | func decBrightness(val float64) error { 116 | return withBrightness(func(c float64) float64 { return c - val }) 117 | } 118 | 119 | func incBrightness(val float64) error { 120 | return withBrightness(func(c float64) float64 { return c + val }) 121 | } 122 | -------------------------------------------------------------------------------- /build/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Egor Malyutin 2 | 3 | pkgname=funcd-git 4 | pkgver=r22.ae55f2a 5 | _pkgname=funcd 6 | pkgrel=2 7 | pkgdesc="Daemon for functional keys (works without X11)" 8 | url="https://github.com/malyutinegor/funcd" 9 | arch=('x86_64' 'i686') 10 | license=('MIT') 11 | depends=('alsa-utils' 'xorg-xinput') 12 | makedepends=('go') 13 | source=("${_pkgname}::git+https://github.com/malyutinegor/funcd.git") 14 | sha256sums=('SKIP') 15 | 16 | pkgver() { 17 | cd "${srcdir}/${_pkgname}" 18 | 19 | printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" 20 | } 21 | 22 | prepare() { 23 | cd "${srcdir}" 24 | 25 | GOROOT="/usr/lib/go" GOPATH="${srcdir}/go" PATH="$PATH:$GOPATH/bin" \ 26 | go get -v -u github.com/malyutinegor/funcd 27 | } 28 | 29 | build() { 30 | cd "${srcdir}/go/src/github.com/malyutinegor/funcd" 31 | 32 | GOROOT="/usr/lib/go" GOPATH="${srcdir}/go" PATH="$PATH:$GOPATH/bin" \ 33 | go build github.com/malyutinegor/funcd 34 | } 35 | 36 | package() { 37 | cd "${srcdir}/go/src/github.com/malyutinegor/funcd" 38 | 39 | install -Dm755 "funcd" "${pkgdir}/usr/bin/funcd" 40 | install -Dm755 "funcd.service" "${pkgdir}/etc/systemd/system/funcd.service" 41 | } 42 | -------------------------------------------------------------------------------- /devices.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/gvalkov/golang-evdev" 5 | ) 6 | 7 | func findDevices() ([]*InputDevice, error) { 8 | devices, err := ListInputDevices() 9 | if err != nil { 10 | return []*InputDevice{}, err 11 | } 12 | 13 | results := []*InputDevice{} 14 | 15 | for _, device := range devices { 16 | for _, cp := range device.Capabilities { 17 | for _, key := range cp { 18 | if _, ok := bindings[key.Code]; ok { 19 | cont := false 20 | for _, result := range results { 21 | if device.Fn == result.Fn { 22 | cont = true 23 | break 24 | } 25 | } 26 | 27 | if !cont { 28 | results = append(results, device) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | return results, nil 36 | } 37 | -------------------------------------------------------------------------------- /funcd.service: -------------------------------------------------------------------------------- 1 | [Service] 2 | Type=simple 3 | ExecStart=/usr/bin/funcd 4 | Restart=on-failure 5 | 6 | [Install] 7 | WantedBy=multi-user.target 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | . "github.com/gvalkov/golang-evdev" 8 | ) 9 | 10 | func main() { 11 | if os.Getuid() != 0 { 12 | log.Print("Funcd must be started from root user.") 13 | } 14 | 15 | var err error 16 | 17 | lastBrightness, err = getBrightness() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | users, err = getUsers() 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | devices, err := findDevices() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | done := make(chan bool) 33 | 34 | for _, device := range devices { 35 | go func(device *InputDevice) { 36 | for { 37 | func() { 38 | // Don't panic! 39 | defer func() { 40 | if r := recover(); r != nil { 41 | log.Print("Panic: ", r) 42 | } 43 | }() 44 | 45 | for { 46 | ev, err := device.ReadOne() 47 | if err != nil { 48 | done <- true 49 | log.Print(err) 50 | } 51 | 52 | binding, ok := bindings[int(ev.Code)] 53 | if ok { 54 | err = binding(ev) 55 | if err != nil { 56 | log.Print(err) 57 | } 58 | } 59 | } 60 | }() 61 | } 62 | }(device) 63 | } 64 | 65 | log.Printf("Funcd started") 66 | 67 | for _ = range devices { 68 | <-done 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /touchpad.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | idRegexp1 = regexp.MustCompile("TouchPad\\s*id\\=[0-9]{1,2}") 14 | idRegexp2 = regexp.MustCompile("[0-9]{1,2}") 15 | stateRegexp1 = regexp.MustCompile("(?m)Device Enabled.*$") 16 | stateRegexp2 = regexp.MustCompile("(?m)\\d+$") 17 | ) 18 | 19 | func getTouchpadID() (int, error) { 20 | out, err := exec.Command("xinput", "list").Output() 21 | if err != nil { 22 | return 0, err 23 | } 24 | 25 | res1 := idRegexp1.Find(out) 26 | if res1 == nil { 27 | return 0, errors.New("Not found touchpad ID") 28 | } 29 | 30 | res2 := idRegexp2.Find(res1) 31 | if res2 == nil { 32 | return 0, errors.New("Not found touchpad ID") 33 | } 34 | 35 | return strconv.Atoi(strings.TrimSpace(string(res2))) 36 | } 37 | 38 | func getTouchpadState(id int) (bool, error) { 39 | out, err := exec.Command("xinput", "list-props", fmt.Sprint(id)).Output() 40 | if err != nil { 41 | return false, err 42 | } 43 | 44 | res1 := stateRegexp1.Find(out) 45 | if res1 == nil { 46 | return false, errors.New("Not found touchpad state") 47 | } 48 | 49 | res2 := stateRegexp2.Find(res1) 50 | if res2 == nil { 51 | return false, errors.New("Not found touchpad state") 52 | } 53 | 54 | return "1" == strings.TrimSpace(string(res2)), nil 55 | } 56 | 57 | func enableTouchpad(id int) error { 58 | return exec.Command("xinput", "enable", fmt.Sprint(id)).Run() 59 | } 60 | 61 | func disableTouchpad(id int) error { 62 | return exec.Command("xinput", "disable", fmt.Sprint(id)).Run() 63 | } 64 | 65 | func toggleTouchpad() error { 66 | id, err := getTouchpadID() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | s, err := getTouchpadState(id) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | if s { 77 | return disableTouchpad(id) 78 | } else { 79 | return enableTouchpad(id) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // https://www.socketloop.com/tutorials/golang-get-all-local-users-and-print-out-their-home-directory-description-and-group-id 12 | 13 | type User struct { 14 | Uid uint32 15 | Gid uint32 16 | } 17 | 18 | var users = []User{} 19 | 20 | func getUsers() ([]User, error) { 21 | result := []User{} 22 | 23 | file, err := os.Open("/etc/passwd") 24 | 25 | if err != nil { 26 | return result, err 27 | } 28 | 29 | defer file.Close() 30 | 31 | reader := bufio.NewReader(file) 32 | 33 | for { 34 | line, err := reader.ReadString('\n') 35 | 36 | if equal := strings.Index(line, "#"); equal < 0 { 37 | lineSlice := strings.FieldsFunc(line, func(divide rune) bool { 38 | return divide == ':' 39 | }) 40 | 41 | if len(lineSlice) >= 3 { 42 | uid, err := strconv.Atoi(lineSlice[2]) 43 | if err != nil { 44 | return result, err 45 | } 46 | 47 | if uid >= 1000 && uid != 65534 { 48 | gid, err := strconv.Atoi(lineSlice[3]) 49 | if err != nil { 50 | return result, err 51 | } 52 | 53 | result = append(result, User{Uid: uint32(uid), Gid: uint32(gid)}) 54 | } 55 | } 56 | 57 | } 58 | 59 | if err == io.EOF { 60 | break 61 | } 62 | 63 | if err != nil { 64 | return result, err 65 | } 66 | 67 | } 68 | 69 | return result, nil 70 | } 71 | -------------------------------------------------------------------------------- /volume.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | const volumeDiff = "3" 12 | 13 | func runUser(command string, args ...string) error { 14 | if len(users) == 0 { 15 | return errors.New("Not found any usual user for changing volume") 16 | } 17 | 18 | user := users[0] 19 | cmd := exec.Command(command, args...) 20 | cmd.Env = append(os.Environ(), fmt.Sprint("XDG_RUNTIME_DIR=/run/user/", user.Uid)) 21 | cmd.SysProcAttr = &syscall.SysProcAttr{} 22 | cmd.SysProcAttr.Credential = &syscall.Credential{Uid: user.Uid, Gid: user.Gid} 23 | return cmd.Run() 24 | } 25 | 26 | func decVolume() error { 27 | return runUser("amixer", "set", "Master", volumeDiff+"%-") 28 | } 29 | 30 | func incVolume() error { 31 | return runUser("amixer", "set", "Master", volumeDiff+"%+") 32 | } 33 | 34 | func toggleVolume() error { 35 | return runUser("amixer", "set", "Master", "toggle") 36 | } 37 | --------------------------------------------------------------------------------