diff --git a/go.mod b/go.mod index 0076715..6920eb1 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,11 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200630 require ( github.com/Xuanwo/go-locale v0.2.0 github.com/alecthomas/chroma v0.7.3 - github.com/diamondburned/cchat v0.0.39 - github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846 - github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288 - github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 + github.com/diamondburned/cchat v0.0.40 + github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03 + github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 + github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516 github.com/goodsign/monday v1.0.0 - github.com/google/btree v1.0.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279 diff --git a/go.sum b/go.sum index 39b0196..5ef7aad 100644 --- a/go.sum +++ b/go.sum @@ -21,11 +21,14 @@ 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 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= 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 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= 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 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= 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= @@ -39,48 +42,20 @@ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9 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= +github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSkYxdRRzM8QUAuCswfLmaiBTd2UIU+54= +github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs= github.com/diamondburned/arikawa v0.9.5 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBKOCJpCI= github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= -github.com/diamondburned/cchat v0.0.28 h1:+1VnltW0rl8/NZTUP+x89jVhi3YTTR+e6iLprZ7HcwM= -github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= -github.com/diamondburned/cchat v0.0.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfVaVoFk= -github.com/diamondburned/cchat v0.0.31/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= -github.com/diamondburned/cchat v0.0.32 h1:nLiD4sL9+DLBnvNb9XLidd5peRzTgM9lWcqRsUmm474= -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 v0.0.37 h1:yGz9yls5Lb/vLkU/DU53GjC80WOqoRe229DXsu5mtaY= -github.com/diamondburned/cchat v0.0.37/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= -github.com/diamondburned/cchat v0.0.39 h1:Hxd7swmAIECm0MBd5wb1IFvreChwDFwnAshqgAstWGA= -github.com/diamondburned/cchat v0.0.39/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-discord v0.0.0-20200629023132-0a66a65fcf27 h1:MAWs83jAZKxkU04RAoaewc1xUrLKlB6F5X1ViFRkWdw= -github.com/diamondburned/cchat-discord v0.0.0-20200629023132-0a66a65fcf27/go.mod h1:af673uNyL2NSjYqJ54DotPV/iNY+OvBcQfDqHefbap4= -github.com/diamondburned/cchat-discord v0.0.0-20200630031444-53111f3186b3 h1:NDh2osOfPBtiNzz8ro8P7vN4f91uuVMMBJ8+Q2s/EQo= -github.com/diamondburned/cchat-discord v0.0.0-20200630031444-53111f3186b3/go.mod h1:hj6qS/5TOiIxwWyFMts51ILzY9M3GKbXT31hjVbr9gM= -github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846 h1:LLgI89H9A4ZVf8U7bHmOSkG12b8B6a9dwo32Bq7+0AI= -github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846/go.mod h1:lLWRY7l6cC3bI2ge+Vc7pEhgbIL8XYCeXt+h1PWA/1k= -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/cchat-mock v0.0.0-20200630025821-605d61d89288 h1:ApNV7DUyj+LG5lUxtTv4yvQZcprsDgZji0iiD+LYJmM= -github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288/go.mod h1:Tu+8b1iz9NGeQb2jmndXn+dQ9zBUa8a8ktK9hL5aaxw= -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/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I= +github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03 h1:F5TL7GPRU/D4ldVkS0haY3SiHPtf1Kby/4nbYpm//MQ= +github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03/go.mod h1:p0X6QUH0mxK8yEW0+a4QA77ClAmoxz8CvgbnobMtWQA= +github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 h1:xr07/2cwINyrMqh92pQQJVDfQqG0u6gHAK+ZcGfpSew= +github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3/go.mod h1:SRu3OOeggELFr2Wd3/+SpYV1eNcvSk2LBhM70NOZSG8= github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI= github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= -github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0= -github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ= -github.com/diamondburned/ningen v0.1.0 h1:cTnRNrN0g2Wr/kgjLLpa3pqlbEd6JPNa1yGDer8uV4U= -github.com/diamondburned/ningen v0.1.0/go.mod h1:1vi8mlBlM2xjJy+IugU51q+IMgyNXggS4Xv3SPFd2Q4= -github.com/diamondburned/ningen v0.1.1-0.20200621003851-e5908c53bf21 h1:luSZOjnwoHCbuw5mG3muuDSOUv5YCbFbbjw17dKZ1Ik= -github.com/diamondburned/ningen v0.1.1-0.20200621003851-e5908c53bf21/go.mod h1:1vi8mlBlM2xjJy+IugU51q+IMgyNXggS4Xv3SPFd2Q4= +github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516 h1:6j4oZahbNdVhSEInRfeYbgDpx1FXDfJy6CcUVyWOuVY= +github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I= 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= @@ -125,8 +100,6 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -146,15 +119,21 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 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= @@ -166,6 +145,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 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 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= @@ -262,12 +242,15 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w 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 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= 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= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= diff --git a/internal/gts/gts.go b/internal/gts/gts.go index 6cce724..17855f3 100644 --- a/internal/gts/gts.go +++ b/internal/gts/gts.go @@ -1,7 +1,6 @@ package gts import ( - "context" "os" "time" @@ -159,93 +158,21 @@ func ExecSync(fn func()) <-chan struct{} { return ch } +// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk +// main loop. +func AfterFunc(d time.Duration, f func()) (stop func()) { + h, err := glib.TimeoutAdd( + uint(d.Milliseconds()), + func() bool { f(); return true }, + ) + if err != nil { + panic(err) + } + + return func() { glib.SourceRemove(h) } +} + func EventIsRightClick(ev *gdk.Event) bool { keyev := gdk.EventButtonNewFromEvent(ev) return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY } - -// Reuser is an interface for structs that inherit Reusable. -type Reuser interface { - Context() context.Context - Acquire() int64 - Validate(int64) bool -} - -type AsyncUser = func(context.Context) (interface{}, error) - -// AsyncUse is a handler for structs that implement the Reuser primitive. The -// passed in function will be called asynchronously, but swap will be called in -// the Gtk main thread. -func AsyncUse(r Reuser, swap func(interface{}), fn AsyncUser) { - // Acquire an ID. - id := r.Acquire() - ctx := r.Context() - - Async(func() (func(), error) { - // Run the callback asynchronously. - v, err := fn(ctx) - if err != nil { - return nil, err - } - - return func() { - // Validate the ID. Cancel if it's invalid. - if !r.Validate(id) { - log.Println("Async function value dropped for reusable primitive.") - return - } - - // Update the resource. - swap(v) - }, nil - }) -} - -// Reusable is the synchronization primitive to provide a method for -// asynchronous cancellation and reusability. -// -// It works by copying the ID (time) for each asynchronous operation. The -// operation then completes, and the ID is then compared again before being -// used. It provides a cancellation abstraction around the Gtk main thread. -// -// This struct is not thread-safe, as it relies on the Gtk main thread -// synchronization. -type Reusable struct { - time int64 // creation time, used as ID - ctx context.Context - cancel func() -} - -func NewReusable() *Reusable { - r := &Reusable{} - r.Invalidate() - return r -} - -// Invalidate generates a new ID for the primitive, which would render -// asynchronously updating elements invalid. -func (r *Reusable) Invalidate() { - // Cancel the old context. - if r.cancel != nil { - r.cancel() - } - - // Reset. - r.time = time.Now().UnixNano() - r.ctx, r.cancel = context.WithCancel(context.Background()) -} - -// Context returns the reusable's cancellable context. It never returns nil. -func (r *Reusable) Context() context.Context { - return r.ctx -} - -// Reusable checks the acquired ID against the current one. -func (r *Reusable) Validate(acquired int64) (valid bool) { - return r.time == acquired -} - -// Acquire lends the ID to be given to Reusable() after finishing. -func (r *Reusable) Acquire() int64 { - return r.time -} diff --git a/internal/gts/httputil/image.go b/internal/gts/httputil/image.go index bd7bba1..a412bae 100644 --- a/internal/gts/httputil/image.go +++ b/internal/gts/httputil/image.go @@ -57,7 +57,7 @@ func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...img } // Add a processor to resize. - procs = imgutil.Prepend(imgutil.Resize(w, h), procs) + procs = append(procs, imgutil.Resize(w, h)) ctx, cancel := context.WithCancel(context.Background()) connectDestroyer(img, cancel) diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go index 280c57a..40b6bb3 100644 --- a/internal/ui/messages/container/container.go +++ b/internal/ui/messages/container/container.go @@ -59,8 +59,9 @@ type Controller interface { BindMenu(GridMessage) // Bottomed returns whether or not the message scroller is at the bottom. Bottomed() bool - // ScrollToBottom scrolls the message view to the bottom. - // ScrollToBottom() + // AuthorEvent is called on message create/update. This is used to update + // the typer state. + AuthorEvent(a cchat.MessageAuthor) } // Constructor is an interface for making custom message implementations which diff --git a/internal/ui/messages/container/grid.go b/internal/ui/messages/container/grid.go index 0c63be0..000f8c6 100644 --- a/internal/ui/messages/container/grid.go +++ b/internal/ui/messages/container/grid.go @@ -26,7 +26,6 @@ func NewGridStore(constr Constructor, ctrl Controller) *GridStore { grid.SetRowSpacing(5) grid.SetMarginStart(5) grid.SetMarginEnd(5) - grid.SetMarginBottom(5) grid.Show() primitives.AddClass(grid, "message-grid") @@ -230,6 +229,9 @@ func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessa } func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) { + // Call the event handler last. + defer c.Controller.AuthorEvent(msg.Author()) + // Attempt to update before insertion (aka upsert). if msgc := c.Message(msg); msgc != nil { msgc.UpdateAuthor(msg.Author()) @@ -253,6 +255,9 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) { } func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) { + // Call the event handler last. + defer c.Controller.AuthorEvent(msg.Author()) + if msgc := c.Message(msg); msgc != nil { if author := msg.Author(); author != nil { msgc.UpdateAuthor(author) diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index bb0fd8d..157eec6 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -23,19 +23,31 @@ type InputView struct { } var textCSS = primitives.PrepareCSS(` - textview, textview * { + .message-input, .message-input * { background-color: transparent; } + + .message-input * { + background-color: @theme_base_color; + border: 1px solid alpha(@theme_fg_color, 0.2); + border-radius: 4px; + transition: linear 50ms border-color; + } + + .message-input:focus * { + border-color: @theme_selected_bg_color; + } `) func NewView(ctrl Controller) *InputView { text, _ := gtk.TextViewNew() text.SetSensitive(false) text.SetWrapMode(gtk.WRAP_WORD_CHAR) - text.SetProperty("top-margin", inputmargin) - text.SetProperty("left-margin", inputmargin) - text.SetProperty("right-margin", inputmargin) - text.SetProperty("bottom-margin", inputmargin) + text.SetVAlign(gtk.ALIGN_START) + text.SetProperty("top-margin", 4) + text.SetProperty("bottom-margin", 4) + text.SetProperty("left-margin", 8) + text.SetProperty("right-margin", 8) text.Show() primitives.AddClass(text, "message-input") @@ -79,11 +91,14 @@ type Field struct { editingID string // never empty } -const inputmargin = username.VMargin +var scrollinputCSS = primitives.PrepareCSS(` + .scrolled-input { + margin: 5px; + } +`) func NewField(text *gtk.TextView, ctrl Controller) *Field { username := username.NewContainer() - username.SetVAlign(gtk.ALIGN_END) username.Show() buf, _ := text.GetBuffer() @@ -91,6 +106,9 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field { sw := scrollinput.NewV(text, 150) sw.Show() + primitives.AddClass(sw, "scrolled-input") + primitives.AttachCSS(sw, scrollinputCSS) + box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) box.PackStart(username, false, false, 0) box.PackStart(sw, true, true, 0) diff --git a/internal/ui/messages/input/username.go b/internal/ui/messages/input/username.go deleted file mode 100644 index f2a973d..0000000 --- a/internal/ui/messages/input/username.go +++ /dev/null @@ -1,141 +0,0 @@ -package input - -import ( - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-gtk/internal/gts" - "github.com/diamondburned/cchat-gtk/internal/ui/config" - "github.com/diamondburned/cchat-gtk/internal/ui/rich" - "github.com/diamondburned/cchat/text" - "github.com/diamondburned/imgutil" - "github.com/gotk3/gotk3/gtk" -) - -const AvatarSize = 24 - -var showUser = true -var currentRevealer = func(bool) {} // noop by default - -func init() { - // Bind this revealer in settings. - config.AppearanceAdd("Show Username in Input", config.Switch( - &showUser, - func(b bool) { currentRevealer(b) }, - )) -} - -type usernameContainer struct { - *gtk.Revealer - main *gtk.Box - avatar *rich.Icon - label *rich.Label -} - -var ( - _ cchat.LabelContainer = (*usernameContainer)(nil) - _ cchat.IconContainer = (*usernameContainer)(nil) -) - -func newUsernameContainer() *usernameContainer { - avatar := rich.NewIcon(AvatarSize, imgutil.Round(true)) - avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize) - avatar.Show() - - label := rich.NewLabel(text.Rich{}) - label.SetMaxWidthChars(35) - label.Show() - - box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5) - box.PackStart(avatar, false, false, 0) - box.PackStart(label, false, false, 0) - box.SetMarginStart(10) - box.SetMarginEnd(10) - box.SetMarginTop(inputmargin) - box.SetMarginBottom(inputmargin) - box.SetVAlign(gtk.ALIGN_START) - box.Show() - - rev, _ := gtk.RevealerNew() - rev.SetRevealChild(false) - rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) - rev.SetTransitionDuration(50) - rev.Add(box) - - // Bind the current global revealer to this revealer for settings. This - // operation should be thread-safe, as everything is being done in the main - // thread. - currentRevealer = rev.SetRevealChild - - return &usernameContainer{ - Revealer: rev, - main: box, - avatar: avatar, - label: label, - } -} - -func (u *usernameContainer) SetRevealChild(reveal bool) { - // Only reveal if showUser is true. - u.Revealer.SetRevealChild(reveal && showUser) -} - -// shouldReveal returns whether or not the container should reveal. -func (u *usernameContainer) shouldReveal() bool { - return !u.label.GetLabel().Empty() && showUser -} - -func (u *usernameContainer) Reset() { - u.SetRevealChild(false) - u.avatar.Reset() - u.label.Reset() -} - -// Update is not thread-safe. -func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMessageSender) { - // Set the fallback username. - u.label.SetLabelUnsafe(session.Name()) - // Reveal the name if it's not empty. - u.SetRevealChild(u.shouldReveal()) - - // Does sender (aka Server) implement ServerNickname? If yes, use it. - if nicknamer, ok := sender.(cchat.ServerNickname); ok { - u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname") - } - - // Does session implement an icon? Update if yes. - if iconer, ok := session.(cchat.Icon); ok { - u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL") - } -} - -// GetLabel is not thread-safe. -func (u *usernameContainer) GetLabel() text.Rich { - return u.label.GetLabel() -} - -// SetLabel is thread-safe. -func (u *usernameContainer) SetLabel(content text.Rich) { - gts.ExecAsync(func() { - u.label.SetLabelUnsafe(content) - - // Reveal if the name is not empty. - u.SetRevealChild(u.shouldReveal()) - }) -} - -// SetIcon is thread-safe. -func (u *usernameContainer) SetIcon(url string) { - gts.ExecAsync(func() { - u.avatar.SetIconUnsafe(url) - - // Reveal if the icon URL is not empty. We don't touch anything if the - // URL is empty, as the name might not be. - if url != "" { - u.SetRevealChild(true) - } - }) -} - -// GetIconURL is not thread-safe. -func (u *usernameContainer) GetIconURL() string { - return u.avatar.URL() -} diff --git a/internal/ui/messages/input/username/username.go b/internal/ui/messages/input/username/username.go index de54849..89ca69b 100644 --- a/internal/ui/messages/input/username/username.go +++ b/internal/ui/messages/input/username/username.go @@ -4,6 +4,7 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/ui/config" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat/text" "github.com/diamondburned/imgutil" @@ -11,7 +12,6 @@ import ( ) const AvatarSize = 24 -const VMargin = 4 var showUser = true var currentRevealer = func(bool) {} // noop by default @@ -36,6 +36,12 @@ var ( _ cchat.IconContainer = (*Container)(nil) ) +var usernameCSS = primitives.PrepareCSS(` + .username-view { + margin: 8px 10px; + } +`) + func NewContainer() *Container { avatar := rich.NewIcon(AvatarSize, imgutil.Round(true)) avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize) @@ -48,13 +54,12 @@ func NewContainer() *Container { box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5) box.PackStart(avatar, false, false, 0) box.PackStart(label, false, false, 0) - box.SetMarginStart(10) - box.SetMarginEnd(10) - box.SetMarginTop(VMargin) - box.SetMarginBottom(VMargin) box.SetVAlign(gtk.ALIGN_START) box.Show() + primitives.AddClass(box, "username-view") + primitives.AttachCSS(box, usernameCSS) + rev, _ := gtk.RevealerNew() rev.SetRevealChild(false) rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) diff --git a/internal/ui/messages/typing/state.go b/internal/ui/messages/typing/state.go new file mode 100644 index 0000000..c817f4f --- /dev/null +++ b/internal/ui/messages/typing/state.go @@ -0,0 +1,145 @@ +package typing + +import ( + "sort" + "time" + + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-gtk/internal/gts" + "github.com/pkg/errors" +) + +type State struct { + // states + typers []cchat.Typer + timeout time.Duration + canceler func() + invalidated bool + + // consts + changed func(s *State, empty bool) + stopper func() // stops the event loop, not used atm +} + +var _ cchat.TypingIndicator = (*State)(nil) + +func NewState(changed func(s *State, empty bool)) *State { + s := &State{changed: changed} + s.stopper = gts.AfterFunc(time.Second/2, s.loop) + return s +} + +func (s *State) reset() { + if s.canceler != nil { + s.canceler() + s.canceler = nil + } + + s.timeout = 0 + s.typers = nil + s.invalidated = false +} + +// Subscribe is thread-safe. +func (s *State) Subscribe(indicator cchat.ServerMessageTypingIndicator) { + gts.Async(func() (func(), error) { + c, err := indicator.TypingSubscribe(s) + if err != nil { + return nil, errors.Wrap(err, "Failed to subscribe to typing indicator") + } + + return func() { + s.canceler = c + s.timeout = indicator.TypingTimeout() + }, nil + }) +} + +// loop runs a single iteration of the event loop. This function is not +// thread-safe. +func (s *State) loop() { + // Filter out any expired typers. + t, ok := filterTypers(s.typers, s.timeout) + if ok { + s.invalidated = true + s.typers = t + } + + // Call the event handler if things are invalidated. + if s.invalidated { + s.changed(s, len(s.typers) == 0) + s.invalidated = false + } +} + +// invalidate sorts and invalidates the state. +func (s *State) invalidate() { + // Sort the list of typers again. + sort.Slice(s.typers, func(i, j int) bool { + return s.typers[i].Time().Before(s.typers[j].Time()) + }) + + s.invalidated = true +} + +// AddTyper is thread-safe. +func (s *State) AddTyper(typer cchat.Typer) { + gts.ExecAsync(func() { + defer s.invalidate() + + // If the typer already exists, then pop them to the start of the list. + for i, t := range s.typers { + if t.ID() == typer.ID() { + s.typers[i] = t + return + } + } + + s.typers = append(s.typers, typer) + }) +} + +// RemoveTyper is thread-safe. +func (s *State) RemoveTyper(typerID string) { + gts.ExecAsync(func() { s.removeTyper(typerID) }) +} + +func (s *State) removeTyper(typerID string) { + defer s.invalidate() + + for i, t := range s.typers { + if t.ID() == typerID { + // Remove the quick way. Sort will take care of ordering. + l := len(s.typers) - 1 + s.typers[i] = s.typers[l] + s.typers[l] = nil + s.typers = s.typers[:l] + + return + } + } +} + +func filterTypers(typers []cchat.Typer, timeout time.Duration) ([]cchat.Typer, bool) { + // Fast path. + if len(typers) == 0 || timeout == 0 { + return nil, false + } + + var now = time.Now() + var cut int + + for _, t := range typers { + if now.Sub(t.Time()) < timeout { + typers[cut] = t + cut++ + } + } + + for i := cut; i < len(typers); i++ { + typers[i] = nil + } + + var changed = cut != len(typers) + return typers[:cut], changed +} diff --git a/internal/ui/messages/typing/typing.go b/internal/ui/messages/typing/typing.go index 3427af4..1dafd9f 100644 --- a/internal/ui/messages/typing/typing.go +++ b/internal/ui/messages/typing/typing.go @@ -1,29 +1,19 @@ package typing import ( + "strings" + "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser" "github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/pango" ) -type State struct { - typers []cchat.Typer -} - -func NewState() *State { - return &State{} -} - -func (s *State) Empty() bool { - // return len(s.typers) == 0 - return false -} - var typingIndicatorCSS = primitives.PrepareCSS(` .typing-indicator { - border-radius: 8px 8px 0 0; + margin: 0 6px; + border-radius: 6px 6px 0 0; color: alpha(@theme_fg_color, 0.8); background-color: @theme_base_color; } @@ -35,42 +25,94 @@ var smallfonts = primitives.PrepareCSS(` type Container struct { *gtk.Revealer - empty bool // && state.Empty() - State *State + state *State } -const placeholder = "Bruh moment..." - func New() *Container { d := NewDots() d.Show() - l, _ := gtk.LabelNew(placeholder) + l, _ := gtk.LabelNew("") l.SetXAlign(0) l.SetEllipsize(pango.ELLIPSIZE_END) l.Show() primitives.AttachCSS(l, smallfonts) b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) - b.PackStart(d, false, false, username.VMargin) + b.PackStart(d, false, false, 4) b.PackStart(l, true, true, 0) - b.SetMarginStart(username.VMargin * 2) - b.SetMarginEnd(username.VMargin * 2) b.Show() r, _ := gtk.RevealerNew() r.SetTransitionDuration(50) r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE) - r.SetRevealChild(true) + r.SetRevealChild(false) r.Add(b) - state := NewState() - primitives.AddClass(b, "typing-indicator") primitives.AttachCSS(b, typingIndicatorCSS) + state := NewState(func(s *State, empty bool) { + r.SetRevealChild(!empty) + l.SetMarkup(render(s.typers)) + }) + + // On label destroy, stop the state loop as well. + l.Connect("destroy", state.stopper) + return &Container{ Revealer: r, - State: state, + state: state, } } + +func (c *Container) Reset() { + c.state.reset() + c.SetRevealChild(false) +} + +func (c *Container) RemoveAuthor(author cchat.MessageAuthor) { + c.state.removeTyper(author.ID()) +} + +func (c *Container) TrySubscribe(svmsg cchat.ServerMessage) bool { + ti, ok := svmsg.(cchat.ServerMessageTypingIndicator) + if !ok { + return false + } + + c.state.Subscribe(ti) + return true +} + +func render(typers []cchat.Typer) string { + // fast path + if len(typers) == 0 { + return "" + } + + var builder strings.Builder + + for i, typer := range typers { + builder.WriteString("") + builder.WriteString(parser.RenderMarkup(typer.Name())) + builder.WriteString("") + + switch i { + case len(typers) - 2: + builder.WriteString(" and ") + case len(typers) - 1: + // Write nothing if this is the last item. + default: + builder.WriteString(", ") + } + } + + if len(typers) == 1 { + builder.WriteString(" is typing.") + } else { + builder.WriteString(" are typing.") + } + + return builder.String() +} diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index 99d4719..c53d85b 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -14,6 +14,7 @@ import ( "github.com/diamondburned/cchat-gtk/internal/ui/messages/input" "github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface" "github.com/diamondburned/cchat-gtk/internal/ui/messages/typing" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll" "github.com/diamondburned/cchat-gtk/internal/ui/service/menu" "github.com/gotk3/gotk3/gtk" @@ -81,6 +82,8 @@ func NewView() *View { view.Box.PackStart(view.InputView, false, false, 0) view.Box.Show() + primitives.AddClass(view.Box, "message-view") + // placeholder logo logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256()) logo.Show() @@ -111,6 +114,7 @@ func (v *View) Bottomed() bool { return v.Scroller.Bottomed } func (v *View) Reset() { v.state.Reset() // Reset the state variables. + v.Typing.Reset() // Reset the typing state. v.FaceView.Reset() // Switch back to the main screen. v.InputView.Reset() // Reset the input. v.Container.Reset() // Clean all messages. @@ -164,6 +168,8 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func // Set the cancel handler. v.state.setcurrent(s) + // Try setting the typing indicator if available. + v.Typing.TrySubscribe(server) }, nil }) } @@ -185,6 +191,11 @@ func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) { } } +// AuthorEvent should be called on message create/update/delete. +func (v *View) AuthorEvent(author cchat.MessageAuthor) { + v.Typing.RemoveAuthor(author) +} + // LatestMessageFrom returns the last message ID with that author. func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) { return v.Container.LatestMessageFrom(userID) diff --git a/internal/ui/rich/async.go b/internal/ui/rich/async.go new file mode 100644 index 0000000..6ff3ce3 --- /dev/null +++ b/internal/ui/rich/async.go @@ -0,0 +1,108 @@ +package rich + +import ( + "context" + "log" + "reflect" + "time" + + "github.com/diamondburned/cchat-gtk/internal/gts" +) + +// Reuser is an interface for structs that inherit Reusable. +type Reuser interface { + Context() context.Context + Acquire() int64 + Validate(int64) bool + SwapResource(v interface{}, cancel func()) +} + +type AsyncUser = func(context.Context) (interface{}, func(), error) + +// AsyncUse is a handler for structs that implement the Reuser primitive. The +// passed in function will be called asynchronously, but swap will be called in +// the Gtk main thread. +func AsyncUse(r Reuser, fn AsyncUser) { + // Acquire an ID. + id := r.Acquire() + ctx := r.Context() + + gts.Async(func() (func(), error) { + // Run the callback asynchronously. + v, cancel, err := fn(ctx) + if err != nil { + return nil, err + } + + return func() { + // Validate the ID. Cancel if it's invalid. + if !r.Validate(id) { + log.Println("Async function value dropped for reusable primitive.") + return + } + + // Update the resource. + r.SwapResource(v, cancel) + }, nil + }) +} + +// Reusable is the synchronization primitive to provide a method for +// asynchronous cancellation and reusability. +// +// It works by copying the ID (time) for each asynchronous operation. The +// operation then completes, and the ID is then compared again before being +// used. It provides a cancellation abstraction around the Gtk main thread. +// +// This struct is not thread-safe, as it relies on the Gtk main thread +// synchronization. +type Reusable struct { + time int64 // creation time, used as ID + ctx context.Context + cancel func() + + swapfn reflect.Value // reflect fn + arg1type reflect.Type +} + +var _ Reuser = (*Reusable)(nil) + +func NewReusable(swapperFn interface{}) *Reusable { + r := Reusable{} + r.swapfn = reflect.ValueOf(swapperFn) + r.arg1type = r.swapfn.Type().In(0) + r.Invalidate() + return &r +} + +// Invalidate generates a new ID for the primitive, which would render +// asynchronously updating elements invalid. +func (r *Reusable) Invalidate() { + // Cancel the old context. + if r.cancel != nil { + r.cancel() + } + + // Reset. + r.time = time.Now().UnixNano() + r.ctx, r.cancel = context.WithCancel(context.Background()) +} + +// Context returns the reusable's cancellable context. It never returns nil. +func (r *Reusable) Context() context.Context { + return r.ctx +} + +// Reusable checks the acquired ID against the current one. +func (r *Reusable) Validate(acquired int64) (valid bool) { + return r.time == acquired +} + +// Acquire lends the ID to be given to Reusable() after finishing. +func (r *Reusable) Acquire() int64 { + return r.time +} + +func (r *Reusable) SwapResource(v interface{}, cancel func()) { + r.swapfn.Call([]reflect.Value{reflect.ValueOf(v)}) +} diff --git a/internal/ui/rich/image.go b/internal/ui/rich/image.go index aa1afa8..3eba07d 100644 --- a/internal/ui/rich/image.go +++ b/internal/ui/rich/image.go @@ -16,12 +16,11 @@ type IconerFn = func(context.Context, cchat.IconContainer) (func(), error) type Icon struct { *gtk.Revealer - Image *gtk.Image - resizer imgutil.Processor - procs []imgutil.Processor - size int + Image *gtk.Image + procs []imgutil.Processor + size int - r gts.Reusable + r *Reusable // state url string @@ -50,10 +49,11 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon { Revealer: rev, Image: img, procs: procs, - - r: *gts.NewReusable(), } i.SetSize(sizepx) + i.r = NewReusable(func(ni *nullIcon) { + i.SetIconUnsafe(ni.url) + }) return i } @@ -61,6 +61,7 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon { // Reset wipes the state to be just after construction. func (i *Icon) Reset() { i.url = "" + i.r.Invalidate() // invalidate async fetching images i.Revealer.SetRevealChild(false) i.Image.SetFromPixbuf(nil) // destroy old pb } @@ -99,7 +100,6 @@ func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) { func (i *Icon) SetSize(szpx int) { i.size = szpx i.Image.SetSizeRequest(szpx, szpx) - i.resizer = imgutil.Resize(szpx, szpx) } // AddProcessors is not thread-safe. @@ -112,16 +112,11 @@ func (i *Icon) SetIcon(url string) { gts.ExecAsync(func() { i.SetIconUnsafe(url) }) } -func (i *Icon) swapResource(v interface{}) { - i.SetIconUnsafe(v.(*nullIcon).url) -} - func (i *Icon) AsyncSetIconer(iconer cchat.Icon, wrap string) { - gts.AsyncUse(&i.r, i.swapResource, func(ctx context.Context) (interface{}, error) { + AsyncUse(i.r, func(ctx context.Context) (interface{}, func(), error) { ni := &nullIcon{} f, err := iconer.Icon(ctx, ni) - ni.cancel = f - return ni, err + return ni, f, err }) } @@ -133,7 +128,11 @@ func (i *Icon) SetIconUnsafe(url string) { } func (i *Icon) updateAsync() { - httputil.AsyncImage(i.Image, i.url, imgutil.Prepend(i.resizer, i.procs)...) + if i.size > 0 { + httputil.AsyncImageSized(i.Image, i.url, i.size, i.size, i.procs...) + } else { + httputil.AsyncImage(i.Image, i.url, i.procs...) + } } type ToggleButtonImage struct { diff --git a/internal/ui/rich/label.go b/internal/ui/rich/label.go index 26790cb..100ae9a 100644 --- a/internal/ui/rich/label.go +++ b/internal/ui/rich/label.go @@ -30,7 +30,7 @@ type Label struct { current text.Rich // Reusable primitive. - r gts.Reusable + r *Reusable } var ( @@ -44,12 +44,17 @@ func NewLabel(content text.Rich) *Label { label.SetXAlign(0) // left align label.SetEllipsize(pango.ELLIPSIZE_END) - return &Label{ + l := &Label{ Label: *label, current: content, - // reusable primitive, take reference - r: *gts.NewReusable(), } + + // reusable primitive + l.r = NewReusable(func(nl *nullLabel) { + l.SetLabelUnsafe(nl.Rich) + }) + + return l } // Reset wipes the state to be just after construction. @@ -59,17 +64,11 @@ func (l *Label) Reset() { l.Label.SetText("") } -// swapResource is reserved for internal use only. -func (l *Label) swapResource(v interface{}) { - l.SetLabelUnsafe(v.(*nullLabel).Rich) -} - func (l *Label) AsyncSetLabel(fn LabelerFn, info string) { - gts.AsyncUse(&l.r, l.swapResource, func(ctx context.Context) (interface{}, error) { + AsyncUse(l.r, func(ctx context.Context) (interface{}, func(), error) { nl := &nullLabel{} f, err := fn(ctx, nl) - nl.cancel = f - return nl, err + return nl, f, err }) } diff --git a/internal/ui/rich/rich.go b/internal/ui/rich/rich.go index e9c4e8b..ec85151 100644 --- a/internal/ui/rich/rich.go +++ b/internal/ui/rich/rich.go @@ -17,15 +17,13 @@ func MakeRed(content text.Rich) string { // used for grabbing text without changing state type nullLabel struct { text.Rich - cancel func() } func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t } // used for grabbing url without changing state type nullIcon struct { - url string - cancel func() + url string } func (i *nullIcon) SetIcon(url string) { i.url = url } diff --git a/internal/ui/service/service.go b/internal/ui/service/service.go index 0d624aa..0591743 100644 --- a/internal/ui/service/service.go +++ b/internal/ui/service/service.go @@ -21,11 +21,18 @@ type View struct { Services []*Container } +var servicesCSS = primitives.PrepareCSS(` + .services { + background-color: @theme_base_color; + } +`) + func NewView() *View { box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) box.Show() primitives.AddClass(box, "services") + primitives.AttachCSS(box, servicesCSS) sw, _ := gtk.ScrolledWindowNew(nil, nil) sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) diff --git a/internal/ui/service/session/commander/commander.go b/internal/ui/service/session/commander/commander.go index 778e17a..86d5bbb 100644 --- a/internal/ui/service/session/commander/commander.go +++ b/internal/ui/service/session/commander/commander.go @@ -24,6 +24,10 @@ var monospace = primitives.PrepareCSS(` } `) +var commandPadding = primitives.PrepareCSS(` + * { padding: 8px 12px; } +`) + type Session struct { *gtk.Box @@ -101,6 +105,8 @@ func NewSession(cmder cchat.Commander, buf *Buffer) *Session { primitives.AddClass(b, "commander") primitives.AddClass(view, "command-buffer") primitives.AddClass(input, "command-input") + primitives.AttachCSS(view, commandPadding) + primitives.AttachCSS(input, commandPadding) return session } diff --git a/internal/ui/style.css b/internal/ui/style.css index f126fd6..c9aa4ed 100644 --- a/internal/ui/style.css +++ b/internal/ui/style.css @@ -1,17 +1,10 @@ +/* Make CSS more consistent across themes */ headerbar { padding: 0; } +/* Consistent design */ .services button { border-radius: 0; } -.message-content, .message-content text { - background: none; - box-shadow: none; - border: none; -} +popover > box { margin: 6px; } -popover > box { - margin: 6px; -} - -.command-buffer, .command-input { - padding: 8px 12px; -} +/* Hack to fix the input bar being high in Adwaita */ +.input-field * { min-height: 0; }