mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-11-01 12:04:15 +00:00
diamondburned (Forefront)
f10aa71003
This commit refactors the input container's UI as well as fixing some bugs related to asynchronous fetching of images. It also adds complete typing indicator capabilities, all without using a single mutex!
201 lines
4.2 KiB
Go
201 lines
4.2 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/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
|
"github.com/gotk3/gotk3/gdk"
|
|
"github.com/gotk3/gotk3/gtk"
|
|
"github.com/gotk3/gotk3/pango"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var monospace = primitives.PrepareCSS(`
|
|
* {
|
|
font-family: monospace;
|
|
border-radius: 0;
|
|
}
|
|
`)
|
|
|
|
var commandPadding = primitives.PrepareCSS(`
|
|
* { padding: 8px 12px; }
|
|
`)
|
|
|
|
type Session struct {
|
|
*gtk.Box
|
|
|
|
cmder cchat.Commander
|
|
buffer *Buffer
|
|
cmplt *completer
|
|
|
|
inputbuf *gtk.TextBuffer
|
|
|
|
// words []string
|
|
// index int
|
|
}
|
|
|
|
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 {
|
|
view, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
|
view.SetEditable(false)
|
|
view.SetProperty("monospace", true)
|
|
view.SetPixelsAboveLines(1)
|
|
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
|
view.Show()
|
|
|
|
scroll := autoscroll.NewScrolledWindow()
|
|
scroll.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
|
scroll.Add(view)
|
|
scroll.Show()
|
|
|
|
input, _ := gtk.TextViewNew()
|
|
input.SetSizeRequest(-1, 35) // magic height 35px
|
|
primitives.AttachCSS(input, monospace)
|
|
input.Show()
|
|
|
|
inputbuf, _ := input.GetBuffer()
|
|
|
|
inputscroll := scrollinput.NewH(input)
|
|
inputscroll.Show()
|
|
|
|
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
|
|
sep.Show()
|
|
|
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
|
b.PackStart(scroll, true, true, 0)
|
|
b.PackStart(sep, false, false, 0)
|
|
b.PackStart(inputscroll, false, false, 0)
|
|
|
|
session := &Session{
|
|
Box: b,
|
|
cmder: cmder,
|
|
buffer: buf,
|
|
cmplt: newCompleter(input, cmder),
|
|
inputbuf: inputbuf,
|
|
}
|
|
|
|
input.Connect("key-press-event", session.inputActivate)
|
|
input.GrabFocus()
|
|
|
|
primitives.AddClass(b, "commander")
|
|
primitives.AddClass(view, "command-buffer")
|
|
primitives.AddClass(input, "command-input")
|
|
primitives.AttachCSS(view, commandPadding)
|
|
primitives.AttachCSS(input, commandPadding)
|
|
|
|
return session
|
|
}
|
|
|
|
func (s *Session) inputActivate(v *gtk.TextView, ev *gdk.Event) bool {
|
|
// If the keypress is not enter, then ignore.
|
|
if kev := gdk.EventKeyNewFromEvent(ev); kev.KeyVal() != gdk.KEY_Return {
|
|
return false
|
|
}
|
|
|
|
// If the input is empty, then ignore.
|
|
if len(s.cmplt.Words) == 0 {
|
|
return true
|
|
}
|
|
|
|
r, err := s.cmder.RunCommand(s.cmplt.Words)
|
|
if err != nil {
|
|
s.buffer.WriteError(err)
|
|
return true
|
|
}
|
|
|
|
// Clear the entry.
|
|
s.inputbuf.Delete(s.inputbuf.GetBounds())
|
|
|
|
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(),
|
|
)
|
|
})
|
|
}()
|
|
|
|
return true
|
|
}
|
|
|
|
type completer struct {
|
|
*completion.Completer
|
|
|
|
completer cchat.CommandCompleter
|
|
choices []string
|
|
}
|
|
|
|
func newCompleter(input *gtk.TextView, v cchat.Commander) *completer {
|
|
completer := &completer{}
|
|
completer.Completer = completion.NewCompleter(input, completer)
|
|
|
|
c, ok := v.(cchat.CommandCompleter)
|
|
if ok {
|
|
completer.completer = c
|
|
}
|
|
|
|
return completer
|
|
}
|
|
|
|
func (c *completer) Update(words []string, offset int) []gtk.IWidget {
|
|
if c.completer == nil {
|
|
return nil
|
|
}
|
|
|
|
c.choices = c.completer.CompleteCommand(words, offset)
|
|
var widgets = make([]gtk.IWidget, 0, len(c.choices))
|
|
|
|
for _, choice := range c.choices {
|
|
l, _ := gtk.LabelNew(choice)
|
|
l.SetXAlign(0)
|
|
l.SetEllipsize(pango.ELLIPSIZE_END)
|
|
primitives.AttachCSS(l, monospace)
|
|
l.Show()
|
|
|
|
widgets = append(widgets, l)
|
|
}
|
|
|
|
return widgets
|
|
}
|
|
|
|
func (c *completer) Word(i int) string {
|
|
return c.choices[i]
|
|
}
|