diff --git a/client.go b/client.go new file mode 100644 index 0000000..e087fdd --- /dev/null +++ b/client.go @@ -0,0 +1,167 @@ +package saltyim + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + "time" + + _ "image/jpeg" + _ "image/png" + + "github.com/keys-pub/keys" + "github.com/rivo/tview" + "go.mills.io/salty" + "go.yarn.social/lextwt" +) + +const ( + dateTimeFormat = "2006-01-02 15:04:05" +) + +type chatClient struct { + mu sync.RWMutex + + key *keys.EdX25519Key + uri string + me Addr + inbox string + user string + config Config + + // Configurations. + palette map[string]string +} + +// NewChatClient initializes a new chatClient. +// Sets up connection with broker, and initializes UI. +func NewChatClient(key *keys.EdX25519Key, uri string, me Addr, inbox, user string) (*chatClient, error) { + config, err := Lookup(user) + if err != nil { + return nil, fmt.Errorf("error: failed to lookup user %q: %w", user, err) + } + + return &chatClient{ + key: key, + uri: uri, + me: me, + inbox: inbox, + user: user, + config: config, + + palette: map[string]string{ + "background": "C5C3C6", + "border": "4C5C68", + "title": "46494C", + "date": "E76F51", + "name": "1985A1", + "text": "577399", + }, + }, nil +} + +// newMessageHandler returns an InputHandler that handles outgoing messages. +func (cc *chatClient) newMessageHandler(outCh chan<- string) InputHandler { + handler := func(message string) { + cc.mu.RLock() + outCh <- message + cc.mu.RUnlock() + } + + return handler +} + +// updateChatBox updates chatBox component with incoming messages. +func (cc *chatClient) updateChatBox(inCh <-chan string, app *tview.Application, + chatBox *tview.TextView) { + var prevSender string + var prevTime time.Time + + for in := range inCh { + s, err := lextwt.ParseSalty(in) + if err != nil { + continue + } + + switch s := s.(type) { + case *lextwt.SaltyEvent: + + case *lextwt.SaltyText: + if s.User.String() != cc.user { + continue + } + + cc.mu.RLock() + app.QueueUpdateDraw(func() { + if s.User.String() != prevSender || s.Timestamp.DateTime().After(prevTime.Add(5*time.Minute)) { + fmt.Fprintf(chatBox, "[#%s]%s - [#%s]%s\n", + cc.palette["name"], + s.User.String(), + cc.palette["date"], + s.Timestamp.DateTime().Local().Format(dateTimeFormat), + ) + } + fmt.Fprintf(chatBox, "[#%s]%s", cc.palette["text"], s.LiteralText()) + }) + cc.mu.RUnlock() + + prevSender = s.User.String() + prevTime = s.Timestamp.DateTime() + } + } +} + +// setScreen initializes the layout and UI components. +func (cc *chatClient) SetScreen(inCh <-chan string, outCh chan<- string) { + app := tview.NewApplication() + + // Generate UI components. + cc.mu.RLock() + chatBox := NewChatBox(cc.palette, cc.user) + inputField := NewChatInput(cc.palette, cc.newMessageHandler(outCh)) + cc.mu.RUnlock() + + // Layout the widgets in flex view. + flex := tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(chatBox, 0, 1, false). + AddItem(inputField, 3, 0, false) + + go cc.updateChatBox(inCh, app, chatBox) + + err := app.SetRoot(flex, true).SetFocus(inputField).EnableMouse(true).Run() + if err != nil { + log.Fatal(err) + } +} + +// Open bi-directional stream between client and server. +func (cc *chatClient) RunChat(inCh chan<- string, outCh <-chan string) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Receives incoming messages on a separate goroutine to be non-blocking. + go func() { + for msg := range Read(ctx, cc.key, cc.uri, cc.inbox, "", "") { + inCh <- msg + } + }() + + // Forward/send outgoing messages. + for msg := range outCh { + if strings.TrimSpace(msg) == "" { + continue + } + + b, err := salty.Encrypt(cc.key, PackMessage(cc.me, msg), []string{cc.config.Key}) + if err != nil { + log.Fatalf("error encrypting message: %v", err) + } + + err = Send(cc.config.Endpoint, string(b)) + if err != nil { + log.Fatalf("error sending message: %v", err) + } + } +} diff --git a/cmd/salty-chat/chat.go b/cmd/salty-chat/chat.go index 88ce6fa..532da82 100644 --- a/cmd/salty-chat/chat.go +++ b/cmd/salty-chat/chat.go @@ -2,34 +2,14 @@ package main import ( "fmt" - "log" "os" - "os/signal" - "strings" - "sync" - "syscall" - tm "github.com/buger/goterm" - "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/spf13/viper" - "go.yarn.social/lextwt" - "go.mills.io/salty" "go.mills.io/saltyim" ) -func prompt() (string, error) { - prompt := promptui.Prompt{Label: "> "} - - result, err := prompt.Run() - if err != nil { - return "", fmt.Errorf("error getting prompt response %w", err) - } - - return result, nil -} - var chatCmd = &cobra.Command{ Use: "chat ", Short: "Creates a chat with a specific user", @@ -60,79 +40,17 @@ func chat(identity, uri, inbox, user string) { os.Exit(2) } - config, err := saltyim.Lookup(user) + // Initialize necessary channels. + inCh := make(chan string) + outCh := make(chan string) + + // Initialize client. + cc, err := saltyim.NewChatClient(key, uri, me, inbox, user) if err != nil { - fmt.Fprintf(os.Stderr, "error: failed to lookup user %q: %s", user, err) + fmt.Fprintf(os.Stderr, "error creating chat: %s", err) os.Exit(2) } - tm.Clear() - tm.Printf("Chatting as %s", me) - tm.Printf(" via %s/%s\n", uri, inbox) - tm.Printf(" with %s", user) - tm.Printf(" via %s\n", config.Endpoint) - tm.Flush() - - stop := make(chan struct{}) - - var wg sync.WaitGroup - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - - go func() { - defer wg.Done() - <-sigs - stop <- struct{}{} - }() - wg.Add(1) - - go func() { - defer wg.Done() - for msg := range saltyim.Read(key, uri, inbox, "", "", stop) { - s, err := lextwt.ParseSalty(msg) - if err != nil { - continue - } - switch s := s.(type) { - case *lextwt.SaltyEvent: - - case *lextwt.SaltyText: - if s.User.String() != user { - continue - } - - tm.Println(formatMsg(msg)) - tm.Flush() - } - } - }() - wg.Add(1) - - go func() { - defer wg.Done() - - for { - msg, err := prompt() - if err != nil { - break - } - - if strings.TrimSpace(msg) == "" { - continue - } - - b, err := salty.Encrypt(key, packMsg(me, msg), []string{config.Key}) - if err != nil { - log.Fatalf("error encrypting message: %v", err) - } - - err = saltyim.Send(config.Endpoint, string(b)) - if err != nil { - log.Fatalf("error sending message: %v", err) - } - } - }() - - wg.Wait() + go cc.RunChat(inCh, outCh) + cc.SetScreen(inCh, outCh) } diff --git a/cmd/salty-chat/read.go b/cmd/salty-chat/read.go index 945ccdd..2d016d2 100644 --- a/cmd/salty-chat/read.go +++ b/cmd/salty-chat/read.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "go.yarn.social/lextwt" + "golang.org/x/net/context" "go.mills.io/saltyim" ) @@ -105,17 +106,18 @@ func read(identity, uri, prehook, posthook string, args ...string) { inbox = os.Getenv("USER") } - stop := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigs - stop <- struct{}{} + cancel() }() - for msg := range saltyim.Read(key, uri, inbox, prehook, posthook, stop) { + for msg := range saltyim.Read(ctx, key, uri, inbox, prehook, posthook) { fmt.Println(formatMsg(msg)) } } diff --git a/cmd/salty-chat/send.go b/cmd/salty-chat/send.go index d9a0709..949bf81 100644 --- a/cmd/salty-chat/send.go +++ b/cmd/salty-chat/send.go @@ -7,7 +7,6 @@ import ( "log" "os" "strings" - "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -47,10 +46,6 @@ func init() { rootCmd.AddCommand(sendCmd) } -func packMsg(me saltyim.Addr, msg string) []byte { - return []byte(fmt.Sprint(time.Now().UTC().Format(time.RFC3339), "\t", me.Formatted(), "\t", msg)) -} - func send(identity, user string, args ...string) { user = strings.TrimSpace(user) if user == "" { @@ -87,7 +82,7 @@ func send(identity, user string, args ...string) { msg = strings.Join(args, " ") } - b, err := salty.Encrypt(key, packMsg(me, msg), []string{config.Key}) + b, err := salty.Encrypt(key, saltyim.PackMessage(me, msg), []string{config.Key}) if err != nil { log.Fatalf("error encrypting message: %v", err) } diff --git a/go.mod b/go.mod index e622ffd..168cbb6 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( require ( git.mills.io/prologic/msgbus v0.1.3 github.com/Masterminds/sprig/v3 v3.2.2 - github.com/buger/goterm v1.0.4 + github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 github.com/google/go-cmp v0.5.7 // indirect github.com/google/uuid v1.3.0 // indirect @@ -25,15 +25,15 @@ require ( github.com/keys-pub/keys v0.1.22 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/magiconair/properties v1.8.6 // indirect - github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/copystructure v1.2.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect + github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/afero v1.8.2 // indirect go.yarn.social/lextwt v0.0.0-20220318224940-ae4228bbcb31 go.yarn.social/types v0.0.0-20220304222359-9694f95ad749 // indirect golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect - golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect + golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect gopkg.in/ini.v1 v1.66.4 // indirect ) diff --git a/go.sum b/go.sum index e688c5d..86d546b 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= -github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -90,11 +88,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= @@ -142,6 +137,10 @@ github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwU github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= +github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 h1:Xh9mvwEmhbdXlRSsgn+N0zj/NqnKvpeqL08oKDHln2s= github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -333,13 +332,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +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/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -351,6 +350,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +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/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= @@ -424,6 +425,10 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc= +github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -650,7 +655,6 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -699,11 +703,11 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -727,6 +731,8 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/readmsgs.go b/readmsgs.go index 66c84bc..cd972bc 100644 --- a/readmsgs.go +++ b/readmsgs.go @@ -2,6 +2,7 @@ package saltyim import ( "bytes" + "context" "fmt" "os" "time" @@ -55,7 +56,7 @@ func handleMessage(key *keys.EdX25519Key, prehook, posthook string, msgs chan st } // Read ... -func Read(key *keys.EdX25519Key, uri, inbox, prehook, posthook string, stop chan struct{}) chan string { +func Read(ctx context.Context, key *keys.EdX25519Key, uri, inbox, prehook, posthook string) chan string { client := client.NewClient(uri, nil) msgs := make(chan string) @@ -64,7 +65,7 @@ func Read(key *keys.EdX25519Key, uri, inbox, prehook, posthook string, stop chan s.Start() go func() { - <-stop + <-ctx.Done() s.Stop() close(msgs) }() diff --git a/sendmsg.go b/sendmsg.go index f369800..8819722 100644 --- a/sendmsg.go +++ b/sendmsg.go @@ -2,11 +2,19 @@ package saltyim import ( "bytes" + "fmt" "net/http" + "time" log "github.com/sirupsen/logrus" ) +// Packmessage formts an outoing message in the Message Format +// \t() +func PackMessage(me Addr, msg string) []byte { + return []byte(fmt.Sprint(time.Now().UTC().Format(time.RFC3339), "\t", me.Formatted(), "\t", msg)) +} + // Send sends the encrypted message `msg` to the Endpoint `endpoint` using a // `POST` request and returns nil on success or an error on failure. func Send(endpoint, msg string) error { diff --git a/tview.go b/tview.go new file mode 100644 index 0000000..4f4c8f5 --- /dev/null +++ b/tview.go @@ -0,0 +1,63 @@ +package saltyim + +import ( + "regexp" + "strconv" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// InputHandler handles input from tview.InputField. +type InputHandler func(message string) + +// hexToTCell converts a hex string to the corresponding TCell.Color. +func hexToTCell(hexStr string) tcell.Color { + result, _ := strconv.ParseInt(hexStr, 16, 64) + return tcell.NewHexColor(int32(result)) +} + +// NewChatBox initializes and returns a 'chatBox' to display incoming +// chat messages. +func NewChatBox(palette map[string]string, title string) *tview.TextView { + chatBox := tview.NewTextView(). + SetDynamicColors(true). + SetRegions(true). + SetWordWrap(true). + SetTextAlign(tview.AlignLeft) + + chatBox.SetBorder(true). + SetTitle(title), + SetTitleColor(hexToTCell(palette["title"])). + SetTitleAlign(tview.AlignLeft). + SetBorderColor(hexToTCell(palette["border"])). + SetBackgroundColor(hexToTCell(palette["background"])) + + return chatBox +} + +// NewChatInput initializes and returns a 'chatInput' component +// that handles user inputs and forwards chat messages. +func NewChatInput(palette map[string]string, handler InputHandler) *tview.InputField { + nLine := regexp.MustCompile(`\\n\s?`) + + // Initialize 'inputField' for user inputs. + chatInput := tview.NewInputField() + chatInput.SetFieldBackgroundColor(tcell.ColorBlack). + SetFieldWidth(0). + SetFieldTextColor(hexToTCell(palette["text"])). + SetFieldBackgroundColor(hexToTCell(palette["background"])). + SetDoneFunc(func(key tcell.Key) { + // Send and clear inputField. + if key == tcell.KeyEnter { + message := nLine.ReplaceAllString(chatInput.GetText(), "\n") + "\n" + handler(message) + chatInput.SetText("") + } + }). + SetBorder(true). + SetBorderColor(hexToTCell(palette["border"])). + SetBackgroundColor(hexToTCell(palette["background"])) + + return chatInput +}