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:
parent
5eac0c0eeb
commit
721b9f0c40
7
go.mod
7
go.mod
|
@ -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
22
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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, §ions)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()) },
|
||||
))
|
||||
|
|
58
internal/ui/rich/parser/attrmap/attrmap.go
Normal file
58
internal/ui/rich/parser/attrmap/attrmap.go
Normal 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
|
||||
}
|
203
internal/ui/rich/parser/hl/hl.go
Normal file
203
internal/ui/rich/parser/hl/hl.go
Normal 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, " ")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue