mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-03-28 04:39:20 +00:00
Added completion into commander
This commit is contained in:
parent
47e3e67b95
commit
d1d7288879
|
@ -4,10 +4,10 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat/utils/split"
|
"github.com/diamondburned/cchat/utils/split"
|
||||||
"github.com/diamondburned/imgutil"
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ func New(text *gtk.TextView) *View {
|
||||||
buffer: buffer,
|
buffer: buffer,
|
||||||
}
|
}
|
||||||
|
|
||||||
text.Connect("key-press-event", v.inputKeyDown)
|
text.Connect("key-press-event", completion.KeyDownHandler(list, text.GrabFocus))
|
||||||
buffer.Connect("changed", func() {
|
buffer.Connect("changed", func() {
|
||||||
// Clear the list first.
|
// Clear the list first.
|
||||||
v.Clear()
|
v.Clear()
|
||||||
|
@ -86,22 +86,9 @@ func New(text *gtk.TextView) *View {
|
||||||
})
|
})
|
||||||
|
|
||||||
list.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
list.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||||
// Get iter for word replacing.
|
completion.SwapWord(v.buffer, v.entries[r.GetIndex()].Raw, v.offset)
|
||||||
start, end := getWordIters(v.buffer, v.offset)
|
|
||||||
|
|
||||||
// Get the selected word.
|
|
||||||
i := r.GetIndex()
|
|
||||||
entry := v.entries[i]
|
|
||||||
|
|
||||||
// Replace the word.
|
|
||||||
v.buffer.Delete(start, end)
|
|
||||||
v.buffer.Insert(start, entry.Raw+" ")
|
|
||||||
|
|
||||||
// Clear the list.
|
|
||||||
v.Clear()
|
v.Clear()
|
||||||
|
v.text.GrabFocus() // TODO: remove, maybe not needed
|
||||||
// Reset the focus.
|
|
||||||
v.text.GrabFocus()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return v
|
return v
|
||||||
|
@ -207,81 +194,5 @@ func (v *View) Run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) getInputState() (string, int) {
|
func (v *View) getInputState() (string, int) {
|
||||||
// obtain current state
|
return completion.State(v.buffer)
|
||||||
mark := v.buffer.GetInsert()
|
|
||||||
iter := v.buffer.GetIterAtMark(mark)
|
|
||||||
|
|
||||||
// obtain the input string and the current cursor position
|
|
||||||
start, end := v.buffer.GetBounds()
|
|
||||||
text, _ := v.buffer.GetText(start, end, true)
|
|
||||||
offset := iter.GetOffset()
|
|
||||||
|
|
||||||
return text, offset
|
|
||||||
}
|
|
||||||
|
|
||||||
// inputKeyDown handles keypresses such as Enter and movements.
|
|
||||||
func (v *View) inputKeyDown(_ *gtk.TextView, ev *gdk.Event) (stop bool) {
|
|
||||||
// Do we have any entries? If not, don't bother.
|
|
||||||
if len(v.entries) == 0 {
|
|
||||||
// passthrough.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var evKey = gdk.EventKeyNewFromEvent(ev)
|
|
||||||
var key = evKey.KeyVal()
|
|
||||||
|
|
||||||
switch key {
|
|
||||||
// Did we press an arrow key?
|
|
||||||
case gdk.KEY_Up, gdk.KEY_Down:
|
|
||||||
// Yes, start moving the list up and down.
|
|
||||||
i := v.List.GetSelectedRow().GetIndex()
|
|
||||||
|
|
||||||
switch key {
|
|
||||||
case gdk.KEY_Up:
|
|
||||||
if i--; i < 0 {
|
|
||||||
i = len(v.entries) - 1
|
|
||||||
}
|
|
||||||
case gdk.KEY_Down:
|
|
||||||
if i++; i >= len(v.entries) {
|
|
||||||
i = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row := v.List.GetRowAtIndex(i)
|
|
||||||
row.GrabFocus() // scroll
|
|
||||||
v.List.SelectRow(row) // select
|
|
||||||
v.text.GrabFocus() // unfocus
|
|
||||||
|
|
||||||
// Did we press the Enter or Tab key?
|
|
||||||
case gdk.KEY_Return, gdk.KEY_Tab:
|
|
||||||
// Activate the current row.
|
|
||||||
row := v.List.GetSelectedRow()
|
|
||||||
row.Activate()
|
|
||||||
|
|
||||||
default:
|
|
||||||
// don't passthrough events if none matches.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWordIters(buf *gtk.TextBuffer, offset int) (start, end *gtk.TextIter) {
|
|
||||||
iter := buf.GetIterAtOffset(offset)
|
|
||||||
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
// Seek backwards for space or start-of-line:
|
|
||||||
_, start, ok = iter.BackwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
|
||||||
if !ok {
|
|
||||||
start = buf.GetStartIter()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seek forwards for space or end-of-line:
|
|
||||||
_, end, ok = iter.ForwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
|
||||||
if !ok {
|
|
||||||
end = buf.GetEndIter()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -87,11 +88,7 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
|
|
||||||
buf, _ := text.GetBuffer()
|
buf, _ := text.GetBuffer()
|
||||||
|
|
||||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
sw := scrollinput.NewV(text, 150)
|
||||||
sw.Add(text)
|
|
||||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
|
||||||
sw.SetProperty("propagate-natural-height", true)
|
|
||||||
sw.SetProperty("max-content-height", 150)
|
|
||||||
sw.Show()
|
sw.Show()
|
||||||
|
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
|
102
internal/ui/primitives/completion/completer.go
Normal file
102
internal/ui/primitives/completion/completer.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package completion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat/utils/split"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Completeable interface {
|
||||||
|
Update([]string, int) []gtk.IWidget
|
||||||
|
Word(i int) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Completer struct {
|
||||||
|
ctrl Completeable
|
||||||
|
|
||||||
|
Input *gtk.TextView
|
||||||
|
List *gtk.ListBox
|
||||||
|
Popover *gtk.Popover
|
||||||
|
|
||||||
|
Words []string
|
||||||
|
Index int
|
||||||
|
Cursor int
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapCompleter(input *gtk.TextView, ctrl Completeable) {
|
||||||
|
NewCompleter(input, ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
||||||
|
l, _ := gtk.ListBoxNew()
|
||||||
|
l.Show()
|
||||||
|
|
||||||
|
p := NewPopover(input)
|
||||||
|
p.Add(l)
|
||||||
|
|
||||||
|
c := &Completer{
|
||||||
|
Input: input,
|
||||||
|
List: l,
|
||||||
|
Popover: p,
|
||||||
|
ctrl: ctrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
|
||||||
|
|
||||||
|
ibuf, _ := input.GetBuffer()
|
||||||
|
ibuf.Connect("changed", func() {
|
||||||
|
t, v := State(ibuf)
|
||||||
|
c.Cursor = v
|
||||||
|
c.Words, c.Index = split.SpaceIndexed(t, v)
|
||||||
|
c.complete()
|
||||||
|
})
|
||||||
|
|
||||||
|
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||||
|
SwapWord(ibuf, ctrl.Word(r.GetIndex()), c.Cursor)
|
||||||
|
c.clear()
|
||||||
|
c.Popover.Popdown()
|
||||||
|
input.GrabFocus()
|
||||||
|
})
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Completer) clear() {
|
||||||
|
var children = c.List.GetChildren()
|
||||||
|
if children.Length() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
children.Foreach(func(i interface{}) {
|
||||||
|
w := i.(gtk.IWidget).ToWidget()
|
||||||
|
c.List.Remove(w)
|
||||||
|
w.Destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Completer) complete() {
|
||||||
|
c.clear()
|
||||||
|
|
||||||
|
var widgets []gtk.IWidget
|
||||||
|
if len(c.Words) > 0 {
|
||||||
|
widgets = c.ctrl.Update(c.Words, c.Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(widgets) > 0 {
|
||||||
|
c.Popover.SetPointingTo(CursorRect(c.Input))
|
||||||
|
c.Popover.Popup()
|
||||||
|
} else {
|
||||||
|
c.Popover.Popdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, widget := range widgets {
|
||||||
|
r, _ := gtk.ListBoxRowNew()
|
||||||
|
r.Add(widget)
|
||||||
|
r.Show()
|
||||||
|
|
||||||
|
c.List.Add(r)
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
c.List.SelectRow(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
internal/ui/primitives/completion/utils.go
Normal file
125
internal/ui/primitives/completion/utils.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package completion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var popoverCSS = primitives.PrepareCSS(`
|
||||||
|
popover {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MinPopoverWidth = 250
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPopover(relto gtk.IWidget) *gtk.Popover {
|
||||||
|
p, _ := gtk.PopoverNew(relto)
|
||||||
|
p.SetSizeRequest(MinPopoverWidth, -1)
|
||||||
|
p.SetModal(false)
|
||||||
|
p.SetPosition(gtk.POS_TOP)
|
||||||
|
primitives.AttachCSS(p, popoverCSS)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyDownHandlerFn = func(gtk.IWidget, *gdk.Event) bool
|
||||||
|
|
||||||
|
func KeyDownHandler(l *gtk.ListBox, focus func()) KeyDownHandlerFn {
|
||||||
|
return func(w gtk.IWidget, ev *gdk.Event) bool {
|
||||||
|
// Do we have any entries? If not, don't bother.
|
||||||
|
var length = int(l.GetChildren().Length())
|
||||||
|
if length == 0 {
|
||||||
|
// passthrough.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var evKey = gdk.EventKeyNewFromEvent(ev)
|
||||||
|
var key = evKey.KeyVal()
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
// Did we press an arrow key?
|
||||||
|
case gdk.KEY_Up, gdk.KEY_Down:
|
||||||
|
// Yes, start moving the list up and down.
|
||||||
|
i := l.GetSelectedRow().GetIndex()
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case gdk.KEY_Up:
|
||||||
|
if i--; i < 0 {
|
||||||
|
i = length - 1
|
||||||
|
}
|
||||||
|
case gdk.KEY_Down:
|
||||||
|
if i++; i >= length {
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row := l.GetRowAtIndex(i)
|
||||||
|
row.GrabFocus() // scroll
|
||||||
|
l.SelectRow(row) // select
|
||||||
|
focus() // unfocus
|
||||||
|
|
||||||
|
// Did we press the Enter or Tab key?
|
||||||
|
case gdk.KEY_Return, gdk.KEY_Tab:
|
||||||
|
// Activate the current row.
|
||||||
|
l.GetSelectedRow().Activate()
|
||||||
|
focus()
|
||||||
|
|
||||||
|
default:
|
||||||
|
// passthrough events if none matches.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SwapWord(b *gtk.TextBuffer, word string, offset int) {
|
||||||
|
// Get iter for word replacing.
|
||||||
|
start, end := GetWordIters(b, offset)
|
||||||
|
b.Delete(start, end)
|
||||||
|
b.Insert(start, word+" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CursorRect(i *gtk.TextView) gdk.Rectangle {
|
||||||
|
r, _ := i.GetCursorLocations(nil)
|
||||||
|
x, _ := i.BufferToWindowCoords(gtk.TEXT_WINDOW_WIDGET, r.GetX(), r.GetY())
|
||||||
|
r.SetX(x)
|
||||||
|
r.SetY(0)
|
||||||
|
return *r
|
||||||
|
}
|
||||||
|
|
||||||
|
func State(buf *gtk.TextBuffer) (string, int) {
|
||||||
|
// obtain current state
|
||||||
|
mark := buf.GetInsert()
|
||||||
|
iter := buf.GetIterAtMark(mark)
|
||||||
|
|
||||||
|
// obtain the input string and the current cursor position
|
||||||
|
start, end := buf.GetBounds()
|
||||||
|
text, _ := buf.GetText(start, end, true)
|
||||||
|
offset := iter.GetOffset()
|
||||||
|
|
||||||
|
return text, offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWordIters(buf *gtk.TextBuffer, offset int) (start, end *gtk.TextIter) {
|
||||||
|
iter := buf.GetIterAtOffset(offset)
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
// Seek backwards for space or start-of-line:
|
||||||
|
_, start, ok = iter.BackwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
||||||
|
if !ok {
|
||||||
|
start = buf.GetStartIter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek forwards for space or end-of-line:
|
||||||
|
_, end, ok = iter.ForwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
||||||
|
if !ok {
|
||||||
|
end = buf.GetEndIter()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
27
internal/ui/primitives/scrollinput/scrollinput.go
Normal file
27
internal/ui/primitives/scrollinput/scrollinput.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package scrollinput
|
||||||
|
|
||||||
|
import "github.com/gotk3/gotk3/gtk"
|
||||||
|
|
||||||
|
func NewV(text *gtk.TextView, maxHeight int) *gtk.ScrolledWindow {
|
||||||
|
// Wrap mode needed since we're not doing horizontal scrolling.
|
||||||
|
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||||
|
|
||||||
|
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||||
|
sw.Add(text)
|
||||||
|
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||||
|
sw.SetProperty("propagate-natural-height", true)
|
||||||
|
sw.SetProperty("max-content-height", maxHeight)
|
||||||
|
|
||||||
|
return sw
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewH(text *gtk.TextView) *gtk.ScrolledWindow {
|
||||||
|
text.SetHExpand(true)
|
||||||
|
|
||||||
|
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||||
|
sw.Add(text)
|
||||||
|
sw.SetPolicy(gtk.POLICY_EXTERNAL, gtk.POLICY_NEVER)
|
||||||
|
sw.SetProperty("propagate-natural-width", true)
|
||||||
|
|
||||||
|
return sw
|
||||||
|
}
|
62
internal/ui/service/session/commander/buffer.go
Normal file
62
internal/ui/service/session/commander/buffer.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package commander
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
b.CreateTag("system", map[string]interface{}{
|
||||||
|
"foreground": "#808080",
|
||||||
|
})
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteSystem is not thread-safe.
|
||||||
|
func (b *Buffer) WriteSystem(bytes []byte) {
|
||||||
|
b.InsertWithTagByName(b.GetEndIter(), string(bytes), "system")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printlnf is not thread-safe.
|
||||||
|
func (b *Buffer) Printlnf(f string, v ...interface{}) {
|
||||||
|
b.WriteSystem([]byte(fmt.Sprintf(f+"\n", v...)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write is thread-safe.
|
||||||
|
func (b *Buffer) Write(bytes []byte) (int, error) {
|
||||||
|
gts.ExecAsync(func() { b.Insert(b.GetEndIter(), string(bytes)) })
|
||||||
|
return len(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) ShowDialog() {
|
||||||
|
SpawnDialog(b)
|
||||||
|
}
|
|
@ -9,62 +9,15 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"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/autoscroll"
|
||||||
"github.com/google/shlex"
|
"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/gtk"
|
||||||
|
"github.com/gotk3/gotk3/pango"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionCommander interface {
|
var monospace = primitives.PrepareCSS(`
|
||||||
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;
|
font-family: monospace;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
@ -73,9 +26,15 @@ var entryCSS = primitives.PrepareCSS(`
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
words []string
|
|
||||||
cmder cchat.Commander
|
cmder cchat.Commander
|
||||||
buffer *Buffer
|
buffer *Buffer
|
||||||
|
cmplt *completer
|
||||||
|
|
||||||
|
inputbuf *gtk.TextBuffer
|
||||||
|
|
||||||
|
// words []string
|
||||||
|
// index int
|
||||||
}
|
}
|
||||||
|
|
||||||
func SpawnDialog(buf *Buffer) {
|
func SpawnDialog(buf *Buffer) {
|
||||||
|
@ -98,69 +57,73 @@ func SpawnDialog(buf *Buffer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
||||||
v, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
view, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
||||||
v.SetEditable(false)
|
view.SetEditable(false)
|
||||||
v.SetProperty("monospace", true)
|
view.SetProperty("monospace", true)
|
||||||
v.SetBorderWidth(8)
|
view.SetPixelsAboveLines(1)
|
||||||
v.SetPixelsAboveLines(1)
|
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||||
v.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
view.Show()
|
||||||
v.Show()
|
|
||||||
|
|
||||||
s := autoscroll.NewScrolledWindow()
|
scroll := autoscroll.NewScrolledWindow()
|
||||||
s.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
scroll.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||||
s.Add(v)
|
scroll.Add(view)
|
||||||
s.Show()
|
scroll.Show()
|
||||||
|
|
||||||
i, _ := gtk.EntryNew()
|
input, _ := gtk.TextViewNew()
|
||||||
primitives.AttachCSS(i, entryCSS)
|
input.SetSizeRequest(-1, 35) // magic height 35px
|
||||||
i.Show()
|
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, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
b.PackStart(s, true, true, 0)
|
b.PackStart(scroll, true, true, 0)
|
||||||
b.PackStart(i, false, false, 0)
|
b.PackStart(sep, false, false, 0)
|
||||||
|
b.PackStart(inputscroll, false, false, 0)
|
||||||
|
|
||||||
session := &Session{
|
session := &Session{
|
||||||
Box: b,
|
Box: b,
|
||||||
cmder: cmder,
|
cmder: cmder,
|
||||||
buffer: buf,
|
buffer: buf,
|
||||||
|
cmplt: newCompleter(input, cmder),
|
||||||
|
inputbuf: inputbuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
i.Connect("activate", session.inputActivate)
|
input.Connect("key-press-event", session.inputActivate)
|
||||||
// Split words on typing to provide live errors.
|
input.GrabFocus()
|
||||||
i.Connect("changed", func(i *gtk.Entry) {
|
|
||||||
t, _ := i.GetText()
|
|
||||||
|
|
||||||
w, err := shlex.Split(t)
|
primitives.AddClass(b, "commander")
|
||||||
if err != nil {
|
primitives.AddClass(view, "command-buffer")
|
||||||
i.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
|
primitives.AddClass(input, "command-input")
|
||||||
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
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) inputActivate(e *gtk.Entry) {
|
func (s *Session) inputActivate(v *gtk.TextView, ev *gdk.Event) bool {
|
||||||
// If the input is empty, then ignore.
|
// If the keypress is not enter, then ignore.
|
||||||
if len(s.words) == 0 {
|
if kev := gdk.EventKeyNewFromEvent(ev); kev.KeyVal() != gdk.KEY_Return {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := s.cmder.RunCommand(s.words)
|
// 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 {
|
if err != nil {
|
||||||
s.buffer.WriteError(err)
|
s.buffer.WriteError(err)
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the entry.
|
// Clear the entry.
|
||||||
e.SetText("")
|
s.inputbuf.Delete(s.inputbuf.GetBounds())
|
||||||
|
|
||||||
var then = time.Now()
|
var then = time.Now()
|
||||||
s.buffer.Printlnf("%s: Running command...", then.Format(time.Kitchen))
|
s.buffer.Printlnf("%s: Running command...", then.Format(time.Kitchen))
|
||||||
|
@ -182,4 +145,50 @@ func (s *Session) inputActivate(e *gtk.Entry) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,3 +11,7 @@ headerbar { padding: 0; }
|
||||||
popover > box {
|
popover > box {
|
||||||
margin: 6px;
|
margin: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.command-buffer, .command-input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue