1package vt
2
3import (
4 "strconv"
5 "strings"
6
7 "github.com/bnema/vev/pkg/renderer"
8)
9
10const (
11 progressStateClear = iota
12 progressStateNormal
13 progressStateError
14 progressStateIndeterminate
15 progressStatePaused
16)
17
18func (s *Screen) consumeOSC(data []byte) (consumed int, partial bool) {
19 for i := 2; i < len(data); i++ {
20 switch data[i] {
21 case 0x07:
22 s.applyOSC(data[2:i], []byte{0x07})
23 s.handleOSC(data[2:i])
24 return i + 1, false
25 case 0x1b:
26 if i+1 < len(data) && data[i+1] == '\\' {
27 s.applyOSC(data[2:i], []byte{0x1b, '\\'})
28 s.handleOSC(data[2:i])
29 return i + 2, false
30 }
31 }
32 }
33 return 0, true
34}
35
36func (s *Screen) applyOSC(payload, terminator []byte) {
37 if !s.defaultColorsKnown {
38 return
39 }
40
41 var color renderer.RGB
42 switch string(payload) {
43 case "10;?":
44 color = s.defaultFG
45 case "11;?":
46 color = s.defaultBG
47 default:
48 return
49 }
50
51 resp := make([]byte, 0, len(payload)+len("\x1b];rgb:0000/0000/0000")+len(terminator))
52 resp = append(resp, "\x1b]"...)
53 resp = append(resp, payload[:2]...)
54 resp = append(resp, ";rgb:"...)
55 resp = appendOSCColorComponent(resp, color.R)
56 resp = append(resp, '/')
57 resp = appendOSCColorComponent(resp, color.G)
58 resp = append(resp, '/')
59 resp = appendOSCColorComponent(resp, color.B)
60 resp = append(resp, terminator...)
61 s.respond(resp)
62}
63
64func appendOSCColorComponent(dst []byte, c uint8) []byte {
65 const hex = "0123456789abcdef"
66 v := uint16(c)<<8 | uint16(c)
67 return append(dst, hex[v>>12&0xf], hex[v>>8&0xf], hex[v>>4&0xf], hex[v&0xf])
68}
69
70// handleOSC inspects a complete OSC payload (between "ESC ]" and its
71// terminator). Terminal titles, notification sequences (OSC 9, OSC 777
72// "notify"), and clipboard set requests (OSC 52) are acted on; every other OSC
73// is discarded.
74
75func (s *Screen) handleOSC(payload []byte) {
76 if len(payload) >= len("52;") && payload[0] == '5' && payload[1] == '2' && payload[2] == ';' {
77 s.handleOSC52(string(payload[len("52;"):]))
78 return
79 }
80 p := string(payload)
81 if selector, title, ok := strings.Cut(p, ";"); ok && (selector == "0" || selector == "2") {
82 s.terminalTitle = title
83 return
84 }
85 if p == "9;4" || strings.HasPrefix(p, "9;4;") {
86 s.handleProgress(p[len("9;4"):])
87 return
88 }
89 if s.OnNotify == nil {
90 return
91 }
92 switch {
93 case strings.HasPrefix(p, "9;"):
94 s.OnNotify("", p[len("9;"):])
95 case strings.HasPrefix(p, "777;"):
96 parts := strings.SplitN(p[len("777;"):], ";", 3)
97 if parts[0] != "notify" {
98 return
99 }
100 var title, body string
101 if len(parts) > 1 {
102 title = parts[1]
103 }
104 if len(parts) > 2 {
105 body = parts[2]
106 }
107 s.OnNotify(title, body)
108 }
109}
110
111func (s *Screen) handleProgress(rest string) {
112 rest = strings.TrimPrefix(rest, ";")
113 if rest == "" {
114 return
115 }
116 token, _, _ := strings.Cut(rest, ";")
117 state, err := strconv.Atoi(token)
118 if err != nil {
119 return
120 }
121 switch state {
122 case progressStateNormal, progressStateIndeterminate, progressStatePaused:
123 s.progressState = state
124 case progressStateClear:
125 previous := s.progressState
126 s.progressState = state
127 if previous == progressStateNormal || previous == progressStateIndeterminate || previous == progressStatePaused {
128 if s.OnProgress != nil {
129 s.OnProgress(false)
130 }
131 }
132 case progressStateError:
133 previous := s.progressState
134 s.progressState = state
135 if previous != progressStateError {
136 if s.OnProgress != nil {
137 s.OnProgress(true)
138 }
139 }
140 }
141}
142
143// handleOSC52 parses the "<selection>;<data>" remainder of an OSC 52
144// clipboard payload (selection may be empty; split on the first ";"). A
145// clipboard query (data == "?") is always ignored — vev never answers
146// clipboard queries. A payload with no second ";" is malformed and ignored.
147
148func (s *Screen) handleOSC52(rest string) {
149 _, data, ok := strings.Cut(rest, ";")
150 if !ok {
151 return
152 }
153 if data == "?" {
154 return
155 }
156 if s.OnClipboard != nil {
157 s.OnClipboard(data)
158 }
159}