1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-12-22 20:27:07 +00:00

Added syntax highlighting

This commit is contained in:
diamondburned (Forefront) 2020-06-28 16:01:08 -07:00
parent 5eac0c0eeb
commit 721b9f0c40
15 changed files with 447 additions and 117 deletions

7
go.mod
View file

@ -4,11 +4,14 @@ go 1.14
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6
// replace github.com/diamondburned/cchat-discord => ../cchat-discord/
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/diamondburned/cchat v0.0.34
github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.0.35
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3
github.com/diamondburned/cchat-mock v0.0.0-20200620231423-b286a0301190
github.com/diamondburned/cchat-mock v0.0.0-20200628063912-3155c1b6d6a9
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64
github.com/goodsign/monday v1.0.0
github.com/google/btree v1.0.0 // indirect

22
go.sum
View file

@ -21,6 +21,12 @@ github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
github.com/Xuanwo/go-locale v0.2.0 h1:1N8SGG2VNpLl6VVa8ueZm3Nm+dxvk8ffY9aviKHl4IE=
github.com/Xuanwo/go-locale v0.2.0/go.mod h1:6qbT9M726OJgyiGZro2YwPmx63wQzlH+VvtjJWQoftw=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI=
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -28,6 +34,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -41,12 +49,16 @@ github.com/diamondburned/cchat v0.0.32 h1:nLiD4sL9+DLBnvNb9XLidd5peRzTgM9lWcqRsU
github.com/diamondburned/cchat v0.0.32/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.34 h1:BGiVxMRA9dmW3rLilIldBvjVan7eTTpaWCCfX9IKBYU=
github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk=
github.com/diamondburned/cchat v0.0.35/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3 h1:8RCcaY3gtA+8NG2mwkcC/PIFK+eS8XnGyeVaUbCXbF0=
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3/go.mod h1:4q0jHEl1gJEzkS92oacwcSf9+3fFcNPukOpURDJpV/A=
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d h1:LkzARyvdGRvAsaKEPTV3XcqMHENH6J+KRAI+3sq41Qs=
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d/go.mod h1:SVTt5je4G+re8aSVJAFk/x8vvbRzXdpKgSKmVGoM1tg=
github.com/diamondburned/cchat-mock v0.0.0-20200620231423-b286a0301190 h1:mHbA/xhL584IToD38m3QkeU1cRWIPzBZCDFi1Wv0Bz4=
github.com/diamondburned/cchat-mock v0.0.0-20200620231423-b286a0301190/go.mod h1:u4aWUu4be+JkuMtTIGJNS79T1b+8ruijVn9qL/LM4Rk=
github.com/diamondburned/cchat-mock v0.0.0-20200628063912-3155c1b6d6a9 h1:PnVUCrLTsQlafutS13ST7292WkBpiMJzA7125q02LkA=
github.com/diamondburned/cchat-mock v0.0.0-20200628063912-3155c1b6d6a9/go.mod h1:kKtLZzvkJdeLpiNVmwe15Bl202gfbIHC/LwWUbsSnls=
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6 h1:ZzLrfQqszhzWI7zqwltzQIWtppfcL7m2aIEpB4kuqx0=
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0=
@ -59,6 +71,8 @@ github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xC
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -124,8 +138,11 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/markbates/pkger v0.17.0 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY=
github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -133,6 +150,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@ -141,6 +159,8 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/twmb/murmur3 v1.1.3 h1:D83U0XYKcHRYwYIpBKf3Pks91Z0Byda/9SJ8B6EMRcA=
@ -223,8 +243,10 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View file

@ -25,21 +25,7 @@ var shortTruncators = []truncator{
}
func TimeAgo(t time.Time) string {
ensureLocale()
trunc := t
now := time.Now()
for _, truncator := range shortTruncators {
trunc = trunc.Truncate(truncator.d)
now = now.Truncate(truncator.d)
if trunc.Equal(now) || truncator.d == -1 {
return monday.Format(t, truncator.s, Locale)
}
}
return ""
return timeAgo(t, shortTruncators)
}
var longTruncators = []truncator{
@ -49,12 +35,20 @@ var longTruncators = []truncator{
}
func TimeAgoLong(t time.Time) string {
return timeAgo(t, longTruncators)
}
func TimeAgoShort(t time.Time) string {
return monday.Format(t, "15:04", Locale)
}
func timeAgo(t time.Time, truncs []truncator) string {
ensureLocale()
trunc := t
now := time.Now()
for _, truncator := range longTruncators {
for _, truncator := range truncs {
trunc = trunc.Truncate(truncator.d)
now = now.Truncate(truncator.d)

View file

@ -2,7 +2,11 @@
package config
import (
"encoding/json"
"sort"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/pkg/errors"
)
const ConfigFile = "config.json"
@ -24,13 +28,38 @@ func (s Section) String() string {
}
}
var Sections = [sectionLen][]Entry{}
type SectionEntries map[string]EntryValue
func sortSection(section Section) {
// TODO: remove the sorting and allow for declarative ordering
sort.Slice(Sections[section], func(i, j int) bool {
return Sections[section][i].Name < Sections[section][j].Name
})
// UnmarshalJSON ignores all JSON entries with unknown keys.
func (s SectionEntries) UnmarshalJSON(b []byte) error {
var entries map[string]json.RawMessage
if err := json.Unmarshal(b, &entries); err != nil {
return err
}
for k, entry := range s {
v, ok := entries[k]
if ok {
if err := entry.UnmarshalJSON(v); err != nil {
// Non-fatal error.
log.Error(errors.Wrapf(err, "Failed to unmarshal key %q", k))
}
}
}
return nil
}
var sections = [sectionLen]SectionEntries{}
func AppearanceAdd(name string, value EntryValue) {
sc := sections[Appearance]
if sc == nil {
sc = make(SectionEntries, 1)
sections[Appearance] = sc
}
sc[name] = value
}
type Entry struct {
@ -38,18 +67,27 @@ type Entry struct {
Value EntryValue
}
func AppearanceAdd(name string, value EntryValue) {
Sections[Appearance] = append(Sections[Appearance], Entry{
Name: name,
Value: value,
})
sortSection(Appearance)
func Sections() (sects [sectionLen][]Entry) {
for i, section := range sections {
var sect = make([]Entry, 0, len(section))
for k, v := range section {
sect = append(sect, Entry{k, v})
}
sort.Slice(sect, func(i, j int) bool {
return sect[i].Name < sect[j].Name
})
sects[i] = sect
}
return
}
func Save() error {
return MarshalToFile(ConfigFile, Sections)
return MarshalToFile(ConfigFile, sections)
}
func Restore() error {
return UnmarshalFromFile(ConfigFile, &Sections)
return UnmarshalFromFile(ConfigFile, &sections)
}

View file

@ -70,7 +70,7 @@ func Section(entries []config.Entry) *gtk.Grid {
func NewPreferenceDialog() *Dialog {
var dialog = NewDialog()
for i, section := range config.Sections {
for i, section := range config.Sections() {
grid := Section(section)
name := config.Section(i).String()

View file

@ -98,3 +98,52 @@ func (s *_switch) UnmarshalJSON(b []byte) error {
*s.value = value
return nil
}
type _inputentry struct {
value *string
change func(string) error
}
func InputEntry(value *string, change func(string) error) EntryValue {
return &_inputentry{value, change}
}
func (e *_inputentry) Construct() gtk.IWidget {
entry, _ := gtk.EntryNew()
entry.SetHExpand(true)
entry.SetText(*e.value)
entry.Connect("changed", func() {
v, err := entry.GetText()
if err != nil {
return
}
*e.value = v
if e.change != nil {
if err := e.change(v); err != nil {
entry.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
entry.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
} else {
entry.RemoveIcon(gtk.ENTRY_ICON_SECONDARY)
}
}
})
entry.Show()
return entry
}
func (e *_inputentry) MarshalJSON() ([]byte, error) {
return json.Marshal(*e.value)
}
func (e *_inputentry) UnmarshalJSON(b []byte) error {
var value string
if err := json.Unmarshal(b, &value); err != nil {
return err
}
*e.value = value
return nil
}

View file

@ -1,10 +1,14 @@
package cozy
import (
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/gotk3/gotk3/gtk"
)
@ -38,6 +42,11 @@ func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
func (c *CollapsedMessage) Collapsed() bool { return true }
func (c *CollapsedMessage) UpdateTimestamp(t time.Time) {
c.GenericContainer.UpdateTimestamp(t)
c.Timestamp.SetMarkup(rich.Small(humanize.TimeAgoShort(t)))
}
func (c *CollapsedMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
// Remove GenericContainer's widgets from the containers.
grid.Remove(c.Timestamp)

View file

@ -141,21 +141,23 @@ func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSende
f.Sender = sender
f.text.SetSensitive(true)
// Do we support message editing?
if editor, ok := sender.(cchat.ServerMessageEditor); ok {
f.editor = editor
// Allow editor to be nil.
ed, ok := sender.(cchat.ServerMessageEditor)
if !ok {
log.Printlnf("Editor is not implemented for %T", sender)
}
f.editor = ed
}
}
// Editable returns whether or not the input field has editing capabilities.
func (f *Field) Editable() bool {
return f.editor != nil
// Editable returns whether or not the input field can be edited.
func (f *Field) Editable(msgID string) bool {
return f.editor != nil && f.editor.MessageEditable(msgID)
}
func (f *Field) StartEditing(msgID string) bool {
// Do we support message editing? If not, exit.
if !f.Editable() {
if !f.Editable(msgID) {
return false
}

View file

@ -37,11 +37,6 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
// If Arrow Up is pressed, then we might want to edit the latest message if
// any.
case gdk.KEY_Up:
// We don't support message editing, so passthrough events.
if !f.Editable() {
return false
}
// Do we have input? If we do, then we shouldn't touch it.
if f.textLen() > 0 {
return false
@ -54,6 +49,11 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
return false
}
// If we don't support message editing, then passthrough events.
if !f.Editable(id) {
return false
}
// Start editing.
f.StartEditing(id)

View file

@ -44,7 +44,7 @@ func (f *Field) sendInput() {
}
// Are we editing anything?
if id := f.editingID; f.Editable() && id != "" {
if id := f.editingID; f.Editable(id) && id != "" {
go func() {
if err := f.editor.EditMessage(id, text); err != nil {
log.Error(errors.Wrap(err, "Failed to edit message"))

View file

@ -97,7 +97,7 @@ func NewEmptyContainer() *GenericContainer {
content.SetLineWrap(true)
content.SetLineWrapMode(pango.WRAP_WORD_CHAR)
content.SetXAlign(0) // left align
// content.SetSelectable(true)
content.SetSelectable(true)
content.Show()
// Add CSS classes.

View file

@ -106,6 +106,14 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
// Bind the state.
v.state.bind(session, server)
// Skipping ok check because sender can be nil. Without the empty
// check, Go will panic.
sender, _ := server.(cchat.ServerMessageSender)
// We're setting this variable before actually calling JoinServer. This is
// because new messages created by JoinServer will use this state for things
// such as determinining if it's deletable or not.
v.InputView.SetSender(session, sender)
gts.Async(func() (func(), error) {
// We can use a background context here, as the user can't go anywhere
// that would require cancellation anyway. This is done in ui.go.
@ -127,10 +135,6 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
// Set the cancel handler.
v.state.setcurrent(s)
// Skipping ok check because sender can be nil. Without the empty
// check, Go will panic.
sender, _ := server.(cchat.ServerMessageSender)
v.InputView.SetSender(session, sender)
}, nil
})
}
@ -180,7 +184,7 @@ func (v *View) BindMenu(msg container.GridMessage) {
var mitems []menu.Item
// Do we have editing capabilities? If yes, append a button to allow it.
if v.InputView.Editable() {
if v.InputView.Editable(msg.ID()) {
mitems = append(mitems, menu.SimpleItem(
"Edit", func() { v.InputView.StartEditing(msg.ID()) },
))

View file

@ -0,0 +1,58 @@
package attrmap
import (
"fmt"
"sort"
)
type AppendMap struct {
appended map[int]string
indices []int
}
func NewAppendedMap() AppendMap {
return AppendMap{
appended: make(map[int]string),
indices: []int{},
}
}
func (a *AppendMap) Span(start, end int, attr string) {
a.Add(start, `<span `+attr+`>`)
a.Add(end, "</span>")
}
func (a *AppendMap) Pair(start, end int, open, close string) {
a.Add(start, open)
a.Add(end, close)
}
func (a *AppendMap) Addf(ind int, f string, argv ...interface{}) {
a.Add(ind, fmt.Sprintf(f, argv...))
}
func (a *AppendMap) Pad(ind int) {
a.Add(ind, "\n")
}
func (a *AppendMap) Add(ind int, attr string) {
if _, ok := a.appended[ind]; ok {
a.appended[ind] += attr
return
}
a.appended[ind] = attr
a.indices = append(a.indices, ind)
}
func (a AppendMap) Get(ind int) string {
return a.appended[ind]
}
func (a *AppendMap) Finalize(strlen int) []int {
// make sure there's always a closing tag at the end so the entire string
// gets flushed.
a.Add(strlen, "")
sort.Ints(a.indices)
return a.indices
}

View file

@ -0,0 +1,203 @@
package hl
import (
"errors"
"fmt"
"html"
"io"
"strings"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/diamondburned/cchat-gtk/internal/ui/config"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
"github.com/diamondburned/cchat/text"
)
var (
lexerMap = map[string]chroma.Lexer{}
// this is not thread-safe, but we can reuse everything, as we know Gtk will
// run everything in the main thread.
fmtter = formatter{}
// tokenType -> span attrs
css = map[chroma.TokenType]string{}
)
func init() {
var name = "algol_nu" // default
ChangeStyle(name)
config.AppearanceAdd("Code Highlight Style", config.InputEntry(&name, ChangeStyle))
}
func Tokenize(language, source string) chroma.Iterator {
var lexer = getLexer(language)
if lexer == nil {
lexer = lexers.Fallback
}
i, _ := lexer.Tokenise(nil, source)
return i
}
func Render(w io.Writer, language, source string) {
if i := Tokenize(language, source); i != nil {
fmtter.Format(w, i)
} else {
// Fallback.
w.Write([]byte(source))
}
}
func RenderString(language, source string) string {
var buf strings.Builder
Render(&buf, language, source)
return buf.String()
}
func Segments(appendmap *attrmap.AppendMap, src string, seg text.Codeblocker) {
var start, end = seg.Bounds()
appendmap.Span(start, end, `font_family="monospace"`)
if i := Tokenize(seg.CodeblockLanguage(), src[start:end]); i != nil {
fmtter.segments(appendmap, start, i)
}
}
func ChangeStyle(styleName string) error {
s := styles.Get(styleName)
// styleName == "" => no highlighting, not an error
if s == styles.Fallback && styleName != "" {
return errors.New("Unknown style")
}
css = styleToCSS(s)
return nil
}
func getLexer(lang string) chroma.Lexer {
v, ok := lexerMap[lang]
if ok {
return v
}
v = lexers.Get(lang)
if v != nil {
lexerMap[lang] = v
return v
}
return nil
}
// Formatter that generates Pango markup.
type formatter struct {
highlightRanges [][2]int
}
func (f *formatter) reset() {
f.highlightRanges = f.highlightRanges[:0]
}
func (f *formatter) segments(appendmap *attrmap.AppendMap, offset int, iter chroma.Iterator) {
f.reset()
for _, token := range iter.Tokens() {
attr := f.styleAttr(token.Type)
if attr != "" {
appendmap.Addf(offset, `<span %s>`, attr)
}
offset += len(token.Value)
if attr != "" {
appendmap.Add(offset, "</span>")
}
}
}
func (f *formatter) Format(w io.Writer, iterator chroma.Iterator) {
f.reset()
for _, token := range iterator.Tokens() {
var code = strings.Replace(html.EscapeString(token.String()), "\t", " ", -1)
if attr := f.styleAttr(token.Type); attr != "" {
code = fmt.Sprintf(`<span font_family="monospace" %s>%s</span>`, attr, code)
}
w.Write([]byte(code))
}
}
func (f *formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
var next = false
for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
highlightIndex++
next = true
}
if highlightIndex < len(f.highlightRanges) {
hrange := f.highlightRanges[highlightIndex]
if line >= hrange[0] && line <= hrange[1] {
return true, next
}
}
return false, next
}
func (f *formatter) styleAttr(tt chroma.TokenType) string {
if _, ok := css[tt]; !ok {
tt = tt.SubCategory()
}
if _, ok := css[tt]; !ok {
tt = tt.Category()
}
if t, ok := css[tt]; ok {
return t
}
return ""
}
func styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
classes := map[chroma.TokenType]string{}
bg := style.Get(chroma.Background)
for t := range chroma.StandardTypes {
var entry = style.Get(t)
if t != chroma.Background {
entry = entry.Sub(bg)
}
if entry.IsZero() {
continue
}
classes[t] = styleEntryToTag(entry)
}
return classes
}
func styleEntryToTag(e chroma.StyleEntry) string {
var attrs = make([]string, 0, 1)
if e.Colour.IsSet() {
attrs = append(attrs, fmt.Sprintf(`foreground="%s"`, e.Colour.String()))
}
if e.Bold == chroma.Yes {
attrs = append(attrs, `weight="bold"`)
}
if e.Italic == chroma.Yes {
attrs = append(attrs, `style="italic"`)
}
if e.Underline == chroma.Yes {
attrs = append(attrs, `underline="single"`)
}
return strings.Join(attrs, " ")
}

View file

@ -5,65 +5,14 @@ import (
"fmt"
"html"
"net/url"
"sort"
"strings"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/hl"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
)
type attrAppendMap struct {
appended map[int]string
indices []int
}
func newAttrAppendedMap() attrAppendMap {
return attrAppendMap{
appended: make(map[int]string),
indices: []int{},
}
}
func (a *attrAppendMap) span(start, end int, attr string) {
a.add(start, `<span `+attr+`>`)
a.add(end, "</span>")
}
func (a *attrAppendMap) pair(start, end int, open, close string) {
a.add(start, open)
a.add(end, close)
}
func (a *attrAppendMap) addf(ind int, f string, argv ...interface{}) {
a.add(ind, fmt.Sprintf(f, argv...))
}
func (a *attrAppendMap) pad(ind int) {
a.add(ind, "\n")
}
func (a *attrAppendMap) add(ind int, attr string) {
if _, ok := a.appended[ind]; ok {
a.appended[ind] += attr
return
}
a.appended[ind] = attr
a.indices = append(a.indices, ind)
}
func (a attrAppendMap) get(ind int) string {
return a.appended[ind]
}
func (a *attrAppendMap) finalize(strlen int) []int {
// make sure there's always a closing tag at the end so the entire string
// gets flushed.
a.add(strlen, "")
sort.Ints(a.indices)
return a.indices
}
func markupAttr(attr text.Attribute) string {
// meme fast path
if attr == 0 {
@ -109,7 +58,7 @@ func RenderMarkup(content text.Rich) string {
// })
// map to append strings to indices
var appended = newAttrAppendedMap()
var appended = attrmap.NewAppendedMap()
// Parse all segments.
for _, segment := range content.Segments {
@ -117,37 +66,36 @@ func RenderMarkup(content text.Rich) string {
switch segment := segment.(type) {
case text.Linker:
appended.addf(start, `<a href="%s">`, html.EscapeString(segment.Link()))
appended.add(end, "</a>")
appended.Addf(start, `<a href="%s">`, html.EscapeString(segment.Link()))
appended.Add(end, "</a>")
case text.Imager:
// Ends don't matter with images.
appended.add(start, composeImageMarkup(segment))
appended.Add(start, composeImageMarkup(segment))
case text.Colorer:
appended.span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color()))
appended.Span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color()))
case text.Attributor:
appended.span(start, end, markupAttr(segment.Attribute()))
appended.Span(start, end, markupAttr(segment.Attribute()))
case text.Codeblocker:
// Treat codeblocks the same as a monospace tag.
// TODO: add highlighting
appended.span(start, end, `font_family="monospace"`)
// Syntax highlight the codeblock.
hl.Segments(&appended, content.Content, segment)
case text.Quoteblocker:
// TODO: pls.
appended.span(start, end, `color="#789922"`)
appended.Span(start, end, `color="#789922"`)
}
}
var lastIndex = 0
for _, index := range appended.finalize(len(content.Content)) {
for _, index := range appended.Finalize(len(content.Content)) {
// Write the content.
buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
// Write the tags.
buf.WriteString(appended.get(index))
buf.WriteString(appended.Get(index))
// Set the last index.
lastIndex = index
}