commit dff2f5e
delthas
·
2024-07-02 23:50:44 +0000 UTC
parent d47a847
Add link image previews This is made so easy thanks to Vaxis!
M
app.go
+78,
-0
1@@ -4,7 +4,12 @@ import (
2 "crypto/tls"
3 "errors"
4 "fmt"
5+ "image"
6+ _ "image/gif"
7+ _ "image/jpeg"
8+ _ "image/png"
9 "net"
10+ "net/http"
11 "os"
12 "os/exec"
13 "strings"
14@@ -17,6 +22,7 @@ import (
15 "golang.org/x/net/context"
16 "golang.org/x/net/proxy"
17
18+ "git.sr.ht/~delthas/senpai/events"
19 "git.sr.ht/~delthas/senpai/irc"
20 "git.sr.ht/~delthas/senpai/ui"
21 )
22@@ -109,6 +115,9 @@ type App struct {
23 lastCloseTime time.Time
24
25 lastConfirm string
26+
27+ imageLoading bool
28+ imageOverlay bool
29 }
30
31 func NewApp(cfg Config) (app *App, err error) {
32@@ -481,6 +490,13 @@ func (app *App) handleUIEvent(ev interface{}) bool {
33 app.win.JumpBufferNetwork(ev.NetID, ev.Buffer)
34 case statusLine:
35 app.addStatusLine(ev.netID, ev.line)
36+ case *events.EventClickLink:
37+ app.handleLinkEvent(ev)
38+ case *events.EventImageLoaded:
39+ app.win.ShowImage(ev.Image)
40+ if ev.Image == nil {
41+ app.imageLoading = false
42+ }
43 default:
44 // TODO: missing event types
45 }
46@@ -490,6 +506,15 @@ func (app *App) handleUIEvent(ev interface{}) bool {
47 func (app *App) handleMouseEvent(ev vaxis.Mouse) {
48 x, y := ev.Col, ev.Row
49 w, h := app.win.Size()
50+
51+ if app.imageOverlay && ev.Button == vaxis.MouseLeftButton {
52+ if ev.EventType == vaxis.EventPress {
53+ app.win.ShowImage(nil)
54+ app.imageOverlay = false
55+ }
56+ return
57+ }
58+
59 if ev.EventType == vaxis.EventPress {
60 if ev.Button == vaxis.MouseWheelUp {
61 if x < app.win.ChannelWidth() || (app.win.ChannelWidth() == 0 && y == h-1) {
62@@ -527,6 +552,8 @@ func (app *App) handleMouseEvent(ev vaxis.Mouse) {
63 app.win.SetMouseShape(vaxis.MouseShapeResizeHorizontal)
64 } else if x > w-app.win.MemberWidth() && y >= 2 {
65 app.win.ClickMember(y - 2 + app.win.MemberOffset())
66+ } else {
67+ app.win.Click(x, y)
68 }
69 }
70 if ev.Button == vaxis.MouseMiddleButton {
71@@ -781,6 +808,57 @@ func (app *App) handleKeyEvent(ev vaxis.Key) {
72 }
73 }
74
75+func (app *App) fetchImage(link string) (image.Image, error) {
76+ c := http.Client{
77+ Timeout: 6 * time.Second,
78+ }
79+ res, err := c.Head(link)
80+ if err != nil {
81+ return nil, err
82+ }
83+ if res.StatusCode != http.StatusOK {
84+ return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
85+ }
86+ contentType := res.Header.Get("Content-Type")
87+ switch contentType {
88+ case "image/gif", "image/jpeg", "image/png":
89+ default:
90+ return nil, fmt.Errorf("unexpected content type: %v", contentType)
91+ }
92+ res, err = c.Get(link)
93+ if err != nil {
94+ return nil, err
95+ }
96+ img, _, err := image.Decode(res.Body)
97+ res.Body.Close()
98+ if err != nil {
99+ return nil, err
100+ }
101+ return img, nil
102+}
103+
104+func (app *App) handleLinkEvent(ev *events.EventClickLink) {
105+ app.imageLoading = true
106+ go func() {
107+ img, err := app.fetchImage(ev.Link)
108+ if err != nil {
109+ app.events <- event{
110+ src: "*",
111+ content: &events.EventImageLoaded{
112+ Image: nil,
113+ },
114+ }
115+ } else {
116+ app.events <- event{
117+ src: "*",
118+ content: &events.EventImageLoaded{
119+ Image: img,
120+ },
121+ }
122+ }
123+ }()
124+}
125+
126 // requestHistory is a wrapper around irc.Session.RequestHistory to only request
127 // history when needed.
128 func (app *App) requestHistory() {
+9,
-0
1@@ -76,6 +76,15 @@ In order to select text with a mouse, hold SHIFT while clicking and dragging
2 the mouse. *Clicking and dragging without holding SHIFT will not work, as
3 senpai eats these events for eg selecting channels.*
4
5+# OPENING LINKS
6+
7+In order to open links, refer to your terminal manual. On most terminals,
8+opening links is done by holding CTRL, or SHIFT, while clicking the link.
9+On the *foot* terminal, links can be opened by pressing CTRL+SHIFT+O.
10+
11+Additionally, by simply clicking an image link with the mouse, a small preview
12+will be shown, which can be closed by clicking again anywhere in the interface.
13+
14 # KEYBOARD SHORTCUTS
15
16 *CTRL-A*
+22,
-0
1@@ -0,0 +1,22 @@
2+package events
3+
4+import "image"
5+
6+type EventClick struct {
7+ NetID string
8+ Buffer string
9+}
10+
11+type EventClickNick struct {
12+ EventClick
13+ Nick string
14+}
15+
16+type EventClickLink struct {
17+ EventClick
18+ Link string
19+}
20+
21+type EventImageLoaded struct {
22+ Image image.Image // nil if error
23+}
M
go.mod
+2,
-0
1@@ -22,3 +22,5 @@ require (
2 golang.org/x/image v0.18.0 // indirect
3 golang.org/x/sys v0.21.0 // indirect
4 )
5+
6+replace git.sr.ht/~rockorager/vaxis => git.sr.ht/~delthas/vaxis v0.8.6-0.20240702234839-1922cc2973f6
M
go.sum
+2,
-2
1@@ -1,7 +1,7 @@
2+git.sr.ht/~delthas/vaxis v0.8.6-0.20240702234839-1922cc2973f6 h1:we7TiRN0uoGHTgGUXbmBZXwgUkGBOm4CbeC8ip41uhQ=
3+git.sr.ht/~delthas/vaxis v0.8.6-0.20240702234839-1922cc2973f6/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
4 git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 h1:9Udx5fm4vRtmgDIBjy2ef5QioHbzpw5oHabbhpAUyEw=
5 git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
6-git.sr.ht/~rockorager/vaxis v0.9.2 h1:OKPaCG3S4b6jWnwUPC0zX+Zw16hhLcSgC2mLw/nwsV0=
7-git.sr.ht/~rockorager/vaxis v0.9.2/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
8 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
9 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
10 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+20,
-1
1@@ -7,6 +7,8 @@ import (
2 "time"
3
4 "git.sr.ht/~rockorager/vaxis"
5+
6+ "git.sr.ht/~delthas/senpai/events"
7 )
8
9 const Overlay = "/overlay"
10@@ -853,7 +855,8 @@ func (bs *BufferList) DrawHorizontalBufferList(vx *Vaxis, x0, y0, width int, off
11 }
12 }
13
14-func (bs *BufferList) DrawTimeline(vx *Vaxis, x0, y0, nickColWidth int) {
15+func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
16+ vx := ui.vx
17 clearArea(vx, x0, y0, bs.tlInnerWidth+nickColWidth+9, bs.tlHeight+2)
18
19 b := bs.cur()
20@@ -966,6 +969,7 @@ func (bs *BufferList) DrawTimeline(vx *Vaxis, x0, y0, nickColWidth int) {
21 continue
22 }
23
24+ xb := x
25 if y >= y0 {
26 dx, di := printCluster(vx, x, y, -1, l, style)
27 x += dx
28@@ -977,6 +981,21 @@ func (bs *BufferList) DrawTimeline(vx *Vaxis, x0, y0, nickColWidth int) {
29 lbi += len(c)
30 l = l[len([]rune(c)):]
31 }
32+
33+ if style.Hyperlink != "" {
34+ ui.clickEvents = append(ui.clickEvents, clickEvent{
35+ xb: xb,
36+ xe: x,
37+ y: y,
38+ event: &events.EventClickLink{
39+ EventClick: events.EventClick{
40+ NetID: b.netID,
41+ Buffer: b.title,
42+ },
43+ Link: style.Hyperlink,
44+ },
45+ })
46+ }
47 }
48 }
49
M
ui/ui.go
+66,
-4
1@@ -2,11 +2,13 @@ package ui
2
3 import (
4 "fmt"
5+ "image"
6 "strings"
7 "sync/atomic"
8 "time"
9
10 "git.sr.ht/~rockorager/vaxis"
11+ "git.sr.ht/~rockorager/vaxis/widgets/align"
12
13 "git.sr.ht/~delthas/senpai/irc"
14 )
15@@ -37,9 +39,16 @@ type Vaxis struct {
16 window vaxis.Window
17 }
18
19+type clickEvent struct {
20+ xb int
21+ xe int
22+ y int
23+ event interface{}
24+}
25+
26 type UI struct {
27 vx *Vaxis
28- Events chan vaxis.Event
29+ Events chan any
30 exit atomic.Value // bool
31 config Config
32
33@@ -59,11 +68,16 @@ type UI struct {
34
35 channelColClicked bool
36 memberColClicked bool
37+
38+ clickEvents []clickEvent
39+
40+ image vaxis.Image
41 }
42
43 func New(config Config) (ui *UI, err error) {
44 ui = &UI{
45- config: config,
46+ config: config,
47+ clickEvents: make([]clickEvent, 0, 128),
48 }
49 if config.ChanColEnabled {
50 ui.channelWidth = config.ChanColWidth
51@@ -94,7 +108,7 @@ func New(config Config) (ui *UI, err error) {
52
53 ui.exit.Store(false)
54
55- ui.Events = make(chan vaxis.Event, 128)
56+ ui.Events = make(chan any, 128)
57 go func() {
58 for !ui.ShouldExit() {
59 ev := ui.vx.PollEvent()
60@@ -219,6 +233,15 @@ func (ui *UI) ClickMember(i int) {
61 ui.memberClicked = i
62 }
63
64+func (ui *UI) Click(x, y int) {
65+ for _, ev := range ui.clickEvents {
66+ if x >= ev.xb && x < ev.xe && y == ev.y {
67+ ui.Events <- ev.event
68+ break
69+ }
70+ }
71+}
72+
73 func (ui *UI) ScrollUp() {
74 ui.bs.ScrollUp(ui.bs.tlHeight / 2)
75 }
76@@ -523,6 +546,9 @@ func (ui *UI) Resize() {
77 ui.bs.ResizeTimeline(innerWidth, h-2, textWidth)
78 }
79 ui.ScrollToBuffer()
80+ if ui.image != nil {
81+ ui.image.Resize(w, h)
82+ }
83 ui.vx.Refresh()
84 }
85
86@@ -538,10 +564,41 @@ func (ui *UI) Notify(title string, body string) {
87 ui.vx.Notify(title, body)
88 }
89
90+func (ui *UI) ImageReady() bool {
91+ if ui.image == nil {
92+ return false
93+ }
94+ iw, ih := ui.image.CellSize()
95+ return iw != 0 || ih != 0
96+}
97+
98+func (ui *UI) ShowImage(img image.Image) bool {
99+ if img == nil {
100+ if ui.image != nil {
101+ ui.image.Destroy()
102+ ui.image = nil
103+ }
104+ return true
105+ }
106+
107+ vi, err := ui.vx.NewImage(img)
108+ if err != nil {
109+ return false
110+ }
111+ w, h := ui.vx.window.Size()
112+ w = w * 9 / 10
113+ h = h * 9 / 10
114+ vi.Resize(w, h)
115+ ui.image = vi
116+ return true
117+}
118+
119 func (ui *UI) Draw(members []irc.Member) {
120+ ui.clickEvents = ui.clickEvents[:0]
121+
122 w, h := ui.vx.window.Size()
123
124- ui.bs.DrawTimeline(ui.vx, ui.channelWidth, 0, ui.config.NickColWidth)
125+ ui.bs.DrawTimeline(ui, ui.channelWidth, 0, ui.config.NickColWidth)
126 if ui.channelWidth == 0 {
127 ui.bs.DrawHorizontalBufferList(ui.vx, 0, h-1, w-ui.memberWidth, &ui.channelOffset)
128 } else {
129@@ -584,6 +641,11 @@ func (ui *UI) Draw(members []irc.Member) {
130 ui.e.Draw(ui.vx, 9+ui.channelWidth+ui.config.NickColWidth, h-1, hint)
131 }
132
133+ if ui.image != nil {
134+ iw, ih := ui.image.CellSize()
135+ ui.image.Draw(align.Center(ui.vx.window, iw, ih))
136+ }
137+
138 ui.vx.Render()
139 }
140
+9,
-0
1@@ -45,6 +45,15 @@ func (app *App) addStatusLine(netID string, line ui.Line) {
2 }
3
4 func (app *App) setStatus() {
5+ if app.imageLoading && app.win.ImageReady() {
6+ app.imageLoading = false
7+ app.imageOverlay = true
8+ }
9+ if app.imageLoading {
10+ app.win.SetStatus("Loading image...")
11+ return
12+ }
13+
14 netID, buffer := app.win.CurrentBuffer()
15 s := app.sessions[netID]
16 if s == nil {