Feat: edit agent png cards
This commit is contained in:
@@ -37,12 +37,12 @@
|
|||||||
- connection to a model status;
|
- connection to a model status;
|
||||||
- ===== /llamacpp specific (it has a different body -> interface instead of global var)
|
- ===== /llamacpp specific (it has a different body -> interface instead of global var)
|
||||||
- edit syscards / create new ones;
|
- edit syscards / create new ones;
|
||||||
- consider adding use /completion of llamacpp, since openai endpoint clearly has template|format issues;
|
- consider adding use /completion of llamacpp, since openai endpoint clearly has template|format issues; +
|
||||||
- change temp, min-p and other params from tui;
|
- change temp, min-p and other params from tui;
|
||||||
- DRY;
|
- DRY; +
|
||||||
- keybind to switch between openai and llamacpp endpoints;
|
- keybind to switch between openai and llamacpp endpoints;
|
||||||
- option to remove <thinking> from chat history;
|
- option to remove <thinking> from chat history;
|
||||||
- in chat management table add preview of the last message;
|
- in chat management table add preview of the last message; +
|
||||||
|
|
||||||
### FIX:
|
### FIX:
|
||||||
- bot responding (or hanging) blocks everything; +
|
- bot responding (or hanging) blocks everything; +
|
||||||
@@ -67,3 +67,5 @@
|
|||||||
- F1 can load any chat, by loading chat of other agent it does not switch agents, if that chat is continued, it will rewrite agent in db; (either allow only chats from current agent OR switch agent on chat loading); +
|
- F1 can load any chat, by loading chat of other agent it does not switch agents, if that chat is continued, it will rewrite agent in db; (either allow only chats from current agent OR switch agent on chat loading); +
|
||||||
- after chat is deleted: load undeleted chat; +
|
- after chat is deleted: load undeleted chat; +
|
||||||
- name split for llamacpp completion. user msg should end with 'bot_name:';
|
- name split for llamacpp completion. user msg should end with 'bot_name:';
|
||||||
|
- add retry on failed call (and EOF);
|
||||||
|
- model info shold be an event and show disconnect status when fails;
|
||||||
|
|||||||
65
bot.go
65
bot.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -43,32 +44,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ====
|
|
||||||
|
|
||||||
// DEPRECATED
|
|
||||||
// func formMsg(chatBody *models.ChatBody, newMsg, role string) io.Reader {
|
|
||||||
// if newMsg != "" { // otherwise let the bot continue
|
|
||||||
// newMsg := models.RoleMsg{Role: role, Content: newMsg}
|
|
||||||
// chatBody.Messages = append(chatBody.Messages, newMsg)
|
|
||||||
// // if rag
|
|
||||||
// if cfg.RAGEnabled {
|
|
||||||
// ragResp, err := chatRagUse(newMsg.Content)
|
|
||||||
// if err != nil {
|
|
||||||
// logger.Error("failed to form a rag msg", "error", err)
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// ragMsg := models.RoleMsg{Role: cfg.ToolRole, Content: ragResp}
|
|
||||||
// chatBody.Messages = append(chatBody.Messages, ragMsg)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// data, err := json.Marshal(chatBody)
|
|
||||||
// if err != nil {
|
|
||||||
// logger.Error("failed to form a msg", "error", err)
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// return bytes.NewReader(data)
|
|
||||||
// }
|
|
||||||
|
|
||||||
func fetchModelName() {
|
func fetchModelName() {
|
||||||
api := "http://localhost:8080/v1/models"
|
api := "http://localhost:8080/v1/models"
|
||||||
resp, err := httpClient.Get(api)
|
resp, err := httpClient.Get(api)
|
||||||
@@ -293,6 +268,42 @@ func chatToText(showSys bool) string {
|
|||||||
return strings.Join(s, "")
|
return strings.Join(s, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func removeThinking() {
|
||||||
|
// s := chatToTextSlice(false) // will delete tools messages though
|
||||||
|
// chat := strings.Join(s, "")
|
||||||
|
// chat = thinkRE.ReplaceAllString(chat, "")
|
||||||
|
// reS := fmt.Sprintf("[%s:\n,%s:\n]", cfg.AssistantRole, cfg.UserRole)
|
||||||
|
// // no way to know what agent wrote which msg
|
||||||
|
// s = regexp.MustCompile(reS).Split(chat, -1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func textToMsgs(text string) []models.RoleMsg {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
roleRE := regexp.MustCompile(`^\(\d+\) <.*>:`)
|
||||||
|
resp := []models.RoleMsg{}
|
||||||
|
oldrole := ""
|
||||||
|
for _, line := range lines {
|
||||||
|
if roleRE.MatchString(line) {
|
||||||
|
// extract role
|
||||||
|
role := ""
|
||||||
|
// if role changes
|
||||||
|
if role != oldrole {
|
||||||
|
oldrole = role
|
||||||
|
// newmsg
|
||||||
|
msg := models.RoleMsg{
|
||||||
|
Role: role,
|
||||||
|
}
|
||||||
|
resp = append(resp, msg)
|
||||||
|
}
|
||||||
|
resp[len(resp)-1].Content += "\n" + line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(resp) != 0 {
|
||||||
|
resp[0].Content = strings.TrimPrefix(resp[0].Content, "\n")
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
func applyCharCard(cc *models.CharCard) {
|
func applyCharCard(cc *models.CharCard) {
|
||||||
cfg.AssistantRole = cc.Role
|
cfg.AssistantRole = cc.Role
|
||||||
// TODO: need map role->icon
|
// TODO: need map role->icon
|
||||||
@@ -381,6 +392,6 @@ func init() {
|
|||||||
Messages: lastChat,
|
Messages: lastChat,
|
||||||
}
|
}
|
||||||
initChunkParser()
|
initChunkParser()
|
||||||
go runModelNameTicker(time.Second * 120)
|
// go runModelNameTicker(time.Second * 120)
|
||||||
// tempLoad()
|
// tempLoad()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type CharCardSpec struct {
|
|||||||
Spec string `json:"spec"`
|
Spec string `json:"spec"`
|
||||||
SpecVersion string `json:"spec_version"`
|
SpecVersion string `json:"spec_version"`
|
||||||
Tags []any `json:"tags"`
|
Tags []any `json:"tags"`
|
||||||
|
Extentions []byte `json:"extentions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Spec2Wrapper struct {
|
type Spec2Wrapper struct {
|
||||||
@@ -43,3 +44,15 @@ type CharCard struct {
|
|||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
FilePath string `json:"filepath"`
|
FilePath string `json:"filepath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cc *CharCard) ToSpec(userName string) *CharCardSpec {
|
||||||
|
descr := strings.ReplaceAll(strings.ReplaceAll(cc.SysPrompt, cc.Role, "{{char}}"), userName, "{{user}}")
|
||||||
|
return &CharCardSpec{
|
||||||
|
Name: cc.Role,
|
||||||
|
Description: descr,
|
||||||
|
FirstMes: cc.FirstMsg,
|
||||||
|
Spec: "chara_card_v2",
|
||||||
|
SpecVersion: "2.0",
|
||||||
|
Extentions: []byte("{}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,15 +8,22 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
embType = "tEXt"
|
embType = "tEXt"
|
||||||
|
cKey = "chara"
|
||||||
|
IEND = "IEND"
|
||||||
|
header = "\x89PNG\r\n\x1a\n"
|
||||||
|
writeHeader = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var tEXtChunkDataSpecification = "%s\x00%s"
|
||||||
|
|
||||||
type PngEmbed struct {
|
type PngEmbed struct {
|
||||||
Key string
|
Key string
|
||||||
Value string
|
Value string
|
||||||
@@ -107,7 +114,7 @@ func readCardJson(fname string) (*models.CharCard, error) {
|
|||||||
return &card, nil
|
return &card, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadDirCards(dirname, uname string) ([]*models.CharCard, error) {
|
func ReadDirCards(dirname, uname string, log *slog.Logger) ([]*models.CharCard, error) {
|
||||||
files, err := os.ReadDir(dirname)
|
files, err := os.ReadDir(dirname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -121,7 +128,7 @@ func ReadDirCards(dirname, uname string) ([]*models.CharCard, error) {
|
|||||||
fpath := path.Join(dirname, f.Name())
|
fpath := path.Join(dirname, f.Name())
|
||||||
cc, err := ReadCard(fpath, uname)
|
cc, err := ReadCard(fpath, uname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// logger.Warn("failed to load card", "error", err)
|
log.Warn("failed to load card", "error", err)
|
||||||
continue
|
continue
|
||||||
// return nil, err // better to log and continue
|
// return nil, err // better to log and continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ var (
|
|||||||
ErrBadLength = errors.New("bad length")
|
ErrBadLength = errors.New("bad length")
|
||||||
)
|
)
|
||||||
|
|
||||||
const header = "\x89PNG\r\n\x1a\n"
|
|
||||||
|
|
||||||
type PngChunk struct {
|
type PngChunk struct {
|
||||||
typ string
|
typ string
|
||||||
length int32
|
length int32
|
||||||
|
|||||||
116
pngmeta/partswriter.go
Normal file
116
pngmeta/partswriter.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package pngmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"elefant/models"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Writer struct {
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPNGWriter(w io.Writer) (*Writer, error) {
|
||||||
|
if _, err := io.WriteString(w, writeHeader); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Writer{w}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteChunk(length int32, typ string, r io.Reader) error {
|
||||||
|
if err := binary.Write(w.w, binary.BigEndian, length); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.w.Write([]byte(typ)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
checksummer := crc32.NewIEEE()
|
||||||
|
checksummer.Write([]byte(typ))
|
||||||
|
if _, err := io.CopyN(io.MultiWriter(w.w, checksummer), r, int64(length)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := binary.Write(w.w, binary.BigEndian, checksummer.Sum32()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteToPng(c *models.CharCardSpec, fpath, outfile string) error {
|
||||||
|
data, err := os.ReadFile(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Base64 encode the JSON data
|
||||||
|
base64Data := base64.StdEncoding.EncodeToString(jsonData)
|
||||||
|
pe := PngEmbed{
|
||||||
|
Key: cKey,
|
||||||
|
Value: base64Data,
|
||||||
|
}
|
||||||
|
w, err := WritetEXtToPngBytes(data, pe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(outfile, w.Bytes(), 0666)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WritetEXtToPngBytes(inputBytes []byte, pe PngEmbed) (outputBytes bytes.Buffer, err error) {
|
||||||
|
if !(string(inputBytes[:8]) == header) {
|
||||||
|
return outputBytes, errors.New("wrong file format")
|
||||||
|
}
|
||||||
|
reader := bytes.NewReader(inputBytes)
|
||||||
|
pngr, err := NewPNGStepReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return outputBytes, fmt.Errorf("NewReader(): %s", err)
|
||||||
|
}
|
||||||
|
pngw, err := NewPNGWriter(&outputBytes)
|
||||||
|
if err != nil {
|
||||||
|
return outputBytes, fmt.Errorf("NewWriter(): %s", err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
chunk, err := pngr.Next()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return outputBytes, fmt.Errorf("NextChunk(): %s", err)
|
||||||
|
}
|
||||||
|
if chunk.Type() != embType {
|
||||||
|
// IENDChunkType will only appear on the final iteration of a valid PNG
|
||||||
|
if chunk.Type() == IEND {
|
||||||
|
// This is where we inject tEXtChunkType as the penultimate chunk with the new value
|
||||||
|
newtEXtChunk := []byte(fmt.Sprintf(tEXtChunkDataSpecification, pe.Key, pe.Value))
|
||||||
|
if err := pngw.WriteChunk(int32(len(newtEXtChunk)), embType, bytes.NewBuffer(newtEXtChunk)); err != nil {
|
||||||
|
return outputBytes, fmt.Errorf("WriteChunk(): %s", err)
|
||||||
|
}
|
||||||
|
// Now we end the buffer with IENDChunkType chunk
|
||||||
|
if err := pngw.WriteChunk(chunk.length, chunk.Type(), chunk); err != nil {
|
||||||
|
return outputBytes, fmt.Errorf("WriteChunk(): %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// writes back original chunk to buffer
|
||||||
|
if err := pngw.WriteChunk(chunk.length, chunk.Type(), chunk); err != nil {
|
||||||
|
return outputBytes, fmt.Errorf("WriteChunk(): %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := io.Copy(io.Discard, chunk); err != nil {
|
||||||
|
return outputBytes, fmt.Errorf("io.Copy(io.Discard, chunk): %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := chunk.Close(); err != nil {
|
||||||
|
return outputBytes, fmt.Errorf("chunk.Close(): %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputBytes, nil
|
||||||
|
}
|
||||||
23
tables.go
23
tables.go
@@ -4,9 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"elefant/models"
|
"elefant/models"
|
||||||
|
"elefant/pngmeta"
|
||||||
"elefant/rag"
|
"elefant/rag"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
@@ -14,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
||||||
actions := []string{"load", "rename", "delete"}
|
actions := []string{"load", "rename", "delete", "update card"}
|
||||||
chatList := make([]string, len(chatMap))
|
chatList := make([]string, len(chatMap))
|
||||||
i := 0
|
i := 0
|
||||||
for name := range chatMap {
|
for name := range chatMap {
|
||||||
@@ -62,6 +64,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
|||||||
tc.SetTextColor(tcell.ColorRed)
|
tc.SetTextColor(tcell.ColorRed)
|
||||||
chatActTable.SetSelectable(false, false)
|
chatActTable.SetSelectable(false, false)
|
||||||
selectedChat := chatList[row]
|
selectedChat := chatList[row]
|
||||||
|
defer pages.RemovePage(historyPage)
|
||||||
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
||||||
switch tc.Text {
|
switch tc.Text {
|
||||||
case "load":
|
case "load":
|
||||||
@@ -98,8 +101,24 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
|||||||
textView.SetText(chatToText(cfg.ShowSys))
|
textView.SetText(chatToText(cfg.ShowSys))
|
||||||
pages.RemovePage(historyPage)
|
pages.RemovePage(historyPage)
|
||||||
return
|
return
|
||||||
|
case "update card":
|
||||||
|
// save updated card
|
||||||
|
fi := strings.Index(selectedChat, "_")
|
||||||
|
agentName := selectedChat[fi+1:]
|
||||||
|
cc, ok := sysMap[agentName]
|
||||||
|
if !ok {
|
||||||
|
logger.Warn("no such card", "agent", agentName)
|
||||||
|
//no:lint
|
||||||
|
notifyUser("error", "no such card: "+agentName)
|
||||||
|
}
|
||||||
|
if err := pngmeta.WriteToPng(cc.ToSpec(cfg.UserRole), cc.FilePath, cc.FilePath); err != nil {
|
||||||
|
logger.Error("failed to write charcard",
|
||||||
|
"error", err)
|
||||||
|
}
|
||||||
|
// pages.RemovePage(historyPage)
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
pages.RemovePage(historyPage)
|
// pages.RemovePage(historyPage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
1
tools.go
1
tools.go
@@ -12,6 +12,7 @@ var (
|
|||||||
toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`)
|
toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`)
|
||||||
quotesRE = regexp.MustCompile(`(".*?")`)
|
quotesRE = regexp.MustCompile(`(".*?")`)
|
||||||
starRE = regexp.MustCompile(`(\*.*?\*)`)
|
starRE = regexp.MustCompile(`(\*.*?\*)`)
|
||||||
|
thinkRE = regexp.MustCompile(`(<think>.*?</think>)`)
|
||||||
// codeBlokRE = regexp.MustCompile(`(\x60\x60\x60.*?\x60\x60\x60)`)
|
// codeBlokRE = regexp.MustCompile(`(\x60\x60\x60.*?\x60\x60\x60)`)
|
||||||
basicSysMsg = `Large Language Model that helps user with any of his requests.`
|
basicSysMsg = `Large Language Model that helps user with any of his requests.`
|
||||||
toolSysMsg = `You're a helpful assistant.
|
toolSysMsg = `You're a helpful assistant.
|
||||||
|
|||||||
24
tui.go
24
tui.go
@@ -77,11 +77,13 @@ Press Enter to go back
|
|||||||
|
|
||||||
func colorText() {
|
func colorText() {
|
||||||
// INFO: is there a better way to markdown?
|
// INFO: is there a better way to markdown?
|
||||||
tv := textView.GetText(false)
|
text := textView.GetText(false)
|
||||||
cq := quotesRE.ReplaceAllString(tv, `[orange:-:-]$1[-:-:-]`)
|
text = quotesRE.ReplaceAllString(text, `[orange:-:-]$1[-:-:-]`)
|
||||||
|
text = starRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`)
|
||||||
|
text = thinkRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`)
|
||||||
// cb := codeBlockColor(cq)
|
// cb := codeBlockColor(cq)
|
||||||
// cb := codeBlockRE.ReplaceAllString(cq, `[blue:black:i]$1[-:-:-]`)
|
// cb := codeBlockRE.ReplaceAllString(cq, `[blue:black:i]$1[-:-:-]`)
|
||||||
textView.SetText(starRE.ReplaceAllString(cq, `[turquoise::i]$1[-:-:-]`))
|
textView.SetText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStatusLine() {
|
func updateStatusLine() {
|
||||||
@@ -91,7 +93,7 @@ func updateStatusLine() {
|
|||||||
func initSysCards() ([]string, error) {
|
func initSysCards() ([]string, error) {
|
||||||
labels := []string{}
|
labels := []string{}
|
||||||
labels = append(labels, sysLabels...)
|
labels = append(labels, sysLabels...)
|
||||||
cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole)
|
cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to read sys dir", "error", err)
|
logger.Error("failed to read sys dir", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -116,7 +118,7 @@ func startNewChat() {
|
|||||||
logger.Warn("no such sys msg", "name", cfg.AssistantRole)
|
logger.Warn("no such sys msg", "name", cfg.AssistantRole)
|
||||||
}
|
}
|
||||||
// set chat body
|
// set chat body
|
||||||
chatBody.Messages = defaultStarter
|
chatBody.Messages = chatBody.Messages[:2]
|
||||||
textView.SetText(chatToText(cfg.ShowSys))
|
textView.SetText(chatToText(cfg.ShowSys))
|
||||||
newChat := &models.Chat{
|
newChat := &models.Chat{
|
||||||
ID: id + 1,
|
ID: id + 1,
|
||||||
@@ -186,7 +188,17 @@ func init() {
|
|||||||
SetChangedFunc(func() {
|
SetChangedFunc(func() {
|
||||||
app.Draw()
|
app.Draw()
|
||||||
})
|
})
|
||||||
textView.SetBorder(true).SetTitle("chat")
|
// textView.SetBorder(true).SetTitle("chat")
|
||||||
|
textView.SetDoneFunc(func(key tcell.Key) {
|
||||||
|
currentSelection := textView.GetHighlights()
|
||||||
|
if key == tcell.KeyEnter {
|
||||||
|
if len(currentSelection) > 0 {
|
||||||
|
textView.Highlight()
|
||||||
|
} else {
|
||||||
|
textView.Highlight("0").ScrollToHighlight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
focusSwitcher[textArea] = textView
|
focusSwitcher[textArea] = textView
|
||||||
focusSwitcher[textView] = textArea
|
focusSwitcher[textView] = textArea
|
||||||
position = tview.NewTextView().
|
position = tview.NewTextView().
|
||||||
|
|||||||
Reference in New Issue
Block a user