Compare commits
No commits in common. "master" and "gocui" have entirely different histories.
6
.github/issue_template.md
vendored
6
.github/issue_template.md
vendored
@ -1,6 +0,0 @@
|
||||
Please read [CONTRIBUTING.md](https://github.com/erroneousboat/slack-term/blob/master/CONTRIBUTING.md)
|
||||
|
||||
---
|
||||
|
||||
Version:
|
||||
Installation method:
|
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@ -1 +0,0 @@
|
||||
Please read [CONTRIBUTING.md](https://github.com/erroneousboat/slack-term/blob/master/CONTRIBUTING.md)
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
||||
bin/
|
||||
vendor/*
|
||||
!vendor.json
|
||||
|
@ -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.
|
31
Dockerfile
31
Dockerfile
@ -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
|
20
Makefile
20
Makefile
@ -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
127
README.md
@ -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 |
|
||||
|
@ -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
342
components/channels_bkp.go
Normal 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
|
||||
}
|
@ -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
253
components/chat_bkp.go
Normal 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
14
components/components.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
137
components/input_bkp.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
144
config/config.go
144
config/config.go
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
@ -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
21
go.mod
@ -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
53
go.sum
@ -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=
|
@ -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
81
main.go
@ -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)
|
||||
}
|
||||
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 1.2 MiB |
868
service/slack.go
868
service/slack.go
File diff suppressed because it is too large
Load Diff
25
vendor/github.com/0xAX/notificator/.gitignore
generated
vendored
25
vendor/github.com/0xAX/notificator/.gitignore
generated
vendored
@ -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
|
27
vendor/github.com/0xAX/notificator/LICENSE
generated
vendored
27
vendor/github.com/0xAX/notificator/LICENSE
generated
vendored
@ -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.
|
49
vendor/github.com/0xAX/notificator/README.md
generated
vendored
49
vendor/github.com/0xAX/notificator/README.md
generated
vendored
@ -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)
|
166
vendor/github.com/0xAX/notificator/notification.go
generated
vendored
166
vendor/github.com/0xAX/notificator/notification.go
generated
vendored
@ -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
|
||||
}
|
||||
}
|
3
vendor/github.com/OpenPeeDeeP/xdg/.gitignore
generated
vendored
3
vendor/github.com/OpenPeeDeeP/xdg/.gitignore
generated
vendored
@ -1,3 +0,0 @@
|
||||
*.test
|
||||
*.out
|
||||
.DS_STORE
|
12
vendor/github.com/OpenPeeDeeP/xdg/.travis.yml
generated
vendored
12
vendor/github.com/OpenPeeDeeP/xdg/.travis.yml
generated
vendored
@ -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)
|
29
vendor/github.com/OpenPeeDeeP/xdg/LICENSE
generated
vendored
29
vendor/github.com/OpenPeeDeeP/xdg/LICENSE
generated
vendored
@ -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.
|
25
vendor/github.com/OpenPeeDeeP/xdg/README.md
generated
vendored
25
vendor/github.com/OpenPeeDeeP/xdg/README.md
generated
vendored
@ -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.
|
16
vendor/github.com/OpenPeeDeeP/xdg/appveyor.yml
generated
vendored
16
vendor/github.com/OpenPeeDeeP/xdg/appveyor.yml
generated
vendored
@ -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
|
8
vendor/github.com/OpenPeeDeeP/xdg/go.mod
generated
vendored
8
vendor/github.com/OpenPeeDeeP/xdg/go.mod
generated
vendored
@ -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
|
||||
)
|
8
vendor/github.com/OpenPeeDeeP/xdg/go.sum
generated
vendored
8
vendor/github.com/OpenPeeDeeP/xdg/go.sum
generated
vendored
@ -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=
|
163
vendor/github.com/OpenPeeDeeP/xdg/xdg.go
generated
vendored
163
vendor/github.com/OpenPeeDeeP/xdg/xdg.go
generated
vendored
@ -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
|
||||
}
|
30
vendor/github.com/OpenPeeDeeP/xdg/xdg_darwin.go
generated
vendored
30
vendor/github.com/OpenPeeDeeP/xdg/xdg_darwin.go
generated
vendored
@ -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")
|
||||
}
|
30
vendor/github.com/OpenPeeDeeP/xdg/xdg_linux.go
generated
vendored
30
vendor/github.com/OpenPeeDeeP/xdg/xdg_linux.go
generated
vendored
@ -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")
|
||||
}
|
27
vendor/github.com/OpenPeeDeeP/xdg/xdg_windows.go
generated
vendored
27
vendor/github.com/OpenPeeDeeP/xdg/xdg_windows.go
generated
vendored
@ -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")
|
||||
}
|
26
vendor/github.com/erroneousboat/termui/.gitignore
generated
vendored
26
vendor/github.com/erroneousboat/termui/.gitignore
generated
vendored
@ -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
|
6
vendor/github.com/erroneousboat/termui/.travis.yml
generated
vendored
6
vendor/github.com/erroneousboat/termui/.travis.yml
generated
vendored
@ -1,6 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- tip
|
||||
|
||||
script: go test -v ./
|
22
vendor/github.com/erroneousboat/termui/LICENSE
generated
vendored
22
vendor/github.com/erroneousboat/termui/LICENSE
generated
vendored
@ -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.
|
||||
|
151
vendor/github.com/erroneousboat/termui/README.md
generated
vendored
151
vendor/github.com/erroneousboat/termui/README.md
generated
vendored
@ -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)
|
149
vendor/github.com/erroneousboat/termui/barchart.go
generated
vendored
149
vendor/github.com/erroneousboat/termui/barchart.go
generated
vendored
@ -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
|
||||
}
|
240
vendor/github.com/erroneousboat/termui/block.go
generated
vendored
240
vendor/github.com/erroneousboat/termui/block.go
generated
vendored
@ -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 }
|
20
vendor/github.com/erroneousboat/termui/block_common.go
generated
vendored
20
vendor/github.com/erroneousboat/termui/block_common.go
generated
vendored
@ -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 = '»'
|
14
vendor/github.com/erroneousboat/termui/block_windows.go
generated
vendored
14
vendor/github.com/erroneousboat/termui/block_windows.go
generated
vendored
@ -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 = '+'
|
106
vendor/github.com/erroneousboat/termui/buffer.go
generated
vendored
106
vendor/github.com/erroneousboat/termui/buffer.go
generated
vendored
@ -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
|
||||
}
|
72
vendor/github.com/erroneousboat/termui/canvas.go
generated
vendored
72
vendor/github.com/erroneousboat/termui/canvas.go
generated
vendored
@ -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
|
||||
}
|
54
vendor/github.com/erroneousboat/termui/config.py
generated
vendored
54
vendor/github.com/erroneousboat/termui/config.py
generated
vendored
@ -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()
|
29
vendor/github.com/erroneousboat/termui/doc.go
generated
vendored
29
vendor/github.com/erroneousboat/termui/doc.go
generated
vendored
@ -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
|
328
vendor/github.com/erroneousboat/termui/events.go
generated
vendored
328
vendor/github.com/erroneousboat/termui/events.go
generated
vendored
@ -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
|
||||
}
|
109
vendor/github.com/erroneousboat/termui/gauge.go
generated
vendored
109
vendor/github.com/erroneousboat/termui/gauge.go
generated
vendored
@ -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
|
||||
}
|
30
vendor/github.com/erroneousboat/termui/glide.lock
generated
vendored
30
vendor/github.com/erroneousboat/termui/glide.lock
generated
vendored
@ -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
|
9
vendor/github.com/erroneousboat/termui/glide.yaml
generated
vendored
9
vendor/github.com/erroneousboat/termui/glide.yaml
generated
vendored
@ -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
|
279
vendor/github.com/erroneousboat/termui/grid.go
generated
vendored
279
vendor/github.com/erroneousboat/termui/grid.go
generated
vendored
@ -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
|
222
vendor/github.com/erroneousboat/termui/helper.go
generated
vendored
222
vendor/github.com/erroneousboat/termui/helper.go
generated
vendored
@ -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
|
||||
}
|
331
vendor/github.com/erroneousboat/termui/linechart.go
generated
vendored
331
vendor/github.com/erroneousboat/termui/linechart.go
generated
vendored
@ -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
|
||||
}
|
11
vendor/github.com/erroneousboat/termui/linechart_others.go
generated
vendored
11
vendor/github.com/erroneousboat/termui/linechart_others.go
generated
vendored
@ -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 = '└'
|
11
vendor/github.com/erroneousboat/termui/linechart_windows.go
generated
vendored
11
vendor/github.com/erroneousboat/termui/linechart_windows.go
generated
vendored
@ -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 = '+'
|
89
vendor/github.com/erroneousboat/termui/list.go
generated
vendored
89
vendor/github.com/erroneousboat/termui/list.go
generated
vendored
@ -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
|
||||
}
|
242
vendor/github.com/erroneousboat/termui/mbarchart.go
generated
vendored
242
vendor/github.com/erroneousboat/termui/mbarchart.go
generated
vendored
@ -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
|
||||
}
|
28
vendor/github.com/erroneousboat/termui/mkdocs.yml
generated
vendored
28
vendor/github.com/erroneousboat/termui/mkdocs.yml
generated
vendored
@ -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
|
73
vendor/github.com/erroneousboat/termui/par.go
generated
vendored
73
vendor/github.com/erroneousboat/termui/par.go
generated
vendored
@ -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
|
||||
}
|
78
vendor/github.com/erroneousboat/termui/pos.go
generated
vendored
78
vendor/github.com/erroneousboat/termui/pos.go
generated
vendored
@ -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)
|
||||
}
|
164
vendor/github.com/erroneousboat/termui/render.go
generated
vendored
164
vendor/github.com/erroneousboat/termui/render.go
generated
vendored
@ -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
|
||||
}
|
167
vendor/github.com/erroneousboat/termui/sparkline.go
generated
vendored
167
vendor/github.com/erroneousboat/termui/sparkline.go
generated
vendored
@ -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
|
||||
}
|
185
vendor/github.com/erroneousboat/termui/table.go
generated
vendored
185
vendor/github.com/erroneousboat/termui/table.go
generated
vendored
@ -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
|
||||
}
|
278
vendor/github.com/erroneousboat/termui/textbuilder.go
generated
vendored
278
vendor/github.com/erroneousboat/termui/textbuilder.go
generated
vendored
@ -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{}
|
||||
}
|
140
vendor/github.com/erroneousboat/termui/theme.go
generated
vendored
140
vendor/github.com/erroneousboat/termui/theme.go
generated
vendored
@ -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)
|
||||
}
|
94
vendor/github.com/erroneousboat/termui/widget.go
generated
vendored
94
vendor/github.com/erroneousboat/termui/widget.go
generated
vendored
@ -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)
|
||||
}
|
25
vendor/github.com/gorilla/websocket/.gitignore
generated
vendored
25
vendor/github.com/gorilla/websocket/.gitignore
generated
vendored
@ -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
|
9
vendor/github.com/gorilla/websocket/AUTHORS
generated
vendored
9
vendor/github.com/gorilla/websocket/AUTHORS
generated
vendored
@ -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>
|
||||
|
22
vendor/github.com/gorilla/websocket/LICENSE
generated
vendored
22
vendor/github.com/gorilla/websocket/LICENSE
generated
vendored
@ -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.
|
64
vendor/github.com/gorilla/websocket/README.md
generated
vendored
64
vendor/github.com/gorilla/websocket/README.md
generated
vendored
@ -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.
|
||||
|
395
vendor/github.com/gorilla/websocket/client.go
generated
vendored
395
vendor/github.com/gorilla/websocket/client.go
generated
vendored
@ -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
|
||||
}
|
16
vendor/github.com/gorilla/websocket/client_clone.go
generated
vendored
16
vendor/github.com/gorilla/websocket/client_clone.go
generated
vendored
@ -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()
|
||||
}
|
38
vendor/github.com/gorilla/websocket/client_clone_legacy.go
generated
vendored
38
vendor/github.com/gorilla/websocket/client_clone_legacy.go
generated
vendored
@ -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,
|
||||
}
|
||||
}
|
148
vendor/github.com/gorilla/websocket/compression.go
generated
vendored
148
vendor/github.com/gorilla/websocket/compression.go
generated
vendored
@ -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
|
||||
}
|
1201
vendor/github.com/gorilla/websocket/conn.go
generated
vendored
1201
vendor/github.com/gorilla/websocket/conn.go
generated
vendored
File diff suppressed because it is too large
Load Diff
15
vendor/github.com/gorilla/websocket/conn_write.go
generated
vendored
15
vendor/github.com/gorilla/websocket/conn_write.go
generated
vendored
@ -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
|
||||
}
|
18
vendor/github.com/gorilla/websocket/conn_write_legacy.go
generated
vendored
18
vendor/github.com/gorilla/websocket/conn_write_legacy.go
generated
vendored
@ -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
|
||||
}
|
227
vendor/github.com/gorilla/websocket/doc.go
generated
vendored
227
vendor/github.com/gorilla/websocket/doc.go
generated
vendored
@ -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
|
3
vendor/github.com/gorilla/websocket/go.mod
generated
vendored
3
vendor/github.com/gorilla/websocket/go.mod
generated
vendored
@ -1,3 +0,0 @@
|
||||
module github.com/gorilla/websocket
|
||||
|
||||
go 1.12
|
0
vendor/github.com/gorilla/websocket/go.sum
generated
vendored
0
vendor/github.com/gorilla/websocket/go.sum
generated
vendored
42
vendor/github.com/gorilla/websocket/join.go
generated
vendored
42
vendor/github.com/gorilla/websocket/join.go
generated
vendored
@ -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
|
||||
}
|
60
vendor/github.com/gorilla/websocket/json.go
generated
vendored
60
vendor/github.com/gorilla/websocket/json.go
generated
vendored
@ -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
|
||||
}
|
54
vendor/github.com/gorilla/websocket/mask.go
generated
vendored
54
vendor/github.com/gorilla/websocket/mask.go
generated
vendored
@ -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
|
||||
}
|
15
vendor/github.com/gorilla/websocket/mask_safe.go
generated
vendored
15
vendor/github.com/gorilla/websocket/mask_safe.go
generated
vendored
@ -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
|
||||
}
|
102
vendor/github.com/gorilla/websocket/prepared.go
generated
vendored
102
vendor/github.com/gorilla/websocket/prepared.go
generated
vendored
@ -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 }
|
77
vendor/github.com/gorilla/websocket/proxy.go
generated
vendored
77
vendor/github.com/gorilla/websocket/proxy.go
generated
vendored
@ -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
|
||||
}
|
363
vendor/github.com/gorilla/websocket/server.go
generated
vendored
363
vendor/github.com/gorilla/websocket/server.go
generated
vendored
@ -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)]
|
||||
}
|
19
vendor/github.com/gorilla/websocket/trace.go
generated
vendored
19
vendor/github.com/gorilla/websocket/trace.go
generated
vendored
@ -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
|
||||
}
|
12
vendor/github.com/gorilla/websocket/trace_17.go
generated
vendored
12
vendor/github.com/gorilla/websocket/trace_17.go
generated
vendored
@ -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)
|
||||
}
|
283
vendor/github.com/gorilla/websocket/util.go
generated
vendored
283
vendor/github.com/gorilla/websocket/util.go
generated
vendored
@ -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
|
||||
}
|
473
vendor/github.com/gorilla/websocket/x_net_proxy.go
generated
vendored
473
vendor/github.com/gorilla/websocket/x_net_proxy.go
generated
vendored
@ -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
|
||||
}
|
21
vendor/github.com/lithammer/fuzzysearch/LICENSE
generated
vendored
21
vendor/github.com/lithammer/fuzzysearch/LICENSE
generated
vendored
@ -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
Loading…
Reference in New Issue
Block a user