Skip to content

Commit 05fdcf8

Browse files
authored
Merge pull request #14 from Daniel-Ric/feature/2026-02-03/add-animated-progress-bar-for-ip-lookups
Animated progress bar and detailed lookup progress
2 parents d4b3cbb + c2a755c commit 05fdcf8

3 files changed

Lines changed: 98 additions & 25 deletions

File tree

internal/cli/cli.go

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,8 @@ func (a *App) executeDirect(config DirectConfig) error {
355355
ctx, cancel := context.WithTimeout(context.Background(), a.inputTimeout)
356356
defer cancel()
357357

358-
resultText, err := withSpinner("Abfrage", func() string {
358+
resultText, err := withSpinner("Abfrage", func(frame int) string {
359+
_ = frame
359360
return "Server wird abgefragt"
360361
}, 120*time.Millisecond, func() (string, error) {
361362
result, err := ping.Execute(ctx, config.Edition, config.Host, config.Port)
@@ -376,17 +377,11 @@ func (a *App) executeLookup(config LookupConfig) error {
376377
ctx := context.Background()
377378

378379
var current atomic.Value
379-
current.Store("Domains werden überprüft")
380-
formatProgress := func(subdomain, ending string) string {
381-
sub := subdomain
382-
if sub == "" {
383-
sub = "(keine)"
384-
}
385-
return fmt.Sprintf("Subdomain: %s | Endung: %s", sub, ending)
386-
}
380+
current.Store(ping.LookupProgress{})
387381

388-
resultText, err := withSpinner("IP Lookup", func() string {
389-
return current.Load().(string)
382+
resultText, err := withSpinner("IP Lookup", func(frame int) string {
383+
progress := current.Load().(ping.LookupProgress)
384+
return formatLookupProgress(progress, frame)
390385
}, 120*time.Millisecond, func() (string, error) {
391386
result, lookupErr := ping.LookupDomains(ctx, ping.LookupConfig{
392387
Edition: config.Edition,
@@ -395,8 +390,8 @@ func (a *App) executeLookup(config LookupConfig) error {
395390
Subdomains: config.Subdomains,
396391
DomainEndings: config.Endings,
397392
Concurrency: 24,
398-
Progress: func(subdomain, ending string) {
399-
current.Store(formatProgress(subdomain, ending))
393+
Progress: func(progress ping.LookupProgress) {
394+
current.Store(progress)
400395
},
401396
})
402397
if lookupErr != nil {
@@ -412,6 +407,53 @@ func (a *App) executeLookup(config LookupConfig) error {
412407
return nil
413408
}
414409

410+
func formatLookupProgress(progress ping.LookupProgress, frame int) string {
411+
if progress.Total == 0 {
412+
return "Domains werden überprüft"
413+
}
414+
subdomain := progress.Subdomain
415+
if subdomain == "" {
416+
subdomain = "(keine)"
417+
}
418+
bar := buildProgressBar(progress.Completed, progress.Total, frame, 20)
419+
percent := (float64(progress.Completed) / float64(progress.Total)) * 100
420+
return fmt.Sprintf(
421+
"%s %d/%d (%.0f%%) | Subdomain: %s | Endung: %s | Host: %s",
422+
bar,
423+
progress.Completed,
424+
progress.Total,
425+
percent,
426+
subdomain,
427+
progress.Ending,
428+
progress.Host,
429+
)
430+
}
431+
432+
func buildProgressBar(completed, total, frame, width int) string {
433+
if total <= 0 || width <= 0 {
434+
return "[--------------------]"
435+
}
436+
if completed > total {
437+
completed = total
438+
}
439+
filled := (completed * width) / total
440+
if filled > width {
441+
filled = width
442+
}
443+
bar := make([]rune, width)
444+
for i := 0; i < width; i++ {
445+
bar[i] = '░'
446+
}
447+
for i := 0; i < filled; i++ {
448+
bar[i] = '█'
449+
}
450+
if completed < total && filled < width {
451+
animation := []rune{'▏', '▎', '▍', '▌', '▋', '▊', '▉'}
452+
bar[filled] = animation[frame%len(animation)]
453+
}
454+
return fmt.Sprintf("[%s]", string(bar))
455+
}
456+
415457
func (a *App) askAgain() (bool, error) {
416458
index, err := selectOption("Nächster Schritt", []string{"Neue Abfrage", "Beenden"})
417459
if err != nil {

internal/cli/ui.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func renderSpinnerPage(title, message, frame string) {
213213
renderPage(title, []string{fmt.Sprintf("%s %s", style(message, colorDim), style(frame, colorAccent))})
214214
}
215215

216-
func withSpinner(title string, message func() string, tick time.Duration, action func() (string, error)) (string, error) {
216+
func withSpinner(title string, message func(frame int) string, tick time.Duration, action func() (string, error)) (string, error) {
217217
resultCh := make(chan struct {
218218
result string
219219
err error
@@ -245,7 +245,7 @@ func withSpinner(title string, message func() string, tick time.Duration, action
245245
fmt.Println()
246246
return res.result, res.err
247247
case <-ticker.C:
248-
line := fmt.Sprintf("%s %s", style(message(), colorDim), style(frames[frame], colorAccent))
248+
line := fmt.Sprintf("%s %s", style(message(frame), colorDim), style(frames[frame], colorAccent))
249249
if lastLen > 0 && len(line) < lastLen {
250250
line += strings.Repeat(" ", lastLen-len(line))
251251
}

internal/ping/lookup.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type LookupConfig struct {
1515
Subdomains []string
1616
DomainEndings []string
1717
Concurrency int
18-
Progress func(subdomain, ending string)
18+
Progress func(progress LookupProgress)
1919
}
2020

2121
type LookupMatch struct {
@@ -29,6 +29,15 @@ type LookupResult struct {
2929
Completed int
3030
}
3131

32+
type LookupProgress struct {
33+
Subdomain string
34+
Ending string
35+
Host string
36+
Attempt int
37+
Total int
38+
Completed int
39+
}
40+
3241
func LookupDomains(ctx context.Context, config LookupConfig) (LookupResult, error) {
3342
baseHost := strings.TrimSpace(config.BaseHost)
3443
if baseHost == "" {
@@ -53,29 +62,46 @@ func LookupDomains(ctx context.Context, config LookupConfig) (LookupResult, erro
5362
return LookupResult{}, fmt.Errorf("keine kombinationen verfügbar")
5463
}
5564

56-
candidates := make(chan string, concurrency)
65+
type lookupCandidate struct {
66+
subdomain string
67+
ending string
68+
host string
69+
attempt int
70+
}
71+
72+
candidates := make(chan lookupCandidate, concurrency)
5773
results := make(chan LookupMatch, concurrency)
5874
var completed int64
5975

6076
var wg sync.WaitGroup
6177
worker := func() {
6278
defer wg.Done()
63-
for host := range candidates {
79+
for candidate := range candidates {
6480
select {
6581
case <-ctx.Done():
6682
return
6783
default:
6884
}
6985

70-
res, err := Execute(ctx, config.Edition, host, config.Port)
71-
atomic.AddInt64(&completed, 1)
86+
res, err := Execute(ctx, config.Edition, candidate.host, config.Port)
87+
currentCompleted := int(atomic.AddInt64(&completed, 1))
88+
if config.Progress != nil {
89+
config.Progress(LookupProgress{
90+
Subdomain: candidate.subdomain,
91+
Ending: candidate.ending,
92+
Host: candidate.host,
93+
Attempt: candidate.attempt,
94+
Total: total,
95+
Completed: currentCompleted,
96+
})
97+
}
7298
if err != nil {
7399
continue
74100
}
75101
select {
76102
case <-ctx.Done():
77103
return
78-
case results <- LookupMatch{Host: host, Result: res}:
104+
case results <- LookupMatch{Host: candidate.host, Result: res}:
79105
}
80106
}
81107
}
@@ -87,15 +113,20 @@ func LookupDomains(ctx context.Context, config LookupConfig) (LookupResult, erro
87113

88114
go func() {
89115
defer close(candidates)
116+
attempt := 0
90117
for _, sub := range subdomains {
91118
for _, ending := range endings {
92-
if config.Progress != nil {
93-
config.Progress(sub, ending)
94-
}
119+
attempt++
120+
host := buildHost(sub, baseHost, ending)
95121
select {
96122
case <-ctx.Done():
97123
return
98-
case candidates <- buildHost(sub, baseHost, ending):
124+
case candidates <- lookupCandidate{
125+
subdomain: sub,
126+
ending: ending,
127+
host: host,
128+
attempt: attempt,
129+
}:
99130
}
100131
}
101132
}

0 commit comments

Comments
 (0)