mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-09-27 20:48:45 +00:00
186 lines
3.8 KiB
Go
186 lines
3.8 KiB
Go
|
package commander
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"time"
|
||
|
|
||
|
"github.com/diamondburned/cchat"
|
||
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
|
||
|
"github.com/google/shlex"
|
||
|
"github.com/gotk3/gotk3/gtk"
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
type SessionCommander interface {
|
||
|
cchat.Session
|
||
|
cchat.Commander
|
||
|
}
|
||
|
|
||
|
type Buffer struct {
|
||
|
*gtk.TextBuffer
|
||
|
svcname string
|
||
|
cmder SessionCommander
|
||
|
}
|
||
|
|
||
|
// NewBuffer creates a new buffer with the given SessionCommander, or returns
|
||
|
// nil if cmder is nil.
|
||
|
func NewBuffer(svc cchat.Service, cmder SessionCommander) *Buffer {
|
||
|
if cmder == nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
b, _ := gtk.TextBufferNew(nil)
|
||
|
b.CreateTag("error", map[string]interface{}{
|
||
|
"foreground": "#FF0000",
|
||
|
})
|
||
|
return &Buffer{b, svc.Name().Content, cmder}
|
||
|
}
|
||
|
|
||
|
// WriteError is not thread-safe.
|
||
|
func (b *Buffer) WriteError(err error) {
|
||
|
b.InsertWithTagByName(b.GetEndIter(), err.Error()+"\n", "error")
|
||
|
}
|
||
|
|
||
|
// WriteUnsafe is not thread-safe.
|
||
|
func (b *Buffer) WriteUnsafe(bytes []byte) {
|
||
|
b.Insert(b.GetEndIter(), string(bytes))
|
||
|
}
|
||
|
|
||
|
// Printlnf is not thread-safe.
|
||
|
func (b *Buffer) Printlnf(f string, v ...interface{}) {
|
||
|
b.WriteUnsafe([]byte(fmt.Sprintf(f+"\n", v...)))
|
||
|
}
|
||
|
|
||
|
// Write is thread-safe.
|
||
|
func (b *Buffer) Write(bytes []byte) (int, error) {
|
||
|
gts.ExecAsync(func() { b.WriteUnsafe(bytes) })
|
||
|
return len(bytes), nil
|
||
|
}
|
||
|
|
||
|
func (b *Buffer) ShowDialog() {
|
||
|
SpawnDialog(b)
|
||
|
}
|
||
|
|
||
|
var entryCSS = primitives.PrepareCSS(`
|
||
|
* {
|
||
|
font-family: monospace;
|
||
|
border-radius: 0;
|
||
|
}
|
||
|
`)
|
||
|
|
||
|
type Session struct {
|
||
|
*gtk.Box
|
||
|
words []string
|
||
|
cmder cchat.Commander
|
||
|
buffer *Buffer
|
||
|
}
|
||
|
|
||
|
func SpawnDialog(buf *Buffer) {
|
||
|
s := NewSession(buf.cmder, buf)
|
||
|
s.Show()
|
||
|
|
||
|
h, _ := gtk.HeaderBarNew()
|
||
|
h.SetTitle(fmt.Sprintf(
|
||
|
"Commander: %s on %s",
|
||
|
buf.cmder.Name().Content, buf.svcname,
|
||
|
))
|
||
|
h.SetShowCloseButton(true)
|
||
|
h.Show()
|
||
|
|
||
|
d, _ := gts.NewEmptyModalDialog()
|
||
|
d.SetDefaultSize(450, 250)
|
||
|
d.SetTitlebar(h)
|
||
|
d.Add(s)
|
||
|
d.Show()
|
||
|
}
|
||
|
|
||
|
func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
||
|
v, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
||
|
v.SetEditable(false)
|
||
|
v.SetProperty("monospace", true)
|
||
|
v.SetBorderWidth(8)
|
||
|
v.SetPixelsAboveLines(1)
|
||
|
v.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||
|
v.Show()
|
||
|
|
||
|
s := autoscroll.NewScrolledWindow()
|
||
|
s.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||
|
s.Add(v)
|
||
|
s.Show()
|
||
|
|
||
|
i, _ := gtk.EntryNew()
|
||
|
primitives.AttachCSS(i, entryCSS)
|
||
|
i.Show()
|
||
|
|
||
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||
|
b.PackStart(s, true, true, 0)
|
||
|
b.PackStart(i, false, false, 0)
|
||
|
|
||
|
session := &Session{
|
||
|
Box: b,
|
||
|
cmder: cmder,
|
||
|
buffer: buf,
|
||
|
}
|
||
|
|
||
|
i.Connect("activate", session.inputActivate)
|
||
|
// Split words on typing to provide live errors.
|
||
|
i.Connect("changed", func(i *gtk.Entry) {
|
||
|
t, _ := i.GetText()
|
||
|
|
||
|
w, err := shlex.Split(t)
|
||
|
if err != nil {
|
||
|
i.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
|
||
|
i.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
|
||
|
session.words = nil
|
||
|
} else {
|
||
|
i.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "")
|
||
|
session.words = w
|
||
|
}
|
||
|
})
|
||
|
|
||
|
// Focus on the input by default.
|
||
|
i.GrabFocus()
|
||
|
|
||
|
return session
|
||
|
}
|
||
|
|
||
|
func (s *Session) inputActivate(e *gtk.Entry) {
|
||
|
// If the input is empty, then ignore.
|
||
|
if len(s.words) == 0 {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
r, err := s.cmder.RunCommand(s.words)
|
||
|
if err != nil {
|
||
|
s.buffer.WriteError(err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Clear the entry.
|
||
|
e.SetText("")
|
||
|
|
||
|
var then = time.Now()
|
||
|
s.buffer.Printlnf("%s: Running command...", then.Format(time.Kitchen))
|
||
|
|
||
|
go func() {
|
||
|
_, err := io.Copy(s.buffer, r)
|
||
|
r.Close()
|
||
|
|
||
|
gts.ExecAsync(func() {
|
||
|
if err != nil {
|
||
|
s.buffer.WriteError(errors.Wrap(err, "Internal error"))
|
||
|
}
|
||
|
|
||
|
var now = time.Now()
|
||
|
s.buffer.Printlnf(
|
||
|
"%s: Finished running command, took %s.",
|
||
|
now.Format(time.Kitchen),
|
||
|
now.Sub(then).String(),
|
||
|
)
|
||
|
})
|
||
|
}()
|
||
|
}
|