~f4814n/frost

79d0baa592bb0712efab8f51d8332b24f01b175d — Fabian Geiselhart 10 months ago 5651cdd
UsersView: Implement Profile Pictures
M cmd/frost/debug.go => cmd/frost/debug.go +1 -1
@@ 3,8 3,8 @@
package main

import (
	_ "net/http/pprof"
	"net/http"
	_ "net/http/pprof"
)

func init() {

A cmd/frost/events.go => cmd/frost/events.go +30 -0
@@ 0,0 1,30 @@
package main

import (
	"git.sr.ht/~f4814n/matrix"
	"net/url"
)

// Received by: App
// Sent by: Everyting
type CacheFetchEvent struct {
	URI      *url.URL
	File     chan matrix.File
	Error    chan error
	Progress chan ProgressPoint
}

type ProgressPoint struct {
	BytesDownloaded, BytesTotal int64
	Percent                     float32
}

// Received by: App
// Sent by: Everyting
type CacheFetchThumbnailEvent struct {
	URI           *url.URL
	Method        matrix.ThumbnailMethod
	Height, Width int
	Thumbnail     chan matrix.Thumbnail
	Error         chan error
}

M cmd/frost/list_view.go => cmd/frost/list_view.go +10 -29
@@ 9,7 9,6 @@ import (
	"golang.org/x/exp/shiny/materialdesign/icons"

	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/text"
	"gioui.org/unit"


@@ 38,7 37,7 @@ func NewListView(cli *matrix.Client) *ListView {
		matrixEvents: make(chan matrix.Event, 100),
		roomList:     NewRoomList(log),
		menuButton:   new(widget.Clickable),
		log: log,
		log:          log,
	}
}



@@ 81,7 80,7 @@ func (l *ListView) update(gtx Gtx) {
			if e.Type == gesture.TypeClick {
				l.tx <- LoadViewEvent{
					Position: Middle,
					View: NewRoomView(l.cli, room.room),
					View:     NewRoomView(l.cli, room.room),
				}
			}
		}


@@ 106,7 105,7 @@ func (l *ListView) Stop() {
type RoomList struct {
	list  *layout.List
	rooms []roomListElement
	log *logrus.Entry
	log   *logrus.Entry
}

func NewRoomList(log *logrus.Entry) *RoomList {


@@ 164,40 163,22 @@ func NewRoomListElement(room matrix.Room, event matrix.Event) roomListElement {
}

func (w *roomListElement) Layout(gtx Gtx) Dims {
	in := layout.Inset{
		Left:  unit.Dp(16),
		Right: unit.Dp(16),
	}

	return in.Layout(gtx, func(gtx Gtx) Dims {
		in := layout.Inset{Top: unit.Dp(16), Bottom: unit.Dp(16)}
		dims := in.Layout(gtx, func(gtx Gtx) Dims {
			return centerRowOpts().Layout(gtx,
				layout.Rigid(w.layoutIcon),
				layout.Rigid(func(gtx Gtx) Dims {
					return column().Layout(gtx,
						layout.Rigid(w.layoutUpper),
						layout.Rigid(w.layoutLastEvent),
					)
				}),
			)
		})
		pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
		w.click.Add(gtx.Ops)
		return dims
	})
	return IconListElement{Click: w.click, Icon: w.layoutIcon, Content: func(gtx Gtx) Dims {
		return column().Layout(gtx,
			layout.Rigid(w.layoutUpper),
			layout.Rigid(w.layoutLastEvent),
		)
	}}.Layout(gtx)
}

func (w *roomListElement) layoutIcon(gtx Gtx) Dims {
	in := layout.Inset{Right: unit.Dp(12)}

	var initial string
	for _, c := range w.room.Displayname() {
		initial = string(unicode.ToUpper(c))
		break
	}

	return in.Layout(gtx, InitialSign{Initial: initial}.Layout)
	return InitialSign{Initial: initial}.Layout(gtx)
}

func (w *roomListElement) layoutUpper(gtx Gtx) Dims {

M cmd/frost/log.go => cmd/frost/log.go +1 -1
@@ 1,8 1,8 @@
package main

import (
	"github.com/sirupsen/logrus"
	"git.sr.ht/~f4814n/matrix"
	"github.com/sirupsen/logrus"
)

type MatrixLogger struct {

M cmd/frost/login_view.go => cmd/frost/login_view.go +1 -1
@@ 38,7 38,7 @@ func NewLoginView() *LoginView {
		usernameEditor: &widget.Editor{Submit: true, SingleLine: true},
		passwordEditor: &widget.Editor{Submit: true, SingleLine: true, Mask: '•'},
		loginButton:    new(widget.Clickable),
		log: logrus.WithField("component", "login_view"),
		log:            logrus.WithField("component", "login_view"),
	}
}


M cmd/frost/main.go => cmd/frost/main.go +123 -16
@@ 2,6 2,7 @@ package main

import (
	"context"
	"errors"
	"net/http"

	"gioui.org/app"


@@ 12,6 13,7 @@ import (
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~f4814n/matrix/util/cache"
	"github.com/sirupsen/logrus"
)



@@ 32,18 34,18 @@ type AppConfig struct {
}

type SessionConfig struct {
	MxID string `json:"mxid"`
	MxID     string `json:"mxid"`
	DeviceID string `json:"device_id"`
}

type App struct {
	window *app.Window
	cli *matrix.Client
	rx, tx chan Event
	view   View
	config AppConfig
	window   *app.Window
	cli      *matrix.Client
	rx, tx   chan Event
	view     View
	config   AppConfig
	platform Platform
	log *logrus.Entry
	log      *logrus.Entry
}

func newApp() *App {


@@ 53,15 55,15 @@ func newApp() *App {
		tx: make(chan Event, 10),
		cli: matrix.NewClient(matrix.ClientOpts{
			HTTPClient: http.DefaultClient,
			Backend: platform.NewBackend(),
			Logger: MatrixLogger{logrus.WithField("component", "matrix")},
			Backend:    platform.NewBackend(),
			Logger:     MatrixLogger{logrus.WithField("component", "matrix")},
		}),
		window: app.NewWindow(
			app.Size(unit.Dp(400), unit.Dp(800)),
			app.Title("Frost"),
		),
		platform: platform,
		log: logrus.WithField("component", "app"),
		log:      logrus.WithField("component", "app"),
	}
}



@@ 99,6 101,10 @@ func (a *App) run() error {
				go a.login(e)
			case InvalidationEvent:
				a.window.Invalidate()
			case CacheFetchEvent:
				go a.cacheFetchEvent(e)
			case CacheFetchThumbnailEvent:
				go a.cacheFetchThumbnailEvent(e)
			}
		}
	}


@@ 114,7 120,7 @@ func (a *App) login(event LoginStartEvent) {
	a.log.Debug("Successfully authenticated")

	a.config.Session = &SessionConfig{
		MxID: a.cli.MxID(),
		MxID:     a.cli.MxID(),
		DeviceID: a.cli.DeviceID(),
	}
	err = a.platform.FlushConfig(a.config)


@@ 125,7 131,7 @@ func (a *App) login(event LoginStartEvent) {
	a.tx <- UnloadViewEvent{Overlay}
	a.tx <- LoadViewEvent{
		Position: Left,
		View: NewListView(a.cli),
		View:     NewListView(a.cli),
	}

	matrixEvents := make(chan matrix.Event, 1000)


@@ 144,12 150,12 @@ func (a *App) initialView() {

		a.tx <- LoadViewEvent{
			Position: Left,
			View: NewListView(a.cli),
			View:     NewListView(a.cli),
		}
	} else {
		a.tx <- LoadViewEvent{
			Position: Overlay,
			View: NewLoginView(),
			View:     NewLoginView(),
		}
	}



@@ 162,7 168,7 @@ func (a *App) sync() {
		OnError: func(err error) error {
			logrus.WithFields(logrus.Fields{
				"component": "matrix",
				"error": err,
				"error":     err,
			}).Warn("Sync error")
			return nil
		},


@@ 179,8 185,109 @@ func (a *App) sync() {
	}()
}

func (a *App) cacheFetchEvent(e CacheFetchEvent) {
	log := a.log.WithFields(logrus.Fields{
		"component": "cache",
		"uri":       e.URI.String(),
	})
	log.Debug("Requested file file")

	file, err := a.cli.DownloadFile(e.URI, "")
	if err != nil {
		log.WithField("error", err).Warn("Failed to get file information")
		return
	}

	progChan, errChan := a.platform.Cache.Put(e.URI, file)

	for {
		select {
		case p := <-progChan:
			progress := ProgressPoint{
				BytesDownloaded: p,
				BytesTotal:      file.Size,
				Percent:         float32(p/file.Size) * 100,
			}
			e.Progress <- progress
		case err := <-errChan:
			if err != nil {
				log.WithField("error", err).Warn("Failed to download file")
				e.Error <- err
				return
			}

			file, err := a.platform.Cache.Get(e.URI)
			if err != nil {
				log.WithField("error", err).Warn("Failed to load file from cache")
				e.Error <- err
				return
			}

			e.File <- file
			e.Error <- nil
			e.Progress <- ProgressPoint{
				BytesDownloaded: file.Size,
				BytesTotal:      file.Size,
				Percent:         100,
			}

			log.Debug("Download complete")
			return
		}
	}
}

func (a *App) cacheFetchThumbnailEvent(e CacheFetchThumbnailEvent) {
	log := a.log.WithFields(logrus.Fields{
		"component": "cache",
		"uri":       e.URI.String(),
		"method":    e.Method,
		"height":    e.Height,
		"width":     e.Width,
	})
	log.Debug("Requested thumbnail")

	thumb, err := a.platform.Cache.GetThumbnail(e.URI, e.Method, e.Width, e.Height)

	if errors.Is(err, cache.ErrNotComplete) {
		panic("concurrent cache access")
	} else if errors.Is(err, cache.ErrCacheMiss) {
		thumb, err := a.cli.DownloadThumbnail(e.URI, e.Width, e.Height, e.Method)
		if err != nil {
			log.WithField("error", err).Warn("Failed to get image information")
			e.Error <- cache.DownloadError{Err: err}
			return
		}

		_, errChan := a.platform.Cache.PutThumbnail(e.URI, e.Method, e.Width, e.Height, thumb)
		err = <-errChan
		if err != nil {
			log.WithField("error", err).Warn("Failed to download thumbnail content")
			e.Error <- err
			return
		}

		thumb, err = a.platform.Cache.GetThumbnail(e.URI, e.Method, e.Width, e.Height)
		if err != nil {
			log.WithField("error", err).Error("Failed to load thumbnail just downloaded")
			e.Error <- err
			return
		}

		e.Thumbnail <- thumb
		e.Error <- nil
	} else if err != nil {
		log.WithField("error", err).Error("Unexpected error while loading thumbnail from cache")
		e.Error <- err
		return
	} else {
		e.Thumbnail <- thumb
		e.Error <- nil
	}
}

func main() {
	logrus.SetLevel(logrus.TraceLevel)
	logrus.SetLevel(logrus.DebugLevel)
	go func() {
		a := newApp()
		if err := a.run(); err != nil {

M cmd/frost/merge_view.go => cmd/frost/merge_view.go +5 -5
@@ 80,15 80,15 @@ func (o *MergeView) Start(rx, tx chan Event) {
			select {
			case <-o.stop:
				return
			case e := <- o.rxL:
			case e := <-o.rxL:
				o.receiveEvent(e)
			case e := <- o.rxM:
			case e := <-o.rxM:
				o.receiveEvent(e)
			case e := <- o.rxR:
			case e := <-o.rxR:
				o.receiveEvent(e)
			case e := <- o.rxO:
			case e := <-o.rxO:
				o.receiveEvent(e)
			case e := <- o.rx:
			case e := <-o.rx:
				o.receiveEvent(e)
			}
		}

M cmd/frost/platform_js.go => cmd/frost/platform_js.go +4 -1
@@ 2,16 2,19 @@ package main

import (
	"git.sr.ht/~f4814n/matrix"
	"github.com/sirupsen/logrus"
	"git.sr.ht/~f4814n/matrix/util/cache"
	backend "git.sr.ht/~f4814n/matrix/backend/memory"
	"github.com/sirupsen/logrus"
)

type Platform struct {
	log *logrus.Entry
	Cache cache.Cache
}

func InitPlatform() Platform {
	return Platform{
		Cache: new(cache.MemoryCache),
		log: logrus.WithField("component", "platform_js"),
	}
}

M cmd/frost/platform_linux.go => cmd/frost/platform_linux.go +14 -4
@@ 1,19 1,22 @@
package main

import (
	"os"
	"fmt"
	"encoding/json"
	"fmt"
	"git.sr.ht/~f4814n/matrix"
	backend "git.sr.ht/~f4814n/matrix/backend/sqlite"
	"git.sr.ht/~f4814n/matrix/util/cache"
	"os"
)

var (
	dataDir = fmt.Sprintf("%s/frost", getenv("XDG_DATA_HOME", "~/.local/share"))
	dataDir        = fmt.Sprintf("%s/frost", getenv("XDG_DATA_HOME", "~/.local/share"))
	cacheDir       = fmt.Sprintf("%s/frost", getenv("XDG_CACHE_HOME", "~/.cache"))
	configFilePath = dataDir + "/config.json"
)

type Platform struct {
	Cache cache.Cache
}

func InitPlatform() Platform {


@@ 22,7 25,14 @@ func InitPlatform() Platform {
		os.Mkdir(dataDir, os.ModePerm) // XXX Correct permissions
	}

	return Platform{}
	// Create Cache
	cache, err := cache.NewFSCache(cacheDir)
	if err != nil {
		// XXX Fallback to memory cache, warn
		panic(err.Error())
	}

	return Platform{Cache: cache}
}

func (p Platform) LoadConfig() (cfg AppConfig, err error) {

M cmd/frost/room_view.go => cmd/frost/room_view.go +9 -9
@@ 29,9 29,9 @@ type RoomView struct {
	history     matrix.History
	roomHistory *roomHistory

	backButton      *widget.Clickable
	settingsButton  *widget.Clickable
	userListButton  *widget.Clickable
	backButton     *widget.Clickable
	settingsButton *widget.Clickable
	userListButton *widget.Clickable

	messageComposer *messageComposer



@@ 47,10 47,10 @@ func NewRoomView(cli *matrix.Client, room matrix.Room) *RoomView {
		roomHistory:     newRoomHistory(cli.User().ID),
		history:         room.History(),
		backButton:      new(widget.Clickable),
		settingsButton:      new(widget.Clickable),
		userListButton:      new(widget.Clickable),
		settingsButton:  new(widget.Clickable),
		userListButton:  new(widget.Clickable),
		messageComposer: newMessageComposer(),
		log: logrus.WithField("component", "room_view"),
		log:             logrus.WithField("component", "room_view"),
	}
}



@@ 87,7 87,7 @@ func (r *RoomView) update() {
	if r.userListButton.Clicked() {
		r.log.Info("Opening UsersView")
		r.tx <- LoadViewEvent{
			View: NewUsersView(r.room),
			View:     NewUsersView(r.room),
			Position: Right,
		}
	}


@@ 213,8 213,8 @@ func (w *roomHistory) element(gtx Gtx, index int) Dims {
	}
	return in.Layout(gtx, event{
		Event: msg,
		Own: ownEvent(w.user, msg),
		log: logrus.WithField("event", msg),
		Own:   ownEvent(w.user, msg),
		log:   logrus.WithField("event", msg),
	}.Layout)
}


M cmd/frost/users_view.go => cmd/frost/users_view.go +118 -13
@@ 1,14 1,32 @@
package main

import (
	"gioui.org/widget/material"
	"gioui.org/widget"
	"golang.org/x/exp/shiny/materialdesign/icons"
	"gioui.org/layout"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
	"github.com/sirupsen/logrus"
	"golang.org/x/exp/shiny/materialdesign/icons"
	_ "golang.org/x/image/bmp"
	"golang.org/x/image/draw"
	_ "golang.org/x/image/tiff"
	_ "golang.org/x/image/vp8"
	_ "golang.org/x/image/vp8l"
	_ "golang.org/x/image/webp"
	"image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"net/url"
	"unicode"
)

type AddAvatarEvent struct {
	Image image.Image
	User  matrix.User
}

type UsersView struct {
	rx, tx chan Event



@@ 17,8 35,10 @@ type UsersView struct {
	backButton *widget.Clickable

	userList *layout.List
	users []matrix.Member
	
	users    []matrix.Member

	avatars map[string]image.Image

	room matrix.Room

	log *logrus.Entry


@@ 27,10 47,11 @@ type UsersView struct {
func NewUsersView(room matrix.Room) *UsersView {
	return &UsersView{
		matrixEvents: make(chan matrix.Event, 100),
		backButton: new(widget.Clickable),
		userList: &layout.List{Axis: layout.Vertical},
		room: room,
		log: logrus.WithField("component", "users_view"),
		avatars:      make(map[string]image.Image),
		backButton:   new(widget.Clickable),
		userList:     &layout.List{Axis: layout.Vertical},
		room:         room,
		log:          logrus.WithField("component", "users_view"),
	}

}


@@ 43,7 64,6 @@ func (v *UsersView) Start(rx, tx chan Event) {
	for _, m := range v.room.Members() {
		v.addUser(m)
	}

}

func (v *UsersView) addUser(user matrix.Member) {


@@ 55,10 75,66 @@ func (v *UsersView) addUser(user matrix.Member) {

	v.users = append(v.users, user)
	v.log.WithField("user", user.Displayname()).Debug("Adding user")

	go v.updateAvatar(user)
}

func (v *UsersView) updateAvatar(user matrix.Member) {
	uriString := v.room.GetState("m.room.member", user.ID).Content["avatar_url"]

	log := v.log.WithFields(logrus.Fields{
		"user": user.Displayname(),
		"url":  uriString,
	})

	if _, ok := uriString.(string); !ok {
		log.Debug("avatar_url is not a string. Empty?")
		return
	}

	uri, err := url.Parse(uriString.(string))
	if err != nil {
		log.WithField("error", err).Debug("User has invalid avatar_url")
		return
	}

	errChan := make(chan error, 1)
	thumbChan := make(chan matrix.Thumbnail, 1)

	v.tx <- CacheFetchThumbnailEvent{
		URI:       uri,
		Method:    matrix.Scale,
		Height:    64,
		Width:     64,
		Thumbnail: thumbChan,
		Error:     errChan,
	}

	err = <-errChan
	if err != nil {
		log.WithField("error", err).Debug("Failed to fetch thumbnail")
		return
	}

	thumb := <-thumbChan

	img, _, err := image.Decode(thumb.Content)
	if err != nil {
		log.WithField("error", err).Debug("Failed to decode image")
		return
	}

	dst := image.NewRGBA(image.Rect(0, 0, 46, 46))
	draw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)

	v.rx <- AddAvatarEvent{
		Image: dst,
		User:  user.User,
	}
}

func (v *UsersView) update(gtx Gtx) {
	loop:
loop:
	for {
		select {
		case event := <-v.matrixEvents:


@@ 69,6 145,12 @@ func (v *UsersView) update(gtx Gtx) {
					}
				}
			}
		case event := <-v.rx:
			switch event := event.(type) {
			case AddAvatarEvent:
				v.log.WithField("user", event.User.ID).Debug("Adding avatar image")
				v.avatars[event.User.ID] = event.Image
			}
		default:
			break loop
		}


@@ 93,7 175,30 @@ func (v *UsersView) Layout(gtx Gtx) Dims {
}

func (v *UsersView) listElem(gtx Gtx, index int) Dims {
	return material.Body1(theme, v.users[index].Displayname()).Layout(gtx)
	user := v.users[index]

	var initial string
	for _, c := range user.Displayname() {
		initial = string(unicode.ToUpper(c))
		break
	}

	icon := InitialSign{Initial: initial}.Layout
	if img, ok := v.avatars[user.ID]; ok {
		icon = func(gtx Gtx) Dims {
			gtx.Constraints = layout.Exact(image.Point{
				X: gtx.Px(unit.Dp(46)),
				Y: gtx.Px(unit.Dp(46)),
			})
			return RoundImage{Image: img}.Layout(gtx)
		}
	}

	elem := IconListElement{
		Icon:    icon,
		Content: material.Body1(theme, user.Displayname()).Layout,
	}
	return elem.Layout(gtx)
}

func (v *UsersView) Stop() {


@@ 107,7 212,7 @@ func (v *UsersView) topbar(gtx Gtx) Dims {
			return fill{theme.Color.Primary}.Layout(gtx)
		}),
		layout.Stacked(func(gtx Gtx) Dims {
			return layout.Flex{Alignment:layout.Middle}.Layout(gtx,
			return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
				layout.Rigid(func(gtx Gtx) Dims {
					btn := material.IconButton(theme, v.backButton, mustIcon(icons.NavigationArrowBack))
					return btn.Layout(gtx)

M cmd/frost/util.go => cmd/frost/util.go +1 -2
@@ 1,9 1,9 @@
package main

import (
	"fmt"
	"image/color"
	"time"
	"fmt"

	"gioui.org/f32"
	"gioui.org/layout"


@@ 126,7 126,6 @@ func mustIcon(data []byte) *widget.Icon {
	return ico
}


func logEvent(log *logrus.Entry, event interface{}) {
	log.WithField("event", fmt.Sprintf("%#v", event)).Trace("Received event")
}

M cmd/frost/widgets.go => cmd/frost/widgets.go +46 -1
@@ 5,10 5,14 @@ import (
	"image/color"

	"gioui.org/f32"
	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
)



@@ 45,7 49,7 @@ func initialColor(initial byte) color.RGBA {
type clipCircle struct {
}

func (cc *clipCircle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
func (cc clipCircle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
	macro := op.Record(gtx.Ops)
	dims := w(gtx)
	macroCall := macro.Stop()


@@ 64,3 68,44 @@ func (cc *clipCircle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensi
	stack.Pop()
	return dims
}

type IconListElement struct {
	Icon    layout.Widget
	Content layout.Widget
	Click   *gesture.Click
}

func (w IconListElement) Layout(gtx Gtx) Dims {
	in := layout.Inset{
		Left:  unit.Dp(16),
		Right: unit.Dp(16),
	}

	return in.Layout(gtx, func(gtx Gtx) Dims {
		in := layout.Inset{Top: unit.Dp(16), Bottom: unit.Dp(16)}
		dims := in.Layout(gtx, func(gtx Gtx) Dims {
			return centerRowOpts().Layout(gtx,
				layout.Rigid(func(gtx Gtx) Dims {
					in := layout.Inset{Right: unit.Dp(12)}
					return in.Layout(gtx, w.Icon)
				}),
				layout.Rigid(w.Content),
			)
		})
		pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
		w.Click.Add(gtx.Ops)
		return dims
	})
}

type RoundImage struct {
	Image image.Image
}

func (w RoundImage) Layout(gtx Gtx) Dims {
	cc := clipCircle{}
	return cc.Layout(gtx, func(gtx Gtx) Dims {
		op := paint.NewImageOp(w.Image)
		return widget.Image{Src: op, Scale: 1}.Layout(gtx)
	})
}

M flake.lock => flake.lock +4 -4
@@ 33,10 33,10 @@
    },
    "nixpkgs": {
      "locked": {
        "lastModified": 1603816224,
        "narHash": "sha256-6BhJ3O1Jn4PNclKiDgyb0tygZ7iVePJXa51hIWeizzM=",
        "path": "/nix/store/xmymx75fqbpbkp66ch30y15vzmm0gd67-source",
        "rev": "9034f83740d25ac7a824633ee8d7f35669284be8",
        "lastModified": 1604243500,
        "narHash": "sha256-uu/BxxaeLiAWAbf4P8vrfJzPyKdhnlpUMuViBcXjsHQ=",
        "path": "/nix/store/s6kwb1d0qq7m69p08h6c0iqjm3izhhxm-source",
        "rev": "1c50dc407cfc4ed42cf753d2e41499842288d1d8",
        "type": "path"
      },
      "original": {

M flake.nix => flake.nix +1 -1
@@ 38,7 38,7 @@
        name = "frost";
        src = self;
        subPackages = [ "./cmd/frost" ];
        vendorSha256 = "O7lNg6UzWEZXhpuS4j4rksDyPI0TuUIXw4WZPebNVXI=";
        vendorSha256 = "seuYxvDoTzGOBYiYZJMWuLS/W0hv2wwgp+oIeyXgoPg=";
      };

      linuxPackages = flake-utils.lib.eachSystem [

M go.mod => go.mod +3 -1
@@ 5,7 5,9 @@ go 1.14
require (
	gioui.org v0.0.0-20201018162216-7a4b48f67b54
	gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a
	git.sr.ht/~f4814n/matrix v0.0.0-20201105194455-ef46d2c900a6
	git.sr.ht/~f4814n/matrix v0.0.0-20201117163840-4881dc01ab55
	github.com/sirupsen/logrus v1.7.0
	golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7
	golang.org/x/image v0.0.0-20200618115811-c13761719519
	golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 // indirect
)

M go.sum => go.sum +8 -0
@@ 2,10 2,15 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
gioui.org v0.0.0-20200726090130-3b95e2918359/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
gioui.org v0.0.0-20201018162216-7a4b48f67b54 h1:mM+tSH6C2GBkaErKviXhGL22qiGNgUMutvOYMyAsBK8=
gioui.org v0.0.0-20201018162216-7a4b48f67b54/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw=
gioui.org v0.0.0-20201116155112-01e8308a83fb h1:Koy7mY0TAvjA0x98FOx0kImcsjvTsnJnj+rIYhEybPE=
gioui.org v0.0.0-20201116155112-01e8308a83fb/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw=
gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a h1:BPgJqeQSxuX6CzFQ2EGDj4Kr2ZzwiqwdLyAYLXgxR+k=
gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a/go.mod h1:dlmJnCEkOpRaChYxRmJZ5S4jk6y7DCfWnec39xGbUYk=
git.sr.ht/~f4814n/matrix v0.0.0-20201105194455-ef46d2c900a6 h1:G6SYycKY2PyeaBMhaHK3toX0eNpXmwBV74kK80SblPo=
git.sr.ht/~f4814n/matrix v0.0.0-20201105194455-ef46d2c900a6/go.mod h1:LinuYhN8Rh6XQ9Cy+XM1hTTZT6zDs6ONBTwXAoWCjI4=
git.sr.ht/~f4814n/matrix v0.0.0-20201117163840-4881dc01ab55 h1:eU6c32iLiDeJjmSNdLgbUGkv+Knvx4jxHcbgYCSmyAg=
git.sr.ht/~f4814n/matrix v0.0.0-20201117163840-4881dc01ab55/go.mod h1:LinuYhN8Rh6XQ9Cy+XM1hTTZT6zDs6ONBTwXAoWCjI4=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=


@@ 35,6 40,7 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=


@@ 70,6 76,8 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=