Skip to content

Commit fbdc9f6

Browse files
committed
Release v0.1.6-rc3
1 parent 2f3d6e0 commit fbdc9f6

4 files changed

Lines changed: 265 additions & 40 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ build-windows:
2626
@$(MAKE) build-dist TARGET_OS=windows TARGET_ARCH=amd64
2727

2828
release-check:
29-
@test -n "$(RELEASE_VERSION)" || (echo "release builds require HEAD to be tagged (for example v0.1.6 or v0.1.6-rc1)"; exit 1)
29+
@test -n "$(RELEASE_VERSION)" || (echo "release builds require HEAD to be tagged (for example v0.1.6 or v0.1.6-rc3)"; exit 1)
3030
@test -z "$(WORKTREE_STATUS)" || (echo "release builds require a clean git worktree"; exit 1)
3131

3232
release: release-check

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ The git tag is the release source of truth. This matches a normal GitFlow
5959
process:
6060

6161
- tag `v0.1.6` for a final release
62-
- tag `v0.1.6-rc1` or `v0.1.6-rc2` for release candidates
62+
- tag `v0.1.6-rc3` for the current release candidate
6363

6464
To build a release artifact from the current tag:
6565

@@ -75,7 +75,7 @@ make release-windows
7575

7676
Release builds are intentionally strict:
7777

78-
- `HEAD` must be exactly on a tag such as `v0.1.6` or `v0.1.6-rc1`
78+
- `HEAD` must be exactly on a tag such as `v0.1.6` or `v0.1.6-rc3`
7979
- the git worktree must be clean
8080

8181
If those checks pass, the artifact filename will match the release tag exactly.
@@ -140,7 +140,7 @@ Notes:
140140
- Relative import paths are resolved relative to the `config.json` directory.
141141
- `Save Config` keeps import paths relative to the config file when possible, so shared configs stay portable.
142142
- The config file sets startup defaults; it does not auto-run imports, scans, or DNSTT.
143-
- Ask bug reporters to include the version shown in the header, for example `v0.1.6-rc1`.
143+
- Ask bug reporters to include the version shown in the header, for example `v0.1.6-rc3`.
144144

145145
## Quick Guide
146146

ui.go

Lines changed: 110 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
505520
func (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

513528
func (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+
609653
func (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 {
34013418
func 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+
34263486
func 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+
41134187
func singleIPFromPrefix(prefix string) (string, bool) {
41144188
parsed, err := netip.ParsePrefix(prefix)
41154189
if err != nil {

0 commit comments

Comments
 (0)