Skip to content

Commit fbace04

Browse files
authored
Add IP inspect shortcut to domain lookups (#5)
1 parent f450276 commit fbace04

1 file changed

Lines changed: 157 additions & 35 deletions

File tree

internal/display/interactive.go

Lines changed: 157 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package display
22

33
import (
4+
"context"
45
"fmt"
6+
"net"
57
"strings"
8+
"time"
69

710
"github.com/charmbracelet/bubbles/spinner"
811
"github.com/charmbracelet/bubbles/viewport"
@@ -66,30 +69,35 @@ var (
6669
const chromeHeight = 4
6770

6871
type Model struct {
69-
domain string
70-
isIP bool
71-
active tab
72-
showRaw bool
73-
info *model.DomainInfo
74-
ipInfo *rdap.IPInfo
75-
whoisErr error
76-
dnsData *dns.Records
77-
mailData *mail.Records
78-
tlsData *tlsinfo.CertInfo
79-
httpData *httpinfo.Result
80-
stackData *stack.Result
81-
dnsErr error
82-
mailErr error
83-
tlsErr error
84-
httpErr error
85-
stackErr error
86-
loading bool
87-
quitting bool
88-
viewport viewport.Model
89-
spinner spinner.Model
90-
ready bool
91-
width int
92-
height int
72+
domain string
73+
isIP bool
74+
active tab
75+
showRaw bool
76+
info *model.DomainInfo
77+
ipInfo *rdap.IPInfo
78+
whoisErr error
79+
dnsData *dns.Records
80+
mailData *mail.Records
81+
tlsData *tlsinfo.CertInfo
82+
httpData *httpinfo.Result
83+
stackData *stack.Result
84+
dnsErr error
85+
mailErr error
86+
tlsErr error
87+
httpErr error
88+
stackErr error
89+
ipJumpErr error
90+
prevDomain string
91+
prevInfo *model.DomainInfo
92+
prevWhoisErr error
93+
resolvingIP bool
94+
loading bool
95+
quitting bool
96+
viewport viewport.Model
97+
spinner spinner.Model
98+
ready bool
99+
width int
100+
height int
93101
}
94102

95103
type whoisResultMsg struct {
@@ -127,6 +135,11 @@ type stackResultMsg struct {
127135
err error
128136
}
129137

138+
type resolveIPResultMsg struct {
139+
ip string
140+
err error
141+
}
142+
130143
func NewModel(domain string) Model {
131144
s := spinner.New()
132145
s.Spinner = spinner.Dot
@@ -169,17 +182,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
169182
m.width = msg.Width
170183
m.height = msg.Height
171184
SetWidth(m.width)
172-
173-
chrome := chromeHeight
174-
if m.isIP {
175-
chrome = 3 // top border + bottom border + footer (no tab bar)
176-
}
177-
vpHeight := m.height - chrome
178-
if vpHeight < 1 {
179-
vpHeight = 1
180-
}
181-
182-
vpWidth := innerWidth()
185+
vpWidth, vpHeight := m.viewportSize()
183186

184187
if !m.ready {
185188
m.viewport = viewport.New(vpWidth, vpHeight)
@@ -205,6 +208,27 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
205208
m.quitting = true
206209
return m, tea.Quit
207210
case "esc", "w":
211+
if m.isIP && m.prevDomain != "" {
212+
m.isIP = false
213+
m.domain = m.prevDomain
214+
m.active = tabWhois
215+
m.showRaw = false
216+
m.loading = false
217+
m.resolvingIP = false
218+
m.info = m.prevInfo
219+
m.whoisErr = m.prevWhoisErr
220+
m.ipInfo = nil
221+
m.prevDomain = ""
222+
m.prevInfo = nil
223+
m.prevWhoisErr = nil
224+
m.ipJumpErr = nil
225+
if m.ready {
226+
m.applyViewportSize()
227+
m.updateViewport()
228+
m.viewport.GotoTop()
229+
}
230+
return m, nil
231+
}
208232
m.switchTab(tabWhois)
209233
return m, nil
210234
case "d":
@@ -254,6 +278,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
254278
m.viewport.GotoTop()
255279
return m, nil
256280
}
281+
case "i":
282+
if !m.isIP && m.active == tabWhois {
283+
m.ipJumpErr = nil
284+
m.resolvingIP = true
285+
m.loading = true
286+
m.updateViewport()
287+
return m, resolveFirstIP(m.domain)
288+
}
257289
}
258290

259291
case spinner.TickMsg:
@@ -267,6 +299,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
267299

268300
case whoisResultMsg:
269301
m.loading = false
302+
m.resolvingIP = false
270303
if msg.err != nil {
271304
m.whoisErr = msg.err
272305
} else {
@@ -276,7 +309,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
276309
return m, nil
277310

278311
case ipResultMsg:
312+
if !m.isIP {
313+
return m, nil
314+
}
279315
m.loading = false
316+
m.resolvingIP = false
280317
if msg.err != nil {
281318
m.whoisErr = msg.err
282319
} else {
@@ -287,38 +324,72 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
287324

288325
case mailResultMsg:
289326
m.loading = false
327+
m.resolvingIP = false
290328
m.mailData = msg.records
291329
m.mailErr = msg.err
292330
m.updateViewport()
293331
return m, nil
294332

295333
case dnsResultMsg:
296334
m.loading = false
335+
m.resolvingIP = false
297336
m.dnsData = msg.records
298337
m.dnsErr = msg.err
299338
m.updateViewport()
300339
return m, nil
301340

302341
case tlsResultMsg:
303342
m.loading = false
343+
m.resolvingIP = false
304344
m.tlsData = msg.cert
305345
m.tlsErr = msg.err
306346
m.updateViewport()
307347
return m, nil
308348

309349
case httpResultMsg:
310350
m.loading = false
351+
m.resolvingIP = false
311352
m.httpData = msg.result
312353
m.httpErr = msg.err
313354
m.updateViewport()
314355
return m, nil
315356

316357
case stackResultMsg:
317358
m.loading = false
359+
m.resolvingIP = false
318360
m.stackData = msg.result
319361
m.stackErr = msg.err
320362
m.updateViewport()
321363
return m, nil
364+
365+
case resolveIPResultMsg:
366+
m.loading = false
367+
m.resolvingIP = false
368+
if msg.err != nil {
369+
m.ipJumpErr = msg.err
370+
m.updateViewport()
371+
return m, nil
372+
}
373+
m.ipJumpErr = nil
374+
m.prevDomain = m.domain
375+
m.prevInfo = m.info
376+
m.prevWhoisErr = m.whoisErr
377+
m.isIP = true
378+
m.domain = msg.ip
379+
m.active = tabWhois
380+
m.showRaw = false
381+
m.loading = true
382+
m.info = nil
383+
m.ipInfo = nil
384+
m.whoisErr = nil
385+
386+
if m.ready {
387+
m.applyViewportSize()
388+
m.updateViewport()
389+
m.viewport.GotoTop()
390+
}
391+
392+
return m, fetchIP(msg.ip)
322393
}
323394

324395
var cmd tea.Cmd
@@ -331,6 +402,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
331402
func (m *Model) switchTab(t tab) {
332403
m.active = t
333404
m.showRaw = false
405+
m.ipJumpErr = nil
334406
m.updateViewport()
335407
m.viewport.GotoTop()
336408
}
@@ -360,6 +432,9 @@ func (m Model) contentForTab(t tab) string {
360432
}
361433
return ""
362434
}
435+
if m.loading && m.resolvingIP {
436+
return m.loadingText("Resolving IP...")
437+
}
363438
if m.loading && m.info == nil {
364439
return m.loadingText("Looking up WHOIS...")
365440
}
@@ -484,6 +559,14 @@ func (m Model) View() string {
484559
footerParts = append(footerParts, "r raw")
485560
}
486561
}
562+
if m.ipJumpErr != nil {
563+
footerParts = append(footerParts, "i failed")
564+
} else if !m.isIP && m.active == tabWhois {
565+
footerParts = append(footerParts, "i inspect ip")
566+
}
567+
if m.isIP && m.prevDomain != "" {
568+
footerParts = append(footerParts, "esc/w back")
569+
}
487570
footerParts = append(footerParts, "q quit")
488571
b.WriteString(footerStyle.Render(strings.Join(footerParts, " • ")))
489572

@@ -558,6 +641,45 @@ func fetchIP(ip string) tea.Cmd {
558641
}
559642
}
560643

644+
func resolveFirstIP(domain string) tea.Cmd {
645+
return func() tea.Msg {
646+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
647+
defer cancel()
648+
ips, err := net.DefaultResolver.LookupIPAddr(ctx, domain)
649+
if err != nil {
650+
return resolveIPResultMsg{err: err}
651+
}
652+
if len(ips) == 0 {
653+
return resolveIPResultMsg{err: fmt.Errorf("no IP found for %s", domain)}
654+
}
655+
656+
for _, ip := range ips {
657+
if v4 := ip.IP.To4(); v4 != nil {
658+
return resolveIPResultMsg{ip: v4.String()}
659+
}
660+
}
661+
return resolveIPResultMsg{ip: ips[0].IP.String()}
662+
}
663+
}
664+
665+
func (m Model) viewportSize() (int, int) {
666+
chrome := chromeHeight
667+
if m.isIP {
668+
chrome = 3 // top border + bottom border + footer (no tab bar)
669+
}
670+
vpHeight := m.height - chrome
671+
if vpHeight < 1 {
672+
vpHeight = 1
673+
}
674+
return innerWidth(), vpHeight
675+
}
676+
677+
func (m *Model) applyViewportSize() {
678+
vpWidth, vpHeight := m.viewportSize()
679+
m.viewport.Width = vpWidth
680+
m.viewport.Height = vpHeight
681+
}
682+
561683
func fetchWhois(domain string) tea.Cmd {
562684
return func() tea.Msg {
563685
info, err := resolver.Lookup(domain)

0 commit comments

Comments
 (0)