11package display
22
33import (
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 (
6669const chromeHeight = 4
6770
6871type 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
95103type 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+
130143func 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) {
331402func (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+
561683func fetchWhois (domain string ) tea.Cmd {
562684 return func () tea.Msg {
563685 info , err := resolver .Lookup (domain )
0 commit comments