Add desktop notifications

Fixes #116
This commit is contained in:
erroneousboat 2018-04-01 13:03:28 +02:00
parent 2ccc92179b
commit 50114764a7
10 changed files with 352 additions and 19 deletions

8
Gopkg.lock generated
View File

@ -1,6 +1,12 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/0xAX/notificator"
packages = ["."]
revision = "88d57ee9043ba88d6a62e437fa15dda1ca0d2b59"
[[projects]]
name = "github.com/erroneousboat/termui"
packages = ["."]
@ -50,6 +56,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "353a2a71e00ecf8dd6123a02828c450fa6d38472a98792d2d8a4cd6349900f11"
inputs-digest = "6cdfd0125aad6371a6f4e75c7fc29507cee4a6001a6c68e06c7237066a31153a"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,4 +1,4 @@
Slack-Term
slack-term
==========
A [Slack](https://slack.com) client for your terminal.
@ -42,7 +42,11 @@ Setup
"slack_token": "yourslacktokenhere",
// OPTIONAL: set the width of the sidebar (between 1 and 11), default is 1
"sidebar_width": 3,
"sidebar_width": 1,
// OPTIONAL: turn on desktop notifications for all incoming messages,
default is false
"notify": false,
// OPTIONAL: define custom key mappings, defaults are:
"key_map": {

View File

@ -12,6 +12,7 @@ import (
// Config is the definition of a Config struct
type Config struct {
SlackToken string `json:"slack_token"`
Notify bool `json:"notify"`
SidebarWidth int `json:"sidebar_width"`
MainWidth int `json:"-"`
KeyMap map[string]keyMapping `json:"key_map"`
@ -59,6 +60,7 @@ func getDefaultConfig() Config {
return Config{
SidebarWidth: 1,
MainWidth: 11,
Notify: true,
KeyMap: map[string]keyMapping{
"command": {
"i": "mode-insert",

View File

@ -4,6 +4,7 @@ import (
"net/http"
_ "net/http/pprof"
"github.com/0xAX/notificator"
"github.com/erroneousboat/termui"
termbox "github.com/nsf/termbox-go"
@ -26,6 +27,7 @@ type AppContext struct {
Config *config.Config
Debug bool
Mode string
Notify *notificator.Notificator
}
// CreateAppContext creates an application context which can be passed
@ -92,5 +94,6 @@ func CreateAppContext(flgConfig string, flgDebug bool) (*AppContext, error) {
Config: config,
Debug: flgDebug,
Mode: CommandMode,
Notify: notificator.New(notificator.Options{AppName: "slack-term"}),
}, nil
}

View File

@ -6,6 +6,7 @@ import (
"strconv"
"time"
"github.com/0xAX/notificator"
"github.com/erroneousboat/termui"
"github.com/nlopes/slack"
termbox "github.com/nsf/termbox-go"
@ -14,7 +15,8 @@ import (
"github.com/erroneousboat/slack-term/views"
)
var timer *time.Timer
var scrollTimer *time.Timer
var notifyTimer *time.Timer
// actionMap binds specific action names to the function counterparts,
// these action names can then be used to bind them to specific keys
@ -114,7 +116,7 @@ func messageHandler(ctx *context.AppContext) {
// Add message to the selected channel
if ev.Channel == ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID {
// reverse order of messages, mainly done
// Reverse order of messages, mainly done
// when attachments are added to message
for i := len(msg) - 1; i >= 0; i-- {
ctx.View.Chat.AddMessage(
@ -252,12 +254,12 @@ func actionSearch(ctx *context.AppContext, key rune) {
actionInput(ctx.View, key)
go func() {
if timer != nil {
timer.Stop()
if scrollTimer != nil {
scrollTimer.Stop()
}
timer = time.NewTimer(time.Second / 4)
<-timer.C
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually search when the time expires
term := ctx.View.Input.GetText()
@ -311,15 +313,15 @@ func actionGetMessages(ctx *context.AppContext) {
// the list without executing the actionChangeChannel event
func actionMoveCursorUpChannels(ctx *context.AppContext) {
go func() {
if timer != nil {
timer.Stop()
if scrollTimer != nil {
scrollTimer.Stop()
}
ctx.View.Channels.MoveCursorUp()
termui.Render(ctx.View.Channels)
timer = time.NewTimer(time.Second / 4)
<-timer.C
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually change channel when the timer expires
actionChangeChannel(ctx)
@ -331,15 +333,15 @@ func actionMoveCursorUpChannels(ctx *context.AppContext) {
// the list without executing the actionChangeChannel event
func actionMoveCursorDownChannels(ctx *context.AppContext) {
go func() {
if timer != nil {
timer.Stop()
if scrollTimer != nil {
scrollTimer.Stop()
}
ctx.View.Channels.MoveCursorDown()
termui.Render(ctx.View.Channels)
timer = time.NewTimer(time.Second / 4)
<-timer.C
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually change channel when the timer expires
actionChangeChannel(ctx)
@ -402,7 +404,28 @@ func actionNewMessage(ctx *context.AppContext, channelID string) {
ctx.Service.MarkAsUnread(channelID)
ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString())
termui.Render(ctx.View.Channels)
// Terminal bell
fmt.Print("\a")
// Desktop notification
if ctx.Config.Notify {
go func() {
if notifyTimer != nil {
notifyTimer.Stop()
}
notifyTimer = time.NewTimer(time.Second * 2)
<-notifyTimer.C
// Only actually notify when time expires
ctx.Notify.Push(
"slack-term",
ctx.Service.CreateNotifyMessage(channelID), "",
notificator.UR_NORMAL,
)
}()
}
}
func actionSetPresence(ctx *context.AppContext, channelID string, presence string) {

View File

@ -306,8 +306,9 @@ func (s *SlackService) MarkAsRead(channelID int) {
}
}
// MarkAsUnread will set the channel as unread
func (s *SlackService) MarkAsUnread(channelID string) {
// FindChannel will loop over s.Channels to find the index where the
// channelID equals the ID
func (s *SlackService) FindChannel(channelID string) int {
var index int
for i, channel := range s.Channels {
if channel.ID == channelID {
@ -315,9 +316,21 @@ func (s *SlackService) MarkAsUnread(channelID string) {
break
}
}
return index
}
// MarkAsUnread will set the channel as unread
func (s *SlackService) MarkAsUnread(channelID string) {
index := s.FindChannel(channelID)
s.Channels[index].Notification = true
}
// GetChannelName will return the name for a specific channelID
func (s *SlackService) GetChannelName(channelID string) string {
index := s.FindChannel(channelID)
return s.Channels[index].Name
}
// SendMessage will send a message to a particular channel
func (s *SlackService) SendMessage(channelID int, message string) {
@ -519,6 +532,21 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent
return msgs, nil
}
func (s *SlackService) CreateNotifyMessage(channelID string) string {
channel := s.Channels[s.FindChannel(channelID)]
switch channel.Type {
case ChannelTypeChannel:
return fmt.Sprintf("Message received on channel: %s", channel.Name)
case ChannelTypeGroup:
return fmt.Sprintf("Message received in group: %s", channel.Name)
case ChannelTypeIM:
return fmt.Sprintf("Message received from: %s", channel.Name)
}
return ""
}
// parseMessage will parse a message string and find and replace:
// - emoji's
// - mentions

25
vendor/github.com/0xAX/notificator/.gitignore generated vendored Normal file
View File

@ -0,0 +1,25 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
.idea

27
vendor/github.com/0xAX/notificator/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2014, 0xAX
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

49
vendor/github.com/0xAX/notificator/README.md generated vendored Normal file
View File

@ -0,0 +1,49 @@
notificator
===========================
Desktop notification with Golang for:
* Windows with `growlnotify`;
* Mac OS X with `terminal-notifier` (if installed) or `osascript` (native, 10.9 Mavericks or Up.);
* Linux with `notify-send` for Gnome and `kdialog` for Kde.
Usage
------
```go
package main
import (
"github.com/0xAX/notificator"
)
var notify *notificator.Notificator
func main() {
notify = notificator.New(notificator.Options{
DefaultIcon: "icon/default.png",
AppName: "My test App",
})
notify.Push("title", "text", "/home/user/icon.png", notificator.UR_CRITICAL)
}
```
TODO
-----
* Add more options for different notificators.
Сontribution
------------
* Fork;
* Make changes;
* Send pull request;
* Thank you.
author
----------
[@0xAX](https://twitter.com/0xAX)

166
vendor/github.com/0xAX/notificator/notification.go generated vendored Normal file
View File

@ -0,0 +1,166 @@
package notificator
import (
"fmt"
"os/exec"
"runtime"
"strconv"
"strings"
)
type Options struct {
DefaultIcon string
AppName string
}
const (
UR_NORMAL = "normal"
UR_CRITICAL = "critical"
)
type notifier interface {
push(title string, text string, iconPath string) *exec.Cmd
pushCritical(title string, text string, iconPath string) *exec.Cmd
}
type Notificator struct {
notifier notifier
defaultIcon string
}
func (n Notificator) Push(title string, text string, iconPath string, urgency string) error {
icon := n.defaultIcon
if iconPath != "" {
icon = iconPath
}
if urgency == UR_CRITICAL {
return n.notifier.pushCritical(title, text, icon).Run()
}
return n.notifier.push(title, text, icon).Run()
}
type osxNotificator struct {
AppName string
}
func (o osxNotificator) push(title string, text string, iconPath string) *exec.Cmd {
// Checks if terminal-notifier exists, and is accessible.
term_notif := CheckTermNotif()
os_version_check := CheckMacOSVersion()
// if terminal-notifier exists, use it.
// else, fall back to osascript. (Mavericks and later.)
if term_notif == true {
return exec.Command("terminal-notifier", "-title", o.AppName, "-message", text, "-subtitle", title, "-appIcon", iconPath)
} else if os_version_check == true {
title = strings.Replace(title, `"`, `\"`, -1)
text = strings.Replace(text, `"`, `\"`, -1)
notification := fmt.Sprintf("display notification \"%s\" with title \"%s\" subtitle \"%s\"", text, o.AppName, title)
return exec.Command("osascript", "-e", notification)
}
// finally falls back to growlnotify.
return exec.Command("growlnotify", "-n", o.AppName, "--image", iconPath, "-m", title)
}
// Causes the notification to stick around until clicked.
func (o osxNotificator) pushCritical(title string, text string, iconPath string) *exec.Cmd {
// same function as above...
term_notif := CheckTermNotif()
os_version_check := CheckMacOSVersion()
if term_notif == true {
// timeout set to 30 seconds, to show the importance of the notification
return exec.Command("terminal-notifier", "-title", o.AppName, "-message", text, "-subtitle", title, "-timeout", "30")
} else if os_version_check == true {
notification := fmt.Sprintf("display notification \"%s\" with title \"%s\" subtitle \"%s\"", text, o.AppName, title)
return exec.Command("osascript", "-e", notification)
}
return exec.Command("growlnotify", "-n", o.AppName, "--image", iconPath, "-m", title)
}
type linuxNotificator struct{}
func (l linuxNotificator) push(title string, text string, iconPath string) *exec.Cmd {
return exec.Command("notify-send", "-i", iconPath, title, text)
}
// Causes the notification to stick around until clicked.
func (l linuxNotificator) pushCritical(title string, text string, iconPath string) *exec.Cmd {
return exec.Command("notify-send", "-i", iconPath, title, text, "-u", "critical")
}
type windowsNotificator struct{}
func (w windowsNotificator) push(title string, text string, iconPath string) *exec.Cmd {
return exec.Command("growlnotify", "/i:", iconPath, "/t:", title, text)
}
// Causes the notification to stick around until clicked.
func (w windowsNotificator) pushCritical(title string, text string, iconPath string) *exec.Cmd {
return exec.Command("notify-send", "-i", iconPath, title, text, "/s", "true", "/p", "2")
}
func New(o Options) *Notificator {
var Notifier notifier
switch runtime.GOOS {
case "darwin":
Notifier = osxNotificator{AppName: o.AppName}
case "linux":
Notifier = linuxNotificator{}
case "windows":
Notifier = windowsNotificator{}
}
return &Notificator{notifier: Notifier, defaultIcon: o.DefaultIcon}
}
// Helper function for macOS
func CheckTermNotif() bool {
// Checks if terminal-notifier exists, and is accessible.
if err := exec.Command("which", "terminal-notifier").Run(); err != nil {
return false
}
// no error, so return true. (terminal-notifier exists)
return true
}
func CheckMacOSVersion() bool {
// Checks if the version of macOS is 10.9 or Higher (osascript support for notifications.)
cmd := exec.Command("sw_vers", "-productVersion")
check, _ := cmd.Output()
version := strings.Split(strings.TrimSpace(string(check)), ".")
// semantic versioning of macOS
major, _ := strconv.Atoi(version[0])
minor, _ := strconv.Atoi(version[1])
if major < 10 {
return false
} else if major == 10 && minor < 9 {
return false
} else {
return true
}
}