commit 488d264
delthas
·
2023-10-25 08:27:48 +0000 UTC
parent 206be65
Add icon, custom notifications through local integrations This adds custom notifications sent directly (bypassing the terminal), that enables us to automatically dismiss them, add a nice icon, and to automatically switch to the appropriate buffer when clicked. This can be disabled with `local-integrations false` in the config if any issues are encountered. If you do, please open a ticket so this can be investigated. (I'm aware this might be a bit experimental.) This also adds support for OSC 176 ([1]), which enables users to use senpai as an "application", meaning it can be started from the application launcher, pinned to a task bar, have a proper icon and standalone window grouping, etc. This is at the time of writing only supported on foot. [1]: https://gist.github.com/delthas/d451e2cc1573bb2364839849c7117239
18 files changed,
+316,
-45
M
Makefile
+12,
-1
1@@ -10,6 +10,7 @@ PREFIX ?= /usr/local
2 BINDIR ?= bin
3 MANDIR ?= share/man
4 APPDIR ?= share/applications
5+ICONDIR ?= share/icons
6
7 ifeq (0, $(shell $(GIT) status >/dev/null 2>&1; echo $$?))
8 export SOURCE_DATE_EPOCH ?= $(shell $(GIT) log -1 --pretty=%ct)
9@@ -31,6 +32,12 @@ doc/senpai.5: doc/senpai.5.scd
10 $(SCDOC) < doc/senpai.5.scd > doc/senpai.5
11 endif
12
13+res: res/icon.128.png res/icon.48.png
14+res/icon.128.png: res/icon.svg
15+ rsvg-convert -o res/icon.128.png res/icon.svg
16+res/icon.48.png: res/icon.svg
17+ rsvg-convert -w 48 -h 48 -o res/icon.48.png res/icon.svg
18+
19 clean:
20 $(RM) -rf senpai doc/senpai.1 doc/senpai.5
21 install:
22@@ -38,10 +45,14 @@ install:
23 mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR)/man1
24 mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR)/man5
25 mkdir -p $(DESTDIR)$(PREFIX)/$(APPDIR)
26+ mkdir -p $(DESTDIR)$(PREFIX)/$(ICONDIR)/hicolor/48x48/apps
27+ mkdir -p $(DESTDIR)$(PREFIX)/$(ICONDIR)/hicolor/scalable/apps
28 cp -f senpai $(DESTDIR)$(PREFIX)/$(BINDIR)
29 cp -f doc/senpai.1 $(DESTDIR)$(PREFIX)/$(MANDIR)/man1
30 cp -f doc/senpai.5 $(DESTDIR)$(PREFIX)/$(MANDIR)/man5
31 cp -f contrib/senpai.desktop $(DESTDIR)$(PREFIX)/$(APPDIR)/senpai.desktop
32+ cp -f res/icon.48.png $(DESTDIR)$(PREFIX)/$(ICONDIR)/hicolor/48x48/apps/senpai.png
33+ cp -f res/icon.svg $(DESTDIR)$(PREFIX)/$(ICONDIR)/hicolor/scalable/apps/senpai.svg
34 uninstall:
35 $(RM) $(DESTDIR)$(PREFIX)/$(BINDIR)/senpai
36 $(RM) $(DESTDIR)$(PREFIX)/$(MANDIR)/man1/senpai.1
37@@ -51,4 +62,4 @@ uninstall:
38 emoji:
39 curl -sSfL -o emoji.json "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
40
41-.PHONY: all senpai doc clean install uninstall emoji
42+.PHONY: all senpai doc res clean install uninstall emoji
+1,
-1
1@@ -1,4 +1,4 @@
2-# NOTICE me :senpai!
3+ 
4
5 *Welcome home, desune~*
6
M
app.go
+10,
-6
1@@ -159,11 +159,18 @@ func NewApp(cfg Config) (app *App, err error) {
2 MergeLine: func(former *ui.Line, addition ui.Line) {
3 app.mergeLine(former, addition)
4 },
5- Colors: cfg.Colors,
6+ Colors: cfg.Colors,
7+ LocalIntegrations: cfg.LocalIntegrations,
8 })
9 if err != nil {
10 return
11 }
12+ ui.NotifyStart(func(ev *ui.NotifyEvent) {
13+ app.events <- event{
14+ src: "*",
15+ content: ev,
16+ }
17+ })
18 app.win.SetPrompt(ui.Styled(">",
19 tcell.
20 StyleDefault.
21@@ -470,6 +477,8 @@ func (app *App) handleUIEvent(ev interface{}) bool {
22 case *tcell.EventError:
23 // happens when the terminal is closing: in which case, exit
24 return false
25+ case *ui.NotifyEvent:
26+ app.win.JumpBufferNetwork(ev.NetID, ev.Buffer)
27 case statusLine:
28 app.addStatusLine(ev.netID, ev.line)
29 default:
30@@ -1191,11 +1200,6 @@ func (app *App) notifyHighlight(buffer, nick, content string, current bool) {
31 if app.cfg.OnHighlightBeep {
32 app.win.Beep()
33 }
34- if buffer != nick {
35- app.win.Notify(fmt.Sprintf("%s — %s", buffer, nick), content)
36- } else {
37- app.win.Notify(nick, content)
38- }
39
40 if app.cfg.Transient {
41 return
+3,
-2
1@@ -258,8 +258,9 @@ func init() {
2 Handle: commandDoBack,
3 },
4 "SHRUG": {
5- Desc: "send a shrug to the current channel ¯\\_(ツ)_/¯",
6- Handle: commandDoShrug,
7+ Desc: "send a shrug to the current channel ¯\\_(ツ)_/¯",
8+ MaxArgs: maxArgsInfinite,
9+ Handle: commandDoShrug,
10 },
11 "TABLEFLIP": {
12 Desc: "send a tableflip to the current channel (╯°□°)╯︵ ┻━┻",
+14,
-4
1@@ -113,8 +113,9 @@ type Config struct {
2
3 Colors ui.ConfigColors
4
5- Debug bool
6- Transient bool
7+ Debug bool
8+ Transient bool
9+ LocalIntegrations bool
10 }
11
12 func DefaultHighlightPath() (string, error) {
13@@ -157,7 +158,9 @@ func Defaults() Config {
14 Self: tcell.ColorRed,
15 },
16 },
17- Debug: false,
18+ Debug: false,
19+ Transient: false,
20+ LocalIntegrations: true,
21 }
22 }
23
24@@ -436,10 +439,17 @@ func unmarshal(filename string, cfg *Config) (err error) {
25 if err := d.ParseParams(&transient); err != nil {
26 return err
27 }
28-
29 if cfg.Transient, err = strconv.ParseBool(transient); err != nil {
30 return err
31 }
32+ case "local-integrations":
33+ var localIntegrations string
34+ if err := d.ParseParams(&localIntegrations); err != nil {
35+ return err
36+ }
37+ if cfg.LocalIntegrations, err = strconv.ParseBool(localIntegrations); err != nil {
38+ return err
39+ }
40 default:
41 return fmt.Errorf("unknown directive %q", d.Name)
42 }
+2,
-2
1@@ -1,5 +1,5 @@
2 [Desktop Entry]
3-Version=1.0
4+Version=1.5
5 Name=senpai
6
7 GenericName=IRC Client
8@@ -8,6 +8,6 @@ Keywords=Chat;IRC;IM;Messaging
9 Categories=Network;InstantMessaging;Chat;IRCClient;ConsoleOnly
10
11 Type=Application
12-Icon=utilities-terminal
13+Icon=senpai
14 Terminal=true
15 Exec=senpai
+8,
-1
1@@ -203,7 +203,14 @@ colors {
2 *-transient*
3 Advanced.
4 Run an ephemeral instance without disk reads/writes (except for the initial
5- configuration).
6+ configuration). Can be useful for public instances.
7+ Defaults to false.
8+
9+*-local-integrations*
10+ Advanced.
11+ Enables integrations with the local system (e.g. notifications through
12+ DBus). Can be useful to disable on systems planned to be used through SSH.
13+ Defaults to true.
14
15 # EXAMPLES
16
M
go.mod
+2,
-1
1@@ -7,10 +7,11 @@ require (
2 github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878
3 github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118
4 github.com/gdamore/tcell/v2 v2.6.1-0.20230327043120-47ec3a77754f
5+ github.com/godbus/dbus/v5 v5.1.0
6 github.com/mattn/go-runewidth v0.0.15
7 golang.org/x/net v0.18.0
8 golang.org/x/time v0.4.0
9 mvdan.cc/xurls/v2 v2.5.0
10 )
11
12-replace github.com/gdamore/tcell/v2 => github.com/delthas/tcell/v2 v2.4.1-0.20230710100648-1489e78d90fb
13+replace github.com/gdamore/tcell/v2 => github.com/delthas/tcell/v2 v2.4.1-0.20240531114655-837a7d7b4e80
M
go.sum
+2,
-2
1@@ -6,8 +6,8 @@ github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878 h1:v8W8eW7eb2bHFX
2 github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878/go.mod h1:aGVXnhWpDlt5U4SphG97o1gszctZKvBTXy320E8Buw4=
3 github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118 h1:Xzf9ra1QRJXD62gwudjI2iBq7x9CusvHd83Dg2OnUmE=
4 github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118/go.mod h1:sG54BxlyQgIskYURLrg7mvhoGBe0Qq12DNtYRALwNa4=
5-github.com/delthas/tcell/v2 v2.4.1-0.20230710100648-1489e78d90fb h1:x0hrYPzXpmn3L/4QnW0UXJnHX9oz0OcZNcsSgregusw=
6-github.com/delthas/tcell/v2 v2.4.1-0.20230710100648-1489e78d90fb/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
7+github.com/delthas/tcell/v2 v2.4.1-0.20240531114655-837a7d7b4e80 h1:acsB0QcLr1zyWOfFcLf78ZuTmtaf/LORGP9jB87U7x0=
8+github.com/delthas/tcell/v2 v2.4.1-0.20240531114655-837a7d7b4e80/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
9 github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
10 github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
11 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+0,
-0
+0,
-0
+93,
-0
1@@ -0,0 +1,93 @@
2+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
3+<svg
4+ version="1.0"
5+ width="128"
6+ height="128"
7+ viewBox="0 0 128 128"
8+ preserveAspectRatio="xMidYMid"
9+ id="svg3479"
10+ sodipodi:docname="icon.svg"
11+ inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
12+ inkscape:export-filename="icon.png"
13+ inkscape:export-xdpi="96"
14+ inkscape:export-ydpi="96"
15+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
16+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
17+ xmlns="http://www.w3.org/2000/svg"
18+ xmlns:svg="http://www.w3.org/2000/svg">
19+ <defs
20+ id="defs3483" />
21+ <sodipodi:namedview
22+ id="namedview3481"
23+ pagecolor="#ffffff"
24+ bordercolor="#000000"
25+ borderopacity="0.25"
26+ inkscape:showpageshadow="2"
27+ inkscape:pageopacity="0.0"
28+ inkscape:pagecheckerboard="true"
29+ inkscape:deskcolor="#d1d1d1"
30+ showgrid="false"
31+ inkscape:zoom="4"
32+ inkscape:cx="-0.125"
33+ inkscape:cy="39.5"
34+ inkscape:window-width="1920"
35+ inkscape:window-height="1003"
36+ inkscape:window-x="0"
37+ inkscape:window-y="0"
38+ inkscape:window-maximized="1"
39+ inkscape:current-layer="svg3479" />
40+ <g
41+ id="layer1"
42+ inkscape:label="bg"
43+ style="fill:#ffffff;fill-opacity:1"
44+ transform="matrix(0.5,0,0,0.5,-0.04910925,1.6651491)">
45+ <path
46+ d="m 104.93059,244.22515 c -10.165747,-1.21785 -23.526487,-10.95321 -29.463217,-8.68031 -22.24324,8.51592 -46.15474,14.61633 -69.1464293,9.11534 -13.02622,-7.24614 10.2177793,-12.83119 15.3464293,-17.71534 6.8822,-5.79118 17.43606,-12.65692 10.9856,-23.23925 -6.78257,-10.27433 -9.79723,-21.6306 -11.44847,-33.22892 -2.13943,-15.02726 -1.90743,-30.87623 2.2285,-45.48183 1.21444,-11.49591 6.79834,-22.57484 3.93437,-34.325001 -0.78559,-13.35391 -2.76141,-27.15536 -0.61677,-40.47812 2.74232,-17.03572 10.65432,-46.1953996 16.99177,-48.8968795 9.53301,2.4527799 40.98926,18.8077195 55.53125,26.9249995 11.689307,-4.85371 44.804157,-8.88091 63.200007,-0.48125 7.10846,-3.5044 38.20103,-20.5061196 55.96875,-24.5437496 21.18763,29.5973096 20.53081,61.6060896 19.9961,94.4929686 -0.94077,11.881982 4.4766,23.410432 4.6038,35.359572 0.63665,20.68499 -0.8976,47.28222 -9.97284,63.001 -9.07523,15.71877 -22.6399,23.75964 -29.53217,29.5964 -27.17856,19.79263 -64.8632,22.62281 -98.60668,18.58037 z"
47+ id="path3459"
48+ sodipodi:nodetypes="sscccaccacccccczcs"
49+ inkscape:label="path3459"
50+ style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.25" />
51+ </g>
52+ <g
53+ fill="#ae1c3e"
54+ id="g3461"
55+ style="display:inline"
56+ transform="matrix(0.125,0,0,0.125,-1.2404222,1.3125691)">
57+ <path
58+ d="M 429.25283,979.72123 C 388.58988,974.84984 335.14692,935.90839 311.4,945 222.42704,979.06366 126.78105,1003.4653 34.814258,981.46133 -17.290612,952.47677 75.685376,930.13658 96.2,910.6 123.72878,887.43527 165.94423,859.97232 140.14238,817.64297 113.0121,776.54566 100.95349,731.12059 94.348505,684.72732 85.790813,624.61825 86.718794,561.22239 103.2625,502.8 108.12029,456.81635 130.45589,412.50063 119,365.5 115.85765,312.08433 107.95436,256.87855 116.5329,203.5875 127.50221,135.44462 159.15018,18.805918 184.5,8 222.63202,17.811086 348.45702,83.230874 406.625,115.7 453.38221,96.28515 585.84159,80.176359 659.425,113.775 687.85883,99.757373 812.2291,31.750515 883.3,15.6 c 84.75052,118.38924 82.12325,246.42436 79.98438,377.97188 -3.76306,47.5279 17.90642,93.64169 18.41523,141.43828 2.54657,82.73994 -3.59041,189.12888 -39.89136,252.00397 -36.30095,62.8751 -90.5596,95.03859 -118.12871,118.38561 C 714.9653,984.57026 564.22674,995.891 429.25283,979.72123 Z M 634.03281,937.20156 C 738.63162,914.40544 849.6249,850.69192 902.23204,772.80404 954.83918,694.91616 955.22204,552.74752 922.8,470.2 886.69807,421.92322 845.9285,503.25797 799.275,491.1625 778.40073,451.16921 787.9339,390.9059 753.94688,351.73125 730.72388,333.14636 694.60838,381.08906 675,402 651.03278,423.62811 618.27321,423.31475 593.20332,403.11541 556.54774,378.84317 518.48929,308.90638 506.6125,301.8 467.14975,278.18777 417.0012,422.02277 379,445.2 347.51789,435.00073 316.13891,399.59444 293.2125,390.7375 277.59504,422.47841 268.65135,509.09683 217,476 109.44556,440.94812 135.70188,636.20062 144.9,687.775 c 6.88827,46.64852 29.44388,89.68865 50.3,131.05625 26.22145,41.75614 -52.04564,96.68553 -82.14257,124.24478 -29.308662,29.09579 83.93913,11.02304 119.09062,-7.59478 44.31718,-23.47238 88.97238,-43.57365 136.44116,-18.46877 78.442,41.48572 176.12397,36.07016 265.4436,20.18908 z M 186.4,402 C 443.56992,125.11145 709.49727,252.20559 874.40156,371.91719 892.14964,386.00736 912.96754,402.50182 888.125,372.0375 849.28566,307.67218 800.46119,246.33937 733,210.2 630.05449,154.34323 501.55005,137.33389 397.1625,184 317.09423,216.25051 249.77229,276.1874 204.7,349.8 c -8.36438,9.14385 -46.30266,81.47 -18.3,52.2 z M 908.2,302 c -9.74086,-60.0409 -15.8199,-121.64067 -37,-179.2 -42.50967,3.2415 -83.00387,20.21343 -120.30469,40.02969 C 810.78958,199.8115 867.8333,246.26382 907.1,306 Z M 175,301.9 C 213.16155,246.44818 264.04943,199.5736 323.2,167.1 280.11719,137.38433 267.53567,134.05195 223,122.4 187.57338,150.59491 164.94853,253.90394 175,301.9 Z"
59+ id="path5617"
60+ sodipodi:nodetypes="sscccaccacccccczcsczcccccsccccccssccccccccccccccccc"
61+ inkscape:label="path3459"
62+ style="display:inline" />
63+ </g>
64+ <g
65+ fill="#290225"
66+ id="g3457"
67+ transform="matrix(0.125,0,0,0.125,-1.2404222,1.3125691)">
68+ <path
69+ d="M 523.89756,864.80123 C 487.19342,864.1306 450.28522,852.26521 422.31631,827.99814 403.99497,812.17597 386.3883,792.62565 382.0027,768.0033 c -6.2748,-10.75998 -2.25189,-25.10976 10.22706,-28.99153 10.53946,-3.50982 21.7721,-1.32749 32.5553,-0.48554 15.44431,10.52796 18.3642,31.72315 32.17712,43.19145 72.28607,63.8139 141.24031,32.30483 183.91029,-33.06162 5.23999,-12.4268 20.85981,-13.8226 32.42347,-10.97094 13.38499,2.36866 22.38351,16.43701 18.74151,29.56063 -4.07066,15.85353 -13.6748,29.73373 -22.84155,43.0151 -21.2275,22.61414 -48.23242,40.41912 -78.5021,48.09542 -21.68598,5.67189 -44.42667,7.76436 -66.79624,6.44496 z"
70+ id="path3451"
71+ sodipodi:nodetypes="cccccccccccc" />
72+ <path
73+ d="m 437.63785,591.11841 c -19.24093,-55.25607 -64.6646,-91.56451 -106.95892,-42.90313 -16.17392,18.60877 -18.99634,59.54456 -51.05341,53.52092 -9.82422,-1.84601 -13.75801,-17.01306 -13.07888,-26.98611 1.98258,-29.11439 23.58004,-57.70687 48.50857,-72.87745 26.11412,-15.89208 63.27587,-22.59306 91.15614,-10.05528 35.39748,15.91827 83.02104,59.59116 67.24744,95.05334 -4.88667,10.98619 -31.86689,15.60294 -35.82094,4.24771 z"
74+ id="path3453"
75+ sodipodi:nodetypes="sssaaaas" />
76+ <path
77+ d="m 631.10457,589.84476 c 19.24093,-55.25607 64.6646,-91.56451 106.95892,-42.90313 16.17392,18.60877 18.99634,59.54456 51.05341,53.52092 9.82422,-1.84601 13.75801,-17.01306 13.07888,-26.98611 -1.98258,-29.11439 -23.58004,-57.70687 -48.50857,-72.87745 -26.11412,-15.89208 -63.27587,-22.59306 -91.15614,-10.05528 -35.39748,15.91827 -83.02104,59.59116 -67.24744,95.05334 4.88667,10.98619 31.86689,15.60294 35.82094,4.24771 z"
78+ id="path5567"
79+ sodipodi:nodetypes="sssaaaas" />
80+ </g>
81+ <g
82+ fill="#ed91c0"
83+ id="g3467"
84+ style="display:inline"
85+ transform="matrix(0.125,0,0,0.125,-1.2404222,1.3125691)">
86+ <path
87+ d="m 330.5,681.3 c 0,29.4 -23.8,53.2 -53.2,53.2 -29.4,0 -53.2,-23.8 -53.2,-53.2 0,-29.3 23.8,-53.2 53.2,-53.2 29.4,0 53.2,23.9 53.2,53.2 z m -65.5,52"
88+ id="path3463" />
89+ <path
90+ d="m 852.8,678.5 c 0,29.4 -23.8,53.2 -53.2,53.2 -29.4,0 -53.3,-23.8 -53.3,-53.2 0,-29.4 23.9,-53.3 53.3,-53.3 29.4,0 53.2,23.9 53.2,53.3 z M 785,729.7"
91+ id="path3465"
92+ style="display:inline" />
93+ </g>
94+</svg>
+0,
-0
+25,
-12
1@@ -189,13 +189,14 @@ func (l *Line) NewLines(width int) []int {
2 }
3
4 type buffer struct {
5- netID string
6- netName string
7- title string
8- highlights int
9- unread bool
10- read time.Time
11- openedOnce bool
12+ netID string
13+ netName string
14+ title string
15+ highlights int
16+ notifications []int
17+ unread bool
18+ read time.Time
19+ openedOnce bool
20
21 // This is the "last read" timestamp when the buffer was last focused.
22 // If the "last read" timestamp changes while the buffer is focused,
23@@ -275,9 +276,8 @@ func (bs *BufferList) To(i int) bool {
24 if len(bs.list) <= bs.current {
25 bs.current = len(bs.list) - 1
26 }
27+ bs.clearRead(i)
28 b := bs.list[bs.current]
29- b.highlights = 0
30- b.unread = false
31 b.unreadRuler = b.read
32 if len(b.lines) > 0 {
33 l := b.lines[len(b.lines)-1]
34@@ -383,6 +383,7 @@ func (bs *BufferList) Remove(netID, title string) bool {
35 }
36 updated := bs.current == idx
37
38+ bs.clearRead(idx)
39 bs.list = append(bs.list[:idx], bs.list[idx+1:]...)
40 if bs.current >= idx {
41 bs.current--
42@@ -406,6 +407,7 @@ func (bs *BufferList) RemoveNetwork(netID string) {
43 if idx == bs.current {
44 updated = true
45 }
46+ bs.clearRead(idx)
47 bs.list = append(bs.list[:idx], bs.list[idx+1:]...)
48 if bs.current >= idx {
49 bs.current--
50@@ -526,8 +528,20 @@ func (bs *BufferList) SetTopic(netID, title string, topic string) {
51 b.topic = topic
52 }
53
54+func (bs *BufferList) clearRead(i int) {
55+ b := &bs.list[i]
56+ b.highlights = 0
57+ b.unread = false
58+ if len(b.notifications) > 0 {
59+ for _, id := range b.notifications {
60+ notifyClose(id)
61+ }
62+ b.notifications = b.notifications[:0]
63+ }
64+}
65+
66 func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) {
67- _, b := bs.at(netID, title)
68+ i, b := bs.at(netID, title)
69 if b == nil {
70 return
71 }
72@@ -543,8 +557,7 @@ func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) {
73 }
74 }
75 if clearRead {
76- b.highlights = 0
77- b.unread = false
78+ bs.clearRead(i)
79 }
80 if b.read.Before(timestamp) {
81 b.read = timestamp
+6,
-0
1@@ -0,0 +1,6 @@
2+package ui
3+
4+type NotifyEvent struct {
5+ NetID string
6+ Buffer string
7+}
+13,
-0
1@@ -0,0 +1,13 @@
2+//go:build !linux
3+// +build !linux
4+
5+package ui
6+
7+func (ui *UI) notify(title, content string) int {
8+ ui.screen.Notify(title, content)
9+ return -1
10+}
11+
12+func notifyClose(id int) {}
13+
14+func NotifyStart(f func(event NotifyEvent)) {}
+93,
-0
1@@ -0,0 +1,93 @@
2+//go:build linux
3+// +build linux
4+
5+package ui
6+
7+import (
8+ "sync"
9+
10+ "github.com/godbus/dbus/v5"
11+)
12+
13+var notificationsLock sync.Mutex
14+var notifications = make(map[int]*NotifyEvent)
15+
16+func notifyDBus(title, content string) int {
17+ conn, err := dbus.SessionBus()
18+ if err != nil {
19+ return -1
20+ }
21+ var r uint32
22+ obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
23+ err = obj.Call("org.freedesktop.Notifications.Notify", 0, "senpai", uint32(0), "image-path", title, content, []string{
24+ "default", "Open",
25+ }, map[string]dbus.Variant{
26+ "category": dbus.MakeVariant("im.received"),
27+ "desktop-entry": dbus.MakeVariant("senpai"),
28+ "image-path": dbus.MakeVariant("senpai"),
29+ "urgency": dbus.MakeVariant(uint8(1)), // Normal
30+ }, int32(-1)).Store(&r)
31+ if err != nil {
32+ return -1
33+ }
34+ return int(r)
35+}
36+
37+func (ui *UI) notify(target NotifyEvent, title, content string) int {
38+ if ui.config.LocalIntegrations {
39+ id := notifyDBus(title, content)
40+ if id > 0 {
41+ notificationsLock.Lock()
42+ notifications[id] = &target
43+ notificationsLock.Unlock()
44+ return id
45+ }
46+ }
47+
48+ ui.screen.Notify(title, content)
49+ return -1
50+}
51+
52+func notifyClose(id int) {
53+ conn, err := dbus.SessionBus()
54+ if err != nil {
55+ return
56+ }
57+ obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
58+ obj.Call("org.freedesktop.Notifications.CloseNotification", 0, uint32(id))
59+}
60+
61+func NotifyStart(opened func(*NotifyEvent)) {
62+ conn, err := dbus.SessionBus()
63+ if err != nil {
64+ return
65+ }
66+ if err := conn.AddMatchSignal(
67+ dbus.WithMatchObjectPath("/org/freedesktop/Notifications"),
68+ dbus.WithMatchInterface("org.freedesktop.Notifications"),
69+ dbus.WithMatchSender("org.freedesktop.Notifications.ActionInvoked"),
70+ ); err != nil {
71+ return
72+ }
73+ c := make(chan *dbus.Signal, 64)
74+ conn.Signal(c)
75+ go func() {
76+ for v := range c {
77+ switch v.Name {
78+ case "org.freedesktop.Notifications.NotificationClosed":
79+ id := int(v.Body[0].(uint32))
80+ notificationsLock.Lock()
81+ delete(notifications, id)
82+ notificationsLock.Unlock()
83+ case "org.freedesktop.Notifications.ActionInvoked":
84+ id := int(v.Body[0].(uint32))
85+ notificationsLock.Lock()
86+ target, ok := notifications[id]
87+ notificationsLock.Unlock()
88+ if ok {
89+ opened(target)
90+ }
91+ }
92+ }
93+ }()
94+}
M
ui/ui.go
+32,
-13
1@@ -12,16 +12,17 @@ import (
2 )
3
4 type Config struct {
5- NickColWidth int
6- ChanColWidth int
7- ChanColEnabled bool
8- MemberColWidth int
9- MemberColEnabled bool
10- TextMaxWidth int
11- AutoComplete func(cursorIdx int, text []rune) []Completion
12- Mouse bool
13- MergeLine func(former *Line, addition Line)
14- Colors ConfigColors
15+ NickColWidth int
16+ ChanColWidth int
17+ ChanColEnabled bool
18+ MemberColWidth int
19+ MemberColEnabled bool
20+ TextMaxWidth int
21+ AutoComplete func(cursorIdx int, text []rune) []Completion
22+ Mouse bool
23+ MergeLine func(former *Line, addition Line)
24+ Colors ConfigColors
25+ LocalIntegrations bool
26 }
27
28 type ConfigColors struct {
29@@ -78,6 +79,7 @@ func New(config Config) (ui *UI, err error) {
30 ui.screen.EnablePaste()
31 ui.screen.SetCursorStyle(tcell.CursorStyleSteadyBar)
32 ui.screen.SetTitle("senpai")
33+ ui.screen.SetAppID("senpai")
34
35 _, h := ui.screen.Size()
36 ui.screen.Clear()
37@@ -298,6 +300,24 @@ func (ui *UI) RemoveNetworkBuffers(netID string) {
38
39 func (ui *UI) AddLine(netID, buffer string, line Line) {
40 ui.bs.AddLine(netID, buffer, line)
41+
42+ curNetID, curBuffer := ui.bs.Current()
43+ _, b := ui.bs.at(netID, buffer)
44+ if b != nil && line.Notify == NotifyHighlight && (curNetID != netID || curBuffer != buffer) {
45+ var header string
46+ if buffer != line.Head {
47+ header = fmt.Sprintf("%s — %s", buffer, line.Head)
48+ } else {
49+ header = line.Head
50+ }
51+ id := ui.notify(NotifyEvent{
52+ NetID: netID,
53+ Buffer: buffer,
54+ }, header, line.Body.String())
55+ if id >= 0 {
56+ b.notifications = append(b.notifications, id)
57+ }
58+ }
59 }
60
61 func (ui *UI) AddLines(netID, buffer string, before, after []Line) {
62@@ -328,10 +348,9 @@ func (ui *UI) JumpBufferIndex(i int) bool {
63 return false
64 }
65
66-func (ui *UI) JumpBufferNetwork(netID, sub string) bool {
67- subLower := strings.ToLower(sub)
68+func (ui *UI) JumpBufferNetwork(netID, buffer string) bool {
69 for i, b := range ui.bs.list {
70- if b.netID == netID && strings.Contains(strings.ToLower(b.title), subLower) {
71+ if b.netID == netID && strings.ToLower(b.title) == strings.ToLower(buffer) {
72 if ui.bs.To(i) {
73 ui.memberOffset = 0
74 }