1
0
mirror of https://github.com/Rudi9719/kbtui.git synced 2026-03-22 17:57:23 +00:00

84 Commits

Author SHA1 Message Date
97e055dd61 Sort conversationSlice 2020-01-28 12:43:19 -05:00
9b6bd7442b Merge pull request #55 from Rudi9719/dev
Dev to Master
2020-01-08 09:46:36 -05:00
ae9f71be96 Remove comment for debugging print 2019-12-26 12:40:25 -05:00
88a6f709f9 Merge branch 'dev' of https://github.com/rudi9719/kbtui into dev 2019-12-20 12:02:36 -05:00
f222464849 Multi platform (#54)
* Test go.yaml

* Update go.yml

* Update go.yml

* Update go.yml
2019-12-20 12:02:04 -05:00
f55270f423 Rudi9719 workflow (#53)
* Update go.yml

* Update go.yml
2019-12-20 10:59:21 -05:00
67fc7c6e5d Check if api.Result is nil before reading it in populateChat() 2019-12-20 09:15:55 -05:00
fc48ad5c4a Add replies to messages 2019-12-20 08:44:51 -05:00
66e2552a7e Don't error out on api errors 2019-12-19 12:19:34 -05:00
9714e39751 Merge pull request #52 from Rudi9719/go-workflow
Go workflow
2019-12-18 14:01:18 -05:00
8abde78bf7 Update go.yml 2019-12-18 13:43:03 -05:00
ff120c0366 Create go.yml 2019-12-18 13:39:00 -05:00
0a4c2614a8 Fix multiline code to have newlines for mkbot gameroom etc 2019-12-18 13:35:14 -05:00
12d41c018e Merge pull request #50 from erAck/fix-home-expansion
"~/" does not work as hoped for
2019-12-16 07:48:00 -05:00
9a71d50ab7 Remove unused import 2019-12-06 11:29:47 -05:00
db08278780 Remove auto-RAMRAPE and instead have it triggered on command 2019-12-06 11:24:28 -05:00
84e5beada4 "~/" does not work as hoped for
i.e. it is expanded only by a shell, not a system call, so
~/.config/kbtui.toml was never used.
2019-12-03 21:22:16 +01:00
cd79a10528 Fix ReplaceAll so that standard repo go can compile 2019-12-02 18:02:29 -05:00
a5f017de45 Merge pull request #48 from C0DK/show-follow
Show follow
2019-12-01 15:14:37 -05:00
5c5ebc1d45 Merge branch 'dev' into show-follow 2019-12-01 15:14:30 -05:00
3d353b33e8 Merge pull request #49 from C0DK/fix-formatting-shit
minor formatting fixes
2019-12-01 15:12:26 -05:00
023f22a1ea More User Info
Added:
- `/inspect` (`/id`)
- `/follow`
- `/unfollow`
2019-12-01 20:42:59 +01:00
bc7ef238de minor formatting fixes 2019-11-28 20:24:09 +01:00
e5285d9d36 Merge pull request #47 from Rudi9719/dev
Dev
2019-11-19 07:42:34 -05:00
e91506d319 Merge branch 'dev' of https://github.com/rudi9719/kbtui into dev 2019-11-13 19:11:28 +00:00
dc67abb1a3 Merge pull request #45 from C0DK/format-fixes
Revised some styling elements
2019-11-13 11:57:43 -05:00
1d95fea9f3 Revised some styling elements
- Colored block codes in a whole block
- Colored quotes
- Fixed a few bugs
2019-11-13 17:47:45 +01:00
74da5c530e Merge pull request #43 from Rudi9719/Rudi9719-ebuild
Rudi9719 ebuild
2019-11-08 09:41:05 -05:00
430c427487 Delete go.sum 2019-11-08 09:40:45 -05:00
d02dd317a5 Delete go.mod 2019-11-08 09:40:20 -05:00
90d336988e Merge pull request #41 from Rudi9719/dev
Dev
2019-11-01 10:14:07 -04:00
69b5442ac9 Merge pull request #39 from haukened/fix-list-populate
Fix list populate
2019-11-01 10:06:30 -04:00
27261223f0 updated populateChat() to call populateList() to mark chat messages as read when joining a channel or refreshing the chat 2019-10-31 08:48:16 -06:00
aa01e9cb40 Fix <code> tag formatting to show code properly 2019-10-31 08:39:39 -04:00
e4784e4fb2 updated function names 2019-10-30 12:51:04 -06:00
80896e5323 Merge remote-tracking branch 'upstream/dev' into feature-scrolling 2019-10-30 12:03:34 -06:00
e1c8890721 updated to not scroll past the bottom of the chat 2019-10-30 09:54:26 -06:00
4ac523e7f7 Merge pull request #36 from haukened/dev
Adding `/exec` functionality
2019-10-30 08:37:38 -04:00
37dc6b8d4f added autoScrollView() on joining channel 2019-10-29 18:21:36 -06:00
60c5302aec added scrolling functionality 2019-10-29 18:21:06 -06:00
bf0c271d2a updated to use instead of 2019-10-29 16:01:17 -06:00
5e6e97d7f6 updated to have lastChat var and new keybinding for CTRL+z 2019-10-29 15:37:01 -06:00
4a7818c79e updated to fill in lastChat 2019-10-29 15:31:30 -06:00
f890a6e56c added initial cmdExec 2019-10-29 15:31:00 -06:00
e60f65d59c updated to remove old emoji and add execcmd 2019-10-29 13:53:25 -06:00
938d6c855c Merge pull request #34 from haukened/dev
Unicode Emoji Support
2019-10-28 18:19:11 -04:00
549e9f8ad6 removed UNICODE_EMOJI_SUPPORT var in favor of new config schema 2019-10-28 15:59:03 -06:00
a03b5ff630 updated to use new config variable 2019-10-28 15:58:33 -06:00
e67b0e4265 resolving merge conflicts 2019-10-28 15:46:05 -06:00
d74ac7b1e1 re-adding config file at community request 2019-10-28 15:38:27 -06:00
e1f0314b87 Merge branch 'fix/go-modules' of https://github.com/Tjzabel/kbtui into dev 2019-10-28 16:53:58 -04:00
384d0cfd9c Merge branch 'tab-fix-01' of https://github.com/haukened/kbtui into dev 2019-10-28 16:47:18 -04:00
33551d9c8b Merge branch 'ColorsAndConfigCleanup' of keybase://team/kbtui.dev.dxb/kbtui into dev 2019-10-28 16:33:44 -04:00
Sam
876668d3aa Use github.com/pelletier/go-toml instead of github.com/BurntSushi/toml 2019-10-28 15:23:41 -04:00
Sam
05afd348d0 Load default config first, so missing values remain default 2019-10-26 22:16:37 -04:00
Sam
a8e0ed17b9 Add unicode_emojis flag to config 2019-10-26 21:38:01 -04:00
Sam
99922c2582 Update config to be more uniform 2019-10-26 21:37:02 -04:00
0812c57a6a added travis to gitignore 2019-10-26 15:31:16 -06:00
8c0e6211aa set error message back to the way it was. was trying a change to track an error 2019-10-26 15:13:36 -06:00
de5f7da139 fixed error where unicode emojis were missing 2019-10-26 14:54:43 -06:00
2fb6bb0bc0 added emoji unicode and resolution support 2019-10-26 14:13:03 -06:00
4cc825860e added emoji config file support 2019-10-26 14:12:46 -06:00
1e6e34fbc5 removed config file, added to gitignore 2019-10-26 13:40:11 -06:00
de05697c89 reverting main.go to upstream 2019-10-26 13:32:02 -06:00
bb3ec46a43 Merge branch 'tab-fix-01' into dev 2019-10-26 13:25:16 -06:00
790d295e2e fixes issue with tab completion where commands would not clear input box titles 2019-10-26 13:22:59 -06:00
2484578775 Merge remote-tracking branch 'upstream/dev' into dev 2019-10-26 13:08:32 -06:00
Sam
33a68670c9 Clean up colors, clean up config, make config actually be used, add default config 2019-10-25 17:18:50 -04:00
e017ffcb55 Add support for Go modules. 2019-10-25 14:46:06 -04:00
0c866e24c4 Go build tags are space separated. 2019-10-25 14:45:53 -04:00
e033215cc9 Merge branch 'FixColorRegex' of keybase://team/kbtui.dev.dxb/kbtui into dev 2019-10-25 11:16:06 -04:00
Sam
f09f2969d9 Fix bug in colorRegex() that was highighting usernames in the middle of strings when a user wasn't actually being mentioned 2019-10-24 22:23:25 -04:00
7408db2625 fixed replacements to use regex so to not break strings 2019-10-23 16:59:46 -06:00
299f5103a5 upstream merge 2019-10-23 16:01:54 -06:00
e692500606 removed os.Args in favor of flags functionality. created emoji flag 2019-10-20 15:54:17 -06:00
b0ea0c0b63 added notes 2019-10-20 15:53:40 -06:00
0ba2507891 added error handling 2019-10-20 14:35:43 -06:00
bd02981b52 new funcs to render unicode emojis and resolve emoji aliases 2019-10-20 14:33:10 -06:00
6d86fdd578 Added unicode emoji support global 2019-10-19 15:17:26 -06:00
f7f0e61ecd fixed bad return statement 2019-10-19 14:57:11 -06:00
0ccf655aa4 fixed missing struct member 2019-10-19 14:54:45 -06:00
f65ac0e231 updated to use new static emoji map for tab completion 2019-10-19 14:53:14 -06:00
517e8ecd46 added static emoji map with unicode and descriptions 2019-10-19 14:52:55 -06:00
dd81d08db7 removed dynamic emoji building 2019-10-19 14:52:05 -06:00
27 changed files with 1083 additions and 483 deletions

40
.github/workflows/go.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Go
on: [push]
jobs:
build:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
name: Build
runs-on: ${{ matrix.platform }}
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Get dependencies
run: |
go get -v -t -d ./...
go get github.com/magefile/mage
- name: Build
run: go run build.go buildbeta
- name: Upload Artifact
if: matrix.platform != 'windows-latest'
uses: actions/upload-artifact@v1.0.0
with:
name: kbtui-${{ matrix.platform }}-buildbeta
path: kbtui
- name: Upload Artifact
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v1.0.0
with:
name: kbtui-${{ matrix.platform }}-buildbeta
path: kbtui.exe

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ emojiList.go
.idea/* .idea/*
.idea .idea
*.log *.log
.travis.yml

93
cmdConfig.go Normal file
View File

@ -0,0 +1,93 @@
// +build !rm_basic_commands allcommands setcmd
package main
import (
"fmt"
"io/ioutil"
"os"
"github.com/pelletier/go-toml"
)
func init() {
command := Command{
Cmd: []string{"config"},
Description: "Change various settings",
Help: "",
Exec: cmdConfig,
}
RegisterCommand(command)
}
func cmdConfig(cmd []string) {
var err error
switch {
case len(cmd) == 2:
if cmd[1] == "load" {
config, err = readConfig()
if err != nil {
printError(err.Error())
return
}
printInfoF("Config file loaded: $TEXT", config.Colors.Feed.File.stylize(config.filepath))
return
}
case len(cmd) > 2:
if cmd[1] == "load" {
config, err = readConfig(cmd[3])
if err != nil {
printError(err.Error())
return
}
printInfoF("Config file loaded: $TEXT", config.Colors.Feed.File.stylize(config.filepath))
return
}
}
printError("Must pass a valid command")
}
func readConfig(filepath ...string) (*Config, error) {
var result = new(Config)
var configFile, path string
var env bool
// Load default config first, this way any values missing from the provided config file will remain the default value
d := []byte(defaultConfig)
toml.Unmarshal(d, result)
switch len(filepath) {
case 0:
configFile, env = os.LookupEnv("KBTUI_CFG")
if !env {
path, env = os.LookupEnv("HOME")
if env {
configFile = path + "/.config/kbtui.toml"
if _, err := os.Stat(configFile); os.IsNotExist(err) {
configFile = "kbtui.toml"
}
} else {
configFile = "kbtui.toml"
}
}
default:
configFile = filepath[0]
if _, err := os.Stat(configFile); os.IsNotExist(err) {
return result, fmt.Errorf("Unable to load config: %s not found", configFile)
}
}
f, err := ioutil.ReadFile(configFile)
if err != nil {
f = []byte(defaultConfig)
}
err = toml.Unmarshal(f, result)
if err != nil {
return result, err
}
result.filepath = configFile
return result, nil
}

View File

@ -21,7 +21,7 @@ func init() {
func cmdDownloadFile(cmd []string) { func cmdDownloadFile(cmd []string) {
if len(cmd) < 2 { if len(cmd) < 2 {
printInfo(fmt.Sprintf("%s%s $messageId $fileName - Download a file to user's downloadpath", cmdPrefix, cmd[0])) printInfo(fmt.Sprintf("%s%s $messageId $fileName - Download a file to user's downloadpath", config.Basics.CmdPrefix, cmd[0]))
return return
} }
messageID, err := strconv.Atoi(cmd[1]) messageID, err := strconv.Atoi(cmd[1])
@ -46,11 +46,12 @@ func cmdDownloadFile(cmd []string) {
fileName = api.Result.Messages[0].Msg.Content.Attachment.Object.Filename fileName = api.Result.Messages[0].Msg.Content.Attachment.Object.Filename
} }
_, err = chat.Download(messageID, fmt.Sprintf("%s/%s", downloadPath, fileName)) _, err = chat.Download(messageID, fmt.Sprintf("%s/%s", config.Basics.DownloadPath, fileName))
channelName := messageLinkKeybaseColor.stylize(channel.Name) channelName := config.Colors.Message.LinkKeybase.stylize(channel.Name)
fileNameStylizied := config.Colors.Feed.File.stylize(fileName)
if err != nil { if err != nil {
printErrorF(fmt.Sprintf("There was an error downloading %s from $TEXT", fileName), channelName) printErrorF("There was an error downloading $TEXT from $TEXT", fileNameStylizied, channelName)
} else { } else {
printInfoF(fmt.Sprintf("Downloaded %s from $TEXT", fileName), channelName) printInfoF("Downloaded $TEXT from $TEXT", fileNameStylizied, channelName)
} }
} }

55
cmdExec.go Normal file
View File

@ -0,0 +1,55 @@
// +build !rm_basic_commands allcommands execcmd
package main
import (
"fmt"
"strings"
)
func init() {
command := Command{
Cmd: []string{"exec", "ex"},
Description: "$keybase args - executes keybase $args and returns the output",
Help: "",
Exec: cmdExec,
}
RegisterCommand(command)
}
func cmdExec(cmd []string) {
l := len(cmd)
switch {
case l >= 2:
if cmd[1] == "keybase" {
// if the user types /exec keybase wallet list
// only send ["wallet", "list"]
runKeybaseExec(cmd[2:])
} else {
// send everything except the command
runKeybaseExec(cmd[1:])
}
case l == 1:
fallthrough
default:
printExecHelp()
}
}
func runKeybaseExec(args []string) {
outputBytes, err := k.Exec(args...)
if err != nil {
printToView("Feed", fmt.Sprintf("Exec error: %+v", err))
} else {
channel.Name = ""
// unjoin the chat
clearView("Chat")
setViewTitle("Input", fmt.Sprintf(" /exec %s ", strings.Join(args, " ")))
output := string(outputBytes)
printToView("Chat", fmt.Sprintf("%s", output))
}
}
func printExecHelp() {
printInfo(fmt.Sprintf("To execute a keybase command use %sexec <keybase args>", config.Basics.CmdPrefix))
}

34
cmdFollow.go Normal file
View File

@ -0,0 +1,34 @@
// +build !rm_basic_commands allcommands followcmd
package main
import (
"fmt"
)
func init() {
command := Command{
Cmd: []string{"follow"},
Description: "$username - Follows the given user",
Help: "",
Exec: cmdFollow,
}
RegisterCommand(command)
}
func cmdFollow(cmd []string) {
if len(cmd) == 2 {
go follow(cmd[1])
} else {
printFollowHelp()
}
}
func follow(username string) {
k.Exec("follow", username, "-y")
printInfoF("Now follows $TEXT", config.Colors.Message.LinkKeybase.stylize(username))
followedInSteps[username] = 1
}
func printFollowHelp() {
printInfo(fmt.Sprintf("To follow a user use %sfollow <username>", config.Basics.CmdPrefix))
}

View File

@ -25,7 +25,7 @@ func cmdHelp(cmd []string) {
if len(cmd) == 1 { if len(cmd) == 1 {
sort.Strings(baseCommands) sort.Strings(baseCommands)
for _, c := range baseCommands { for _, c := range baseCommands {
helpText = fmt.Sprintf("%s%s%s\t\t%s\n", helpText, cmdPrefix, c, commands[c].Description) helpText = fmt.Sprintf("%s%s%s\t\t%s\n", helpText, config.Basics.CmdPrefix, c, commands[c].Description)
} }
if len(typeCommands) > 0 { if len(typeCommands) > 0 {
for c := range typeCommands { for c := range typeCommands {

135
cmdInspect.go Normal file
View File

@ -0,0 +1,135 @@
// +build !rm_basic_commands allcommands inspectcmd
package main
import (
"fmt"
"regexp"
"samhofi.us/x/keybase"
"strconv"
"strings"
)
func init() {
command := Command{
Cmd: []string{"inspect", "id"},
Description: "$identifier - shows info about $identifier ($identifier is either username, messageId or team)",
Help: "",
Exec: cmdInspect,
}
RegisterCommand(command)
}
func cmdInspect(cmd []string) {
if len(cmd) == 2 {
regexIsNumeric := regexp.MustCompile(`^\d+$`)
if regexIsNumeric.MatchString(cmd[1]) {
// Then it must be a message id
id, _ := strconv.Atoi(cmd[1])
go printMessage(id)
} else {
go printUser(strings.Replace(cmd[1], "@", "", -1))
}
} else {
printInfo(fmt.Sprintf("To inspect something use %sid <username/messageId>", config.Basics.CmdPrefix))
}
}
func printMessage(id int) {
chat := k.NewChat(channel)
messages, err := chat.ReadMessage(id)
if err == nil {
var response StyledString
if messages != nil && len((*messages).Result.Messages) > 0 {
message := (*messages).Result.Messages[0].Msg
var apiCast keybase.ChatAPI
apiCast.Msg = &message
response = formatOutput(apiCast)
} else {
response = config.Colors.Feed.Error.stylize("message not found")
}
printToView("Chat", response.string())
}
}
func formatProofs(userLookup keybase.UserAPI) StyledString {
messageColor := config.Colors.Message
message := basicStyle.stylize("")
for _, proof := range userLookup.Them[0].ProofsSummary.All {
style := config.Colors.Feed.Success
if proof.State != 1 {
style = config.Colors.Feed.Error
}
proofString := style.stylize("Proof [$NAME@$SITE]: $URL\n")
proofString = proofString.replace("$NAME", messageColor.SenderDefault.stylize(proof.Nametag))
proofString = proofString.replace("$SITE", messageColor.SenderDevice.stylize(proof.ProofType))
proofString = proofString.replace("$URL", messageColor.LinkURL.stylize(proof.HumanURL))
message = message.append(proofString)
}
return message.appendString("\n")
}
func formatProfile(userLookup keybase.UserAPI) StyledString {
messageColor := config.Colors.Message
user := userLookup.Them[0]
profileText := messageColor.Body.stylize("Name: $FNAME\nLocation: $LOC\nBio: $BIO\n")
profileText = profileText.replaceString("$FNAME", user.Profile.FullName)
profileText = profileText.replaceString("$LOC", user.Profile.Location)
profileText = profileText.replaceString("$BIO", user.Profile.Bio)
return profileText
}
func formatFollowState(userLookup keybase.UserAPI) StyledString {
username := userLookup.Them[0].Basics.Username
followSteps := followedInSteps[username]
if followSteps == 1 {
return config.Colors.Feed.Success.stylize("<Followed!>\n\n")
} else if followSteps > 1 {
var steps []string
for head := username; head != ""; head = trustTreeParent[head] {
steps = append(steps, fmt.Sprintf("[%s]", head))
}
trustLine := fmt.Sprintf("Indirect follow: <%s>\n\n", strings.Join(steps, " Followed by "))
return config.Colors.Message.Body.stylize(trustLine)
}
return basicStyle.stylize("")
}
func formatFollowerAndFollowedList(username string, listType string) StyledString {
messageColor := config.Colors.Message
response := basicStyle.stylize("")
bytes, _ := k.Exec("list-"+listType, username)
bigString := string(bytes)
lines := strings.Split(bigString, "\n")
response = response.appendString(fmt.Sprintf("%s (%d): ", listType, len(lines)-1))
for i, user := range lines[:len(lines)-1] {
if i != 0 {
response = response.appendString(", ")
}
response = response.append(messageColor.LinkKeybase.stylize(user))
response = response.append(getUserFlags(user))
}
return response.appendString("\n\n")
}
func printUser(username string) {
messageColor := config.Colors.Message
userLookup, _ := k.UserLookup(username)
response := messageColor.Header.stylize("[Inspecting `$USER`]\n")
response = response.replace("$USER", messageColor.SenderDefault.stylize(username))
response = response.append(formatProfile(userLookup))
response = response.append(formatFollowState(userLookup))
response = response.append(formatProofs(userLookup))
response = response.append(formatFollowerAndFollowedList(username, "followers"))
response = response.append(formatFollowerAndFollowedList(username, "following"))
printToView("Chat", response.string())
}

View File

@ -41,12 +41,14 @@ func cmdJoin(cmd []string) {
channel.TopicName = "" channel.TopicName = ""
channel.MembersType = keybase.USER channel.MembersType = keybase.USER
} }
printInfoF("You are joining: $TEXT", messageLinkKeybaseColor.stylize(joinedName)) printInfoF("You are joining: $TEXT", config.Colors.Message.LinkKeybase.stylize(joinedName))
clearView("Chat") clearView("Chat")
setViewTitle("Input", fmt.Sprintf(" %s ", joinedName)) setViewTitle("Input", fmt.Sprintf(" %s ", joinedName))
lastChat = joinedName
autoScrollView("Chat")
go populateChat() go populateChat()
default: default:
printInfo(fmt.Sprintf("To join a team use %sjoin <team> <channel>", cmdPrefix)) printInfo(fmt.Sprintf("To join a team use %sjoin <team> <channel>", config.Basics.CmdPrefix))
printInfo(fmt.Sprintf("To join a PM use %sjoin <user>", cmdPrefix)) printInfo(fmt.Sprintf("To join a PM use %sjoin <user>", config.Basics.CmdPrefix))
} }
} }

View File

@ -22,7 +22,7 @@ func init() {
func cmdReply(cmd []string) { func cmdReply(cmd []string) {
chat := k.NewChat(channel) chat := k.NewChat(channel)
if len(cmd) < 2 { if len(cmd) < 2 {
printInfo(fmt.Sprintf("%s%s $ID - Reply to message $ID", cmdPrefix, cmd[0])) printInfo(fmt.Sprintf("%s%s $ID - Reply to message $ID", config.Basics.CmdPrefix, cmd[0]))
return return
} }
messageID, err := strconv.Atoi(cmd[1]) messageID, err := strconv.Atoi(cmd[1])

125
cmdSet.go
View File

@ -1,125 +0,0 @@
// +build !rm_basic_commands allcommands setcmd
package main
import (
"fmt"
"os"
"strings"
"github.com/pelletier/go-toml"
)
func init() {
command := Command{
Cmd: []string{"set", "config"},
Description: "Change various settings",
Help: "",
Exec: cmdSet,
}
RegisterCommand(command)
}
func printSetting(cmd []string) {
switch cmd[1] {
case "load":
loadFromToml()
case "downloadPath":
printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], downloadPath))
case "outputFormat":
printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], outputFormat))
case "dateFormat":
printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], dateFormat))
case "timeFormat":
printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], timeFormat))
case "cmdPrefix":
printInfo(fmt.Sprintf("Setting for %s -> %s", cmd[1], cmdPrefix))
default:
printError(fmt.Sprintf("Unknown config value %s", cmd[1]))
}
}
func cmdSet(cmd []string) {
if len(cmd) < 2 {
printError("No config value specified")
return
}
if len(cmd) < 3 {
printSetting(cmd)
return
}
switch cmd[1] {
case "downloadPath":
if len(cmd) != 3 {
printError("Invalid download path.")
}
downloadPath = cmd[2]
case "outputFormat":
outputFormat = strings.Join(cmd[1:], " ")
case "dateFormat":
dateFormat = strings.Join(cmd[1:], " ")
case "timeFormat":
timeFormat = strings.Join(cmd[1:], " ")
case "cmdPrefix":
cmdPrefix = cmd[2]
default:
printError(fmt.Sprintf("Unknown config value %s", cmd[1]))
}
}
func loadFromToml() {
configFile, env := os.LookupEnv("KBTUI_CFG")
if !env {
configFile = "~/.config/kbtui.toml"
if _, err := os.Stat(configFile); os.IsNotExist(err) {
configFile = "kbtui.toml"
}
}
printInfoF("Loading config from toml: $TEXT", messageAttachmentColor.stylize(configFile))
config, err := toml.LoadFile(configFile)
if err != nil {
printError(fmt.Sprintf("Could not read config file: %+v", err))
return
}
colorless = config.GetDefault("Basics.colorless", false).(bool)
if config.Has("Basics.colorless") {
colorless = config.Get("Basics.colorless").(bool)
}
if config.Has("Basics.downloadPath") {
downloadPath = config.Get("Basics.downloadPath").(string)
}
if config.Has("Basics.cmdPrefix") {
cmdPrefix = config.Get("Basics.cmdPrefix").(string)
}
if config.Has("Formatting.outputFormat") {
outputFormat = config.Get("Formatting.outputFormat").(string)
}
if config.Has("Formatting.dateFormat") {
dateFormat = config.Get("Formatting.dateFormat").(string)
}
if config.Has("Formatting.timeFormat") {
timeFormat = config.Get("Formatting.timeFormat").(string)
}
channelsColor = styleFromConfig(config, "channels.basic")
channelsHeaderColor = styleFromConfig(config, "channels.header")
channelUnreadColor = styleFromConfig(config, "channels.unread")
mentionColor = styleFromConfig(config, "message.mention")
messageHeaderColor = styleFromConfig(config, "message.header")
messageIDColor = styleFromConfig(config, "message.id")
messageTimeColor = styleFromConfig(config, "message.time")
messageSenderDefaultColor = styleFromConfig(config, "message.sender_default")
messageSenderDeviceColor = styleFromConfig(config, "message.sender_device")
messageBodyColor = styleFromConfig(config, "message.body")
messageAttachmentColor = styleFromConfig(config, "message.attachment")
messageLinkURLColor = styleFromConfig(config, "message.link_url")
messageLinkKeybaseColor = styleFromConfig(config, "message.link_keybase")
messageReactionColor = styleFromConfig(config, "message.reaction")
messageCodeColor = styleFromConfig(config, "message.code")
feedColor = styleFromConfig(config, "feed.basic")
errorColor = styleFromConfig(config, "feed.error")
RunCommand("clean")
}

View File

@ -22,6 +22,6 @@ func cmdStream(cmd []string) {
channel.Name = "" channel.Name = ""
printInfo("You are now viewing the formatted stream") printInfo("You are now viewing the formatted stream")
setViewTitle("Input", fmt.Sprintf(" Stream - Not in a chat. %sj to join ", cmdPrefix)) setViewTitle("Input", fmt.Sprintf(" Stream - Not in a chat. %sj to join ", config.Basics.CmdPrefix))
clearView("Chat") clearView("Chat")
} }

19
cmdTags.go Normal file
View File

@ -0,0 +1,19 @@
// +ignore
// +build allcommands tagscmd
package main
func init() {
command := Command{
Cmd: []string{"tags", "map"},
Description: "$- Create map of users following users, to populate $TAGS",
Help: "",
Exec: cmdTags,
}
RegisterCommand(command)
}
func cmdTags(cmd []string) {
go generateFollowersList()
}

33
cmdUnfollow.go Normal file
View File

@ -0,0 +1,33 @@
// +build !rm_basic_commands allcommands followcmd
package main
import (
"fmt"
)
func init() {
command := Command{
Cmd: []string{"unfollow"},
Description: "$username - Unfollows the given user",
Help: "",
Exec: cmdUnfollow,
}
RegisterCommand(command)
}
func cmdUnfollow(cmd []string) {
if len(cmd) == 2 {
go unfollow(cmd[1])
} else {
printUnfollowHelp()
}
}
func unfollow(username string) {
k.Exec("unfollow", username)
printInfoF("Now unfollows $TEXT", config.Colors.Message.LinkKeybase.stylize(username))
}
func printUnfollowHelp() {
printInfo(fmt.Sprintf("To unfollow a user use %sunfollow <username>", config.Basics.CmdPrefix))
}

View File

@ -21,7 +21,7 @@ func init() {
func cmdUploadFile(cmd []string) { func cmdUploadFile(cmd []string) {
if len(cmd) < 2 { if len(cmd) < 2 {
printInfo(fmt.Sprintf("%s%s $filePath $fileName - Upload file from absolute path with optional name", cmdPrefix, cmd[0])) printInfo(fmt.Sprintf("%s%s $filePath $fileName - Upload file from absolute path with optional name", config.Basics.CmdPrefix, cmd[0]))
return return
} }
filePath := cmd[1] filePath := cmd[1]
@ -40,10 +40,11 @@ func cmdUploadFile(cmd []string) {
} }
chat := k.NewChat(channel) chat := k.NewChat(channel)
_, err := chat.Upload(fileName, filePath) _, err := chat.Upload(fileName, filePath)
channelName := messageLinkKeybaseColor.stylize(channel.Name).string() channelName := config.Colors.Message.LinkKeybase.stylize(channel.Name)
fileNameStylized := config.Colors.Feed.File.stylize(filePath)
if err != nil { if err != nil {
printError(fmt.Sprintf("There was an error uploading %s to %s\n%+v", filePath, channelName, err)) printError(fmt.Sprintf("There was an error uploading %s to %s\n%+v", filePath, channel.Name, err))
} else { } else {
printInfo(fmt.Sprintf("Uploaded %s to %s", filePath, channelName)) printInfoF("Uploaded $TEXT to $TEXT", fileNameStylized, channelName)
} }
} }

View File

@ -65,7 +65,7 @@ func cmdPopulateWall(cmd []string) {
return return
} }
printInfoF("Displaying public messages for user $TEXT", messageLinkKeybaseColor.stylize(requestedUsers)) printInfoF("Displaying public messages for user $TEXT", config.Colors.Message.LinkKeybase.stylize(requestedUsers))
for _, chann := range users { for _, chann := range users {
chat := k.NewChat(chann) chat := k.NewChat(chann)
api, err := chat.Read() api, err := chat.Read()
@ -81,7 +81,7 @@ func cmdPopulateWall(cmd []string) {
apiCast.Msg = &api.Result.Messages[i].Msg apiCast.Msg = &api.Result.Messages[i].Msg
result[apiCast.Msg.SentAt] = apiCast result[apiCast.Msg.SentAt] = apiCast
newMessage := formatOutput(apiCast) newMessage := formatOutput(apiCast)
printMe = append(printMe, newMessage) printMe = append(printMe, newMessage.string())
} }
} }
@ -96,7 +96,7 @@ func cmdPopulateWall(cmd []string) {
sort.Ints(keys) sort.Ints(keys)
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
for _, k := range keys { for _, k := range keys {
actuallyPrintMe += formatOutput(result[k]) + "\n" actuallyPrintMe += formatOutput(result[k]).string() + "\n"
} }
printToView("Chat", fmt.Sprintf("\n<Wall>\n\n%s\nYour wall query took %s\n</Wall>\n", actuallyPrintMe, time.Since(start))) printToView("Chat", fmt.Sprintf("\n<Wall>\n\n%s\nYour wall query took %s\n</Wall>\n", actuallyPrintMe, time.Since(start)))
} }

239
colors.go
View File

@ -2,16 +2,12 @@ package main
import ( import (
"fmt" "fmt"
"github.com/pelletier/go-toml"
"regexp" "regexp"
"strings" "strings"
) )
// Begin Colors
type color int
const ( const (
black color = iota black int = iota
red red
green green
yellow yellow
@ -19,103 +15,89 @@ const (
magenta magenta
cyan cyan
grey grey
normal color = -1 normal int = -1
) )
func colorFromString(s string) color { var colorMapString = map[string]int{
s = strings.ToLower(s) "black": black,
switch s { "red": red,
case "black": "green": green,
return black "yellow": yellow,
case "red": "purple": purple,
return red "magenta": magenta,
case "green": "cyan": cyan,
return green "grey": grey,
case "yellow": "normal": normal,
return yellow
case "purple":
return purple
case "magenta":
return magenta
case "cyan":
return cyan
case "grey":
return grey
case "normal":
return normal
default:
printError(fmt.Sprintf("color `%s` cannot be parsed.", s))
} }
var colorMapInt = map[int]string{
black: "black",
red: "red",
green: "green",
yellow: "yellow",
purple: "purple",
magenta: "magenta",
cyan: "cyan",
grey: "grey",
normal: "normal",
}
func colorFromString(color string) int {
var result int
color = strings.ToLower(color)
result, ok := colorMapString[color]
if !ok {
return normal return normal
} }
return result
// Style struct for specializing the style/color of a stylize
type Style struct {
foregroundColor color
backgroundColor color
bold bool
italic bool // Currently not supported by the UI library
underline bool
strikethrough bool // Currently not supported by the UI library
inverse bool
} }
var basicStyle = Style{normal, normal, false, false, false, false, false} func colorFromInt(color int) string {
var result string
func styleFromConfig(config *toml.Tree, key string) Style { result, ok := colorMapInt[color]
key = "Colors." + key + "." if !ok {
style := basicStyle return "normal"
if config.Has(key + "foreground") {
style = style.withForeground(colorFromString(config.Get(key + "foreground").(string)))
} }
if config.Has(key + "background") { return result
style = style.withForeground(colorFromString(config.Get(key + "background").(string)))
}
if config.GetDefault(key+"bold", false).(bool) {
style = style.withBold()
}
if config.GetDefault(key+"italic", false).(bool) {
style = style.withItalic()
}
if config.GetDefault(key+"underline", false).(bool) {
style = style.withUnderline()
}
if config.GetDefault(key+"strikethrough", false).(bool) {
style = style.withStrikethrough()
}
if config.GetDefault(key+"inverse", false).(bool) {
style = style.withInverse()
} }
return style var basicStyle = Style{
Foreground: colorMapInt[normal],
Background: colorMapInt[normal],
Italic: false,
Bold: false,
Underline: false,
Strikethrough: false,
Inverse: false,
} }
func (s Style) withForeground(f color) Style { func (s Style) withForeground(color int) Style {
s.foregroundColor = f s.Foreground = colorFromInt(color)
return s return s
} }
func (s Style) withBackground(f color) Style { func (s Style) withBackground(color int) Style {
s.backgroundColor = f s.Background = colorFromInt(color)
return s return s
} }
func (s Style) withBold() Style { func (s Style) withBold() Style {
s.bold = true s.Bold = true
return s return s
} }
func (s Style) withInverse() Style { func (s Style) withInverse() Style {
s.inverse = true s.Inverse = true
return s return s
} }
func (s Style) withItalic() Style { func (s Style) withItalic() Style {
s.italic = true s.Italic = true
return s return s
} }
func (s Style) withStrikethrough() Style { func (s Style) withStrikethrough() Style {
s.strikethrough = true s.Strikethrough = true
return s return s
} }
func (s Style) withUnderline() Style { func (s Style) withUnderline() Style {
s.underline = true s.Underline = true
return s return s
} }
@ -123,33 +105,34 @@ func (s Style) withUnderline() Style {
// which essentially just adds on top. that is relevant in the case of // which essentially just adds on top. that is relevant in the case of
// bold/italic etc - it should add style - not clear. // bold/italic etc - it should add style - not clear.
func (s Style) toANSI() string { func (s Style) toANSI() string {
if colorless { if config.Basics.Colorless {
return "" return ""
} }
output := "\x1b[0m\x1b[0" styleSlice := []string{"0"}
if s.foregroundColor != normal {
output += fmt.Sprintf(";%d", 30+s.foregroundColor) if colorFromString(s.Foreground) != normal {
styleSlice = append(styleSlice, fmt.Sprintf("%d", 30+colorFromString(s.Foreground)))
} }
if s.backgroundColor != normal { if colorFromString(s.Background) != normal {
output += fmt.Sprintf(";%d", 40+s.backgroundColor) styleSlice = append(styleSlice, fmt.Sprintf("%d", 40+colorFromString(s.Background)))
} }
if s.bold { if s.Bold {
output += ";1" styleSlice = append(styleSlice, "1")
} }
if s.italic { if s.Italic {
output += ";3" styleSlice = append(styleSlice, "3")
} }
if s.underline { if s.Underline {
output += ";4" styleSlice = append(styleSlice, "4")
} }
if s.inverse { if s.Inverse {
output += ";7" styleSlice = append(styleSlice, "7")
} }
if s.strikethrough { if s.Strikethrough {
output += ";9" styleSlice = append(styleSlice, "9")
} }
return output + "m" return "\x1b[" + strings.Join(styleSlice, ";") + "m"
} }
// End Colors // End Colors
@ -161,6 +144,12 @@ type StyledString struct {
style Style style Style
} }
func (ss StyledString) withStyle(style Style) StyledString {
return StyledString{ss.message, style}
}
// TODO change StyledString to have styles at start-end indexes.
// TODO handle all formatting types // TODO handle all formatting types
func (s Style) sprintf(base string, parts ...StyledString) StyledString { func (s Style) sprintf(base string, parts ...StyledString) StyledString {
text := s.stylize(removeFormatting(base)) text := s.stylize(removeFormatting(base))
@ -177,50 +166,58 @@ func (s Style) sprintf(base string, parts ...StyledString) StyledString {
func (s Style) stylize(msg string) StyledString { func (s Style) stylize(msg string) StyledString {
return StyledString{msg, s} return StyledString{msg, s}
} }
func (t StyledString) stringFollowedByStyle(style Style) string { func (ss StyledString) stringFollowedByStyle(style Style) string {
return t.style.toANSI() + t.message + style.toANSI() return ss.style.toANSI() + ss.message + style.toANSI()
} }
func (t StyledString) string() string { func (ss StyledString) string() string {
return t.stringFollowedByStyle(basicStyle) return ss.stringFollowedByStyle(basicStyle)
} }
func (t StyledString) replace(match string, value StyledString) StyledString { func (ss StyledString) replace(match string, value StyledString) StyledString {
return t.replaceN(match, value, -1) return ss.replaceN(match, value, -1)
} }
func (t StyledString) replaceN(match string, value StyledString, n int) StyledString { func (ss StyledString) replaceN(match string, value StyledString, n int) StyledString {
t.message = strings.Replace(t.message, match, value.stringFollowedByStyle(t.style), n) ss.message = strings.Replace(ss.message, match, value.stringFollowedByStyle(ss.style), n)
return t return ss
} }
func (t StyledString) replaceString(match string, value string) StyledString { func (ss StyledString) replaceString(match string, value string) StyledString {
t.message = strings.Replace(t.message, match, value, -1) ss.message = strings.Replace(ss.message, match, value, -1)
return t return ss
}
func (t StyledString) replaceRegex(match string, value StyledString) StyledString {
var re = regexp.MustCompile("(" + match + ")")
t.message = re.ReplaceAllString(t.message, value.stringFollowedByStyle(t.style))
return t
} }
// Overrides current formatting // Overrides current formatting
func (t StyledString) colorRegex(match string, style Style) StyledString { func (ss StyledString) colorRegex(match string, style Style) StyledString {
re := regexp.MustCompile("(" + match + ")") return ss.regexReplaceFunc(match, func(subString string) string {
subStrings := re.FindAllString(t.message, -1) return style.stylize(removeFormatting(subString)).stringFollowedByStyle(ss.style)
for _, element := range subStrings { })
cleanSubstring := style.stylize(removeFormatting(element))
t.message = strings.Replace(t.message, element, cleanSubstring.stringFollowedByStyle(t.style), -1)
} }
return t
// Old versionreturn t.replaceRegex(match, style.stylize(`$1`)) // Replacer function takes the current match as input and should return how the match should be preseneted instead
func (ss StyledString) regexReplaceFunc(match string, replacer func(string) string) StyledString {
re := regexp.MustCompile(match)
locations := re.FindAllStringIndex(ss.message, -1)
var newMessage string
var prevIndex int
for _, loc := range locations {
newSubstring := replacer(ss.message[loc[0]:loc[1]])
newMessage += ss.message[prevIndex:loc[0]]
newMessage += newSubstring
prevIndex = loc[1]
}
// Append any string after the final match
newMessage += ss.message[prevIndex:len(ss.message)]
ss.message = newMessage
return ss
} }
// Appends the other stylize at the end, but retains same style // Appends the other stylize at the end, but retains same style
func (t StyledString) append(other StyledString) StyledString { func (ss StyledString) append(other StyledString) StyledString {
t.message = t.message + other.stringFollowedByStyle(t.style) ss.message = ss.message + other.stringFollowedByStyle(ss.style)
return t return ss
} }
func (t StyledString) appendString(other string) StyledString { func (ss StyledString) appendString(other string) StyledString {
t.message += other ss.message += other
return t return ss
} }
// Begin Formatting // Begin Formatting

87
defaultConfig.go Normal file
View File

@ -0,0 +1,87 @@
package main
var defaultConfig = `
[basics]
download_path = "/tmp/"
colorless = false
unicode_emojis = true
# The prefix before evaluating a command
cmd_prefix = "/"
[formatting]
# BASH-like PS1 variable equivalent
output_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_stream_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_mention_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG"
pm_format = "PM from $USER@$DEVICE: $MSG"
# 02 = Day, Jan = Month, 06 = Year
date_format = "02Jan06"
# 15 = hours, 04 = minutes, 05 = seconds
time_format = "15:04"
icon_following_user = "[*]"
icon_indirect_following_user = "[?]"
[colors]
[colors.channels]
[colors.channels.basic]
foreground = "normal"
[colors.channels.header]
foreground = "magenta"
bold = true
[colors.channels.unread]
foreground = "green"
italic = true
[colors.message]
[colors.message.body]
foreground = "normal"
[colors.message.header]
foreground = "grey"
bold = true
[colors.message.mention]
foreground = "green"
italic = true
bold = true
[colors.message.id]
foreground = "yellow"
bold = true
[colors.message.time]
foreground = "magenta"
bold = true
[colors.message.sender_default]
foreground = "cyan"
bold = true
[colors.message.sender_device]
foreground = "cyan"
bold = true
[colors.message.sender_tags]
foreground = "yellow"
[colors.message.attachment]
foreground = "red"
[colors.message.link_url]
foreground = "yellow"
[colors.message.link_keybase]
foreground = "cyan"
[colors.message.reaction]
foreground = "magenta"
bold = true
[colors.message.quote]
foreground = "green"
[colors.message.code]
foreground = "cyan"
background = "grey"
[colors.feed]
[colors.feed.basic]
foreground = "grey"
[colors.feed.error]
foreground = "red"
[colors.feed.success]
foreground = "green"
[colors.feed.file]
foreground = "yellow"
`

47
emojiMap.go Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,63 +1,83 @@
[Basics] [basics]
downloadPath = "/tmp/" download_path = "/tmp/"
colorless = false colorless = false
# The prefix before evaluating a command unicode_emojis = true
cmdPrefix = "/"
[Formatting] # The prefix before evaluating a command
cmd_prefix = "/"
[formatting]
# BASH-like PS1 variable equivalent # BASH-like PS1 variable equivalent
outputFormat = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" output_format = "$REPL┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_stream_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG"
output_mention_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG"
pm_format = "PM from $USER@$DEVICE: $MSG"
# 02 = Day, Jan = Month, 06 = Year # 02 = Day, Jan = Month, 06 = Year
dateFormat = "02Jan06" date_format = "02Jan06"
# 15 = hours, 04 = minutes, 05 = seconds # 15 = hours, 04 = minutes, 05 = seconds
timeFormat = "15:04" time_format = "15:04"
icon_following_user = "[*]"
icon_indirect_following_user = "[?]"
[Colors] [colors]
[Colors.channels] [colors.channels]
[Colors.channels.basic] [colors.channels.basic]
foreground = "normal" foreground = "normal"
[Colors.channels.header] [colors.channels.header]
foreground = "magenta" foreground = "magenta"
bold = true bold = true
[Colors.channels.unread] [colors.channels.unread]
foreground = "green" foreground = "green"
italic = true italic = true
[Colors.message] [colors.message]
[Colors.message.body] [colors.message.body]
foreground = "normal" foreground = "normal"
[Colors.message.header] [colors.message.header]
foreground = "grey" foreground = "grey"
[Colors.message.mention] bold = true
[colors.message.mention]
foreground = "green" foreground = "green"
italic = true italic = true
bold = true bold = true
[Colors.message.id] [colors.message.id]
foreground = "yellow" foreground = "yellow"
[Colors.message.time] bold = true
[colors.message.time]
foreground = "magenta" foreground = "magenta"
[Colors.message.sender_default] bold = true
[colors.message.sender_default]
foreground = "cyan" foreground = "cyan"
bold = true bold = true
[Colors.message.sender_device] [colors.message.sender_device]
foreground = "cyan" foreground = "cyan"
[Colors.message.attachment] bold = true
[colors.message.sender_tags]
foreground = "yellow"
[colors.message.attachment]
foreground = "red" foreground = "red"
[Colors.message.link_url] [colors.message.link_url]
foreground = "yellow" foreground = "yellow"
[Colors.message.link_keybase] [colors.message.link_keybase]
foreground = "yellow" foreground = "cyan"
[Colors.message.reaction] [colors.message.reaction]
foreground = "magenta" foreground = "magenta"
bold = true bold = true
[Colors.message.code] [colors.message.quote]
foreground = "green"
[colors.message.code]
foreground = "cyan" foreground = "cyan"
background = "grey" background = "grey"
[Colors.feed]
[Colors.feed.basic] [colors.feed]
[colors.feed.basic]
foreground = "grey" foreground = "grey"
[Colors.feed.error] [colors.feed.error]
foreground = "red" foreground = "red"
[colors.feed.success]
foreground = "green"
[colors.feed.file]
foreground = "yellow"

79
mage.go
View File

@ -3,65 +3,13 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"os" "os"
"strings"
"github.com/magefile/mage/mg" "github.com/magefile/mage/mg"
"github.com/magefile/mage/sh" "github.com/magefile/mage/sh"
) )
// emoji related constants
const emojiList = "https://raw.githubusercontent.com/CodeFreezr/emojo/master/db/v5/emoji-v5.json"
const emojiFileName = "emojiList.go"
// json parsing structure
type emoji struct {
Num int `json:"No"`
Emoji string `json:"Emoji"`
Category string `json:"Category"`
SubCategory string `json:"SubCategory"`
Unicode string `json:"Unicode"`
Name string `json:"Name"`
Tags string `json:"Tags"`
Shortcode string `json:"Shortcode"`
}
// This func downloaded and parses the emojis from online into a slice of all shortnames
// to be used as a lookup for tab completion for emojis
// this way the pull from GitHub only has to be done at build time.
func createEmojiSlice() ([]string, error) {
result, err := http.Get(emojiList)
if err != nil {
return nil, err
}
defer result.Body.Close()
emojiList, err := ioutil.ReadAll(result.Body)
if err != nil {
return nil, err
}
var emojis []emoji
if err := json.Unmarshal(emojiList, &emojis); err != nil {
return nil, err
}
var emojiSlice []string
for _, emj := range emojis {
if len(emj.Shortcode) == 0 || strings.Contains(emj.Shortcode, "_tone") {
// dont add them
continue
}
emojiSlice = append(emojiSlice, emj.Shortcode)
}
return emojiSlice, nil
}
func getRemotePackages() error { func getRemotePackages() error {
var packages = []string{ var packages = []string{
"samhofi.us/x/keybase", "samhofi.us/x/keybase",
@ -87,28 +35,6 @@ func exit(err error) {
} }
} }
// Build kbtui with emoji lookup support
func BuildEmoji() error {
mg.Deps(getRemotePackages)
emojis, err := createEmojiSlice()
if err != nil {
return err
}
f, err := os.Create(emojiFileName)
if err != nil {
return err
}
defer f.Close()
fileContent := fmt.Sprintf("package main\n\nvar emojiSlice = %#v", emojis)
_, err = f.WriteString(fileContent)
if err != nil {
return err
}
f.Sync()
return nil
}
// Build kbtui with just the basic commands. // Build kbtui with just the basic commands.
func Build() { func Build() {
mg.Deps(getRemotePackages) mg.Deps(getRemotePackages)
@ -157,7 +83,7 @@ func BuildAllCommands() {
// Build kbtui with all Commands and TypeCommands enabled. // Build kbtui with all Commands and TypeCommands enabled.
func BuildAllCommandsT() { func BuildAllCommandsT() {
mg.Deps(getRemotePackages) mg.Deps(getRemotePackages)
if err := sh.Run("go", "build", "-tags", "type_commands,allcommands"); err != nil { if err := sh.Run("go", "build", "-tags", "type_commands allcommands"); err != nil {
defer func() { defer func() {
exit(err) exit(err)
}() }()
@ -167,8 +93,7 @@ func BuildAllCommandsT() {
// Build kbtui with beta functionality // Build kbtui with beta functionality
func BuildBeta() { func BuildBeta() {
mg.Deps(getRemotePackages) mg.Deps(getRemotePackages)
mg.Deps(BuildEmoji) if err := sh.Run("go", "build", "-tags", "allcommands showreactionscmd tabcompletion execcmd"); err != nil {
if err := sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,emojiList,tabcompletion"); err != nil {
defer func() { defer func() {
exit(err) exit(err)
}() }()

245
main.go
View File

@ -4,11 +4,13 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"sort"
"strings" "strings"
"time" "time"
"github.com/awesome-gocui/gocui" "github.com/awesome-gocui/gocui"
"samhofi.us/x/keybase" "samhofi.us/x/keybase"
"unicode/utf8"
) )
var ( var (
@ -22,9 +24,12 @@ var (
channels []keybase.Channel channels []keybase.Channel
stream = false stream = false
lastMessage keybase.ChatAPI lastMessage keybase.ChatAPI
lastChat = ""
g *gocui.Gui g *gocui.Gui
) )
var config *Config
func main() { func main() {
if !k.LoggedIn { if !k.LoggedIn {
fmt.Println("You are not logged in.") fmt.Println("You are not logged in.")
@ -37,7 +42,7 @@ func main() {
} }
defer g.Close() defer g.Close()
g.SetManagerFunc(layout) g.SetManagerFunc(layout)
go RunCommand("config", "load") RunCommand("config", "load")
go populateList() go populateList()
go updateChatWindow() go updateChatWindow()
if len(os.Args) > 1 { if len(os.Args) > 1 {
@ -82,7 +87,7 @@ func layout(g *gocui.Gui) error {
chatView.Autoscroll = true chatView.Autoscroll = true
chatView.Wrap = true chatView.Wrap = true
welcomeText := basicStyle.stylize("Welcome $USER!\n\nYour chats will appear here.\nSupported commands are as follows:\n") welcomeText := basicStyle.stylize("Welcome $USER!\n\nYour chats will appear here.\nSupported commands are as follows:\n")
welcomeText = welcomeText.replace("$USER", mentionColor.stylize(k.Username)) welcomeText = welcomeText.replace("$USER", config.Colors.Message.Mention.stylize(k.Username))
fmt.Fprintln(chatView, welcomeText.string()) fmt.Fprintln(chatView, welcomeText.string())
RunCommand("help") RunCommand("help")
} }
@ -95,7 +100,7 @@ func layout(g *gocui.Gui) error {
} }
inputView.Editable = true inputView.Editable = true
inputView.Wrap = true inputView.Wrap = true
inputView.Title = fmt.Sprintf(" Not in a chat - write `%sj` to join", cmdPrefix) inputView.Title = fmt.Sprintf(" Not in a chat - write `%sj` to join", config.Basics.CmdPrefix)
g.Cursor = true g.Cursor = true
} }
if listView, err4 := g.SetView("List", 0, 0, maxX/2-maxX/3-1, maxY-1, 0); err4 != nil { if listView, err4 := g.SetView("List", 0, 0, maxX/2-maxX/3-1, maxY-1, 0); err4 != nil {
@ -107,7 +112,68 @@ func layout(g *gocui.Gui) error {
} }
return nil return nil
} }
func scrollViewUp(v *gocui.View) error {
scrollView(v, -1)
return nil
}
func scrollViewDown(v *gocui.View) error {
scrollView(v, 1)
return nil
}
func scrollView(v *gocui.View, delta int) error {
if v != nil {
_, y := v.Size()
ox, oy := v.Origin()
if oy+delta > strings.Count(v.ViewBuffer(), "\n")-y {
v.Autoscroll = true
} else {
v.Autoscroll = false
if err := v.SetOrigin(ox, oy+delta); err != nil {
return err
}
}
}
return nil
}
func autoScrollView(vn string) error {
v, err := g.View(vn)
if err != nil {
return err
} else if v != nil {
v.Autoscroll = true
}
return nil
}
func initKeybindings() error { func initKeybindings() error {
if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
cv, _ := g.View("Chat")
err := scrollViewUp(cv)
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
cv, _ := g.View("Chat")
err := scrollViewDown(cv)
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyEsc, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
autoScrollView("Chat")
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error { func(g *gocui.Gui, v *gocui.View) error {
input, err := getInputString("Input") input, err := getInputString("Input")
@ -122,6 +188,13 @@ func initKeybindings() error {
}); err != nil { }); err != nil {
return err return err
} }
if err := g.SetKeybinding("", gocui.KeyCtrlZ, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
cmdJoin([]string{"/join", lastChat})
return nil
}); err != nil {
return err
}
if err := g.SetKeybinding("Edit", gocui.KeyCtrlC, gocui.ModNone, if err := g.SetKeybinding("Edit", gocui.KeyCtrlC, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error { func(g *gocui.Gui, v *gocui.View) error {
popupView("Chat") popupView("Chat")
@ -255,17 +328,20 @@ func printError(message string) {
printErrorF(message) printErrorF(message)
} }
func printErrorF(message string, parts ...StyledString) { func printErrorF(message string, parts ...StyledString) {
printToView("Feed", errorColor.sprintf(removeFormatting(message), parts...).string()) printToView("Feed", config.Colors.Feed.Error.sprintf(removeFormatting(message), parts...).string())
} }
// this removes formatting // this removes formatting
func printInfo(message string) { func printInfo(message string) {
printInfoF(message) printInfoF(message)
} }
func printInfoStyledString(message StyledString) {
printInfoF("$TEXT", message)
}
// this removes formatting // this removes formatting
func printInfoF(message string, parts ...StyledString) { func printInfoF(message string, parts ...StyledString) {
printToView("Feed", feedColor.sprintf(removeFormatting(message), parts...).string()) printToView("Feed", config.Colors.Feed.Basic.sprintf(removeFormatting(message), parts...).string())
} }
func printToView(viewName string, message string) { func printToView(viewName string, message string) {
g.Update(func(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error {
@ -273,6 +349,10 @@ func printToView(viewName string, message string) {
if err != nil { if err != nil {
return err return err
} }
if config.Basics.UnicodeEmojis {
message = emojiUnicodeConvert(message)
}
fmt.Fprintf(updatingView, "%s\n", message) fmt.Fprintf(updatingView, "%s\n", message)
return nil return nil
}) })
@ -297,7 +377,7 @@ func populateChat() {
chat := k.NewChat(channel) chat := k.NewChat(channel)
maxX, _ := g.Size() maxX, _ := g.Size()
api, err := chat.Read(maxX / 2) api, err := chat.Read(maxX / 2)
if err != nil { if err != nil || api.Result == nil {
for _, testChan := range channels { for _, testChan := range channels {
if channel.Name == testChan.Name { if channel.Name == testChan.Name {
channel = testChan channel = testChan
@ -326,7 +406,7 @@ func populateChat() {
} }
var apiCast keybase.ChatAPI var apiCast keybase.ChatAPI
apiCast.Msg = &message.Msg apiCast.Msg = &message.Msg
newMessage := formatOutput(apiCast) newMessage := formatOutput(apiCast).string()
printMe = append(printMe, newMessage) printMe = append(printMe, newMessage)
} }
} }
@ -337,7 +417,7 @@ func populateChat() {
} }
} }
printToView("Chat", actuallyPrintMe) printToView("Chat", actuallyPrintMe)
go populateList()
} }
func populateList() { func populateList() {
_, maxY := g.Size() _, maxY := g.Size()
@ -345,19 +425,23 @@ func populateList() {
log.Printf("%+v", err) log.Printf("%+v", err)
} else { } else {
clearView("List") clearView("List")
var textBase = channelsColor.stylize("") conversationSlice := testVar.Result.Conversations
var recentPMs = textBase.append(channelsHeaderColor.stylize("---[PMs]---\n")) sort.SliceStable(conversationSlice, func(i, j int) bool {
return conversationSlice[i].ActiveAt > conversationSlice[j].ActiveAt
})
var textBase = config.Colors.Channels.Basic.stylize("")
var recentPMs = textBase.append(config.Colors.Channels.Header.stylize("---[PMs]---\n"))
var recentPMsCount = 0 var recentPMsCount = 0
var recentChannels = textBase.append(channelsHeaderColor.stylize("---[Teams]---\n")) var recentChannels = textBase.append(config.Colors.Channels.Header.stylize("---[Teams]---\n"))
var recentChannelsCount = 0 var recentChannelsCount = 0
for _, s := range testVar.Result.Conversations { for _, s := range conversationSlice {
channels = append(channels, s.Channel) channels = append(channels, s.Channel)
if s.Channel.MembersType == keybase.TEAM { if s.Channel.MembersType == keybase.TEAM {
recentChannelsCount++ recentChannelsCount++
if recentChannelsCount <= ((maxY - 2) / 3) { if recentChannelsCount <= ((maxY - 2) / 3) {
channel := fmt.Sprintf("%s\n\t#%s\n", s.Channel.Name, s.Channel.TopicName) channel := fmt.Sprintf("%s\n\t#%s\n", s.Channel.Name, s.Channel.TopicName)
if s.Unread { if s.Unread {
recentChannels = recentChannels.append(channelUnreadColor.stylize("*" + channel)) recentChannels = recentChannels.append(config.Colors.Channels.Unread.stylize("*" + channel))
} else { } else {
recentChannels = recentChannels.appendString(channel) recentChannels = recentChannels.appendString(channel)
} }
@ -367,7 +451,7 @@ func populateList() {
if recentPMsCount <= ((maxY - 2) / 3) { if recentPMsCount <= ((maxY - 2) / 3) {
pmName := fmt.Sprintf("%s\n", cleanChannelName(s.Channel.Name)) pmName := fmt.Sprintf("%s\n", cleanChannelName(s.Channel.Name))
if s.Unread { if s.Unread {
recentPMs = recentPMs.append(channelUnreadColor.stylize("*" + pmName)) recentPMs = recentPMs.append(config.Colors.Channels.Unread.stylize("*" + pmName))
} else { } else {
recentPMs = recentPMs.appendString(pmName) recentPMs = recentPMs.appendString(pmName)
} }
@ -384,35 +468,55 @@ func populateList() {
// Formatting // Formatting
func formatMessageBody(body string) StyledString { func formatMessageBody(body string) StyledString {
output := messageBodyColor.stylize(body) body = strings.Replace(body, "```", "\n<code>\n", -1)
message := config.Colors.Message.Body.stylize(body)
output = colorReplaceMentionMe(output) message = message.colorRegex(`@[\w_]*([\.#][\w_]+)*`, config.Colors.Message.LinkKeybase)
output = output.colorRegex(`_[^_]*_`, messageBodyColor.withItalic()) message = colorReplaceMentionMe(message)
output = output.colorRegex(`~[^~]*~`, messageBodyColor.withStrikethrough())
output = output.colorRegex(`@[\w_]*(\.[\w_]+)*`, messageLinkKeybaseColor) // TODO when gocui actually fixes there shit with formatting, then un comment these lines
// message = message.colorRegex(`_[^_]*_`, config.Colors.Message.Body.withItalic())
// message = message.colorRegex(`~[^~]*~`, config.Colors.Message.Body.withStrikethrough())
message = message.colorRegex(`@[\w_]*([\.#][\w_]+)*`, config.Colors.Message.LinkKeybase)
// TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m) // TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m)
output = output.colorRegex(`\*[^\*]*\*`, messageBodyColor.withBold()) message = message.colorRegex(`\*[^\*]*\*`, config.Colors.Message.Body.withBold())
output = output.replaceString("```", "<code>") message = message.colorRegex("^>.*$", config.Colors.Message.Quote)
// TODO make background color cover whole line message = message.regexReplaceFunc("\n<code>(.*\n)*<code>\n", func(match string) string {
output = output.colorRegex("<code>(.*\n)*<code>", messageCodeColor) maxWidth, _ := g.Size()
output = output.colorRegex("`[^`]*`", messageCodeColor) output := ""
match = strings.Replace(strings.Replace(match, "```", "\n<code>\n", -1), "\t", " ", -1)
match = removeFormatting(match)
lines := strings.Split(match, "\n")
for _, line := range lines {
maxLineLength := maxWidth/2 + maxWidth/3 - 2
spaces := maxLineLength - utf8.RuneCountInString(line)
for i := 1; spaces < 0; i++ {
spaces = i*maxLineLength - utf8.RuneCountInString(line)
}
output += line + strings.Repeat(" ", spaces) + "\n"
}
// TODO stylize should remove formatting - in general everything should
return config.Colors.Message.Code.stylize(output).stringFollowedByStyle(message.style)
})
message = message.colorRegex("`[^`]*`", config.Colors.Message.Code)
// mention URL // mention URL
output = output.colorRegex(`(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))`, messageLinkURLColor) message = message.colorRegex(`(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))`, config.Colors.Message.LinkURL)
return output return message
} }
// TODO use this more // TODO use this more
func formatChannel(ch keybase.Channel) StyledString { func formatChannel(ch keybase.Channel) StyledString {
return messageLinkKeybaseColor.stylize(fmt.Sprintf("@%s#%s", ch.Name, ch.TopicName)) return config.Colors.Message.LinkKeybase.stylize(fmt.Sprintf("@%s#%s", ch.Name, ch.TopicName))
} }
func colorReplaceMentionMe(msg StyledString) StyledString { func colorReplaceMentionMe(msg StyledString) StyledString {
return msg.colorRegex("(@?"+k.Username+")", mentionColor) return msg.colorRegex(`(@?\b`+k.Username+`\b)`, config.Colors.Message.Mention)
} }
func colorUsername(username string) StyledString { func colorUsername(username string) StyledString {
var color = messageSenderDefaultColor var color = config.Colors.Message.SenderDefault
if username == k.Username { if username == k.Username {
color = mentionColor color = config.Colors.Message.Mention
} }
return color.stylize(username) return color.stylize(username)
} }
@ -422,42 +526,59 @@ func cleanChannelName(c string) string {
return strings.Replace(newChannelName, fmt.Sprintf(",%s", k.Username), "", 1) return strings.Replace(newChannelName, fmt.Sprintf(",%s", k.Username), "", 1)
} }
func formatMessage(api keybase.ChatAPI, formatString string) string { func formatMessage(api keybase.ChatAPI, formatString string) StyledString {
ret := messageHeaderColor.stylize("") msg := api.Msg
msgType := api.Msg.Content.Type ret := config.Colors.Message.Header.stylize("")
msgType := msg.Content.Type
switch msgType { switch msgType {
case "text", "attachment": case "text", "attachment":
ret = messageHeaderColor.stylize(formatString) ret = config.Colors.Message.Header.stylize(formatString)
tm := time.Unix(int64(api.Msg.SentAt), 0) tm := time.Unix(int64(msg.SentAt), 0)
var msg = formatMessageBody(api.Msg.Content.Text.Body) var body = formatMessageBody(msg.Content.Text.Body)
if msgType == "attachment" { if msgType == "attachment" {
msg = messageBodyColor.stylize("$TITLE\n$FILE") body = config.Colors.Message.Body.stylize("$TITLE\n$FILE")
attachment := api.Msg.Content.Attachment attachment := msg.Content.Attachment
msg = msg.replaceString("$TITLE", attachment.Object.Title) body = body.replaceString("$TITLE", attachment.Object.Title)
msg = msg.replace("$FILE", messageAttachmentColor.stylize(fmt.Sprintf("[Attachment: %s]", attachment.Object.Filename))) body = body.replace("$FILE", config.Colors.Message.Attachment.stylize(fmt.Sprintf("[Attachment: %s]", attachment.Object.Filename)))
}
reply := ""
if msg.Content.Text.ReplyTo != 0 {
chat := k.NewChat(channel)
replyMsg, replErr := chat.ReadMessage(msg.Content.Text.ReplyTo)
if replErr == nil {
replyUser := replyMsg.Result.Messages[0].Msg.Sender.Username
replyBody := ""
if replyMsg.Result.Messages[0].Msg.Content.Type == "text" {
replyBody = replyMsg.Result.Messages[0].Msg.Content.Text.Body
}
reply = fmt.Sprintf("\nReplyTo> %s: %s\n", replyUser, replyBody)
}
} }
user := colorUsername(api.Msg.Sender.Username) user := colorUsername(msg.Sender.Username)
device := messageSenderDeviceColor.stylize(api.Msg.Sender.DeviceName) device := config.Colors.Message.SenderDevice.stylize(msg.Sender.DeviceName)
msgID := messageIDColor.stylize(fmt.Sprintf("%d", api.Msg.ID)) msgID := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", msg.ID))
date := messageTimeColor.stylize(tm.Format(dateFormat)) date := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.DateFormat))
msgTime := messageTimeColor.stylize(tm.Format(timeFormat)) msgTime := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.TimeFormat))
c0ck := config.Colors.Message.Quote.stylize(reply)
channelName := messageIDColor.stylize(fmt.Sprintf("@%s#%s", api.Msg.Channel.Name, api.Msg.Channel.TopicName)) channelName := config.Colors.Message.ID.stylize(fmt.Sprintf("@%s#%s", msg.Channel.Name, msg.Channel.TopicName))
ret = ret.replace("$MSG", msg) ret = ret.replace("$REPL", c0ck)
ret = ret.replace("$MSG", body)
ret = ret.replace("$USER", user) ret = ret.replace("$USER", user)
ret = ret.replace("$DEVICE", device) ret = ret.replace("$DEVICE", device)
ret = ret.replace("$ID", msgID) ret = ret.replace("$ID", msgID)
ret = ret.replace("$TIME", msgTime) ret = ret.replace("$TIME", msgTime)
ret = ret.replace("$DATE", date) ret = ret.replace("$DATE", date)
ret = ret.replace("$TEAM", channelName) ret = ret.replace("$TEAM", channelName)
ret = ret.replace("$TAGS", getUserFlags(api.Msg.Sender.Username))
} }
return ret.string() return ret
} }
func formatOutput(api keybase.ChatAPI) string {
format := outputFormat func formatOutput(api keybase.ChatAPI) StyledString {
format := config.Formatting.OutputFormat
if stream { if stream {
format = outputStreamFormat format = config.Formatting.OutputStreamFormat
} }
return formatMessage(api, format) return formatMessage(api, format)
} }
@ -466,6 +587,10 @@ func formatOutput(api keybase.ChatAPI) string {
// Input handling // Input handling
func handleMessage(api keybase.ChatAPI) { func handleMessage(api keybase.ChatAPI) {
if api.ErrorListen != nil {
printError(fmt.Sprintf("%+v", api.ErrorListen))
return
}
if _, ok := typeCommands[api.Msg.Content.Type]; ok { if _, ok := typeCommands[api.Msg.Content.Type]; ok {
if api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name { if api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name {
if channel.MembersType == keybase.TEAM && channel.TopicName != api.Msg.Channel.TopicName { if channel.MembersType == keybase.TEAM && channel.TopicName != api.Msg.Channel.TopicName {
@ -485,7 +610,7 @@ func handleMessage(api keybase.ChatAPI) {
if m.Text == k.Username { if m.Text == k.Username {
// We are in a team // We are in a team
if topicName != channel.TopicName { if topicName != channel.TopicName {
printInfo(formatMessage(api, mentionFormat)) printInfoStyledString(formatMessage(api, config.Formatting.OutputMentionFormat))
fmt.Print("\a") fmt.Print("\a")
} }
@ -494,7 +619,7 @@ func handleMessage(api keybase.ChatAPI) {
} }
} else { } else {
if msgSender != channel.Name { if msgSender != channel.Name {
printInfo(formatMessage(api, pmFormat)) printInfoStyledString(formatMessage(api, config.Formatting.PMFormat))
fmt.Print("\a") fmt.Print("\a")
} }
@ -502,7 +627,7 @@ func handleMessage(api keybase.ChatAPI) {
} }
if api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name { if api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name {
if channel.MembersType == keybase.USER || channel.MembersType == keybase.TEAM && channel.TopicName == api.Msg.Channel.TopicName { if channel.MembersType == keybase.USER || channel.MembersType == keybase.TEAM && channel.TopicName == api.Msg.Channel.TopicName {
printToView("Chat", formatOutput(api)) printToView("Chat", formatOutput(api).string())
chat := k.NewChat(channel) chat := k.NewChat(channel)
lastMessage.ID = api.Msg.ID lastMessage.ID = api.Msg.ID
chat.Read(api.Msg.ID) chat.Read(api.Msg.ID)
@ -510,9 +635,9 @@ func handleMessage(api keybase.ChatAPI) {
} }
} else { } else {
if api.Msg.Channel.MembersType == keybase.TEAM { if api.Msg.Channel.MembersType == keybase.TEAM {
printToView("Chat", formatOutput(api)) printToView("Chat", formatOutput(api).string())
} else { } else {
printToView("Chat", formatMessage(api, pmFormat)) printToView("Chat", formatMessage(api, config.Formatting.PMFormat).string())
} }
} }
} else { } else {
@ -546,8 +671,8 @@ func handleInput(viewName string) error {
if inputString == "" { if inputString == "" {
return nil return nil
} }
if strings.HasPrefix(inputString, cmdPrefix) { if strings.HasPrefix(inputString, config.Basics.CmdPrefix) {
cmd := deleteEmpty(strings.Split(inputString[len(cmdPrefix):], " ")) cmd := deleteEmpty(strings.Split(inputString[len(config.Basics.CmdPrefix):], " "))
if len(cmd) < 1 { if len(cmd) < 1 {
return nil return nil
} }
@ -566,6 +691,7 @@ func handleInput(viewName string) error {
cmd[0] = inputString[:1] cmd[0] = inputString[:1]
RunCommand(cmd...) RunCommand(cmd...)
} else { } else {
inputString = resolveRootEmojis(inputString)
go sendChat(inputString) go sendChat(inputString)
} }
// restore any tab completion view titles on input commit // restore any tab completion view titles on input commit
@ -577,6 +703,7 @@ func handleInput(viewName string) error {
return nil return nil
} }
func sendChat(message string) { func sendChat(message string) {
autoScrollView("Chat")
chat := k.NewChat(channel) chat := k.NewChat(channel)
_, err := chat.Send(message) _, err := chat.Send(message)
if err != nil { if err != nil {

View File

@ -66,7 +66,8 @@ func handleTab(viewName string) error {
// Main tab completion functions // Main tab completion functions
func getEmojiTabCompletionSlice(inputWord string) []string { func getEmojiTabCompletionSlice(inputWord string) []string {
// use the emojiSlice from emojiList.go and filter it for the input word // use the emojiSlice from emojiList.go and filter it for the input word
resultSlice := filterStringSlice(emojiSlice, inputWord) //resultSlice := filterStringSlice(emojiSlice, inputWord)
resultSlice := filterEmojiMap(emojiMap, inputWord)
return resultSlice return resultSlice
} }
func getChannelTabCompletionSlice(inputWord string) []string { func getChannelTabCompletionSlice(inputWord string) []string {
@ -152,6 +153,15 @@ func filterStringSlice(ss []string, fv string) []string {
} }
return rs return rs
} }
func filterEmojiMap(eMap map[string]emojiData, fv string) []string {
var rs []string
for k, _ := range eMap {
if strings.HasPrefix(k, fv) {
rs = append(rs, k)
}
}
return rs
}
func longestCommonPrefix(ss []string) string { func longestCommonPrefix(ss []string) string {
// cover the case where the slice has no or one members // cover the case where the slice has no or one members
switch len(ss) { switch len(ss) {

View File

@ -22,9 +22,9 @@ func init() {
func tcmdShowReactions(m keybase.ChatAPI) { func tcmdShowReactions(m keybase.ChatAPI) {
team := false team := false
user := colorUsername(m.Msg.Sender.Username) user := colorUsername(m.Msg.Sender.Username)
id := messageIDColor.stylize(fmt.Sprintf("%d", m.Msg.Content.Reaction.M)) id := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", m.Msg.Content.Reaction.M))
reaction := messageReactionColor.stylize(m.Msg.Content.Reaction.B) reaction := config.Colors.Message.Reaction.stylize(m.Msg.Content.Reaction.B)
where := messageLinkKeybaseColor.stylize("a PM") where := config.Colors.Message.LinkKeybase.stylize("a PM")
if m.Msg.Channel.MembersType == keybase.TEAM { if m.Msg.Channel.MembersType == keybase.TEAM {
team = true team = true
where = formatChannel(m.Msg.Channel) where = formatChannel(m.Msg.Channel)

View File

@ -17,3 +17,83 @@ type TypeCommand struct {
Description string // A short description of the command Description string // A short description of the command
Exec func(keybase.ChatAPI) // A function that takes a raw chat message as input Exec func(keybase.ChatAPI) // A function that takes a raw chat message as input
} }
// Config holds user-configurable values
type Config struct {
filepath string `toml:"-"` // filepath is not stored in the config file, but is written to the Config struct so it's known where the config was loaded from
Basics Basics `toml:"basics"`
Formatting Formatting `toml:"formatting"`
Colors Colors `toml:"colors"`
}
// Basics holds the 'basics' section of the config file
type Basics struct {
DownloadPath string `toml:"download_path"`
Colorless bool `toml:"colorless"`
CmdPrefix string `toml:"cmd_prefix"`
UnicodeEmojis bool `toml:"unicode_emojis"`
}
// Formatting holds the 'formatting' section of the config file
type Formatting struct {
OutputFormat string `toml:"output_format"`
OutputStreamFormat string `toml:"output_stream_format"`
OutputMentionFormat string `toml:"output_mention_format"`
PMFormat string `toml:"pm_format"`
DateFormat string `toml:"date_format"`
TimeFormat string `toml:"time_format"`
IconFollowingUser string `toml:"icon_following_user"`
IconIndirectFollowUser string `toml:"icon_indirect_following_user"`
}
// Colors holds the 'colors' section of the config file
type Colors struct {
Channels Channels `toml:"channels"`
Message Message `toml:"message"`
Feed Feed `toml:"feed"`
}
// Style holds basic style information
type Style struct {
Foreground string `toml:"foreground"`
Background string `toml:"background"`
Italic bool `toml:"italic"`
Bold bool `toml:"bold"`
Underline bool `toml:"underline"`
Strikethrough bool `toml:"strikethrough"`
Inverse bool `toml:"inverse"`
}
// Channels holds the style information for various elements of a channel
type Channels struct {
Basic Style `toml:"basic"`
Header Style `toml:"header"`
Unread Style `toml:"unread"`
}
// Message holds the style information for various elements of a message
type Message struct {
Body Style `toml:"body"`
Header Style `toml:"header"`
Mention Style `toml:"mention"`
ID Style `toml:"id"`
Tags Style `toml:"tags"`
Time Style `toml:"time"`
SenderDefault Style `toml:"sender_default"`
SenderDevice Style `toml:"sender_device"`
SenderTags Style `toml:"sender_tags"`
Attachment Style `toml:"attachment"`
LinkURL Style `toml:"link_url"`
LinkKeybase Style `toml:"link_keybase"`
Reaction Style `toml:"reaction"`
Quote Style `toml:"quote"`
Code Style `toml:"code"`
}
// Feed holds the style information for various elements of the feed window
type Feed struct {
Basic Style `toml:"basic"`
Error Style `toml:"error"`
File Style `toml:"file"`
Success Style `toml:"success"`
}

View File

@ -1,40 +0,0 @@
package main
// Path where Downloaded files will default to
var downloadPath = "/tmp/"
var colorless bool = false
var channelsColor = basicStyle
var channelUnreadColor = channelsColor.withForeground(green).withItalic()
var channelsHeaderColor = channelsColor.withForeground(magenta).withBold()
var mentionColor = basicStyle.withForeground(green)
var messageHeaderColor = basicStyle.withForeground(grey)
var messageIDColor = basicStyle.withForeground(yellow)
var messageTimeColor = basicStyle.withForeground(magenta)
var messageSenderDefaultColor = basicStyle.withForeground(cyan)
var messageSenderDeviceColor = messageSenderDefaultColor
var messageBodyColor = basicStyle
var messageAttachmentColor = basicStyle.withForeground(red)
var messageLinkURLColor = basicStyle.withForeground(yellow)
var messageLinkKeybaseColor = basicStyle.withForeground(yellow)
var messageReactionColor = basicStyle.withForeground(magenta)
var messageCodeColor = basicStyle.withBackground(grey).withForeground(cyan)
var feedColor = basicStyle.withForeground(grey)
var errorColor = basicStyle.withForeground(red)
// BASH-like PS1 variable equivalent
var outputFormat = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
var outputStreamFormat = "┌──[$TEAM] [$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG"
var mentionFormat = outputStreamFormat
var pmFormat = "PM from $USER@$DEVICE: $MSG"
// 02 = Day, Jan = Month, 06 = Year
var dateFormat = "02Jan06"
// 15 = hours, 04 = minutes, 05 = seconds
var timeFormat = "15:04"
// The prefix before evaluating a command
var cmdPrefix = "/"

58
userTags.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"strings"
)
var followedInSteps = make(map[string]int)
var trustTreeParent = make(map[string]string)
func clearFlagCache() {
followedInSteps = make(map[string]int)
trustTreeParent = make(map[string]string)
}
var maxDepth = 4
func generateFollowersList() {
// Does a BFS of followedInSteps
queue := []string{k.Username}
printInfo("Generating Tree of Trust...")
lastDepth := 1
for len(queue) > 0 {
head := queue[0]
queue = queue[1:]
depth := followedInSteps[head] + 1
if depth > maxDepth {
continue
}
if depth > lastDepth {
printInfo(fmt.Sprintf("Trust generated at Level #%d", depth-1))
lastDepth = depth
}
bytes, _ := k.Exec("list-following", head)
bigString := string(bytes)
following := strings.Split(bigString, "\n")
for _, user := range following {
if followedInSteps[user] == 0 && user != k.Username {
followedInSteps[user] = depth
trustTreeParent[user] = head
queue = append(queue, user)
}
}
}
printInfo(fmt.Sprintf("Trust-level estabilished for %d users", len(followedInSteps)))
}
func getUserFlags(username string) StyledString {
tags := ""
followDepth := followedInSteps[username]
if followDepth == 1 {
tags += fmt.Sprintf(" %s", config.Formatting.IconFollowingUser)
} else if followDepth > 1 {
tags += fmt.Sprintf(" %s%d", config.Formatting.IconIndirectFollowUser, followDepth-1)
}
return config.Colors.Message.SenderTags.stylize(tags)
}