1// Package daemon holds vev's server-side session multiplexer use case.
2package daemon
3
4import (
5 "bytes"
6
7 renderer "github.com/bnema/vev-vt/ansi"
8 "github.com/bnema/vev/internal/domain"
9 "github.com/bnema/vev/internal/domain/layout"
10 "github.com/bnema/vev/internal/domain/terminalcap"
11 "github.com/bnema/vev/internal/ports"
12 themeui "github.com/bnema/vev/internal/usecase/theme"
13)
14
15// completedSynchronizedUpdate reports a full DEC 2026 batch contained in one
16// PTY read. The VT's final state is inactive in this case, so it cannot be
17// inferred from before/after SyncUpdateActive alone.
18func completedSynchronizedUpdate(data []byte) bool {
19 _, tail, found := bytes.Cut(data, []byte(renderer.SyncStartCSI))
20 return found && bytes.Contains(tail, []byte(renderer.SyncEndCSI))
21}
22
23// paneRenderable reports dynamic composition visibility. Callers must not
24// hold coordinator locks: this acquires daemon/session/tab/pane-adjacent locks.
25func (d *Daemon) paneRenderable(sess *session, tb *tab, p *pane) bool {
26 if sess == nil || tb == nil || p == nil {
27 return false
28 }
29 sess.mu.Lock()
30 active := sess.attachmentViewsTabLocked(tb)
31 attached := len(sess.attachments) != 0
32 sess.mu.Unlock()
33
34 if !active || !attached {
35 // A headless tab is still renderable while a preview subscriber
36 // watches it: the preview service composes its frames from this tab.
37 rc := sess.renderCoordinator()
38 if rc == nil || !rc.hasPreviewSubscribers() {
39 return false
40 }
41 }
42 tb.mu.Lock()
43 defer tb.mu.Unlock()
44 if tb.floating.pane == p {
45 return active && attached && tb.floating.state == floatingVisible
46 }
47 placements, ok := solvedPlacementsLocked(tb)
48 if !ok {
49 return false
50 }
51 for _, placement := range placements {
52 if placement.ID == p.id && !placement.Collapsed && placement.Content.Width > 0 && placement.Content.Height > 0 {
53 return true
54 }
55 }
56 return false
57}
58
59func (d *Daemon) readPanePTY(p *pane) {
60 if p == nil {
61 return
62 }
63 buf := make([]byte, ptyReadBufSize)
64 p.mu.Lock()
65 p.screen.OnResponse = func(b []byte) { p.ptyResponses = append(p.ptyResponses, b...) }
66 p.screen.OnBell = func() { p.ptyAttention = true }
67 p.screen.OnNotify = func(string, string) { p.ptyAttention = true }
68 p.screen.OnProgress = func(bool) { p.ptyAttention = true }
69 p.screen.OnClipboard = func(b64 string) { p.ptyClipboards = append(p.ptyClipboards, b64) }
70 p.mu.Unlock()
71 for {
72 n, err := p.pty.Read(buf)
73 if n > 0 {
74 d.processPanePTYData(p, buf[:n], true)
75 }
76 if err != nil {
77 lease := p.effectLease()
78 if sessionName, current := paneEffectSessionName(lease); current {
79 d.log.Info("pane pty closed", "err", err, "session", sessionName)
80 } else {
81 d.log.Info("pane pty closed", "err", err)
82 }
83 if p.onExit != nil {
84 p.onExit()
85 } else {
86 d.reapPaneOwner(p)
87 }
88 return
89 }
90 }
91}
92
93type panePTYEffects struct {
94 lease paneEffectLease
95 renderCoordinator *renderCoordinator
96 wasSyncing bool
97 isSyncing bool
98 completeSyncRead bool
99 syncEnded bool
100 attention bool
101 responses []byte
102 clipboards []string
103 syncCleanup syncTimerCleanup
104}
105
106// processPanePTYData is the sole VT parsing path. While a resize apply owns the
107// pane, reader calls append exact bytes and returns immediately; it never waits
108// for PTY.Resize or loses a short read.
109func (d *Daemon) processPanePTYData(p *pane, data []byte, bufferDuringApply bool) {
110 if p == nil {
111 return
112 }
113 p.mu.Lock()
114 if bufferDuringApply && p.resizeApplying {
115 p.resizePending = append(p.resizePending, data...)
116 p.mu.Unlock()
117 return
118 }
119 effects := panePTYEffects{lease: p.effectLeaseLocked()}
120 owner := effects.lease.owner
121 if owner != nil && owner.session != nil {
122 effects.renderCoordinator = owner.session.renderCoordinator()
123 }
124 effects.wasSyncing = p.screen.SyncUpdateActive()
125 writePaneScreenLocked(p, data)
126 p.refreshTerminalTitleLocked()
127 effects.isSyncing = p.screen.SyncUpdateActive()
128 effects.completeSyncRead = !effects.wasSyncing && !effects.isSyncing && completedSynchronizedUpdate(data)
129 if effects.wasSyncing != effects.isSyncing && owner != nil && owner.session != nil {
130 if effects.isSyncing {
131 syncGen := owner.session.syncGen.Add(1)
132 p.syncGen = syncGen
133 if effects.renderCoordinator != nil {
134 lease := effects.lease
135 effects.syncCleanup = effects.renderCoordinator.beginSyncBatchWithRenderability(p, syncGen, func() bool {
136 return lease.Current() && d.paneRenderable(owner.session, owner.tab, p)
137 }, func() {
138 p.mu.Lock()
139 if p.owner.Load() == lease.owner && p.syncGen == syncGen && p.screen.SyncUpdateActive() {
140 p.screen.ForceSyncEnd()
141 }
142 p.mu.Unlock()
143 })
144 }
145 } else {
146 effects.syncEnded = effects.renderCoordinator != nil && effects.renderCoordinator.removeSyncEnd(p, p.syncGen, true)
147 }
148 }
149 effects.attention = p.ptyAttention
150 p.ptyAttention = false
151 effects.responses = append([]byte(nil), p.ptyResponses...)
152 effects.responses = filterUnsupportedKittyAnimationResponses(effects.responses)
153 p.ptyResponses = p.ptyResponses[:0]
154 effects.clipboards = append([]string(nil), p.ptyClipboards...)
155 p.ptyClipboards = p.ptyClipboards[:0]
156 p.mu.Unlock()
157 effects.syncCleanup.finish()
158 d.applyPanePTYEffects(p, effects)
159}
160
161// filterUnsupportedKittyAnimationResponses keeps older icat releases from
162// leaving vev's static-animation fallback replies in the shared PTY input
163// queue. Current Kitty sends animation controls with q=2, but older releases
164// requested error replies and then exited without consuming them. Transfer
165// capability probes use ordinary image IDs and retain their ENOTSUP replies.
166func filterUnsupportedKittyAnimationResponses(data []byte) []byte {
167 if !bytes.Contains(data, []byte("i=0,I=")) || !bytes.Contains(data, []byte(";ENOTSUP")) {
168 return data
169 }
170 out := make([]byte, 0, len(data))
171 for len(data) != 0 {
172 start := bytes.Index(data, []byte("\x1b_G"))
173 if start < 0 {
174 out = append(out, data...)
175 break
176 }
177 out = append(out, data[:start]...)
178 endOffset := bytes.Index(data[start+3:], []byte("\x1b\\"))
179 if endOffset < 0 {
180 out = append(out, data[start:]...)
181 break
182 }
183 end := start + 3 + endOffset + 2
184 record := data[start:end]
185 body := record[3 : len(record)-2]
186 if !bytes.Contains(body, []byte("i=0,I=")) || !bytes.HasSuffix(body, []byte(";ENOTSUP")) {
187 out = append(out, record...)
188 }
189 data = data[end:]
190 }
191 return out
192}
193
194// applyPanePTYEffects emits only effects still bound to the owner generation
195// that parsed them. Each owner-sensitive boundary revalidates independently so
196// ownership publication can retire the remaining effects without rerouting
197// bytes that belonged to the old owner.
198func (d *Daemon) applyPanePTYEffects(p *pane, effects panePTYEffects) {
199 owner := effects.lease.owner
200 if owner == nil && p.prestarted && len(effects.responses) > 0 {
201 // An unclaimed spare still answers its shell's startup queries; a
202 // closed spare only fails the write.
203 _, _ = p.pty.Write(effects.responses)
204 }
205 if owner == nil || owner.session == nil || owner.tab == nil {
206 return
207 }
208 if effects.lease.Current() {
209 markSnapshotDirty(owner.session)
210 }
211 if effects.attention && effects.lease.Current() {
212 d.noteAttention(owner.session, owner.tab)
213 }
214 for _, b64 := range effects.clipboards {
215 if effects.lease.Current() {
216 d.forwardClipboardAsync(effects.lease, b64)
217 }
218 }
219 if len(effects.responses) > 0 && effects.lease.Current() {
220 if _, err := p.pty.Write(effects.responses); err != nil {
221 if sessionName, current := paneEffectSessionName(effects.lease); current {
222 d.log.Warn("pty response write failed", "err", err, "session", sessionName)
223 }
224 }
225 }
226 if effects.renderCoordinator == nil || !effects.lease.Current() {
227 return
228 }
229 renderable := d.paneRenderable(owner.session, owner.tab, p)
230 if !effects.lease.Current() {
231 return
232 }
233 if effects.wasSyncing != effects.isSyncing {
234 if effects.isSyncing {
235 if renderable {
236 effects.renderCoordinator.invalidate(renderInvalidation{class: invalidateOutput, producer: "render.go"})
237 }
238 } else if effects.syncEnded && renderable {
239 effects.renderCoordinator.invalidate(renderInvalidation{class: invalidateUrgent, producer: "render.go"})
240 }
241 }
242 if effects.completeSyncRead && renderable {
243 effects.renderCoordinator.invalidate(renderInvalidation{class: invalidateUrgent, producer: "render.go"})
244 } else if renderable && !effects.isSyncing && effects.wasSyncing == effects.isSyncing {
245 effects.renderCoordinator.invalidate(renderInvalidation{class: invalidateOutput, producer: "render.go"})
246 }
247}
248
249// paint draws the composed client frame (active tab plus status bar) and
250// sends the resulting bytes. The renderer shadow is reset on explicit invalidations
251// such as switch/create/close/rename/resize so the repaint is complete.
252func (d *Daemon) observeRuntime(kind ports.RuntimeMarkKind, bytes uint64, valid bool) {
253 if d != nil && d.runtimeObserver != nil {
254 d.runtimeObserver.ObserveRuntime(ports.NewRuntimeMark("daemon", kind, bytes, valid))
255 }
256}
257
258// runtimeMarkBatch captures immutable runtime marks during a render transaction.
259// paint owns attachment sendMu while it captures, composes, and emits; JSONL
260// observer I/O must therefore happen only after that ownership is released.
261type runtimeMarkBatch struct {
262 observer ports.RuntimeObserver
263 marks []ports.RuntimeMark
264 attachmentEffect *attachmentEffect
265}
266
267func (d *Daemon) newRuntimeMarkBatch() runtimeMarkBatch {
268 if d == nil {
269 return runtimeMarkBatch{}
270 }
271 return runtimeMarkBatch{observer: d.runtimeObserver}
272}
273
274func (b *runtimeMarkBatch) span(start, end ports.RuntimeMarkKind, bytes uint64) func(uint64, bool) {
275 if b == nil || b.observer == nil {
276 return func(uint64, bool) {}
277 }
278 correlation := ports.NewRuntimeCorrelation()
279 b.marks = append(b.marks, ports.NewRuntimeMarkWithCorrelation("daemon", correlation, start, bytes, true))
280 return func(endBytes uint64, valid bool) {
281 b.marks = append(b.marks, ports.NewRuntimeMarkWithCorrelation("daemon", correlation, end, endBytes, valid))
282 }
283}
284
285func (b *runtimeMarkBatch) flush() {
286 if b == nil || b.observer == nil {
287 return
288 }
289 for _, mark := range b.marks {
290 b.observer.ObserveRuntime(mark)
291 }
292}
293
294type paintResult uint8
295
296const (
297 paintRejected paintResult = iota
298 paintEmitted
299 paintBlockedCapacity
300)
301
302func (d *Daemon) paint(entry *session, ac *attachedClient, reset bool, lease *attachmentLease) paintResult {
303 return d.paintWithActivationEffect(entry, ac, reset, lease, nil)
304}
305
306// activation owns the drained transition gate; only its explicit ticket may
307// publish while ordinary input and render admission remain disabled.
308func (d *Daemon) paintWithActivationEffect(entry *session, ac *attachedClient, reset bool, lease *attachmentLease, activation *attachmentEffect) paintResult {
309 // Session-owned PTY preparation remains local; attachment rendering is
310 // captured through session.captureRenderState.
311 sess := entry
312 local := sess != nil
313 marks := d.newRuntimeMarkBatch()
314 var paintEffect *attachmentEffect
315 if activation != nil {
316 paintEffect = activation
317 marks.attachmentEffect = activation
318 } else if ac.attachmentActivity() != attachmentActive {
319 return paintRejected
320 } else if lease != nil {
321 token := captureAttachmentCapability(entry, ac, ac.transport())
322 token.lease = lease
323 ticket, admitted := ac.beginAttachmentEffect(token)
324 if !admitted {
325 return paintRejected
326 }
327 paintEffect = ticket
328 marks.attachmentEffect = ticket
329 defer ticket.End()
330 }
331 var tb *tab
332 if local {
333 tb = sess.tabForAttachment(ac)
334 if tb == nil {
335 return paintRejected
336 }
337 }
338
339 ac.sendMu.Lock()
340 result, afterUnlock := d.paintLocked(entry, ac, tb, reset, lease, paintEffect, &marks)
341 ac.sendMu.Unlock()
342 afterUnlock()
343 return result
344}
345
346func noAfterUnlock() {}
347
348// paintLocked is the complete capture/compose/publish transaction. It runs with
349// ac.sendMu held and never releases it; paint is the sole owner of that lock.
350// The returned function runs after sendMu is released, with no attachment,
351// session, tab, or pane lock held.
352func (d *Daemon) paintLocked(entry *session, ac *attachedClient, tb *tab, reset bool, lease *attachmentLease, paintEffect *attachmentEffect, marks *runtimeMarkBatch) (paintResult, func()) {
353 sess := entry
354 local := sess != nil
355 if paintEffect != nil {
356 if !paintEffect.current() {
357 return paintRejected, noAfterUnlock
358 }
359 } else {
360 // Direct/headless rendering has no Attachment effect and retains its
361 // membership-only fixture path.
362 entry.core().mu.Lock()
363 _, owned := entry.core().attachments[ac]
364 entry.core().mu.Unlock()
365 if !owned || ac.currentAttachmentSession() != entry {
366 return paintRejected, noAfterUnlock
367 }
368 }
369 // Capacity is checked before any destructive capture. Refresh the atomic
370 // readiness snapshot while sendMu is held so direct test/setup mutations of
371 // the stream fields remain covered without making coordinator probes wait on
372 // a blocked transport send.
373 if ac.output != nil {
374 ac.output.syncCapacityLocked()
375 }
376 // The coordinator is the normal gate, but this ownership check also protects
377 // direct resize and test paint paths from consuming damage that cannot be
378 // emitted.
379 if ac.output != nil && ac.output.atCapacity() {
380 // The coordinator owns the blocked interval; this guard only protects
381 // direct test/resize paint calls from destructive capture.
382 return paintBlockedCapacity, noAfterUnlock
383 }
384 var uiFence uint64
385 if lease != nil {
386 if rc := attachmentRenderCoordinator(entry); rc != nil {
387 uiFence = rc.captureUIFence(lease)
388 }
389 }
390 // Composition owns attachment sendMu; initialize its lazy overlay state
391 // under that same ownership so concurrent fallback paints cannot observe a
392 // partially published runtime.
393 ac.initOverlays()
394 frameCapture := ac.afterFrame.beginCapture()
395 overlays := ac.overlays.SnapshotForRender()
396 repaintAttachedClients := false
397 var published bool
398 var followUp frameFollowUp
399 afterUnlock := func() {
400 // Frame follow-up (notices, repaints, send-error cleanup) runs first,
401 // then the captured sequence is emitted before any follow-up repaint can
402 // introduce another boundary.
403 if published {
404 d.settleFrame(entry, ac, marks, followUp)
405 }
406 overlays.Unlock()
407 marks.flush()
408 ac.afterFrame.runReady()
409 if repaintAttachedClients {
410 d.repaintAllAttachedClients()
411 }
412 }
413 if local {
414 d.refreshSessionFocusedTitles(sess)
415 }
416 // Contextual ranks are completely captured under paletteMu. Compose them
417 // without reading the live MRU, whose order may have changed mid-interaction.
418 statusFeedback := overlays.statusFeedback
419 if statusFeedback == "" && overlays.resizeActive {
420 statusFeedback = "resize: h/j/k/l or arrows · = equalize · q/esc/enter exit"
421 }
422 bars := d.barStateForAttachmentPaletteHintsFor(entry, ac, statusFeedback, overlays.paletteHints, overlays.paletteRouteSnapshot)
423 applied := ac.getAppliedTheme()
424 bars.theme = applied.Raw
425 if local {
426 attentionVisible := pulseVisible(bars.attentionFrame)
427 repaintAttachedClients = sess.ackAttention(tb, ac, attentionVisible)
428 }
429
430 // One snapshot keeps palette and floating settings from the same reload.
431 cfg := d.currentRuntimeConfig()
432 paletteCfg, floatingCfg := cfg.palette, cfg.floating
433 // Title refresh may inspect process state, so it remains before the capture
434 // boundary and outside tab/pane ownership locks.
435 if local {
436 tb.mu.Lock()
437 titleIDs := ac.render.capture.titleIDs[:0]
438 if tb.tree != nil {
439 titleIDs = appendStackPaneIDs(titleIDs, tb.tree.Root)
440 }
441 ac.render.capture.titleIDs = titleIDs
442 floating := tb.floating.pane
443 hasFloating := tb.floating.state == floatingVisible && floating != nil
444 tb.mu.Unlock()
445 for _, id := range titleIDs {
446 d.refreshPaneTitle(sess, id, tb)
447 }
448 if hasFloating {
449 d.refreshPaneDisplayTitle(sess, floating, false)
450 }
451 }
452
453 capturedOverlays := capturedOverlayRenderState{
454 copyActive: overlays.copyActive, copySearchActive: overlays.copySearchModel != nil,
455 paletteActive: overlays.paletteActive, promptActive: overlays.promptActive,
456 resizeActive: overlays.resizeActive, statusFeedback: statusFeedback,
457 }
458 endCapture := marks.span(ports.RuntimeCaptureStart, ports.RuntimeCaptureEnd, 0)
459 state, ok := entry.captureRenderState(ac, renderCaptureRequest{
460 bars: bars,
461 overlays: capturedOverlays,
462 floatingCfg: floatingCfg,
463 styles: applied.Resolved.Styles,
464 styleGeneration: applied.Generation,
465 reset: reset,
466 lease: lease,
467 uiFence: uiFence,
468 })
469 endCapture(0, ok)
470 if !ok {
471 return paintRejected, afterUnlock
472 }
473 if ac.renderStages.capture != nil {
474 ac.renderStages.capture()
475 }
476 state.frameCapture = frameCapture
477 captureOverlayLayers(state, overlays, paletteCfg)
478 endCompose := marks.span(ports.RuntimeComposeStart, ports.RuntimeComposeEnd, 0)
479 composed := composeFrame(*state, ac.render.cache, ac.render.spare)
480 endCompose(0, true)
481 if ac.renderStages.compose != nil {
482 ac.renderStages.compose()
483 }
484 followUp, published = d.publishFrameLocked(entry, ac, state, composed, marks)
485 return paintEmitted, afterUnlock
486}
487
488var fallbackChromeStyles = themeui.Resolve(themeui.Theme{}, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}).Styles
489
490var (
491 fallbackANSI16Styles = themeui.ResolveForColor(themeui.Theme{}, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}).Styles
492 fallbackMonoStyles = themeui.ResolveForColor(themeui.Theme{}, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome}).Styles
493)
494
495// fallbackStylesFor is the static pre-theme chrome for one attachment's mode.
496func fallbackStylesFor(color terminalcap.ColorCapabilities) themeui.Styles {
497 switch color.Mode {
498 case terminalcap.ANSI16:
499 return fallbackANSI16Styles
500 case terminalcap.Monochrome:
501 return fallbackMonoStyles
502 default:
503 return fallbackChromeStyles
504 }
505}
506
507func resolveStyles(styles []themeui.Styles) themeui.Styles {
508 if len(styles) > 0 {
509 return styles[0]
510 }
511 return fallbackChromeStyles
512}
513
514type barCache struct {
515 top []renderer.Cell
516 bottom []renderer.Cell
517}
518
519func (c *barCache) Reset() {
520 c.top = nil
521 c.bottom = nil
522}
523
524func offsetPlacement(pl layout.Placement, dx, dy int) layout.Placement {
525 pl.Content.X += dx
526 pl.Content.Y += dy
527 pl.TitleBar.X += dx
528 pl.TitleBar.Y += dy
529 return pl
530}
531
532func blitPaneFrame(dst renderer.Frame, r domain.Rect, src renderer.Frame, dim bool, dimmer themeui.Dimmer) {
533 startX, startY := max(r.X, 0), max(r.Y, 0)
534 endX, endY := min(r.X+r.Width, dst.Width), min(r.Y+r.Height, dst.Height)
535 for y := startY; y < endY; y++ {
536 sy := y - r.Y
537 if sy < 0 || sy >= src.Height {
538 continue
539 }
540 for x := startX; x < endX; x++ {
541 sx := x - r.X
542 if sx < 0 || sx >= src.Width {
543 continue
544 }
545 cell := src.At(sx, sy)
546 if dim {
547 cell.Style = dimmer.Dim(cell.Style)
548 }
549 dst.Set(x, y, cell)
550 }
551 }
552}
553
554// drawDividers paints each precomputed divider gap onto frame, offsetting by
555// dy (composeFrame's content area starts one row below the top status bar).
556// The dividers themselves come from layout.SolveWithDividers, computed once
557// per capture alongside pane placements, rather than recomputed here.
558func drawDividers(frame renderer.Frame, dividers []layout.Divider, dy int, style renderer.Style) {
559 for _, d := range dividers {
560 glyph := rune('│')
561 if d.Dir == layout.Vertical {
562 glyph = '─'
563 }
564 y0 := max(d.Rect.Y+dy, 0)
565 y1 := min(d.Rect.Y+dy+d.Rect.Height, frame.Height)
566 x0 := max(d.Rect.X, 0)
567 x1 := min(d.Rect.X+d.Rect.Width, frame.Width)
568 for y := y0; y < y1; y++ {
569 for x := x0; x < x1; x++ {
570 frame.Set(x, y, renderer.Cell{Rune: glyph, Style: style})
571 }
572 }
573 }
574}
575
576func (c *barCache) capture(top, bottom []renderer.Cell) {
577 c.top = cloneCells(c.top, top)
578 c.bottom = cloneCells(c.bottom, bottom)
579}
580
581func cloneCells(dst, src []renderer.Cell) []renderer.Cell {
582 if cap(dst) < len(src) {
583 dst = make([]renderer.Cell, len(src))
584 } else {
585 dst = dst[:len(src)]
586 }
587 copy(dst, src)
588 return dst
589}
590
591func sameCells(a, b []renderer.Cell) bool {
592 if len(a) != len(b) {
593 return false
594 }
595 for i := range a {
596 if !a[i].Equal(b[i]) {
597 return false
598 }
599 }
600 return true
601}
602
603func offsetDamage(in []renderer.Damage) []renderer.Damage {
604 return translateDamage(in, 0, 1)
605}
606
607func translateDamage(in []renderer.Damage, dx, dy int) []renderer.Damage {
608 out := make([]renderer.Damage, len(in))
609 for i, d := range in {
610 out[i] = d
611 if d.Kind != renderer.DamageFullRedraw {
612 out[i].X += dx
613 out[i].Y += dy
614 }
615 }
616 return out
617}
618
619func translatePaneDamage(d renderer.Damage, content domain.Rect, area domain.Rect) []renderer.Damage {
620 if d.Kind == renderer.DamageFullRedraw {
621 return []renderer.Damage{d}
622 }
623 if d.Kind == renderer.DamageScrollUp && (content.X != 0 || content.Width != area.Width) {
624 return []renderer.Damage{{Kind: renderer.DamageText, X: content.X, Y: content.Y, Width: content.Width, Height: content.Height}}
625 }
626 d.X += content.X
627 d.Y += content.Y
628 return []renderer.Damage{d}
629}