mirror of
https://github.com/Rudi9719/kbtui.git
synced 2026-03-22 14:27:23 +00:00
Compare commits
84 Commits
rudi9719/b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
97e055dd61
|
|||
| 9b6bd7442b | |||
| ae9f71be96 | |||
| 88a6f709f9 | |||
| f222464849 | |||
| f55270f423 | |||
| 67fc7c6e5d | |||
| fc48ad5c4a | |||
| 66e2552a7e | |||
| 9714e39751 | |||
| 8abde78bf7 | |||
| ff120c0366 | |||
| 0a4c2614a8 | |||
| 12d41c018e | |||
| 9a71d50ab7 | |||
| db08278780 | |||
| 84e5beada4 | |||
|
cd79a10528
|
|||
| a5f017de45 | |||
| 5c5ebc1d45 | |||
| 3d353b33e8 | |||
| 023f22a1ea | |||
| bc7ef238de | |||
| e5285d9d36 | |||
| e91506d319 | |||
| dc67abb1a3 | |||
| 1d95fea9f3 | |||
| 74da5c530e | |||
| 430c427487 | |||
| d02dd317a5 | |||
| 90d336988e | |||
| 69b5442ac9 | |||
| 27261223f0 | |||
|
aa01e9cb40
|
|||
| e4784e4fb2 | |||
| 80896e5323 | |||
| e1c8890721 | |||
| 4ac523e7f7 | |||
| 37dc6b8d4f | |||
| 60c5302aec | |||
| bf0c271d2a | |||
| 5e6e97d7f6 | |||
| 4a7818c79e | |||
| f890a6e56c | |||
| e60f65d59c | |||
| 938d6c855c | |||
| 549e9f8ad6 | |||
| a03b5ff630 | |||
| e67b0e4265 | |||
| d74ac7b1e1 | |||
| e1f0314b87 | |||
| 384d0cfd9c | |||
| 33551d9c8b | |||
| 876668d3aa | |||
| 05afd348d0 | |||
| a8e0ed17b9 | |||
| 99922c2582 | |||
| 0812c57a6a | |||
| 8c0e6211aa | |||
| de5f7da139 | |||
| 2fb6bb0bc0 | |||
| 4cc825860e | |||
| 1e6e34fbc5 | |||
| de05697c89 | |||
| bb3ec46a43 | |||
| 790d295e2e | |||
| 2484578775 | |||
| 33a68670c9 | |||
| e017ffcb55 | |||
| 0c866e24c4 | |||
|
e033215cc9
|
|||
| f09f2969d9 | |||
| 7408db2625 | |||
| 299f5103a5 | |||
| e692500606 | |||
| b0ea0c0b63 | |||
| 0ba2507891 | |||
| bd02981b52 | |||
| 6d86fdd578 | |||
| f7f0e61ecd | |||
| 0ccf655aa4 | |||
| f65ac0e231 | |||
| 517e8ecd46 | |||
| dd81d08db7 |
40
.github/workflows/go.yml
vendored
Normal file
40
.github/workflows/go.yml
vendored
Normal 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
1
.gitignore
vendored
@ -6,3 +6,4 @@ emojiList.go
|
||||
.idea/*
|
||||
.idea
|
||||
*.log
|
||||
.travis.yml
|
||||
|
||||
93
cmdConfig.go
Normal file
93
cmdConfig.go
Normal 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
|
||||
}
|
||||
@ -21,7 +21,7 @@ func init() {
|
||||
func cmdDownloadFile(cmd []string) {
|
||||
|
||||
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
|
||||
}
|
||||
messageID, err := strconv.Atoi(cmd[1])
|
||||
@ -46,11 +46,12 @@ func cmdDownloadFile(cmd []string) {
|
||||
fileName = api.Result.Messages[0].Msg.Content.Attachment.Object.Filename
|
||||
}
|
||||
|
||||
_, err = chat.Download(messageID, fmt.Sprintf("%s/%s", downloadPath, fileName))
|
||||
channelName := messageLinkKeybaseColor.stylize(channel.Name)
|
||||
_, err = chat.Download(messageID, fmt.Sprintf("%s/%s", config.Basics.DownloadPath, fileName))
|
||||
channelName := config.Colors.Message.LinkKeybase.stylize(channel.Name)
|
||||
fileNameStylizied := config.Colors.Feed.File.stylize(fileName)
|
||||
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 {
|
||||
printInfoF(fmt.Sprintf("Downloaded %s from $TEXT", fileName), channelName)
|
||||
printInfoF("Downloaded $TEXT from $TEXT", fileNameStylizied, channelName)
|
||||
}
|
||||
}
|
||||
|
||||
55
cmdExec.go
Normal file
55
cmdExec.go
Normal 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
34
cmdFollow.go
Normal 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))
|
||||
}
|
||||
@ -25,7 +25,7 @@ func cmdHelp(cmd []string) {
|
||||
if len(cmd) == 1 {
|
||||
sort.Strings(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 {
|
||||
for c := range typeCommands {
|
||||
|
||||
135
cmdInspect.go
Normal file
135
cmdInspect.go
Normal 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())
|
||||
}
|
||||
@ -41,12 +41,14 @@ func cmdJoin(cmd []string) {
|
||||
channel.TopicName = ""
|
||||
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")
|
||||
setViewTitle("Input", fmt.Sprintf(" %s ", joinedName))
|
||||
lastChat = joinedName
|
||||
autoScrollView("Chat")
|
||||
go populateChat()
|
||||
default:
|
||||
printInfo(fmt.Sprintf("To join a team use %sjoin <team> <channel>", cmdPrefix))
|
||||
printInfo(fmt.Sprintf("To join a PM use %sjoin <user>", 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>", config.Basics.CmdPrefix))
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ func init() {
|
||||
func cmdReply(cmd []string) {
|
||||
chat := k.NewChat(channel)
|
||||
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
|
||||
}
|
||||
messageID, err := strconv.Atoi(cmd[1])
|
||||
|
||||
125
cmdSet.go
125
cmdSet.go
@ -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")
|
||||
}
|
||||
@ -22,6 +22,6 @@ func cmdStream(cmd []string) {
|
||||
channel.Name = ""
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
19
cmdTags.go
Normal file
19
cmdTags.go
Normal 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
33
cmdUnfollow.go
Normal 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))
|
||||
}
|
||||
@ -21,7 +21,7 @@ func init() {
|
||||
|
||||
func cmdUploadFile(cmd []string) {
|
||||
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
|
||||
}
|
||||
filePath := cmd[1]
|
||||
@ -40,10 +40,11 @@ func cmdUploadFile(cmd []string) {
|
||||
}
|
||||
chat := k.NewChat(channel)
|
||||
_, 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 {
|
||||
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 {
|
||||
printInfo(fmt.Sprintf("Uploaded %s to %s", filePath, channelName))
|
||||
printInfoF("Uploaded $TEXT to $TEXT", fileNameStylized, channelName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ func cmdPopulateWall(cmd []string) {
|
||||
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 {
|
||||
chat := k.NewChat(chann)
|
||||
api, err := chat.Read()
|
||||
@ -81,7 +81,7 @@ func cmdPopulateWall(cmd []string) {
|
||||
apiCast.Msg = &api.Result.Messages[i].Msg
|
||||
result[apiCast.Msg.SentAt] = apiCast
|
||||
newMessage := formatOutput(apiCast)
|
||||
printMe = append(printMe, newMessage)
|
||||
printMe = append(printMe, newMessage.string())
|
||||
|
||||
}
|
||||
}
|
||||
@ -96,7 +96,7 @@ func cmdPopulateWall(cmd []string) {
|
||||
sort.Ints(keys)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
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)))
|
||||
}
|
||||
|
||||
243
colors.go
243
colors.go
@ -2,16 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pelletier/go-toml"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Begin Colors
|
||||
type color int
|
||||
|
||||
const (
|
||||
black color = iota
|
||||
black int = iota
|
||||
red
|
||||
green
|
||||
yellow
|
||||
@ -19,103 +15,89 @@ const (
|
||||
magenta
|
||||
cyan
|
||||
grey
|
||||
normal color = -1
|
||||
normal int = -1
|
||||
)
|
||||
|
||||
func colorFromString(s string) color {
|
||||
s = strings.ToLower(s)
|
||||
switch s {
|
||||
case "black":
|
||||
return black
|
||||
case "red":
|
||||
return red
|
||||
case "green":
|
||||
return green
|
||||
case "yellow":
|
||||
return yellow
|
||||
case "purple":
|
||||
return purple
|
||||
case "magenta":
|
||||
return magenta
|
||||
case "cyan":
|
||||
return cyan
|
||||
case "grey":
|
||||
return grey
|
||||
case "normal":
|
||||
var colorMapString = map[string]int{
|
||||
"black": black,
|
||||
"red": red,
|
||||
"green": green,
|
||||
"yellow": yellow,
|
||||
"purple": purple,
|
||||
"magenta": magenta,
|
||||
"cyan": cyan,
|
||||
"grey": grey,
|
||||
"normal": normal,
|
||||
}
|
||||
|
||||
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
|
||||
default:
|
||||
printError(fmt.Sprintf("color `%s` cannot be parsed.", s))
|
||||
}
|
||||
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
|
||||
func colorFromInt(color int) string {
|
||||
var result string
|
||||
result, ok := colorMapInt[color]
|
||||
if !ok {
|
||||
return "normal"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var basicStyle = Style{normal, normal, false, false, false, false, false}
|
||||
|
||||
func styleFromConfig(config *toml.Tree, key string) Style {
|
||||
key = "Colors." + key + "."
|
||||
style := basicStyle
|
||||
if config.Has(key + "foreground") {
|
||||
style = style.withForeground(colorFromString(config.Get(key + "foreground").(string)))
|
||||
}
|
||||
if config.Has(key + "background") {
|
||||
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 {
|
||||
s.foregroundColor = f
|
||||
func (s Style) withForeground(color int) Style {
|
||||
s.Foreground = colorFromInt(color)
|
||||
return s
|
||||
}
|
||||
func (s Style) withBackground(f color) Style {
|
||||
s.backgroundColor = f
|
||||
func (s Style) withBackground(color int) Style {
|
||||
s.Background = colorFromInt(color)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s Style) withBold() Style {
|
||||
s.bold = true
|
||||
s.Bold = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withInverse() Style {
|
||||
s.inverse = true
|
||||
s.Inverse = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withItalic() Style {
|
||||
s.italic = true
|
||||
s.Italic = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withStrikethrough() Style {
|
||||
s.strikethrough = true
|
||||
s.Strikethrough = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withUnderline() Style {
|
||||
s.underline = true
|
||||
s.Underline = true
|
||||
return s
|
||||
}
|
||||
|
||||
@ -123,33 +105,34 @@ func (s Style) withUnderline() Style {
|
||||
// which essentially just adds on top. that is relevant in the case of
|
||||
// bold/italic etc - it should add style - not clear.
|
||||
func (s Style) toANSI() string {
|
||||
if colorless {
|
||||
if config.Basics.Colorless {
|
||||
return ""
|
||||
}
|
||||
output := "\x1b[0m\x1b[0"
|
||||
if s.foregroundColor != normal {
|
||||
output += fmt.Sprintf(";%d", 30+s.foregroundColor)
|
||||
styleSlice := []string{"0"}
|
||||
|
||||
if colorFromString(s.Foreground) != normal {
|
||||
styleSlice = append(styleSlice, fmt.Sprintf("%d", 30+colorFromString(s.Foreground)))
|
||||
}
|
||||
if s.backgroundColor != normal {
|
||||
output += fmt.Sprintf(";%d", 40+s.backgroundColor)
|
||||
if colorFromString(s.Background) != normal {
|
||||
styleSlice = append(styleSlice, fmt.Sprintf("%d", 40+colorFromString(s.Background)))
|
||||
}
|
||||
if s.bold {
|
||||
output += ";1"
|
||||
if s.Bold {
|
||||
styleSlice = append(styleSlice, "1")
|
||||
}
|
||||
if s.italic {
|
||||
output += ";3"
|
||||
if s.Italic {
|
||||
styleSlice = append(styleSlice, "3")
|
||||
}
|
||||
if s.underline {
|
||||
output += ";4"
|
||||
if s.Underline {
|
||||
styleSlice = append(styleSlice, "4")
|
||||
}
|
||||
if s.inverse {
|
||||
output += ";7"
|
||||
if s.Inverse {
|
||||
styleSlice = append(styleSlice, "7")
|
||||
}
|
||||
if s.strikethrough {
|
||||
output += ";9"
|
||||
if s.Strikethrough {
|
||||
styleSlice = append(styleSlice, "9")
|
||||
}
|
||||
|
||||
return output + "m"
|
||||
return "\x1b[" + strings.Join(styleSlice, ";") + "m"
|
||||
}
|
||||
|
||||
// End Colors
|
||||
@ -161,6 +144,12 @@ type StyledString struct {
|
||||
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
|
||||
func (s Style) sprintf(base string, parts ...StyledString) StyledString {
|
||||
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 {
|
||||
return StyledString{msg, s}
|
||||
}
|
||||
func (t StyledString) stringFollowedByStyle(style Style) string {
|
||||
return t.style.toANSI() + t.message + style.toANSI()
|
||||
func (ss StyledString) stringFollowedByStyle(style Style) string {
|
||||
return ss.style.toANSI() + ss.message + style.toANSI()
|
||||
}
|
||||
func (t StyledString) string() string {
|
||||
return t.stringFollowedByStyle(basicStyle)
|
||||
func (ss StyledString) string() string {
|
||||
return ss.stringFollowedByStyle(basicStyle)
|
||||
}
|
||||
|
||||
func (t StyledString) replace(match string, value StyledString) StyledString {
|
||||
return t.replaceN(match, value, -1)
|
||||
func (ss StyledString) replace(match string, value StyledString) StyledString {
|
||||
return ss.replaceN(match, value, -1)
|
||||
}
|
||||
func (t StyledString) replaceN(match string, value StyledString, n int) StyledString {
|
||||
t.message = strings.Replace(t.message, match, value.stringFollowedByStyle(t.style), n)
|
||||
return t
|
||||
func (ss StyledString) replaceN(match string, value StyledString, n int) StyledString {
|
||||
ss.message = strings.Replace(ss.message, match, value.stringFollowedByStyle(ss.style), n)
|
||||
return ss
|
||||
}
|
||||
func (t StyledString) replaceString(match string, value string) StyledString {
|
||||
t.message = strings.Replace(t.message, match, value, -1)
|
||||
return t
|
||||
}
|
||||
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
|
||||
func (ss StyledString) replaceString(match string, value string) StyledString {
|
||||
ss.message = strings.Replace(ss.message, match, value, -1)
|
||||
return ss
|
||||
}
|
||||
|
||||
// Overrides current formatting
|
||||
func (t StyledString) colorRegex(match string, style Style) StyledString {
|
||||
re := regexp.MustCompile("(" + match + ")")
|
||||
subStrings := re.FindAllString(t.message, -1)
|
||||
for _, element := range subStrings {
|
||||
cleanSubstring := style.stylize(removeFormatting(element))
|
||||
t.message = strings.Replace(t.message, element, cleanSubstring.stringFollowedByStyle(t.style), -1)
|
||||
func (ss StyledString) colorRegex(match string, style Style) StyledString {
|
||||
return ss.regexReplaceFunc(match, func(subString string) string {
|
||||
return style.stylize(removeFormatting(subString)).stringFollowedByStyle(ss.style)
|
||||
})
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
return t
|
||||
// Old versionreturn t.replaceRegex(match, style.stylize(`$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
|
||||
func (t StyledString) append(other StyledString) StyledString {
|
||||
t.message = t.message + other.stringFollowedByStyle(t.style)
|
||||
return t
|
||||
func (ss StyledString) append(other StyledString) StyledString {
|
||||
ss.message = ss.message + other.stringFollowedByStyle(ss.style)
|
||||
return ss
|
||||
}
|
||||
func (t StyledString) appendString(other string) StyledString {
|
||||
t.message += other
|
||||
return t
|
||||
func (ss StyledString) appendString(other string) StyledString {
|
||||
ss.message += other
|
||||
return ss
|
||||
}
|
||||
|
||||
// Begin Formatting
|
||||
|
||||
87
defaultConfig.go
Normal file
87
defaultConfig.go
Normal 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
47
emojiMap.go
Normal file
File diff suppressed because one or more lines are too long
94
kbtui.toml
94
kbtui.toml
@ -1,63 +1,83 @@
|
||||
[Basics]
|
||||
downloadPath = "/tmp/"
|
||||
[basics]
|
||||
download_path = "/tmp/"
|
||||
colorless = false
|
||||
# The prefix before evaluating a command
|
||||
cmdPrefix = "/"
|
||||
unicode_emojis = true
|
||||
|
||||
[Formatting]
|
||||
# The prefix before evaluating a command
|
||||
cmd_prefix = "/"
|
||||
|
||||
[formatting]
|
||||
# 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
|
||||
dateFormat = "02Jan06"
|
||||
date_format = "02Jan06"
|
||||
|
||||
# 15 = hours, 04 = minutes, 05 = seconds
|
||||
timeFormat = "15:04"
|
||||
time_format = "15:04"
|
||||
|
||||
icon_following_user = "[*]"
|
||||
icon_indirect_following_user = "[?]"
|
||||
|
||||
[Colors]
|
||||
[Colors.channels]
|
||||
[Colors.channels.basic]
|
||||
[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.channels.header]
|
||||
foreground = "magenta"
|
||||
bold = true
|
||||
[Colors.channels.unread]
|
||||
foreground = "green"
|
||||
italic = true
|
||||
|
||||
[Colors.message]
|
||||
[Colors.message.body]
|
||||
foreground = "normal"
|
||||
[Colors.message.header]
|
||||
[colors.message.header]
|
||||
foreground = "grey"
|
||||
[Colors.message.mention]
|
||||
bold = true
|
||||
[colors.message.mention]
|
||||
foreground = "green"
|
||||
italic = true
|
||||
bold = true
|
||||
[Colors.message.id]
|
||||
[colors.message.id]
|
||||
foreground = "yellow"
|
||||
[Colors.message.time]
|
||||
bold = true
|
||||
[colors.message.time]
|
||||
foreground = "magenta"
|
||||
[Colors.message.sender_default]
|
||||
bold = true
|
||||
[colors.message.sender_default]
|
||||
foreground = "cyan"
|
||||
bold = true
|
||||
[Colors.message.sender_device]
|
||||
[colors.message.sender_device]
|
||||
foreground = "cyan"
|
||||
[Colors.message.attachment]
|
||||
bold = true
|
||||
[colors.message.sender_tags]
|
||||
foreground = "yellow"
|
||||
[colors.message.attachment]
|
||||
foreground = "red"
|
||||
[Colors.message.link_url]
|
||||
[colors.message.link_url]
|
||||
foreground = "yellow"
|
||||
[Colors.message.link_keybase]
|
||||
foreground = "yellow"
|
||||
[Colors.message.reaction]
|
||||
[colors.message.link_keybase]
|
||||
foreground = "cyan"
|
||||
[colors.message.reaction]
|
||||
foreground = "magenta"
|
||||
bold = true
|
||||
[Colors.message.code]
|
||||
[colors.message.quote]
|
||||
foreground = "green"
|
||||
[colors.message.code]
|
||||
foreground = "cyan"
|
||||
background = "grey"
|
||||
[Colors.feed]
|
||||
[Colors.feed.basic]
|
||||
|
||||
[colors.feed]
|
||||
[colors.feed.basic]
|
||||
foreground = "grey"
|
||||
[Colors.feed.error]
|
||||
foreground = "red"
|
||||
[colors.feed.error]
|
||||
foreground = "red"
|
||||
[colors.feed.success]
|
||||
foreground = "green"
|
||||
[colors.feed.file]
|
||||
foreground = "yellow"
|
||||
|
||||
79
mage.go
79
mage.go
@ -3,65 +3,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/magefile/mage/mg"
|
||||
"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 {
|
||||
var packages = []string{
|
||||
"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.
|
||||
func Build() {
|
||||
mg.Deps(getRemotePackages)
|
||||
@ -157,7 +83,7 @@ func BuildAllCommands() {
|
||||
// Build kbtui with all Commands and TypeCommands enabled.
|
||||
func BuildAllCommandsT() {
|
||||
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() {
|
||||
exit(err)
|
||||
}()
|
||||
@ -167,8 +93,7 @@ func BuildAllCommandsT() {
|
||||
// Build kbtui with beta functionality
|
||||
func BuildBeta() {
|
||||
mg.Deps(getRemotePackages)
|
||||
mg.Deps(BuildEmoji)
|
||||
if err := sh.Run("go", "build", "-tags", "allcommands,showreactionscmd,emojiList,tabcompletion"); err != nil {
|
||||
if err := sh.Run("go", "build", "-tags", "allcommands showreactionscmd tabcompletion execcmd"); err != nil {
|
||||
defer func() {
|
||||
exit(err)
|
||||
}()
|
||||
|
||||
245
main.go
245
main.go
@ -4,11 +4,13 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"samhofi.us/x/keybase"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -22,9 +24,12 @@ var (
|
||||
channels []keybase.Channel
|
||||
stream = false
|
||||
lastMessage keybase.ChatAPI
|
||||
lastChat = ""
|
||||
g *gocui.Gui
|
||||
)
|
||||
|
||||
var config *Config
|
||||
|
||||
func main() {
|
||||
if !k.LoggedIn {
|
||||
fmt.Println("You are not logged in.")
|
||||
@ -37,7 +42,7 @@ func main() {
|
||||
}
|
||||
defer g.Close()
|
||||
g.SetManagerFunc(layout)
|
||||
go RunCommand("config", "load")
|
||||
RunCommand("config", "load")
|
||||
go populateList()
|
||||
go updateChatWindow()
|
||||
if len(os.Args) > 1 {
|
||||
@ -82,7 +87,7 @@ func layout(g *gocui.Gui) error {
|
||||
chatView.Autoscroll = true
|
||||
chatView.Wrap = true
|
||||
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())
|
||||
RunCommand("help")
|
||||
}
|
||||
@ -95,7 +100,7 @@ func layout(g *gocui.Gui) error {
|
||||
}
|
||||
inputView.Editable = 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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
input, err := getInputString("Input")
|
||||
@ -122,6 +188,13 @@ func initKeybindings() error {
|
||||
}); err != nil {
|
||||
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,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
popupView("Chat")
|
||||
@ -255,17 +328,20 @@ func printError(message string) {
|
||||
printErrorF(message)
|
||||
}
|
||||
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
|
||||
func printInfo(message string) {
|
||||
printInfoF(message)
|
||||
}
|
||||
func printInfoStyledString(message StyledString) {
|
||||
printInfoF("$TEXT", message)
|
||||
}
|
||||
|
||||
// this removes formatting
|
||||
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) {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
@ -273,6 +349,10 @@ func printToView(viewName string, message string) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.Basics.UnicodeEmojis {
|
||||
message = emojiUnicodeConvert(message)
|
||||
}
|
||||
fmt.Fprintf(updatingView, "%s\n", message)
|
||||
return nil
|
||||
})
|
||||
@ -297,7 +377,7 @@ func populateChat() {
|
||||
chat := k.NewChat(channel)
|
||||
maxX, _ := g.Size()
|
||||
api, err := chat.Read(maxX / 2)
|
||||
if err != nil {
|
||||
if err != nil || api.Result == nil {
|
||||
for _, testChan := range channels {
|
||||
if channel.Name == testChan.Name {
|
||||
channel = testChan
|
||||
@ -326,7 +406,7 @@ func populateChat() {
|
||||
}
|
||||
var apiCast keybase.ChatAPI
|
||||
apiCast.Msg = &message.Msg
|
||||
newMessage := formatOutput(apiCast)
|
||||
newMessage := formatOutput(apiCast).string()
|
||||
printMe = append(printMe, newMessage)
|
||||
}
|
||||
}
|
||||
@ -337,7 +417,7 @@ func populateChat() {
|
||||
}
|
||||
}
|
||||
printToView("Chat", actuallyPrintMe)
|
||||
|
||||
go populateList()
|
||||
}
|
||||
func populateList() {
|
||||
_, maxY := g.Size()
|
||||
@ -345,19 +425,23 @@ func populateList() {
|
||||
log.Printf("%+v", err)
|
||||
} else {
|
||||
clearView("List")
|
||||
var textBase = channelsColor.stylize("")
|
||||
var recentPMs = textBase.append(channelsHeaderColor.stylize("---[PMs]---\n"))
|
||||
conversationSlice := testVar.Result.Conversations
|
||||
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 recentChannels = textBase.append(channelsHeaderColor.stylize("---[Teams]---\n"))
|
||||
var recentChannels = textBase.append(config.Colors.Channels.Header.stylize("---[Teams]---\n"))
|
||||
var recentChannelsCount = 0
|
||||
for _, s := range testVar.Result.Conversations {
|
||||
for _, s := range conversationSlice {
|
||||
channels = append(channels, s.Channel)
|
||||
if s.Channel.MembersType == keybase.TEAM {
|
||||
recentChannelsCount++
|
||||
if recentChannelsCount <= ((maxY - 2) / 3) {
|
||||
channel := fmt.Sprintf("%s\n\t#%s\n", s.Channel.Name, s.Channel.TopicName)
|
||||
if s.Unread {
|
||||
recentChannels = recentChannels.append(channelUnreadColor.stylize("*" + channel))
|
||||
recentChannels = recentChannels.append(config.Colors.Channels.Unread.stylize("*" + channel))
|
||||
} else {
|
||||
recentChannels = recentChannels.appendString(channel)
|
||||
}
|
||||
@ -367,7 +451,7 @@ func populateList() {
|
||||
if recentPMsCount <= ((maxY - 2) / 3) {
|
||||
pmName := fmt.Sprintf("%s\n", cleanChannelName(s.Channel.Name))
|
||||
if s.Unread {
|
||||
recentPMs = recentPMs.append(channelUnreadColor.stylize("*" + pmName))
|
||||
recentPMs = recentPMs.append(config.Colors.Channels.Unread.stylize("*" + pmName))
|
||||
} else {
|
||||
recentPMs = recentPMs.appendString(pmName)
|
||||
}
|
||||
@ -384,35 +468,55 @@ func populateList() {
|
||||
|
||||
// Formatting
|
||||
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)
|
||||
output = output.colorRegex(`_[^_]*_`, messageBodyColor.withItalic())
|
||||
output = output.colorRegex(`~[^~]*~`, messageBodyColor.withStrikethrough())
|
||||
output = output.colorRegex(`@[\w_]*(\.[\w_]+)*`, messageLinkKeybaseColor)
|
||||
message = message.colorRegex(`@[\w_]*([\.#][\w_]+)*`, config.Colors.Message.LinkKeybase)
|
||||
message = colorReplaceMentionMe(message)
|
||||
|
||||
// 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)
|
||||
output = output.colorRegex(`\*[^\*]*\*`, messageBodyColor.withBold())
|
||||
output = output.replaceString("```", "<code>")
|
||||
// TODO make background color cover whole line
|
||||
output = output.colorRegex("<code>(.*\n)*<code>", messageCodeColor)
|
||||
output = output.colorRegex("`[^`]*`", messageCodeColor)
|
||||
message = message.colorRegex(`\*[^\*]*\*`, config.Colors.Message.Body.withBold())
|
||||
message = message.colorRegex("^>.*$", config.Colors.Message.Quote)
|
||||
message = message.regexReplaceFunc("\n<code>(.*\n)*<code>\n", func(match string) string {
|
||||
maxWidth, _ := g.Size()
|
||||
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
|
||||
output = output.colorRegex(`(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))`, messageLinkURLColor)
|
||||
return output
|
||||
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 message
|
||||
}
|
||||
|
||||
// TODO use this more
|
||||
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 {
|
||||
return msg.colorRegex("(@?"+k.Username+")", mentionColor)
|
||||
return msg.colorRegex(`(@?\b`+k.Username+`\b)`, config.Colors.Message.Mention)
|
||||
}
|
||||
func colorUsername(username string) StyledString {
|
||||
var color = messageSenderDefaultColor
|
||||
var color = config.Colors.Message.SenderDefault
|
||||
if username == k.Username {
|
||||
color = mentionColor
|
||||
color = config.Colors.Message.Mention
|
||||
}
|
||||
return color.stylize(username)
|
||||
}
|
||||
@ -422,42 +526,59 @@ func cleanChannelName(c string) string {
|
||||
return strings.Replace(newChannelName, fmt.Sprintf(",%s", k.Username), "", 1)
|
||||
}
|
||||
|
||||
func formatMessage(api keybase.ChatAPI, formatString string) string {
|
||||
ret := messageHeaderColor.stylize("")
|
||||
msgType := api.Msg.Content.Type
|
||||
func formatMessage(api keybase.ChatAPI, formatString string) StyledString {
|
||||
msg := api.Msg
|
||||
ret := config.Colors.Message.Header.stylize("")
|
||||
msgType := msg.Content.Type
|
||||
switch msgType {
|
||||
case "text", "attachment":
|
||||
ret = messageHeaderColor.stylize(formatString)
|
||||
tm := time.Unix(int64(api.Msg.SentAt), 0)
|
||||
var msg = formatMessageBody(api.Msg.Content.Text.Body)
|
||||
ret = config.Colors.Message.Header.stylize(formatString)
|
||||
tm := time.Unix(int64(msg.SentAt), 0)
|
||||
var body = formatMessageBody(msg.Content.Text.Body)
|
||||
if msgType == "attachment" {
|
||||
msg = messageBodyColor.stylize("$TITLE\n$FILE")
|
||||
attachment := api.Msg.Content.Attachment
|
||||
msg = msg.replaceString("$TITLE", attachment.Object.Title)
|
||||
msg = msg.replace("$FILE", messageAttachmentColor.stylize(fmt.Sprintf("[Attachment: %s]", attachment.Object.Filename)))
|
||||
body = config.Colors.Message.Body.stylize("$TITLE\n$FILE")
|
||||
attachment := msg.Content.Attachment
|
||||
body = body.replaceString("$TITLE", attachment.Object.Title)
|
||||
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)
|
||||
device := messageSenderDeviceColor.stylize(api.Msg.Sender.DeviceName)
|
||||
msgID := messageIDColor.stylize(fmt.Sprintf("%d", api.Msg.ID))
|
||||
date := messageTimeColor.stylize(tm.Format(dateFormat))
|
||||
msgTime := messageTimeColor.stylize(tm.Format(timeFormat))
|
||||
|
||||
channelName := messageIDColor.stylize(fmt.Sprintf("@%s#%s", api.Msg.Channel.Name, api.Msg.Channel.TopicName))
|
||||
ret = ret.replace("$MSG", msg)
|
||||
user := colorUsername(msg.Sender.Username)
|
||||
device := config.Colors.Message.SenderDevice.stylize(msg.Sender.DeviceName)
|
||||
msgID := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", msg.ID))
|
||||
date := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.DateFormat))
|
||||
msgTime := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.TimeFormat))
|
||||
c0ck := config.Colors.Message.Quote.stylize(reply)
|
||||
channelName := config.Colors.Message.ID.stylize(fmt.Sprintf("@%s#%s", msg.Channel.Name, msg.Channel.TopicName))
|
||||
ret = ret.replace("$REPL", c0ck)
|
||||
ret = ret.replace("$MSG", body)
|
||||
ret = ret.replace("$USER", user)
|
||||
ret = ret.replace("$DEVICE", device)
|
||||
ret = ret.replace("$ID", msgID)
|
||||
ret = ret.replace("$TIME", msgTime)
|
||||
ret = ret.replace("$DATE", date)
|
||||
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 {
|
||||
format = outputStreamFormat
|
||||
format = config.Formatting.OutputStreamFormat
|
||||
}
|
||||
return formatMessage(api, format)
|
||||
}
|
||||
@ -466,6 +587,10 @@ func formatOutput(api keybase.ChatAPI) string {
|
||||
|
||||
// Input handling
|
||||
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 api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name {
|
||||
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 {
|
||||
// We are in a team
|
||||
if topicName != channel.TopicName {
|
||||
printInfo(formatMessage(api, mentionFormat))
|
||||
printInfoStyledString(formatMessage(api, config.Formatting.OutputMentionFormat))
|
||||
fmt.Print("\a")
|
||||
}
|
||||
|
||||
@ -494,7 +619,7 @@ func handleMessage(api keybase.ChatAPI) {
|
||||
}
|
||||
} else {
|
||||
if msgSender != channel.Name {
|
||||
printInfo(formatMessage(api, pmFormat))
|
||||
printInfoStyledString(formatMessage(api, config.Formatting.PMFormat))
|
||||
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 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)
|
||||
lastMessage.ID = api.Msg.ID
|
||||
chat.Read(api.Msg.ID)
|
||||
@ -510,9 +635,9 @@ func handleMessage(api keybase.ChatAPI) {
|
||||
}
|
||||
} else {
|
||||
if api.Msg.Channel.MembersType == keybase.TEAM {
|
||||
printToView("Chat", formatOutput(api))
|
||||
printToView("Chat", formatOutput(api).string())
|
||||
} else {
|
||||
printToView("Chat", formatMessage(api, pmFormat))
|
||||
printToView("Chat", formatMessage(api, config.Formatting.PMFormat).string())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -546,8 +671,8 @@ func handleInput(viewName string) error {
|
||||
if inputString == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(inputString, cmdPrefix) {
|
||||
cmd := deleteEmpty(strings.Split(inputString[len(cmdPrefix):], " "))
|
||||
if strings.HasPrefix(inputString, config.Basics.CmdPrefix) {
|
||||
cmd := deleteEmpty(strings.Split(inputString[len(config.Basics.CmdPrefix):], " "))
|
||||
if len(cmd) < 1 {
|
||||
return nil
|
||||
}
|
||||
@ -566,6 +691,7 @@ func handleInput(viewName string) error {
|
||||
cmd[0] = inputString[:1]
|
||||
RunCommand(cmd...)
|
||||
} else {
|
||||
inputString = resolveRootEmojis(inputString)
|
||||
go sendChat(inputString)
|
||||
}
|
||||
// restore any tab completion view titles on input commit
|
||||
@ -577,6 +703,7 @@ func handleInput(viewName string) error {
|
||||
return nil
|
||||
}
|
||||
func sendChat(message string) {
|
||||
autoScrollView("Chat")
|
||||
chat := k.NewChat(channel)
|
||||
_, err := chat.Send(message)
|
||||
if err != nil {
|
||||
|
||||
@ -66,7 +66,8 @@ func handleTab(viewName string) error {
|
||||
// Main tab completion functions
|
||||
func getEmojiTabCompletionSlice(inputWord string) []string {
|
||||
// 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
|
||||
}
|
||||
func getChannelTabCompletionSlice(inputWord string) []string {
|
||||
@ -152,6 +153,15 @@ func filterStringSlice(ss []string, fv string) []string {
|
||||
}
|
||||
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 {
|
||||
// cover the case where the slice has no or one members
|
||||
switch len(ss) {
|
||||
|
||||
@ -22,9 +22,9 @@ func init() {
|
||||
func tcmdShowReactions(m keybase.ChatAPI) {
|
||||
team := false
|
||||
user := colorUsername(m.Msg.Sender.Username)
|
||||
id := messageIDColor.stylize(fmt.Sprintf("%d", m.Msg.Content.Reaction.M))
|
||||
reaction := messageReactionColor.stylize(m.Msg.Content.Reaction.B)
|
||||
where := messageLinkKeybaseColor.stylize("a PM")
|
||||
id := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", m.Msg.Content.Reaction.M))
|
||||
reaction := config.Colors.Message.Reaction.stylize(m.Msg.Content.Reaction.B)
|
||||
where := config.Colors.Message.LinkKeybase.stylize("a PM")
|
||||
if m.Msg.Channel.MembersType == keybase.TEAM {
|
||||
team = true
|
||||
where = formatChannel(m.Msg.Channel)
|
||||
|
||||
80
types.go
80
types.go
@ -17,3 +17,83 @@ type TypeCommand struct {
|
||||
Description string // A short description of the command
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -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
58
userTags.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user