Initial commit of rework
This commit is contained in:
3
.github/workflows/go.yml
vendored
3
.github/workflows/go.yml
vendored
@ -21,10 +21,9 @@ jobs:
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
go get github.com/magefile/mage
|
||||
|
||||
- name: Build
|
||||
run: go run build.go buildbeta
|
||||
run: go build
|
||||
- name: Upload Artifact
|
||||
if: matrix.platform != 'windows-latest'
|
||||
uses: actions/upload-artifact@v1.0.0
|
||||
|
||||
12
build.go
12
build.go
@ -1,12 +0,0 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/magefile/mage/mage"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(mage.Main())
|
||||
}
|
||||
21
cmdClean.go
21
cmdClean.go
@ -1,21 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands cleancmd
|
||||
|
||||
package main
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"clean", "c"},
|
||||
Description: "- Clean, or redraw chat view",
|
||||
Help: "",
|
||||
Exec: cmdClean,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdClean(cmd []string) {
|
||||
clearView("Chat")
|
||||
clearView("List")
|
||||
go populateChat()
|
||||
go populateList()
|
||||
}
|
||||
93
cmdConfig.go
93
cmdConfig.go
@ -1,93 +0,0 @@
|
||||
// +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
|
||||
}
|
||||
33
cmdDelete.go
33
cmdDelete.go
@ -1,33 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands deletecmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"delete", "del", "-"},
|
||||
Description: "$messageId - Delete a message by $messageId",
|
||||
Help: "",
|
||||
Exec: cmdDelete,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
func cmdDelete(cmd []string) {
|
||||
var messageID int
|
||||
if len(cmd) > 1 {
|
||||
messageID, _ = strconv.Atoi(cmd[1])
|
||||
} else {
|
||||
messageID = lastMessage.ID
|
||||
}
|
||||
|
||||
chat := k.NewChat(channel)
|
||||
_, err := chat.Delete(messageID)
|
||||
if err != nil {
|
||||
printError("There was an error deleting your message.")
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands downloadcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"download", "d"},
|
||||
Description: "$messageId $fileName - Download a file to user's downloadpath",
|
||||
Help: "",
|
||||
Exec: cmdDownloadFile,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdDownloadFile(cmd []string) {
|
||||
|
||||
if len(cmd) < 2 {
|
||||
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])
|
||||
if err != nil {
|
||||
printError("There was an error converting your messageID to an int")
|
||||
return
|
||||
}
|
||||
chat := k.NewChat(channel)
|
||||
api, err := chat.ReadMessage(messageID)
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("There was an error pulling message %d", messageID))
|
||||
return
|
||||
}
|
||||
if api.Result.Messages[0].Msg.Content.Type != "attachment" {
|
||||
printError("No attachment detected")
|
||||
return
|
||||
}
|
||||
var fileName string
|
||||
if len(cmd) == 3 {
|
||||
fileName = cmd[2]
|
||||
} else {
|
||||
fileName = api.Result.Messages[0].Msg.Content.Attachment.Object.Filename
|
||||
}
|
||||
|
||||
_, 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("There was an error downloading $TEXT from $TEXT", fileNameStylizied, channelName)
|
||||
} else {
|
||||
printInfoF("Downloaded $TEXT from $TEXT", fileNameStylizied, channelName)
|
||||
}
|
||||
}
|
||||
68
cmdEdit.go
68
cmdEdit.go
@ -1,68 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands editcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"edit", "e"},
|
||||
Description: "$messageID - Edit a message (messageID is optional)",
|
||||
Help: "",
|
||||
Exec: cmdEdit,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdEdit(cmd []string) {
|
||||
var messageID int
|
||||
chat := k.NewChat(channel)
|
||||
if len(cmd) == 2 || len(cmd) == 1 {
|
||||
if len(cmd) == 2 {
|
||||
messageID, _ = strconv.Atoi(cmd[1])
|
||||
} else if lastMessage.ID != 0 {
|
||||
message, _ := chat.ReadMessage(lastMessage.ID)
|
||||
lastMessage.Type = message.Result.Messages[0].Msg.Content.Type
|
||||
if lastMessage.Type != "text" {
|
||||
printError("Last message isn't editable (is it an edit?)")
|
||||
return
|
||||
}
|
||||
messageID = lastMessage.ID
|
||||
} else {
|
||||
printError("No message to edit")
|
||||
return
|
||||
}
|
||||
origMessage, _ := chat.ReadMessage(messageID)
|
||||
if origMessage.Result.Messages[0].Msg.Content.Type != "text" {
|
||||
printInfo(fmt.Sprintf("%+v", origMessage))
|
||||
return
|
||||
}
|
||||
if origMessage.Result.Messages[0].Msg.Sender.Username != k.Username {
|
||||
printError("You cannot edit another user's messages.")
|
||||
return
|
||||
}
|
||||
editString := origMessage.Result.Messages[0].Msg.Content.Text.Body
|
||||
clearView("Edit")
|
||||
popupView("Edit")
|
||||
printToView("Edit", fmt.Sprintf("/e %d %s", messageID, editString))
|
||||
setViewTitle("Edit", fmt.Sprintf(" Editing message %d ", messageID))
|
||||
moveCursorToEnd("Edit")
|
||||
return
|
||||
}
|
||||
if len(cmd) < 3 {
|
||||
printError("Not enough options for Edit")
|
||||
return
|
||||
}
|
||||
messageID, _ = strconv.Atoi(cmd[1])
|
||||
newMessage := strings.Join(cmd[2:], " ")
|
||||
_, err := chat.Edit(messageID, newMessage)
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("Error editing message %d, %+v", messageID, err))
|
||||
}
|
||||
|
||||
}
|
||||
55
cmdExec.go
55
cmdExec.go
@ -1,55 +0,0 @@
|
||||
// +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
34
cmdFollow.go
@ -1,34 +0,0 @@
|
||||
// +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))
|
||||
}
|
||||
39
cmdHelp.go
39
cmdHelp.go
@ -1,39 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands helpcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"help", "h"},
|
||||
Description: "Show information about available commands",
|
||||
Help: "",
|
||||
Exec: cmdHelp,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdHelp(cmd []string) {
|
||||
var helpText string
|
||||
var tCommands []string
|
||||
if len(cmd) == 1 {
|
||||
sort.Strings(baseCommands)
|
||||
for _, c := range baseCommands {
|
||||
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 {
|
||||
tCommands = append(tCommands, typeCommands[c].Name)
|
||||
}
|
||||
sort.Strings(tCommands)
|
||||
helpText = fmt.Sprintf("%s\nThe following Type Commands are currently loaded: %s", helpText, strings.Join(tCommands, ", "))
|
||||
}
|
||||
}
|
||||
printToView("Chat", helpText)
|
||||
}
|
||||
135
cmdInspect.go
135
cmdInspect.go
@ -1,135 +0,0 @@
|
||||
// +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())
|
||||
}
|
||||
54
cmdJoin.go
54
cmdJoin.go
@ -1,54 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands joincmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"samhofi.us/x/keybase"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"join", "j"},
|
||||
Description: "$team/user $channel - Join a chat, $user or $team $channel",
|
||||
Help: "",
|
||||
Exec: cmdJoin,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdJoin(cmd []string) {
|
||||
stream = false
|
||||
switch l := len(cmd); l {
|
||||
case 3:
|
||||
fallthrough
|
||||
case 2:
|
||||
// if people write it in one singular line, with a `#`
|
||||
firstArgSplit := strings.Split(cmd[1], "#")
|
||||
channel.Name = strings.Replace(firstArgSplit[0], "@", "", 1)
|
||||
joinedName := fmt.Sprintf("@%s", channel.Name)
|
||||
if l == 3 || len(firstArgSplit) == 2 {
|
||||
channel.MembersType = keybase.TEAM
|
||||
if l == 3 {
|
||||
channel.TopicName = strings.Replace(cmd[2], "#", "", 1)
|
||||
} else {
|
||||
channel.TopicName = firstArgSplit[1]
|
||||
}
|
||||
joinedName = fmt.Sprintf("%s#%s", joinedName, channel.TopicName)
|
||||
} else {
|
||||
channel.TopicName = ""
|
||||
channel.MembersType = keybase.USER
|
||||
}
|
||||
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>", config.Basics.CmdPrefix))
|
||||
printInfo(fmt.Sprintf("To join a PM use %sjoin <user>", config.Basics.CmdPrefix))
|
||||
}
|
||||
}
|
||||
36
cmdPost.go
36
cmdPost.go
@ -1,36 +0,0 @@
|
||||
// +ignore
|
||||
// +build allcommands postcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"samhofi.us/x/keybase"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"post"},
|
||||
Description: "- Post public messages on your wall",
|
||||
Help: "",
|
||||
Exec: cmdPost,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
func cmdPost(cmd []string) {
|
||||
var pubChan keybase.Channel
|
||||
pubChan.Public = true
|
||||
pubChan.MembersType = keybase.USER
|
||||
pubChan.Name = k.Username
|
||||
post := strings.Join(cmd[1:], " ")
|
||||
chat := k.NewChat(pubChan)
|
||||
_, err := chat.Send(post)
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("There was an error with your post: %+v", err))
|
||||
} else {
|
||||
printInfo("You have publically posted to your wall, signed by your current device.")
|
||||
}
|
||||
}
|
||||
43
cmdReact.go
43
cmdReact.go
@ -1,43 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands reactcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"react", "r", "+"},
|
||||
Description: "$messageID $reaction - React to a message (messageID is optional)",
|
||||
Help: "",
|
||||
Exec: cmdReact,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdReact(cmd []string) {
|
||||
if len(cmd) > 2 {
|
||||
reactToMessageID(cmd[1], strings.Join(cmd[2:], " "))
|
||||
} else if len(cmd) == 2 {
|
||||
reactToMessage(cmd[1])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func reactToMessage(reaction string) {
|
||||
doReact(lastMessage.ID, reaction)
|
||||
}
|
||||
func reactToMessageID(messageID string, reaction string) {
|
||||
ID, _ := strconv.Atoi(messageID)
|
||||
doReact(ID, reaction)
|
||||
}
|
||||
func doReact(messageID int, reaction string) {
|
||||
chat := k.NewChat(channel)
|
||||
_, err := chat.React(messageID, reaction)
|
||||
if err != nil {
|
||||
printError("There was an error reacting to the message.")
|
||||
}
|
||||
}
|
||||
38
cmdReply.go
38
cmdReply.go
@ -1,38 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands replycmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"reply", "re"},
|
||||
Description: "$messageId $response - Reply to a message",
|
||||
Help: "",
|
||||
Exec: cmdReply,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdReply(cmd []string) {
|
||||
chat := k.NewChat(channel)
|
||||
if len(cmd) < 2 {
|
||||
printInfo(fmt.Sprintf("%s%s $ID - Reply to message $ID", config.Basics.CmdPrefix, cmd[0]))
|
||||
return
|
||||
}
|
||||
messageID, err := strconv.Atoi(cmd[1])
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("There was an error determining message ID %s", cmd[1]))
|
||||
return
|
||||
}
|
||||
_, err = chat.Reply(messageID, strings.Join(cmd[2:], " "))
|
||||
if err != nil {
|
||||
printError("There was an error with your reply.")
|
||||
return
|
||||
}
|
||||
}
|
||||
23
cmdShrug.go
23
cmdShrug.go
@ -1,23 +0,0 @@
|
||||
// +ignore
|
||||
// +build allcommands shrugcmd
|
||||
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"shrug", "shrg"},
|
||||
Description: "$message - append a shrug ( ¯\\_(ツ)_/¯ )to your message",
|
||||
Help: "",
|
||||
Exec: cmdShrug,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdShrug(cmd []string) {
|
||||
cmd = append(cmd, " ¯\\_(ツ)_/¯")
|
||||
|
||||
sendChat(strings.Join(cmd[1:], " "))
|
||||
}
|
||||
27
cmdStream.go
27
cmdStream.go
@ -1,27 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands streamcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"stream", "s"},
|
||||
Description: "- Stream all incoming messages",
|
||||
Help: "",
|
||||
Exec: cmdStream,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdStream(cmd []string) {
|
||||
stream = true
|
||||
channel.Name = ""
|
||||
|
||||
printInfo("You are now viewing the formatted stream")
|
||||
setViewTitle("Input", fmt.Sprintf(" Stream - Not in a chat. %sj to join ", config.Basics.CmdPrefix))
|
||||
clearView("Chat")
|
||||
}
|
||||
19
cmdTags.go
19
cmdTags.go
@ -1,19 +0,0 @@
|
||||
// +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()
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
// +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))
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands uploadcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"upload", "u"},
|
||||
Description: "$filePath $fileName - Upload file from absolute path with optional name",
|
||||
Help: "",
|
||||
Exec: cmdUploadFile,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdUploadFile(cmd []string) {
|
||||
if len(cmd) < 2 {
|
||||
printInfo(fmt.Sprintf("%s%s $filePath $fileName - Upload file from absolute path with optional name", config.Basics.CmdPrefix, cmd[0]))
|
||||
return
|
||||
}
|
||||
filePath := cmd[1]
|
||||
if !strings.HasPrefix(filePath, "/") {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("There was an error determining path %+v", err))
|
||||
}
|
||||
filePath = fmt.Sprintf("%s/%s", dir, filePath)
|
||||
}
|
||||
var fileName string
|
||||
if len(cmd) == 3 {
|
||||
fileName = cmd[2]
|
||||
} else {
|
||||
fileName = ""
|
||||
}
|
||||
chat := k.NewChat(channel)
|
||||
_, err := chat.Upload(fileName, filePath)
|
||||
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, channel.Name, err))
|
||||
} else {
|
||||
printInfoF("Uploaded $TEXT to $TEXT", fileNameStylized, channelName)
|
||||
}
|
||||
}
|
||||
108
cmdWall.go
108
cmdWall.go
@ -1,108 +0,0 @@
|
||||
// +ignore
|
||||
// +build allcommands wallcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"samhofi.us/x/keybase"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"wall", "w"},
|
||||
Description: "$user / !all - Show public messages for a user or all users you follow",
|
||||
Help: "",
|
||||
Exec: cmdWall,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
func cmdWall(cmd []string) {
|
||||
go cmdPopulateWall(cmd)
|
||||
}
|
||||
|
||||
func cmdPopulateWall(cmd []string) {
|
||||
var users []keybase.Channel
|
||||
var requestedUsers string
|
||||
var printMe []string
|
||||
var actuallyPrintMe string
|
||||
result := make(map[int]keybase.ChatAPI)
|
||||
start := time.Now()
|
||||
if len(cmd) > 1 {
|
||||
if cmd[1] == "!all" {
|
||||
go cmdAllWall()
|
||||
return
|
||||
}
|
||||
for _, username := range cmd[1:] {
|
||||
requestedUsers += fmt.Sprintf("%s ", username)
|
||||
var newChan keybase.Channel
|
||||
newChan.MembersType = keybase.USER
|
||||
newChan.Name = username
|
||||
newChan.TopicName = ""
|
||||
newChan.Public = true
|
||||
users = append(users, newChan)
|
||||
}
|
||||
} else if channel.MembersType == keybase.USER {
|
||||
users = append(users, channel)
|
||||
users[0].Public = true
|
||||
requestedUsers += cleanChannelName(channel.Name)
|
||||
|
||||
} else {
|
||||
requestedUsers += k.Username
|
||||
var newChan keybase.Channel
|
||||
newChan.MembersType = keybase.USER
|
||||
newChan.Name = k.Username
|
||||
newChan.TopicName = ""
|
||||
newChan.Public = true
|
||||
users = append(users, newChan)
|
||||
}
|
||||
if len(users) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
if len(users) < 6 {
|
||||
printError(fmt.Sprintf("There was an error for user %s: %+v", cleanChannelName(chann.Name), err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for i, message := range api.Result.Messages {
|
||||
if message.Msg.Content.Type == "text" {
|
||||
var apiCast keybase.ChatAPI
|
||||
apiCast.Msg = &api.Result.Messages[i].Msg
|
||||
result[apiCast.Msg.SentAt] = apiCast
|
||||
newMessage := formatOutput(apiCast)
|
||||
printMe = append(printMe, newMessage.string())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
keys := make([]int, 0, len(result))
|
||||
for k := range result {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
for _, k := range keys {
|
||||
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)))
|
||||
}
|
||||
func cmdAllWall() {
|
||||
bytes, _ := k.Exec("list-following")
|
||||
bigString := string(bytes)
|
||||
following := strings.Split(bigString, "\n")
|
||||
go cmdPopulateWall(following)
|
||||
}
|
||||
63
cmdWallet.go
63
cmdWallet.go
@ -1,63 +0,0 @@
|
||||
// ignore
|
||||
// +build allcommands walletcmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var walletConfirmationCode string
|
||||
var walletConfirmationUser string
|
||||
var walletTransactionAmnt string
|
||||
|
||||
func init() {
|
||||
command := Command{
|
||||
Cmd: []string{"wallet", "confirm"},
|
||||
Description: "$user $amount / $user $confirmation - Send or confirm a wallet payment",
|
||||
Help: "",
|
||||
Exec: cmdWallet,
|
||||
}
|
||||
|
||||
RegisterCommand(command)
|
||||
}
|
||||
|
||||
func cmdWallet(cmd []string) {
|
||||
if len(cmd) < 3 {
|
||||
return
|
||||
}
|
||||
if cmd[0] == "wallet" {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"abcdefghijklmnopqrstuvwxyz" +
|
||||
"0123456789")
|
||||
length := 8
|
||||
var b strings.Builder
|
||||
for i := 0; i < length; i++ {
|
||||
b.WriteRune(chars[rand.Intn(len(chars))])
|
||||
}
|
||||
walletConfirmationCode = b.String()
|
||||
walletConfirmationUser = cmd[1]
|
||||
walletTransactionAmnt = cmd[2]
|
||||
printInfo(fmt.Sprintf("To confirm sending %s to %s, type /confirm %s %s", cmd[2], cmd[1], cmd[1], walletConfirmationCode))
|
||||
|
||||
} else if cmd[0] == "confirm" {
|
||||
if cmd[1] == walletConfirmationUser && cmd[2] == walletConfirmationCode {
|
||||
txWallet := k.NewWallet()
|
||||
wAPI, err := txWallet.SendXLM(walletConfirmationUser, walletTransactionAmnt, "")
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("There was an error with your wallet tx:\n\t%+v", err))
|
||||
} else {
|
||||
printInfo(fmt.Sprintf("You have sent %sXLM to %s with tx ID: %s", wAPI.Result.Amount, wAPI.Result.ToUsername, wAPI.Result.TxID))
|
||||
}
|
||||
|
||||
} else {
|
||||
printError("There was an error validating your confirmation. Your wallet has been untouched.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
228
colors.go
228
colors.go
@ -1,228 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
black int = iota
|
||||
red
|
||||
green
|
||||
yellow
|
||||
purple
|
||||
magenta
|
||||
cyan
|
||||
grey
|
||||
normal int = -1
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func colorFromInt(color int) string {
|
||||
var result string
|
||||
result, ok := colorMapInt[color]
|
||||
if !ok {
|
||||
return "normal"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var basicStyle = Style{
|
||||
Foreground: colorMapInt[normal],
|
||||
Background: colorMapInt[normal],
|
||||
Italic: false,
|
||||
Bold: false,
|
||||
Underline: false,
|
||||
Strikethrough: false,
|
||||
Inverse: false,
|
||||
}
|
||||
|
||||
func (s Style) withForeground(color int) Style {
|
||||
s.Foreground = colorFromInt(color)
|
||||
return s
|
||||
}
|
||||
func (s Style) withBackground(color int) Style {
|
||||
s.Background = colorFromInt(color)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s Style) withBold() Style {
|
||||
s.Bold = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withInverse() Style {
|
||||
s.Inverse = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withItalic() Style {
|
||||
s.Italic = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withStrikethrough() Style {
|
||||
s.Strikethrough = true
|
||||
return s
|
||||
}
|
||||
func (s Style) withUnderline() Style {
|
||||
s.Underline = true
|
||||
return s
|
||||
}
|
||||
|
||||
// TODO create both as `reset` (which it is now) as well as `append`
|
||||
// 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 config.Basics.Colorless {
|
||||
return ""
|
||||
}
|
||||
styleSlice := []string{"0"}
|
||||
|
||||
if colorFromString(s.Foreground) != normal {
|
||||
styleSlice = append(styleSlice, fmt.Sprintf("%d", 30+colorFromString(s.Foreground)))
|
||||
}
|
||||
if colorFromString(s.Background) != normal {
|
||||
styleSlice = append(styleSlice, fmt.Sprintf("%d", 40+colorFromString(s.Background)))
|
||||
}
|
||||
if s.Bold {
|
||||
styleSlice = append(styleSlice, "1")
|
||||
}
|
||||
if s.Italic {
|
||||
styleSlice = append(styleSlice, "3")
|
||||
}
|
||||
if s.Underline {
|
||||
styleSlice = append(styleSlice, "4")
|
||||
}
|
||||
if s.Inverse {
|
||||
styleSlice = append(styleSlice, "7")
|
||||
}
|
||||
if s.Strikethrough {
|
||||
styleSlice = append(styleSlice, "9")
|
||||
}
|
||||
|
||||
return "\x1b[" + strings.Join(styleSlice, ";") + "m"
|
||||
}
|
||||
|
||||
// End Colors
|
||||
// Begin StyledString
|
||||
|
||||
// StyledString is used to save a message with a style, which can then later be rendered to a string
|
||||
type StyledString struct {
|
||||
message string
|
||||
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))
|
||||
//TODO handle posibility to escape
|
||||
re := regexp.MustCompile(`\$TEXT`)
|
||||
for len(re.FindAllString(text.message, 1)) > 0 {
|
||||
part := parts[0]
|
||||
parts = parts[1:]
|
||||
text = text.replaceN("$TEXT", part, 1)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (s Style) stylize(msg string) StyledString {
|
||||
return StyledString{msg, s}
|
||||
}
|
||||
func (ss StyledString) stringFollowedByStyle(style Style) string {
|
||||
return ss.style.toANSI() + ss.message + style.toANSI()
|
||||
}
|
||||
func (ss StyledString) string() string {
|
||||
return ss.stringFollowedByStyle(basicStyle)
|
||||
}
|
||||
|
||||
func (ss StyledString) replace(match string, value StyledString) StyledString {
|
||||
return ss.replaceN(match, value, -1)
|
||||
}
|
||||
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 (ss StyledString) replaceString(match string, value string) StyledString {
|
||||
ss.message = strings.Replace(ss.message, match, value, -1)
|
||||
return ss
|
||||
}
|
||||
|
||||
// Overrides current formatting
|
||||
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]
|
||||
}
|
||||
// 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 (ss StyledString) append(other StyledString) StyledString {
|
||||
ss.message = ss.message + other.stringFollowedByStyle(ss.style)
|
||||
return ss
|
||||
}
|
||||
func (ss StyledString) appendString(other string) StyledString {
|
||||
ss.message += other
|
||||
return ss
|
||||
}
|
||||
|
||||
// Begin Formatting
|
||||
|
||||
func removeFormatting(s string) string {
|
||||
reFormatting := regexp.MustCompile(`(?m)\x1b\[(\d*;?)*m`)
|
||||
return reFormatting.ReplaceAllString(s, "")
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
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
47
emojiMap.go
File diff suppressed because one or more lines are too long
28
go.mod
28
go.mod
@ -1,16 +1,24 @@
|
||||
module github.com/rudi9719/kbtui
|
||||
|
||||
go 1.16
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/awesome-gocui/gocui v1.0.1-0.20210720125732-36a608772b4d
|
||||
github.com/gdamore/tcell/v2 v2.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magefile/mage v1.11.0
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/pelletier/go-toml v1.9.1
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
github.com/charmbracelet/bubbletea v0.20.0
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/muesli/reflow v0.3.0
|
||||
samhofi.us/x/keybase v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
samhofi.us/x/keybase/v2 v2.1.1
|
||||
)
|
||||
|
||||
76
go.sum
76
go.sum
@ -1,46 +1,56 @@
|
||||
github.com/awesome-gocui/gocui v1.0.0 h1:1bf0DAr2JqWNxGFS8Kex4fM/khICjEnCi+a1+NfWy+w=
|
||||
github.com/awesome-gocui/gocui v1.0.0/go.mod h1:UvP3dP6+UsTGl9IuqP36wzz6Lemo90wn5p3tJvZ2OqY=
|
||||
github.com/awesome-gocui/gocui v1.0.1-0.20210720125732-36a608772b4d h1:5TGmGxIeTNcsvqqL1kbcPNP7RMG0wZtvPgmNmqB/UeY=
|
||||
github.com/awesome-gocui/gocui v1.0.1-0.20210720125732-36a608772b4d/go.mod h1:UvP3dP6+UsTGl9IuqP36wzz6Lemo90wn5p3tJvZ2OqY=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.3.5 h1:fSiuoOf40N1w1otj2kQf4IlJ7rI/dcF3zVZL+GRmwuQ=
|
||||
github.com/gdamore/tcell/v2 v2.3.5/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
|
||||
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
|
||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
||||
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
|
||||
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
|
||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
|
||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
|
||||
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc=
|
||||
github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b h1:qh4f65QIVFjq9eBURLEYWqaEXmOyqdUyiBSgaXWccWk=
|
||||
golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00=
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
samhofi.us/x/keybase v1.0.0 h1:ht//EtYMS/hQeZCznA1ibQ515JCKaEkvTD/tarw/9k8=
|
||||
samhofi.us/x/keybase v1.0.0/go.mod h1:fcva80IUFyWcHtV4bBSzgKg07K6Rvuvi3GtGCLNGkyE=
|
||||
samhofi.us/x/keybase/v2 v2.1.1 h1:XPWrmdbJCrNcsW3sRuR6WuALYOZt7O+av0My6YoehqE=
|
||||
samhofi.us/x/keybase/v2 v2.1.1/go.mod h1:lJivwhzMSV+WUg+XUbatszStjjFVcuLGl+xcQpqQ5GQ=
|
||||
|
||||
14
keybase.go
Normal file
14
keybase.go
Normal file
@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"samhofi.us/x/keybase/v2/types/chat1"
|
||||
)
|
||||
|
||||
|
||||
func handleChat(m chat1.MsgSummary) {
|
||||
log.Println(m)
|
||||
mainModel.chat = append(mainModel.chat, m)
|
||||
mainModel.Update(m)
|
||||
}
|
||||
101
mage.go
101
mage.go
@ -1,101 +0,0 @@
|
||||
// +build mage
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/magefile/mage/mg"
|
||||
"github.com/magefile/mage/sh"
|
||||
)
|
||||
|
||||
func getRemotePackages() error {
|
||||
var packages = []string{
|
||||
"samhofi.us/x/keybase",
|
||||
"github.com/awesome-gocui/gocui",
|
||||
"github.com/magefile/mage/mage",
|
||||
"github.com/magefile/mage/mg",
|
||||
"github.com/magefile/mage/sh",
|
||||
"github.com/pelletier/go-toml",
|
||||
}
|
||||
for _, p := range packages {
|
||||
if err := sh.Run("go", "get", "-u", p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// proper error reporting and exit code
|
||||
func exit(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Build kbtui with just the basic commands.
|
||||
func Build() {
|
||||
mg.Deps(getRemotePackages)
|
||||
if err := sh.Run("go", "build"); err != nil {
|
||||
defer func() {
|
||||
exit(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Build kbtui with the basic commands, and the ShowReactions "TypeCommand".
|
||||
// The ShowReactions TypeCommand will print a message in the feed window when
|
||||
// a reaction is received in the current conversation.
|
||||
func BuildShowReactions() {
|
||||
mg.Deps(getRemotePackages)
|
||||
if err := sh.Run("go", "build", "-tags", "showreactionscmd"); err != nil {
|
||||
defer func() {
|
||||
exit(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Build kbtui with the basec commands, and the AutoReact "TypeCommand".
|
||||
// The AutoReact TypeCommand will automatically react to every message
|
||||
// received in the current conversation. This gets pretty annoying, and
|
||||
// is not recommended.
|
||||
func BuildAutoReact() {
|
||||
mg.Deps(getRemotePackages)
|
||||
if err := sh.Run("go", "build", "-tags", "autoreactcmd"); err != nil {
|
||||
defer func() {
|
||||
exit(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Build kbtui with all commands and TypeCommands disabled.
|
||||
func BuildAllCommands() {
|
||||
mg.Deps(getRemotePackages)
|
||||
if err := sh.Run("go", "build", "-tags", "allcommands"); err != nil {
|
||||
defer func() {
|
||||
exit(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
defer func() {
|
||||
exit(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Build kbtui with beta functionality
|
||||
func BuildBeta() {
|
||||
mg.Deps(getRemotePackages)
|
||||
if err := sh.Run("go", "build", "-tags", "allcommands showreactionscmd tabcompletion execcmd"); err != nil {
|
||||
defer func() {
|
||||
exit(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
801
main.go
801
main.go
@ -1,758 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"samhofi.us/x/keybase"
|
||||
"unicode/utf8"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/muesli/reflow/indent"
|
||||
|
||||
"samhofi.us/x/keybase/v2"
|
||||
"samhofi.us/x/keybase/v2/types/chat1"
|
||||
)
|
||||
|
||||
var (
|
||||
typeCommands = make(map[string]TypeCommand)
|
||||
commands = make(map[string]Command)
|
||||
baseCommands = make([]string, 0)
|
||||
|
||||
dev = false
|
||||
k = keybase.NewKeybase()
|
||||
channel keybase.Channel
|
||||
channels []keybase.Channel
|
||||
stream = false
|
||||
lastMessage keybase.ChatAPI
|
||||
lastChat = ""
|
||||
g *gocui.Gui
|
||||
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render
|
||||
k = keybase.NewKeybase()
|
||||
mainModel *model
|
||||
)
|
||||
|
||||
var config *Config
|
||||
|
||||
func main() {
|
||||
if !k.LoggedIn {
|
||||
fmt.Println("You are not logged in.")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
g, err = gocui.NewGui(gocui.OutputNormal, false)
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
defer g.Close()
|
||||
g.SetManagerFunc(layout)
|
||||
RunCommand("config", "load")
|
||||
go populateList()
|
||||
go updateChatWindow()
|
||||
if len(os.Args) > 1 {
|
||||
os.Args[0] = "join"
|
||||
RunCommand(os.Args...)
|
||||
var (
|
||||
daemonMode bool
|
||||
showHelp bool
|
||||
opts []tea.ProgramOption
|
||||
)
|
||||
|
||||
}
|
||||
fmt.Println("initKeybindings")
|
||||
if err := initKeybindings(); err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
go generateChannelTabCompletionSlice()
|
||||
}
|
||||
flag.BoolVar(&daemonMode, "d", false, "run as a daemon")
|
||||
flag.BoolVar(&showHelp, "h", false, "show help")
|
||||
flag.Parse()
|
||||
|
||||
// Gocui basic setup
|
||||
func layout(g *gocui.Gui) error {
|
||||
maxX, maxY := g.Size()
|
||||
if editView, err := g.SetView("Edit", maxX/2-maxX/3+1, maxY/2, maxX-2, maxY/2+10, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
editView.Editable = true
|
||||
editView.Wrap = true
|
||||
fmt.Fprintln(editView, "Edit window. Should disappear")
|
||||
if showHelp {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
if feedView, err := g.SetView("Feed", maxX/2-maxX/3, 0, maxX-1, maxY/5, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
feedView.Autoscroll = true
|
||||
feedView.Wrap = true
|
||||
feedView.Title = "Feed Window"
|
||||
printInfo("Feed Window - If you are mentioned or receive a PM it will show here")
|
||||
}
|
||||
if chatView, err2 := g.SetView("Chat", maxX/2-maxX/3, maxY/5+1, maxX-1, maxY-5, 0); err2 != nil {
|
||||
if err2 != gocui.ErrUnknownView {
|
||||
return err2
|
||||
}
|
||||
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", config.Colors.Message.Mention.stylize(k.Username))
|
||||
fmt.Fprintln(chatView, welcomeText.string())
|
||||
RunCommand("help")
|
||||
}
|
||||
if inputView, err3 := g.SetView("Input", maxX/2-maxX/3, maxY-4, maxX-1, maxY-1, 0); err3 != nil {
|
||||
if err3 != gocui.ErrUnknownView {
|
||||
return err3
|
||||
}
|
||||
if _, err := g.SetCurrentView("Input"); err != nil {
|
||||
return err
|
||||
}
|
||||
inputView.Editable = true
|
||||
inputView.Wrap = true
|
||||
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 {
|
||||
if err4 != gocui.ErrUnknownView {
|
||||
return err4
|
||||
}
|
||||
listView.Title = "Channels"
|
||||
fmt.Fprintf(listView, "Lists\nWindow\nTo view\n activity")
|
||||
}
|
||||
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")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if input != "" {
|
||||
clearView("Input")
|
||||
return nil
|
||||
}
|
||||
return gocui.ErrQuit
|
||||
}); 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")
|
||||
popupView("Input")
|
||||
clearView("Edit")
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("Input", gocui.KeyEnter, gocui.ModNone,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
return handleInput("Input")
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("Input", gocui.KeyTab, gocui.ModNone,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
return handleTab("Input")
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("Edit", gocui.KeyEnter, gocui.ModNone,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
popupView("Chat")
|
||||
popupView("Input")
|
||||
return handleInput("Edit")
|
||||
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("Input", gocui.KeyArrowUp, gocui.ModNone,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
RunCommand("edit")
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// End gocui basic setup
|
||||
|
||||
// Gocui helper funcs
|
||||
func setViewTitle(viewName string, title string) {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
updatingView, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updatingView.Title = title
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func getViewTitle(viewName string) string {
|
||||
view, err := g.View(viewName)
|
||||
if err != nil {
|
||||
// in case there is active tab completion, filter that to just the view title and not the completion options.
|
||||
printError(fmt.Sprintf("Error getting view title: %s", err))
|
||||
return ""
|
||||
}
|
||||
return strings.Split(view.Title, "||")[0]
|
||||
}
|
||||
func popupView(viewName string) {
|
||||
_, err := g.SetCurrentView(viewName)
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("%+v", err))
|
||||
}
|
||||
_, err = g.SetViewOnTop(viewName)
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("%+v", err))
|
||||
}
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
updatingView, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updatingView.MoveCursor(0, 0)
|
||||
|
||||
return nil
|
||||
|
||||
})
|
||||
}
|
||||
func moveCursorToEnd(viewName string) {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
inputView, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputString, _ := getInputString(viewName)
|
||||
stringLen := len(inputString)
|
||||
maxX, _ := inputView.Size()
|
||||
x := stringLen % maxX
|
||||
y := stringLen / maxX
|
||||
inputView.SetCursor(0, 0)
|
||||
inputView.SetOrigin(0, 0)
|
||||
inputView.MoveCursor(x, y)
|
||||
return nil
|
||||
|
||||
})
|
||||
}
|
||||
func clearView(viewName string) {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
inputView, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputView.Clear()
|
||||
inputView.SetCursor(0, 0)
|
||||
inputView.SetOrigin(0, 0)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
func writeToView(viewName string, message string) {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
updatingView, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range message {
|
||||
updatingView.EditWrite(c)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// this removes formatting
|
||||
func printError(message string) {
|
||||
printErrorF(message)
|
||||
}
|
||||
func printErrorF(message string, parts ...StyledString) {
|
||||
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", config.Colors.Feed.Basic.sprintf(removeFormatting(message), parts...).string())
|
||||
}
|
||||
func printToView(viewName string, message string) {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
updatingView, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.Basics.UnicodeEmojis {
|
||||
message = emojiUnicodeConvert(message)
|
||||
}
|
||||
fmt.Fprintf(updatingView, "%s\n", message)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// End gocui helper funcs
|
||||
|
||||
// Update/Populate views automatically
|
||||
func updateChatWindow() {
|
||||
|
||||
runOpts := keybase.RunOptions{
|
||||
Dev: dev,
|
||||
}
|
||||
k.Run(func(api keybase.ChatAPI) {
|
||||
handleMessage(api)
|
||||
},
|
||||
runOpts)
|
||||
|
||||
}
|
||||
func populateChat() {
|
||||
lastMessage.ID = 0
|
||||
chat := k.NewChat(channel)
|
||||
maxX, _ := g.Size()
|
||||
api, err := chat.Read(maxX / 2)
|
||||
if err != nil || api.Result == nil {
|
||||
for _, testChan := range channels {
|
||||
if channel.Name == testChan.Name {
|
||||
channel = testChan
|
||||
channel.TopicName = "general"
|
||||
}
|
||||
}
|
||||
chat = k.NewChat(channel)
|
||||
_, err2 := chat.Read(2)
|
||||
if err2 != nil {
|
||||
printError(fmt.Sprintf("%+v", err))
|
||||
return
|
||||
}
|
||||
go populateChat()
|
||||
go generateChannelTabCompletionSlice()
|
||||
return
|
||||
}
|
||||
var printMe []string
|
||||
var actuallyPrintMe string
|
||||
if len(api.Result.Messages) > 0 {
|
||||
lastMessage.ID = api.Result.Messages[0].Msg.ID
|
||||
}
|
||||
for _, message := range api.Result.Messages {
|
||||
if message.Msg.Content.Type == "text" || message.Msg.Content.Type == "attachment" {
|
||||
if lastMessage.ID < 1 {
|
||||
lastMessage.ID = message.Msg.ID
|
||||
}
|
||||
var apiCast keybase.ChatAPI
|
||||
apiCast.Msg = &message.Msg
|
||||
newMessage := formatOutput(apiCast).string()
|
||||
printMe = append(printMe, newMessage)
|
||||
}
|
||||
}
|
||||
for i := len(printMe) - 1; i >= 0; i-- {
|
||||
actuallyPrintMe += printMe[i]
|
||||
if i > 0 {
|
||||
actuallyPrintMe += "\n"
|
||||
}
|
||||
}
|
||||
printToView("Chat", actuallyPrintMe)
|
||||
go populateList()
|
||||
}
|
||||
func populateList() {
|
||||
_, maxY := g.Size()
|
||||
if testVar, err := k.ChatList(); err != nil {
|
||||
log.Printf("%+v", err)
|
||||
if daemonMode || !isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
// If we're in daemon mode don't render the TUI
|
||||
opts = []tea.ProgramOption{tea.WithoutRenderer()}
|
||||
} else {
|
||||
clearView("List")
|
||||
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(config.Colors.Channels.Header.stylize("---[Teams]---\n"))
|
||||
var recentChannelsCount = 0
|
||||
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(config.Colors.Channels.Unread.stylize("*" + channel))
|
||||
} else {
|
||||
recentChannels = recentChannels.appendString(channel)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
recentPMsCount++
|
||||
if recentPMsCount <= ((maxY - 2) / 3) {
|
||||
pmName := fmt.Sprintf("%s\n", cleanChannelName(s.Channel.Name))
|
||||
if s.Unread {
|
||||
recentPMs = recentPMs.append(config.Colors.Channels.Unread.stylize("*" + pmName))
|
||||
} else {
|
||||
recentPMs = recentPMs.appendString(pmName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
printToView("List", fmt.Sprintf("%s%s", recentPMs.string(), recentChannels.string()))
|
||||
generateRecentTabCompletionSlice()
|
||||
// If we're in TUI mode, discard log output
|
||||
log.SetOutput(ioutil.Discard)
|
||||
}
|
||||
m1 := newModel()
|
||||
mainModel = &m1
|
||||
chatHandler := handleChat
|
||||
handlers := keybase.Handlers{
|
||||
ChatHandler: &chatHandler,
|
||||
}
|
||||
go k.Run(handlers, &keybase.RunOptions{})
|
||||
p := tea.NewProgram(mainModel, opts...)
|
||||
if err := p.Start(); err != nil {
|
||||
fmt.Println("Error starting Bubble Tea program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// End update/populate views automatically
|
||||
func newModel() model {
|
||||
sp := spinner.New()
|
||||
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("206"))
|
||||
|
||||
// Formatting
|
||||
func formatMessageBody(body string) StyledString {
|
||||
body = strings.Replace(body, "```", "\n<code>\n", -1)
|
||||
message := config.Colors.Message.Body.stylize(body)
|
||||
|
||||
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)
|
||||
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
|
||||
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 config.Colors.Message.LinkKeybase.stylize(fmt.Sprintf("@%s#%s", ch.Name, ch.TopicName))
|
||||
}
|
||||
|
||||
func colorReplaceMentionMe(msg StyledString) StyledString {
|
||||
return msg.colorRegex(`(@?\b`+k.Username+`\b)`, config.Colors.Message.Mention)
|
||||
}
|
||||
func colorUsername(username string) StyledString {
|
||||
var color = config.Colors.Message.SenderDefault
|
||||
if username == k.Username {
|
||||
color = config.Colors.Message.Mention
|
||||
return model{
|
||||
spinner: sp,
|
||||
}
|
||||
return color.stylize(username)
|
||||
}
|
||||
|
||||
func cleanChannelName(c string) string {
|
||||
newChannelName := strings.Replace(c, fmt.Sprintf("%s,", k.Username), "", 1)
|
||||
return strings.Replace(newChannelName, fmt.Sprintf(",%s", k.Username), "", 1)
|
||||
func (m model) Init() tea.Cmd {
|
||||
log.Println("Starting work...")
|
||||
return tea.Batch(
|
||||
spinner.Tick,
|
||||
)
|
||||
}
|
||||
|
||||
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 = config.Colors.Message.Header.stylize(formatString)
|
||||
tm := time.Unix(int64(msg.SentAt), 0)
|
||||
var body = formatMessageBody(msg.Content.Text.Body)
|
||||
if msgType == "attachment" {
|
||||
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(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
|
||||
}
|
||||
|
||||
func formatOutput(api keybase.ChatAPI) StyledString {
|
||||
format := config.Formatting.OutputFormat
|
||||
if stream {
|
||||
format = config.Formatting.OutputStreamFormat
|
||||
}
|
||||
return formatMessage(api, format)
|
||||
}
|
||||
|
||||
// End formatting
|
||||
|
||||
// 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 {
|
||||
} else {
|
||||
go typeCommands[api.Msg.Content.Type].Exec(api)
|
||||
}
|
||||
}
|
||||
}
|
||||
if api.Msg.Content.Type == "text" || api.Msg.Content.Type == "attachment" {
|
||||
go populateList()
|
||||
msgSender := api.Msg.Sender.Username
|
||||
if !stream {
|
||||
if msgSender != k.Username {
|
||||
if api.Msg.Channel.MembersType == keybase.TEAM {
|
||||
topicName := api.Msg.Channel.TopicName
|
||||
for _, m := range api.Msg.Content.Text.UserMentions {
|
||||
if m.Text == k.Username {
|
||||
// We are in a team
|
||||
if topicName != channel.TopicName {
|
||||
printInfoStyledString(formatMessage(api, config.Formatting.OutputMentionFormat))
|
||||
fmt.Print("\a")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if msgSender != channel.Name {
|
||||
printInfoStyledString(formatMessage(api, config.Formatting.PMFormat))
|
||||
fmt.Print("\a")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
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).string())
|
||||
chat := k.NewChat(channel)
|
||||
lastMessage.ID = api.Msg.ID
|
||||
chat.Read(api.Msg.ID)
|
||||
}
|
||||
}
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "ctrl+c" {
|
||||
m.quitting= true
|
||||
return m, tea.Quit
|
||||
} else {
|
||||
if api.Msg.Channel.MembersType == keybase.TEAM {
|
||||
printToView("Chat", formatOutput(api).string())
|
||||
} else {
|
||||
printToView("Chat", formatMessage(api, config.Formatting.PMFormat).string())
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
} else {
|
||||
//TODO: For edit/delete run this
|
||||
if api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name {
|
||||
go populateChat()
|
||||
}
|
||||
}
|
||||
}
|
||||
func getInputString(viewName string) (string, error) {
|
||||
inputView, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
retString := inputView.Buffer()
|
||||
retString = strings.Replace(retString, "\n", "", 800)
|
||||
return retString, err
|
||||
}
|
||||
func deleteEmpty(s []string) []string {
|
||||
var r []string
|
||||
for _, str := range s {
|
||||
if str != "" {
|
||||
r = append(r, str)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
func handleInput(viewName string) error {
|
||||
clearView(viewName)
|
||||
inputString, _ := getInputString(viewName)
|
||||
if inputString == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(inputString, config.Basics.CmdPrefix) {
|
||||
cmd := deleteEmpty(strings.Split(inputString[len(config.Basics.CmdPrefix):], " "))
|
||||
if len(cmd) < 1 {
|
||||
return nil
|
||||
}
|
||||
if c, ok := commands[cmd[0]]; ok {
|
||||
c.Exec(cmd)
|
||||
return nil
|
||||
} else if cmd[0] == "q" || cmd[0] == "quit" {
|
||||
return gocui.ErrQuit
|
||||
} else {
|
||||
printError(fmt.Sprintf("Command '%s' not recognized", cmd[0]))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if inputString[:1] == "+" || inputString[:1] == "-" {
|
||||
cmd := strings.Split(inputString, " ")
|
||||
cmd[0] = inputString[:1]
|
||||
RunCommand(cmd...)
|
||||
} else {
|
||||
inputString = resolveRootEmojis(inputString)
|
||||
go sendChat(inputString)
|
||||
}
|
||||
// restore any tab completion view titles on input commit
|
||||
if newViewTitle := getViewTitle(viewName); newViewTitle != "" {
|
||||
setViewTitle(viewName, newViewTitle)
|
||||
}
|
||||
|
||||
go populateList()
|
||||
return nil
|
||||
}
|
||||
func sendChat(message string) {
|
||||
autoScrollView("Chat")
|
||||
chat := k.NewChat(channel)
|
||||
_, err := chat.Send(message)
|
||||
if err != nil {
|
||||
printError(fmt.Sprintf("There was an error %+v", err))
|
||||
case spinner.TickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
case chat1.MsgSummary:
|
||||
log.Println("chat1.MsgSummary passed to m.Update()")
|
||||
var cmd tea.Cmd
|
||||
return m, cmd
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// End input handling
|
||||
func (m model) View() string {
|
||||
s := "\n"
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
// RegisterTypeCommand registers a command to be used within the client
|
||||
func RegisterTypeCommand(c TypeCommand) error {
|
||||
var notAdded string
|
||||
for _, cmd := range c.Cmd {
|
||||
if _, ok := typeCommands[cmd]; !ok {
|
||||
typeCommands[cmd] = c
|
||||
continue
|
||||
for _, res := range mainModel.chat {
|
||||
log.Println(res)
|
||||
if res.Content.TypeName == "text" {
|
||||
s += fmt.Sprintf("%+v: %+v\n", res.Sender.Username, res.Content.Text.Body)
|
||||
}
|
||||
notAdded = fmt.Sprintf("%s, %s", notAdded, cmd)
|
||||
}
|
||||
if notAdded != "" {
|
||||
return fmt.Errorf("The following aliases were not added because they already exist: %s", notAdded)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterCommand registers a command to be used within the client
|
||||
func RegisterCommand(c Command) error {
|
||||
var notAdded string
|
||||
for i, cmd := range c.Cmd {
|
||||
if _, ok := commands[cmd]; !ok {
|
||||
if i == 0 {
|
||||
baseCommands = append(baseCommands, cmd)
|
||||
}
|
||||
commands[cmd] = c
|
||||
continue
|
||||
}
|
||||
notAdded = fmt.Sprintf("%s, %s", notAdded, cmd)
|
||||
}
|
||||
if notAdded != "" {
|
||||
return fmt.Errorf("The following aliases were not added because they already exist: %s", notAdded)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
s += helpStyle("\nCtrl+C to exit\n")
|
||||
|
||||
// RunCommand calls a command as if it was run by the user
|
||||
func RunCommand(c ...string) {
|
||||
commands[c[0]].Exec(c)
|
||||
if m.quitting {
|
||||
s += "\n"
|
||||
}
|
||||
|
||||
return indent.String(s, 1)
|
||||
}
|
||||
223
tabComplete.go
223
tabComplete.go
@ -1,223 +0,0 @@
|
||||
// +build !rm_basic_commands allcommands tabcompletion
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"samhofi.us/x/keybase"
|
||||
)
|
||||
|
||||
var (
|
||||
tabSlice []string
|
||||
commandSlice []string
|
||||
)
|
||||
|
||||
// This defines the handleTab function thats called by key bindind tab for the input control.
|
||||
func handleTab(viewName string) error {
|
||||
inputString, err := getInputString(viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// if you successfully get an input string, grab the last word from the string
|
||||
ss := regexp.MustCompile(`[ #]`).Split(inputString, -1)
|
||||
s := ss[len(ss)-1]
|
||||
// create a variable in which to store the result
|
||||
var resultSlice []string
|
||||
// if the word starts with a : its an emoji lookup
|
||||
if strings.HasPrefix(s, ":") {
|
||||
resultSlice = getEmojiTabCompletionSlice(s)
|
||||
} else if strings.HasPrefix(s, "/") {
|
||||
generateCommandTabCompletionSlice()
|
||||
s = strings.Replace(s, "/", "", 1)
|
||||
resultSlice = getCommandTabCompletionSlice(s)
|
||||
} else {
|
||||
if strings.HasPrefix(s, "@") {
|
||||
// now in case the word (s) is a mention @something, lets remove it to normalize
|
||||
s = strings.Replace(s, "@", "", 1)
|
||||
}
|
||||
// now call get the list of all possible cantidates that have that as a prefix
|
||||
resultSlice = getChannelTabCompletionSlice(s)
|
||||
}
|
||||
rLen := len(resultSlice)
|
||||
lcp := longestCommonPrefix(resultSlice)
|
||||
if lcp != "" {
|
||||
originalViewTitle := getViewTitle("Input")
|
||||
newViewTitle := ""
|
||||
if rLen >= 1 && originalViewTitle != "" {
|
||||
if rLen == 1 {
|
||||
newViewTitle = originalViewTitle
|
||||
} else if rLen <= 5 {
|
||||
newViewTitle = fmt.Sprintf("%s|| %s", originalViewTitle, strings.Join(resultSlice, " "))
|
||||
} else if rLen > 5 {
|
||||
newViewTitle = fmt.Sprintf("%s|| %s +%d more", originalViewTitle, strings.Join(resultSlice[:6], " "), rLen-5)
|
||||
}
|
||||
setViewTitle(viewName, newViewTitle)
|
||||
remainder := stringRemainder(s, lcp)
|
||||
writeToView(viewName, remainder)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 := filterEmojiMap(emojiMap, inputWord)
|
||||
return resultSlice
|
||||
}
|
||||
func getChannelTabCompletionSlice(inputWord string) []string {
|
||||
// use the tabSlice from above and filter it for the input word
|
||||
resultSlice := filterStringSlice(tabSlice, inputWord)
|
||||
return resultSlice
|
||||
}
|
||||
func getCommandTabCompletionSlice(inputWord string) []string {
|
||||
// use the commandSlice from above and filter it for the input word
|
||||
resultSlice := filterStringSlice(commandSlice, inputWord)
|
||||
return resultSlice
|
||||
}
|
||||
|
||||
//Generator Functions (should be called externally when chat/list/join changes
|
||||
func generateChannelTabCompletionSlice() {
|
||||
// fetch all members of the current channel and add them to the slice
|
||||
channelSlice := getCurrentChannelMembership()
|
||||
for _, m := range channelSlice {
|
||||
tabSlice = appendIfNotInSlice(tabSlice, m)
|
||||
}
|
||||
}
|
||||
func generateCommandTabCompletionSlice() {
|
||||
// get the maps of all built commands - this should only need to be done on startup
|
||||
// removing typeCommands for now, since they aren't actually commands you can type - contrary to the naming
|
||||
/*for commandString1 := range typeCommands {
|
||||
commandSlice = appendIfNotInSlice(commandSlice, commandString1)
|
||||
}*/
|
||||
for commandString2 := range commands {
|
||||
commandSlice = appendIfNotInSlice(commandSlice, commandString2)
|
||||
}
|
||||
for _, commandString3 := range baseCommands {
|
||||
commandSlice = appendIfNotInSlice(commandSlice, commandString3)
|
||||
}
|
||||
}
|
||||
func generateRecentTabCompletionSlice() {
|
||||
var recentSlice []string
|
||||
for _, s := range channels {
|
||||
if s.MembersType == keybase.TEAM {
|
||||
// its a team so add the topic name and channel name
|
||||
recentSlice = appendIfNotInSlice(recentSlice, s.TopicName)
|
||||
recentSlice = appendIfNotInSlice(recentSlice, s.Name)
|
||||
} else {
|
||||
//its a user, so clean the name and append
|
||||
recentSlice = appendIfNotInSlice(recentSlice, cleanChannelName(s.Name))
|
||||
}
|
||||
}
|
||||
for _, s := range recentSlice {
|
||||
tabSlice = appendIfNotInSlice(tabSlice, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func getCurrentChannelMembership() []string {
|
||||
var rs []string
|
||||
if channel.Name != "" {
|
||||
t := k.NewTeam(channel.Name)
|
||||
testVar, err := t.MemberList()
|
||||
if err != nil {
|
||||
return rs // then this isn't a team, its a PM or there was an error in the API call
|
||||
}
|
||||
for _, m := range testVar.Result.Members.Owners {
|
||||
rs = append(rs, fmt.Sprintf("%+v", m.Username))
|
||||
}
|
||||
for _, m := range testVar.Result.Members.Admins {
|
||||
rs = append(rs, fmt.Sprintf("%+v", m.Username))
|
||||
}
|
||||
for _, m := range testVar.Result.Members.Writers {
|
||||
rs = append(rs, fmt.Sprintf("%+v", m.Username))
|
||||
}
|
||||
for _, m := range testVar.Result.Members.Readers {
|
||||
rs = append(rs, fmt.Sprintf("%+v", m.Username))
|
||||
}
|
||||
|
||||
}
|
||||
return rs
|
||||
}
|
||||
func filterStringSlice(ss []string, fv string) []string {
|
||||
var rs []string
|
||||
for _, s := range ss {
|
||||
if strings.HasPrefix(s, fv) {
|
||||
rs = append(rs, s)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return ss[0]
|
||||
}
|
||||
// all strings are compared by bytes here forward (TBD unicode normalization?)
|
||||
// establish min, max lenth members of the slice by iterating over the members
|
||||
min, max := ss[0], ss[0]
|
||||
for _, s := range ss[1:] {
|
||||
switch {
|
||||
case s < min:
|
||||
min = s
|
||||
case s > max:
|
||||
max = s
|
||||
}
|
||||
}
|
||||
// then iterate over the characters from min to max, as soon as chars don't match return
|
||||
for i := 0; i < len(min) && i < len(max); i++ {
|
||||
if min[i] != max[i] {
|
||||
return min[:i]
|
||||
}
|
||||
}
|
||||
// to cover the case where all members are equal, just return one
|
||||
return min
|
||||
}
|
||||
func stringRemainder(aStr, bStr string) string {
|
||||
var long, short string
|
||||
//figure out which string is longer
|
||||
switch {
|
||||
case len(aStr) < len(bStr):
|
||||
short = aStr
|
||||
long = bStr
|
||||
default:
|
||||
short = bStr
|
||||
long = aStr
|
||||
}
|
||||
// iterate over the strings using an external iterator so we don't lose the value
|
||||
i := 0
|
||||
for i < len(short) && i < len(long) {
|
||||
if short[i] != long[i] {
|
||||
// the strings aren't equal so don't return anything
|
||||
return ""
|
||||
}
|
||||
i++
|
||||
}
|
||||
// return whatever's left of the longer string
|
||||
return long[i:]
|
||||
}
|
||||
func appendIfNotInSlice(ss []string, s string) []string {
|
||||
for _, element := range ss {
|
||||
if element == s {
|
||||
return ss
|
||||
}
|
||||
}
|
||||
return append(ss, s)
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
// +build !rm_basic_commands type_commands showreactionscmd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"samhofi.us/x/keybase"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := TypeCommand{
|
||||
Cmd: []string{"reaction"},
|
||||
Name: "ShowReactions",
|
||||
Description: "Prints a message in the feed any time a reaction is received",
|
||||
Exec: tcmdShowReactions,
|
||||
}
|
||||
|
||||
RegisterTypeCommand(command)
|
||||
}
|
||||
|
||||
func tcmdShowReactions(m keybase.ChatAPI) {
|
||||
team := false
|
||||
user := colorUsername(m.Msg.Sender.Username)
|
||||
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)
|
||||
} else {
|
||||
}
|
||||
printInfoF("$TEXT reacted to [$TEXT] with $TEXT in $TEXT", user, id, reaction, where)
|
||||
if channel.Name == m.Msg.Channel.Name {
|
||||
if team {
|
||||
if channel.TopicName == m.Msg.Channel.TopicName {
|
||||
clearView("Chat")
|
||||
go populateChat()
|
||||
}
|
||||
|
||||
} else {
|
||||
clearView("Chat")
|
||||
go populateChat()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
14
types.go
14
types.go
@ -1,6 +1,16 @@
|
||||
package main
|
||||
|
||||
import "samhofi.us/x/keybase"
|
||||
import "samhofi.us/x/keybase/v2/types/chat1"
|
||||
import "github.com/charmbracelet/bubbles/spinner"
|
||||
|
||||
type model struct {
|
||||
chat []chat1.MsgSummary
|
||||
conversations []Channels
|
||||
feed []chat1.MsgSummary
|
||||
currentConversation chat1.ChatChannel
|
||||
spinner spinner.Model
|
||||
quitting bool
|
||||
}
|
||||
|
||||
// Command outlines a command
|
||||
type Command struct {
|
||||
@ -15,7 +25,7 @@ type TypeCommand struct {
|
||||
Cmd []string // Message types that trigger this command
|
||||
Name string // The name of this 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(chat1.MsgSummary) // A function that takes a raw chat message as input
|
||||
}
|
||||
|
||||
// Config holds user-configurable values
|
||||
|
||||
58
userTags.go
58
userTags.go
@ -1,58 +0,0 @@
|
||||
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