mirror of
https://github.com/logos-messaging/logos-chat.git
synced 2026-05-02 02:33:49 +00:00
Remove Tui Examples - Replace with logos-core
This commit is contained in:
parent
d83c09c82b
commit
13832d4e5e
@ -1,7 +0,0 @@
|
|||||||
import chronos
|
|
||||||
|
|
||||||
import tui/tui
|
|
||||||
|
|
||||||
|
|
||||||
when isMainModule:
|
|
||||||
waitFor main()
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import illwill
|
|
||||||
|
|
||||||
|
|
||||||
type
|
|
||||||
Pane* = object
|
|
||||||
xStart*: int
|
|
||||||
yStart*: int
|
|
||||||
width*: int
|
|
||||||
height*: int
|
|
||||||
|
|
||||||
proc getPanes*(): seq[Pane] =
|
|
||||||
|
|
||||||
let statusBarHeight = 6
|
|
||||||
let convoWidth = 40
|
|
||||||
let inboxHeight = 5
|
|
||||||
let footerHeight = 14
|
|
||||||
let modalXOffset = 10
|
|
||||||
let modalHeight = 10
|
|
||||||
|
|
||||||
result = @[]
|
|
||||||
let statusBar = Pane(
|
|
||||||
xStart: 0,
|
|
||||||
yStart: 0,
|
|
||||||
width: terminalWidth(),
|
|
||||||
height: statusBarHeight
|
|
||||||
)
|
|
||||||
result.add(statusBar)
|
|
||||||
|
|
||||||
let convoPane = Pane(
|
|
||||||
xStart: 0,
|
|
||||||
yStart: statusBar.yStart + statusBar.height,
|
|
||||||
width: convoWidth,
|
|
||||||
height: terminalHeight() - statusBar.height - footerHeight - 1
|
|
||||||
)
|
|
||||||
result.add(convoPane)
|
|
||||||
|
|
||||||
|
|
||||||
let msgPane = Pane(
|
|
||||||
xStart: convoPane.width,
|
|
||||||
yStart: statusBar.yStart + statusBar.height,
|
|
||||||
width: terminalWidth() - convoPane.width,
|
|
||||||
height: convoPane.height - inboxHeight
|
|
||||||
)
|
|
||||||
result.add(msgPane)
|
|
||||||
|
|
||||||
let msgInputPane = Pane(
|
|
||||||
xStart: convoPane.width,
|
|
||||||
yStart: msgPane.yStart + msgPane.height,
|
|
||||||
width: msgPane.width,
|
|
||||||
height: inboxHeight
|
|
||||||
)
|
|
||||||
result.add(msgInputPane)
|
|
||||||
|
|
||||||
let footerPane = Pane(
|
|
||||||
xStart: 0,
|
|
||||||
yStart: convoPane.yStart + convoPane.height,
|
|
||||||
width: int(terminalWidth()),
|
|
||||||
height: footerHeight
|
|
||||||
)
|
|
||||||
result.add(footerPane)
|
|
||||||
|
|
||||||
|
|
||||||
let modalPane = Pane(
|
|
||||||
xStart: modalXOffset,
|
|
||||||
yStart: int(terminalHeight()/2 - modalHeight/2),
|
|
||||||
width: int(terminalWidth() - 2*modalXOffset),
|
|
||||||
height: int(modalHeight)
|
|
||||||
)
|
|
||||||
result.add(modalPane)
|
|
||||||
|
|
||||||
|
|
||||||
proc offsetPane(pane: Pane): Pane =
|
|
||||||
result = Pane(
|
|
||||||
xStart: pane.xStart ,
|
|
||||||
yStart: pane.yStart + 1,
|
|
||||||
width: pane.width ,
|
|
||||||
height: pane.height - 2
|
|
||||||
)
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import chronicles
|
|
||||||
import chronos
|
|
||||||
import libp2p/crypto/crypto
|
|
||||||
import sequtils
|
|
||||||
import std/json
|
|
||||||
import std/jsonutils except distinctBase
|
|
||||||
import std/marshal
|
|
||||||
import std/options
|
|
||||||
import std/os
|
|
||||||
import std/streams
|
|
||||||
import strformat
|
|
||||||
import strutils
|
|
||||||
import tables
|
|
||||||
|
|
||||||
import chat/crypto/ecdh
|
|
||||||
import chat/delivery/waku_client
|
|
||||||
import chat/identity
|
|
||||||
|
|
||||||
|
|
||||||
const REGISTRATION_DIR = ".registry"
|
|
||||||
const KEY_DIR = ".savedkeys"
|
|
||||||
|
|
||||||
|
|
||||||
type Config* = object
|
|
||||||
ident*: Identity
|
|
||||||
waku*: WakuConfig
|
|
||||||
|
|
||||||
type SavedConfig* = object
|
|
||||||
name*: string
|
|
||||||
idkey*: seq[byte]
|
|
||||||
nodekey*: seq[byte]
|
|
||||||
port*: uint16
|
|
||||||
clusterId*: uint16
|
|
||||||
shardId*: seq[uint16]
|
|
||||||
pubsubTopic*: string
|
|
||||||
staticPeers*: seq[string]
|
|
||||||
|
|
||||||
|
|
||||||
proc toWakuConfig(s: SavedConfig): WakuConfig =
|
|
||||||
result = WakuConfig(
|
|
||||||
nodekey: crypto.PrivateKey.init(s.nodekey).get(),
|
|
||||||
port: s.port,
|
|
||||||
clusterId: s.clusterId,
|
|
||||||
shardId: s.shardId,
|
|
||||||
pubsubTopic: s.pubsubTopic,
|
|
||||||
staticPeers: s.staticPeers
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
proc toIdent(s: SavedConfig): Identity =
|
|
||||||
result = Identity(
|
|
||||||
name: s.name,
|
|
||||||
privateKey: loadPrivateKeyFromBytes(s.idkey).get()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
proc register(name: string, multiAddr: string) {.async.} =
|
|
||||||
|
|
||||||
notice "Registering Account", name=name, maddr=multiAddr
|
|
||||||
|
|
||||||
if not dirExists(REGISTRATION_DIR):
|
|
||||||
createDir(REGISTRATION_DIR)
|
|
||||||
|
|
||||||
try:
|
|
||||||
writeFile(joinPath(REGISTRATION_DIR, fmt"{name.toLower()}.maddr"), multiAddr)
|
|
||||||
except IOError as e:
|
|
||||||
echo "Failed to write registration file: ", e.msg
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
proc fetchRegistrations*(): Table[string, string] =
|
|
||||||
|
|
||||||
let allFiles = toSeq(walkFiles(fmt"{REGISTRATION_DIR}/*"))
|
|
||||||
result = allFiles.mapIt((splitFile(it)[1], readFile(it).strip())).toTable()
|
|
||||||
|
|
||||||
|
|
||||||
proc loadCfg(file: string): Option[Config] =
|
|
||||||
let data = parseFile(file)
|
|
||||||
|
|
||||||
let cfg = Config(
|
|
||||||
ident: toIdent(data.to(SavedConfig)),
|
|
||||||
waku: toWakuConfig(data.to(SavedConfig))
|
|
||||||
)
|
|
||||||
|
|
||||||
result = some(cfg)
|
|
||||||
|
|
||||||
|
|
||||||
proc fetchCfg(name: string): Option[Config ] =
|
|
||||||
let allFiles = toSeq(walkFiles(fmt"{KEY_DIR}/*"))
|
|
||||||
|
|
||||||
for file in allFiles:
|
|
||||||
if name == splitFile(file)[1]:
|
|
||||||
return loadCfg(file)
|
|
||||||
return none(Config)
|
|
||||||
|
|
||||||
|
|
||||||
proc saveCfg(name:string, cfg: Config) =
|
|
||||||
|
|
||||||
let s = SavedConfig(
|
|
||||||
name: name,
|
|
||||||
idkey: cfg.ident.privatekey.bytes().toSeq(),
|
|
||||||
nodekey: cfg.waku.nodekey.getBytes().get(),
|
|
||||||
port: cfg.waku.port,
|
|
||||||
clusterId: cfg.waku.clusterId,
|
|
||||||
shardId: cfg.waku.shardId,
|
|
||||||
pubsubTopic: cfg.waku.pubsubTopic,
|
|
||||||
staticPeers: cfg.waku.staticPeers
|
|
||||||
)
|
|
||||||
|
|
||||||
let json = jsonutils.toJson(s)
|
|
||||||
|
|
||||||
|
|
||||||
if not dirExists(KEY_DIR):
|
|
||||||
createDir(KEY_DIR)
|
|
||||||
|
|
||||||
try:
|
|
||||||
writeFile(joinPath(KEY_DIR, fmt"{name.toLower()}.cfg"), $json)
|
|
||||||
except IOError as e:
|
|
||||||
echo "Failed to write cfg file: ", e.msg
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
proc getCfg*(name: string): Future[Config] {.async.} =
|
|
||||||
let cfgOpt = fetchCfg(name)
|
|
||||||
if cfgOpt.isSome:
|
|
||||||
result = cfgOpt.get()
|
|
||||||
else:
|
|
||||||
let newCfg = Config(
|
|
||||||
ident: createIdentity(name),
|
|
||||||
waku: DefaultConfig()
|
|
||||||
)
|
|
||||||
saveCfg(name, newCfg)
|
|
||||||
await register(name, newCfg.waku.getMultiAddr())
|
|
||||||
|
|
||||||
result = newCfg
|
|
||||||
@ -1,584 +0,0 @@
|
|||||||
|
|
||||||
import algorithm
|
|
||||||
import chronicles
|
|
||||||
import chronos
|
|
||||||
import illwill
|
|
||||||
import libp2p/crypto/crypto
|
|
||||||
import times
|
|
||||||
import strformat
|
|
||||||
import strutils
|
|
||||||
import sugar
|
|
||||||
import tables
|
|
||||||
|
|
||||||
import chat
|
|
||||||
import content_types/all
|
|
||||||
|
|
||||||
import layout
|
|
||||||
import persistence
|
|
||||||
import utils
|
|
||||||
|
|
||||||
const charVert = "│"
|
|
||||||
const charHoriz = "─"
|
|
||||||
const charTopLeft = "┌"
|
|
||||||
const charTopRight = "┐"
|
|
||||||
const charBottomLeft = "└"
|
|
||||||
const charBottomRight = "┘"
|
|
||||||
|
|
||||||
type
|
|
||||||
|
|
||||||
LogEntry = object
|
|
||||||
level: string
|
|
||||||
ts: DateTime
|
|
||||||
msg: string
|
|
||||||
|
|
||||||
Message = object
|
|
||||||
sender: string
|
|
||||||
content: string
|
|
||||||
timestamp: DateTime
|
|
||||||
id: string
|
|
||||||
isAcknowledged: bool
|
|
||||||
|
|
||||||
ConvoInfo = object
|
|
||||||
name: string
|
|
||||||
convo: Conversation
|
|
||||||
messages: seq[Message]
|
|
||||||
lastMsgTime*: DateTime
|
|
||||||
isTooLong*: bool
|
|
||||||
|
|
||||||
ChatApp = ref object
|
|
||||||
client: Client
|
|
||||||
tb: TerminalBuffer
|
|
||||||
conversations: Table[string, ConvoInfo]
|
|
||||||
selectedConv: string
|
|
||||||
inputBuffer: string
|
|
||||||
inputInviteBuffer: string
|
|
||||||
scrollOffset: int
|
|
||||||
messageScrollOffset: int
|
|
||||||
inviteModal: bool
|
|
||||||
|
|
||||||
currentInviteLink: string
|
|
||||||
isInviteReady: bool
|
|
||||||
peerCount: int
|
|
||||||
|
|
||||||
logMsgs: seq[LogEntry]
|
|
||||||
|
|
||||||
proc `==`(a,b: ConvoInfo):bool =
|
|
||||||
if a.name==b.name:
|
|
||||||
true
|
|
||||||
else:
|
|
||||||
false
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# Data Management
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
proc addMessage(conv: var ConvoInfo, messageId: MessageId, sender: string, content: string) =
|
|
||||||
|
|
||||||
let now = now()
|
|
||||||
conv.messages.add(Message(
|
|
||||||
sender: sender,
|
|
||||||
id: messageId,
|
|
||||||
content: content,
|
|
||||||
timestamp: now,
|
|
||||||
isAcknowledged: false
|
|
||||||
))
|
|
||||||
conv.lastMsgTime = now
|
|
||||||
|
|
||||||
|
|
||||||
proc mostRecentConvos(app: ChatApp): seq[ConvoInfo] =
|
|
||||||
var convos = collect(for v in app.conversations.values: v)
|
|
||||||
convos.sort(proc(a, b: ConvoInfo): int = -cmp(a.lastMsgTime, b.lastMsgTime))
|
|
||||||
|
|
||||||
return convos
|
|
||||||
|
|
||||||
proc getSelectedConvo(app: ChatApp): ptr ConvoInfo =
|
|
||||||
if app.conversations.hasKey(app.selectedConv):
|
|
||||||
return addr app.conversations[app.selectedConv]
|
|
||||||
|
|
||||||
return addr app.conversations[app.mostRecentConvos()[0].name]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# ChatSDK Setup
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
proc createChatClient(name: string): Future[Client] {.async.} =
|
|
||||||
var cfg = await getCfg(name)
|
|
||||||
result = newClient(cfg.waku, cfg.ident)
|
|
||||||
|
|
||||||
|
|
||||||
proc createInviteLink(app: var ChatApp): string =
|
|
||||||
app.client.createIntroBundle().toLink()
|
|
||||||
|
|
||||||
|
|
||||||
proc createConvo(app: ChatApp) {.async.} =
|
|
||||||
discard await app.client.newPrivateConversation(toBundle(app.inputInviteBuffer.strip()).get())
|
|
||||||
|
|
||||||
proc sendMessage(app: ChatApp, convoInfo: ptr ConvoInfo, msg: string) {.async.} =
|
|
||||||
|
|
||||||
var msgId = ""
|
|
||||||
if convoInfo.convo != nil:
|
|
||||||
msgId = await convoInfo.convo.sendMessage(initTextFrame(msg).toContentFrame())
|
|
||||||
|
|
||||||
convoInfo[].addMessage(msgId, "You", app.inputBuffer)
|
|
||||||
|
|
||||||
|
|
||||||
proc setupChatSdk(app: ChatApp) =
|
|
||||||
|
|
||||||
let client = app.client
|
|
||||||
|
|
||||||
app.client.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async.} =
|
|
||||||
info "New Message: ", convoId = convo.id(), msg= msg
|
|
||||||
app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: "NewMsg"))
|
|
||||||
|
|
||||||
var contentStr = case msg.content.contentType
|
|
||||||
of text:
|
|
||||||
decode(msg.content.bytes, TextFrame).get().text
|
|
||||||
of unknown:
|
|
||||||
"<Unhandled Message Type>"
|
|
||||||
|
|
||||||
app.conversations[convo.id()].messages.add(Message(sender: msg.sender.toHex(), content: contentStr, timestamp: now()))
|
|
||||||
)
|
|
||||||
|
|
||||||
app.client.onNewConversation(proc(convo: Conversation) {.async.} =
|
|
||||||
app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt"Adding Convo: {convo.id()}"))
|
|
||||||
info "New Conversation: ", convoId = convo.id()
|
|
||||||
|
|
||||||
app.conversations[convo.id()] = ConvoInfo(name: convo.id(), convo: convo, messages: @[], lastMsgTime: now(), isTooLong: false)
|
|
||||||
)
|
|
||||||
|
|
||||||
app.client.onDeliveryAck(proc(convo: Conversation, msgId: string) {.async.} =
|
|
||||||
info "DeliveryAck", msgId=msgId
|
|
||||||
app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt"Ack:{msgId}"))
|
|
||||||
|
|
||||||
var s = ""
|
|
||||||
var msgs = addr app.conversations[convo.id()].messages
|
|
||||||
for i in countdown(msgs[].high, 0):
|
|
||||||
s = fmt"{s},{msgs[i].id}"
|
|
||||||
var m = addr msgs[i]
|
|
||||||
|
|
||||||
if m.id == msgId:
|
|
||||||
m.isAcknowledged = true
|
|
||||||
break # Stop after
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# Draw Funcs
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
proc resetTuiCursor(tb: var TerminalBuffer) =
|
|
||||||
tb.setForegroundColor(fgWhite)
|
|
||||||
tb.setBackgroundColor(bgBlack)
|
|
||||||
|
|
||||||
|
|
||||||
proc drawOutline(tb: var TerminalBuffer, layout: Pane, color: ForegroundColor,
|
|
||||||
bg: BackgroundColor = bgBlack) =
|
|
||||||
|
|
||||||
for x in layout.xStart+1..<layout.xStart+layout.width:
|
|
||||||
tb.write(x, layout.yStart, charHoriz, color, bg)
|
|
||||||
tb.write(x, layout.yStart+layout.height-1, charHoriz, color, bg)
|
|
||||||
|
|
||||||
for y in layout.yStart+1..<layout.yStart+layout.height:
|
|
||||||
tb.write(layout.xStart, y, charVert, color, bg)
|
|
||||||
tb.write(layout.xStart+layout.width-1, y, charVert, color, bg)
|
|
||||||
|
|
||||||
tb.write(layout.xStart, layout.yStart, charTopLeft, color, bg)
|
|
||||||
tb.write(layout.xStart+layout.width-1, layout.yStart, charTopRight, color, bg)
|
|
||||||
tb.write(layout.xStart, layout.yStart+layout.height-1, charBottomLeft, color, bg)
|
|
||||||
tb.write(layout.xStart+layout.width-1, layout.yStart+layout.height-1,
|
|
||||||
charBottomRight, color, bgBlack)
|
|
||||||
|
|
||||||
|
|
||||||
proc drawStatusBar(app: ChatApp, layout: Pane , fg: ForegroundColor, bg: BackgroundColor) =
|
|
||||||
var tb = app.tb
|
|
||||||
tb.setForegroundColor(fg)
|
|
||||||
tb.setBackgroundColor(bg)
|
|
||||||
|
|
||||||
for x in layout.xStart..<layout.width:
|
|
||||||
for y in layout.yStart..<layout.height:
|
|
||||||
tb.write(x, y, " ")
|
|
||||||
|
|
||||||
var i = layout.yStart + 1
|
|
||||||
var chunk = layout.width - 9
|
|
||||||
tb.write(1, i, "Name: " & app.client.getName())
|
|
||||||
inc i
|
|
||||||
tb.write(1, i, fmt"PeerCount: {app.peerCount}")
|
|
||||||
inc i
|
|
||||||
tb.write(1, i, "Link: ")
|
|
||||||
for a in 0..(app.currentInviteLink.len div chunk) :
|
|
||||||
tb.write(1+6 , i, app.currentInviteLink[a*chunk ..< min((a+1)*chunk, app.currentInviteLink.len)])
|
|
||||||
inc i
|
|
||||||
|
|
||||||
resetTuiCursor(tb)
|
|
||||||
|
|
||||||
|
|
||||||
proc drawConvoItem(app: ChatApp,
|
|
||||||
layout: Pane, convo: ConvoInfo, isSelected: bool = false) =
|
|
||||||
|
|
||||||
var tb = app.tb
|
|
||||||
let xOffset = 3
|
|
||||||
let yOffset = 1
|
|
||||||
|
|
||||||
let dt = convo.lastMsgTime
|
|
||||||
|
|
||||||
let c = if isSelected: fgMagenta else: fgCyan
|
|
||||||
if isSelected:
|
|
||||||
tb.setForegroundColor(fgMagenta)
|
|
||||||
else:
|
|
||||||
tb.setForegroundColor(fgCyan)
|
|
||||||
|
|
||||||
drawOutline(tb, layout, c)
|
|
||||||
tb.write(layout.xStart+xOffset, layout.yStart+yOffset, convo.name)
|
|
||||||
tb.write(layout.xStart+xOffset, layout.yStart+yOffset+1,dt.format("yyyy-MM-dd HH:mm:ss"))
|
|
||||||
|
|
||||||
|
|
||||||
proc drawConversationPane( app: ChatApp, layout: Pane) =
|
|
||||||
var a = layout
|
|
||||||
a.width -= 2
|
|
||||||
a.height = 6
|
|
||||||
for convo in app.mostRecentConvos():
|
|
||||||
drawConvoItem(app, a, convo, app.selectedConv == convo.name)
|
|
||||||
a.yStart = a.yStart + a.height
|
|
||||||
|
|
||||||
|
|
||||||
proc splitAt(s: string, index: int): (string, string) =
|
|
||||||
let splitIndex = min(index, s.len)
|
|
||||||
return (s[0..<splitIndex], s[splitIndex..^1])
|
|
||||||
|
|
||||||
# Eww
|
|
||||||
proc drawMsgPane( app: ChatApp, layout: Pane) =
|
|
||||||
var tb = app.tb
|
|
||||||
drawOutline(tb, layout, fgYellow)
|
|
||||||
|
|
||||||
var convo = app.getSelectedConvo()
|
|
||||||
|
|
||||||
let xStart = layout.xStart+1
|
|
||||||
let yStart = layout.yStart+1
|
|
||||||
|
|
||||||
|
|
||||||
let w = layout.width
|
|
||||||
let h = layout.height
|
|
||||||
|
|
||||||
let maxContentWidth = w - 10
|
|
||||||
|
|
||||||
let xEnd = layout.xStart + maxContentWidth
|
|
||||||
let yEnd = layout.yStart + layout.height - 2
|
|
||||||
|
|
||||||
|
|
||||||
tb.setForegroundColor(fgGreen)
|
|
||||||
|
|
||||||
let x = xStart + 2
|
|
||||||
|
|
||||||
if not convo.isTooLong:
|
|
||||||
var y = yStart
|
|
||||||
for i in 0..convo.messages.len-1:
|
|
||||||
|
|
||||||
let m = convo.messages[i]
|
|
||||||
let timeStr = m.timestamp.format("HH:mm:ss")
|
|
||||||
var deliveryIcon = " "
|
|
||||||
var remainingText = m.content
|
|
||||||
|
|
||||||
if m.sender == "You":
|
|
||||||
tb.setForegroundColor(fgYellow)
|
|
||||||
deliveryIcon = if m.isAcknowledged: "✔" else: "◯"
|
|
||||||
else:
|
|
||||||
tb.setForegroundColor(fgGreen)
|
|
||||||
|
|
||||||
if y > yEnd:
|
|
||||||
convo.isTooLong = true
|
|
||||||
app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt" TOO LONG: {convo.name}"))
|
|
||||||
|
|
||||||
return
|
|
||||||
tb.write(x, y, fmt"[{timeStr}] {deliveryIcon} {m.sender}")
|
|
||||||
y = y + 1
|
|
||||||
|
|
||||||
while remainingText.len > 0:
|
|
||||||
if y > yEnd:
|
|
||||||
convo.isTooLong = true
|
|
||||||
app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt" TOO LON2: {convo.name}"))
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
let (line, remain) = remainingText.splitAt(maxContentWidth)
|
|
||||||
remainingText = remain
|
|
||||||
|
|
||||||
tb.write(x+3, y, line)
|
|
||||||
y = y + 1
|
|
||||||
y = y + 1
|
|
||||||
else:
|
|
||||||
var y = yEnd
|
|
||||||
for i in countdown(convo.messages.len-1, 0):
|
|
||||||
|
|
||||||
let m = convo.messages[i]
|
|
||||||
let timeStr = m.timestamp.format("HH:mm:ss")
|
|
||||||
var deliveryIcon = " "
|
|
||||||
var remainingText = m.content
|
|
||||||
|
|
||||||
if m.sender == "You":
|
|
||||||
tb.setForegroundColor(fgYellow)
|
|
||||||
deliveryIcon = if m.isAcknowledged: "✔" else: "◯"
|
|
||||||
else:
|
|
||||||
tb.setForegroundColor(fgGreen)
|
|
||||||
|
|
||||||
# Print lines in reverse order
|
|
||||||
while remainingText.len > 0:
|
|
||||||
if (y <= yStart + 1):
|
|
||||||
return
|
|
||||||
var lineLen = remainingText.len mod maxContentWidth
|
|
||||||
if lineLen == 0:
|
|
||||||
lineLen = maxContentWidth
|
|
||||||
|
|
||||||
let line = remainingText[^lineLen..^1]
|
|
||||||
remainingText = remainingText[0..^lineLen+1]
|
|
||||||
|
|
||||||
tb.write(x+3, y, line)
|
|
||||||
y = y - 1
|
|
||||||
|
|
||||||
tb.write(x, y, fmt"[{timeStr}] {deliveryIcon} {m.sender}")
|
|
||||||
y = y - 2
|
|
||||||
|
|
||||||
proc drawMsgInput( app: ChatApp, layout: Pane) =
|
|
||||||
var tb = app.tb
|
|
||||||
drawOutline(tb, layout, fgCyan)
|
|
||||||
|
|
||||||
|
|
||||||
let inputY = layout.yStart + 2
|
|
||||||
let paneStart = layout.xStart
|
|
||||||
|
|
||||||
# Draw input prompt
|
|
||||||
tb.write(paneStart + 1, inputY, " > " & app.inputBuffer, fgWhite)
|
|
||||||
|
|
||||||
# Draw cursor
|
|
||||||
let cursorX = paneStart + 3 + app.inputBuffer.len + 1
|
|
||||||
if cursorX < paneStart + layout.width - 1:
|
|
||||||
tb.write(cursorX, inputY, "_", fgYellow)
|
|
||||||
|
|
||||||
discard
|
|
||||||
|
|
||||||
proc drawFooter( app: ChatApp, layout: Pane) =
|
|
||||||
var tb = app.tb
|
|
||||||
drawOutline(tb, layout, fgBlue)
|
|
||||||
|
|
||||||
let xStart = layout.xStart + 3
|
|
||||||
let yStart = layout.yStart + 2
|
|
||||||
|
|
||||||
for i in countdown(app.logMsgs.len - 1, 0):
|
|
||||||
let o = app.logMsgs[i]
|
|
||||||
let timeStr = o.ts.format("HH:mm:ss")
|
|
||||||
let s = fmt"[{timeStr}] {o.level} - {o.msg}"
|
|
||||||
app.tb.write( xStart, yStart+i*2, s )
|
|
||||||
discard
|
|
||||||
|
|
||||||
|
|
||||||
proc drawModal(app: ChatApp, layout: Pane,
|
|
||||||
color: ForegroundColor, bg: BackgroundColor = bgBlack) =
|
|
||||||
|
|
||||||
var tb = app.tb
|
|
||||||
tb.setForegroundColor(color)
|
|
||||||
tb.setBackgroundColor(bg)
|
|
||||||
for x in layout.xStart..<layout.xStart+layout.width:
|
|
||||||
for y in layout.yStart..<layout.yStart+layout.height:
|
|
||||||
tb.write(x, y, " ", color)
|
|
||||||
|
|
||||||
tb.setForegroundColor(fgBlack)
|
|
||||||
tb.setBackgroundColor(bgGreen)
|
|
||||||
tb.write(layout.xStart + 2, layout.yStart + 2, "Paste Invite")
|
|
||||||
|
|
||||||
tb.setForegroundColor(fgWhite)
|
|
||||||
tb.setBackgroundColor(bgBlack)
|
|
||||||
let inputLine = 5
|
|
||||||
|
|
||||||
for i in layout.xStart+3..<layout.xStart+layout.width-3:
|
|
||||||
for y in (layout.yStart + inputLine - 1)..<(layout.yStart+inputLine + 2):
|
|
||||||
tb.write(i, y, " ")
|
|
||||||
|
|
||||||
# Draw input prompt
|
|
||||||
tb.write(layout.xStart+5, layout.yStart+inputLine, "> " & app.inputInviteBuffer)
|
|
||||||
|
|
||||||
# Draw cursor
|
|
||||||
let cursorX = layout.xStart+5+1 + app.inputInviteBuffer.len
|
|
||||||
if cursorX < terminalWidth() - 1:
|
|
||||||
tb.write(cursorX, layout.yStart+inputLine, "_", fgYellow)
|
|
||||||
|
|
||||||
tb.setForegroundColor(fgBlack)
|
|
||||||
tb.setBackgroundColor(bgGreen)
|
|
||||||
tb.write(layout.xStart+5, layout.yStart+inputLine+3, "InviteLink: " & app.currentInviteLink)
|
|
||||||
|
|
||||||
resetTuiCursor(tb)
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# Input Handling
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
proc gePreviousConv(app: ChatApp): string =
|
|
||||||
let convos = app.mostRecentConvos()
|
|
||||||
let i = convos.find(app.getSelectedConvo()[])
|
|
||||||
|
|
||||||
return convos[max(i-1, 0)].name
|
|
||||||
|
|
||||||
|
|
||||||
proc getNextConv(app: ChatApp): string =
|
|
||||||
let convos = app.mostRecentConvos()
|
|
||||||
var s = ""
|
|
||||||
for c in convos:
|
|
||||||
s = s & c.name
|
|
||||||
|
|
||||||
let i = convos.find(app.getSelectedConvo()[])
|
|
||||||
app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt"i:{convos[min(i+1, convos.len-1)].isTooLong}"))
|
|
||||||
|
|
||||||
return convos[min(i+1, convos.len-1)].name
|
|
||||||
|
|
||||||
|
|
||||||
proc handleInput(app: ChatApp, key: Key) {.async.} =
|
|
||||||
case key
|
|
||||||
of Key.Up:
|
|
||||||
app.selectedConv = app.gePreviousConv()
|
|
||||||
of Key.Down:
|
|
||||||
app.selectedConv = app.getNextConv()
|
|
||||||
|
|
||||||
of Key.PageUp:
|
|
||||||
app.messageScrollOffset = min(app.messageScrollOffset + 5, 0)
|
|
||||||
of Key.PageDown:
|
|
||||||
app.messageScrollOffset = max(app.messageScrollOffset - 5,
|
|
||||||
-(max(0, app.getSelectedConvo().messages.len - 10)))
|
|
||||||
of Key.Enter:
|
|
||||||
|
|
||||||
if app.inviteModal:
|
|
||||||
notice "Enter Invite", link= app.inputInviteBuffer
|
|
||||||
app.inviteModal = false
|
|
||||||
app.isInviteReady = true
|
|
||||||
else:
|
|
||||||
if app.inputBuffer.len > 0 and app.conversations.len > 0:
|
|
||||||
|
|
||||||
let sc = app.getSelectedConvo()
|
|
||||||
await app.sendMessage(sc, app.inputBuffer)
|
|
||||||
|
|
||||||
app.inputBuffer = ""
|
|
||||||
app.messageScrollOffset = 0 # Auto-scroll to bottom
|
|
||||||
of Key.Backspace:
|
|
||||||
if app.inputBuffer.len > 0:
|
|
||||||
app.inputBuffer.setLen(app.inputBuffer.len - 1)
|
|
||||||
of Key.Tab:
|
|
||||||
app.inviteModal = not app.inviteModal
|
|
||||||
|
|
||||||
if app.inviteModal:
|
|
||||||
app.currentInviteLink = app.client.createIntroBundle().toLink()
|
|
||||||
of Key.Escape, Key.CtrlC:
|
|
||||||
quit(0)
|
|
||||||
else:
|
|
||||||
# Handle regular character input
|
|
||||||
let ch = char(key)
|
|
||||||
if ch.isAlphaNumeric() or ch in " !@#$%^&*()_+-=[]{}|;':\",./<>?":
|
|
||||||
if app.inviteModal:
|
|
||||||
app.inputInviteBuffer.add(ch)
|
|
||||||
else:
|
|
||||||
app.inputBuffer.add(ch)
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# Tasks
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
proc appLoop(app: ChatApp, panes: seq[Pane]) : Future[void] {.async.} =
|
|
||||||
illwillInit(fullscreen = false)
|
|
||||||
# Clear buffer
|
|
||||||
while true:
|
|
||||||
await sleepAsync(chronos.milliseconds(5))
|
|
||||||
app.tb.clear()
|
|
||||||
|
|
||||||
drawStatusBar(app, panes[0], fgBlack, getIdColor(app.client.getId()))
|
|
||||||
drawConversationPane(app, panes[1])
|
|
||||||
drawMsgPane(app, panes[2])
|
|
||||||
|
|
||||||
if app.inviteModal:
|
|
||||||
drawModal(app, panes[5], fgYellow, bgGreen)
|
|
||||||
else:
|
|
||||||
drawMsgInput(app, panes[3])
|
|
||||||
|
|
||||||
drawFooter(app, panes[4])
|
|
||||||
|
|
||||||
# Draw help text
|
|
||||||
app.tb.write(1, terminalHeight()-1, "Tab: Invite Modal | ↑/↓: Select conversation | PgUp/PgDn: Scroll messages | Enter: Send | Esc: Quit", fgGreen)
|
|
||||||
|
|
||||||
# Display buffer
|
|
||||||
app.tb.display()
|
|
||||||
|
|
||||||
# Handle input
|
|
||||||
let key = getKey()
|
|
||||||
await handleInput(app, key)
|
|
||||||
|
|
||||||
if app.isInviteReady:
|
|
||||||
|
|
||||||
try:
|
|
||||||
let sanitized = app.inputInviteBuffer.replace(" ", "").replace("\r","")
|
|
||||||
discard await app.client.newPrivateConversation(toBundle(app.inputInviteBuffer.strip()).get())
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
info "bad invite", invite = app.inputInviteBuffer
|
|
||||||
app.inputInviteBuffer = ""
|
|
||||||
app.isInviteReady = false
|
|
||||||
|
|
||||||
proc peerWatch(app: ChatApp): Future[void] {.async.} =
|
|
||||||
while true:
|
|
||||||
await sleepAsync(chronos.seconds(1))
|
|
||||||
app.peerCount = app.client.ds.getConnectedPeerCount()
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# Main
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
proc initChatApp(client: Client): Future[ChatApp] {.async.} =
|
|
||||||
|
|
||||||
var app = ChatApp(
|
|
||||||
client: client,
|
|
||||||
tb: newTerminalBuffer(terminalWidth(), terminalHeight()),
|
|
||||||
conversations: initTable[string, ConvoInfo](),
|
|
||||||
selectedConv: "Bob",
|
|
||||||
inputBuffer: "",
|
|
||||||
scrollOffset: 0,
|
|
||||||
messageScrollOffset: 0,
|
|
||||||
isInviteReady: false,
|
|
||||||
peerCount: -1,
|
|
||||||
logMsgs: @[]
|
|
||||||
)
|
|
||||||
|
|
||||||
app.setupChatSdk()
|
|
||||||
await app.client.start()
|
|
||||||
|
|
||||||
|
|
||||||
# Add some sample conversations with messages
|
|
||||||
var sender = "Nobody"
|
|
||||||
var conv1 = ConvoInfo(name: "ReadMe", messages: @[])
|
|
||||||
conv1.addMessage("",sender, "First start multiple clients and ensure, that he PeerCount is correct (it's listed in the top left corner)")
|
|
||||||
conv1.addMessage("",sender, "Once connected, The sender needs to get the recipients introduction link. The links contains the key material and information required to initialize a conversation. Press `Tab` to generate a link")
|
|
||||||
conv1.addMessage("",sender, "Paste the link from one client into another. This will start the initialization protocol, which will send an invite to the recipient and negotiate a conversation")
|
|
||||||
conv1.addMessage("",sender, "Once established, Applications are notified by a callback that a new conversation has been established, and participants can send messages")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.conversations[conv1.name] = conv1
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
proc main*() {.async.} =
|
|
||||||
|
|
||||||
let args = getCmdArgs()
|
|
||||||
let client = await createChatClient(args.username)
|
|
||||||
var app = await initChatApp(client)
|
|
||||||
|
|
||||||
let tasks: seq[Future[void]] = @[
|
|
||||||
appLoop(app, getPanes()),
|
|
||||||
peerWatch(app)
|
|
||||||
]
|
|
||||||
|
|
||||||
discard await allFinished(tasks)
|
|
||||||
|
|
||||||
when isMainModule:
|
|
||||||
waitFor main()
|
|
||||||
|
|
||||||
# this is not nim code
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import std/parseopt
|
|
||||||
import illwill
|
|
||||||
import times
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# Command Line Args
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
type CmdArgs* = object
|
|
||||||
username*: string
|
|
||||||
invite*: string
|
|
||||||
|
|
||||||
proc getCmdArgs*(): CmdArgs =
|
|
||||||
var username = ""
|
|
||||||
var invite = ""
|
|
||||||
for kind, key, val in getopt():
|
|
||||||
case kind
|
|
||||||
of cmdArgument:
|
|
||||||
discard
|
|
||||||
of cmdLongOption, cmdShortOption:
|
|
||||||
case key
|
|
||||||
of "name", "n":
|
|
||||||
username = val
|
|
||||||
of "invite", "i":
|
|
||||||
invite = val
|
|
||||||
of cmdEnd:
|
|
||||||
break
|
|
||||||
if username == "":
|
|
||||||
username = "<anonymous>"
|
|
||||||
|
|
||||||
result = CmdArgs(username: username, invite:invite)
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# Utils
|
|
||||||
#################################################
|
|
||||||
|
|
||||||
proc getIdColor*(id: string): BackgroundColor =
|
|
||||||
var i = ord(id[0])
|
|
||||||
|
|
||||||
|
|
||||||
let colors = @[bgCyan,
|
|
||||||
bgGreen,
|
|
||||||
bgMagenta,
|
|
||||||
bgRed,
|
|
||||||
bgYellow,
|
|
||||||
bgBlue
|
|
||||||
]
|
|
||||||
|
|
||||||
return colors[i mod colors.len]
|
|
||||||
|
|
||||||
proc toStr*(ts: DateTime): string =
|
|
||||||
ts.format("HH:mm:ss")
|
|
||||||
Loading…
x
Reference in New Issue
Block a user