@@ -37,6 +37,7 @@ type layoutMetrics struct {
3737 height int
3838 operatorWidth int
3939 rightWidth int
40+ detailsWidth int
4041 activityWidth int
4142 activityHeight int
4243 footerHeight int
@@ -73,7 +74,7 @@ const (
7374 formNoteWrapWidth = 24
7475 formSidebarWrapWidth = 20
7576 uiSeparatorLine = "────────────────────────"
76- uiVersionLabel = "v0.1.6-rc1 "
77+ uiVersionLabel = "v0.1.6-rc3 "
7778 operatorPlaceholder = "Paste or Import"
7879 customOperatorKey = "custom"
7980 customOperatorName = "Custom Targets"
@@ -365,7 +366,7 @@ func (u *ui) layoutModeForSize(width, height int) layoutMode {
365366 if width <= 0 || height <= 0 {
366367 return layoutWide
367368 }
368- if width < 170 || height < 40 {
369+ if width < 180 || height < 40 {
369370 return layoutCompact
370371 }
371372 return layoutWide
@@ -450,6 +451,7 @@ func (u *ui) calculateLayoutMetrics(width, height int) layoutMetrics {
450451 metrics .formNoteWidth = clampInt (contentWidth / 5 + 18 , 28 , 42 )
451452 metrics .formSidebarWidth = clampInt (contentWidth / 4 + 8 , 24 , 36 )
452453 detailsWidth := max (contentWidth - metrics .activityWidth - 4 , 36 )
454+ metrics .detailsWidth = detailsWidth
453455 metrics .detailsGuideWidth = clampGuideWrapWidth (detailsWidth - 3 )
454456 default :
455457 metrics .operatorWidth = clampInt (width / 6 , 24 , 30 )
@@ -458,6 +460,7 @@ func (u *ui) calculateLayoutMetrics(width, height int) layoutMetrics {
458460 metrics .formNoteWidth = formNoteWrapWidth
459461 metrics .formSidebarWidth = formSidebarWrapWidth
460462 detailsWidth := max (width - metrics .operatorWidth - metrics .rightWidth - 6 , 48 )
463+ metrics .detailsWidth = detailsWidth
461464 metrics .detailsGuideWidth = clampGuideWrapWidth (detailsWidth - 3 )
462465 }
463466
@@ -502,6 +505,18 @@ func clampGuideWrapWidth(width int) int {
502505 return width
503506}
504507
508+ func (u * ui ) detailsRenderWidth () int {
509+ width := u .layoutState .detailsWidth
510+ if width <= 0 {
511+ width = 80
512+ }
513+ width -= 3
514+ if width < 32 {
515+ return 32
516+ }
517+ return width
518+ }
519+
505520func (u * ui ) populateOperators () {
506521 u .operatorList .AddItem (operatorPlaceholder , "" , 0 , nil )
507522 for _ , op := range u .operators {
@@ -511,6 +526,7 @@ func (u *ui) populateOperators() {
511526}
512527
513528func (u * ui ) handleKeys (event * tcell.EventKey ) * tcell.EventKey {
529+ event = normalizeNavigationEvent (event )
514530 if u .focusIsEditable () {
515531 if isClipboardPasteEvent (event ) {
516532 if err := u .pasteClipboardIntoFocusedEditable (); err != nil {
@@ -606,6 +622,34 @@ func (u *ui) handleKeys(event *tcell.EventKey) *tcell.EventKey {
606622 return event
607623}
608624
625+ func normalizeNavigationEvent (event * tcell.EventKey ) * tcell.EventKey {
626+ if event == nil {
627+ return nil
628+ }
629+
630+ shift := event .Modifiers ()& tcell .ModShift != 0
631+ switch {
632+ case event .Key () == tcell .KeyBacktab :
633+ return tcell .NewEventKey (tcell .KeyBacktab , 0 , tcell .ModNone )
634+ case event .Key () == tcell .KeyEnter || event .Key () == tcell .KeyCtrlM :
635+ return tcell .NewEventKey (tcell .KeyEnter , 0 , tcell .ModNone )
636+ case event .Key () == tcell .KeyTab || event .Key () == tcell .KeyCtrlI :
637+ if shift {
638+ return tcell .NewEventKey (tcell .KeyBacktab , 0 , tcell .ModNone )
639+ }
640+ return tcell .NewEventKey (tcell .KeyTab , 0 , tcell .ModNone )
641+ case event .Key () == tcell .KeyRune && event .Rune () == '\r' :
642+ return tcell .NewEventKey (tcell .KeyEnter , 0 , tcell .ModNone )
643+ case event .Key () == tcell .KeyRune && event .Rune () == '\t' :
644+ if shift {
645+ return tcell .NewEventKey (tcell .KeyBacktab , 0 , tcell .ModNone )
646+ }
647+ return tcell .NewEventKey (tcell .KeyTab , 0 , tcell .ModNone )
648+ default :
649+ return event
650+ }
651+ }
652+
609653func (u * ui ) cycleFocus () {
610654 targets := u .focusTargets ()
611655 current := u .app .GetFocus ()
@@ -1704,21 +1748,7 @@ func (u *ui) renderDetails() {
17041748 }
17051749 } else {
17061750 builder .WriteString ("DNS Hosts\n " )
1707- for index , resolver := range resolvers {
1708- fmt .Fprintf (& builder , "%02d %-15s %-4s %d/6 %-34s %5d ms %-7s %s\n " ,
1709- index + 1 ,
1710- resolver .IP ,
1711- displayTransport (resolver .Transport ),
1712- resolver .TunnelScore ,
1713- resolverTunnelDetails (resolver ),
1714- resolver .LatencyMillis ,
1715- dnsttStatusLabel (resolver ),
1716- resolver .Prefix ,
1717- )
1718- if showDNSTTError (resolver ) {
1719- fmt .Fprintf (& builder , " dnstt error: %s\n " , displayDNSTTError (resolver .DNSTTError ))
1720- }
1721- }
1751+ writeResolverRows (& builder , resolvers , customTargets , u .detailsRenderWidth ())
17221752 }
17231753 case screenDNSTT :
17241754 u .details .SetTitle ("DNSTT Details" )
@@ -1729,6 +1759,7 @@ func (u *ui) renderDetails() {
17291759 activeScanForCurrentOperator := u .activeScanOperator == operatorKey && u .scanCancel != nil
17301760 activeDNSTTForCurrentOperator := u .activeDNSTTOperator == operatorKey && u .dnsttCancel != nil
17311761 lookup , lookupLoaded := u .lookupCache [operatorKey ]
1762+ customTargets := lookupLoaded && lookupUsesCustomTargets (lookup )
17321763 u .writeStageWorkflow (& builder , operatorKey , lookup , lookupLoaded , progress , dnsttState , activeScanForCurrentOperator , activeDNSTTForCurrentOperator , qualifiedCount )
17331764 writeDNSTTOptionGuide (& builder , u .detailsGuideWrapWidth ())
17341765 fmt .Fprintf (& builder , "Operator: %s\n " , operator .Name )
@@ -1780,21 +1811,7 @@ func (u *ui) renderDetails() {
17801811 builder .WriteString ("No DNS services reached yet.\n " )
17811812 } else {
17821813 builder .WriteString ("DNS Hosts\n " )
1783- for index , resolver := range resolvers {
1784- fmt .Fprintf (& builder , "%02d %-15s %-4s %d/6 %-34s %5d ms %-7s %s\n " ,
1785- index + 1 ,
1786- resolver .IP ,
1787- displayTransport (resolver .Transport ),
1788- resolver .TunnelScore ,
1789- resolverTunnelDetails (resolver ),
1790- resolver .LatencyMillis ,
1791- dnsttStatusLabel (resolver ),
1792- resolver .Prefix ,
1793- )
1794- if showDNSTTError (resolver ) {
1795- fmt .Fprintf (& builder , " dnstt error: %s\n " , displayDNSTTError (resolver .DNSTTError ))
1796- }
1797- }
1814+ writeResolverRows (& builder , resolvers , customTargets , u .detailsRenderWidth ())
17981815 }
17991816 }
18001817
@@ -3401,13 +3418,15 @@ func dnsttStatusLabel(resolver model.Resolver) string {
34013418func resolverTunnelDetails (resolver model.Resolver ) string {
34023419 flag := func (ok bool , label string ) string {
34033420 if ok {
3404- return label + "✓ "
3421+ return label + "+ "
34053422 }
3406- return label + "✗ "
3423+ return label + "- "
34073424 }
3408- edns := flag ( resolver . TunnelEDNS0Support , "EDNS" )
3425+ edns := "E-"
34093426 if resolver .TunnelEDNS0Support && resolver .TunnelEDNSMaxPayload > 0 {
3410- edns += fmt .Sprintf ("(%d)" , resolver .TunnelEDNSMaxPayload )
3427+ edns = fmt .Sprintf ("E%d" , resolver .TunnelEDNSMaxPayload )
3428+ } else if resolver .TunnelEDNS0Support {
3429+ edns = "E+"
34113430 }
34123431 return fmt .Sprintf ("%s %s %s %s %s %s" ,
34133432 flag (resolver .TunnelNSSupport , "NS" ),
@@ -3423,6 +3442,47 @@ func showDNSTTError(resolver model.Resolver) bool {
34233442 return resolver .DNSTTChecked && ! resolver .DNSTTE2EOK && strings .TrimSpace (resolver .DNSTTError ) != ""
34243443}
34253444
3445+ func writeResolverRows (builder * strings.Builder , resolvers []model.Resolver , customTargets bool , width int ) {
3446+ for index , resolver := range resolvers {
3447+ writeClippedDetailLine (builder , fmt .Sprintf ("%02d %s %s %d/6 %dms %s" ,
3448+ index + 1 ,
3449+ resolver .IP ,
3450+ displayTransport (resolver .Transport ),
3451+ resolver .TunnelScore ,
3452+ resolver .LatencyMillis ,
3453+ dnsttStatusLabel (resolver ),
3454+ ), width )
3455+ writeClippedDetailLine (builder , " caps: " + resolverTunnelDetails (resolver ), width )
3456+ if target := displayResolverTarget (resolver , customTargets ); target != "" {
3457+ writeClippedDetailLine (builder , " target: " + target , width )
3458+ }
3459+ if showDNSTTError (resolver ) {
3460+ writeClippedDetailLine (builder , " dnstt error: " + displayDNSTTError (resolver .DNSTTError ), width )
3461+ }
3462+ }
3463+ }
3464+
3465+ func writeClippedDetailLine (builder * strings.Builder , text string , width int ) {
3466+ builder .WriteString (truncateDisplayText (text , width ))
3467+ builder .WriteByte ('\n' )
3468+ }
3469+
3470+ func truncateDisplayText (text string , width int ) string {
3471+ trimmed := strings .TrimRight (text , " " )
3472+ if width <= 0 {
3473+ return trimmed
3474+ }
3475+
3476+ runes := []rune (trimmed )
3477+ if len (runes ) <= width {
3478+ return trimmed
3479+ }
3480+ if width <= 3 {
3481+ return string (runes [:width ])
3482+ }
3483+ return string (runes [:width - 3 ]) + "..."
3484+ }
3485+
34263486func displayDNSTTError (value string ) string {
34273487 text := strings .TrimSpace (value )
34283488 if len (text ) <= 120 {
@@ -4110,6 +4170,20 @@ func displayTargetEntry(entry model.PrefixEntry, imported bool) string {
41104170 return entry .Prefix
41114171}
41124172
4173+ func displayResolverTarget (resolver model.Resolver , customTargets bool ) string {
4174+ prefix := strings .TrimSpace (resolver .Prefix )
4175+ if prefix == "" {
4176+ return ""
4177+ }
4178+ if addr , ok := singleIPFromPrefix (prefix ); ok && strings .TrimSpace (resolver .IP ) == addr {
4179+ return ""
4180+ }
4181+ if customTargets {
4182+ return displayTargetEntry (model.PrefixEntry {Prefix : prefix }, true )
4183+ }
4184+ return prefix
4185+ }
4186+
41134187func singleIPFromPrefix (prefix string ) (string , bool ) {
41144188 parsed , err := netip .ParsePrefix (prefix )
41154189 if err != nil {
0 commit comments