Gregory Rudolph
3 years ago
34 changed files with 167 additions and 2598 deletions
@ -1,12 +0,0 @@ |
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/magefile/mage/mage" |
|
||||||
"os" |
|
||||||
) |
|
||||||
|
|
||||||
func main() { |
|
||||||
os.Exit(mage.Main()) |
|
||||||
} |
|
@ -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() |
|
||||||
} |
|
@ -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 |
|
||||||
} |
|
@ -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) |
|
||||||
} |
|
||||||
} |
|
@ -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)) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -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)) |
|
||||||
} |
|
@ -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)) |
|
||||||
} |
|
@ -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) |
|
||||||
} |
|
@ -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()) |
|
||||||
} |
|
@ -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)) |
|
||||||
} |
|
||||||
} |
|
@ -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.") |
|
||||||
} |
|
||||||
} |
|
@ -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.") |
|
||||||
} |
|
||||||
} |
|
@ -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 |
|
||||||
} |
|
||||||
} |
|
@ -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:], " ")) |
|
||||||
} |
|
@ -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") |
|
||||||
} |
|
@ -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) |
|
||||||
} |
|
||||||
} |
|
@ -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) |
|
||||||
} |
|
@ -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.") |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -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" |
|
||||||
` |
|
File diff suppressed because one or more lines are too long
@ -1,16 +1,24 @@ |
|||||||
module github.com/rudi9719/kbtui |
module github.com/rudi9719/kbtui |
||||||
|
|
||||||
go 1.16 |
go 1.17 |
||||||
|
|
||||||
require ( |
require ( |
||||||
github.com/awesome-gocui/gocui v1.0.1-0.20210720125732-36a608772b4d |
github.com/charmbracelet/bubbles v0.10.3 |
||||||
github.com/gdamore/tcell/v2 v2.4.0 // indirect |
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/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/mattn/go-runewidth v0.0.13 // indirect |
||||||
github.com/pelletier/go-toml v1.9.1 |
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect |
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect |
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect |
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect |
github.com/rivo/uniseg v0.2.0 // indirect |
||||||
golang.org/x/text v0.3.6 // indirect |
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect |
||||||
samhofi.us/x/keybase v1.0.0 |
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect |
||||||
|
samhofi.us/x/keybase/v2 v2.1.1 |
||||||
) |
) |
||||||
|
@ -1,46 +1,56 @@ |
|||||||
github.com/awesome-gocui/gocui v1.0.0 h1:1bf0DAr2JqWNxGFS8Kex4fM/khICjEnCi+a1+NfWy+w= |
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= |
||||||
github.com/awesome-gocui/gocui v1.0.0/go.mod h1:UvP3dP6+UsTGl9IuqP36wzz6Lemo90wn5p3tJvZ2OqY= |
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho= |
||||||
github.com/awesome-gocui/gocui v1.0.1-0.20210720125732-36a608772b4d h1:5TGmGxIeTNcsvqqL1kbcPNP7RMG0wZtvPgmNmqB/UeY= |
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= |
||||||
github.com/awesome-gocui/gocui v1.0.1-0.20210720125732-36a608772b4d/go.mod h1:UvP3dP6+UsTGl9IuqP36wzz6Lemo90wn5p3tJvZ2OqY= |
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= |
||||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= |
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc= |
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= |
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM= |
||||||
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= |
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= |
||||||
github.com/gdamore/tcell/v2 v2.3.5 h1:fSiuoOf40N1w1otj2kQf4IlJ7rI/dcF3zVZL+GRmwuQ= |
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= |
||||||
github.com/gdamore/tcell/v2 v2.3.5/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= |
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= |
||||||
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= |
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= |
||||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= |
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= |
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= |
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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= |
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= |
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/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= |
||||||
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= |
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= |
||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= |
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= |
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= |
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= |
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 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= |
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= |
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/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= |
||||||
github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= |
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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= |
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= |
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= |
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= |
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= |
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= |
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b h1:qh4f65QIVFjq9eBURLEYWqaEXmOyqdUyiBSgaXWccWk= |
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= |
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/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/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00= |
||||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= |
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= |
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= |
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= |
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= |
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
||||||
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= |
|
||||||
samhofi.us/x/keybase v1.0.0 h1:ht//EtYMS/hQeZCznA1ibQ515JCKaEkvTD/tarw/9k8= |
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 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= |
||||||
|
@ -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) |
||||||
|
} |
@ -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) |
|
||||||
}() |
|
||||||
} |
|
||||||
} |
|
@ -1,758 +1,119 @@ |
|||||||
package main |
package main |
||||||
|
|
||||||
import ( |
import ( |
||||||
|
"flag" |
||||||
"fmt" |
"fmt" |
||||||
|
"io/ioutil" |
||||||
"log" |
"log" |
||||||
"os" |
"os" |
||||||
"sort" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/awesome-gocui/gocui" |
|
||||||
"samhofi.us/x/keybase" |
|
||||||
"unicode/utf8" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
tea "github.com/charmbracelet/bubbletea" |
||||||
typeCommands = make(map[string]TypeCommand) |
"github.com/charmbracelet/bubbles/spinner" |
||||||
commands = make(map[string]Command) |
"github.com/charmbracelet/lipgloss" |
||||||
baseCommands = make([]string, 0) |
"github.com/mattn/go-isatty" |
||||||
|
"github.com/muesli/reflow/indent" |
||||||
|
|
||||||
dev = false |
"samhofi.us/x/keybase/v2" |
||||||
k = keybase.NewKeybase() |
"samhofi.us/x/keybase/v2/types/chat1" |
||||||
channel keybase.Channel |
|
||||||
channels []keybase.Channel |
|
||||||
stream = false |
|
||||||
lastMessage keybase.ChatAPI |
|
||||||
lastChat = "" |
|
||||||
g *gocui.Gui |
|
||||||
) |
) |
||||||
|
|
||||||
var config *Config |
var ( |
||||||
|
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render |
||||||
|
k = keybase.NewKeybase() |
||||||
|
mainModel *model |
||||||
|
) |
||||||
|
|
||||||
func main() { |
func main() { |
||||||
if !k.LoggedIn { |
var ( |
||||||
fmt.Println("You are not logged in.") |
daemonMode bool |
||||||
return |
showHelp bool |
||||||
} |
opts []tea.ProgramOption |
||||||
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...) |
|
||||||
|
|
||||||
} |
|
||||||
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() |
|
||||||
} |
|
||||||
|
|
||||||
// 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 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
|
flag.BoolVar(&daemonMode, "d", false, "run as a daemon") |
||||||
func updateChatWindow() { |
flag.BoolVar(&showHelp, "h", false, "show help") |
||||||
|
flag.Parse() |
||||||
|
|
||||||
runOpts := keybase.RunOptions{ |
if showHelp { |
||||||
Dev: dev, |
flag.Usage() |
||||||
|
os.Exit(0) |
||||||
} |
} |
||||||
k.Run(func(api keybase.ChatAPI) { |
|
||||||
handleMessage(api) |
|
||||||
}, |
|
||||||
runOpts) |
|
||||||
|
|
||||||
} |
if daemonMode || !isatty.IsTerminal(os.Stdout.Fd()) { |
||||||
func populateChat() { |
// If we're in daemon mode don't render the TUI
|
||||||
lastMessage.ID = 0 |
opts = []tea.ProgramOption{tea.WithoutRenderer()} |
||||||
chat := k.NewChat(channel) |
} else { |
||||||
maxX, _ := g.Size() |
// If we're in TUI mode, discard log output
|
||||||
api, err := chat.Read(maxX / 2) |
log.SetOutput(ioutil.Discard) |
||||||
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-- { |
m1 := newModel() |
||||||
actuallyPrintMe += printMe[i] |
mainModel = &m1 |
||||||
if i > 0 { |
chatHandler := handleChat |
||||||
actuallyPrintMe += "\n" |
handlers := keybase.Handlers{ |
||||||
} |
ChatHandler: &chatHandler, |
||||||
} |
} |
||||||
printToView("Chat", actuallyPrintMe) |
go k.Run(handlers, &keybase.RunOptions{}) |
||||||
go populateList() |
p := tea.NewProgram(mainModel, opts...) |
||||||
} |
if err := p.Start(); err != nil { |
||||||
func populateList() { |
fmt.Println("Error starting Bubble Tea program:", err) |
||||||
_, maxY := g.Size() |
os.Exit(1) |
||||||
if testVar, err := k.ChatList(); err != nil { |
|
||||||
log.Printf("%+v", err) |
|
||||||
} 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() |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
// End update/populate views automatically
|
func newModel() model { |
||||||
|
sp := spinner.New() |
||||||
// Formatting
|
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("206")) |
||||||
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 model{ |
||||||
return msg.colorRegex(`(@?\b`+k.Username+`\b)`, config.Colors.Message.Mention) |
spinner: sp, |
||||||
} |
|
||||||
func colorUsername(username string) StyledString { |
|
||||||
var color = config.Colors.Message.SenderDefault |
|
||||||
if username == k.Username { |
|
||||||
color = config.Colors.Message.Mention |
|
||||||
} |
} |
||||||
return color.stylize(username) |
|
||||||
} |
} |
||||||
|
|
||||||
func cleanChannelName(c string) string { |
func (m model) Init() tea.Cmd { |
||||||
newChannelName := strings.Replace(c, fmt.Sprintf("%s,", k.Username), "", 1) |
log.Println("Starting work...") |
||||||
return strings.Replace(newChannelName, fmt.Sprintf(",%s", k.Username), "", 1) |
return tea.Batch( |
||||||
|
spinner.Tick, |
||||||
|
) |
||||||
} |
} |
||||||
|
|
||||||
func formatMessage(api keybase.ChatAPI, formatString string) StyledString { |
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { |
||||||
msg := api.Msg |
switch msg := msg.(type) { |
||||||
ret := config.Colors.Message.Header.stylize("") |
case tea.KeyMsg: |
||||||
msgType := msg.Content.Type |
if msg.String() == "ctrl+c" { |
||||||
switch msgType { |
m.quitting= true |
||||||
case "text", "attachment": |
return m, tea.Quit |
||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
} else { |
} else { |
||||||
if api.Msg.Channel.MembersType == keybase.TEAM { |
return m, nil |
||||||
printToView("Chat", formatOutput(api).string()) |
|
||||||
} else { |
|
||||||
printToView("Chat", formatMessage(api, config.Formatting.PMFormat).string()) |
|
||||||
} |
|
||||||
} |
|
||||||
} else { |
|
||||||
//TODO: For edit/delete run this
|
|
||||||
if api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name { |
|
||||||
go populateChat() |
|
||||||
} |
} |
||||||
} |
case spinner.TickMsg: |
||||||
} |
var cmd tea.Cmd |
||||||
func getInputString(viewName string) (string, error) { |
m.spinner, cmd = m.spinner.Update(msg) |
||||||
inputView, err := g.View(viewName) |
return m, cmd |
||||||
if err != nil { |
case chat1.MsgSummary: |
||||||
return "", err |
log.Println("chat1.MsgSummary passed to m.Update()") |
||||||
} |
var cmd tea.Cmd |
||||||
retString := inputView.Buffer() |
return m, cmd |
||||||
retString = strings.Replace(retString, "\n", "", 800) |
default: |
||||||
return retString, err |
return m, nil |
||||||
} |
|
||||||
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)) |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
// End input handling
|
func (m model) View() string { |
||||||
|
s := "\n" |
||||||
|
|
||||||
func quit(g *gocui.Gui, v *gocui.View) error { |
for _, res := range mainModel.chat { |
||||||
return gocui.ErrQuit |
log.Println(res) |
||||||
} |
if res.Content.TypeName == "text" { |
||||||
|
s += fmt.Sprintf("%+v: %+v\n", res.Sender.Username, res.Content.Text.Body) |
||||||
// 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 |
|
||||||
} |
} |
||||||
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
|
s += helpStyle("\nCtrl+C to exit\n") |
||||||
func RegisterCommand(c Command) error { |
|
||||||
var notAdded string |
if m.quitting { |
||||||
for i, cmd := range c.Cmd { |
s += "\n" |
||||||
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 |
|
||||||
} |
|
||||||
|
|
||||||
// RunCommand calls a command as if it was run by the user
|
return indent.String(s, 1) |
||||||
func RunCommand(c ...string) { |
|
||||||
commands[c[0]].Exec(c) |
|
||||||
} |
} |
@ -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() |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -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) |
|
||||||
} |
|
Loading…
Reference in new issue