Compare commits

...

No commits in common. "master" and "gocui" have entirely different histories.

259 changed files with 1860 additions and 55442 deletions

View File

@ -1,6 +0,0 @@
Please read [CONTRIBUTING.md](https://github.com/erroneousboat/slack-term/blob/master/CONTRIBUTING.md)
---
Version:
Installation method:

View File

@ -1 +0,0 @@
Please read [CONTRIBUTING.md](https://github.com/erroneousboat/slack-term/blob/master/CONTRIBUTING.md)

2
.gitignore vendored
View File

@ -1 +1,3 @@
bin/
vendor/*
!vendor.json

View File

@ -1,36 +0,0 @@
Contributing
============
Purpose
-------
First and foremost, slack-term is a personal project. With this project I
never set out to create a "better" slack version than the official slack
client. And as such, my intention for the project is not to create a feature
complete alternative to the official slack client. It's cool that people
want to use slack-term as an alternative, but I started this project because
I wanted to be able to send and receive slack messages from the terminal to
slack.
Issues
------
When posting issues please mention the following:
* Which version of slack-term you're using
* Method of installation (binary, go, or make)
Feature requests
----------------
When creating an issue that is a feature request, I will label it as such,
however this doesn't mean that it is an approved feature request. I will
leave it open for discussion and decide whether it is feasible and/or
desirable.
Pull requests
-------------
Before creating a solution, please create an issue that is open for discussion
first. This is to avoid dissappointment, for all the work you might have
done, that might not get merged or needs to be refactored.

View File

@ -1,31 +0,0 @@
FROM golang:alpine as builder
ENV PATH /go/bin:/usr/local/go/bin:$PATH
ENV GOPATH /go
RUN apk add --no-cache \
ca-certificates
COPY . /go/src/github.com/erroneousboat/slack-term
RUN set -x \
&& apk add --no-cache --virtual .build-deps \
git \
gcc \
libc-dev \
libgcc \
make \
&& cd /go/src/github.com/erroneousboat/slack-term \
&& make build \
&& mv ./bin/slack-term /usr/bin/slack-term \
&& apk del .build-deps \
&& rm -rf /go
FROM alpine:latest
ENV USER root
COPY --from=builder /usr/bin/slack-term /usr/bin/slack-term
COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs
ENTRYPOINT stty cols 25 && slack-term -config config

View File

@ -3,12 +3,9 @@ default: test
# -timeout timout in seconds
# -v verbose output
test:
@ echo "+ $@"
@echo "+ $@"
@ go test -timeout=5s -v
dev: build
@ ./bin/slack-term -debug
# `CGO_ENABLED=0`
# Because of dynamically linked libraries, this will statically compile the
# app with all libraries built in. You won't be able to cross-compile if CGO
@ -22,10 +19,6 @@ dev: build
# We're setting the OS to linux (in case someone builds the binary on Mac or
# Windows)
#
# `-mod=vendor`
# This ensures that the build process will use the modules in the vendor
# folder.
#
# `-a`
# Force rebuilding of package, all import will be rebuilt with cgo disabled,
# which means all the imports will be rebuilt with cgo disabled.
@ -43,17 +36,17 @@ dev: build
# Location of the source files
build:
@ echo "+ $@"
@ CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term .
@ CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term .
# Cross-compile
# http://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5
build-linux:
@ echo "+ $@"
@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term-linux-amd64 .
@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term-linux-amd64 .
build-mac:
@ echo "+ $@"
@ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term-darwin-amd64 .
@ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term-darwin-amd64 .
run: build
@ echo "+ $@"
@ -63,11 +56,6 @@ install:
@ echo "+ $@"
@ go install .
modules:
@ echo "+ $@"
@ go mod tidy
@ go mod vendor
build-all: build build-linux build-mac
.PHONY: default test build build-linux build-mac run install

127
README.md
View File

@ -1,80 +1,91 @@
slack-term
Slack-Term
==========
A [Slack](https://slack.com) client for your terminal.
![Screenshot](/screenshot.png?raw=true)
Installation
------------
Getting started
---------------
#### Binary installation
1. [Download](https://github.com/erroneousboat/slack-term/releases) a
compatible version for your system, and place where you can access it from
the command line like, `~/bin`, `/usr/local/bin`, or `/usr/local/sbin`. Or
get it via Go:
[Download](https://github.com/erroneousboat/slack-term/releases) a
compatible binary for your system. For convenience, place `slack-term` in a
directory where you can access it from the command line. Usually this is
`/usr/local/bin`.
```bash
$ mv slack-term /usr/local/bin
```
```bash
$ go get -u github.com/erroneousboat/slack-term
```
#### Via Go
2. Get a slack token, click [here](https://api.slack.com/docs/oauth-test-tokens)
If you want, you can also get `slack-term` via Go:
3. Create a `slack-term.json` file, place it in your home directory. The file
should resemble the following structure (don't forget to remove the comments):
```bash
$ go get -u github.com/erroneousboat/slack-term
$ cd $GOPATH/src/github.com/erroneousboat/slack-term
$ go install .
```
```javascript
{
"slack_token": "yourslacktokenhere",
#### Via docker
// OPTIONAL: add the following to use light theme, default is dark
"theme": "light",
You can also run it with docker, make sure you have a valid config file
on your host system.
// OPTIONAL: set the width of the sidebar (between 1 and 11), default is 1
"sidebar_width": 3,
```bash
docker run -it -v [config-file]:/config erroneousboat/slack-term
```
// OPTIONAL: define custom key mappings, defaults are:
"key_map": {
"command": {
"i": "mode-insert",
"k": "channel-up",
"j": "channel-down",
"g": "channel-top",
"G": "channel-bottom",
"<previous>": "chat-up",
"C-b": "chat-up",
"C-u": "chat-up",
"<next>": "chat-down",
"C-f": "chat-down",
"C-d": "chat-down",
"q": "quit",
"<f1>": "help"
},
"insert": {
"<left>": "cursor-left",
"<right>": "cursor-right",
"<enter>": "send",
"<escape>": "mode-command",
"<backspace>": "backspace",
"C-8": "backspace",
"<delete>": "delete",
"<space>": "space"
},
"search": {
"<left>": "cursor-left",
"<right>": "cursor-right",
"<escape>": "clear-input",
"<enter>": "clear-input",
"<backspace>": "backspace",
"C-8": "backspace",
"<delete>": "delete",
"<space>": "space"
}
}
}
```
Setup
-----
4. Run `slack-term`:
1. Get a slack token, click [here](https://github.com/erroneousboat/slack-term/wiki#running-slack-term-without-legacy-tokens)
```bash
$ slack-term
2. Running `slack-term` for the first time, will create a default config file at
`~/.config/slack-term/config`.
```bash
$ slack-term
```
3. Update the config file and update your `slack_token` For more configuration
options of the `config` file, see the [wiki](https://github.com/erroneousboat/slack-term/wiki).
```javascript
{
"slack_token": "yourslacktokenhere"
}
```
Usage
-----
When everything is setup correctly you can run `slack-term` with the following
command:
```bash
$ slack-term
```
// or specify the location of the config file
$ slack-term -config [path-to-config-file]
```
Default Key Mapping
-------------------
Below are the default key-mappings for `slack-term`, you can change them
in your `config` file.
| mode | key | action |
|---------|-----------|----------------------------|
| command | `i` | insert mode |
@ -83,18 +94,12 @@ in your `config` file.
| command | `j` | move channel cursor down |
| command | `g` | move channel cursor top |
| command | `G` | move channel cursor bottom |
| command | `K` | thread up |
| command | `J` | thread down |
| command | `G` | move channel cursor bottom |
| command | `pg-up` | scroll chat pane up |
| command | `ctrl-b` | scroll chat pane up |
| command | `ctrl-u` | scroll chat pane up |
| command | `pg-down` | scroll chat pane down |
| command | `ctrl-f` | scroll chat pane down |
| command | `ctrl-d` | scroll chat pane down |
| command | `n` | next search match |
| command | `N` | previous search match |
| command | `,` | jump to next notification |
| command | `q` | quit |
| command | `f1` | help |
| insert | `left` | move input cursor left |

View File

@ -2,10 +2,11 @@ package components
import (
"fmt"
"html"
"strings"
"github.com/erroneousboat/termui"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/erroneousboat/gocui"
"github.com/erroneousboat/slack-term/service"
)
const (
@ -14,382 +15,282 @@ const (
IconChannel = "#"
IconGroup = "☰"
IconIM = "●"
IconMpIM = "☰"
IconNotification = "*"
PresenceAway = "away"
PresenceActive = "active"
ChannelTypeChannel = "channel"
ChannelTypeGroup = "group"
ChannelTypeIM = "im"
ChannelTypeMpIM = "mpim"
)
type ChannelItem struct {
ID string
Name string
Topic string
Type string
UserID string
Presence string
Notification bool
StylePrefix string
StyleIcon string
StyleText string
}
// ToString will set the label of the channel, how it will be
// displayed on screen. Based on the type, different icons are
// shown, as well as an optional notification icon.
func (c ChannelItem) ToString() string {
var prefix string
if c.Notification {
prefix = IconNotification
} else {
prefix = " "
}
var icon string
switch c.Type {
case ChannelTypeChannel:
icon = IconChannel
case ChannelTypeGroup:
icon = IconGroup
case ChannelTypeMpIM:
icon = IconMpIM
case ChannelTypeIM:
switch c.Presence {
case PresenceActive:
icon = IconOnline
case PresenceAway:
icon = IconOffline
default:
icon = IconIM
}
}
label := fmt.Sprintf(
"[%s](%s) [%s](%s) [%s](%s)",
prefix, c.StylePrefix,
icon, c.StyleIcon,
c.Name, c.StyleText,
)
return label
}
// GetChannelName will return a formatted representation of the
// name of the channel
func (c ChannelItem) GetChannelName() string {
var channelName string
if c.Topic != "" {
channelName = fmt.Sprintf("%s - %s",
html.UnescapeString(c.Name),
html.UnescapeString(c.Topic),
)
} else {
channelName = c.Name
}
return channelName
}
// Channels is the definition of a Channels component
type Channels struct {
ChannelItems []ChannelItem
List *termui.List
SelectedChannel int // index of which channel is selected from the List
Offset int // from what offset are channels rendered
Component
Items []string
SelectedChannel int // index of which channel is selected from the Items
Offset int // from what offset are channels rendered, FIXME probably not necessary anymore
CursorPosition int // the y position of the 'cursor'
SearchMatches []int // index of the search matches
SearchPosition int // current position of a search match
// SelectorBGColor
// SelectorFGColor
}
// CreateChannels is the constructor for the Channels component
func CreateChannelsComponent(height int) *Channels {
channels := &Channels{
List: termui.NewList(),
}
// Constructor for the Channels component
func CreateChannelsComponent(x, y, w, h int) *Channels {
channels := &Channels{}
channels.List.BorderLabel = "Channels"
channels.List.Height = height
channels.SelectedChannel = 0
channels.Offset = 0
channels.CursorPosition = channels.List.InnerBounds().Min.Y
channels.Name = "channels"
channels.Y = y
channels.X = x
channels.Width = w
channels.Height = h
return channels
}
// Buffer implements interface termui.Bufferer
func (c *Channels) Buffer() termui.Buffer {
buf := c.List.Buffer()
for i, item := range c.ChannelItems[c.Offset:] {
y := c.List.InnerBounds().Min.Y + i
if y > c.List.InnerBounds().Max.Y-1 {
break
// Layout will setup the visible part of the Channels component and implements
// the gocui.Manager interface
func (c *Channels) Layout(g *gocui.Gui) error {
if v, err := g.SetView(c.Name, c.X, c.Y, c.X+c.Width, c.Y+c.Height); err != nil {
if err != gocui.ErrUnknownView {
return err
}
// Set the visible cursor
var cells []termui.Cell
if y == c.CursorPosition {
cells = termui.DefaultTxBuilder.Build(
item.ToString(), c.List.ItemBgColor, c.List.ItemFgColor)
} else {
cells = termui.DefaultTxBuilder.Build(
item.ToString(), c.List.ItemFgColor, c.List.ItemBgColor)
v.Highlight = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
for _, item := range c.Items {
fmt.Fprintln(v, item)
}
// Append ellipsis when overflows
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
c.View = v
x := c.List.InnerBounds().Min.X
for _, cell := range cells {
buf.Set(x, y, cell)
x += cell.Width()
}
}
return nil
}
// When not at the end of the pane fill it up empty characters
for x < c.List.InnerBounds().Max.X {
if y == c.CursorPosition {
buf.Set(x, y,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemBgColor,
Bg: c.List.ItemFgColor,
},
)
} else {
buf.Set(
x, y,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
Bg: c.List.ItemBgColor,
},
)
}
x++
// SetChannels will set the channels from the service, passed as an argument
// to the Items field
// FIXME: maybe rename to LoadChannels?
func (c *Channels) SetChannels(channels []service.Channel) {
for _, slackChan := range channels {
label := setChannelLabel(slackChan, false)
c.Items = append(c.Items, label)
}
}
// SetPresenceChannels will set the icon for all the IM channels
func (c *Channels) SetPresenceChannels(channels []service.Channel) {
for _, slackChan := range channels {
if slackChan.Type == service.ChannelTypeIM {
c.SetPresenceChannel(channels, slackChan.UserID, slackChan.Presence)
}
}
return buf
}
// GetHeight implements interface termui.GridBufferer
func (c *Channels) GetHeight() int {
return c.List.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (c *Channels) SetWidth(w int) {
c.List.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (c *Channels) SetX(x int) {
c.List.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (c *Channels) SetY(y int) {
c.List.SetY(y)
}
func (c *Channels) SetChannels(channels []ChannelItem) {
c.ChannelItems = channels
}
func (c *Channels) MarkAsRead(channelID int) {
c.ChannelItems[channelID].Notification = false
}
func (c *Channels) MarkAsUnread(channelID string) {
index := c.FindChannel(channelID)
c.ChannelItems[index].Notification = true
}
func (c *Channels) SetPresence(channelID string, presence string) {
index := c.FindChannel(channelID)
c.ChannelItems[index].Presence = presence
}
func (c *Channels) FindChannel(channelID string) int {
// SetPresence will set the correct icon for one IM channel
func (c *Channels) SetPresenceChannel(channels []service.Channel, userID string, presence string) {
// Get the correct Channel from svc.Channels
var index int
for i, channel := range c.ChannelItems {
if channel.ID == channelID {
for i, channel := range channels {
if userID == channel.UserID {
index = i
break
}
}
return index
switch presence {
case PresenceActive:
c.Items[index] = strings.Replace(
c.Items[index], IconOffline, IconOnline, 1,
)
case PresenceAway:
c.Items[index] = strings.Replace(
c.Items[index], IconOnline, IconOffline, 1,
)
default:
c.Items[index] = strings.Replace(
c.Items[index], IconOnline, IconOffline, 1,
)
}
}
// SetSelectedChannel sets the SelectedChannel given the index
// TODO: documentation
func (c *Channels) SetSelectedChannel(index int) {
c.SelectedChannel = index
}
// Get SelectedChannel returns the ChannelItem that is currently selected
func (c *Channels) GetSelectedChannel() ChannelItem {
return c.ChannelItems[c.SelectedChannel]
// TODO: documentation
func (c *Channels) GetSelectedChannel() string {
return c.Items[c.SelectedChannel]
}
// MoveCursorUp will decrease the SelectedChannel by 1
func (c *Channels) MoveCursorUp() {
func (c *Channels) MoveCursorUp() error {
if c.SelectedChannel > 0 {
c.SetSelectedChannel(c.SelectedChannel - 1)
c.ScrollUp()
c.MarkAsRead()
}
return nil
}
// MoveCursorDown will increase the SelectedChannel by 1
func (c *Channels) MoveCursorDown() {
if c.SelectedChannel < len(c.ChannelItems)-1 {
func (c *Channels) MoveCursorDown() error {
if c.SelectedChannel < len(c.Items)-1 {
c.SetSelectedChannel(c.SelectedChannel + 1)
c.ScrollDown()
c.MarkAsRead()
}
return nil
}
// MoveCursorTop will move the cursor to the top of the channels
func (c *Channels) MoveCursorTop() {
c.SetSelectedChannel(0)
c.CursorPosition = c.List.InnerBounds().Min.Y
c.Offset = 0
}
// func (c *Channels) MoveCursorTop() {
// c.SetSelectedChannel(0)
// c.CursorPosition = c.List.InnerBounds().Min.Y // FIXME
// c.Offset = 0
// }
// MoveCursorBottom will move the cursor to the bottom of the channels
func (c *Channels) MoveCursorBottom() {
c.SetSelectedChannel(len(c.ChannelItems) - 1)
offset := len(c.ChannelItems) - (c.List.InnerBounds().Max.Y - 1)
if offset < 0 {
c.Offset = 0
c.CursorPosition = c.SelectedChannel + 1
} else {
c.Offset = offset
c.CursorPosition = c.List.InnerBounds().Max.Y - 1
}
}
// func (c *Channels) MoveCursorBottom() {
// c.SetSelectedChannel(len(c.Items) - 1)
//
// offset := len(c.List.Items) - (c.List.InnerBounds().Max.Y - 1) // FIXME
//
// if offset < 0 {
// c.Offset = 0
// c.CursorPosition = c.SelectedChannel + 1
// } else {
// c.Offset = offset
// c.CursorPosition = c.List.InnerBounds().Max.Y - 1 // FIXME
// }
// }
// ScrollUp enables us to scroll through the channel list when it overflows
func (c *Channels) ScrollUp() {
// Is cursor at the top of the channel view?
if c.CursorPosition == c.List.InnerBounds().Min.Y {
if c.Offset > 0 {
c.Offset--
}
} else {
c.CursorPosition--
originX, originY := c.View.Origin()
cursorX, cursorY := c.View.Cursor()
// When cursor is at the beginning of the view then decrease
// the origin of the view
if cursorY-1 < 0 {
c.View.SetOrigin(originX, originY-1)
}
c.View.SetCursor(cursorX, cursorY-1)
}
// ScrollDown enables us to scroll through the channel list when it overflows
func (c *Channels) ScrollDown() {
// Is the cursor at the bottom of the channel view?
if c.CursorPosition == c.List.InnerBounds().Max.Y-1 {
if c.Offset < len(c.ChannelItems)-1 {
c.Offset++
}
} else {
c.CursorPosition++
originX, originY := c.View.Origin()
cursorX, cursorY := c.View.Cursor()
// When cursor is at the end of the view then increase
// the origin of the view
if cursorY+1 > c.Height-2 {
c.View.SetOrigin(originX, originY+1)
}
c.View.SetCursor(cursorX, cursorY+1)
}
// Search will search through the channels to find a channel,
// when a match has been found the selected channel will then
// be the channel that has been found
func (c *Channels) Search(term string) {
c.SearchMatches = make([]int, 0)
// func (c *Channels) Search(term string) {
// for i, item := range c.Items {
// if strings.Contains(item, term) {
//
// // The new position
// newPos := i
//
// // Is the new position in range of the current view?
// minRange := c.Offset
// maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2) // FIXME
//
// if newPos < minRange {
// // newPos is above, we need to scroll up.
// c.SetSelectedChannel(i)
//
// // How much do we need to scroll to get it into range?
// c.Offset = c.Offset - (minRange - newPos)
// } else if newPos > maxRange {
// // newPos is below, we need to scroll down
// c.SetSelectedChannel(i)
//
// // How much do we need to scroll to get it into range?
// c.Offset = c.Offset + (newPos - maxRange)
// } else {
// // newPos is inside range
// c.SetSelectedChannel(i)
// }
//
// // Set cursor to correct position
// c.CursorPosition = (newPos - c.Offset) + 1
//
// break
// }
// }
// }
targets := make([]string, 0)
for _, c := range c.ChannelItems {
targets = append(targets, c.Name)
}
matches := fuzzy.Find(term, targets)
for _, m := range matches {
for i, item := range c.ChannelItems {
if m == item.Name {
c.SearchMatches = append(c.SearchMatches, i)
break
}
}
}
if len(c.SearchMatches) > 0 {
c.GotoPositionSearch(0)
c.SearchPosition = 0
}
}
// GotoPosition is used by to automatically scroll to a specific
// location in the channels component
func (c *Channels) GotoPosition(newPos int) {
// Is the new position in range of the current view?
minRange := c.Offset
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
if newPos < minRange {
// newPos is above, we need to scroll up.
c.SetSelectedChannel(newPos)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset - (minRange - newPos)
} else if newPos > maxRange {
// newPos is below, we need to scroll down
c.SetSelectedChannel(newPos)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset + (newPos - maxRange)
} else {
// newPos is inside range
c.SetSelectedChannel(newPos)
}
// Set cursor to correct position
c.CursorPosition = (newPos - c.Offset) + 1
}
// GotoPosition is used by the search functionality to automatically
// scroll to a specific location in the channels component
func (c *Channels) GotoPositionSearch(position int) {
newPos := c.SearchMatches[position]
c.GotoPosition(newPos)
}
// SearchNext allows us to cycle through the c.SearchMatches
func (c *Channels) SearchNext() {
newPosition := c.SearchPosition + 1
if newPosition <= len(c.SearchMatches)-1 {
c.GotoPositionSearch(newPosition)
c.SearchPosition = newPosition
}
}
// SearchPrev allows us to cycle through the c.SearchMatches
func (c *Channels) SearchPrev() {
newPosition := c.SearchPosition - 1
if newPosition >= 0 {
c.GotoPositionSearch(newPosition)
c.SearchPosition = newPosition
}
}
// Jump to the first channel with a notification
func (c *Channels) Jump() {
for i, channel := range c.ChannelItems {
if channel.Notification {
c.GotoPosition(i)
// MarkAsUnread will be called when a new message arrives and will
// render an notification icon in front of the channel name
func (c *Channels) MarkAsUnRead(channels []service.Channel, channelID string) {
// Get the correct Channel from svc.Channels
var index int
for i, channel := range channels {
if channelID == channel.ID {
index = i
break
}
}
if !strings.Contains(c.Items[index], IconNotification) {
// The order of svc.Channels relates to the order of
// List.Items, index will be the index of the channel
c.Items[index] = fmt.Sprintf(
"%s %s", IconNotification, strings.TrimSpace(c.Items[index]),
)
}
// Play terminal bell sound
fmt.Print("\a")
}
// MarkAsRead will remove the notification icon in front of
// a channel that received a new message. This will happen as one will
// move up or down the cursor for Channels
func (c *Channels) MarkAsRead() {
channelName := strings.Split(
c.Items[c.SelectedChannel],
fmt.Sprintf("%s ", IconNotification),
)
if len(channelName) > 1 {
c.Items[c.SelectedChannel] = fmt.Sprintf(" %s", channelName[1])
} else {
c.Items[c.SelectedChannel] = channelName[0]
}
}
// setChannelLabel will set the label of the channel, meaning, how it
// is displayed on screen. Based on the type, different icons are
// shown, as well as an optional notification icon.
// func setChannelLabel(channel service.Channel, notification bool) string {
// var prefix string
// if notification {
// prefix = IconNotification
// } else {
// prefix = " "
// }
//
// var label string
// switch channel.Type {
// case service.ChannelTypeChannel:
// label = fmt.Sprintf("%s %s %s", prefix, IconChannel, channel.Name)
// case service.ChannelTypeGroup:
// label = fmt.Sprintf("%s %s %s", prefix, IconGroup, channel.Name)
// case service.ChannelTypeIM:
// label = fmt.Sprintf("%s %s %s", prefix, IconIM, channel.Name)
// }
//
// return label
// }

342
components/channels_bkp.go Normal file
View File

@ -0,0 +1,342 @@
package components
import (
"fmt"
"strings"
"github.com/gizak/termui"
"github.com/erroneousboat/slack-term/service"
)
// Channels is the definition of a Channels component
type ChannelsBKP struct {
List *termui.List
SelectedChannel int // index of which channel is selected from the List
Offset int // from what offset are channels rendered
CursorPosition int // the y position of the 'cursor'
}
// CreateChannels is the constructor for the Channels component
func CreateChannels(svc *service.SlackService, inputHeight int) *ChannelsBKP {
channels := &ChannelsBKP{
List: termui.NewList(),
}
channels.List.BorderLabel = "Channels"
channels.List.Height = termui.TermHeight() - inputHeight
channels.SelectedChannel = 0
channels.Offset = 0
channels.CursorPosition = channels.List.InnerBounds().Min.Y
channels.GetChannels(svc)
channels.SetPresenceForIMChannels(svc)
return channels
}
// Buffer implements interface termui.Bufferer
func (c *ChannelsBKP) Buffer() termui.Buffer {
buf := c.List.Buffer()
for i, item := range c.List.Items[c.Offset:] {
y := c.List.InnerBounds().Min.Y + i
if y > c.List.InnerBounds().Max.Y-1 {
break
}
var cells []termui.Cell
if y == c.CursorPosition {
cells = termui.DefaultTxBuilder.Build(
item, c.List.ItemBgColor, c.List.ItemFgColor)
} else {
cells = termui.DefaultTxBuilder.Build(
item, c.List.ItemFgColor, c.List.ItemBgColor)
}
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
x := 0
for _, cell := range cells {
width := cell.Width()
buf.Set(c.List.InnerBounds().Min.X+x, y, cell)
x += width
}
// When not at the end of the pane fill it up empty characters
for x < c.List.InnerBounds().Max.X-1 {
if y == c.CursorPosition {
buf.Set(x+1, y,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemBgColor,
Bg: c.List.ItemFgColor,
},
)
} else {
buf.Set(
x+1, y,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
Bg: c.List.ItemBgColor,
},
)
}
x++
}
}
return buf
}
// GetHeight implements interface termui.GridBufferer
func (c *ChannelsBKP) GetHeight() int {
return c.List.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (c *ChannelsBKP) SetWidth(w int) {
c.List.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (c *ChannelsBKP) SetX(x int) {
c.List.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (c *ChannelsBKP) SetY(y int) {
c.List.SetY(y)
}
// GetChannels will get all available channels from the SlackService
func (c *ChannelsBKP) GetChannels(svc *service.SlackService) {
for _, slackChan := range svc.GetChannels() {
label := setChannelLabel(slackChan, false)
c.List.Items = append(c.List.Items, label)
}
}
// SetPresenceForIMChannels this will set the correct icon for
// IM channels for when they're online of away
func (c *ChannelsBKP) SetPresenceForIMChannels(svc *service.SlackService) {
for _, slackChan := range svc.GetChannels() {
if slackChan.Type == service.ChannelTypeIM {
presence, err := svc.GetUserPresence(slackChan.UserID)
if err != nil {
continue
}
c.SetPresence(svc, slackChan.UserID, presence)
}
}
}
// SetSelectedChannel sets the SelectedChannel given the index
func (c *ChannelsBKP) SetSelectedChannel(index int) {
c.SelectedChannel = index
}
// MoveCursorUp will decrease the SelectedChannel by 1
func (c *ChannelsBKP) MoveCursorUp() {
if c.SelectedChannel > 0 {
c.SetSelectedChannel(c.SelectedChannel - 1)
c.ScrollUp()
c.ClearNewMessageIndicator()
}
}
// MoveCursorDown will increase the SelectedChannel by 1
func (c *ChannelsBKP) MoveCursorDown() {
if c.SelectedChannel < len(c.List.Items)-1 {
c.SetSelectedChannel(c.SelectedChannel + 1)
c.ScrollDown()
c.ClearNewMessageIndicator()
}
}
// MoveCursorTop will move the cursor to the top of the channels
func (c *ChannelsBKP) MoveCursorTop() {
c.SetSelectedChannel(0)
c.CursorPosition = c.List.InnerBounds().Min.Y
c.Offset = 0
}
// MoveCursorBottom will move the cursor to the bottom of the channels
func (c *ChannelsBKP) MoveCursorBottom() {
c.SetSelectedChannel(len(c.List.Items) - 1)
offset := len(c.List.Items) - (c.List.InnerBounds().Max.Y - 1)
if offset < 0 {
c.Offset = 0
c.CursorPosition = c.SelectedChannel + 1
} else {
c.Offset = offset
c.CursorPosition = c.List.InnerBounds().Max.Y - 1
}
}
// ScrollUp enables us to scroll through the channel list when it overflows
func (c *ChannelsBKP) ScrollUp() {
// Is cursor at the top of the channel view?
if c.CursorPosition == c.List.InnerBounds().Min.Y {
if c.Offset > 0 {
c.Offset--
}
} else {
c.CursorPosition--
}
}
// ScrollDown enables us to scroll through the channel list when it overflows
func (c *ChannelsBKP) ScrollDown() {
// Is the cursor at the bottom of the channel view?
if c.CursorPosition == c.List.InnerBounds().Max.Y-1 {
if c.Offset < len(c.List.Items)-1 {
c.Offset++
}
} else {
c.CursorPosition++
}
}
// Search will search through the channels to find a channel,
// when a match has been found the selected channel will then
// be the channel that has been found
func (c *ChannelsBKP) Search(term string) {
for i, item := range c.List.Items {
if strings.Contains(item, term) {
// The new position
newPos := i
// Is the new position in range of the current view?
minRange := c.Offset
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
if newPos < minRange {
// newPos is above, we need to scroll up.
c.SetSelectedChannel(i)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset - (minRange - newPos)
} else if newPos > maxRange {
// newPos is below, we need to scroll down
c.SetSelectedChannel(i)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset + (newPos - maxRange)
} else {
// newPos is inside range
c.SetSelectedChannel(i)
}
// Set cursor to correct position
c.CursorPosition = (newPos - c.Offset) + 1
break
}
}
}
// SetNotification will be called when a new message arrives and will
// render an notification icon in front of the channel name
func (c *ChannelsBKP) SetNotification(svc *service.SlackService, channelID string) {
// Get the correct Channel from svc.Channels
var index int
for i, channel := range svc.Channels {
if channelID == channel.ID {
index = i
break
}
}
if !strings.Contains(c.List.Items[index], IconNotification) {
// The order of svc.Channels relates to the order of
// List.Items, index will be the index of the channel
c.List.Items[index] = fmt.Sprintf(
"%s %s", IconNotification, strings.TrimSpace(c.List.Items[index]),
)
}
// Play terminal bell sound
fmt.Print("\a")
}
// ClearNewMessageIndicator will remove the notification icon in front of
// a channel that received a new message. This will happen as one will
// move up or down the cursor for Channels
func (c *ChannelsBKP) ClearNewMessageIndicator() {
channelName := strings.Split(
c.List.Items[c.SelectedChannel],
fmt.Sprintf("%s ", IconNotification),
)
if len(channelName) > 1 {
c.List.Items[c.SelectedChannel] = fmt.Sprintf(" %s", channelName[1])
} else {
c.List.Items[c.SelectedChannel] = channelName[0]
}
}
// SetReadMark will send the ReadMark event on the service
func (c *ChannelsBKP) SetReadMark(svc *service.SlackService) {
svc.SetChannelReadMark(svc.SlackChannels[c.SelectedChannel])
}
// SetPresence will set the correct icon for a IM Channel
func (c *ChannelsBKP) SetPresence(svc *service.SlackService, userID string, presence string) {
// Get the correct Channel from svc.Channels
var index int
for i, channel := range svc.Channels {
if userID == channel.UserID {
index = i
break
}
}
switch presence {
case PresenceActive:
c.List.Items[index] = strings.Replace(
c.List.Items[index], IconOffline, IconOnline, 1,
)
case PresenceAway:
c.List.Items[index] = strings.Replace(
c.List.Items[index], IconOnline, IconOffline, 1,
)
default:
c.List.Items[index] = strings.Replace(
c.List.Items[index], IconOnline, IconOffline, 1,
)
}
}
// setChannelLabel will set the label of the channel, meaning, how it
// is displayed on screen. Based on the type, different icons are
// shown, as well as an optional notification icon.
func setChannelLabel(channel service.Channel, notification bool) string {
var prefix string
if notification {
prefix = IconNotification
} else {
prefix = " "
}
var label string
switch channel.Type {
case service.ChannelTypeChannel:
label = fmt.Sprintf("%s %s %s", prefix, IconChannel, channel.Name)
case service.ChannelTypeGroup:
label = fmt.Sprintf("%s %s %s", prefix, IconGroup, channel.Name)
case service.ChannelTypeIM:
label = fmt.Sprintf("%s %s %s", prefix, IconIM, channel.Name)
}
return label
}

View File

@ -2,363 +2,67 @@ package components
import (
"fmt"
"sort"
"strings"
"time"
"github.com/erroneousboat/termui"
runewidth "github.com/mattn/go-runewidth"
"github.com/erroneousboat/slack-term/config"
"github.com/erroneousboat/gocui"
)
// Chat is the definition of a Chat component
type Chat struct {
List *termui.List
Messages map[string]Message
Offset int
Component
Items []string
}
// CreateChatComponent is the constructor for the Chat struct
func CreateChatComponent(inputHeight int) *Chat {
chat := &Chat{
List: termui.NewList(),
Messages: make(map[string]Message),
Offset: 0,
}
// Constructor for the Chat component
func CreateChatComponent(x, y, w, h int) *Chat {
chat := &Chat{}
chat.List.Height = termui.TermHeight() - inputHeight
chat.List.Overflow = "wrap"
chat.Name = "chat"
chat.Y = y
chat.X = x
chat.Width = w
chat.Height = h
return chat
}
// Buffer implements interface termui.Bufferer
func (c *Chat) Buffer() termui.Buffer {
// Convert Messages into termui.Cell
cells := c.MessagesToCells(c.Messages)
// Layout will setup the visible part of the Chat component and implements
// the gocui.Manager interface
func (c *Chat) Layout(g *gocui.Gui) error {
// We will create an array of Line structs, this allows us
// to more easily render the items in a list. We will range
// over the cells we've created and create a Line within
// the bounds of the Chat pane
type Line struct {
cells []termui.Cell
if v, err := g.SetView(c.Name, c.X, c.Y, c.X+c.Width, c.Y+c.Height); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Wrap = true
v.Autoscroll = true // FIXME: see if you need this
for _, msg := range c.Items {
fmt.Fprintln(v, msg)
}
c.View = v
}
lines := []Line{}
line := Line{}
return nil
}
// When we encounter a newline or, are at the bounds of the chat view we
// stop iterating over the cells and add the line to the line array
x := 0
for _, cell := range cells {
// When we encounter a newline we add the line to the array
if cell.Ch == '\n' {
lines = append(lines, line)
// Reset for new line
line = Line{}
x = 0
continue
}
if x+cell.Width() > c.List.InnerBounds().Dx() {
lines = append(lines, line)
// Reset for new line
line = Line{}
x = 0
}
line.cells = append(line.cells, cell)
x += cell.Width()
// FIXME: maybe not necessary
func (c *Chat) Refresh() {
for _, msg := range c.Items {
fmt.Fprintln(c.View, msg)
}
// Append the last line to the array when we didn't encounter any
// newlines or were at the bounds of the chat view
lines = append(lines, line)
// We will print lines bottom up, it will loop over the lines
// backwards and for every line it'll set the cell in that line.
// Offset is the number which allows us to begin printing the
// line above the last line.
buf := c.List.Buffer()
linesHeight := len(lines)
paneMinY := c.List.InnerBounds().Min.Y
paneMaxY := c.List.InnerBounds().Max.Y
currentY := paneMaxY - 1
for i := (linesHeight - 1) - c.Offset; i >= 0; i-- {
if currentY < paneMinY {
break
}
x := c.List.InnerBounds().Min.X
for _, cell := range lines[i].cells {
buf.Set(x, currentY, cell)
x += cell.Width()
}
// When we're not at the end of the pane, fill it up
// with empty characters
for x < c.List.InnerBounds().Max.X {
buf.Set(
x, currentY,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
Bg: c.List.ItemBgColor,
},
)
x += runewidth.RuneWidth(' ')
}
currentY--
}
// If the space above currentY is empty we need to fill
// it up with blank lines, otherwise the List object will
// render the items top down, and the result will mix.
for currentY >= paneMinY {
x := c.List.InnerBounds().Min.X
for x < c.List.InnerBounds().Max.X {
buf.Set(
x, currentY,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
Bg: c.List.ItemBgColor,
},
)
x += runewidth.RuneWidth(' ')
}
currentY--
}
return buf
}
// GetHeight implements interface termui.GridBufferer
func (c *Chat) GetHeight() int {
return c.List.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (c *Chat) SetWidth(w int) {
c.List.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (c *Chat) SetX(x int) {
c.List.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (c *Chat) SetY(y int) {
c.List.SetY(y)
}
// GetMaxItems return the maximal amount of items can fit in the Chat
// component
func (c *Chat) GetMaxItems() int {
return c.List.InnerBounds().Max.Y - c.List.InnerBounds().Min.Y
}
// SetMessages will put the provided messages into the Messages field of the
// Chat view
func (c *Chat) SetMessages(messages []Message) {
// Reset offset first, when scrolling in view and changing channels we
// want the offset to be 0 when loading new messages
c.Offset = 0
// SetMessage will put the provided message into the the Items field
// of the Chat view
func (c *Chat) SetMessages(messages []string) {
for _, msg := range messages {
c.Messages[msg.ID] = msg
c.Items = append(c.Items, msg)
}
}
// AddMessage adds a single message to Messages
func (c *Chat) AddMessage(message Message) {
c.Messages[message.ID] = message
}
// AddReply adds a single reply to a parent thread, it also sets
// the thread separator
func (c *Chat) AddReply(parentID string, message Message) {
// It is possible that a message is received but the parent is not
// present in the chat view
if _, ok := c.Messages[parentID]; ok {
message.Thread = " "
c.Messages[parentID].Messages[message.ID] = message
} else {
c.AddMessage(message)
}
}
// IsNewThread check whether a message that is going to be added as
// a child to a parent message, is the first one or not
func (c *Chat) IsNewThread(parentID string) bool {
if parent, ok := c.Messages[parentID]; ok {
if len(parent.Messages) > 0 {
return true
}
}
return false
}
// ClearMessages clear the c.Messages
// ClearMessages clear the c.Items
func (c *Chat) ClearMessages() {
c.Messages = make(map[string]Message)
}
// ScrollUp will render the chat messages based on the Offset of the Chat
// pane.
//
// Offset is 0 when scrolled down. (we loop backwards over the array, so we
// start with rendering last item in the list at the maximum y of the Chat
// pane). Increasing the Offset will thus result in substracting the offset
// from the len(Chat.Messages).
func (c *Chat) ScrollUp() {
c.Offset = c.Offset + 10
// Protect overscrolling
if c.Offset > len(c.Messages) {
c.Offset = len(c.Messages)
}
}
// ScrollDown will render the chat messages based on the Offset of the Chat
// pane.
//
// Offset is 0 when scrolled down. (we loop backwards over the array, so we
// start with rendering last item in the list at the maximum y of the Chat
// pane). Increasing the Offset will thus result in substracting the offset
// from the len(Chat.Messages).
func (c *Chat) ScrollDown() {
c.Offset = c.Offset - 10
// Protect overscrolling
if c.Offset < 0 {
c.Offset = 0
}
}
// SetBorderLabel will set Label of the Chat pane to the specified string
func (c *Chat) SetBorderLabel(channelName string) {
c.List.BorderLabel = channelName
}
// MessagesToCells is a wrapper around MessageToCells to use for a slice of
// of type Message
func (c *Chat) MessagesToCells(msgs map[string]Message) []termui.Cell {
cells := make([]termui.Cell, 0)
sortedMessages := SortMessages(msgs)
for i, msg := range sortedMessages {
cells = append(cells, c.MessageToCells(msg)...)
if len(msg.Messages) > 0 {
cells = append(cells, termui.Cell{Ch: '\n'})
cells = append(cells, c.MessagesToCells(msg.Messages)...)
}
// Add a newline after every message
if i < len(sortedMessages)-1 {
cells = append(cells, termui.Cell{Ch: '\n'})
}
}
return cells
}
// MessageToCells will convert a Message struct to termui.Cell
//
// We're building parts of the message individually, or else DefaultTxBuilder
// will interpret potential markdown usage in a message as well.
func (c *Chat) MessageToCells(msg Message) []termui.Cell {
cells := make([]termui.Cell, 0)
// When msg.Time and msg.Name are empty (in the case of attachments)
// don't add the time and name parts.
if (msg.Time != time.Time{} && msg.Name != "") {
// Time
cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetTime(),
termui.ColorDefault, termui.ColorDefault)...,
)
// Thread
cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetThread(),
termui.ColorDefault, termui.ColorDefault)...,
)
// Name
cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetName(),
termui.ColorDefault, termui.ColorDefault)...,
)
}
// Hack, in order to get the correct fg and bg attributes. This is
// because the readAttr function in termui is unexported.
txCells := termui.DefaultTxBuilder.Build(
msg.GetContent(),
termui.ColorDefault, termui.ColorDefault,
)
// Text
for _, r := range msg.Content {
cells = append(
cells,
termui.Cell{
Ch: r,
Fg: txCells[0].Fg,
Bg: txCells[0].Bg,
},
)
}
return cells
}
// Help shows the usage and key bindings in the chat pane
func (c *Chat) Help(usage string, cfg *config.Config) {
msgUsage := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: usage,
}
c.Messages[msgUsage.ID] = msgUsage
for mode, mapping := range cfg.KeyMap {
msgMode := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: fmt.Sprintf("%s", strings.ToUpper(mode)),
}
c.Messages[msgMode.ID] = msgMode
msgNewline := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: "",
}
c.Messages[msgNewline.ID] = msgNewline
var keys []string
for k := range mapping {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
msgKey := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: fmt.Sprintf(" %-12s%-15s", k, mapping[k]),
}
c.Messages[msgKey.ID] = msgKey
}
msgNewline.ID = fmt.Sprintf("%d", time.Now().UnixNano())
c.Messages[msgNewline.ID] = msgNewline
}
c.Items = []string{}
c.View.Clear()
}

253
components/chat_bkp.go Normal file
View File

@ -0,0 +1,253 @@
package components
import (
"fmt"
"html"
"sort"
"strings"
"github.com/gizak/termui"
"github.com/erroneousboat/slack-term/config"
"github.com/erroneousboat/slack-term/service"
)
// Chat is the definition of a Chat component
type ChatBKP struct {
List *termui.List
Offset int
}
// CreateChat is the constructor for the Chat struct
func CreateChat(svc *service.SlackService, inputHeight int, selectedSlackChannel interface{}, selectedChannel service.Channel) *ChatBKP {
chat := &ChatBKP{
List: termui.NewList(),
Offset: 0,
}
chat.List.Height = termui.TermHeight() - inputHeight
chat.List.Overflow = "wrap"
chat.GetMessages(svc, selectedSlackChannel)
chat.SetBorderLabel(selectedChannel)
return chat
}
// Buffer implements interface termui.Bufferer
func (c *ChatBKP) Buffer() termui.Buffer {
// Build cells, after every item put a newline
cells := termui.DefaultTxBuilder.Build(
strings.Join(c.List.Items, "\n"),
c.List.ItemFgColor, c.List.ItemBgColor,
)
// We will create an array of Line structs, this allows us
// to more easily render the items in a list. We will range
// over the cells we've created and create a Line within
// the bounds of the Chat pane
type Line struct {
cells []termui.Cell
}
lines := []Line{}
line := Line{}
x := 0
for _, cell := range cells {
if cell.Ch == '\n' {
lines = append(lines, line)
line = Line{}
x = 0
continue
}
if x+cell.Width() > c.List.InnerBounds().Dx() {
lines = append(lines, line)
line = Line{}
x = 0
}
line.cells = append(line.cells, cell)
x++
}
lines = append(lines, line)
// We will print lines bottom up, it will loop over the lines
// backwards and for every line it'll set the cell in that line.
// Offset is the number which allows us to begin printing the
// line above the last line.
buf := c.List.Buffer()
linesHeight := len(lines)
paneMinY := c.List.InnerBounds().Min.Y
paneMaxY := c.List.InnerBounds().Max.Y
currentY := paneMaxY - 1
for i := (linesHeight - 1) - c.Offset; i >= 0; i-- {
if currentY < paneMinY {
break
}
x := c.List.InnerBounds().Min.X
for _, cell := range lines[i].cells {
buf.Set(x, currentY, cell)
x += cell.Width()
}
// When we're not at the end of the pane, fill it up
// with empty characters
for x < c.List.InnerBounds().Max.X {
buf.Set(
x, currentY,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
Bg: c.List.ItemBgColor,
},
)
x++
}
currentY--
}
// If the space above currentY is empty we need to fill
// it up with blank lines, otherwise the List object will
// render the items top down, and the result will mix.
for currentY >= paneMinY {
x := c.List.InnerBounds().Min.X
for x < c.List.InnerBounds().Max.X {
buf.Set(
x, currentY,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
Bg: c.List.ItemBgColor,
},
)
x++
}
currentY--
}
return buf
}
// GetHeight implements interface termui.GridBufferer
func (c *ChatBKP) GetHeight() int {
return c.List.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (c *ChatBKP) SetWidth(w int) {
c.List.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (c *ChatBKP) SetX(x int) {
c.List.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (c *ChatBKP) SetY(y int) {
c.List.SetY(y)
}
// GetMessages will get an array of strings for a specific channel which will
// contain messages in turn all these messages will be added to List.Items
func (c *ChatBKP) GetMessages(svc *service.SlackService, channel interface{}) {
// Get the count of message that fit in the pane
count := c.List.InnerBounds().Max.Y - c.List.InnerBounds().Min.Y
messages := svc.GetMessages(channel, count)
for _, message := range messages {
c.AddMessage(message)
}
}
// AddMessage adds a single message to List.Items
func (c *ChatBKP) AddMessage(message string) {
c.List.Items = append(c.List.Items, html.UnescapeString(message))
}
// ClearMessages clear the List.Items
func (c *ChatBKP) ClearMessages() {
c.List.Items = []string{}
}
// ScrollUp will render the chat messages based on the Offset of the Chat
// pane.
//
// Offset is 0 when scrolled down. (we loop backwards over the array, so we
// start with rendering last item in the list at the maximum y of the Chat
// pane). Increasing the Offset will thus result in substracting the offset
// from the len(Chat.List.Items).
func (c *ChatBKP) ScrollUp() {
c.Offset = c.Offset + 10
// Protect overscrolling
if c.Offset > len(c.List.Items)-1 {
c.Offset = len(c.List.Items) - 1
}
}
// ScrollDown will render the chat messages based on the Offset of the Chat
// pane.
//
// Offset is 0 when scrolled down. (we loop backwards over the array, so we
// start with rendering last item in the list at the maximum y of the Chat
// pane). Increasing the Offset will thus result in substracting the offset
// from the len(Chat.List.Items).
func (c *ChatBKP) ScrollDown() {
c.Offset = c.Offset - 10
// Protect overscrolling
if c.Offset < 0 {
c.Offset = 0
}
}
// SetBorderLabel will set Label of the Chat pane to the specified string
func (c *ChatBKP) SetBorderLabel(channel service.Channel) {
var channelName string
if channel.Topic != "" {
channelName = fmt.Sprintf("%s - %s",
channel.Name,
channel.Topic,
)
} else {
channelName = channel.Name
}
c.List.BorderLabel = channelName
}
// Help shows the usage and key bindings in the chat pane
func (c *ChatBKP) Help(cfg *config.Config) {
help := []string{
"slack-term - slack client for your terminal",
"",
"USAGE:",
" slack-term -config [path-to-config]",
"",
"KEY BINDINGS:",
"",
}
for mode, mapping := range cfg.KeyMap {
help = append(help, fmt.Sprintf(" %s", strings.ToUpper(mode)))
help = append(help, "")
var keys []string
for k := range mapping {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
help = append(help, fmt.Sprintf(" %-12s%-15s", k, mapping[k]))
}
help = append(help, "")
}
c.List.Items = help
}

14
components/components.go Normal file
View File

@ -0,0 +1,14 @@
package components
import "github.com/erroneousboat/gocui"
// TODO: documentation
// Component
type Component struct {
Name string
X int
Y int
Width int
Height int
View *gocui.View
}

View File

@ -3,73 +3,51 @@ package components
import (
"fmt"
"github.com/erroneousboat/termui"
"github.com/erroneousboat/gocui"
)
// Debug can be used to relay debugging information in the Debug component,
// see event.go on how to use it
// Debug component gives the possibility to print
// debugging statements in the GUI.
//
// Usage:
//
// ctx.View.Debug.SetText("debugging statement")
type Debug struct {
Par *termui.Par
List *termui.List
Component
Text string
}
func CreateDebugComponent(inputHeight int) *Debug {
debug := &Debug{
List: termui.NewList(),
}
debug.List.BorderLabel = "Debug"
debug.List.Height = termui.TermHeight() - inputHeight
debug.List.Overflow = "wrap"
func CreateDebugComponent(x, y, w, h int) *Debug {
debug := &Debug{}
debug.Name = "debug"
debug.X = x
debug.Y = y
debug.Width = w
debug.Height = h
return debug
}
// Buffer implements interface termui.Bufferer
func (d *Debug) Buffer() termui.Buffer {
return d.List.Buffer()
}
// Layout will setup the visible part of the Debug component and implements
// the gocui.Manager interface
func (d *Debug) Layout(g *gocui.Gui) error {
if v, err := g.SetView(d.Name, d.X, d.Y, d.X+d.Width, d.Y+d.Height); err != nil {
if err != gocui.ErrUnknownView {
return err
}
// GetHeight implements interface termui.GridBufferer
func (d *Debug) GetHeight() int {
return d.List.Block.GetHeight()
}
v.Wrap = true
v.Autoscroll = true
// SetWidth implements interface termui.GridBufferer
func (d *Debug) SetWidth(w int) {
d.List.SetWidth(w)
}
fmt.Fprintln(v, d.Text)
// SetX implements interface termui.GridBufferer
func (d *Debug) SetX(x int) {
d.List.SetX(x)
}
d.View = v
// SetY implements interface termui.GridBufferer
func (d *Debug) SetY(y int) {
d.List.SetY(y)
}
// Println will add the text to the Debug component
func (d *Debug) Println(text string) {
d.List.Items = append(d.List.Items, text)
// When at the end remove first item
if len(d.List.Items) > d.List.InnerBounds().Max.Y-1 {
d.List.Items = d.List.Items[1:]
}
termui.Render(d)
return nil
}
func (d *Debug) Sprintf(format string, a ...interface{}) {
text := fmt.Sprintf(format, a...)
d.List.Items = append(d.List.Items, text)
// When at the end remove first item
if len(d.List.Items) > d.List.InnerBounds().Max.Y-1 {
d.List.Items = d.List.Items[1:]
}
termui.Render(d)
// SetText will set the text of the Debug component
func (d *Debug) SetText(text string) {
fmt.Fprintln(d.View, text)
}

View File

@ -1,220 +1,38 @@
package components
import (
"github.com/erroneousboat/termui"
runewidth "github.com/mattn/go-runewidth"
"github.com/erroneousboat/gocui"
)
// Input is the definition of an Input component
type Input struct {
Par *termui.Par
Text []rune
CursorPositionScreen int
CursorPositionText int
Offset int
Component
// Text []rune
}
// CreateInput is the constructor of the Input struct
func CreateInputComponent() *Input {
input := &Input{
Par: termui.NewPar(""),
Text: make([]rune, 0),
CursorPositionScreen: 0,
CursorPositionText: 0,
Offset: 0,
}
func CreateInputComponent(x, y, w, h int) *Input {
input := &Input{}
input.Par.Height = 3
input.Name = "input"
input.Y = y
input.X = x
input.Width = w
input.Height = h
return input
}
// Buffer implements interface termui.Bufferer
func (i *Input) Buffer() termui.Buffer {
buf := i.Par.Buffer()
// Set visible cursor, get char at screen cursor position
char := buf.At(i.Par.InnerX()+i.CursorPositionScreen, i.Par.Block.InnerY())
buf.Set(
i.Par.InnerX()+i.CursorPositionScreen,
i.Par.Block.InnerY(),
termui.Cell{
Ch: char.Ch,
Fg: i.Par.TextBgColor,
Bg: i.Par.TextFgColor,
},
)
return buf
}
// GetHeight implements interface termui.GridBufferer
func (i *Input) GetHeight() int {
return i.Par.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (i *Input) SetWidth(w int) {
i.Par.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (i *Input) SetX(x int) {
i.Par.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (i *Input) SetY(y int) {
i.Par.SetY(y)
}
// Insert will insert a given key at the place of the current CursorPositionText
func (i *Input) Insert(key rune) {
// Append key to the left side
left := make([]rune, len(i.Text[0:i.CursorPositionText]))
copy(left, i.Text[0:i.CursorPositionText])
left = append(left, key)
// Combine left and right side
i.Text = append(left, i.Text[i.CursorPositionText:]...)
i.MoveCursorRight()
}
// Backspace will remove a character in front of the CursorPositionText
func (i *Input) Backspace() {
if i.CursorPositionText > 0 {
// We want the cursor to stay in the same spot when the text
// overflow, revealing the test on the left side when using
// backspace. When all the text has been revealed will move
// the cursor to the left.
if i.Offset > 0 {
i.Offset--
i.CursorPositionText--
} else {
i.MoveCursorLeft()
// Layout will setup the visible part of the Channels component and implements
// the gocui.Manager interface
func (i *Input) Layout(g *gocui.Gui) error {
if v, err := g.SetView(i.Name, i.X, i.Y, i.X+i.Width, i.Y+i.Height); err != nil {
if err != gocui.ErrUnknownView {
return err
}
i.Text = append(i.Text[0:i.CursorPositionText], i.Text[i.CursorPositionText+1:]...)
i.Par.Text = string(i.Text[i.Offset:])
v.Editable = true
i.View = v
}
}
// Delete will remove a character at the CursorPositionText
func (i *Input) Delete() {
if i.CursorPositionText < len(i.Text) {
i.Text = append(i.Text[0:i.CursorPositionText], i.Text[i.CursorPositionText+1:]...)
i.Par.Text = string(i.Text[i.Offset:])
}
}
// MoveCursorRight will increase the current CursorPositionText with 1
func (i *Input) MoveCursorRight() {
if i.CursorPositionText < len(i.Text) {
i.CursorPositionText++
i.ScrollRight()
}
i.Par.Text = string(i.Text[i.Offset:])
}
// MoveCursorLeft will decrease the current CursorPositionText with 1
func (i *Input) MoveCursorLeft() {
if i.CursorPositionText > 0 {
i.CursorPositionText--
i.ScrollLeft()
}
i.Par.Text = string(i.Text[i.Offset:])
}
func (i *Input) ScrollLeft() {
// Is the cursor at the far left of the Input component?
if i.CursorPositionScreen == 0 {
// Decrease offset to show what is on the left side
if i.Offset > 0 {
i.Offset--
}
} else {
i.CursorPositionScreen -= i.GetRuneWidthRight()
}
}
func (i *Input) ScrollRight() {
// Is the cursor at the far right of the Input component, cursor
// isn't at the end of the text
if (i.CursorPositionScreen + i.GetRuneWidthLeft()) > i.Par.InnerBounds().Dx()-1 {
// Increase offset to show what is on the right side
if i.Offset < len(i.Text) {
i.Offset = i.CalculateOffset()
i.CursorPositionScreen = i.GetRuneWidthOffsetToCursor()
}
} else {
i.CursorPositionScreen += i.GetRuneWidthLeft()
}
}
// CalculateOffset will, based on the width of the runes on the
// left of the text cursor, calculate the offset that needs to
// be used by the Inpute Component
func (i *Input) CalculateOffset() int {
var offset int
var currentRuneWidth int
for j := (i.CursorPositionText - 1); currentRuneWidth < i.GetMaxWidth()-1; j-- {
currentRuneWidth += runewidth.RuneWidth(i.Text[j])
offset = j
}
return offset
}
// GetRunWidthOffsetToCursor will get the rune width of all
// the runes from the offset until the text cursor
func (i *Input) GetRuneWidthOffsetToCursor() int {
return runewidth.StringWidth(string(i.Text[i.Offset:i.CursorPositionText]))
}
// GetRuneWidthLeft will get the width of a rune on the left side
// of the CursorPositionText
func (i *Input) GetRuneWidthLeft() int {
return runewidth.RuneWidth(i.Text[i.CursorPositionText-1])
}
// GetRuneWidthLeft will get the width of a rune on the right side
// of the CursorPositionText
func (i *Input) GetRuneWidthRight() int {
return runewidth.RuneWidth(i.Text[i.CursorPositionText])
}
// IsEmpty will return true when the input is empty
func (i *Input) IsEmpty() bool {
if i.Par.Text == "" {
return true
}
return false
}
// Clear will empty the input and move the cursor to the start position
func (i *Input) Clear() {
i.Text = make([]rune, 0)
i.Par.Text = ""
i.CursorPositionScreen = 0
i.CursorPositionText = 0
i.Offset = 0
}
// GetText returns the text currently in the input
func (i *Input) GetText() string {
return string(i.Text)
}
// GetMaxWidth returns the maximum number of positions
// the Input component can display
func (i *Input) GetMaxWidth() int {
return i.Par.InnerBounds().Dx() - 1
return nil
}

137
components/input_bkp.go Normal file
View File

@ -0,0 +1,137 @@
package components
import (
"github.com/gizak/termui"
"github.com/erroneousboat/slack-term/service"
)
// Input is the definition of an Input component
type InputBKP struct {
Par *termui.Par
Text []rune
CursorPosition int
}
// CreateInput is the constructor of the Input struct
func CreateInput() *InputBKP {
input := &InputBKP{
Par: termui.NewPar(""),
Text: make([]rune, 0),
CursorPosition: 0,
}
input.Par.Height = 3
return input
}
// Buffer implements interface termui.Bufferer
func (i *InputBKP) Buffer() termui.Buffer {
buf := i.Par.Buffer()
// Set visible cursor
char := buf.At(i.Par.InnerX()+i.CursorPosition, i.Par.Block.InnerY())
buf.Set(
i.Par.InnerX()+i.CursorPosition,
i.Par.Block.InnerY(),
termui.Cell{
Ch: char.Ch,
Fg: i.Par.TextBgColor,
Bg: i.Par.TextFgColor,
},
)
return buf
}
// GetHeight implements interface termui.GridBufferer
func (i *InputBKP) GetHeight() int {
return i.Par.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (i *InputBKP) SetWidth(w int) {
i.Par.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (i *InputBKP) SetX(x int) {
i.Par.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (i *InputBKP) SetY(y int) {
i.Par.SetY(y)
}
// SendMessage send the input text through the SlackService
func (i *InputBKP) SendMessage(svc *service.SlackService, channel string, message string) {
svc.SendMessage(channel, message)
}
// Insert will insert a given key at the place of the current CursorPosition
func (i *InputBKP) Insert(key rune) {
if len(i.Text) < i.Par.InnerBounds().Dx()-1 {
left := make([]rune, len(i.Text[0:i.CursorPosition]))
copy(left, i.Text[0:i.CursorPosition])
left = append(left, key)
i.Text = append(left, i.Text[i.CursorPosition:]...)
i.Par.Text = string(i.Text)
i.MoveCursorRight()
}
}
// Backspace will remove a character in front of the CursorPosition
func (i *InputBKP) Backspace() {
if i.CursorPosition > 0 {
i.Text = append(i.Text[0:i.CursorPosition-1], i.Text[i.CursorPosition:]...)
i.Par.Text = string(i.Text)
i.MoveCursorLeft()
}
}
// Delete will remove a character at the CursorPosition
func (i *InputBKP) Delete() {
if i.CursorPosition < len(i.Text) {
i.Text = append(i.Text[0:i.CursorPosition], i.Text[i.CursorPosition+1:]...)
i.Par.Text = string(i.Text)
}
}
// MoveCursorRight will increase the current CursorPosition with 1
func (i *InputBKP) MoveCursorRight() {
if i.CursorPosition < len(i.Text) {
i.CursorPosition++
}
}
// MoveCursorLeft will decrease the current CursorPosition with 1
func (i *InputBKP) MoveCursorLeft() {
if i.CursorPosition > 0 {
i.CursorPosition--
}
}
// IsEmpty will return true when the input is empty
func (i *InputBKP) IsEmpty() bool {
if i.Par.Text == "" {
return true
}
return false
}
// Clear will empty the input and move the cursor to the start position
func (i *InputBKP) Clear() {
i.Text = make([]rune, 0)
i.Par.Text = ""
i.CursorPosition = 0
}
// GetText returns the text currently in the input
func (i *InputBKP) GetText() string {
return i.Par.Text
}

View File

@ -1,95 +0,0 @@
package components
import (
"fmt"
"sort"
"strings"
"time"
)
var (
COLORS = []string{
"fg-black",
"fg-red",
"fg-green",
"fg-yellow",
"fg-blue",
"fg-magenta",
"fg-cyan",
"fg-white",
}
)
type Message struct {
ID string
Messages map[string]Message
Time time.Time
Thread string
Name string
Content string
StyleTime string
StyleThread string
StyleName string
StyleText string
FormatTime string
}
func (m Message) GetTime() string {
return fmt.Sprintf(
"[[%s]](%s) ",
m.Time.Format(m.FormatTime),
m.StyleTime,
)
}
func (m Message) GetThread() string {
return fmt.Sprintf("[%s](%s)",
m.Thread,
m.StyleThread,
)
}
func (m Message) GetName() string {
return fmt.Sprintf("[<%s>](%s) ",
m.Name,
m.colorizeName(m.StyleName),
)
}
func (m Message) GetContent() string {
return fmt.Sprintf("[.](%s)", m.StyleText)
}
func (m Message) colorizeName(styleName string) string {
if strings.Contains(styleName, "colorize") {
var sum int
for _, c := range m.Name {
sum = sum + int(c)
}
i := sum % len(COLORS)
return strings.Replace(m.StyleName, "colorize", COLORS[i], -1)
}
return styleName
}
func SortMessages(msgs map[string]Message) []Message {
keys := make([]string, 0)
for k := range msgs {
keys = append(keys, k)
}
sort.Strings(keys)
sortedMessages := make([]Message, 0)
for _, k := range keys {
sortedMessages = append(sortedMessages, msgs[k])
}
return sortedMessages
}

View File

@ -1,14 +1,6 @@
package components
import (
"github.com/erroneousboat/termui"
)
const (
CommandMode = "NORMAL"
InsertMode = "INSERT"
SearchMode = "SEARCH"
)
import "github.com/gizak/termui"
// Mode is the definition of Mode component
type Mode struct {
@ -16,13 +8,12 @@ type Mode struct {
}
// CreateMode is the constructor of the Mode struct
func CreateModeComponent() *Mode {
func CreateMode() *Mode {
mode := &Mode{
Par: termui.NewPar(CommandMode),
Par: termui.NewPar("NORMAL"),
}
mode.Par.Height = 3
mode.SetCommandMode()
return mode
}
@ -89,18 +80,3 @@ func (m *Mode) SetX(x int) {
func (m *Mode) SetY(y int) {
m.Par.SetY(y)
}
func (m *Mode) SetInsertMode() {
m.Par.Text = InsertMode
termui.Render(m)
}
func (m *Mode) SetCommandMode() {
m.Par.Text = CommandMode
termui.Render(m)
}
func (m *Mode) SetSearchMode() {
m.Par.Text = SearchMode
termui.Render(m)
}

View File

@ -1,26 +0,0 @@
package components
import (
"github.com/erroneousboat/termui"
)
type Threads struct {
*Channels
}
func CreateThreadsComponent(height int) *Threads {
threads := &Threads{
Channels: &Channels{
List: termui.NewList(),
},
}
threads.List.BorderLabel = "Threads"
threads.List.Height = height
threads.SelectedChannel = 0
threads.Offset = 0
threads.CursorPosition = threads.List.InnerBounds().Min.Y
return threads
}

View File

@ -3,105 +3,28 @@ package config
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
fp "path/filepath"
"github.com/OpenPeeDeeP/xdg"
"github.com/erroneousboat/termui"
)
const (
NotifyAll = "all"
NotifyMention = "mention"
"github.com/gizak/termui"
)
// Config is the definition of a Config struct
type Config struct {
SlackToken string `json:"slack_token"`
Notify string `json:"notify"`
Emoji bool `json:"emoji"`
Theme string `json:"theme"`
SidebarWidth int `json:"sidebar_width"`
MainWidth int `json:"-"`
ThreadsWidth int `json:"threads_width"`
KeyMap map[string]keyMapping `json:"key_map"`
Theme Theme `json:"theme"`
}
type keyMapping map[string]string
// NewConfig loads the config file and returns a Config struct
func NewConfig(filepath string) (*Config, error) {
cfg := getDefaultConfig()
// Open config file, and when none is found or present create
// a default empty one, at the default filepath location
file, err := os.Open(filepath)
if err != nil {
file, err = CreateConfigFile(filepath)
if err != nil {
return &cfg, fmt.Errorf("couldn't open the slack-term config file: (%v)", err)
}
}
if err := json.NewDecoder(file).Decode(&cfg); err != nil {
return &cfg, fmt.Errorf("the slack-term config file isn't valid json: (%v)", err)
}
if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 {
return &cfg, errors.New("please specify the 'sidebar_width' between 1 and 11")
}
cfg.MainWidth = 12 - cfg.SidebarWidth
switch cfg.Notify {
case NotifyAll, NotifyMention, "":
break
default:
return &cfg, fmt.Errorf("unsupported setting for notify: %s", cfg.Notify)
}
termui.ColorMap = map[string]termui.Attribute{
"fg": termui.StringToAttribute(cfg.Theme.View.Fg),
"bg": termui.StringToAttribute(cfg.Theme.View.Bg),
"border.fg": termui.StringToAttribute(cfg.Theme.View.BorderFg),
"border.bg": termui.StringToAttribute(cfg.Theme.View.BorderBg),
"label.fg": termui.StringToAttribute(cfg.Theme.View.LabelFg),
"label.bg": termui.StringToAttribute(cfg.Theme.View.LabelBg),
}
return &cfg, nil
}
func CreateConfigFile(filepath string) (*os.File, error) {
filepath = fmt.Sprintf("%s/slack-term/%s", xdg.ConfigHome(), "config")
if _, err := os.Stat(filepath); os.IsNotExist(err) {
os.MkdirAll(fp.Dir(filepath), os.ModePerm)
}
payload := "{\"slack_token\": \"\"}"
err := ioutil.WriteFile(filepath, []byte(payload), 0755)
if err != nil {
return nil, err
}
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
return file, nil
}
func getDefaultConfig() Config {
return Config{
cfg := Config{
Theme: "dark",
SidebarWidth: 1,
MainWidth: 11,
ThreadsWidth: 1,
Notify: "",
Emoji: false,
KeyMap: map[string]keyMapping{
"command": {
"i": "mode-insert",
@ -110,17 +33,12 @@ func getDefaultConfig() Config {
"j": "channel-down",
"g": "channel-top",
"G": "channel-bottom",
"K": "thread-up",
"J": "thread-down",
"<previous>": "chat-up",
"C-b": "chat-up",
"C-u": "chat-up",
"<next>": "chat-down",
"C-f": "chat-down",
"C-d": "chat-down",
"n": "channel-search-next",
"N": "channel-search-prev",
"'": "channel-jump",
"q": "quit",
"<f1>": "help",
},
@ -145,27 +63,37 @@ func getDefaultConfig() Config {
"<space>": "space",
},
},
Theme: Theme{
View: View{
Fg: "white",
Bg: "default",
BorderFg: "white",
BorderBg: "",
LabelFg: "green,bold",
LabelBg: "",
},
Channel: Channel{
Prefix: "",
Icon: "",
Text: "",
},
Message: Message{
Time: "",
TimeFormat: "15:04",
Thread: "fg-bold",
Name: "",
Text: "",
},
},
}
file, err := os.Open(filepath)
if err != nil {
return &cfg, err
}
if err := json.NewDecoder(file).Decode(&cfg); err != nil {
return &cfg, err
}
if cfg.SlackToken == "" {
return &cfg, errors.New("couldn't find 'slack_token' parameter")
}
if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 {
return &cfg, errors.New("please specify the 'sidebar_width' between 1 and 11")
}
cfg.MainWidth = 12 - cfg.SidebarWidth
if cfg.Theme == "light" {
termui.ColorMap = map[string]termui.Attribute{
"fg": termui.ColorBlack,
"bg": termui.ColorWhite,
"border.fg": termui.ColorBlack,
"label.fg": termui.ColorBlue,
"par.fg": termui.ColorYellow,
"par.label.bg": termui.ColorWhite,
}
}
return &cfg, nil
}

View File

@ -1,30 +0,0 @@
package config
type Theme struct {
View View `json:"view"`
Channel Channel `json:"channel"`
Message Message `json:"message"`
}
type View struct {
Fg string `json:"fg"` // Foreground text
Bg string `json:"bg"` // Background text
BorderFg string `json:"border_fg"` // Border foreground
BorderBg string `json:"border_bg"` // Border background
LabelFg string `json:"label_fg"` // Label text foreground
LabelBg string `json:"label_bg"` // Label text background
}
type Message struct {
Time string `json:"time"`
Name string `json:"name"`
Thread string `json:"thread"`
Text string `json:"text"`
TimeFormat string `json:"time_format"`
}
type Channel struct {
Prefix string `json:"prefix"`
Icon string `json:"icon"`
Text string `json:"text"`
}

View File

@ -1,13 +1,7 @@
package context
import (
"errors"
"net/http"
_ "net/http/pprof"
"os"
"github.com/0xAX/notificator"
"github.com/erroneousboat/termui"
"github.com/gizak/termui"
termbox "github.com/nsf/termbox-go"
"github.com/erroneousboat/slack-term/config"
@ -19,142 +13,40 @@ const (
CommandMode = "command"
InsertMode = "insert"
SearchMode = "search"
ChatFocus = iota
ThreadFocus
)
type AppContext struct {
Version string
Usage string
EventQueue chan termbox.Event
Service *service.SlackService
Body *termui.Grid
View *views.View
Config *config.Config
Debug bool
Mode string
Focus int
Notify *notificator.Notificator
}
// CreateAppContext creates an application context which can be passed
// and referenced througout the application
func CreateAppContext(flgConfig string, flgToken string, flgDebug bool, version string, usage string) (*AppContext, error) {
if flgDebug {
go func() {
http.ListenAndServe(":6060", nil)
}()
}
// Loading screen
views.Loading()
func CreateAppContext(flgConfig string) (*AppContext, error) {
// Load config
config, err := config.NewConfig(flgConfig)
if err != nil {
return nil, err
}
// When slack token isn't set in the config file, we'll check
// the command-line flag or the environment variable
if config.SlackToken == "" {
if flgToken != "" {
config.SlackToken = flgToken
} else {
config.SlackToken = os.Getenv("SLACK_TOKEN")
}
}
// Create desktop notifier
var notify *notificator.Notificator
if config.Notify != "" {
notify = notificator.New(notificator.Options{AppName: "slack-term"})
if notify == nil {
return nil, errors.New(
"desktop notifications are not supported for your OS",
)
}
}
// Create Service
svc, err := service.NewSlackService(config)
svc, err := service.NewSlackService(config.SlackToken)
if err != nil {
return nil, err
}
// Create the main view
view, err := views.CreateView(config, svc)
if err != nil {
return nil, err
}
columns := []*termui.Row{
termui.NewCol(config.SidebarWidth, 0, view.Channels),
}
threads := false
if len(view.Threads.ChannelItems) > 0 {
threads = true
}
// Setup the interface
if threads && flgDebug {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(config.MainWidth-config.ThreadsWidth-3, 0, view.Chat),
termui.NewCol(config.ThreadsWidth, 0, view.Threads),
termui.NewCol(3, 0, view.Debug),
}...,
)
} else if threads {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(config.MainWidth-config.ThreadsWidth, 0, view.Chat),
termui.NewCol(config.ThreadsWidth, 0, view.Threads),
}...,
)
} else if flgDebug {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(config.MainWidth-5, 0, view.Chat),
termui.NewCol(config.MainWidth-6, 0, view.Debug),
}...,
)
} else {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(config.MainWidth, 0, view.Chat),
}...,
)
}
termui.Body.AddRows(
termui.NewRow(columns...),
termui.NewRow(
termui.NewCol(config.SidebarWidth, 0, view.Mode),
termui.NewCol(config.MainWidth, 0, view.Input),
),
)
termui.Body.Align()
termui.Render(termui.Body)
// Create ChatView
view := views.CreateChatView(svc)
return &AppContext{
Version: version,
Usage: usage,
EventQueue: make(chan termbox.Event, 20),
Service: svc,
Body: termui.Body,
View: view,
Config: config,
Debug: flgDebug,
Mode: CommandMode,
Focus: ChatFocus,
Notify: notify,
}, nil
}

21
go.mod
View File

@ -1,21 +0,0 @@
module github.com/erroneousboat/slack-term
go 1.12
require (
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b
github.com/OpenPeeDeeP/xdg v0.2.0
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04
github.com/gorilla/websocket v1.4.2 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lithammer/fuzzysearch v1.1.0
github.com/maruel/panicparse v1.1.1 // indirect
github.com/mattn/go-runewidth v0.0.7
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7
github.com/pkg/errors v0.9.1 // indirect
github.com/slack-go/slack v0.6.3
github.com/stretchr/testify v1.4.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.4 // indirect
)

53
go.sum
View File

@ -1,53 +0,0 @@
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b h1:Sn+u6zpXFyfm2X7ruh+z6SJiUVyFg8YElh6HIOhrRCA=
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/OpenPeeDeeP/xdg v0.2.0 h1:xr89rnllbkRkM7SV9Y++FJ8TGkbdkhhBQm5kOkGT7AE=
github.com/OpenPeeDeeP/xdg v0.2.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04 h1:DaFwoQC0Neeb2y2CVFxDPrS1BeyWAkKc4VVBDTZ0N98=
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04/go.mod h1:UPpsbgDrqmUayOFOkCGD7+xrBIml/1dA0dsqTRnZqac=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A=
github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ=
github.com/maruel/panicparse v1.1.1 h1:k62YPcEoLncEEpjMt92GtG5ugb8WL/510Ys3/h5IkRc=
github.com/maruel/panicparse v1.1.1/go.mod h1:nty42YY5QByNC5MM7q/nj938VbgPU7avs45z6NClpxI=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA=
github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk=
github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7 h1:OkWEy7aQeQTbgdrcGi9bifx+Y6bMM7ae7y42hDFaBvA=
github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/slack-go/slack v0.6.3 h1:qU037g8gQ71EuH6S9zYKnvYrEUj0fLFH4HFekFqBoRU=
github.com/slack-go/slack v0.6.3/go.mod h1:HE4RwNe7YpOg/F0vqo5PwXH3Hki31TplTvKRW9dGGaw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,71 +1,47 @@
package handlers
import (
"fmt"
"log"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/0xAX/notificator"
"github.com/erroneousboat/termui"
termbox "github.com/nsf/termbox-go"
"github.com/slack-go/slack"
"github.com/erroneousboat/slack-term/components"
"github.com/erroneousboat/slack-term/config"
"github.com/erroneousboat/slack-term/context"
"github.com/erroneousboat/slack-term/views"
)
var scrollTimer *time.Timer
var notifyTimer *time.Timer
var timer *time.Timer
// actionMap binds specific action names to the function counterparts,
// these action names can then be used to bind them to specific keys
// in the Config.
var actionMap = map[string]func(*context.AppContext){
"space": actionSpace,
"backspace": actionBackSpace,
"delete": actionDelete,
"cursor-right": actionMoveCursorRight,
"cursor-left": actionMoveCursorLeft,
"send": actionSend,
"quit": actionQuit,
"mode-insert": actionInsertMode,
"mode-command": actionCommandMode,
"mode-search": actionSearchMode,
"clear-input": actionClearInput,
"channel-up": actionMoveCursorUpChannels,
"channel-down": actionMoveCursorDownChannels,
"channel-top": actionMoveCursorTopChannels,
"channel-bottom": actionMoveCursorBottomChannels,
"channel-search-next": actionSearchNextChannels,
"channel-search-prev": actionSearchPrevChannels,
"channel-jump": actionJumpChannels,
"thread-up": actionMoveCursorUpThreads,
"thread-down": actionMoveCursorDownThreads,
"chat-up": actionScrollUpChat,
"chat-down": actionScrollDownChat,
"help": actionHelp,
// "space": actionSpace,
// "backspace": actionBackSpace,
// "delete": actionDelete,
// "cursor-right": actionMoveCursorRight,
// "cursor-left": actionMoveCursorLeft,
// "send": actionSend,
"quit": actionQuit,
// "mode-insert": actionInsertMode,
// "mode-command": actionCommandMode,
// "mode-search": actionSearchMode,
// "clear-input": actionClearInput,
"channel-up": actionMoveCursorUpChannels,
"channel-down": actionMoveCursorDownChannels,
// "channel-top": actionMoveCursorTopChannels,
// "channel-bottom": actionMoveCursorBottomChannels,
// "chat-up": actionScrollUpChat,
// "chat-down": actionScrollDownChat,
// "help": actionHelp,
}
// Initialize will start a combination of event handlers and 'background tasks'
func Initialize(ctx *context.AppContext) {
// Keyboard events
func RegisterEventHandlers(ctx *context.AppContext) {
eventHandler(ctx)
// RTM incoming events
messageHandler(ctx)
// User presence
go actionSetPresenceAll(ctx)
// incomingMessageHandler(ctx)
}
// eventHandler will handle events created by the user
// TODO: add incomingMessageHandler to the select statement
func eventHandler(ctx *context.AppContext) {
go func() {
for {
@ -73,116 +49,63 @@ func eventHandler(ctx *context.AppContext) {
}
}()
go func() {
for {
ev := <-ctx.EventQueue
handleTermboxEvents(ctx, ev)
handleMoreTermboxEvents(ctx, ev)
// Place your debugging statements here
if ctx.Debug {
ctx.View.Debug.Println(
"event received",
)
}
}
}()
}
func handleTermboxEvents(ctx *context.AppContext, ev termbox.Event) bool {
switch ev.Type {
case termbox.EventKey:
actionKeyEvent(ctx, ev)
case termbox.EventResize:
actionResizeEvent(ctx, ev)
}
return true
}
func handleMoreTermboxEvents(ctx *context.AppContext, ev termbox.Event) bool {
for {
select {
case ev := <-ctx.EventQueue:
ok := handleTermboxEvents(ctx, ev)
if !ok {
return false
switch ev.Type {
case termbox.EventKey:
actionKeyEvent(ctx, ev)
// case termbox.EventResize:
// actionResizeEvent(ctx, ev)
}
default:
return true
}
ctx.View.GUI.Flush()
}
}
// messageHandler will handle events created by the service
func messageHandler(ctx *context.AppContext) {
go func() {
for {
select {
case rtmEvent := <-ctx.Service.RTM.IncomingEvents:
switch ev := rtmEvent.Data.(type) {
case *slack.MessageEvent:
// Construct message
msg, err := ctx.Service.CreateMessageFromMessageEvent(ev, ev.Channel)
if err != nil {
continue
}
// Add message to the selected channel
if ev.Channel == ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID {
// Get the thread timestamp of the event, we need to
// check the previous message as well, because edited
// message don't have the thread timestamp
var threadTimestamp string
if ev.ThreadTimestamp != "" {
threadTimestamp = ev.ThreadTimestamp
} else if ev.PreviousMessage != nil && ev.PreviousMessage.ThreadTimestamp != "" {
threadTimestamp = ev.PreviousMessage.ThreadTimestamp
} else {
threadTimestamp = ""
}
// When timestamp isn't set this is a thread reply,
// handle as such
if threadTimestamp != "" {
ctx.View.Chat.AddReply(threadTimestamp, msg)
} else if threadTimestamp == "" && ctx.Focus == context.ChatFocus {
ctx.View.Chat.AddMessage(msg)
}
// we (mis)use actionChangeChannel, to rerender, the
// view when a new thread has been started
if ctx.View.Chat.IsNewThread(threadTimestamp) {
actionChangeChannel(ctx)
} else {
termui.Render(ctx.View.Chat)
}
// TODO: set Chat.Offset to 0, to automatically scroll
// down?
}
// Set new message indicator for channel, I'm leaving
// this here because I also want to be notified when
// I'm currently in a channel but not in the terminal
// window (tmux). But only create a notification when
// it comes from someone else but the current user.
if ev.User != ctx.Service.CurrentUserID {
actionNewMessage(ctx, ev)
}
case *slack.PresenceChangeEvent:
actionSetPresence(ctx, ev.User, ev.Presence)
case *slack.RTMError:
ctx.View.Debug.Println(
ev.Error(),
)
}
}
}
}()
}
// func incomingMessageHandler(ctx *context.AppContext) {
// go func() {
// for {
// select {
// case msg := <-ctx.Service.RTM.IncomingEvents:
// switch ev := msg.Data.(type) {
// case *slack.MessageEvent:
// // Construct message
// msg := ctx.Service.CreateMessageFromMessageEvent(ev)
//
// // Add message to the selected channel
// if ev.Channel == ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID {
//
// // reverse order of messages, mainly done
// // when attachments are added to message
// for i := len(msg) - 1; i >= 0; i-- {
// ctx.View.Chat.AddMessage(msg[i])
// }
//
// termui.Render(ctx.View.Chat)
//
// // TODO: set Chat.Offset to 0, to automatically scroll
// // down?
// }
//
// // Set new message indicator for channel, I'm leaving
// // this here because I also want to be notified when
// // I'm currently in a channel but not in the terminal
// // window (tmux). But only create a notification when
// // it comes from someone else but the current user.
// if ev.User != ctx.Service.CurrentUserID {
// actionNewMessage(ctx, ev.Channel)
// }
// case *slack.PresenceChangeEvent:
// actionSetPresence(ctx, ev.User, ev.Presence)
// }
// }
// }
// }()
// }
func actionKeyEvent(ctx *context.AppContext, ev termbox.Event) {
@ -199,513 +122,233 @@ func actionKeyEvent(ctx *context.AppContext, ev termbox.Event) {
action(ctx)
}
} else {
if ctx.Mode == context.InsertMode && ev.Ch != 0 {
actionInput(ctx.View, ev.Ch)
} else if ctx.Mode == context.SearchMode && ev.Ch != 0 {
actionSearch(ctx, ev.Ch)
}
// if ctx.Mode == context.InsertMode && ev.Ch != 0 {
// actionInput(ctx.View, ev.Ch)
// } else if ctx.Mode == context.SearchMode && ev.Ch != 0 {
// actionSearch(ctx, ev.Ch)
// }
}
}
func actionResizeEvent(ctx *context.AppContext, ev termbox.Event) {
// When terminal window is too small termui will panic, here
// we won't resize when the terminal window is too small.
if termui.TermWidth() < 25 || termui.TermHeight() < 5 {
return
}
// func actionResizeEvent(ctx *context.AppContext, ev termbox.Event) {
// termui.Body.Width = termui.TermWidth()
// termui.Body.Align()
// termui.Render(termui.Body)
// }
termui.Body.Width = termui.TermWidth()
// func actionInput(view *views.View, key rune) {
// view.Input.Insert(key)
// termui.Render(view.Input)
// }
// Vertical resize components
ctx.View.Channels.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
ctx.View.Chat.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
ctx.View.Debug.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
// func actionClearInput(ctx *context.AppContext) {
// // Clear input
// ctx.View.Input.Clear()
// ctx.View.Refresh()
//
// // Set command mode
// actionCommandMode(ctx)
// }
termui.Body.Align()
termui.Render(termui.Body)
}
// func actionSpace(ctx *context.AppContext) {
// actionInput(ctx.View, ' ')
// }
func actionRedrawGrid(ctx *context.AppContext, threads bool, debug bool) {
termui.Clear()
termui.Body = termui.NewGrid()
termui.Body.X = 0
termui.Body.Y = 0
termui.Body.BgColor = termui.ThemeAttr("bg")
termui.Body.Width = termui.TermWidth()
// func actionBackSpace(ctx *context.AppContext) {
// ctx.View.Input.Backspace()
// termui.Render(ctx.View.Input)
// }
columns := []*termui.Row{
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Channels),
}
// func actionDelete(ctx *context.AppContext) {
// ctx.View.Input.Delete()
// termui.Render(ctx.View.Input)
// }
if threads && debug {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth-3, 0, ctx.View.Chat),
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
termui.NewCol(3, 0, ctx.View.Debug),
}...,
)
} else if threads {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth, 0, ctx.View.Chat),
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
}...,
)
} else if debug {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth-5, 0, ctx.View.Chat),
termui.NewCol(ctx.Config.MainWidth-6, 0, ctx.View.Debug),
}...,
)
} else {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Chat),
}...,
)
}
// func actionMoveCursorRight(ctx *context.AppContext) {
// ctx.View.Input.MoveCursorRight()
// termui.Render(ctx.View.Input)
// }
termui.Body.AddRows(
termui.NewRow(columns...),
termui.NewRow(
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Mode),
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Input),
),
)
// func actionMoveCursorLeft(ctx *context.AppContext) {
// ctx.View.Input.MoveCursorLeft()
// termui.Render(ctx.View.Input)
// }
termui.Body.Align()
termui.Render(termui.Body)
}
// func actionSend(ctx *context.AppContext) {
// if !ctx.View.Input.IsEmpty() {
//
// // Clear message before sending, to combat
// // quick succession of actionSend
// message := ctx.View.Input.GetText()
// ctx.View.Input.Clear()
// ctx.View.Refresh()
//
// ctx.View.Input.SendMessage(
// ctx.Service,
// ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID,
// message,
// )
// }
// }
func actionInput(view *views.View, key rune) {
view.Input.Insert(key)
termui.Render(view.Input)
}
// func actionSearch(ctx *context.AppContext, key rune) {
// go func() {
// if timer != nil {
// timer.Stop()
// }
//
// actionInput(ctx.View, key)
//
// timer = time.NewTimer(time.Second / 4)
// <-timer.C
//
// term := ctx.View.Input.GetText()
// ctx.View.Channels.Search(term)
// actionChangeChannel(ctx)
// }()
// }
func actionClearInput(ctx *context.AppContext) {
// Clear input
ctx.View.Input.Clear()
ctx.View.Refresh()
// Set command mode
actionCommandMode(ctx)
}
func actionSpace(ctx *context.AppContext) {
actionInput(ctx.View, ' ')
}
func actionBackSpace(ctx *context.AppContext) {
ctx.View.Input.Backspace()
termui.Render(ctx.View.Input)
}
func actionDelete(ctx *context.AppContext) {
ctx.View.Input.Delete()
termui.Render(ctx.View.Input)
}
func actionMoveCursorRight(ctx *context.AppContext) {
ctx.View.Input.MoveCursorRight()
termui.Render(ctx.View.Input)
}
func actionMoveCursorLeft(ctx *context.AppContext) {
ctx.View.Input.MoveCursorLeft()
termui.Render(ctx.View.Input)
}
func actionSend(ctx *context.AppContext) {
if !ctx.View.Input.IsEmpty() {
// Clear message before sending, to combat
// quick succession of actionSend
message := ctx.View.Input.GetText()
ctx.View.Input.Clear()
termui.Render(ctx.View.Input)
// Send slash command
isCmd, err := ctx.Service.SendCommand(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message,
)
if err != nil {
ctx.View.Debug.Println(
err.Error(),
)
}
// Send message
if !isCmd {
if ctx.Focus == context.ChatFocus {
err := ctx.Service.SendMessage(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message,
)
if err != nil {
ctx.View.Debug.Println(
err.Error(),
)
}
}
if ctx.Focus == context.ThreadFocus {
err := ctx.Service.SendReply(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
message,
)
if err != nil {
ctx.View.Debug.Println(
err.Error(),
)
}
}
}
// Clear notification icon if there is any
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
if channelItem.Notification {
ctx.Service.MarkAsRead(channelItem)
ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
}
termui.Render(ctx.View.Channels)
}
}
// actionSearch will search through the channels based on the users
// input. A time is implemented to make sure the actual searching
// and changing of channels is done when the user's typing is paused.
func actionSearch(ctx *context.AppContext, key rune) {
actionInput(ctx.View, key)
go func() {
if scrollTimer != nil {
scrollTimer.Stop()
}
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually search when the time expires
term := ctx.View.Input.GetText()
ctx.View.Channels.Search(term)
actionChangeChannel(ctx)
}()
}
// actionQuit will exit the program by using os.Exit, this is
// done because we are using a custom termui EvtStream. Which
// we won't be able to call termui.StopLoop() on. See main.go
// for the customEvtStream and why this is done.
// actionQuit will exit the program
func actionQuit(ctx *context.AppContext) {
termbox.Close()
os.Exit(0)
}
func actionInsertMode(ctx *context.AppContext) {
ctx.Mode = context.InsertMode
ctx.View.Mode.SetInsertMode()
}
// func actionInsertMode(ctx *context.AppContext) {
// ctx.Mode = context.InsertMode
// ctx.View.Mode.Par.Text = "INSERT"
// termui.Render(ctx.View.Mode)
// }
func actionCommandMode(ctx *context.AppContext) {
ctx.Mode = context.CommandMode
ctx.View.Mode.SetCommandMode()
}
// func actionCommandMode(ctx *context.AppContext) {
// ctx.Mode = context.CommandMode
// ctx.View.Mode.Par.Text = "NORMAL"
// termui.Render(ctx.View.Mode)
// }
func actionSearchMode(ctx *context.AppContext) {
ctx.Mode = context.SearchMode
ctx.View.Mode.SetSearchMode()
}
// func actionSearchMode(ctx *context.AppContext) {
// ctx.Mode = context.SearchMode
// ctx.View.Mode.Par.Text = "SEARCH"
// termui.Render(ctx.View.Mode)
// }
func actionGetMessages(ctx *context.AppContext) {
msgs, _, err := ctx.Service.GetMessages(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
ctx.View.Chat.SetMessages(msgs)
termui.Render(ctx.View.Chat)
}
// func actionGetMessages(ctx *context.AppContext) {
// ctx.View.Chat.GetMessages(
// ctx.Service,
// ctx.Service.Channels[ctx.View.Channels.SelectedChannel],
// )
//
// termui.Render(ctx.View.Chat)
// }
// actionMoveCursorUpChannels will execute the actionChangeChannel
// function. A timer is implemented to support fast scrolling through
// the list without executing the actionChangeChannel event
// the list without executing the actionChangeChannel event.
func actionMoveCursorUpChannels(ctx *context.AppContext) {
go func() {
if scrollTimer != nil {
scrollTimer.Stop()
if timer != nil {
timer.Stop()
}
ctx.View.Channels.MoveCursorUp()
termui.Render(ctx.View.Channels)
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
timer = time.NewTimer(time.Second / 4)
<-timer.C
// Only actually change channel when the timer expires
// Only actually change channel when timer expired
actionChangeChannel(ctx)
// Flush, because this is run in a goroutine
ctx.View.GUI.Flush()
}()
}
// actionMoveCursorDownChannels will execute the actionChangeChannel
// function. A timer is implemented to support fast scrolling through
// the list without executing the actionChangeChannel event
// the list without executing the actionChangeChannel event.
func actionMoveCursorDownChannels(ctx *context.AppContext) {
go func() {
if scrollTimer != nil {
scrollTimer.Stop()
if timer != nil {
timer.Stop()
}
ctx.View.Channels.MoveCursorDown()
termui.Render(ctx.View.Channels)
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
timer = time.NewTimer(time.Second / 4)
<-timer.C
// Only actually change channel when the timer expires
// Only actually change channel when timer expired
actionChangeChannel(ctx)
// Flush, because this is run in a goroutine
ctx.View.GUI.Flush()
}()
}
func actionMoveCursorTopChannels(ctx *context.AppContext) {
ctx.View.Channels.MoveCursorTop()
actionChangeChannel(ctx)
}
// func actionMoveCursorTopChannels(ctx *context.AppContext) {
// ctx.View.Channels.MoveCursorTop()
// actionChangeChannel(ctx)
// }
func actionMoveCursorBottomChannels(ctx *context.AppContext) {
ctx.View.Channels.MoveCursorBottom()
actionChangeChannel(ctx)
}
func actionSearchNextChannels(ctx *context.AppContext) {
ctx.View.Channels.SearchNext()
actionChangeChannel(ctx)
}
func actionSearchPrevChannels(ctx *context.AppContext) {
ctx.View.Channels.SearchPrev()
actionChangeChannel(ctx)
}
func actionJumpChannels(ctx *context.AppContext) {
ctx.View.Channels.Jump()
actionChangeChannel(ctx)
}
// func actionMoveCursorBottomChannels(ctx *context.AppContext) {
// ctx.View.Channels.MoveCursorBottom()
// actionChangeChannel(ctx)
// }
func actionChangeChannel(ctx *context.AppContext) {
// Clear messages from Chat pane
ctx.View.Chat.ClearMessages()
// Get messages of the SelectedChannel, and get the count of messages
// that fit into the Chat component
msgs, threads, err := ctx.Service.GetMessages(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
// TODO: Get the count of message that fit in the pane
// Get message for the new selected channel
messages := ctx.Service.GetMessages(
ctx.Service.GetSlackChannel(ctx.View.Channels.SelectedChannel),
10,
)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
// Set messages for the channel
ctx.View.Chat.SetMessages(msgs)
// Set the threads identifiers in the threads pane
var haveThreads bool
if len(threads) > 0 {
haveThreads = true
// Make the first thread the current Channel
ctx.View.Threads.SetChannels(
append(
[]components.ChannelItem{ctx.View.Channels.GetSelectedChannel()},
threads...,
),
)
// Reset position of SelectedChannel
ctx.View.Threads.MoveCursorTop()
}
// Set messages for the new channel
ctx.View.Chat.SetMessages(messages)
// TODO
// Set channel name for the Chat pane
ctx.View.Chat.SetBorderLabel(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].GetChannelName(),
)
// ctx.View.Chat.SetBorderLabel(
// ctx.Service.Channels[ctx.View.Channels.SelectedChannel],
// )
// Clear notification icon if there is any
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
if channelItem.Notification {
ctx.Service.MarkAsRead(channelItem)
ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
}
// TODO
// Set read mark
// ctx.View.Channels.SetReadMark(ctx.Service)
// Redraw grid, necessary when threads and/or debug is set. We will redraw
// the grid when there are threads, or we just came from a thread and went
// to a channel without threads. Hence the clearing of ChannelItems of
// Threads.
if haveThreads {
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
} else if !haveThreads && len(ctx.View.Threads.ChannelItems) > 0 {
ctx.View.Threads.SetChannels([]components.ChannelItem{})
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
} else {
termui.Render(ctx.View.Threads)
termui.Render(ctx.View.Channels)
termui.Render(ctx.View.Chat)
}
// Set focus, necessary to know when replying to thread or chat
ctx.Focus = context.ChatFocus
// FIXME: maybe not necessary
// Refresh Chat component
ctx.View.Chat.Refresh()
}
func actionChangeThread(ctx *context.AppContext) {
// Clear messages from Chat pane
ctx.View.Chat.ClearMessages()
// func actionNewMessage(ctx *context.AppContext, channelID string) {
// ctx.View.Channels.SetNotification(ctx.Service, channelID)
// termui.Render(ctx.View.Channels)
// }
// The first channel in the Thread list is current Channel. Set context
// Focus and messages accordingly.
var err error
msgs := []components.Message{}
if ctx.View.Threads.SelectedChannel == 0 {
ctx.Focus = context.ChatFocus
// func actionSetPresence(ctx *context.AppContext, channelID string, presence string) {
// ctx.View.Channels.SetPresence(ctx.Service, channelID, presence)
// termui.Render(ctx.View.Channels)
// }
msgs, _, err = ctx.Service.GetMessages(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
} else {
ctx.Focus = context.ThreadFocus
// func actionScrollUpChat(ctx *context.AppContext) {
// ctx.View.Chat.ScrollUp()
// termui.Render(ctx.View.Chat)
// }
msgs, err = ctx.Service.GetMessageByID(
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
}
// func actionScrollDownChat(ctx *context.AppContext) {
// ctx.View.Chat.ScrollDown()
// termui.Render(ctx.View.Chat)
// }
// Set messages for the channel
ctx.View.Chat.SetMessages(msgs)
termui.Render(ctx.View.Channels)
termui.Render(ctx.View.Threads)
termui.Render(ctx.View.Chat)
}
func actionMoveCursorUpThreads(ctx *context.AppContext) {
go func() {
if scrollTimer != nil {
scrollTimer.Stop()
}
ctx.View.Threads.MoveCursorUp()
termui.Render(ctx.View.Threads)
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually change channel when the timer expires
actionChangeThread(ctx)
}()
}
func actionMoveCursorDownThreads(ctx *context.AppContext) {
go func() {
if scrollTimer != nil {
scrollTimer.Stop()
}
ctx.View.Threads.MoveCursorDown()
termui.Render(ctx.View.Threads)
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually change thread when the timer expires
actionChangeThread(ctx)
}()
}
// actionNewMessage will set the new message indicator for a channel, and
// if configured will also display a desktop notification
func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) {
ctx.View.Channels.MarkAsUnread(ev.Channel)
termui.Render(ctx.View.Channels)
// Terminal bell
fmt.Print("\a")
// Desktop notification
if ctx.Config.Notify == config.NotifyMention {
if isMention(ctx, ev) {
createNotifyMessage(ctx, ev)
}
} else if ctx.Config.Notify == config.NotifyAll {
createNotifyMessage(ctx, ev)
}
}
func actionSetPresence(ctx *context.AppContext, channelID string, presence string) {
ctx.View.Channels.SetPresence(channelID, presence)
termui.Render(ctx.View.Channels)
}
// actionPresenceAll will set the presence of the user list. Because the
// requests to the endpoint are rate limited we implement a timeout here.
func actionSetPresenceAll(ctx *context.AppContext) {
for _, chn := range ctx.Service.Conversations {
if chn.IsIM {
presence, err := ctx.Service.GetUserPresence(chn.User)
if err != nil {
presence = "away"
}
ctx.View.Channels.SetPresence(chn.ID, presence)
termui.Render(ctx.View.Channels)
time.Sleep(1200 * time.Millisecond)
}
}
}
func actionScrollUpChat(ctx *context.AppContext) {
ctx.View.Chat.ScrollUp()
termui.Render(ctx.View.Chat)
}
func actionScrollDownChat(ctx *context.AppContext) {
ctx.View.Chat.ScrollDown()
termui.Render(ctx.View.Chat)
}
func actionHelp(ctx *context.AppContext) {
ctx.View.Chat.ClearMessages()
ctx.View.Chat.Help(ctx.Usage, ctx.Config)
termui.Render(ctx.View.Chat)
}
// func actionHelp(ctx *context.AppContext) {
// ctx.View.Chat.Help(ctx.Config)
// termui.Render(ctx.View.Chat)
// }
// GetKeyString will return a string that resembles the key event from
// termbox. This is blatanly copied from termui because it is an unexported
@ -756,53 +399,3 @@ func getKeyString(e termbox.Event) string {
ek = pre + mod + k
return ek
}
// isMention check if the message event either contains a
// mention or is posted on an IM channel.
func isMention(ctx *context.AppContext, ev *slack.MessageEvent) bool {
channel := ctx.View.Channels.ChannelItems[ctx.View.Channels.FindChannel(ev.Channel)]
if channel.Type == components.ChannelTypeIM {
return true
}
// Mentions have the following format:
// <@U12345|erroneousboat>
// <@U12345>
r := regexp.MustCompile(`\<@(\w+\|*\w+)\>`)
matches := r.FindAllString(ev.Text, -1)
for _, match := range matches {
if strings.Contains(match, ctx.Service.CurrentUserID) {
return true
}
}
return false
}
func createNotifyMessage(ctx *context.AppContext, ev *slack.MessageEvent) {
go func() {
if notifyTimer != nil {
notifyTimer.Stop()
}
// Only actually notify when time expires
notifyTimer = time.NewTimer(time.Second * 2)
<-notifyTimer.C
var message string
channel := ctx.View.Channels.ChannelItems[ctx.View.Channels.FindChannel(ev.Channel)]
switch channel.Type {
case components.ChannelTypeChannel:
message = fmt.Sprintf("Message received on channel: %s", channel.Name)
case components.ChannelTypeGroup:
message = fmt.Sprintf("Message received in group: %s", channel.Name)
case components.ChannelTypeIM:
message = fmt.Sprintf("Message received from: %s", channel.Name)
default:
message = fmt.Sprintf("Message received from: %s", channel.Name)
}
ctx.Notify.Push("slack-term", message, "", notificator.UR_NORMAL)
}()
}

81
main.go
View File

@ -4,18 +4,17 @@ import (
"flag"
"fmt"
"log"
"os"
"github.com/OpenPeeDeeP/xdg"
"github.com/erroneousboat/termui"
termbox "github.com/nsf/termbox-go"
"net/http"
_ "net/http/pprof"
"os/user"
"path"
"github.com/erroneousboat/slack-term/context"
"github.com/erroneousboat/slack-term/handlers"
)
const (
VERSION = "master"
VERSION = "v2.0.0"
USAGE = `NAME:
slack-term - slack client for your terminal
@ -25,51 +24,31 @@ USAGE:
VERSION:
%s
WEBSITE:
https://github.com/erroneousboat/slack-term
GLOBAL OPTIONS:
-config [path-to-config-file]
-token [slack-token]
-debug
-help, -h
--help, -h
`
)
var (
flgConfig string
flgToken string
flgDebug bool
flgUsage bool
)
func init() {
// Find the default config file
configFile := xdg.New("slack-term", "").QueryConfig("config")
// Get home dir for config file default
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
// Parse flags
flag.StringVar(
&flgConfig,
"config",
configFile,
path.Join(usr.HomeDir, "slack-term.json"),
"location of config file",
)
flag.StringVar(
&flgToken,
"token",
"",
"the slack token",
)
flag.BoolVar(
&flgDebug,
"debug",
false,
"turn on debugging",
)
flag.Usage = func() {
fmt.Printf(USAGE, VERSION)
}
@ -78,35 +57,17 @@ func init() {
}
func main() {
// Start terminal user interface
err := termui.Init()
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Create context
appCTX, err := context.CreateAppContext(flgConfig)
if err != nil {
log.Fatal(err)
}
defer termui.Close()
defer appCTX.View.GUI.Close()
// Create custom event stream for termui because
// termui's one has data race conditions with its
// event handling. We're circumventing it here until
// it has been fixed.
customEvtStream := &termui.EvtStream{
Handlers: make(map[string]func(termui.Event)),
}
termui.DefaultEvtStream = customEvtStream
// Create context
usage := fmt.Sprintf(USAGE, VERSION)
ctx, err := context.CreateAppContext(
flgConfig, flgToken, flgDebug, VERSION, usage,
)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
// Initialize handlers
handlers.Initialize(ctx)
termui.Loop()
// Register handlers
handlers.RegisterEventHandlers(appCTX)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
# 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

View File

@ -1,27 +0,0 @@
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.

View File

@ -1,49 +0,0 @@
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)

View File

@ -1,166 +0,0 @@
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
}
}

View File

@ -1,3 +0,0 @@
*.test
*.out
.DS_STORE

View File

@ -1,12 +0,0 @@
language: go
go:
- 1.11.x
os:
- linux
- osx
before_install:
- go get -t -v ./...
script:
- go test -v -race -covermode=atomic -coverprofile=coverage.txt
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2017, OpenPeeDeeP
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 copyright holder 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.

View File

@ -1,25 +0,0 @@
# XDG [![Build status](https://ci.appveyor.com/api/projects/status/9eoupq9jgsu2p0jv?svg=true)](https://ci.appveyor.com/project/dixonwille/xdg) [![Build Status](https://travis-ci.org/OpenPeeDeeP/xdg.svg?branch=master)](https://travis-ci.org/OpenPeeDeeP/xdg) [![Go Report Card](https://goreportcard.com/badge/github.com/OpenPeeDeeP/xdg)](https://goreportcard.com/report/github.com/OpenPeeDeeP/xdg) [![GoDoc](https://godoc.org/github.com/OpenPeeDeeP/xdg?status.svg)](https://godoc.org/github.com/OpenPeeDeeP/xdg) [![codecov](https://codecov.io/gh/OpenPeeDeeP/xdg/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenPeeDeeP/xdg)
A cross platform package that tries to follow [XDG Standard](https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html) when possible. Since XDG is linux specific, I am only able to follow standards to the T on linux. But for the other operating systems I am finding similar best practice locations for the files.
## Locations Per OS
The following table shows what is used if the envrionment variable is not set. If the variable is set then this package uses that. Linux follows the default standards. Mac does when it comes to the home directory but for system wide it uses the standard `/Library/Application Support`. As for Windows, the variable defaults are just other environment variables set up by the operation system.
> When creating `XDG` application the `Vendor` and `Application` names are appeneded to the end of the path to keep projects unique.
| | Linux | Mac | Windows |
| ---: | :---: | :---: | :---: |
| `XDG_DATA_DIRS` | [`/usr/local/share`, `/usr/share`] | [`/Library/Application Support`] | `%PROGRAMDATA%` |
| `XDG_DATA_HOME` | `~/.local/share` | `~/Library/Application Support` | `%APPDATA%` |
| `XDG_CONFIG_DIRS` | [`/etc/xdg`] | [`/Library/Application Support`] | `%PROGRAMDATA%` |
| `XDG_CONFIG_HOME` | `~/.config` | `~/Library/Application Support` | `%APPDATA%` |
| `XDG_CACHE_HOME` | `~/.cache` | `~/Library/Cache` | `%LOCALAPPDATA%` |
## Notes
- This package does not merge files if they exist across different directories.
- The `Query` methods search through the system variables, `DIRS`, first (when using environment variables first in the variable has presidence). It then checks home variables, `HOME`.
- This package will not create any directories for you. In the standard, it states the following:
> If, when attempting to write a file, the destination directory is non-existant an attempt should be made to create it with permission `0700`. If the destination directory exists already the permissions should not be changed. The application should be prepared to handle the case where the file could not be written, either because the directory was non-existant and could not be created, or for any other reason. In such case it may chose to present an error message to the user.

View File

@ -1,16 +0,0 @@
version: 0.0.1_{build}
build: off
platform: x64
clone_folder: c:\gopath\src\github.com\OpenPeeDeeP\xdg
environment:
GOPATH: c:\gopath
stack: go 1.11
install:
- go get -t -v ./...
- cinst codecov
before_test:
- go vet ./...
test_script:
- go test -v -race -covermode=atomic -coverprofile=coverage.txt
on_success:
- codecov -f coverage.txt

View File

@ -1,8 +0,0 @@
module github.com/OpenPeeDeeP/xdg
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
)

View File

@ -1,8 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

View File

@ -1,163 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package xdg impelements the XDG standard for application file locations.
package xdg
import (
"os"
"path/filepath"
"strings"
)
var defaulter xdgDefaulter = new(osDefaulter)
type xdgDefaulter interface {
defaultDataHome() string
defaultDataDirs() []string
defaultConfigHome() string
defaultConfigDirs() []string
defaultCacheHome() string
}
type osDefaulter struct {
}
//This method is used in the testing suit
// nolint: deadcode
func setDefaulter(def xdgDefaulter) {
defaulter = def
}
// XDG is information about the currently running application
type XDG struct {
Vendor string
Application string
}
// New returns an instance of XDG that is used to grab files for application use
func New(vendor, application string) *XDG {
return &XDG{
Vendor: vendor,
Application: application,
}
}
// DataHome returns the location that should be used for user specific data files for this specific application
func (x *XDG) DataHome() string {
return filepath.Join(DataHome(), x.Vendor, x.Application)
}
// DataDirs returns a list of locations that should be used for system wide data files for this specific application
func (x *XDG) DataDirs() []string {
dataDirs := DataDirs()
for i, dir := range dataDirs {
dataDirs[i] = filepath.Join(dir, x.Vendor, x.Application)
}
return dataDirs
}
// ConfigHome returns the location that should be used for user specific config files for this specific application
func (x *XDG) ConfigHome() string {
return filepath.Join(ConfigHome(), x.Vendor, x.Application)
}
// ConfigDirs returns a list of locations that should be used for system wide config files for this specific application
func (x *XDG) ConfigDirs() []string {
configDirs := ConfigDirs()
for i, dir := range configDirs {
configDirs[i] = filepath.Join(dir, x.Vendor, x.Application)
}
return configDirs
}
// CacheHome returns the location that should be used for application cache files for this specific application
func (x *XDG) CacheHome() string {
return filepath.Join(CacheHome(), x.Vendor, x.Application)
}
// QueryData looks for the given filename in XDG paths for data files.
// Returns an empty string if one was not found.
func (x *XDG) QueryData(filename string) string {
dirs := x.DataDirs()
dirs = append([]string{x.DataHome()}, dirs...)
return returnExist(filename, dirs)
}
// QueryConfig looks for the given filename in XDG paths for config files.
// Returns an empty string if one was not found.
func (x *XDG) QueryConfig(filename string) string {
dirs := x.ConfigDirs()
dirs = append([]string{x.ConfigHome()}, dirs...)
return returnExist(filename, dirs)
}
// QueryCache looks for the given filename in XDG paths for cache files.
// Returns an empty string if one was not found.
func (x *XDG) QueryCache(filename string) string {
return returnExist(filename, []string{x.CacheHome()})
}
func returnExist(filename string, dirs []string) string {
for _, dir := range dirs {
_, err := os.Stat(filepath.Join(dir, filename))
if (err != nil && os.IsExist(err)) || err == nil {
return filepath.Join(dir, filename)
}
}
return ""
}
// DataHome returns the location that should be used for user specific data files
func DataHome() string {
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = defaulter.defaultDataHome()
}
return dataHome
}
// DataDirs returns a list of locations that should be used for system wide data files
func DataDirs() []string {
var dataDirs []string
dataDirsStr := os.Getenv("XDG_DATA_DIRS")
if dataDirsStr != "" {
dataDirs = strings.Split(dataDirsStr, string(os.PathListSeparator))
}
if len(dataDirs) == 0 {
dataDirs = defaulter.defaultDataDirs()
}
return dataDirs
}
// ConfigHome returns the location that should be used for user specific config files
func ConfigHome() string {
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
configHome = defaulter.defaultConfigHome()
}
return configHome
}
// ConfigDirs returns a list of locations that should be used for system wide config files
func ConfigDirs() []string {
var configDirs []string
configDirsStr := os.Getenv("XDG_CONFIG_DIRS")
if configDirsStr != "" {
configDirs = strings.Split(configDirsStr, string(os.PathListSeparator))
}
if len(configDirs) == 0 {
configDirs = defaulter.defaultConfigDirs()
}
return configDirs
}
// CacheHome returns the location that should be used for application cache files
func CacheHome() string {
cacheHome := os.Getenv("XDG_CACHE_HOME")
if cacheHome == "" {
cacheHome = defaulter.defaultCacheHome()
}
return cacheHome
}

View File

@ -1,30 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xdg
import (
"os"
"path/filepath"
)
func (o *osDefaulter) defaultDataHome() string {
return filepath.Join(os.Getenv("HOME"), "Library", "Application Support")
}
func (o *osDefaulter) defaultDataDirs() []string {
return []string{filepath.Join("/Library", "Application Support")}
}
func (o *osDefaulter) defaultConfigHome() string {
return filepath.Join(os.Getenv("HOME"), "Library", "Application Support")
}
func (o *osDefaulter) defaultConfigDirs() []string {
return []string{filepath.Join("/Library", "Application Support")}
}
func (o *osDefaulter) defaultCacheHome() string {
return filepath.Join(os.Getenv("HOME"), "Library", "Caches")
}

View File

@ -1,30 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xdg
import (
"os"
"path/filepath"
)
func (o *osDefaulter) defaultDataHome() string {
return filepath.Join(os.Getenv("HOME"), ".local", "share")
}
func (o *osDefaulter) defaultDataDirs() []string {
return []string{"/usr/local/share/", "/usr/share/"}
}
func (o *osDefaulter) defaultConfigHome() string {
return filepath.Join(os.Getenv("HOME"), ".config")
}
func (o *osDefaulter) defaultConfigDirs() []string {
return []string{"/etc/xdg"}
}
func (o *osDefaulter) defaultCacheHome() string {
return filepath.Join(os.Getenv("HOME"), ".cache")
}

View File

@ -1,27 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xdg
import "os"
func (o *osDefaulter) defaultDataHome() string {
return os.Getenv("APPDATA")
}
func (o *osDefaulter) defaultDataDirs() []string {
return []string{os.Getenv("PROGRAMDATA")}
}
func (o *osDefaulter) defaultConfigHome() string {
return os.Getenv("APPDATA")
}
func (o *osDefaulter) defaultConfigDirs() []string {
return []string{os.Getenv("PROGRAMDATA")}
}
func (o *osDefaulter) defaultCacheHome() string {
return os.Getenv("LOCALAPPDATA")
}

View File

@ -1,26 +0,0 @@
# 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
*.prof
.DS_Store
/vendor

View File

@ -1,6 +0,0 @@
language: go
go:
- tip
script: go test -v ./

View File

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Zack Guo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,151 +0,0 @@
# termui [![Build Status](https://travis-ci.org/gizak/termui.svg?branch=master)](https://travis-ci.org/gizak/termui) [![Doc Status](https://godoc.org/github.com/gizak/termui?status.png)](https://godoc.org/github.com/gizak/termui)
<img src="./_example/dashboard.gif" alt="demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)" width="80%">
`termui` is a cross-platform, easy-to-compile, and fully-customizable terminal dashboard. It is inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib), but purely in Go.
Now version v2 has arrived! It brings new event system, new theme system, new `Buffer` interface and specific colour text rendering. (some docs are missing, but it will be completed soon!)
## Installation
`master` mirrors v2 branch, to install:
go get -u github.com/gizak/termui
It is recommanded to use locked deps by using [glide](https://glide.sh): move to `termui` src directory then run `glide up`.
For the compatible reason, you can choose to install the legacy version of `termui`:
go get gopkg.in/gizak/termui.v1
## Usage
### Layout
To use `termui`, the very first thing you may want to know is how to manage layout. `termui` offers two ways of doing this, known as absolute layout and grid layout.
__Absolute layout__
Each widget has an underlying block structure which basically is a box model. It has border, label and padding properties. A border of a widget can be chosen to hide or display (with its border label), you can pick a different front/back colour for the border as well. To display such a widget at a specific location in terminal window, you need to assign `.X`, `.Y`, `.Height`, `.Width` values for each widget before sending it to `.Render`. Let's demonstrate these by a code snippet:
`````go
import ui "github.com/gizak/termui" // <- ui shortcut, optional
func main() {
err := ui.Init()
if err != nil {
panic(err)
}
defer ui.Close()
p := ui.NewPar(":PRESS q TO QUIT DEMO")
p.Height = 3
p.Width = 50
p.TextFgColor = ui.ColorWhite
p.BorderLabel = "Text Box"
p.BorderFg = ui.ColorCyan
g := ui.NewGauge()
g.Percent = 50
g.Width = 50
g.Height = 3
g.Y = 11
g.BorderLabel = "Gauge"
g.BarColor = ui.ColorRed
g.BorderFg = ui.ColorWhite
g.BorderLabelFg = ui.ColorCyan
ui.Render(p, g) // feel free to call Render, it's async and non-block
// event handler...
}
`````
Note that components can be overlapped (I'd rather call this a feature...), `Render(rs ...Renderer)` renders its args from left to right (i.e. each component's weight is arising from left to right).
__Grid layout:__
<img src="./_example/grid.gif" alt="grid" width="60%">
Grid layout uses [12 columns grid system](http://www.w3schools.com/bootstrap/bootstrap_grid_system.asp) with expressive syntax. To use `Grid`, all we need to do is build a widget tree consisting of `Row`s and `Col`s (Actually a `Col` is also a `Row` but with a widget endpoint attached).
```go
import ui "github.com/gizak/termui"
// init and create widgets...
// build
ui.Body.AddRows(
ui.NewRow(
ui.NewCol(6, 0, widget0),
ui.NewCol(6, 0, widget1)),
ui.NewRow(
ui.NewCol(3, 0, widget2),
ui.NewCol(3, 0, widget30, widget31, widget32),
ui.NewCol(6, 0, widget4)))
// calculate layout
ui.Body.Align()
ui.Render(ui.Body)
```
### Events
`termui` ships with a http-like event mux handling system. All events are channeled up from different sources (typing, click, windows resize, custom event) and then encoded as universal `Event` object. `Event.Path` indicates the event type and `Event.Data` stores the event data struct. Add a handler to a certain event is easy as below:
```go
// handle key q pressing
ui.Handle("/sys/kbd/q", func(ui.Event) {
// press q to quit
ui.StopLoop()
})
ui.Handle("/sys/kbd/C-x", func(ui.Event) {
// handle Ctrl + x combination
})
ui.Handle("/sys/kbd", func(ui.Event) {
// handle all other key pressing
})
// handle a 1s timer
ui.Handle("/timer/1s", func(e ui.Event) {
t := e.Data.(ui.EvtTimer)
// t is a EvtTimer
if t.Count%2 ==0 {
// do something
}
})
ui.Loop() // block until StopLoop is called
```
### Widgets
Click image to see the corresponding demo codes.
[<img src="./_example/par.png" alt="par" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/par.go)
[<img src="./_example/list.png" alt="list" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/list.go)
[<img src="./_example/gauge.png" alt="gauge" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/gauge.go)
[<img src="./_example/linechart.png" alt="linechart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/linechart.go)
[<img src="./_example/barchart.png" alt="barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/barchart.go)
[<img src="./_example/mbarchart.png" alt="barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/mbarchart.go)
[<img src="./_example/sparklines.png" alt="sparklines" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/sparklines.go)
[<img src="./_example/table.png" alt="table" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/table.go)
## GoDoc
[godoc](https://godoc.org/github.com/gizak/termui)
## TODO
- [x] Grid layout
- [x] Event system
- [x] Canvas widget
- [x] Refine APIs
- [ ] Focusable widgets
## Changelog
## License
This library is under the [MIT License](http://opensource.org/licenses/MIT)

View File

@ -1,149 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "fmt"
// BarChart creates multiple bars in a widget:
/*
bc := termui.NewBarChart()
data := []int{3, 2, 5, 3, 9, 5}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.BorderLabel = "Bar Chart"
bc.Data = data
bc.Width = 26
bc.Height = 10
bc.DataLabels = bclabels
bc.TextColor = termui.ColorGreen
bc.BarColor = termui.ColorRed
bc.NumColor = termui.ColorYellow
*/
type BarChart struct {
Block
BarColor Attribute
TextColor Attribute
NumColor Attribute
Data []int
DataLabels []string
BarWidth int
BarGap int
CellChar rune
labels [][]rune
dataNum [][]rune
numBar int
scale float64
max int
}
// NewBarChart returns a new *BarChart with current theme.
func NewBarChart() *BarChart {
bc := &BarChart{Block: *NewBlock()}
bc.BarColor = ThemeAttr("barchart.bar.bg")
bc.NumColor = ThemeAttr("barchart.num.fg")
bc.TextColor = ThemeAttr("barchart.text.fg")
bc.BarGap = 1
bc.BarWidth = 3
bc.CellChar = ' '
return bc
}
func (bc *BarChart) layout() {
bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
bc.dataNum = make([][]rune, len(bc.Data))
for i := 0; i < bc.numBar && i < len(bc.DataLabels) && i < len(bc.Data); i++ {
bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth)
n := bc.Data[i]
s := fmt.Sprint(n)
bc.dataNum[i] = trimStr2Runes(s, bc.BarWidth)
}
//bc.max = bc.Data[0] // what if Data is nil? Sometimes when bar graph is nill it produces panic with panic: runtime error: index out of range
// Assign a negative value to get maxvalue auto-populates
if bc.max == 0 {
bc.max = -1
}
for i := 0; i < len(bc.Data); i++ {
if bc.max < bc.Data[i] {
bc.max = bc.Data[i]
}
}
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
}
func (bc *BarChart) SetMax(max int) {
if max > 0 {
bc.max = max
}
}
// Buffer implements Bufferer interface.
func (bc *BarChart) Buffer() Buffer {
buf := bc.Block.Buffer()
bc.layout()
for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ {
h := int(float64(bc.Data[i]) / bc.scale)
oftX := i * (bc.BarWidth + bc.BarGap)
barBg := bc.Bg
barFg := bc.BarColor
if bc.CellChar == ' ' {
barBg = bc.BarColor
barFg = ColorDefault
if bc.BarColor == ColorDefault { // the same as above
barBg |= AttrReverse
}
}
// plot bar
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
c := Cell{
Ch: bc.CellChar,
Bg: barBg,
Fg: barFg,
}
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k
buf.Set(x, y, c)
}
}
// plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
c := Cell{
Ch: bc.labels[i][j],
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
x := bc.innerArea.Min.X + oftX + k
buf.Set(x, y, c)
k += w
}
// plot num
for j := 0; j < len(bc.dataNum[i]); j++ {
c := Cell{
Ch: bc.dataNum[i][j],
Fg: bc.NumColor,
Bg: barBg,
}
if h == 0 {
c.Bg = bc.Bg
}
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
buf.Set(x, y, c)
}
}
return buf
}

View File

@ -1,240 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "image"
// Hline is a horizontal line.
type Hline struct {
X int
Y int
Len int
Fg Attribute
Bg Attribute
}
// Vline is a vertical line.
type Vline struct {
X int
Y int
Len int
Fg Attribute
Bg Attribute
}
// Buffer draws a horizontal line.
func (l Hline) Buffer() Buffer {
if l.Len <= 0 {
return NewBuffer()
}
return NewFilledBuffer(l.X, l.Y, l.X+l.Len, l.Y+1, HORIZONTAL_LINE, l.Fg, l.Bg)
}
// Buffer draws a vertical line.
func (l Vline) Buffer() Buffer {
if l.Len <= 0 {
return NewBuffer()
}
return NewFilledBuffer(l.X, l.Y, l.X+1, l.Y+l.Len, VERTICAL_LINE, l.Fg, l.Bg)
}
// Buffer draws a box border.
func (b Block) drawBorder(buf Buffer) {
if !b.Border {
return
}
min := b.area.Min
max := b.area.Max
x0 := min.X
y0 := min.Y
x1 := max.X - 1
y1 := max.Y - 1
// draw lines
if b.BorderTop {
buf.Merge(Hline{x0, y0, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
}
if b.BorderBottom {
buf.Merge(Hline{x0, y1, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
}
if b.BorderLeft {
buf.Merge(Vline{x0, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
}
if b.BorderRight {
buf.Merge(Vline{x1, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
}
// draw corners
if b.BorderTop && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 0 {
buf.Set(x0, y0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg})
}
if b.BorderTop && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 0 {
buf.Set(x1, y0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg})
}
if b.BorderBottom && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 1 {
buf.Set(x0, y1, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg})
}
if b.BorderBottom && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 1 {
buf.Set(x1, y1, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg})
}
}
func (b Block) drawBorderLabel(buf Buffer) {
maxTxtW := b.area.Dx() - 2
tx := DTrimTxCls(DefaultTxBuilder.Build(b.BorderLabel, b.BorderLabelFg, b.BorderLabelBg), maxTxtW)
for i, w := 0, 0; i < len(tx); i++ {
buf.Set(b.area.Min.X+1+w, b.area.Min.Y, tx[i])
w += tx[i].Width()
}
}
// Block is a base struct for all other upper level widgets,
// consider it as css: display:block.
// Normally you do not need to create it manually.
type Block struct {
area image.Rectangle
innerArea image.Rectangle
X int
Y int
Border bool
BorderFg Attribute
BorderBg Attribute
BorderLeft bool
BorderRight bool
BorderTop bool
BorderBottom bool
BorderLabel string
BorderLabelFg Attribute
BorderLabelBg Attribute
Display bool
Bg Attribute
Width int
Height int
PaddingTop int
PaddingBottom int
PaddingLeft int
PaddingRight int
id string
Float Align
}
// NewBlock returns a *Block which inherits styles from current theme.
func NewBlock() *Block {
b := Block{}
b.Display = true
b.Border = true
b.BorderLeft = true
b.BorderRight = true
b.BorderTop = true
b.BorderBottom = true
b.BorderBg = ThemeAttr("border.bg")
b.BorderFg = ThemeAttr("border.fg")
b.BorderLabelBg = ThemeAttr("label.bg")
b.BorderLabelFg = ThemeAttr("label.fg")
b.Bg = ThemeAttr("block.bg")
b.Width = 2
b.Height = 2
b.id = GenId()
b.Float = AlignNone
return &b
}
func (b Block) Id() string {
return b.id
}
// Align computes box model
func (b *Block) Align() {
// outer
b.area.Min.X = 0
b.area.Min.Y = 0
b.area.Max.X = b.Width
b.area.Max.Y = b.Height
// float
b.area = AlignArea(TermRect(), b.area, b.Float)
b.area = MoveArea(b.area, b.X, b.Y)
// inner
b.innerArea.Min.X = b.area.Min.X + b.PaddingLeft
b.innerArea.Min.Y = b.area.Min.Y + b.PaddingTop
b.innerArea.Max.X = b.area.Max.X - b.PaddingRight
b.innerArea.Max.Y = b.area.Max.Y - b.PaddingBottom
if b.Border {
if b.BorderLeft {
b.innerArea.Min.X++
}
if b.BorderRight {
b.innerArea.Max.X--
}
if b.BorderTop {
b.innerArea.Min.Y++
}
if b.BorderBottom {
b.innerArea.Max.Y--
}
}
}
// InnerBounds returns the internal bounds of the block after aligning and
// calculating the padding and border, if any.
func (b *Block) InnerBounds() image.Rectangle {
b.Align()
return b.innerArea
}
// Buffer implements Bufferer interface.
// Draw background and border (if any).
func (b *Block) Buffer() Buffer {
b.Align()
buf := NewBuffer()
buf.SetArea(b.area)
buf.Fill(' ', ColorDefault, b.Bg)
b.drawBorder(buf)
b.drawBorderLabel(buf)
return buf
}
// GetHeight implements GridBufferer.
// It returns current height of the block.
func (b Block) GetHeight() int {
return b.Height
}
// SetX implements GridBufferer interface, which sets block's x position.
func (b *Block) SetX(x int) {
b.X = x
}
// SetY implements GridBufferer interface, it sets y position for block.
func (b *Block) SetY(y int) {
b.Y = y
}
// SetWidth implements GridBuffer interface, it sets block's width.
func (b *Block) SetWidth(w int) {
b.Width = w
}
func (b Block) InnerWidth() int {
return b.innerArea.Dx()
}
func (b Block) InnerHeight() int {
return b.innerArea.Dy()
}
func (b Block) InnerX() int {
return b.innerArea.Min.X
}
func (b Block) InnerY() int { return b.innerArea.Min.Y }

View File

@ -1,20 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build !windows
package termui
const TOP_RIGHT = '┐'
const VERTICAL_LINE = '│'
const HORIZONTAL_LINE = '─'
const TOP_LEFT = '┌'
const BOTTOM_RIGHT = '┘'
const BOTTOM_LEFT = '└'
const VERTICAL_LEFT = '┤'
const VERTICAL_RIGHT = '├'
const HORIZONTAL_DOWN = '┬'
const HORIZONTAL_UP = '┴'
const QUOTA_LEFT = '«'
const QUOTA_RIGHT = '»'

View File

@ -1,14 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build windows
package termui
const TOP_RIGHT = '+'
const VERTICAL_LINE = '|'
const HORIZONTAL_LINE = '-'
const TOP_LEFT = '+'
const BOTTOM_RIGHT = '+'
const BOTTOM_LEFT = '+'

View File

@ -1,106 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "image"
// Cell is a rune with assigned Fg and Bg
type Cell struct {
Ch rune
Fg Attribute
Bg Attribute
}
// Buffer is a renderable rectangle cell data container.
type Buffer struct {
Area image.Rectangle // selected drawing area
CellMap map[image.Point]Cell
}
// At returns the cell at (x,y).
func (b Buffer) At(x, y int) Cell {
return b.CellMap[image.Pt(x, y)]
}
// Set assigns a char to (x,y)
func (b Buffer) Set(x, y int, c Cell) {
b.CellMap[image.Pt(x, y)] = c
}
// Bounds returns the domain for which At can return non-zero color.
func (b Buffer) Bounds() image.Rectangle {
x0, y0, x1, y1 := 0, 0, 0, 0
for p := range b.CellMap {
if p.X > x1 {
x1 = p.X
}
if p.X < x0 {
x0 = p.X
}
if p.Y > y1 {
y1 = p.Y
}
if p.Y < y0 {
y0 = p.Y
}
}
return image.Rect(x0, y0, x1+1, y1+1)
}
// SetArea assigns a new rect area to Buffer b.
func (b *Buffer) SetArea(r image.Rectangle) {
b.Area.Max = r.Max
b.Area.Min = r.Min
}
// Sync sets drawing area to the buffer's bound
func (b *Buffer) Sync() {
b.SetArea(b.Bounds())
}
// NewCell returns a new cell
func NewCell(ch rune, fg, bg Attribute) Cell {
return Cell{ch, fg, bg}
}
// Merge merges bs Buffers onto b
func (b *Buffer) Merge(bs ...Buffer) {
for _, buf := range bs {
for p, v := range buf.CellMap {
b.Set(p.X, p.Y, v)
}
b.SetArea(b.Area.Union(buf.Area))
}
}
// NewBuffer returns a new Buffer
func NewBuffer() Buffer {
return Buffer{
CellMap: make(map[image.Point]Cell),
Area: image.Rectangle{}}
}
// Fill fills the Buffer b with ch,fg and bg.
func (b Buffer) Fill(ch rune, fg, bg Attribute) {
for x := b.Area.Min.X; x < b.Area.Max.X; x++ {
for y := b.Area.Min.Y; y < b.Area.Max.Y; y++ {
b.Set(x, y, Cell{ch, fg, bg})
}
}
}
// NewFilledBuffer returns a new Buffer filled with ch, fb and bg.
func NewFilledBuffer(x0, y0, x1, y1 int, ch rune, fg, bg Attribute) Buffer {
buf := NewBuffer()
buf.Area.Min = image.Pt(x0, y0)
buf.Area.Max = image.Pt(x1, y1)
for x := buf.Area.Min.X; x < buf.Area.Max.X; x++ {
for y := buf.Area.Min.Y; y < buf.Area.Max.Y; y++ {
buf.Set(x, y, Cell{ch, fg, bg})
}
}
return buf
}

View File

@ -1,72 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
/*
dots:
,___,
|1 4|
|2 5|
|3 6|
|7 8|
`````
*/
var brailleBase = '\u2800'
var brailleOftMap = [4][2]rune{
{'\u0001', '\u0008'},
{'\u0002', '\u0010'},
{'\u0004', '\u0020'},
{'\u0040', '\u0080'}}
// Canvas contains drawing map: i,j -> rune
type Canvas map[[2]int]rune
// NewCanvas returns an empty Canvas
func NewCanvas() Canvas {
return make(map[[2]int]rune)
}
func chOft(x, y int) rune {
return brailleOftMap[y%4][x%2]
}
func (c Canvas) rawCh(x, y int) rune {
if ch, ok := c[[2]int{x, y}]; ok {
return ch
}
return '\u0000' //brailleOffset
}
// return coordinate in terminal
func chPos(x, y int) (int, int) {
return y / 4, x / 2
}
// Set sets a point (x,y) in the virtual coordinate
func (c Canvas) Set(x, y int) {
i, j := chPos(x, y)
ch := c.rawCh(i, j)
ch |= chOft(x, y)
c[[2]int{i, j}] = ch
}
// Unset removes point (x,y)
func (c Canvas) Unset(x, y int) {
i, j := chPos(x, y)
ch := c.rawCh(i, j)
ch &= ^chOft(x, y)
c[[2]int{i, j}] = ch
}
// Buffer returns un-styled points
func (c Canvas) Buffer() Buffer {
buf := NewBuffer()
for k, v := range c {
buf.Set(k[0], k[1], Cell{Ch: v + brailleBase})
}
return buf
}

View File

@ -1,54 +0,0 @@
#!/usr/bin/env python3
import re
import os
import io
copyright = """// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
"""
exclude_dirs = [".git", "_docs"]
exclude_files = []
include_dirs = [".", "debug", "extra", "test", "_example"]
def is_target(fpath):
if os.path.splitext(fpath)[-1] == ".go":
return True
return False
def update_copyright(fpath):
print("processing " + fpath)
f = io.open(fpath, 'r', encoding='utf-8')
fstr = f.read()
f.close()
# remove old
m = re.search('^// Copyright .+?\r?\n\r?\n', fstr, re.MULTILINE|re.DOTALL)
if m:
fstr = fstr[m.end():]
# add new
fstr = copyright + fstr
f = io.open(fpath, 'w',encoding='utf-8')
f.write(fstr)
f.close()
def main():
for d in include_dirs:
files = [
os.path.join(d, f) for f in os.listdir(d)
if os.path.isfile(os.path.join(d, f))
]
for f in files:
if is_target(f):
update_copyright(f)
if __name__ == '__main__':
main()

View File

@ -1,29 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
/*
Package termui is a library designed for creating command line UI. For more info, goto http://github.com/gizak/termui
A simplest example:
package main
import ui "github.com/gizak/termui"
func main() {
if err:=ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
g := ui.NewGauge()
g.Percent = 50
g.Width = 50
g.BorderLabel = "Gauge"
ui.Render(g)
ui.Loop()
}
*/
package termui

View File

@ -1,328 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"path"
"strconv"
"sync"
"time"
"github.com/nsf/termbox-go"
)
type Event struct {
Type string
Path string
From string
To string
Data interface{}
Time int64
}
var sysEvtChs []chan Event
type EvtKbd struct {
KeyStr string
}
func evtKbd(e termbox.Event) EvtKbd {
ek := EvtKbd{}
k := string(e.Ch)
pre := ""
mod := ""
if e.Mod == termbox.ModAlt {
mod = "M-"
}
if e.Ch == 0 {
if e.Key > 0xFFFF-12 {
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
} else if e.Key > 0xFFFF-25 {
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
k = ks[0xFFFF-int(e.Key)-12]
}
if e.Key <= 0x7F {
pre = "C-"
k = string('a' - 1 + int(e.Key))
kmap := map[termbox.Key][2]string{
termbox.KeyCtrlSpace: {"C-", "<space>"},
termbox.KeyBackspace: {"", "<backspace>"},
termbox.KeyTab: {"", "<tab>"},
termbox.KeyEnter: {"", "<enter>"},
termbox.KeyEsc: {"", "<escape>"},
termbox.KeyCtrlBackslash: {"C-", "\\"},
termbox.KeyCtrlSlash: {"C-", "/"},
termbox.KeySpace: {"", "<space>"},
termbox.KeyCtrl8: {"C-", "8"},
}
if sk, ok := kmap[e.Key]; ok {
pre = sk[0]
k = sk[1]
}
}
}
ek.KeyStr = pre + mod + k
return ek
}
func crtTermboxEvt(e termbox.Event) Event {
systypemap := map[termbox.EventType]string{
termbox.EventKey: "keyboard",
termbox.EventResize: "window",
termbox.EventMouse: "mouse",
termbox.EventError: "error",
termbox.EventInterrupt: "interrupt",
}
ne := Event{From: "/sys", Time: time.Now().Unix()}
typ := e.Type
ne.Type = systypemap[typ]
switch typ {
case termbox.EventKey:
kbd := evtKbd(e)
ne.Path = "/sys/kbd/" + kbd.KeyStr
ne.Data = kbd
case termbox.EventResize:
wnd := EvtWnd{}
wnd.Width = e.Width
wnd.Height = e.Height
ne.Path = "/sys/wnd/resize"
ne.Data = wnd
case termbox.EventError:
err := EvtErr(e.Err)
ne.Path = "/sys/err"
ne.Data = err
case termbox.EventMouse:
m := EvtMouse{}
m.X = e.MouseX
m.Y = e.MouseY
ne.Path = "/sys/mouse"
ne.Data = m
}
return ne
}
type EvtWnd struct {
Width int
Height int
}
type EvtMouse struct {
X int
Y int
Press string
}
type EvtErr error
func hookTermboxEvt() {
for {
e := termbox.PollEvent()
for _, c := range sysEvtChs {
func(ch chan Event) {
ch <- crtTermboxEvt(e)
}(c)
}
}
}
func NewSysEvtCh() chan Event {
ec := make(chan Event)
sysEvtChs = append(sysEvtChs, ec)
return ec
}
var DefaultEvtStream = NewEvtStream()
type EvtStream struct {
sync.RWMutex
srcMap map[string]chan Event
stream chan Event
wg sync.WaitGroup
sigStopLoop chan Event
Handlers map[string]func(Event)
hook func(Event)
}
func NewEvtStream() *EvtStream {
return &EvtStream{
srcMap: make(map[string]chan Event),
stream: make(chan Event),
Handlers: make(map[string]func(Event)),
sigStopLoop: make(chan Event),
}
}
func (es *EvtStream) Init() {
es.Merge("internal", es.sigStopLoop)
go func() {
es.wg.Wait()
close(es.stream)
}()
}
func cleanPath(p string) string {
if p == "" {
return "/"
}
if p[0] != '/' {
p = "/" + p
}
return path.Clean(p)
}
func isPathMatch(pattern, path string) bool {
if len(pattern) == 0 {
return false
}
n := len(pattern)
return len(path) >= n && path[0:n] == pattern
}
func (es *EvtStream) Merge(name string, ec chan Event) {
es.Lock()
defer es.Unlock()
es.wg.Add(1)
es.srcMap[name] = ec
go func(a chan Event) {
for n := range a {
n.From = name
es.stream <- n
}
es.wg.Done()
}(ec)
}
func (es *EvtStream) Handle(path string, handler func(Event)) {
es.Handlers[cleanPath(path)] = handler
}
func findMatch(mux map[string]func(Event), path string) string {
n := -1
pattern := ""
for m := range mux {
if !isPathMatch(m, path) {
continue
}
if len(m) > n {
pattern = m
n = len(m)
}
}
return pattern
}
// Remove all existing defined Handlers from the map
func (es *EvtStream) ResetHandlers() {
for Path, _ := range es.Handlers {
delete(es.Handlers, Path)
}
return
}
func (es *EvtStream) match(path string) string {
return findMatch(es.Handlers, path)
}
func (es *EvtStream) Hook(f func(Event)) {
es.hook = f
}
func (es *EvtStream) Loop() {
for e := range es.stream {
switch e.Path {
case "/sig/stoploop":
return
}
func(a Event) {
es.RLock()
defer es.RUnlock()
if pattern := es.match(a.Path); pattern != "" {
es.Handlers[pattern](a)
}
}(e)
if es.hook != nil {
es.hook(e)
}
}
}
func (es *EvtStream) StopLoop() {
go func() {
e := Event{
Path: "/sig/stoploop",
}
es.sigStopLoop <- e
}()
}
func Merge(name string, ec chan Event) {
DefaultEvtStream.Merge(name, ec)
}
func Handle(path string, handler func(Event)) {
DefaultEvtStream.Handle(path, handler)
}
func ResetHandlers() {
DefaultEvtStream.ResetHandlers()
}
func Loop() {
DefaultEvtStream.Loop()
}
func StopLoop() {
DefaultEvtStream.StopLoop()
}
type EvtTimer struct {
Duration time.Duration
Count uint64
}
func NewTimerCh(du time.Duration) chan Event {
t := make(chan Event)
go func(a chan Event) {
n := uint64(0)
for {
n++
time.Sleep(du)
e := Event{}
e.Type = "timer"
e.Path = "/timer/" + du.String()
e.Time = time.Now().Unix()
e.Data = EvtTimer{
Duration: du,
Count: n,
}
t <- e
}
}(t)
return t
}
var DefaultHandler = func(e Event) {
}
var usrEvtCh = make(chan Event)
func SendCustomEvt(path string, data interface{}) {
e := Event{}
e.Path = path
e.Data = data
e.Time = time.Now().Unix()
usrEvtCh <- e
}

View File

@ -1,109 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"strconv"
"strings"
)
// Gauge is a progress bar like widget.
// A simple example:
/*
g := termui.NewGauge()
g.Percent = 40
g.Width = 50
g.Height = 3
g.BorderLabel = "Slim Gauge"
g.BarColor = termui.ColorRed
g.PercentColor = termui.ColorBlue
*/
const ColorUndef Attribute = Attribute(^uint16(0))
type Gauge struct {
Block
Percent int
BarColor Attribute
PercentColor Attribute
PercentColorHighlighted Attribute
Label string
LabelAlign Align
}
// NewGauge return a new gauge with current theme.
func NewGauge() *Gauge {
g := &Gauge{
Block: *NewBlock(),
PercentColor: ThemeAttr("gauge.percent.fg"),
BarColor: ThemeAttr("gauge.bar.bg"),
Label: "{{percent}}%",
LabelAlign: AlignCenter,
PercentColorHighlighted: ColorUndef,
}
g.Width = 12
g.Height = 5
return g
}
// Buffer implements Bufferer interface.
func (g *Gauge) Buffer() Buffer {
buf := g.Block.Buffer()
// plot bar
w := g.Percent * g.innerArea.Dx() / 100
for i := 0; i < g.innerArea.Dy(); i++ {
for j := 0; j < w; j++ {
c := Cell{}
c.Ch = ' '
c.Bg = g.BarColor
if c.Bg == ColorDefault {
c.Bg |= AttrReverse
}
buf.Set(g.innerArea.Min.X+j, g.innerArea.Min.Y+i, c)
}
}
// plot percentage
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1)
pry := g.innerArea.Min.Y + g.innerArea.Dy()/2
rs := str2runes(s)
var pos int
switch g.LabelAlign {
case AlignLeft:
pos = 0
case AlignCenter:
pos = (g.innerArea.Dx() - strWidth(s)) / 2
case AlignRight:
pos = g.innerArea.Dx() - strWidth(s) - 1
}
pos += g.innerArea.Min.X
for i, v := range rs {
c := Cell{
Ch: v,
Fg: g.PercentColor,
}
if w+g.innerArea.Min.X > pos+i {
c.Bg = g.BarColor
if c.Bg == ColorDefault {
c.Bg |= AttrReverse
}
if g.PercentColorHighlighted != ColorUndef {
c.Fg = g.PercentColorHighlighted
}
} else {
c.Bg = g.Block.Bg
}
buf.Set(1+pos+i, pry, c)
}
return buf
}

View File

@ -1,30 +0,0 @@
hash: 7a754ba100256404a978b2fc8738aee337beb822458e4b6060399fb89ebd215c
updated: 2016-11-03T17:39:24.323773674-04:00
imports:
- name: github.com/maruel/panicparse
version: ad661195ed0e88491e0f14be6613304e3b1141d6
subpackages:
- stack
- name: github.com/mattn/go-runewidth
version: 737072b4e32b7a5018b4a7125da8d12de90e8045
- name: github.com/mitchellh/go-wordwrap
version: ad45545899c7b13c020ea92b2072220eefad42b8
- name: github.com/nsf/termbox-go
version: b6acae516ace002cb8105a89024544a1480655a5
- name: golang.org/x/net
version: 569280fa63be4e201b975e5411e30a92178f0118
subpackages:
- websocket
testImports:
- name: github.com/davecgh/go-spew
version: 346938d642f2ec3594ed81d874461961cd0faa76
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib
- name: github.com/stretchr/testify
version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506
subpackages:
- assert

View File

@ -1,9 +0,0 @@
package: github.com/gizak/termui
import:
- package: github.com/mattn/go-runewidth
- package: github.com/mitchellh/go-wordwrap
- package: github.com/nsf/termbox-go
- package: golang.org/x/net
subpackages:
- websocket
- package: github.com/maruel/panicparse

View File

@ -1,279 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
// GridBufferer introduces a Bufferer that can be manipulated by Grid.
type GridBufferer interface {
Bufferer
GetHeight() int
SetWidth(int)
SetX(int)
SetY(int)
}
// Row builds a layout tree
type Row struct {
Cols []*Row //children
Widget GridBufferer // root
X int
Y int
Width int
Height int
Span int
Offset int
}
// calculate and set the underlying layout tree's x, y, height and width.
func (r *Row) calcLayout() {
r.assignWidth(r.Width)
r.Height = r.solveHeight()
r.assignX(r.X)
r.assignY(r.Y)
}
// tell if the node is leaf in the tree.
func (r *Row) isLeaf() bool {
return r.Cols == nil || len(r.Cols) == 0
}
func (r *Row) isRenderableLeaf() bool {
return r.isLeaf() && r.Widget != nil
}
// assign widgets' (and their parent rows') width recursively.
func (r *Row) assignWidth(w int) {
r.SetWidth(w)
accW := 0 // acc span and offset
calcW := make([]int, len(r.Cols)) // calculated width
calcOftX := make([]int, len(r.Cols)) // computed start position of x
for i, c := range r.Cols {
accW += c.Span + c.Offset
cw := int(float64(c.Span*r.Width) / 12.0)
if i >= 1 {
calcOftX[i] = calcOftX[i-1] +
calcW[i-1] +
int(float64(r.Cols[i-1].Offset*r.Width)/12.0)
}
// use up the space if it is the last col
if i == len(r.Cols)-1 && accW == 12 {
cw = r.Width - calcOftX[i]
}
calcW[i] = cw
r.Cols[i].assignWidth(cw)
}
}
// bottom up calc and set rows' (and their widgets') height,
// return r's total height.
func (r *Row) solveHeight() int {
if r.isRenderableLeaf() {
r.Height = r.Widget.GetHeight()
return r.Widget.GetHeight()
}
maxh := 0
if !r.isLeaf() {
for _, c := range r.Cols {
nh := c.solveHeight()
// when embed rows in Cols, row widgets stack up
if r.Widget != nil {
nh += r.Widget.GetHeight()
}
if nh > maxh {
maxh = nh
}
}
}
r.Height = maxh
return maxh
}
// recursively assign x position for r tree.
func (r *Row) assignX(x int) {
r.SetX(x)
if !r.isLeaf() {
acc := 0
for i, c := range r.Cols {
if c.Offset != 0 {
acc += int(float64(c.Offset*r.Width) / 12.0)
}
r.Cols[i].assignX(x + acc)
acc += c.Width
}
}
}
// recursively assign y position to r.
func (r *Row) assignY(y int) {
r.SetY(y)
if r.isLeaf() {
return
}
for i := range r.Cols {
acc := 0
if r.Widget != nil {
acc = r.Widget.GetHeight()
}
r.Cols[i].assignY(y + acc)
}
}
// GetHeight implements GridBufferer interface.
func (r Row) GetHeight() int {
return r.Height
}
// SetX implements GridBufferer interface.
func (r *Row) SetX(x int) {
r.X = x
if r.Widget != nil {
r.Widget.SetX(x)
}
}
// SetY implements GridBufferer interface.
func (r *Row) SetY(y int) {
r.Y = y
if r.Widget != nil {
r.Widget.SetY(y)
}
}
// SetWidth implements GridBufferer interface.
func (r *Row) SetWidth(w int) {
r.Width = w
if r.Widget != nil {
r.Widget.SetWidth(w)
}
}
// Buffer implements Bufferer interface,
// recursively merge all widgets buffer
func (r *Row) Buffer() Buffer {
merged := NewBuffer()
if r.isRenderableLeaf() {
return r.Widget.Buffer()
}
// for those are not leaves but have a renderable widget
if r.Widget != nil {
merged.Merge(r.Widget.Buffer())
}
// collect buffer from children
if !r.isLeaf() {
for _, c := range r.Cols {
merged.Merge(c.Buffer())
}
}
return merged
}
// Grid implements 12 columns system.
// A simple example:
/*
import ui "github.com/gizak/termui"
// init and create widgets...
// build
ui.Body.AddRows(
ui.NewRow(
ui.NewCol(6, 0, widget0),
ui.NewCol(6, 0, widget1)),
ui.NewRow(
ui.NewCol(3, 0, widget2),
ui.NewCol(3, 0, widget30, widget31, widget32),
ui.NewCol(6, 0, widget4)))
// calculate layout
ui.Body.Align()
ui.Render(ui.Body)
*/
type Grid struct {
Rows []*Row
Width int
X int
Y int
BgColor Attribute
}
// NewGrid returns *Grid with given rows.
func NewGrid(rows ...*Row) *Grid {
return &Grid{Rows: rows}
}
// AddRows appends given rows to Grid.
func (g *Grid) AddRows(rs ...*Row) {
g.Rows = append(g.Rows, rs...)
}
// NewRow creates a new row out of given columns.
func NewRow(cols ...*Row) *Row {
rs := &Row{Span: 12, Cols: cols}
return rs
}
// NewCol accepts: widgets are LayoutBufferer or widgets is A NewRow.
// Note that if multiple widgets are provided, they will stack up in the col.
func NewCol(span, offset int, widgets ...GridBufferer) *Row {
r := &Row{Span: span, Offset: offset}
if widgets != nil && len(widgets) == 1 {
wgt := widgets[0]
nw, isRow := wgt.(*Row)
if isRow {
r.Cols = nw.Cols
} else {
r.Widget = wgt
}
return r
}
r.Cols = []*Row{}
ir := r
for _, w := range widgets {
nr := &Row{Span: 12, Widget: w}
ir.Cols = []*Row{nr}
ir = nr
}
return r
}
// Align calculate each rows' layout.
func (g *Grid) Align() {
h := 0
for _, r := range g.Rows {
r.SetWidth(g.Width)
r.SetX(g.X)
r.SetY(g.Y + h)
r.calcLayout()
h += r.GetHeight()
}
}
// Buffer implements Bufferer interface.
func (g Grid) Buffer() Buffer {
buf := NewBuffer()
for _, r := range g.Rows {
buf.Merge(r.Buffer())
}
return buf
}
var Body *Grid

View File

@ -1,222 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"regexp"
"strings"
tm "github.com/nsf/termbox-go"
)
import rw "github.com/mattn/go-runewidth"
/* ---------------Port from termbox-go --------------------- */
// Attribute is printable cell's color and style.
type Attribute uint16
// 8 basic clolrs
const (
ColorDefault Attribute = iota
ColorBlack
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
)
//Have a constant that defines number of colors
const NumberofColors = 8
// Text style
const (
AttrBold Attribute = 1 << (iota + 9)
AttrUnderline
AttrReverse
)
var (
dot = "…"
dotw = rw.StringWidth(dot)
)
/* ----------------------- End ----------------------------- */
func toTmAttr(x Attribute) tm.Attribute {
return tm.Attribute(x)
}
func str2runes(s string) []rune {
return []rune(s)
}
// Here for backwards-compatibility.
func trimStr2Runes(s string, w int) []rune {
return TrimStr2Runes(s, w)
}
// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
// of that string if string is grather then n. If string is small then w,
// return the runes.
func TrimStr2Runes(s string, w int) []rune {
if w <= 0 {
return []rune{}
}
sw := rw.StringWidth(s)
if sw > w {
return []rune(rw.Truncate(s, w, dot))
}
return str2runes(s)
}
// TrimStrIfAppropriate trim string to "s[:-1] + …"
// if string > width otherwise return string
func TrimStrIfAppropriate(s string, w int) string {
if w <= 0 {
return ""
}
sw := rw.StringWidth(s)
if sw > w {
return rw.Truncate(s, w, dot)
}
return s
}
func strWidth(s string) int {
return rw.StringWidth(s)
}
func charWidth(ch rune) int {
return rw.RuneWidth(ch)
}
var whiteSpaceRegex = regexp.MustCompile(`\s`)
// StringToAttribute converts text to a termui attribute. You may specify more
// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
// are ignored.
func StringToAttribute(text string) Attribute {
text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
attributes := strings.Split(text, ",")
result := Attribute(0)
for _, theAttribute := range attributes {
var match Attribute
switch theAttribute {
case "reset", "default":
match = ColorDefault
case "black":
match = ColorBlack
case "red":
match = ColorRed
case "green":
match = ColorGreen
case "yellow":
match = ColorYellow
case "blue":
match = ColorBlue
case "magenta":
match = ColorMagenta
case "cyan":
match = ColorCyan
case "white":
match = ColorWhite
case "bold":
match = AttrBold
case "underline":
match = AttrUnderline
case "reverse":
match = AttrReverse
}
result |= match
}
return result
}
// TextCells returns a coloured text cells []Cell
func TextCells(s string, fg, bg Attribute) []Cell {
cs := make([]Cell, 0, len(s))
// sequence := MarkdownTextRendererFactory{}.TextRenderer(s).Render(fg, bg)
// runes := []rune(sequence.NormalizedText)
runes := str2runes(s)
for n := range runes {
// point, _ := sequence.PointAt(n, 0, 0)
// cs = append(cs, Cell{point.Ch, point.Fg, point.Bg})
cs = append(cs, Cell{runes[n], fg, bg})
}
return cs
}
// Width returns the actual screen space the cell takes (usually 1 or 2).
func (c Cell) Width() int {
return charWidth(c.Ch)
}
// Copy return a copy of c
func (c Cell) Copy() Cell {
return c
}
// TrimTxCells trims the overflowed text cells sequence.
func TrimTxCells(cs []Cell, w int) []Cell {
if len(cs) <= w {
return cs
}
return cs[:w]
}
// DTrimTxCls trims the overflowed text cells sequence and append dots at the end.
func DTrimTxCls(cs []Cell, w int) []Cell {
l := len(cs)
if l <= 0 {
return []Cell{}
}
rt := make([]Cell, 0, w)
csw := 0
for i := 0; i < l && csw <= w; i++ {
c := cs[i]
cw := c.Width()
if cw+csw < w {
rt = append(rt, c)
csw += cw
} else {
rt = append(rt, Cell{'…', c.Fg, c.Bg})
break
}
}
return rt
}
func CellsToStr(cs []Cell) string {
str := ""
for _, c := range cs {
str += string(c.Ch)
}
return str
}

View File

@ -1,331 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"fmt"
"math"
)
// only 16 possible combinations, why bother
var braillePatterns = map[[2]int]rune{
[2]int{0, 0}: '⣀',
[2]int{0, 1}: '⡠',
[2]int{0, 2}: '⡐',
[2]int{0, 3}: '⡈',
[2]int{1, 0}: '⢄',
[2]int{1, 1}: '⠤',
[2]int{1, 2}: '⠔',
[2]int{1, 3}: '⠌',
[2]int{2, 0}: '⢂',
[2]int{2, 1}: '⠢',
[2]int{2, 2}: '⠒',
[2]int{2, 3}: '⠊',
[2]int{3, 0}: '⢁',
[2]int{3, 1}: '⠡',
[2]int{3, 2}: '⠑',
[2]int{3, 3}: '⠉',
}
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
// LineChart has two modes: braille(default) and dot. Using braille gives 2x capacity as dot mode,
// because one braille char can represent two data points.
/*
lc := termui.NewLineChart()
lc.BorderLabel = "braille-mode Line Chart"
lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
lc.Width = 50
lc.Height = 12
lc.AxesColor = termui.ColorWhite
lc.LineColor = termui.ColorGreen | termui.AttrBold
// termui.Render(lc)...
*/
type LineChart struct {
Block
Data []float64
DataLabels []string // if unset, the data indices will be used
Mode string // braille | dot
DotStyle rune
LineColor Attribute
scale float64 // data span per cell on y-axis
AxesColor Attribute
drawingX int
drawingY int
axisYHeight int
axisXWidth int
axisYLabelGap int
axisXLabelGap int
topValue float64
bottomValue float64
labelX [][]rune
labelY [][]rune
labelYSpace int
maxY float64
minY float64
autoLabels bool
}
// NewLineChart returns a new LineChart with current theme.
func NewLineChart() *LineChart {
lc := &LineChart{Block: *NewBlock()}
lc.AxesColor = ThemeAttr("linechart.axes.fg")
lc.LineColor = ThemeAttr("linechart.line.fg")
lc.Mode = "braille"
lc.DotStyle = '•'
lc.axisXLabelGap = 2
lc.axisYLabelGap = 1
lc.bottomValue = math.Inf(1)
lc.topValue = math.Inf(-1)
return lc
}
// one cell contains two data points
// so the capacity is 2x as dot-mode
func (lc *LineChart) renderBraille() Buffer {
buf := NewBuffer()
// return: b -> which cell should the point be in
// m -> in the cell, divided into 4 equal height levels, which subcell?
getPos := func(d float64) (b, m int) {
cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5)
b = cnt4 / 4
m = cnt4 % 4
return
}
// plot points
for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
b0, m0 := getPos(lc.Data[2*i])
b1, m1 := getPos(lc.Data[2*i+1])
if b0 == b1 {
c := Cell{
Ch: braillePatterns[[2]int{m0, m1}],
Bg: lc.Bg,
Fg: lc.LineColor,
}
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
buf.Set(x, y, c)
} else {
c0 := Cell{Ch: lSingleBraille[m0],
Fg: lc.LineColor,
Bg: lc.Bg}
x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
buf.Set(x0, y0, c0)
c1 := Cell{Ch: rSingleBraille[m1],
Fg: lc.LineColor,
Bg: lc.Bg}
x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
buf.Set(x1, y1, c1)
}
}
return buf
}
func (lc *LineChart) renderDot() Buffer {
buf := NewBuffer()
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
c := Cell{
Ch: lc.DotStyle,
Fg: lc.LineColor,
Bg: lc.Bg,
}
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
buf.Set(x, y, c)
}
return buf
}
func (lc *LineChart) calcLabelX() {
lc.labelX = [][]rune{}
for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ {
if lc.Mode == "dot" {
if l >= len(lc.DataLabels) {
break
}
s := str2runes(lc.DataLabels[l])
w := strWidth(lc.DataLabels[l])
if l+w <= lc.axisXWidth {
lc.labelX = append(lc.labelX, s)
}
l += w + lc.axisXLabelGap
} else { // braille
if 2*l >= len(lc.DataLabels) {
break
}
s := str2runes(lc.DataLabels[2*l])
w := strWidth(lc.DataLabels[2*l])
if l+w <= lc.axisXWidth {
lc.labelX = append(lc.labelX, s)
}
l += w + lc.axisXLabelGap
}
}
}
func shortenFloatVal(x float64) string {
s := fmt.Sprintf("%.2f", x)
if len(s)-3 > 3 {
s = fmt.Sprintf("%.2e", x)
}
if x < 0 {
s = fmt.Sprintf("%.2f", x)
}
return s
}
func (lc *LineChart) calcLabelY() {
span := lc.topValue - lc.bottomValue
lc.scale = span / float64(lc.axisYHeight)
n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
lc.labelY = make([][]rune, n)
maxLen := 0
for i := 0; i < n; i++ {
s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n)))
if len(s) > maxLen {
maxLen = len(s)
}
lc.labelY[i] = s
}
lc.labelYSpace = maxLen
}
func (lc *LineChart) calcLayout() {
// set datalabels if it is not provided
if (lc.DataLabels == nil || len(lc.DataLabels) == 0) || lc.autoLabels {
lc.autoLabels = true
lc.DataLabels = make([]string, len(lc.Data))
for i := range lc.Data {
lc.DataLabels[i] = fmt.Sprint(i)
}
}
// lazy increase, to avoid y shaking frequently
// update bound Y when drawing is gonna overflow
lc.minY = lc.Data[0]
lc.maxY = lc.Data[0]
// valid visible range
vrange := lc.innerArea.Dx()
if lc.Mode == "braille" {
vrange = 2 * lc.innerArea.Dx()
}
if vrange > len(lc.Data) {
vrange = len(lc.Data)
}
for _, v := range lc.Data[:vrange] {
if v > lc.maxY {
lc.maxY = v
}
if v < lc.minY {
lc.minY = v
}
}
span := lc.maxY - lc.minY
if lc.minY < lc.bottomValue {
lc.bottomValue = lc.minY - 0.2*span
}
if lc.maxY > lc.topValue {
lc.topValue = lc.maxY + 0.2*span
}
lc.axisYHeight = lc.innerArea.Dy() - 2
lc.calcLabelY()
lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
lc.calcLabelX()
lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
lc.drawingY = lc.innerArea.Min.Y
}
func (lc *LineChart) plotAxes() Buffer {
buf := NewBuffer()
origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - 2
origX := lc.innerArea.Min.X + lc.labelYSpace
buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})
for x := origX + 1; x < origX+lc.axisXWidth; x++ {
buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
}
for dy := 1; dy <= lc.axisYHeight; dy++ {
buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
}
// x label
oft := 0
for _, rs := range lc.labelX {
if oft+len(rs) > lc.axisXWidth {
break
}
for j, r := range rs {
c := Cell{
Ch: r,
Fg: lc.AxesColor,
Bg: lc.Bg,
}
x := origX + oft + j
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
buf.Set(x, y, c)
}
oft += len(rs) + lc.axisXLabelGap
}
// y labels
for i, rs := range lc.labelY {
for j, r := range rs {
buf.Set(
lc.innerArea.Min.X+j,
origY-i*(lc.axisYLabelGap+1),
Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
}
}
return buf
}
// Buffer implements Bufferer interface.
func (lc *LineChart) Buffer() Buffer {
buf := lc.Block.Buffer()
if lc.Data == nil || len(lc.Data) == 0 {
return buf
}
lc.calcLayout()
buf.Merge(lc.plotAxes())
if lc.Mode == "dot" {
buf.Merge(lc.renderDot())
} else {
buf.Merge(lc.renderBraille())
}
return buf
}

View File

@ -1,11 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build !windows
package termui
const VDASH = '┊'
const HDASH = '┈'
const ORIGIN = '└'

View File

@ -1,11 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build windows
package termui
const VDASH = '|'
const HDASH = '-'
const ORIGIN = '+'

View File

@ -1,89 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "strings"
// List displays []string as its items,
// it has a Overflow option (default is "hidden"), when set to "hidden",
// the item exceeding List's width is truncated, but when set to "wrap",
// the overflowed text breaks into next line.
/*
strs := []string{
"[0] github.com/gizak/termui",
"[1] editbox.go",
"[2] interrupt.go",
"[3] keyboard.go",
"[4] output.go",
"[5] random_out.go",
"[6] dashboard.go",
"[7] nsf/termbox-go"}
ls := termui.NewList()
ls.Items = strs
ls.ItemFgColor = termui.ColorYellow
ls.BorderLabel = "List"
ls.Height = 7
ls.Width = 25
ls.Y = 0
*/
type List struct {
Block
Items []string
Overflow string
ItemFgColor Attribute
ItemBgColor Attribute
}
// NewList returns a new *List with current theme.
func NewList() *List {
l := &List{Block: *NewBlock()}
l.Overflow = "hidden"
l.ItemFgColor = ThemeAttr("list.item.fg")
l.ItemBgColor = ThemeAttr("list.item.bg")
return l
}
// Buffer implements Bufferer interface.
func (l *List) Buffer() Buffer {
buf := l.Block.Buffer()
switch l.Overflow {
case "wrap":
cs := DefaultTxBuilder.Build(strings.Join(l.Items, "\n"), l.ItemFgColor, l.ItemBgColor)
i, j, k := 0, 0, 0
for i < l.innerArea.Dy() && k < len(cs) {
w := cs[k].Width()
if cs[k].Ch == '\n' || j+w > l.innerArea.Dx() {
i++
j = 0
if cs[k].Ch == '\n' {
k++
}
continue
}
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, cs[k])
k++
j++
}
case "hidden":
trimItems := l.Items
if len(trimItems) > l.innerArea.Dy() {
trimItems = trimItems[:l.innerArea.Dy()]
}
for i, v := range trimItems {
cs := DTrimTxCls(DefaultTxBuilder.Build(v, l.ItemFgColor, l.ItemBgColor), l.innerArea.Dx())
j := 0
for _, vv := range cs {
w := vv.Width()
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, vv)
j += w
}
}
}
return buf
}

View File

@ -1,242 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"fmt"
)
// This is the implementation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go
// Multi-Colored-BarChart creates multiple bars in a widget:
/*
bc := termui.NewMBarChart()
data := make([][]int, 2)
data[0] := []int{3, 2, 5, 7, 9, 4}
data[1] := []int{7, 8, 5, 3, 1, 6}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.BorderLabel = "Bar Chart"
bc.Data = data
bc.Width = 26
bc.Height = 10
bc.DataLabels = bclabels
bc.TextColor = termui.ColorGreen
bc.BarColor = termui.ColorRed
bc.NumColor = termui.ColorYellow
*/
type MBarChart struct {
Block
BarColor [NumberofColors]Attribute
TextColor Attribute
NumColor [NumberofColors]Attribute
Data [NumberofColors][]int
DataLabels []string
BarWidth int
BarGap int
labels [][]rune
dataNum [NumberofColors][][]rune
numBar int
scale float64
max int
minDataLen int
numStack int
ShowScale bool
maxScale []rune
}
// NewBarChart returns a new *BarChart with current theme.
func NewMBarChart() *MBarChart {
bc := &MBarChart{Block: *NewBlock()}
bc.BarColor[0] = ThemeAttr("mbarchart.bar.bg")
bc.NumColor[0] = ThemeAttr("mbarchart.num.fg")
bc.TextColor = ThemeAttr("mbarchart.text.fg")
bc.BarGap = 1
bc.BarWidth = 3
return bc
}
func (bc *MBarChart) layout() {
bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
DataLen := 0
LabelLen := len(bc.DataLabels)
bc.minDataLen = 9999 //Set this to some very hight value so that we find the minimum one We want to know which array among data[][] has got the least length
// We need to know how many stack/data array data[0] , data[1] are there
for i := 0; i < len(bc.Data); i++ {
if bc.Data[i] == nil {
break
}
DataLen++
}
bc.numStack = DataLen
//We need to know what is the minimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs
for i := 0; i < DataLen; i++ {
if bc.minDataLen > len(bc.Data[i]) {
bc.minDataLen = len(bc.Data[i])
}
}
if LabelLen > bc.minDataLen {
LabelLen = bc.minDataLen
}
for i := 0; i < LabelLen && i < bc.numBar; i++ {
bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth)
}
for i := 0; i < bc.numStack; i++ {
bc.dataNum[i] = make([][]rune, len(bc.Data[i]))
//For each stack of bar calculate the rune
for j := 0; j < LabelLen && i < bc.numBar; j++ {
n := bc.Data[i][j]
s := fmt.Sprint(n)
bc.dataNum[i][j] = trimStr2Runes(s, bc.BarWidth)
}
//If color is not defined by default then populate a color that is different from the previous bar
if bc.BarColor[i] == ColorDefault && bc.NumColor[i] == ColorDefault {
if i == 0 {
bc.BarColor[i] = ColorBlack
} else {
bc.BarColor[i] = bc.BarColor[i-1] + 1
if bc.BarColor[i] > NumberofColors {
bc.BarColor[i] = ColorBlack
}
}
bc.NumColor[i] = (NumberofColors + 1) - bc.BarColor[i] //Make NumColor opposite of barColor for visibility
}
}
//If Max value is not set then we have to populate, this time the max value will be max(sum(d1[0],d2[0],d3[0]) .... sum(d1[n], d2[n], d3[n]))
if bc.max == 0 {
bc.max = -1
}
for i := 0; i < bc.minDataLen && i < LabelLen; i++ {
var dsum int
for j := 0; j < bc.numStack; j++ {
dsum += bc.Data[j][i]
}
if dsum > bc.max {
bc.max = dsum
}
}
//Finally Calculate max sale
if bc.ShowScale {
s := fmt.Sprintf("%d", bc.max)
bc.maxScale = trimStr2Runes(s, len(s))
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-2)
} else {
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
}
}
func (bc *MBarChart) SetMax(max int) {
if max > 0 {
bc.max = max
}
}
// Buffer implements Bufferer interface.
func (bc *MBarChart) Buffer() Buffer {
buf := bc.Block.Buffer()
bc.layout()
var oftX int
for i := 0; i < bc.numBar && i < bc.minDataLen && i < len(bc.DataLabels); i++ {
ph := 0 //Previous Height to stack up
oftX = i * (bc.BarWidth + bc.BarGap)
for i1 := 0; i1 < bc.numStack; i1++ {
h := int(float64(bc.Data[i1][i]) / bc.scale)
// plot bars
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
c := Cell{
Ch: ' ',
Bg: bc.BarColor[i1],
}
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
c.Bg |= AttrReverse
}
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k - ph
buf.Set(x, y, c)
}
}
ph += h
}
// plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
c := Cell{
Ch: bc.labels[i][j],
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
x := bc.innerArea.Max.X + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
buf.Set(x, y, c)
k += w
}
// plot num
ph = 0 //re-initialize previous height
for i1 := 0; i1 < bc.numStack; i1++ {
h := int(float64(bc.Data[i1][i]) / bc.scale)
for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ {
c := Cell{
Ch: bc.dataNum[i1][i][j],
Fg: bc.NumColor[i1],
Bg: bc.BarColor[i1],
}
if bc.BarColor[i1] == ColorDefault { // the same as above
c.Bg |= AttrReverse
}
if h == 0 {
c.Bg = bc.Bg
}
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - ph
buf.Set(x, y, c)
}
ph += h
}
}
if bc.ShowScale {
//Currently bar graph only supprts data range from 0 to MAX
//Plot 0
c := Cell{
Ch: '0',
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
x := bc.X
buf.Set(x, y, c)
//Plot the maximum sacle value
for i := 0; i < len(bc.maxScale); i++ {
c := Cell{
Ch: bc.maxScale[i],
Bg: bc.Bg,
Fg: bc.TextColor,
}
y := bc.innerArea.Min.Y
x := bc.X + i
buf.Set(x, y, c)
}
}
return buf
}

View File

@ -1,28 +0,0 @@
pages:
- Home: 'index.md'
- Quickstart: 'quickstart.md'
- Recipes: 'recipes.md'
- References:
- Layouts: 'layouts.md'
- Components: 'components.md'
- Events: 'events.md'
- Themes: 'themes.md'
- Versions: 'versions.md'
- About: 'about.md'
site_name: termui
repo_url: https://github.com/gizak/termui/
site_description: 'termui user guide'
site_author: gizak
docs_dir: '_docs'
theme: readthedocs
markdown_extensions:
- smarty
- admonition
- toc
extra:
version: 1.0

View File

@ -1,73 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
// Par displays a paragraph.
/*
par := termui.NewPar("Simple Text")
par.Height = 3
par.Width = 17
par.BorderLabel = "Label"
*/
type Par struct {
Block
Text string
TextFgColor Attribute
TextBgColor Attribute
WrapLength int // words wrap limit. Note it may not work properly with multi-width char
}
// NewPar returns a new *Par with given text as its content.
func NewPar(s string) *Par {
return &Par{
Block: *NewBlock(),
Text: s,
TextFgColor: ThemeAttr("par.text.fg"),
TextBgColor: ThemeAttr("par.text.bg"),
WrapLength: 0,
}
}
// Buffer implements Bufferer interface.
func (p *Par) Buffer() Buffer {
buf := p.Block.Buffer()
fg, bg := p.TextFgColor, p.TextBgColor
cs := DefaultTxBuilder.Build(p.Text, fg, bg)
// wrap if WrapLength set
if p.WrapLength < 0 {
cs = wrapTx(cs, p.Width-2)
} else if p.WrapLength > 0 {
cs = wrapTx(cs, p.WrapLength)
}
y, x, n := 0, 0, 0
for y < p.innerArea.Dy() && n < len(cs) {
w := cs[n].Width()
if cs[n].Ch == '\n' || x+w > p.innerArea.Dx() {
y++
x = 0 // set x = 0
if cs[n].Ch == '\n' {
n++
}
if y >= p.innerArea.Dy() {
buf.Set(p.innerArea.Min.X+p.innerArea.Dx()-1,
p.innerArea.Min.Y+p.innerArea.Dy()-1,
Cell{Ch: '…', Fg: p.TextFgColor, Bg: p.TextBgColor})
break
}
continue
}
buf.Set(p.innerArea.Min.X+x, p.innerArea.Min.Y+y, cs[n])
n++
x += w
}
return buf
}

View File

@ -1,78 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "image"
// Align is the position of the gauge's label.
type Align uint
// All supported positions.
const (
AlignNone Align = 0
AlignLeft Align = 1 << iota
AlignRight
AlignBottom
AlignTop
AlignCenterVertical
AlignCenterHorizontal
AlignCenter = AlignCenterVertical | AlignCenterHorizontal
)
func AlignArea(parent, child image.Rectangle, a Align) image.Rectangle {
w, h := child.Dx(), child.Dy()
// parent center
pcx, pcy := parent.Min.X+parent.Dx()/2, parent.Min.Y+parent.Dy()/2
// child center
ccx, ccy := child.Min.X+child.Dx()/2, child.Min.Y+child.Dy()/2
if a&AlignLeft == AlignLeft {
child.Min.X = parent.Min.X
child.Max.X = child.Min.X + w
}
if a&AlignRight == AlignRight {
child.Max.X = parent.Max.X
child.Min.X = child.Max.X - w
}
if a&AlignBottom == AlignBottom {
child.Max.Y = parent.Max.Y
child.Min.Y = child.Max.Y - h
}
if a&AlignTop == AlignRight {
child.Min.Y = parent.Min.Y
child.Max.Y = child.Min.Y + h
}
if a&AlignCenterHorizontal == AlignCenterHorizontal {
child.Min.X += pcx - ccx
child.Max.X = child.Min.X + w
}
if a&AlignCenterVertical == AlignCenterVertical {
child.Min.Y += pcy - ccy
child.Max.Y = child.Min.Y + h
}
return child
}
func MoveArea(a image.Rectangle, dx, dy int) image.Rectangle {
a.Min.X += dx
a.Max.X += dx
a.Min.Y += dy
a.Max.Y += dy
return a
}
var termWidth int
var termHeight int
func TermRect() image.Rectangle {
return image.Rect(0, 0, termWidth, termHeight)
}

View File

@ -1,164 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"image"
"io"
"sync"
"time"
"fmt"
"os"
"runtime/debug"
"bytes"
"github.com/maruel/panicparse/stack"
tm "github.com/nsf/termbox-go"
)
// Bufferer should be implemented by all renderable components.
type Bufferer interface {
Buffer() Buffer
}
// Init initializes termui library. This function should be called before any others.
// After initialization, the library must be finalized by 'Close' function.
func Init() error {
if err := tm.Init(); err != nil {
return err
}
sysEvtChs = make([]chan Event, 0)
// go hookTermboxEvt()
renderJobs = make(chan []Bufferer)
//renderLock = new(sync.RWMutex)
Body = NewGrid()
Body.X = 0
Body.Y = 0
Body.BgColor = ThemeAttr("bg")
Body.Width = TermWidth()
DefaultEvtStream.Init()
DefaultEvtStream.Merge("termbox", NewSysEvtCh())
DefaultEvtStream.Merge("timer", NewTimerCh(time.Second))
DefaultEvtStream.Merge("custom", usrEvtCh)
DefaultEvtStream.Handle("/", DefaultHandler)
DefaultEvtStream.Handle("/sys/wnd/resize", func(e Event) {
w := e.Data.(EvtWnd)
Body.Width = w.Width
})
DefaultWgtMgr = NewWgtMgr()
DefaultEvtStream.Hook(DefaultWgtMgr.WgtHandlersHook())
go func() {
for bs := range renderJobs {
render(bs...)
}
}()
return nil
}
// Close finalizes termui library,
// should be called after successful initialization when termui's functionality isn't required anymore.
func Close() {
tm.Close()
}
var renderLock sync.Mutex
func termSync() {
renderLock.Lock()
tm.Sync()
termWidth, termHeight = tm.Size()
renderLock.Unlock()
}
// TermWidth returns the current terminal's width.
func TermWidth() int {
termSync()
return termWidth
}
// TermHeight returns the current terminal's height.
func TermHeight() int {
termSync()
return termHeight
}
// Render renders all Bufferer in the given order from left to right,
// right could overlap on left ones.
func render(bs ...Bufferer) {
defer func() {
if e := recover(); e != nil {
Close()
fmt.Fprintf(os.Stderr, "Captured a panic(value=%v) when rendering Bufferer. Exit termui and clean terminal...\nPrint stack trace:\n\n", e)
//debug.PrintStack()
gs, err := stack.ParseDump(bytes.NewReader(debug.Stack()), os.Stderr)
if err != nil {
debug.PrintStack()
os.Exit(1)
}
p := &stack.Palette{}
buckets := stack.SortBuckets(stack.Bucketize(gs, stack.AnyValue))
srcLen, pkgLen := stack.CalcLengths(buckets, false)
for _, bucket := range buckets {
io.WriteString(os.Stdout, p.BucketHeader(&bucket, false, len(buckets) > 1))
io.WriteString(os.Stdout, p.StackLines(&bucket.Signature, srcLen, pkgLen, false))
}
os.Exit(1)
}
}()
for _, b := range bs {
buf := b.Buffer()
// set cels in buf
for p, c := range buf.CellMap {
if p.In(buf.Area) {
tm.SetCell(p.X, p.Y, c.Ch, toTmAttr(c.Fg), toTmAttr(c.Bg))
}
}
}
renderLock.Lock()
// render
tm.Flush()
renderLock.Unlock()
}
func Clear() {
tm.Clear(tm.ColorDefault, toTmAttr(ThemeAttr("bg")))
}
func clearArea(r image.Rectangle, bg Attribute) {
for i := r.Min.X; i < r.Max.X; i++ {
for j := r.Min.Y; j < r.Max.Y; j++ {
tm.SetCell(i, j, ' ', tm.ColorDefault, toTmAttr(bg))
}
}
}
func ClearArea(r image.Rectangle, bg Attribute) {
clearArea(r, bg)
tm.Flush()
}
var renderJobs chan []Bufferer
func Render(bs ...Bufferer) {
//go func() { renderJobs <- bs }()
renderJobs <- bs
}

View File

@ -1,167 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
/*
data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1}
spl := termui.NewSparkline()
spl.Data = data
spl.Title = "Sparkline 0"
spl.LineColor = termui.ColorGreen
*/
type Sparkline struct {
Data []int
Height int
Title string
TitleColor Attribute
LineColor Attribute
displayHeight int
scale float32
max int
}
// Sparklines is a renderable widget which groups together the given sparklines.
/*
spls := termui.NewSparklines(spl0,spl1,spl2) //...
spls.Height = 2
spls.Width = 20
*/
type Sparklines struct {
Block
Lines []Sparkline
displayLines int
displayWidth int
}
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
// Add appends a given Sparkline to s *Sparklines.
func (s *Sparklines) Add(sl Sparkline) {
s.Lines = append(s.Lines, sl)
}
// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines.
func NewSparkline() Sparkline {
return Sparkline{
Height: 1,
TitleColor: ThemeAttr("sparkline.title.fg"),
LineColor: ThemeAttr("sparkline.line.fg")}
}
// NewSparklines return a new *Sparklines with given Sparkline(s), you can always add a new Sparkline later.
func NewSparklines(ss ...Sparkline) *Sparklines {
s := &Sparklines{Block: *NewBlock(), Lines: ss}
return s
}
func (sl *Sparklines) update() {
for i, v := range sl.Lines {
if v.Title == "" {
sl.Lines[i].displayHeight = v.Height
} else {
sl.Lines[i].displayHeight = v.Height + 1
}
}
sl.displayWidth = sl.innerArea.Dx()
// get how many lines gotta display
h := 0
sl.displayLines = 0
for _, v := range sl.Lines {
if h+v.displayHeight <= sl.innerArea.Dy() {
sl.displayLines++
} else {
break
}
h += v.displayHeight
}
for i := 0; i < sl.displayLines; i++ {
data := sl.Lines[i].Data
max := 0
for _, v := range data {
if max < v {
max = v
}
}
sl.Lines[i].max = max
if max != 0 {
sl.Lines[i].scale = float32(8*sl.Lines[i].Height) / float32(max)
} else { // when all negative
sl.Lines[i].scale = 0
}
}
}
// Buffer implements Bufferer interface.
func (sl *Sparklines) Buffer() Buffer {
buf := sl.Block.Buffer()
sl.update()
oftY := 0
for i := 0; i < sl.displayLines; i++ {
l := sl.Lines[i]
data := l.Data
if len(data) > sl.innerArea.Dx() {
data = data[len(data)-sl.innerArea.Dx():]
}
if l.Title != "" {
rs := trimStr2Runes(l.Title, sl.innerArea.Dx())
oftX := 0
for _, v := range rs {
w := charWidth(v)
c := Cell{
Ch: v,
Fg: l.TitleColor,
Bg: sl.Bg,
}
x := sl.innerArea.Min.X + oftX
y := sl.innerArea.Min.Y + oftY
buf.Set(x, y, c)
oftX += w
}
}
for j, v := range data {
// display height of the data point, zero when data is negative
h := int(float32(v)*l.scale + 0.5)
if v < 0 {
h = 0
}
barCnt := h / 8
barMod := h % 8
for jj := 0; jj < barCnt; jj++ {
c := Cell{
Ch: ' ', // => sparks[7]
Bg: l.LineColor,
}
x := sl.innerArea.Min.X + j
y := sl.innerArea.Min.Y + oftY + l.Height - jj
//p.Bg = sl.BgColor
buf.Set(x, y, c)
}
if barMod != 0 {
c := Cell{
Ch: sparks[barMod-1],
Fg: l.LineColor,
Bg: sl.Bg,
}
x := sl.innerArea.Min.X + j
y := sl.innerArea.Min.Y + oftY + l.Height - barCnt
buf.Set(x, y, c)
}
}
oftY += l.displayHeight
}
return buf
}

View File

@ -1,185 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "strings"
/* Table is like:
Awesome Table
Col0 | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 |
Some Item #1 | AAA | 123 | CCCCC | EEEEE | GGGGG | IIIII |
Some Item #2 | BBB | 456 | DDDDD | FFFFF | HHHHH | JJJJJ |
Datapoints are a two dimensional array of strings: [][]string
Example:
data := [][]string{
{"Col0", "Col1", "Col3", "Col4", "Col5", "Col6"},
{"Some Item #1", "AAA", "123", "CCCCC", "EEEEE", "GGGGG", "IIIII"},
{"Some Item #2", "BBB", "456", "DDDDD", "FFFFF", "HHHHH", "JJJJJ"},
}
table := termui.NewTable()
table.Rows = data // type [][]string
table.FgColor = termui.ColorWhite
table.BgColor = termui.ColorDefault
table.Height = 7
table.Width = 62
table.Y = 0
table.X = 0
table.Border = true
*/
// Table tracks all the attributes of a Table instance
type Table struct {
Block
Rows [][]string
CellWidth []int
FgColor Attribute
BgColor Attribute
FgColors []Attribute
BgColors []Attribute
Separator bool
TextAlign Align
}
// NewTable returns a new Table instance
func NewTable() *Table {
table := &Table{Block: *NewBlock()}
table.FgColor = ColorWhite
table.BgColor = ColorDefault
table.Separator = true
return table
}
// CellsWidth calculates the width of a cell array and returns an int
func cellsWidth(cells []Cell) int {
width := 0
for _, c := range cells {
width += c.Width()
}
return width
}
// Analysis generates and returns an array of []Cell that represent all columns in the Table
func (table *Table) Analysis() [][]Cell {
var rowCells [][]Cell
length := len(table.Rows)
if length < 1 {
return rowCells
}
if len(table.FgColors) == 0 {
table.FgColors = make([]Attribute, len(table.Rows))
}
if len(table.BgColors) == 0 {
table.BgColors = make([]Attribute, len(table.Rows))
}
cellWidths := make([]int, len(table.Rows[0]))
for y, row := range table.Rows {
if table.FgColors[y] == 0 {
table.FgColors[y] = table.FgColor
}
if table.BgColors[y] == 0 {
table.BgColors[y] = table.BgColor
}
for x, str := range row {
cells := DefaultTxBuilder.Build(str, table.FgColors[y], table.BgColors[y])
cw := cellsWidth(cells)
if cellWidths[x] < cw {
cellWidths[x] = cw
}
rowCells = append(rowCells, cells)
}
}
table.CellWidth = cellWidths
return rowCells
}
// SetSize calculates the table size and sets the internal value
func (table *Table) SetSize() {
length := len(table.Rows)
if table.Separator {
table.Height = length*2 + 1
} else {
table.Height = length + 2
}
table.Width = 2
if length != 0 {
for _, cellWidth := range table.CellWidth {
table.Width += cellWidth + 3
}
}
}
// CalculatePosition ...
func (table *Table) CalculatePosition(x int, y int, coordinateX *int, coordinateY *int, cellStart *int) {
if table.Separator {
*coordinateY = table.innerArea.Min.Y + y*2
} else {
*coordinateY = table.innerArea.Min.Y + y
}
if x == 0 {
*cellStart = table.innerArea.Min.X
} else {
*cellStart += table.CellWidth[x-1] + 3
}
switch table.TextAlign {
case AlignRight:
*coordinateX = *cellStart + (table.CellWidth[x] - len(table.Rows[y][x])) + 2
case AlignCenter:
*coordinateX = *cellStart + (table.CellWidth[x]-len(table.Rows[y][x]))/2 + 2
default:
*coordinateX = *cellStart + 2
}
}
// Buffer ...
func (table *Table) Buffer() Buffer {
buffer := table.Block.Buffer()
rowCells := table.Analysis()
pointerX := table.innerArea.Min.X + 2
pointerY := table.innerArea.Min.Y
borderPointerX := table.innerArea.Min.X
for y, row := range table.Rows {
for x := range row {
table.CalculatePosition(x, y, &pointerX, &pointerY, &borderPointerX)
background := DefaultTxBuilder.Build(strings.Repeat(" ", table.CellWidth[x]+3), table.BgColors[y], table.BgColors[y])
cells := rowCells[y*len(row)+x]
for i, back := range background {
buffer.Set(borderPointerX+i, pointerY, back)
}
coordinateX := pointerX
for _, printer := range cells {
buffer.Set(coordinateX, pointerY, printer)
coordinateX += printer.Width()
}
if x != 0 {
dividors := DefaultTxBuilder.Build("|", table.FgColors[y], table.BgColors[y])
for _, dividor := range dividors {
buffer.Set(borderPointerX, pointerY, dividor)
}
}
}
if table.Separator {
border := DefaultTxBuilder.Build(strings.Repeat("─", table.Width-2), table.FgColor, table.BgColor)
for i, cell := range border {
buffer.Set(i+1, pointerY+1, cell)
}
}
}
return buffer
}

View File

@ -1,278 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"regexp"
"strings"
"github.com/mitchellh/go-wordwrap"
)
// TextBuilder is a minimal interface to produce text []Cell using specific syntax (markdown).
type TextBuilder interface {
Build(s string, fg, bg Attribute) []Cell
}
// DefaultTxBuilder is set to be MarkdownTxBuilder.
var DefaultTxBuilder = NewMarkdownTxBuilder()
// MarkdownTxBuilder implements TextBuilder interface, using markdown syntax.
type MarkdownTxBuilder struct {
baseFg Attribute
baseBg Attribute
plainTx []rune
markers []marker
}
type marker struct {
st int
ed int
fg Attribute
bg Attribute
}
var colorMap = map[string]Attribute{
"red": ColorRed,
"blue": ColorBlue,
"black": ColorBlack,
"cyan": ColorCyan,
"yellow": ColorYellow,
"white": ColorWhite,
"default": ColorDefault,
"green": ColorGreen,
"magenta": ColorMagenta,
}
var attrMap = map[string]Attribute{
"bold": AttrBold,
"underline": AttrUnderline,
"reverse": AttrReverse,
}
func rmSpc(s string) string {
reg := regexp.MustCompile(`\s+`)
return reg.ReplaceAllString(s, "")
}
// readAttr translates strings like `fg-red,fg-bold,bg-white` to fg and bg Attribute
func (mtb MarkdownTxBuilder) readAttr(s string) (Attribute, Attribute) {
fg := mtb.baseFg
bg := mtb.baseBg
updateAttr := func(a Attribute, attrs []string) Attribute {
for _, s := range attrs {
// replace the color
if c, ok := colorMap[s]; ok {
a &= 0xFF00 // erase clr 0 ~ 8 bits
a |= c // set clr
}
// add attrs
if c, ok := attrMap[s]; ok {
a |= c
}
}
return a
}
ss := strings.Split(s, ",")
fgs := []string{}
bgs := []string{}
for _, v := range ss {
subs := strings.Split(v, "-")
if len(subs) > 1 {
if subs[0] == "fg" {
fgs = append(fgs, subs[1])
}
if subs[0] == "bg" {
bgs = append(bgs, subs[1])
}
}
}
fg = updateAttr(fg, fgs)
bg = updateAttr(bg, bgs)
return fg, bg
}
func (mtb *MarkdownTxBuilder) reset() {
mtb.plainTx = []rune{}
mtb.markers = []marker{}
}
// parse streams and parses text into normalized text and render sequence.
func (mtb *MarkdownTxBuilder) parse(str string) {
rs := str2runes(str)
normTx := []rune{}
square := []rune{}
brackt := []rune{}
accSquare := false
accBrackt := false
cntSquare := 0
reset := func() {
square = []rune{}
brackt = []rune{}
accSquare = false
accBrackt = false
cntSquare = 0
}
// pipe stacks into normTx and clear
rollback := func() {
normTx = append(normTx, square...)
normTx = append(normTx, brackt...)
reset()
}
// chop first and last
chop := func(s []rune) []rune {
return s[1 : len(s)-1]
}
for i, r := range rs {
switch {
// stacking brackt
case accBrackt:
brackt = append(brackt, r)
if ')' == r {
fg, bg := mtb.readAttr(string(chop(brackt)))
st := len(normTx)
ed := len(normTx) + len(square) - 2
mtb.markers = append(mtb.markers, marker{st, ed, fg, bg})
normTx = append(normTx, chop(square)...)
reset()
} else if i+1 == len(rs) {
rollback()
}
// stacking square
case accSquare:
switch {
// squares closed and followed by a '('
case cntSquare == 0 && '(' == r:
accBrackt = true
brackt = append(brackt, '(')
// squares closed but not followed by a '('
case cntSquare == 0:
rollback()
if '[' == r {
accSquare = true
cntSquare = 1
brackt = append(brackt, '[')
} else {
normTx = append(normTx, r)
}
// hit the end
case i+1 == len(rs):
square = append(square, r)
rollback()
case '[' == r:
cntSquare++
square = append(square, '[')
case ']' == r:
cntSquare--
square = append(square, ']')
// normal char
default:
square = append(square, r)
}
// stacking normTx
default:
if '[' == r {
accSquare = true
cntSquare = 1
square = append(square, '[')
} else {
normTx = append(normTx, r)
}
}
}
mtb.plainTx = normTx
}
func wrapTx(cs []Cell, wl int) []Cell {
tmpCell := make([]Cell, len(cs))
copy(tmpCell, cs)
// get the plaintext
plain := CellsToStr(cs)
// wrap
plainWrapped := wordwrap.WrapString(plain, uint(wl))
// find differences and insert
finalCell := tmpCell // finalcell will get the inserts and is what is returned
plainRune := []rune(plain)
plainWrappedRune := []rune(plainWrapped)
trigger := "go"
plainRuneNew := plainRune
for trigger != "stop" {
plainRune = plainRuneNew
for i := range plainRune {
if plainRune[i] == plainWrappedRune[i] {
trigger = "stop"
} else if plainRune[i] != plainWrappedRune[i] && plainWrappedRune[i] == 10 {
trigger = "go"
cell := Cell{10, 0, 0}
j := i - 0
// insert a cell into the []Cell in correct position
tmpCell[i] = cell
// insert the newline into plain so we avoid indexing errors
plainRuneNew = append(plainRune, 10)
copy(plainRuneNew[j+1:], plainRuneNew[j:])
plainRuneNew[j] = plainWrappedRune[j]
// restart the inner for loop until plain and plain wrapped are
// the same; yeah, it's inefficient, but the text amounts
// should be small
break
} else if plainRune[i] != plainWrappedRune[i] &&
plainWrappedRune[i-1] == 10 && // if the prior rune is a newline
plainRune[i] == 32 { // and this rune is a space
trigger = "go"
// need to delete plainRune[i] because it gets rid of an extra
// space
plainRuneNew = append(plainRune[:i], plainRune[i+1:]...)
break
} else {
trigger = "stop" // stops the outer for loop
}
}
}
finalCell = tmpCell
return finalCell
}
// Build implements TextBuilder interface.
func (mtb MarkdownTxBuilder) Build(s string, fg, bg Attribute) []Cell {
mtb.baseFg = fg
mtb.baseBg = bg
mtb.reset()
mtb.parse(s)
cs := make([]Cell, len(mtb.plainTx))
for i := range cs {
cs[i] = Cell{Ch: mtb.plainTx[i], Fg: fg, Bg: bg}
}
for _, mrk := range mtb.markers {
for i := mrk.st; i < mrk.ed; i++ {
cs[i].Fg = mrk.fg
cs[i].Bg = mrk.bg
}
}
return cs
}
// NewMarkdownTxBuilder returns a TextBuilder employing markdown syntax.
func NewMarkdownTxBuilder() TextBuilder {
return MarkdownTxBuilder{}
}

View File

@ -1,140 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "strings"
/*
// A ColorScheme represents the current look-and-feel of the dashboard.
type ColorScheme struct {
BodyBg Attribute
BlockBg Attribute
HasBorder bool
BorderFg Attribute
BorderBg Attribute
BorderLabelTextFg Attribute
BorderLabelTextBg Attribute
ParTextFg Attribute
ParTextBg Attribute
SparklineLine Attribute
SparklineTitle Attribute
GaugeBar Attribute
GaugePercent Attribute
LineChartLine Attribute
LineChartAxes Attribute
ListItemFg Attribute
ListItemBg Attribute
BarChartBar Attribute
BarChartText Attribute
BarChartNum Attribute
MBarChartBar Attribute
MBarChartText Attribute
MBarChartNum Attribute
TabActiveBg Attribute
}
// default color scheme depends on the user's terminal setting.
var themeDefault = ColorScheme{HasBorder: true}
var themeHelloWorld = ColorScheme{
BodyBg: ColorBlack,
BlockBg: ColorBlack,
HasBorder: true,
BorderFg: ColorWhite,
BorderBg: ColorBlack,
BorderLabelTextBg: ColorBlack,
BorderLabelTextFg: ColorGreen,
ParTextBg: ColorBlack,
ParTextFg: ColorWhite,
SparklineLine: ColorMagenta,
SparklineTitle: ColorWhite,
GaugeBar: ColorRed,
GaugePercent: ColorWhite,
LineChartLine: ColorYellow | AttrBold,
LineChartAxes: ColorWhite,
ListItemBg: ColorBlack,
ListItemFg: ColorYellow,
BarChartBar: ColorRed,
BarChartNum: ColorWhite,
BarChartText: ColorCyan,
MBarChartBar: ColorRed,
MBarChartNum: ColorWhite,
MBarChartText: ColorCyan,
TabActiveBg: ColorMagenta,
}
var theme = themeDefault // global dep
// Theme returns the currently used theme.
func Theme() ColorScheme {
return theme
}
// SetTheme sets a new, custom theme.
func SetTheme(newTheme ColorScheme) {
theme = newTheme
}
// UseTheme sets a predefined scheme. Currently available: "hello-world" and
// "black-and-white".
func UseTheme(th string) {
switch th {
case "helloworld":
theme = themeHelloWorld
default:
theme = themeDefault
}
}
*/
var ColorMap = map[string]Attribute{
"fg": ColorWhite,
"bg": ColorDefault,
"border.fg": ColorWhite,
"label.fg": ColorGreen,
"par.fg": ColorYellow,
"par.label.bg": ColorWhite,
}
func ThemeAttr(name string) Attribute {
return lookUpAttr(ColorMap, name)
}
func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
a, ok := clrmap[name]
if ok {
return a
}
ns := strings.Split(name, ".")
for i := range ns {
nn := strings.Join(ns[i:len(ns)], ".")
a, ok = ColorMap[nn]
if ok {
break
}
}
return a
}
// 0<=r,g,b <= 5
func ColorRGB(r, g, b int) Attribute {
within := func(n int) int {
if n < 0 {
return 0
}
if n > 5 {
return 5
}
return n
}
r, b, g = within(r), within(b), within(g)
return Attribute(0x0f + 36*r + 6*g + b)
}

View File

@ -1,94 +0,0 @@
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"fmt"
"sync"
)
// event mixins
type WgtMgr map[string]WgtInfo
type WgtInfo struct {
Handlers map[string]func(Event)
WgtRef Widget
Id string
}
type Widget interface {
Id() string
}
func NewWgtInfo(wgt Widget) WgtInfo {
return WgtInfo{
Handlers: make(map[string]func(Event)),
WgtRef: wgt,
Id: wgt.Id(),
}
}
func NewWgtMgr() WgtMgr {
wm := WgtMgr(make(map[string]WgtInfo))
return wm
}
func (wm WgtMgr) AddWgt(wgt Widget) {
wm[wgt.Id()] = NewWgtInfo(wgt)
}
func (wm WgtMgr) RmWgt(wgt Widget) {
wm.RmWgtById(wgt.Id())
}
func (wm WgtMgr) RmWgtById(id string) {
delete(wm, id)
}
func (wm WgtMgr) AddWgtHandler(id, path string, h func(Event)) {
if w, ok := wm[id]; ok {
w.Handlers[path] = h
}
}
func (wm WgtMgr) RmWgtHandler(id, path string) {
if w, ok := wm[id]; ok {
delete(w.Handlers, path)
}
}
var counter struct {
sync.RWMutex
count int
}
func GenId() string {
counter.Lock()
defer counter.Unlock()
counter.count += 1
return fmt.Sprintf("%d", counter.count)
}
func (wm WgtMgr) WgtHandlersHook() func(Event) {
return func(e Event) {
for _, v := range wm {
if k := findMatch(v.Handlers, e.Path); k != "" {
v.Handlers[k](e)
}
}
}
}
var DefaultWgtMgr WgtMgr
func (b *Block) Handle(path string, handler func(Event)) {
if _, ok := DefaultWgtMgr[b.Id()]; !ok {
DefaultWgtMgr.AddWgt(b)
}
DefaultWgtMgr.AddWgtHandler(b.Id(), path, handler)
}

View File

@ -1,25 +0,0 @@
# 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
.idea/
*.iml

View File

@ -1,9 +0,0 @@
# This is the official list of Gorilla WebSocket authors for copyright
# purposes.
#
# Please keep the list sorted.
Gary Burd <gary@beagledreams.com>
Google LLC (https://opensource.google.com/)
Joachim Bauch <mail@joachim-bauch.de>

View File

@ -1,22 +0,0 @@
Copyright (c) 2013 The Gorilla WebSocket Authors. 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.
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.

View File

@ -1,64 +0,0 @@
# Gorilla WebSocket
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket)
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
### Documentation
* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
### Status
The Gorilla WebSocket package provides a complete and tested implementation of
the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
package API is stable.
### Installation
go get github.com/gorilla/websocket
### Protocol Compliance
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
### Gorilla WebSocket compared with other packages
<table>
<tr>
<th></th>
<th><a href="http://godoc.org/github.com/gorilla/websocket">github.com/gorilla</a></th>
<th><a href="http://godoc.org/golang.org/x/net/websocket">golang.org/x/net</a></th>
</tr>
<tr>
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
<tr><td>Passes <a href="https://github.com/crossbario/autobahn-testsuite">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>
<tr><td>Get the <a href="https://tools.ietf.org/html/rfc6455#section-5.6">type</a> of a received data message</td><td>Yes</td><td>Yes, see note 2</td></tr>
<tr><td colspan="3">Other Features</tr></td>
<tr><td><a href="https://tools.ietf.org/html/rfc7692">Compression Extensions</a></td><td>Experimental</td><td>No</td></tr>
<tr><td>Read message using io.Reader</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextReader">Yes</a></td><td>No, see note 3</td></tr>
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
</table>
Notes:
1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
2. The application can get the type of a received data message by implementing
a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal)
function.
3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
Read returns when the input buffer is full or a frame boundary is
encountered. Each call to Write sends a single frame message. The Gorilla
io.Reader and io.WriteCloser operate on a single WebSocket message.

View File

@ -1,395 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bytes"
"context"
"crypto/tls"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"strings"
"time"
)
// ErrBadHandshake is returned when the server response to opening handshake is
// invalid.
var ErrBadHandshake = errors.New("websocket: bad handshake")
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
// NewClient creates a new client connection using the given net connection.
// The URL u specifies the host and request URI. Use requestHeader to specify
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
// (Cookie). Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etc.
//
// Deprecated: Use Dialer instead.
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
d := Dialer{
ReadBufferSize: readBufSize,
WriteBufferSize: writeBufSize,
NetDial: func(net, addr string) (net.Conn, error) {
return netConn, nil
},
}
return d.Dial(u.String(), requestHeader)
}
// A Dialer contains options for connecting to WebSocket server.
type Dialer struct {
// NetDial specifies the dial function for creating TCP connections. If
// NetDial is nil, net.Dial is used.
NetDial func(network, addr string) (net.Conn, error)
// NetDialContext specifies the dial function for creating TCP connections. If
// NetDialContext is nil, net.DialContext is used.
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
// If Proxy is nil or returns a nil *URL, no proxy is used.
Proxy func(*http.Request) (*url.URL, error)
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
// If nil, the default configuration is used.
TLSClientConfig *tls.Config
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// size is zero, then a useful default size is used. The I/O buffer sizes
// do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the client's requested subprotocols.
Subprotocols []string
// EnableCompression specifies if the client should attempt to negotiate
// per message compression (RFC 7692). Setting this value to true does not
// guarantee that compression will be supported. Currently only "no context
// takeover" modes are supported.
EnableCompression bool
// Jar specifies the cookie jar.
// If Jar is nil, cookies are not sent in requests and ignored
// in responses.
Jar http.CookieJar
}
// Dial creates a new client connection by calling DialContext with a background context.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
return d.DialContext(context.Background(), urlStr, requestHeader)
}
var errMalformedURL = errors.New("malformed ws or wss URL")
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
hostPort = u.Host
hostNoPort = u.Host
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
hostNoPort = hostNoPort[:i]
} else {
switch u.Scheme {
case "wss":
hostPort += ":443"
case "https":
hostPort += ":443"
default:
hostPort += ":80"
}
}
return hostPort, hostNoPort
}
// DefaultDialer is a dialer with all fields set to the default values.
var DefaultDialer = &Dialer{
Proxy: http.ProxyFromEnvironment,
HandshakeTimeout: 45 * time.Second,
}
// nilDialer is dialer to use when receiver is nil.
var nilDialer = *DefaultDialer
// DialContext creates a new client connection. Use requestHeader to specify the
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
// Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// The context will be used in the request and in the Dialer.
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etcetera. The response body may not contain the entire response and does not
// need to be closed by the application.
func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
if d == nil {
d = &nilDialer
}
challengeKey, err := generateChallengeKey()
if err != nil {
return nil, nil, err
}
u, err := url.Parse(urlStr)
if err != nil {
return nil, nil, err
}
switch u.Scheme {
case "ws":
u.Scheme = "http"
case "wss":
u.Scheme = "https"
default:
return nil, nil, errMalformedURL
}
if u.User != nil {
// User name and password are not allowed in websocket URIs.
return nil, nil, errMalformedURL
}
req := &http.Request{
Method: "GET",
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: u.Host,
}
req = req.WithContext(ctx)
// Set the cookies present in the cookie jar of the dialer
if d.Jar != nil {
for _, cookie := range d.Jar.Cookies(u) {
req.AddCookie(cookie)
}
}
// Set the request headers using the capitalization for names and values in
// RFC examples. Although the capitalization shouldn't matter, there are
// servers that depend on it. The Header.Set method is not used because the
// method canonicalizes the header names.
req.Header["Upgrade"] = []string{"websocket"}
req.Header["Connection"] = []string{"Upgrade"}
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
req.Header["Sec-WebSocket-Version"] = []string{"13"}
if len(d.Subprotocols) > 0 {
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
}
for k, vs := range requestHeader {
switch {
case k == "Host":
if len(vs) > 0 {
req.Host = vs[0]
}
case k == "Upgrade" ||
k == "Connection" ||
k == "Sec-Websocket-Key" ||
k == "Sec-Websocket-Version" ||
k == "Sec-Websocket-Extensions" ||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
case k == "Sec-Websocket-Protocol":
req.Header["Sec-WebSocket-Protocol"] = vs
default:
req.Header[k] = vs
}
}
if d.EnableCompression {
req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
}
if d.HandshakeTimeout != 0 {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
defer cancel()
}
// Get network dial function.
var netDial func(network, add string) (net.Conn, error)
if d.NetDialContext != nil {
netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialContext(ctx, network, addr)
}
} else if d.NetDial != nil {
netDial = d.NetDial
} else {
netDialer := &net.Dialer{}
netDial = func(network, addr string) (net.Conn, error) {
return netDialer.DialContext(ctx, network, addr)
}
}
// If needed, wrap the dial function to set the connection deadline.
if deadline, ok := ctx.Deadline(); ok {
forwardDial := netDial
netDial = func(network, addr string) (net.Conn, error) {
c, err := forwardDial(network, addr)
if err != nil {
return nil, err
}
err = c.SetDeadline(deadline)
if err != nil {
c.Close()
return nil, err
}
return c, nil
}
}
// If needed, wrap the dial function to connect through a proxy.
if d.Proxy != nil {
proxyURL, err := d.Proxy(req)
if err != nil {
return nil, nil, err
}
if proxyURL != nil {
dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial))
if err != nil {
return nil, nil, err
}
netDial = dialer.Dial
}
}
hostPort, hostNoPort := hostPortNoPort(u)
trace := httptrace.ContextClientTrace(ctx)
if trace != nil && trace.GetConn != nil {
trace.GetConn(hostPort)
}
netConn, err := netDial("tcp", hostPort)
if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{
Conn: netConn,
})
}
if err != nil {
return nil, nil, err
}
defer func() {
if netConn != nil {
netConn.Close()
}
}()
if u.Scheme == "https" {
cfg := cloneTLSConfig(d.TLSClientConfig)
if cfg.ServerName == "" {
cfg.ServerName = hostNoPort
}
tlsConn := tls.Client(netConn, cfg)
netConn = tlsConn
var err error
if trace != nil {
err = doHandshakeWithTrace(trace, tlsConn, cfg)
} else {
err = doHandshake(tlsConn, cfg)
}
if err != nil {
return nil, nil, err
}
}
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
if err := req.Write(netConn); err != nil {
return nil, nil, err
}
if trace != nil && trace.GotFirstResponseByte != nil {
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
trace.GotFirstResponseByte()
}
}
resp, err := http.ReadResponse(conn.br, req)
if err != nil {
return nil, nil, err
}
if d.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
d.Jar.SetCookies(u, rc)
}
}
if resp.StatusCode != 101 ||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
// Before closing the network connection on return from this
// function, slurp up some of the response to aid application
// debugging.
buf := make([]byte, 1024)
n, _ := io.ReadFull(resp.Body, buf)
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
return nil, resp, ErrBadHandshake
}
for _, ext := range parseExtensions(resp.Header) {
if ext[""] != "permessage-deflate" {
continue
}
_, snct := ext["server_no_context_takeover"]
_, cnct := ext["client_no_context_takeover"]
if !snct || !cnct {
return nil, resp, errInvalidCompression
}
conn.newCompressionWriter = compressNoContextTakeover
conn.newDecompressionReader = decompressNoContextTakeover
break
}
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
netConn.SetDeadline(time.Time{})
netConn = nil // to avoid close in defer.
return conn, resp, nil
}
func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.Handshake(); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

View File

@ -1,16 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.8
package websocket
import "crypto/tls"
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return cfg.Clone()
}

View File

@ -1,38 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8
package websocket
import "crypto/tls"
// cloneTLSConfig clones all public fields except the fields
// SessionTicketsDisabled and SessionTicketKey. This avoids copying the
// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
// config in active use.
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,
Certificates: cfg.Certificates,
NameToCertificate: cfg.NameToCertificate,
GetCertificate: cfg.GetCertificate,
RootCAs: cfg.RootCAs,
NextProtos: cfg.NextProtos,
ServerName: cfg.ServerName,
ClientAuth: cfg.ClientAuth,
ClientCAs: cfg.ClientCAs,
InsecureSkipVerify: cfg.InsecureSkipVerify,
CipherSuites: cfg.CipherSuites,
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
ClientSessionCache: cfg.ClientSessionCache,
MinVersion: cfg.MinVersion,
MaxVersion: cfg.MaxVersion,
CurvePreferences: cfg.CurvePreferences,
}
}

View File

@ -1,148 +0,0 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"compress/flate"
"errors"
"io"
"strings"
"sync"
)
const (
minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
maxCompressionLevel = flate.BestCompression
defaultCompressionLevel = 1
)
var (
flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
flateReaderPool = sync.Pool{New: func() interface{} {
return flate.NewReader(nil)
}}
)
func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
const tail =
// Add four bytes as specified in RFC
"\x00\x00\xff\xff" +
// Add final block to squelch unexpected EOF error from flate reader.
"\x01\x00\x00\xff\xff"
fr, _ := flateReaderPool.Get().(io.ReadCloser)
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
return &flateReadWrapper{fr}
}
func isValidCompressionLevel(level int) bool {
return minCompressionLevel <= level && level <= maxCompressionLevel
}
func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
p := &flateWriterPools[level-minCompressionLevel]
tw := &truncWriter{w: w}
fw, _ := p.Get().(*flate.Writer)
if fw == nil {
fw, _ = flate.NewWriter(tw, level)
} else {
fw.Reset(tw)
}
return &flateWriteWrapper{fw: fw, tw: tw, p: p}
}
// truncWriter is an io.Writer that writes all but the last four bytes of the
// stream to another io.Writer.
type truncWriter struct {
w io.WriteCloser
n int
p [4]byte
}
func (w *truncWriter) Write(p []byte) (int, error) {
n := 0
// fill buffer first for simplicity.
if w.n < len(w.p) {
n = copy(w.p[w.n:], p)
p = p[n:]
w.n += n
if len(p) == 0 {
return n, nil
}
}
m := len(p)
if m > len(w.p) {
m = len(w.p)
}
if nn, err := w.w.Write(w.p[:m]); err != nil {
return n + nn, err
}
copy(w.p[:], w.p[m:])
copy(w.p[len(w.p)-m:], p[len(p)-m:])
nn, err := w.w.Write(p[:len(p)-m])
return n + nn, err
}
type flateWriteWrapper struct {
fw *flate.Writer
tw *truncWriter
p *sync.Pool
}
func (w *flateWriteWrapper) Write(p []byte) (int, error) {
if w.fw == nil {
return 0, errWriteClosed
}
return w.fw.Write(p)
}
func (w *flateWriteWrapper) Close() error {
if w.fw == nil {
return errWriteClosed
}
err1 := w.fw.Flush()
w.p.Put(w.fw)
w.fw = nil
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
}
err2 := w.tw.w.Close()
if err1 != nil {
return err1
}
return err2
}
type flateReadWrapper struct {
fr io.ReadCloser
}
func (r *flateReadWrapper) Read(p []byte) (int, error) {
if r.fr == nil {
return 0, io.ErrClosedPipe
}
n, err := r.fr.Read(p)
if err == io.EOF {
// Preemptively place the reader back in the pool. This helps with
// scenarios where the application does not call NextReader() soon after
// this final read.
r.Close()
}
return n, err
}
func (r *flateReadWrapper) Close() error {
if r.fr == nil {
return io.ErrClosedPipe
}
err := r.fr.Close()
flateReaderPool.Put(r.fr)
r.fr = nil
return err
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.8
package websocket
import "net"
func (c *Conn) writeBufs(bufs ...[]byte) error {
b := net.Buffers(bufs)
_, err := b.WriteTo(c.conn)
return err
}

View File

@ -1,18 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8
package websocket
func (c *Conn) writeBufs(bufs ...[]byte) error {
for _, buf := range bufs {
if len(buf) > 0 {
if _, err := c.conn.Write(buf); err != nil {
return err
}
}
}
return nil
}

View File

@ -1,227 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package websocket implements the WebSocket protocol defined in RFC 6455.
//
// Overview
//
// The Conn type represents a WebSocket connection. A server application calls
// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn:
//
// var upgrader = websocket.Upgrader{
// ReadBufferSize: 1024,
// WriteBufferSize: 1024,
// }
//
// func handler(w http.ResponseWriter, r *http.Request) {
// conn, err := upgrader.Upgrade(w, r, nil)
// if err != nil {
// log.Println(err)
// return
// }
// ... Use conn to send and receive messages.
// }
//
// Call the connection's WriteMessage and ReadMessage methods to send and
// receive messages as a slice of bytes. This snippet of code shows how to echo
// messages using these methods:
//
// for {
// messageType, p, err := conn.ReadMessage()
// if err != nil {
// log.Println(err)
// return
// }
// if err := conn.WriteMessage(messageType, p); err != nil {
// log.Println(err)
// return
// }
// }
//
// In above snippet of code, p is a []byte and messageType is an int with value
// websocket.BinaryMessage or websocket.TextMessage.
//
// An application can also send and receive messages using the io.WriteCloser
// and io.Reader interfaces. To send a message, call the connection NextWriter
// method to get an io.WriteCloser, write the message to the writer and close
// the writer when done. To receive a message, call the connection NextReader
// method to get an io.Reader and read until io.EOF is returned. This snippet
// shows how to echo messages using the NextWriter and NextReader methods:
//
// for {
// messageType, r, err := conn.NextReader()
// if err != nil {
// return
// }
// w, err := conn.NextWriter(messageType)
// if err != nil {
// return err
// }
// if _, err := io.Copy(w, r); err != nil {
// return err
// }
// if err := w.Close(); err != nil {
// return err
// }
// }
//
// Data Messages
//
// The WebSocket protocol distinguishes between text and binary data messages.
// Text messages are interpreted as UTF-8 encoded text. The interpretation of
// binary messages is left to the application.
//
// This package uses the TextMessage and BinaryMessage integer constants to
// identify the two data message types. The ReadMessage and NextReader methods
// return the type of the received message. The messageType argument to the
// WriteMessage and NextWriter methods specifies the type of a sent message.
//
// It is the application's responsibility to ensure that text messages are
// valid UTF-8 encoded text.
//
// Control Messages
//
// The WebSocket protocol defines three types of control messages: close, ping
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
// methods to send a control message to the peer.
//
// Connections handle received close messages by calling the handler function
// set with the SetCloseHandler method and by returning a *CloseError from the
// NextReader, ReadMessage or the message Read method. The default close
// handler sends a close message to the peer.
//
// Connections handle received ping messages by calling the handler function
// set with the SetPingHandler method. The default ping handler sends a pong
// message to the peer.
//
// Connections handle received pong messages by calling the handler function
// set with the SetPongHandler method. The default pong handler does nothing.
// If an application sends ping messages, then the application should set a
// pong handler to receive the corresponding pong.
//
// The control message handler functions are called from the NextReader,
// ReadMessage and message reader Read methods. The default close and ping
// handlers can block these methods for a short time when the handler writes to
// the connection.
//
// The application must read the connection to process close, ping and pong
// messages sent from the peer. If the application is not otherwise interested
// in messages from the peer, then the application should start a goroutine to
// read and discard messages from the peer. A simple example is:
//
// func readLoop(c *websocket.Conn) {
// for {
// if _, _, err := c.NextReader(); err != nil {
// c.Close()
// break
// }
// }
// }
//
// Concurrency
//
// Connections support one concurrent reader and one concurrent writer.
//
// Applications are responsible for ensuring that no more than one goroutine
// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and
// that no more than one goroutine calls the read methods (NextReader,
// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler)
// concurrently.
//
// The Close and WriteControl methods can be called concurrently with all other
// methods.
//
// Origin Considerations
//
// Web browsers allow Javascript applications to open a WebSocket connection to
// any host. It's up to the server to enforce an origin policy using the Origin
// request header sent by the browser.
//
// The Upgrader calls the function specified in the CheckOrigin field to check
// the origin. If the CheckOrigin function returns false, then the Upgrade
// method fails the WebSocket handshake with HTTP status 403.
//
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
// the handshake if the Origin request header is present and the Origin host is
// not equal to the Host request header.
//
// The deprecated package-level Upgrade function does not perform origin
// checking. The application is responsible for checking the Origin header
// before calling the Upgrade function.
//
// Buffers
//
// Connections buffer network input and output to reduce the number
// of system calls when reading or writing messages.
//
// Write buffers are also used for constructing WebSocket frames. See RFC 6455,
// Section 5 for a discussion of message framing. A WebSocket frame header is
// written to the network each time a write buffer is flushed to the network.
// Decreasing the size of the write buffer can increase the amount of framing
// overhead on the connection.
//
// The buffer sizes in bytes are specified by the ReadBufferSize and
// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default
// size of 4096 when a buffer size field is set to zero. The Upgrader reuses
// buffers created by the HTTP server when a buffer size field is set to zero.
// The HTTP server buffers have a size of 4096 at the time of this writing.
//
// The buffer sizes do not limit the size of a message that can be read or
// written by a connection.
//
// Buffers are held for the lifetime of the connection by default. If the
// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the
// write buffer only when writing a message.
//
// Applications should tune the buffer sizes to balance memory use and
// performance. Increasing the buffer size uses more memory, but can reduce the
// number of system calls to read or write the network. In the case of writing,
// increasing the buffer size can reduce the number of frame headers written to
// the network.
//
// Some guidelines for setting buffer parameters are:
//
// Limit the buffer sizes to the maximum expected message size. Buffers larger
// than the largest message do not provide any benefit.
//
// Depending on the distribution of message sizes, setting the buffer size to
// a value less than the maximum expected message size can greatly reduce memory
// use with a small impact on performance. Here's an example: If 99% of the
// messages are smaller than 256 bytes and the maximum message size is 512
// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls
// than a buffer size of 512 bytes. The memory savings is 50%.
//
// A write buffer pool is useful when the application has a modest number
// writes over a large number of connections. when buffers are pooled, a larger
// buffer size has a reduced impact on total memory use and has the benefit of
// reducing system calls and frame overhead.
//
// Compression EXPERIMENTAL
//
// Per message compression extensions (RFC 7692) are experimentally supported
// by this package in a limited capacity. Setting the EnableCompression option
// to true in Dialer or Upgrader will attempt to negotiate per message deflate
// support.
//
// var upgrader = websocket.Upgrader{
// EnableCompression: true,
// }
//
// If compression was successfully negotiated with the connection's peer, any
// message received in compressed form will be automatically decompressed.
// All Read methods will return uncompressed bytes.
//
// Per message compression of messages written to a connection can be enabled
// or disabled by calling the corresponding Conn method:
//
// conn.EnableWriteCompression(false)
//
// Currently this package does not support compression with "context takeover".
// This means that messages must be compressed and decompressed in isolation,
// without retaining sliding window or dictionary state across messages. For
// more details refer to RFC 7692.
//
// Use of compression is experimental and may result in decreased performance.
package websocket

View File

@ -1,3 +0,0 @@
module github.com/gorilla/websocket
go 1.12

View File

View File

@ -1,42 +0,0 @@
// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"io"
"strings"
)
// JoinMessages concatenates received messages to create a single io.Reader.
// The string term is appended to each message. The returned reader does not
// support concurrent calls to the Read method.
func JoinMessages(c *Conn, term string) io.Reader {
return &joinReader{c: c, term: term}
}
type joinReader struct {
c *Conn
term string
r io.Reader
}
func (r *joinReader) Read(p []byte) (int, error) {
if r.r == nil {
var err error
_, r.r, err = r.c.NextReader()
if err != nil {
return 0, err
}
if r.term != "" {
r.r = io.MultiReader(r.r, strings.NewReader(r.term))
}
}
n, err := r.r.Read(p)
if err == io.EOF {
err = nil
r.r = nil
}
return n, err
}

View File

@ -1,60 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"encoding/json"
"io"
)
// WriteJSON writes the JSON encoding of v as a message.
//
// Deprecated: Use c.WriteJSON instead.
func WriteJSON(c *Conn, v interface{}) error {
return c.WriteJSON(v)
}
// WriteJSON writes the JSON encoding of v as a message.
//
// See the documentation for encoding/json Marshal for details about the
// conversion of Go values to JSON.
func (c *Conn) WriteJSON(v interface{}) error {
w, err := c.NextWriter(TextMessage)
if err != nil {
return err
}
err1 := json.NewEncoder(w).Encode(v)
err2 := w.Close()
if err1 != nil {
return err1
}
return err2
}
// ReadJSON reads the next JSON-encoded message from the connection and stores
// it in the value pointed to by v.
//
// Deprecated: Use c.ReadJSON instead.
func ReadJSON(c *Conn, v interface{}) error {
return c.ReadJSON(v)
}
// ReadJSON reads the next JSON-encoded message from the connection and stores
// it in the value pointed to by v.
//
// See the documentation for the encoding/json Unmarshal function for details
// about the conversion of JSON to a Go value.
func (c *Conn) ReadJSON(v interface{}) error {
_, r, err := c.NextReader()
if err != nil {
return err
}
err = json.NewDecoder(r).Decode(v)
if err == io.EOF {
// One value is expected in the message.
err = io.ErrUnexpectedEOF
}
return err
}

View File

@ -1,54 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
// +build !appengine
package websocket
import "unsafe"
const wordSize = int(unsafe.Sizeof(uintptr(0)))
func maskBytes(key [4]byte, pos int, b []byte) int {
// Mask one byte at a time for small buffers.
if len(b) < 2*wordSize {
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}
// Mask one byte at a time to word boundary.
if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
n = wordSize - n
for i := range b[:n] {
b[i] ^= key[pos&3]
pos++
}
b = b[n:]
}
// Create aligned word size key.
var k [wordSize]byte
for i := range k {
k[i] = key[(pos+i)&3]
}
kw := *(*uintptr)(unsafe.Pointer(&k))
// Mask one word at a time.
n := (len(b) / wordSize) * wordSize
for i := 0; i < n; i += wordSize {
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
}
// Mask one byte at a time for remaining bytes.
b = b[n:]
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}

View File

@ -1,15 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
// +build appengine
package websocket
func maskBytes(key [4]byte, pos int, b []byte) int {
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}

View File

@ -1,102 +0,0 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bytes"
"net"
"sync"
"time"
)
// PreparedMessage caches on the wire representations of a message payload.
// Use PreparedMessage to efficiently send a message payload to multiple
// connections. PreparedMessage is especially useful when compression is used
// because the CPU and memory expensive compression operation can be executed
// once for a given set of compression options.
type PreparedMessage struct {
messageType int
data []byte
mu sync.Mutex
frames map[prepareKey]*preparedFrame
}
// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage.
type prepareKey struct {
isServer bool
compress bool
compressionLevel int
}
// preparedFrame contains data in wire representation.
type preparedFrame struct {
once sync.Once
data []byte
}
// NewPreparedMessage returns an initialized PreparedMessage. You can then send
// it to connection using WritePreparedMessage method. Valid wire
// representation will be calculated lazily only once for a set of current
// connection options.
func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) {
pm := &PreparedMessage{
messageType: messageType,
frames: make(map[prepareKey]*preparedFrame),
data: data,
}
// Prepare a plain server frame.
_, frameData, err := pm.frame(prepareKey{isServer: true, compress: false})
if err != nil {
return nil, err
}
// To protect against caller modifying the data argument, remember the data
// copied to the plain server frame.
pm.data = frameData[len(frameData)-len(data):]
return pm, nil
}
func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) {
pm.mu.Lock()
frame, ok := pm.frames[key]
if !ok {
frame = &preparedFrame{}
pm.frames[key] = frame
}
pm.mu.Unlock()
var err error
frame.once.Do(func() {
// Prepare a frame using a 'fake' connection.
// TODO: Refactor code in conn.go to allow more direct construction of
// the frame.
mu := make(chan struct{}, 1)
mu <- struct{}{}
var nc prepareConn
c := &Conn{
conn: &nc,
mu: mu,
isServer: key.isServer,
compressionLevel: key.compressionLevel,
enableWriteCompression: true,
writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize),
}
if key.compress {
c.newCompressionWriter = compressNoContextTakeover
}
err = c.WriteMessage(pm.messageType, pm.data)
frame.data = nc.buf.Bytes()
})
return pm.messageType, frame.data, err
}
type prepareConn struct {
buf bytes.Buffer
net.Conn
}
func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) }
func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil }

View File

@ -1,77 +0,0 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"encoding/base64"
"errors"
"net"
"net/http"
"net/url"
"strings"
)
type netDialerFunc func(network, addr string) (net.Conn, error)
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
return fn(network, addr)
}
func init() {
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil
})
}
type httpProxyDialer struct {
proxyURL *url.URL
forwardDial func(network, addr string) (net.Conn, error)
}
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
hostPort, _ := hostPortNoPort(hpd.proxyURL)
conn, err := hpd.forwardDial(network, hostPort)
if err != nil {
return nil, err
}
connectHeader := make(http.Header)
if user := hpd.proxyURL.User; user != nil {
proxyUser := user.Username()
if proxyPassword, passwordSet := user.Password(); passwordSet {
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
}
}
connectReq := &http.Request{
Method: "CONNECT",
URL: &url.URL{Opaque: addr},
Host: addr,
Header: connectHeader,
}
if err := connectReq.Write(conn); err != nil {
conn.Close()
return nil, err
}
// Read response. It's OK to use and discard buffered reader here becaue
// the remote server does not speak until spoken to.
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, connectReq)
if err != nil {
conn.Close()
return nil, err
}
if resp.StatusCode != 200 {
conn.Close()
f := strings.SplitN(resp.Status, " ", 2)
return nil, errors.New(f[1])
}
return conn, nil
}

View File

@ -1,363 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"errors"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// HandshakeError describes an error with the handshake from the peer.
type HandshakeError struct {
message string
}
func (e HandshakeError) Error() string { return e.message }
// Upgrader specifies parameters for upgrading an HTTP connection to a
// WebSocket connection.
type Upgrader struct {
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// size is zero, then buffers allocated by the HTTP server are used. The
// I/O buffer sizes do not limit the size of the messages that can be sent
// or received.
ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the server's supported protocols in order of
// preference. If this field is not nil, then the Upgrade method negotiates a
// subprotocol by selecting the first match in this list with a protocol
// requested by the client. If there's no match, then no protocol is
// negotiated (the Sec-Websocket-Protocol header is not included in the
// handshake response).
Subprotocols []string
// Error specifies the function for generating HTTP error responses. If Error
// is nil, then http.Error is used to generate the HTTP response.
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
// CheckOrigin returns true if the request Origin header is acceptable. If
// CheckOrigin is nil, then a safe default is used: return false if the
// Origin request header is present and the origin host is not equal to
// request Host header.
//
// A CheckOrigin function should carefully validate the request origin to
// prevent cross-site request forgery.
CheckOrigin func(r *http.Request) bool
// EnableCompression specify if the server should attempt to negotiate per
// message compression (RFC 7692). Setting this value to true does not
// guarantee that compression will be supported. Currently only "no context
// takeover" modes are supported.
EnableCompression bool
}
func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
err := HandshakeError{reason}
if u.Error != nil {
u.Error(w, r, status, err)
} else {
w.Header().Set("Sec-Websocket-Version", "13")
http.Error(w, http.StatusText(status), status)
}
return nil, err
}
// checkSameOrigin returns true if the origin is not set or is equal to the request host.
func checkSameOrigin(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true
}
u, err := url.Parse(origin[0])
if err != nil {
return false
}
return equalASCIIFold(u.Host, r.Host)
}
func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
if u.Subprotocols != nil {
clientProtocols := Subprotocols(r)
for _, serverProtocol := range u.Subprotocols {
for _, clientProtocol := range clientProtocols {
if clientProtocol == serverProtocol {
return clientProtocol
}
}
}
} else if responseHeader != nil {
return responseHeader.Get("Sec-Websocket-Protocol")
}
return ""
}
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
//
// The responseHeader is included in the response to the client's upgrade
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
// application negotiated subprotocol (Sec-WebSocket-Protocol).
//
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
// response.
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
const badHandshake = "websocket: the client is not using the websocket protocol: "
if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
}
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")
}
if r.Method != "GET" {
return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
}
if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
}
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")
}
checkOrigin := u.CheckOrigin
if checkOrigin == nil {
checkOrigin = checkSameOrigin
}
if !checkOrigin(r) {
return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")
}
challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank")
}
subprotocol := u.selectSubprotocol(r, responseHeader)
// Negotiate PMCE
var compress bool
if u.EnableCompression {
for _, ext := range parseExtensions(r.Header) {
if ext[""] != "permessage-deflate" {
continue
}
compress = true
break
}
}
h, ok := w.(http.Hijacker)
if !ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
}
var brw *bufio.ReadWriter
netConn, brw, err := h.Hijack()
if err != nil {
return u.returnError(w, r, http.StatusInternalServerError, err.Error())
}
if brw.Reader.Buffered() > 0 {
netConn.Close()
return nil, errors.New("websocket: client sent data before handshake is complete")
}
var br *bufio.Reader
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
// Reuse hijacked buffered reader as connection reader.
br = brw.Reader
}
buf := bufioWriterBuffer(netConn, brw.Writer)
var writeBuf []byte
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
// Reuse hijacked write buffer as connection buffer.
writeBuf = buf
}
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
c.subprotocol = subprotocol
if compress {
c.newCompressionWriter = compressNoContextTakeover
c.newDecompressionReader = decompressNoContextTakeover
}
// Use larger of hijacked buffer and connection write buffer for header.
p := buf
if len(c.writeBuf) > len(p) {
p = c.writeBuf
}
p = p[:0]
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...)
p = append(p, "\r\n"...)
if c.subprotocol != "" {
p = append(p, "Sec-WebSocket-Protocol: "...)
p = append(p, c.subprotocol...)
p = append(p, "\r\n"...)
}
if compress {
p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
}
for k, vs := range responseHeader {
if k == "Sec-Websocket-Protocol" {
continue
}
for _, v := range vs {
p = append(p, k...)
p = append(p, ": "...)
for i := 0; i < len(v); i++ {
b := v[i]
if b <= 31 {
// prevent response splitting.
b = ' '
}
p = append(p, b)
}
p = append(p, "\r\n"...)
}
}
p = append(p, "\r\n"...)
// Clear deadlines set by HTTP server.
netConn.SetDeadline(time.Time{})
if u.HandshakeTimeout > 0 {
netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
}
if _, err = netConn.Write(p); err != nil {
netConn.Close()
return nil, err
}
if u.HandshakeTimeout > 0 {
netConn.SetWriteDeadline(time.Time{})
}
return c, nil
}
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
//
// Deprecated: Use websocket.Upgrader instead.
//
// Upgrade does not perform origin checking. The application is responsible for
// checking the Origin header before calling Upgrade. An example implementation
// of the same origin policy check is:
//
// if req.Header.Get("Origin") != "http://"+req.Host {
// http.Error(w, "Origin not allowed", http.StatusForbidden)
// return
// }
//
// If the endpoint supports subprotocols, then the application is responsible
// for negotiating the protocol used on the connection. Use the Subprotocols()
// function to get the subprotocols requested by the client. Use the
// Sec-Websocket-Protocol response header to specify the subprotocol selected
// by the application.
//
// The responseHeader is included in the response to the client's upgrade
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
// negotiated subprotocol (Sec-Websocket-Protocol).
//
// The connection buffers IO to the underlying network connection. The
// readBufSize and writeBufSize parameters specify the size of the buffers to
// use. Messages can be larger than the buffers.
//
// If the request is not a valid WebSocket handshake, then Upgrade returns an
// error of type HandshakeError. Applications should handle this error by
// replying to the client with an HTTP error response.
func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) {
u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize}
u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) {
// don't return errors to maintain backwards compatibility
}
u.CheckOrigin = func(r *http.Request) bool {
// allow all connections by default
return true
}
return u.Upgrade(w, r, responseHeader)
}
// Subprotocols returns the subprotocols requested by the client in the
// Sec-Websocket-Protocol header.
func Subprotocols(r *http.Request) []string {
h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol"))
if h == "" {
return nil
}
protocols := strings.Split(h, ",")
for i := range protocols {
protocols[i] = strings.TrimSpace(protocols[i])
}
return protocols
}
// IsWebSocketUpgrade returns true if the client requested upgrade to the
// WebSocket protocol.
func IsWebSocketUpgrade(r *http.Request) bool {
return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
tokenListContainsValue(r.Header, "Upgrade", "websocket")
}
// bufioReaderSize size returns the size of a bufio.Reader.
func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int {
// This code assumes that peek on a reset reader returns
// bufio.Reader.buf[:0].
// TODO: Use bufio.Reader.Size() after Go 1.10
br.Reset(originalReader)
if p, err := br.Peek(0); err == nil {
return cap(p)
}
return 0
}
// writeHook is an io.Writer that records the last slice passed to it vio
// io.Writer.Write.
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
// bufioWriterBuffer grabs the buffer from a bufio.Writer.
func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
// This code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
bw.Reset(&wh)
bw.WriteByte(0)
bw.Flush()
bw.Reset(originalWriter)
return wh.p[:cap(wh.p)]
}

View File

@ -1,19 +0,0 @@
// +build go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
if trace.TLSHandshakeStart != nil {
trace.TLSHandshakeStart()
}
err := doHandshake(tlsConn, cfg)
if trace.TLSHandshakeDone != nil {
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
}
return err
}

View File

@ -1,12 +0,0 @@
// +build !go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
return doHandshake(tlsConn, cfg)
}

View File

@ -1,283 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"io"
"net/http"
"strings"
"unicode/utf8"
)
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func computeAcceptKey(challengeKey string) string {
h := sha1.New()
h.Write([]byte(challengeKey))
h.Write(keyGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func generateChallengeKey() (string, error) {
p := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, p); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(p), nil
}
// Token octets per RFC 2616.
var isTokenOctet = [256]bool{
'!': true,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'*': true,
'+': true,
'-': true,
'.': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'W': true,
'V': true,
'X': true,
'Y': true,
'Z': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'|': true,
'~': true,
}
// skipSpace returns a slice of the string s with all leading RFC 2616 linear
// whitespace removed.
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if b := s[i]; b != ' ' && b != '\t' {
break
}
}
return s[i:]
}
// nextToken returns the leading RFC 2616 token of s and the string following
// the token.
func nextToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if !isTokenOctet[s[i]] {
break
}
}
return s[:i], s[i:]
}
// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616
// and the string following the token or quoted string.
func nextTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return nextToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + 1; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j++
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j++
}
}
return "", ""
}
}
return "", ""
}
// equalASCIIFold returns true if s is equal to t with ASCII case folding as
// defined in RFC 4790.
func equalASCIIFold(s, t string) bool {
for s != "" && t != "" {
sr, size := utf8.DecodeRuneInString(s)
s = s[size:]
tr, size := utf8.DecodeRuneInString(t)
t = t[size:]
if sr == tr {
continue
}
if 'A' <= sr && sr <= 'Z' {
sr = sr + 'a' - 'A'
}
if 'A' <= tr && tr <= 'Z' {
tr = tr + 'a' - 'A'
}
if sr != tr {
return false
}
}
return s == t
}
// tokenListContainsValue returns true if the 1#token header with the given
// name contains a token equal to value with ASCII case folding.
func tokenListContainsValue(header http.Header, name string, value string) bool {
headers:
for _, s := range header[name] {
for {
var t string
t, s = nextToken(skipSpace(s))
if t == "" {
continue headers
}
s = skipSpace(s)
if s != "" && s[0] != ',' {
continue headers
}
if equalASCIIFold(t, value) {
return true
}
if s == "" {
continue headers
}
s = s[1:]
}
}
return false
}
// parseExtensions parses WebSocket extensions from a header.
func parseExtensions(header http.Header) []map[string]string {
// From RFC 6455:
//
// Sec-WebSocket-Extensions = extension-list
// extension-list = 1#extension
// extension = extension-token *( ";" extension-param )
// extension-token = registered-token
// registered-token = token
// extension-param = token [ "=" (token | quoted-string) ]
// ;When using the quoted-string syntax variant, the value
// ;after quoted-string unescaping MUST conform to the
// ;'token' ABNF.
var result []map[string]string
headers:
for _, s := range header["Sec-Websocket-Extensions"] {
for {
var t string
t, s = nextToken(skipSpace(s))
if t == "" {
continue headers
}
ext := map[string]string{"": t}
for {
s = skipSpace(s)
if !strings.HasPrefix(s, ";") {
break
}
var k string
k, s = nextToken(skipSpace(s[1:]))
if k == "" {
continue headers
}
s = skipSpace(s)
var v string
if strings.HasPrefix(s, "=") {
v, s = nextTokenOrQuoted(skipSpace(s[1:]))
s = skipSpace(s)
}
if s != "" && s[0] != ',' && s[0] != ';' {
continue headers
}
ext[k] = v
}
if s != "" && s[0] != ',' {
continue headers
}
result = append(result, ext)
if s == "" {
continue headers
}
s = s[1:]
}
}
return result
}

View File

@ -1,473 +0,0 @@
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy
// Package proxy provides support for a variety of protocols to proxy network
// data.
//
package websocket
import (
"errors"
"io"
"net"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
type proxy_direct struct{}
// Direct is a direct proxy: one that makes network connections directly.
var proxy_Direct = proxy_direct{}
func (proxy_direct) Dial(network, addr string) (net.Conn, error) {
return net.Dial(network, addr)
}
// A PerHost directs connections to a default Dialer unless the host name
// requested matches one of a number of exceptions.
type proxy_PerHost struct {
def, bypass proxy_Dialer
bypassNetworks []*net.IPNet
bypassIPs []net.IP
bypassZones []string
bypassHosts []string
}
// NewPerHost returns a PerHost Dialer that directs connections to either
// defaultDialer or bypass, depending on whether the connection matches one of
// the configured rules.
func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost {
return &proxy_PerHost{
def: defaultDialer,
bypass: bypass,
}
}
// Dial connects to the address addr on the given network through either
// defaultDialer or bypass.
func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
return p.dialerForRequest(host).Dial(network, addr)
}
func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer {
if ip := net.ParseIP(host); ip != nil {
for _, net := range p.bypassNetworks {
if net.Contains(ip) {
return p.bypass
}
}
for _, bypassIP := range p.bypassIPs {
if bypassIP.Equal(ip) {
return p.bypass
}
}
return p.def
}
for _, zone := range p.bypassZones {
if strings.HasSuffix(host, zone) {
return p.bypass
}
if host == zone[1:] {
// For a zone ".example.com", we match "example.com"
// too.
return p.bypass
}
}
for _, bypassHost := range p.bypassHosts {
if bypassHost == host {
return p.bypass
}
}
return p.def
}
// AddFromString parses a string that contains comma-separated values
// specifying hosts that should use the bypass proxy. Each value is either an
// IP address, a CIDR range, a zone (*.example.com) or a host name
// (localhost). A best effort is made to parse the string and errors are
// ignored.
func (p *proxy_PerHost) AddFromString(s string) {
hosts := strings.Split(s, ",")
for _, host := range hosts {
host = strings.TrimSpace(host)
if len(host) == 0 {
continue
}
if strings.Contains(host, "/") {
// We assume that it's a CIDR address like 127.0.0.0/8
if _, net, err := net.ParseCIDR(host); err == nil {
p.AddNetwork(net)
}
continue
}
if ip := net.ParseIP(host); ip != nil {
p.AddIP(ip)
continue
}
if strings.HasPrefix(host, "*.") {
p.AddZone(host[1:])
continue
}
p.AddHost(host)
}
}
// AddIP specifies an IP address that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match an IP.
func (p *proxy_PerHost) AddIP(ip net.IP) {
p.bypassIPs = append(p.bypassIPs, ip)
}
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match.
func (p *proxy_PerHost) AddNetwork(net *net.IPNet) {
p.bypassNetworks = append(p.bypassNetworks, net)
}
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
// "example.com" matches "example.com" and all of its subdomains.
func (p *proxy_PerHost) AddZone(zone string) {
if strings.HasSuffix(zone, ".") {
zone = zone[:len(zone)-1]
}
if !strings.HasPrefix(zone, ".") {
zone = "." + zone
}
p.bypassZones = append(p.bypassZones, zone)
}
// AddHost specifies a host name that will use the bypass proxy.
func (p *proxy_PerHost) AddHost(host string) {
if strings.HasSuffix(host, ".") {
host = host[:len(host)-1]
}
p.bypassHosts = append(p.bypassHosts, host)
}
// A Dialer is a means to establish a connection.
type proxy_Dialer interface {
// Dial connects to the given address via the proxy.
Dial(network, addr string) (c net.Conn, err error)
}
// Auth contains authentication parameters that specific Dialers may require.
type proxy_Auth struct {
User, Password string
}
// FromEnvironment returns the dialer specified by the proxy related variables in
// the environment.
func proxy_FromEnvironment() proxy_Dialer {
allProxy := proxy_allProxyEnv.Get()
if len(allProxy) == 0 {
return proxy_Direct
}
proxyURL, err := url.Parse(allProxy)
if err != nil {
return proxy_Direct
}
proxy, err := proxy_FromURL(proxyURL, proxy_Direct)
if err != nil {
return proxy_Direct
}
noProxy := proxy_noProxyEnv.Get()
if len(noProxy) == 0 {
return proxy
}
perHost := proxy_NewPerHost(proxy, proxy_Direct)
perHost.AddFromString(noProxy)
return perHost
}
// proxySchemes is a map from URL schemes to a function that creates a Dialer
// from a URL with such a scheme.
var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
// by FromURL.
func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) {
if proxy_proxySchemes == nil {
proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error))
}
proxy_proxySchemes[scheme] = f
}
// FromURL returns a Dialer given a URL specification and an underlying
// Dialer for it to make network requests.
func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) {
var auth *proxy_Auth
if u.User != nil {
auth = new(proxy_Auth)
auth.User = u.User.Username()
if p, ok := u.User.Password(); ok {
auth.Password = p
}
}
switch u.Scheme {
case "socks5":
return proxy_SOCKS5("tcp", u.Host, auth, forward)
}
// If the scheme doesn't match any of the built-in schemes, see if it
// was registered by another package.
if proxy_proxySchemes != nil {
if f, ok := proxy_proxySchemes[u.Scheme]; ok {
return f(u, forward)
}
}
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
}
var (
proxy_allProxyEnv = &proxy_envOnce{
names: []string{"ALL_PROXY", "all_proxy"},
}
proxy_noProxyEnv = &proxy_envOnce{
names: []string{"NO_PROXY", "no_proxy"},
}
)
// envOnce looks up an environment variable (optionally by multiple
// names) once. It mitigates expensive lookups on some platforms
// (e.g. Windows).
// (Borrowed from net/http/transport.go)
type proxy_envOnce struct {
names []string
once sync.Once
val string
}
func (e *proxy_envOnce) Get() string {
e.once.Do(e.init)
return e.val
}
func (e *proxy_envOnce) init() {
for _, n := range e.names {
e.val = os.Getenv(n)
if e.val != "" {
return
}
}
}
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
// with an optional username and password. See RFC 1928 and RFC 1929.
func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) {
s := &proxy_socks5{
network: network,
addr: addr,
forward: forward,
}
if auth != nil {
s.user = auth.User
s.password = auth.Password
}
return s, nil
}
type proxy_socks5 struct {
user, password string
network, addr string
forward proxy_Dialer
}
const proxy_socks5Version = 5
const (
proxy_socks5AuthNone = 0
proxy_socks5AuthPassword = 2
)
const proxy_socks5Connect = 1
const (
proxy_socks5IP4 = 1
proxy_socks5Domain = 3
proxy_socks5IP6 = 4
)
var proxy_socks5Errors = []string{
"",
"general failure",
"connection forbidden",
"network unreachable",
"host unreachable",
"connection refused",
"TTL expired",
"command not supported",
"address type not supported",
}
// Dial connects to the address addr on the given network via the SOCKS5 proxy.
func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
}
conn, err := s.forward.Dial(s.network, s.addr)
if err != nil {
return nil, err
}
if err := s.connect(conn, addr); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
// connect takes an existing connection to a socks5 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *proxy_socks5) connect(conn net.Conn, target string) error {
host, portStr, err := net.SplitHostPort(target)
if err != nil {
return err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return errors.New("proxy: failed to parse port number: " + portStr)
}
if port < 1 || port > 0xffff {
return errors.New("proxy: port number out of range: " + portStr)
}
// the size here is just an estimate
buf := make([]byte, 0, 6+len(host))
buf = append(buf, proxy_socks5Version)
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword)
} else {
buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone)
}
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[0] != 5 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
}
if buf[1] == 0xff {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
}
// See RFC 1929
if buf[1] == proxy_socks5AuthPassword {
buf = buf[:0]
buf = append(buf, 1 /* password protocol version */)
buf = append(buf, uint8(len(s.user)))
buf = append(buf, s.user...)
buf = append(buf, uint8(len(s.password)))
buf = append(buf, s.password...)
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[1] != 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
}
}
buf = buf[:0]
buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */)
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
buf = append(buf, proxy_socks5IP4)
ip = ip4
} else {
buf = append(buf, proxy_socks5IP6)
}
buf = append(buf, ip...)
} else {
if len(host) > 255 {
return errors.New("proxy: destination host name too long: " + host)
}
buf = append(buf, proxy_socks5Domain)
buf = append(buf, byte(len(host)))
buf = append(buf, host...)
}
buf = append(buf, byte(port>>8), byte(port))
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
failure := "unknown error"
if int(buf[1]) < len(proxy_socks5Errors) {
failure = proxy_socks5Errors[buf[1]]
}
if len(failure) > 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
}
bytesToDiscard := 0
switch buf[3] {
case proxy_socks5IP4:
bytesToDiscard = net.IPv4len
case proxy_socks5IP6:
bytesToDiscard = net.IPv6len
case proxy_socks5Domain:
_, err := io.ReadFull(conn, buf[:1])
if err != nil {
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
bytesToDiscard = int(buf[0])
default:
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
}
if cap(buf) < bytesToDiscard {
buf = make([]byte, bytesToDiscard)
} else {
buf = buf[:bytesToDiscard]
}
if _, err := io.ReadFull(conn, buf); err != nil {
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
// Also need to discard the port number
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
return nil
}

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018 Peter Lithammer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More